generated from coulomb/repo-seed
feat: add zone layout selector
This commit is contained in:
@@ -219,6 +219,11 @@ Container state belongs in saved or copied graph view state, not in the Fabric
|
||||
payload. It is an operator workspace preference, similar to manual visibility
|
||||
overrides.
|
||||
|
||||
Zone-local layout is also view state. The first selectable algorithms are a
|
||||
compact grid and a circle layout. Switching between them should rearrange the
|
||||
assigned nodes inside each stable container without moving the container itself
|
||||
or changing the underlying Fabric relationships.
|
||||
|
||||
### Context Edges
|
||||
|
||||
Display-only context edges are not zone connectivity. Repository `declares`
|
||||
|
||||
@@ -210,9 +210,10 @@ zone surface position and size in graph coordinates. Global graph layout may
|
||||
place unzoned nodes and provide an initial center for new zones, but existing
|
||||
zone containers should keep their operator-chosen positions when the layout
|
||||
algorithm changes. After the global layout pass, each zone may project its
|
||||
assigned visible nodes into local coordinates inside its container. The first
|
||||
local layout may be a deterministic compact layout; later engines can replace
|
||||
that with per-zone Cytoscape or engine-owned algorithms.
|
||||
assigned visible nodes into local coordinates inside its container. The current
|
||||
local layout choices are compact grid and circle. The selected zone-local
|
||||
layout algorithm belongs in the nested `zone.layout.algorithm` view state so it
|
||||
can be restored by saved or copied views without changing the Fabric payload.
|
||||
|
||||
Zone collapse is a view-only operation. A collapsed zone should hide its visible
|
||||
member nodes, replace them with a synthetic zone node, and draw synthetic
|
||||
|
||||
@@ -455,6 +455,12 @@ def graph_explorer_page() -> str:
|
||||
<option value="accessZone">Access Zone</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="map-control-field"><span class="field-label">Zone Layout <button type="button" class="help-tip" aria-label="Zone layout help" data-help-title="Zone Layout" data-help="Zone layout changes how each zone arranges its assigned visible nodes inside the stable zone rectangle. It does not change the underlying Fabric graph.">?</button></span>
|
||||
<select id="zone-layout-select" title="Choose zone-local layout algorithm">
|
||||
<option value="compact-grid">Grid</option>
|
||||
<option value="circle">Circle</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>
|
||||
@@ -570,6 +576,7 @@ def graph_explorer_page() -> str:
|
||||
const labelSelect = document.getElementById("label-select");
|
||||
const zoneBoundaryToggle = document.getElementById("zone-boundary-toggle");
|
||||
const zoneGroupSelect = document.getElementById("zone-group-select");
|
||||
const zoneLayoutSelect = document.getElementById("zone-layout-select");
|
||||
const nodeTypeFilter = document.getElementById("node-type-filter");
|
||||
const nodeTypeSummary = document.getElementById("node-type-summary");
|
||||
const edgeTypeFilter = document.getElementById("edge-type-filter");
|
||||
@@ -610,6 +617,7 @@ def graph_explorer_page() -> str:
|
||||
let activeLabelMode = "auto";
|
||||
let activeZoneGrouping = "deploymentEnvironment";
|
||||
let activeZoneDefinitionSet = "fabric-default";
|
||||
let activeZoneLayoutAlgorithm = "compact-grid";
|
||||
let profilePersistence = "none";
|
||||
let profiles = [];
|
||||
let currentProfileId = "";
|
||||
@@ -1062,6 +1070,11 @@ def graph_explorer_page() -> str:
|
||||
},
|
||||
};
|
||||
|
||||
const zoneLayoutAlgorithms = new Set(["compact-grid", "circle"]);
|
||||
|
||||
const normalizeZoneLayoutAlgorithm = (value) =>
|
||||
zoneLayoutAlgorithms.has(String(value || "")) ? String(value) : "compact-grid";
|
||||
|
||||
const zoneElementData = (element) => {
|
||||
if (!element) return {};
|
||||
if (typeof element.data === "function") return element.data();
|
||||
@@ -1495,7 +1508,7 @@ def graph_explorer_page() -> str:
|
||||
.slice()
|
||||
.sort((left, right) => zoneNodeSortKey(left).localeCompare(zoneNodeSortKey(right)));
|
||||
if (!nodes.length || !container) return;
|
||||
const algorithm = String(zone.layout?.algorithm || "compact-grid");
|
||||
const algorithm = normalizeZoneLayoutAlgorithm(activeZoneLayoutAlgorithm || zone.layout?.algorithm);
|
||||
if (algorithm === "circle") {
|
||||
layoutZoneNodesInCircle(nodes, container);
|
||||
return;
|
||||
@@ -2031,6 +2044,9 @@ def graph_explorer_page() -> str:
|
||||
const currentZoneViewState = () => {
|
||||
const visible = zoneBoundaryToggle ? zoneBoundaryToggle.checked : true;
|
||||
const grouping = zoneGroupSelect ? zoneGroupSelect.value || "deploymentEnvironment" : "deploymentEnvironment";
|
||||
const layoutAlgorithm = normalizeZoneLayoutAlgorithm(
|
||||
zoneLayoutSelect ? zoneLayoutSelect.value : activeZoneLayoutAlgorithm
|
||||
);
|
||||
const definitionSet = zoneDefinitionSets[activeZoneDefinitionSet]
|
||||
? activeZoneDefinitionSet
|
||||
: "fabric-default";
|
||||
@@ -2038,6 +2054,9 @@ def graph_explorer_page() -> str:
|
||||
visible,
|
||||
grouping,
|
||||
definitionSet,
|
||||
layout: {
|
||||
algorithm: layoutAlgorithm,
|
||||
},
|
||||
presentation: {
|
||||
boundaries: visible,
|
||||
labels: true,
|
||||
@@ -2061,6 +2080,7 @@ def graph_explorer_page() -> str:
|
||||
zoneBoundaries: zone.visible,
|
||||
zoneGrouping: zone.grouping,
|
||||
zoneDefinitionSet: zone.definitionSet,
|
||||
zoneLayoutAlgorithm: zone.layout.algorithm,
|
||||
rules: filterRules.map((rule) => ({...rule})),
|
||||
manualOverrides: {...manualOverrides},
|
||||
};
|
||||
@@ -2093,6 +2113,7 @@ def graph_explorer_page() -> str:
|
||||
if (params.has("zoneBoundaries")) state.zoneBoundaries = params.get("zoneBoundaries") !== "0";
|
||||
if (params.has("zoneGrouping")) state.zoneGrouping = params.get("zoneGrouping") || "";
|
||||
if (params.has("zoneDefinitionSet")) state.zoneDefinitionSet = params.get("zoneDefinitionSet") || "";
|
||||
if (params.has("zoneLayout")) state.zoneLayoutAlgorithm = params.get("zoneLayout") || "";
|
||||
if (params.has("profile")) state.profile = params.get("profile") || "";
|
||||
if (params.has("state")) {
|
||||
try {
|
||||
@@ -2118,6 +2139,7 @@ def graph_explorer_page() -> str:
|
||||
if (state.zoneBoundaries === false) params.set("zoneBoundaries", "0");
|
||||
if (state.zoneGrouping && state.zoneGrouping !== "deploymentEnvironment") params.set("zoneGrouping", state.zoneGrouping);
|
||||
if (state.zoneDefinitionSet && state.zoneDefinitionSet !== "fabric-default") params.set("zoneDefinitionSet", state.zoneDefinitionSet);
|
||||
if (state.zoneLayoutAlgorithm && state.zoneLayoutAlgorithm !== "compact-grid") params.set("zoneLayout", state.zoneLayoutAlgorithm);
|
||||
if (currentProfileId) params.set("profile", currentProfileId);
|
||||
if (includeStateBlob || hasManualOverrides() || filterRules.length > 0) {
|
||||
params.set("state", encodeStateBlob(state));
|
||||
@@ -2142,6 +2164,7 @@ def graph_explorer_page() -> str:
|
||||
const containers = nested.containers && typeof nested.containers === "object"
|
||||
? nested.containers
|
||||
: state.zoneContainers;
|
||||
const layout = nested.layout && typeof nested.layout === "object" ? nested.layout : {};
|
||||
const grouping = nested.grouping || state.zoneGrouping || "deploymentEnvironment";
|
||||
const definitionSet = zoneDefinitionSets[nested.definitionSet || state.zoneDefinitionSet]
|
||||
? nested.definitionSet || state.zoneDefinitionSet
|
||||
@@ -2155,6 +2178,9 @@ def graph_explorer_page() -> str:
|
||||
visible,
|
||||
grouping: zoneGroupSelect && optionExists(zoneGroupSelect, grouping) ? grouping : "deploymentEnvironment",
|
||||
definitionSet,
|
||||
layout: {
|
||||
algorithm: normalizeZoneLayoutAlgorithm(layout.algorithm || state.zoneLayoutAlgorithm),
|
||||
},
|
||||
presentation: {
|
||||
boundaries: "boundaries" in presentation ? presentation.boundaries !== false : visible,
|
||||
labels: "labels" in presentation ? presentation.labels !== false : true,
|
||||
@@ -2173,11 +2199,13 @@ 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 ("zone" in state || "zoneBoundaries" in state || "zoneGrouping" in state || "zoneDefinitionSet" in state) {
|
||||
if ("zone" in state || "zoneBoundaries" in state || "zoneGrouping" in state || "zoneDefinitionSet" in state || "zoneLayoutAlgorithm" in state) {
|
||||
const zone = normalizeZoneViewState(state);
|
||||
activeZoneDefinitionSet = zone.definitionSet;
|
||||
if (zoneBoundaryToggle) zoneBoundaryToggle.checked = zone.visible;
|
||||
if (zoneGroupSelect) zoneGroupSelect.value = zone.grouping;
|
||||
activeZoneLayoutAlgorithm = zone.layout.algorithm;
|
||||
if (zoneLayoutSelect) zoneLayoutSelect.value = activeZoneLayoutAlgorithm;
|
||||
zoneContainerState = zone.containers;
|
||||
}
|
||||
if ("manualOverrides" in state && state.manualOverrides && typeof state.manualOverrides === "object") {
|
||||
@@ -2192,6 +2220,9 @@ def graph_explorer_page() -> str:
|
||||
activeMode = modeSelect.value || "full";
|
||||
activeLabelMode = labelSelect.value || "auto";
|
||||
activeZoneGrouping = zoneGroupSelect ? zoneGroupSelect.value || "deploymentEnvironment" : "deploymentEnvironment";
|
||||
activeZoneLayoutAlgorithm = normalizeZoneLayoutAlgorithm(
|
||||
zoneLayoutSelect ? zoneLayoutSelect.value : activeZoneLayoutAlgorithm
|
||||
);
|
||||
selectedZoneId = "";
|
||||
collapsedZoneSnapshots = new Map();
|
||||
focusSet = null;
|
||||
@@ -2233,6 +2264,9 @@ def graph_explorer_page() -> str:
|
||||
if (zoneGroupSelect && (zoneGroupSelect.value || "deploymentEnvironment") !== "deploymentEnvironment") {
|
||||
parts.push("access zone boundaries");
|
||||
}
|
||||
if (zoneLayoutSelect && (zoneLayoutSelect.value || "compact-grid") !== "compact-grid") {
|
||||
parts.push(`${zoneLayoutSelect.options[zoneLayoutSelect.selectedIndex]?.textContent || zoneLayoutSelect.value} zone layout`);
|
||||
}
|
||||
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"}`);
|
||||
@@ -2968,6 +3002,18 @@ def graph_explorer_page() -> str:
|
||||
updateProfileSummary();
|
||||
updateUrlState();
|
||||
});
|
||||
zoneLayoutSelect.addEventListener("input", () => {
|
||||
activeZoneLayoutAlgorithm = normalizeZoneLayoutAlgorithm(zoneLayoutSelect.value);
|
||||
currentProfileId = "";
|
||||
profileSelect.value = "";
|
||||
applyZoneContainerLayout();
|
||||
fitVisibleGraph();
|
||||
renderZoneOverlay();
|
||||
if (!selected) renderMapOverview();
|
||||
updateProfileControls();
|
||||
updateProfileSummary();
|
||||
updateUrlState();
|
||||
});
|
||||
zoneOverlay.addEventListener("click", (event) => {
|
||||
if (suppressZoneClick) {
|
||||
suppressZoneClick = false;
|
||||
@@ -3089,6 +3135,8 @@ def graph_explorer_page() -> str:
|
||||
zoneBoundaryToggle.checked = true;
|
||||
zoneGroupSelect.value = "deploymentEnvironment";
|
||||
activeZoneGrouping = "deploymentEnvironment";
|
||||
zoneLayoutSelect.value = "compact-grid";
|
||||
activeZoneLayoutAlgorithm = "compact-grid";
|
||||
activeZoneDefinitionSet = "fabric-default";
|
||||
selectedZoneId = "";
|
||||
collapsedZoneSnapshots = new Map();
|
||||
|
||||
@@ -402,6 +402,7 @@ def test_registry_serves_graph_explorer_exports(tmp_path: Path) -> None:
|
||||
assert 'id="zone-overlay"' in page
|
||||
assert 'id="zone-boundary-toggle"' in page
|
||||
assert 'id="zone-group-select"' in page
|
||||
assert 'id="zone-layout-select"' in page
|
||||
assert 'id="node-type-filter"' in page
|
||||
assert 'id="edge-type-filter"' in page
|
||||
assert 'id="rule-panel"' in page
|
||||
@@ -429,6 +430,13 @@ def test_registry_serves_graph_explorer_exports(tmp_path: Path) -> None:
|
||||
assert "zoneDefinitionSet" in page
|
||||
assert "zoneDefinitionSets" in page
|
||||
assert "fabric-default" in page
|
||||
assert "activeZoneLayoutAlgorithm" in page
|
||||
assert "zoneLayoutAlgorithms" in page
|
||||
assert "normalizeZoneLayoutAlgorithm" in page
|
||||
assert "zoneLayoutAlgorithm" in page
|
||||
assert "zoneLayout" in page
|
||||
assert "compact-grid" in page
|
||||
assert "circle" in page
|
||||
assert "zoneContainerState" in page
|
||||
assert "ensureZoneContainer" in page
|
||||
assert "packZoneContainers" in page
|
||||
|
||||
44
workplans/ADHOC-2026-05-25.md
Normal file
44
workplans/ADHOC-2026-05-25.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
id: ADHOC-2026-05-25
|
||||
type: workplan
|
||||
title: "Ad Hoc Fixes 2026-05-25"
|
||||
domain: railiance
|
||||
repo: railiance-fabric
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
created: "2026-05-25"
|
||||
updated: "2026-05-25"
|
||||
---
|
||||
|
||||
# ADHOC-2026-05-25 - Ad Hoc Fixes
|
||||
|
||||
## Add Zone Layout Algorithm Control
|
||||
|
||||
```task
|
||||
id: ADHOC-2026-05-25-T01
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
The graph explorer now lays zone subgraphs out as a grid inside each zone
|
||||
container. Add an operator-facing control that can switch the zone-local layout
|
||||
algorithm while keeping stable zone containers intact.
|
||||
|
||||
Expected result: the map controls expose a zone layout selector, the selected
|
||||
algorithm applies to each zone subgraph, and the setting persists in saved or
|
||||
copied view state.
|
||||
|
||||
Result: Added a `Zone Layout` selector with Grid and Circle algorithms. The
|
||||
selected algorithm is stored in nested zone view state, reflected by the
|
||||
`zoneLayout` URL alias for non-default layout, and reapplies zone-local node
|
||||
placement without moving stable zone containers.
|
||||
|
||||
Verification:
|
||||
|
||||
- `python3 -m pytest tests/test_graph_explorer.py tests/test_zone_view.py -q`
|
||||
passed.
|
||||
- Generated graph explorer JavaScript passed `node --check`.
|
||||
- `python3 -m railiance_fabric.cli validate .` passed.
|
||||
- `python3 -m pytest -q` passed with 72 tests.
|
||||
- Headless Edge screenshots confirmed Grid and Circle zone layouts render.
|
||||
Reference in New Issue
Block a user