generated from coulomb/repo-seed
feat: render graph explorer zone boundaries
This commit is contained in:
@@ -36,7 +36,50 @@ def graph_explorer_page() -> str:
|
||||
}
|
||||
.main { display: grid; grid-template-columns: minmax(0, 1fr) 340px; min-height: 0; }
|
||||
.canvas-wrap { position: relative; min-width: 0; min-height: 0; }
|
||||
#graph-canvas { position: absolute; inset: 0; }
|
||||
#graph-canvas { position: absolute; inset: 0; z-index: 0; }
|
||||
.zone-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
.zone-boundary {
|
||||
position: absolute;
|
||||
border: 2px solid var(--zone-color, #2563eb);
|
||||
border-radius: 8px;
|
||||
background: var(--zone-fill, rgba(37, 99, 235, .06));
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .74), 0 10px 32px rgba(23, 32, 51, .08);
|
||||
min-height: 56px;
|
||||
min-width: 90px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.zone-boundary.selected {
|
||||
border-width: 3px;
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .82), 0 14px 36px rgba(23, 32, 51, .14);
|
||||
}
|
||||
.zone-label {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 10px;
|
||||
z-index: 1;
|
||||
min-height: 26px;
|
||||
max-width: calc(100% - 20px);
|
||||
border-color: var(--zone-color, #2563eb);
|
||||
color: var(--zone-color, #2563eb);
|
||||
background: rgba(255, 255, 255, .96);
|
||||
box-shadow: 0 8px 18px rgba(23, 32, 51, .1);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
padding: 3px 8px;
|
||||
pointer-events: auto;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.zone-label:focus-visible {
|
||||
outline: 2px solid var(--zone-color, #2563eb);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.side {
|
||||
border-left: 1px solid var(--line);
|
||||
background: var(--panel);
|
||||
@@ -181,6 +224,14 @@ def graph_explorer_page() -> str:
|
||||
min-width: 84px;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
.map-control-field input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
min-height: 0;
|
||||
margin: 5px 0;
|
||||
padding: 0;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
.selection-anchor {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
@@ -390,7 +441,17 @@ def graph_explorer_page() -> str:
|
||||
<main class="main">
|
||||
<div class="canvas-wrap">
|
||||
<div id="graph-canvas" role="img" aria-label="Interactive Fabric graph" aria-live="polite"><p class="canvas-notice">Loading graph...</p></div>
|
||||
<div id="zone-overlay" class="zone-overlay" aria-label="Deployment zone boundaries"></div>
|
||||
<div class="map-controls" aria-label="Map navigation controls">
|
||||
<label class="map-control-field"><span class="field-label">Zones <button type="button" class="help-tip" aria-label="Zone boundary help" data-help-title="Zones" data-help="Zone boundaries draw labeled rectangles around visible nodes that share deployment overlay fields. They are visual annotations only.">?</button></span>
|
||||
<input id="zone-boundary-toggle" type="checkbox" aria-label="Show zone boundaries" checked>
|
||||
</label>
|
||||
<label class="map-control-field"><span class="field-label">Zone By <button type="button" class="help-tip" aria-label="Zone grouping help" data-help-title="Zone By" data-help="Environment groups nodes as dev-tegwick, test, or prod. Access Zone groups by intended reachability such as private-dev or production-public.">?</button></span>
|
||||
<select id="zone-group-select" title="Choose zone boundary grouping">
|
||||
<option value="deploymentEnvironment">Environment</option>
|
||||
<option value="accessZone">Access Zone</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="map-control-field"><span class="field-label">Labels <button type="button" class="help-tip" aria-label="Labels help" data-help-title="Labels" data-help="Label density changes only text visibility. Auto hides low-priority labels when the map is dense; Key keeps repositories, services, deployments, servers, and issue markers visible.">?</button></span>
|
||||
<select id="label-select" title="Control node label density">
|
||||
<option value="auto">Auto</option>
|
||||
@@ -495,6 +556,7 @@ def graph_explorer_page() -> str:
|
||||
const manifestUrl = "/exports/graph-explorer/manifest";
|
||||
const graphUrl = "/exports/graph-explorer";
|
||||
const canvas = document.getElementById("graph-canvas");
|
||||
const zoneOverlay = document.getElementById("zone-overlay");
|
||||
const popup = document.getElementById("popup");
|
||||
const helpPopup = document.getElementById("help-popup");
|
||||
const selectionAnchor = document.getElementById("selection-anchor");
|
||||
@@ -503,6 +565,8 @@ def graph_explorer_page() -> str:
|
||||
const modeSelect = document.getElementById("mode-select");
|
||||
const layoutSelect = document.getElementById("layout-select");
|
||||
const labelSelect = document.getElementById("label-select");
|
||||
const zoneBoundaryToggle = document.getElementById("zone-boundary-toggle");
|
||||
const zoneGroupSelect = document.getElementById("zone-group-select");
|
||||
const nodeTypeFilter = document.getElementById("node-type-filter");
|
||||
const nodeTypeSummary = document.getElementById("node-type-summary");
|
||||
const edgeTypeFilter = document.getElementById("edge-type-filter");
|
||||
@@ -541,12 +605,16 @@ def graph_explorer_page() -> str:
|
||||
let allEdgeTypes = [];
|
||||
let activeMode = "full";
|
||||
let activeLabelMode = "auto";
|
||||
let activeZoneGrouping = "deploymentEnvironment";
|
||||
let profilePersistence = "none";
|
||||
let profiles = [];
|
||||
let currentProfileId = "";
|
||||
let filterRules = [];
|
||||
let editingRuleId = "";
|
||||
let orientationContext = null;
|
||||
let selectedZoneId = "";
|
||||
let zoneOverlayFrame = 0;
|
||||
let zoneSummaries = new Map();
|
||||
|
||||
const escapeHtml = (value) => String(value ?? "")
|
||||
.replaceAll("&", "&").replaceAll("<", "<")
|
||||
@@ -903,6 +971,255 @@ def graph_explorer_page() -> str:
|
||||
return warnings;
|
||||
};
|
||||
|
||||
const zonePalette = {
|
||||
"deploymentEnvironment:dev-tegwick": {color: "#0f766e", fill: "rgba(15, 118, 110, .07)"},
|
||||
"deploymentEnvironment:test": {color: "#2563eb", fill: "rgba(37, 99, 235, .07)"},
|
||||
"deploymentEnvironment:prod": {color: "#be123c", fill: "rgba(190, 18, 60, .07)"},
|
||||
};
|
||||
|
||||
const fallbackZonePalette = [
|
||||
{color: "#7c3aed", fill: "rgba(124, 58, 237, .07)"},
|
||||
{color: "#b45309", fill: "rgba(180, 83, 9, .08)"},
|
||||
{color: "#0891b2", fill: "rgba(8, 145, 178, .08)"},
|
||||
{color: "#4f46e5", fill: "rgba(79, 70, 229, .07)"},
|
||||
];
|
||||
|
||||
const hashString = (value) => String(value || "")
|
||||
.split("")
|
||||
.reduce((total, character) => (total * 31 + character.charCodeAt(0)) >>> 0, 0);
|
||||
|
||||
const zoneStyle = (zone) =>
|
||||
zonePalette[zone.id] || fallbackZonePalette[hashString(zone.id) % fallbackZonePalette.length];
|
||||
|
||||
const zoneRank = (zone) => {
|
||||
if (zone.id === "deploymentEnvironment:dev-tegwick") return 0;
|
||||
if (zone.id === "deploymentEnvironment:test") return 1;
|
||||
if (zone.id === "deploymentEnvironment:prod") return 2;
|
||||
return 10 + hashString(zone.id) % 20;
|
||||
};
|
||||
|
||||
const zoneLabelRank = (zone, index) => {
|
||||
if (zone.id === "deploymentEnvironment:test") return 0;
|
||||
if (zone.id === "deploymentEnvironment:prod") return 1;
|
||||
if (zone.id === "deploymentEnvironment:dev-tegwick") return 2;
|
||||
return index % 4;
|
||||
};
|
||||
|
||||
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)));
|
||||
};
|
||||
|
||||
const zoneForData = (data, grouping = activeZoneGrouping) => {
|
||||
if (grouping === "deploymentEnvironment") {
|
||||
const value = String(data.deploymentEnvironment || "").trim();
|
||||
if (!value) return null;
|
||||
const normalized = value.toLowerCase();
|
||||
if (normalized === "all") return null;
|
||||
const label = normalized === "dev"
|
||||
? "dev-tegwick"
|
||||
: normalized === "test" || normalized === "staging"
|
||||
? "test"
|
||||
: normalized === "prod"
|
||||
? normalized
|
||||
: value;
|
||||
return {
|
||||
field: "deploymentEnvironment",
|
||||
id: `deploymentEnvironment:${label}`,
|
||||
label,
|
||||
value,
|
||||
};
|
||||
}
|
||||
if (grouping === "accessZone") {
|
||||
const value = String(data.accessZone || "").trim();
|
||||
if (!value) return null;
|
||||
return {
|
||||
field: "accessZone",
|
||||
id: `accessZone:${value}`,
|
||||
label: value,
|
||||
value,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderedNodeBox = (node) => {
|
||||
try {
|
||||
const box = node.renderedBoundingBox({includeLabels: true, includeOverlays: false});
|
||||
if ([box.x1, box.y1, box.x2, box.y2].every(Number.isFinite)) return box;
|
||||
} catch {
|
||||
// Fall back to the rendered center point below.
|
||||
}
|
||||
const position = node.renderedPosition();
|
||||
const radius = (Number(node.data("visualSize")) || 44) / 2;
|
||||
return {
|
||||
x1: position.x - radius,
|
||||
y1: position.y - radius,
|
||||
x2: position.x + radius,
|
||||
y2: position.y + radius,
|
||||
};
|
||||
};
|
||||
|
||||
const zoneBoundsForNodes = (nodes) => {
|
||||
if (!nodes.length) return null;
|
||||
const bounds = nodes.reduce((box, node) => {
|
||||
const nodeBox = renderedNodeBox(node);
|
||||
return {
|
||||
x1: Math.min(box.x1, nodeBox.x1),
|
||||
y1: Math.min(box.y1, nodeBox.y1),
|
||||
x2: Math.max(box.x2, nodeBox.x2),
|
||||
y2: Math.max(box.y2, nodeBox.y2),
|
||||
};
|
||||
}, {x1: Infinity, y1: Infinity, x2: -Infinity, y2: -Infinity});
|
||||
if (![bounds.x1, bounds.y1, bounds.x2, bounds.y2].every(Number.isFinite)) return null;
|
||||
const padding = 34;
|
||||
let left = bounds.x1 - padding;
|
||||
let top = bounds.y1 - padding;
|
||||
let width = bounds.x2 - bounds.x1 + padding * 2;
|
||||
let height = bounds.y2 - bounds.y1 + padding * 2;
|
||||
const minWidth = 132;
|
||||
const minHeight = 84;
|
||||
if (width < minWidth) {
|
||||
left -= (minWidth - width) / 2;
|
||||
width = minWidth;
|
||||
}
|
||||
if (height < minHeight) {
|
||||
top -= (minHeight - height) / 2;
|
||||
height = minHeight;
|
||||
}
|
||||
return {left, top, width, height};
|
||||
};
|
||||
|
||||
const collectZoneSummaries = () => {
|
||||
const groups = new Map();
|
||||
if (!cy) return groups;
|
||||
cy.elements().filter((element) => element.style("display") !== "none").forEach((element) => {
|
||||
const zone = zoneForData(element.data());
|
||||
if (!zone) return;
|
||||
if (!groups.has(zone.id)) {
|
||||
groups.set(zone.id, {...zone, nodes: [], elements: []});
|
||||
}
|
||||
const group = groups.get(zone.id);
|
||||
group.elements.push(element);
|
||||
if (element.isNode()) group.nodes.push(element);
|
||||
});
|
||||
return groups;
|
||||
};
|
||||
|
||||
const zoneCountsForField = (elements, field) => {
|
||||
if (field !== "deploymentEnvironment" && field !== "accessZone") return groupCounts(elements, field);
|
||||
const groups = new Map();
|
||||
elements.forEach((element) => {
|
||||
const zone = zoneForData(element.data(), field);
|
||||
if (!zone) return;
|
||||
groups.set(zone.label, (groups.get(zone.label) || 0) + 1);
|
||||
});
|
||||
return Array.from(groups.entries())
|
||||
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
|
||||
};
|
||||
|
||||
const renderZoneDetails = (zone) => {
|
||||
selected = null;
|
||||
selectedZoneId = zone.id;
|
||||
const visibleElements = zone.elements || [];
|
||||
const visibleNodes = zone.nodes || [];
|
||||
detailTitle.textContent = zone.label;
|
||||
detailSummary.textContent = `${visibleNodes.length} visible node${visibleNodes.length === 1 ? "" : "s"} in ${ruleAttributeLabels[zone.field] || humanize(zone.field)}`;
|
||||
detailPills.innerHTML = [zone.field, zone.value, activeZoneGrouping]
|
||||
.map((value) => value ? `<span class="pill">${escapeHtml(value)}</span>` : "")
|
||||
.join("");
|
||||
|
||||
const valuesFor = (field) => groupCounts(visibleElements, field)
|
||||
.map(([value, count]) => `${value} (${count})`)
|
||||
.join(", ");
|
||||
const warnings = visibleElements
|
||||
.flatMap((element) => zoneWarningsForData(element.data()).map((warning) =>
|
||||
`${elementLabel(element)}: ${warning}`
|
||||
));
|
||||
const rows = [
|
||||
["visible nodes", String(visibleNodes.length)],
|
||||
["deployment environments", valuesFor("deploymentEnvironment")],
|
||||
["deployment scenarios", valuesFor("deploymentScenario")],
|
||||
["access zones", valuesFor("accessZone")],
|
||||
["routing authorities", valuesFor("routingAuthority")],
|
||||
["policy authorities", valuesFor("policyAuthority")],
|
||||
...warnings.slice(0, 8).map((warning) => ["warning", warning]),
|
||||
];
|
||||
if (warnings.length > 8) rows.push(["warning", `${warnings.length - 8} additional route warnings`]);
|
||||
detailList.innerHTML = rows
|
||||
.filter(([, value]) => value)
|
||||
.map(([key, value]) => `<li class="${key === "warning" ? "orientation-warning" : ""}"><strong>${escapeHtml(key)}</strong> ${escapeHtml(value)}</li>`)
|
||||
.join("");
|
||||
|
||||
const contextIds = new Set(visibleNodes.map((node) => node.id()));
|
||||
visibleElements.filter((element) => element.isEdge()).forEach((edge) => addContextEdge(contextIds, edge));
|
||||
orientationContext = {
|
||||
ids: Array.from(contextIds),
|
||||
profileName: `Zone: ${zone.label}`,
|
||||
};
|
||||
orientationTitle.textContent = `Zone context (${orientationContext.ids.length} items)`;
|
||||
orientationList.innerHTML = [
|
||||
{label: "grouping", value: ruleAttributeLabels[zone.field] || humanize(zone.field), state: "good"},
|
||||
{label: "zone", value: zone.label},
|
||||
{label: "warnings", value: warnings.length ? String(warnings.length) : "none", state: warnings.length ? "warning" : "good"},
|
||||
].map((row) => `
|
||||
<li class="orientation-item ${orientationStateClass(row.state)}">
|
||||
<span class="orientation-label">${escapeHtml(row.label)}</span>
|
||||
<span class="orientation-value">${escapeHtml(row.value)}</span>
|
||||
</li>
|
||||
`).join("");
|
||||
orientationActions.innerHTML = `
|
||||
<button type="button" data-orientation-action="focus">Focus Context</button>
|
||||
<button type="button" data-orientation-action="highlight">Highlight Context</button>
|
||||
<button type="button" data-orientation-action="hide-other">Hide Other</button>
|
||||
<button type="button" data-orientation-action="remove-other">Remove Other</button>
|
||||
<button type="button" data-orientation-action="name-view">Name View</button>
|
||||
`;
|
||||
updateLabelVisibility();
|
||||
updateSelectionAnchor();
|
||||
};
|
||||
|
||||
const renderZoneOverlay = () => {
|
||||
if (!zoneOverlay || !cy) return;
|
||||
const enabled = zoneBoundaryToggle ? zoneBoundaryToggle.checked : true;
|
||||
if (!enabled) {
|
||||
zoneSummaries = new Map();
|
||||
selectedZoneId = "";
|
||||
zoneOverlay.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
zoneSummaries = collectZoneSummaries();
|
||||
const boundaries = Array.from(zoneSummaries.values())
|
||||
.filter((zone) => zone.nodes.length > 0)
|
||||
.sort((left, right) => zoneRank(left) - zoneRank(right) || left.label.localeCompare(right.label))
|
||||
.map((zone, index) => {
|
||||
const bounds = zoneBoundsForNodes(zone.nodes);
|
||||
if (!bounds) return "";
|
||||
const style = zoneStyle(zone);
|
||||
const selectedClass = selectedZoneId === zone.id ? " selected" : "";
|
||||
const labelTop = zoneLabelTop(zone, bounds, index);
|
||||
return `
|
||||
<div class="zone-boundary${selectedClass}" data-zone-id="${escapeHtml(zone.id)}" style="--zone-color:${style.color};--zone-fill:${style.fill};left:${Math.round(bounds.left)}px;top:${Math.round(bounds.top)}px;width:${Math.round(bounds.width)}px;height:${Math.round(bounds.height)}px">
|
||||
<button type="button" class="zone-label" data-zone-id="${escapeHtml(zone.id)}" style="--zone-color:${style.color};top:${labelTop}px" title="${escapeHtml(zone.label)}">${escapeHtml(zone.label)}</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
zoneOverlay.innerHTML = boundaries.join("");
|
||||
if (selectedZoneId && zoneSummaries.has(selectedZoneId)) {
|
||||
renderZoneDetails(zoneSummaries.get(selectedZoneId));
|
||||
} else if (selectedZoneId) {
|
||||
selectedZoneId = "";
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleZoneOverlayUpdate = () => {
|
||||
if (!zoneOverlay || zoneOverlayFrame) return;
|
||||
zoneOverlayFrame = window.requestAnimationFrame(() => {
|
||||
zoneOverlayFrame = 0;
|
||||
renderZoneOverlay();
|
||||
});
|
||||
};
|
||||
|
||||
const groupCounts = (elements, field) => {
|
||||
const counts = new Map();
|
||||
elements.forEach((element) => {
|
||||
@@ -927,7 +1244,7 @@ def graph_explorer_page() -> str:
|
||||
|
||||
const rows = [{label: "visible", value: `${visibleNodes} nodes / ${visibleEdges} edges`}];
|
||||
fields.forEach((field) => {
|
||||
const counts = groupCounts(visibleElements, field);
|
||||
const counts = zoneCountsForField(visibleElements, field);
|
||||
rows.push({
|
||||
label: ruleAttributeLabels[field] || humanize(field),
|
||||
value: counts.length
|
||||
@@ -1032,6 +1349,8 @@ def graph_explorer_page() -> str:
|
||||
edgeTypes: Array.from(selectedEdgeTypes()),
|
||||
review: reviewFilter.value,
|
||||
unresolved: unresolvedFilter.value,
|
||||
zoneBoundaries: zoneBoundaryToggle ? zoneBoundaryToggle.checked : true,
|
||||
zoneGrouping: zoneGroupSelect ? zoneGroupSelect.value || "deploymentEnvironment" : "deploymentEnvironment",
|
||||
rules: filterRules.map((rule) => ({...rule})),
|
||||
manualOverrides: {...manualOverrides},
|
||||
});
|
||||
@@ -1060,6 +1379,8 @@ def graph_explorer_page() -> str:
|
||||
if (params.has("layer")) state.nodeTypes = parseListParam(params.get("layer"));
|
||||
if (params.has("review")) state.review = params.get("review") || "";
|
||||
if (params.has("unresolved")) state.unresolved = params.get("unresolved") || "";
|
||||
if (params.has("zoneBoundaries")) state.zoneBoundaries = params.get("zoneBoundaries") !== "0";
|
||||
if (params.has("zoneGrouping")) state.zoneGrouping = params.get("zoneGrouping") || "";
|
||||
if (params.has("profile")) state.profile = params.get("profile") || "";
|
||||
if (params.has("state")) {
|
||||
try {
|
||||
@@ -1082,6 +1403,8 @@ def graph_explorer_page() -> str:
|
||||
if (state.edgeTypes.length !== allEdgeTypes.length) params.set("edgeTypes", state.edgeTypes.join(","));
|
||||
if (state.review) params.set("review", state.review);
|
||||
if (state.unresolved) params.set("unresolved", state.unresolved);
|
||||
if (state.zoneBoundaries === false) params.set("zoneBoundaries", "0");
|
||||
if (state.zoneGrouping && state.zoneGrouping !== "deploymentEnvironment") params.set("zoneGrouping", state.zoneGrouping);
|
||||
if (currentProfileId) params.set("profile", currentProfileId);
|
||||
if (includeStateBlob || hasManualOverrides() || filterRules.length > 0) {
|
||||
params.set("state", encodeStateBlob(state));
|
||||
@@ -1108,6 +1431,10 @@ def graph_explorer_page() -> str:
|
||||
if ("edgeTypes" in state) setCheckedValues(edgeTypeFilter, (state.edgeTypes || []).filter((value) => allEdgeTypes.includes(value)));
|
||||
if ("review" in state) reviewFilter.value = optionExists(reviewFilter, state.review) ? state.review : "";
|
||||
if ("unresolved" in state) unresolvedFilter.value = optionExists(unresolvedFilter, state.unresolved) ? state.unresolved : "";
|
||||
if ("zoneBoundaries" in state && zoneBoundaryToggle) zoneBoundaryToggle.checked = state.zoneBoundaries !== false;
|
||||
if ("zoneGrouping" in state && zoneGroupSelect) {
|
||||
zoneGroupSelect.value = optionExists(zoneGroupSelect, state.zoneGrouping) ? state.zoneGrouping : "deploymentEnvironment";
|
||||
}
|
||||
if ("manualOverrides" in state && state.manualOverrides && typeof state.manualOverrides === "object") {
|
||||
manualOverrides = {...state.manualOverrides};
|
||||
}
|
||||
@@ -1119,6 +1446,8 @@ def graph_explorer_page() -> str:
|
||||
}
|
||||
activeMode = modeSelect.value || "full";
|
||||
activeLabelMode = labelSelect.value || "auto";
|
||||
activeZoneGrouping = zoneGroupSelect ? zoneGroupSelect.value || "deploymentEnvironment" : "deploymentEnvironment";
|
||||
selectedZoneId = "";
|
||||
focusSet = null;
|
||||
syncFilterSummaries();
|
||||
applyFilters();
|
||||
@@ -1154,6 +1483,10 @@ def graph_explorer_page() -> str:
|
||||
if (selectedEdgeTypes().size !== allEdgeTypes.length) parts.push("edge filter");
|
||||
if (reviewFilter.value) parts.push(`${reviewFilter.value} review`);
|
||||
if (unresolvedFilter.value) parts.push("unresolved only");
|
||||
if (zoneBoundaryToggle && !zoneBoundaryToggle.checked) parts.push("zones off");
|
||||
if (zoneGroupSelect && (zoneGroupSelect.value || "deploymentEnvironment") !== "deploymentEnvironment") {
|
||||
parts.push("access zone boundaries");
|
||||
}
|
||||
if (filterRules.length) parts.push(`${filterRules.length} rule${filterRules.length === 1 ? "" : "s"}`);
|
||||
const overrideCount = Object.keys(manualOverrides).length;
|
||||
if (overrideCount) parts.push(`${overrideCount} override${overrideCount === 1 ? "" : "s"}`);
|
||||
@@ -1327,12 +1660,15 @@ def graph_explorer_page() -> str:
|
||||
hiddenSummary.textContent = removed ? `Hidden ${hidden} / Removed ${removed}` : `Hidden ${hidden}`;
|
||||
updateLabelVisibility();
|
||||
updateSelectionAnchor();
|
||||
scheduleZoneOverlayUpdate();
|
||||
if (!selected) renderMapOverview();
|
||||
if (options.redrawOnRemove && previousRemoved !== ruleRemovalSignature()) runLayout();
|
||||
};
|
||||
|
||||
const showDetails = (element) => {
|
||||
selected = element || null;
|
||||
selectedZoneId = "";
|
||||
scheduleZoneOverlayUpdate();
|
||||
if (!element) {
|
||||
orientationContext = null;
|
||||
renderMapOverview();
|
||||
@@ -1623,6 +1959,7 @@ def graph_explorer_page() -> str:
|
||||
: {name, padding: 48, animate: false};
|
||||
layoutElements.layout(options).run();
|
||||
updateSelectionAnchor();
|
||||
scheduleZoneOverlayUpdate();
|
||||
};
|
||||
|
||||
const renderLegend = (layers) => {
|
||||
@@ -1742,8 +2079,14 @@ def graph_explorer_page() -> str:
|
||||
renderRules();
|
||||
cy.on("tap", "node, edge", (event) => showDetails(event.target));
|
||||
cy.on("tap", (event) => { if (event.target === cy) showDetails(null); });
|
||||
cy.on("pan zoom resize render layoutstop", updateSelectionAnchor);
|
||||
cy.on("position", "node", updateSelectionAnchor);
|
||||
cy.on("pan zoom resize render layoutstop", () => {
|
||||
updateSelectionAnchor();
|
||||
scheduleZoneOverlayUpdate();
|
||||
});
|
||||
cy.on("position", "node", () => {
|
||||
updateSelectionAnchor();
|
||||
scheduleZoneOverlayUpdate();
|
||||
});
|
||||
cy.on("mouseover", "node, edge", (event) => {
|
||||
event.target.addClass("hover");
|
||||
const data = event.target.data();
|
||||
@@ -1811,6 +2154,36 @@ def graph_explorer_page() -> str:
|
||||
updateProfileSummary();
|
||||
updateUrlState();
|
||||
});
|
||||
zoneBoundaryToggle.addEventListener("input", () => {
|
||||
selectedZoneId = "";
|
||||
currentProfileId = "";
|
||||
profileSelect.value = "";
|
||||
renderZoneOverlay();
|
||||
if (!selected) renderMapOverview();
|
||||
updateProfileControls();
|
||||
updateProfileSummary();
|
||||
updateUrlState();
|
||||
});
|
||||
zoneGroupSelect.addEventListener("input", () => {
|
||||
activeZoneGrouping = zoneGroupSelect.value || "deploymentEnvironment";
|
||||
selectedZoneId = "";
|
||||
currentProfileId = "";
|
||||
profileSelect.value = "";
|
||||
renderZoneOverlay();
|
||||
if (!selected) renderMapOverview();
|
||||
updateProfileControls();
|
||||
updateProfileSummary();
|
||||
updateUrlState();
|
||||
});
|
||||
zoneOverlay.addEventListener("click", (event) => {
|
||||
const button = event.target.closest("[data-zone-id]");
|
||||
if (!button) return;
|
||||
const zone = zoneSummaries.get(button.dataset.zoneId);
|
||||
if (!zone) return;
|
||||
event.stopPropagation();
|
||||
selectedZoneId = zone.id;
|
||||
renderZoneOverlay();
|
||||
});
|
||||
ruleTarget.addEventListener("input", () => refreshRuleBuilder({target: ruleTarget.value}));
|
||||
ruleType.addEventListener("input", () => refreshRuleBuilder({target: ruleTarget.value, type: ruleType.value}));
|
||||
ruleAttribute.addEventListener("input", () => refreshRuleBuilder({
|
||||
@@ -1899,6 +2272,10 @@ def graph_explorer_page() -> str:
|
||||
activeMode = "full";
|
||||
labelSelect.value = "auto";
|
||||
activeLabelMode = "auto";
|
||||
zoneBoundaryToggle.checked = true;
|
||||
zoneGroupSelect.value = "deploymentEnvironment";
|
||||
activeZoneGrouping = "deploymentEnvironment";
|
||||
selectedZoneId = "";
|
||||
setCheckedValues(nodeTypeFilter);
|
||||
setCheckedValues(edgeTypeFilter);
|
||||
reviewFilter.value = "";
|
||||
|
||||
Reference in New Issue
Block a user