feat: add draggable graph zones

This commit is contained in:
2026-05-25 01:09:05 +02:00
parent 9b612447ca
commit 0f7b7d1fed
4 changed files with 209 additions and 6 deletions

View File

@@ -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({