generated from coulomb/repo-seed
feat: add draggable graph zones
This commit is contained in:
@@ -65,17 +65,20 @@ def graph_explorer_page() -> str:
|
||||
z-index: 1;
|
||||
min-height: 26px;
|
||||
max-width: calc(100% - 20px);
|
||||
border-color: var(--zone-color, #2563eb);
|
||||
border: 0;
|
||||
color: var(--zone-color, #2563eb);
|
||||
background: rgba(255, 255, 255, .96);
|
||||
box-shadow: 0 8px 18px rgba(23, 32, 51, .1);
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
cursor: grab;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
padding: 3px 8px;
|
||||
padding: 0;
|
||||
pointer-events: auto;
|
||||
text-overflow: ellipsis;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, .78), 0 0 8px rgba(255, 255, 255, .72);
|
||||
}
|
||||
.zone-label.dragging { cursor: grabbing; }
|
||||
.zone-label:focus-visible {
|
||||
outline: 2px solid var(--zone-color, #2563eb);
|
||||
outline-offset: 2px;
|
||||
@@ -616,6 +619,8 @@ def graph_explorer_page() -> str:
|
||||
let selectedZoneId = "";
|
||||
let zoneOverlayFrame = 0;
|
||||
let zoneSummaries = new Map();
|
||||
let zoneDragState = null;
|
||||
let suppressZoneClick = false;
|
||||
let collapsedZoneSnapshots = new Map();
|
||||
const zoneCollapsePrefix = "zone-collapse:";
|
||||
|
||||
@@ -1266,8 +1271,7 @@ def graph_explorer_page() -> str:
|
||||
};
|
||||
|
||||
const zoneLabelTop = (zone, bounds, index) => {
|
||||
const rank = zone.field === "deploymentEnvironment" ? zoneLabelRank(zone, index) : index % 4;
|
||||
return Math.min(8 + rank * 28, Math.max(8, Math.round(bounds.height - 32)));
|
||||
return 8;
|
||||
};
|
||||
|
||||
const zoneForData = (data, grouping = activeZoneGrouping) => {
|
||||
@@ -1593,6 +1597,65 @@ def graph_explorer_page() -> str:
|
||||
}
|
||||
};
|
||||
|
||||
const startZoneDrag = (event, zoneId) => {
|
||||
const zone = zoneSummaries.get(zoneId);
|
||||
if (!cy || !zone || !(zone.nodes || []).length) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
selected = null;
|
||||
selectedZoneId = zone.id;
|
||||
const handle = event.target.closest(".zone-label");
|
||||
handle?.classList.add("dragging");
|
||||
handle?.setPointerCapture?.(event.pointerId);
|
||||
zoneDragState = {
|
||||
zoneId: zone.id,
|
||||
pointerId: event.pointerId,
|
||||
startX: event.clientX,
|
||||
startY: event.clientY,
|
||||
moved: false,
|
||||
handle,
|
||||
nodes: zone.nodes.map((node) => ({
|
||||
node,
|
||||
position: {...node.position()},
|
||||
})),
|
||||
};
|
||||
renderZoneDetails(zone);
|
||||
};
|
||||
|
||||
const moveZoneDrag = (event) => {
|
||||
if (!zoneDragState || event.pointerId !== zoneDragState.pointerId || !cy) return;
|
||||
event.preventDefault();
|
||||
const zoom = cy.zoom() || 1;
|
||||
const dx = (event.clientX - zoneDragState.startX) / zoom;
|
||||
const dy = (event.clientY - zoneDragState.startY) / zoom;
|
||||
if (Math.abs(event.clientX - zoneDragState.startX) + Math.abs(event.clientY - zoneDragState.startY) > 3) {
|
||||
zoneDragState.moved = true;
|
||||
suppressZoneClick = true;
|
||||
}
|
||||
zoneDragState.nodes.forEach(({node, position}) => {
|
||||
if (!node || !node.length) return;
|
||||
node.position({x: position.x + dx, y: position.y + dy});
|
||||
});
|
||||
updateSelectionAnchor();
|
||||
scheduleZoneOverlayUpdate();
|
||||
};
|
||||
|
||||
const finishZoneDrag = (event) => {
|
||||
if (!zoneDragState || event.pointerId !== zoneDragState.pointerId) return;
|
||||
zoneDragState.handle?.classList.remove("dragging");
|
||||
zoneDragState.handle?.releasePointerCapture?.(event.pointerId);
|
||||
const draggedZoneId = zoneDragState.zoneId;
|
||||
const moved = zoneDragState.moved;
|
||||
zoneDragState = null;
|
||||
renderZoneOverlay();
|
||||
if (moved) {
|
||||
const zone = zoneSummaries.get(draggedZoneId);
|
||||
if (zone) renderZoneDetails(zone);
|
||||
updateProfileSummary(`Moved zone "${zone?.label || draggedZoneId}".`);
|
||||
window.setTimeout(() => { suppressZoneClick = false; }, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleZoneOverlayUpdate = () => {
|
||||
if (!zoneOverlay || zoneOverlayFrame) return;
|
||||
zoneOverlayFrame = window.requestAnimationFrame(() => {
|
||||
@@ -2649,6 +2712,10 @@ def graph_explorer_page() -> str:
|
||||
updateUrlState();
|
||||
});
|
||||
zoneOverlay.addEventListener("click", (event) => {
|
||||
if (suppressZoneClick) {
|
||||
suppressZoneClick = false;
|
||||
return;
|
||||
}
|
||||
const button = event.target.closest("[data-zone-id]");
|
||||
if (!button) return;
|
||||
const zone = zoneSummaries.get(button.dataset.zoneId);
|
||||
@@ -2657,6 +2724,14 @@ def graph_explorer_page() -> str:
|
||||
selectedZoneId = zone.id;
|
||||
renderZoneOverlay();
|
||||
});
|
||||
zoneOverlay.addEventListener("pointerdown", (event) => {
|
||||
const handle = event.target.closest(".zone-label[data-zone-id]");
|
||||
if (!handle || event.button !== 0) return;
|
||||
startZoneDrag(event, handle.dataset.zoneId);
|
||||
});
|
||||
window.addEventListener("pointermove", moveZoneDrag);
|
||||
window.addEventListener("pointerup", finishZoneDrag);
|
||||
window.addEventListener("pointercancel", finishZoneDrag);
|
||||
ruleTarget.addEventListener("input", () => refreshRuleBuilder({target: ruleTarget.value}));
|
||||
ruleType.addEventListener("input", () => refreshRuleBuilder({target: ruleTarget.value, type: ruleType.value}));
|
||||
ruleAttribute.addEventListener("input", () => refreshRuleBuilder({
|
||||
|
||||
Reference in New Issue
Block a user