generated from coulomb/repo-seed
feat: persist graph explorer zone state
This commit is contained in:
@@ -188,6 +188,13 @@ multiple zone definitions, and edges crossing zone boundaries. Attraction
|
|||||||
diagnostics such as multiple attraction candidates or depth-limit stops belong
|
diagnostics such as multiple attraction candidates or depth-limit stops belong
|
||||||
to the same resolver diagnostic channel when attraction rules are enabled.
|
to the same resolver diagnostic channel when attraction rules are enabled.
|
||||||
|
|
||||||
|
Saved graph profiles should persist zone view state as an explicit nested
|
||||||
|
`zone` object. The initial fields are `visible`, `grouping`, `definitionSet`,
|
||||||
|
and `presentation`. URL parameters may continue to expose compatibility aliases
|
||||||
|
such as `zoneBoundaries`, `zoneGrouping`, and `zoneDefinitionSet`, but saved
|
||||||
|
profiles should prefer the nested object so future zone definition sets and
|
||||||
|
presentation preferences can be restored without another state migration.
|
||||||
|
|
||||||
## Repo-Scoping Compatibility
|
## Repo-Scoping Compatibility
|
||||||
|
|
||||||
Repo-scoping can adapt without a rewrite because its current graph payload
|
Repo-scoping can adapt without a rewrite because its current graph payload
|
||||||
|
|||||||
@@ -606,6 +606,7 @@ def graph_explorer_page() -> str:
|
|||||||
let activeMode = "full";
|
let activeMode = "full";
|
||||||
let activeLabelMode = "auto";
|
let activeLabelMode = "auto";
|
||||||
let activeZoneGrouping = "deploymentEnvironment";
|
let activeZoneGrouping = "deploymentEnvironment";
|
||||||
|
let activeZoneDefinitionSet = "fabric-default";
|
||||||
let profilePersistence = "none";
|
let profilePersistence = "none";
|
||||||
let profiles = [];
|
let profiles = [];
|
||||||
let currentProfileId = "";
|
let currentProfileId = "";
|
||||||
@@ -1045,6 +1046,14 @@ def graph_explorer_page() -> str:
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const zoneDefinitionSets = {
|
||||||
|
"fabric-default": {
|
||||||
|
id: "fabric-default",
|
||||||
|
label: "Fabric defaults",
|
||||||
|
groupings: ["deploymentEnvironment", "accessZone"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const zoneElementData = (element) => {
|
const zoneElementData = (element) => {
|
||||||
if (!element) return {};
|
if (!element) return {};
|
||||||
if (typeof element.data === "function") return element.data();
|
if (typeof element.data === "function") return element.data();
|
||||||
@@ -1120,6 +1129,7 @@ def graph_explorer_page() -> str:
|
|||||||
};
|
};
|
||||||
|
|
||||||
const zoneDefinitionsForData = (data, grouping = activeZoneGrouping) => {
|
const zoneDefinitionsForData = (data, grouping = activeZoneGrouping) => {
|
||||||
|
if (!zoneDefinitionSets[activeZoneDefinitionSet]) activeZoneDefinitionSet = "fabric-default";
|
||||||
if (grouping === "deploymentEnvironment") return defaultZoneDefinitions.deploymentEnvironment;
|
if (grouping === "deploymentEnvironment") return defaultZoneDefinitions.deploymentEnvironment;
|
||||||
if (grouping === "accessZone") {
|
if (grouping === "accessZone") {
|
||||||
const definition = accessZoneDefinition(zoneFieldValue(data, "accessZone"));
|
const definition = accessZoneDefinition(zoneFieldValue(data, "accessZone"));
|
||||||
@@ -1129,6 +1139,7 @@ def graph_explorer_page() -> str:
|
|||||||
};
|
};
|
||||||
|
|
||||||
const zoneDefinitionsForElements = (elements, grouping = activeZoneGrouping) => {
|
const zoneDefinitionsForElements = (elements, grouping = activeZoneGrouping) => {
|
||||||
|
if (!zoneDefinitionSets[activeZoneDefinitionSet]) activeZoneDefinitionSet = "fabric-default";
|
||||||
if (grouping === "deploymentEnvironment") return defaultZoneDefinitions.deploymentEnvironment;
|
if (grouping === "deploymentEnvironment") return defaultZoneDefinitions.deploymentEnvironment;
|
||||||
if (grouping === "accessZone") {
|
if (grouping === "accessZone") {
|
||||||
const values = new Set();
|
const values = new Set();
|
||||||
@@ -1557,20 +1568,42 @@ def graph_explorer_page() -> str:
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentViewState = () => ({
|
const currentZoneViewState = () => {
|
||||||
search: searchInput.value,
|
const visible = zoneBoundaryToggle ? zoneBoundaryToggle.checked : true;
|
||||||
mode: modeSelect.value || "full",
|
const grouping = zoneGroupSelect ? zoneGroupSelect.value || "deploymentEnvironment" : "deploymentEnvironment";
|
||||||
layout: layoutSelect.value || "cose",
|
const definitionSet = zoneDefinitionSets[activeZoneDefinitionSet]
|
||||||
labelMode: labelSelect.value || "auto",
|
? activeZoneDefinitionSet
|
||||||
nodeTypes: Array.from(selectedNodeTypes()),
|
: "fabric-default";
|
||||||
edgeTypes: Array.from(selectedEdgeTypes()),
|
return {
|
||||||
review: reviewFilter.value,
|
visible,
|
||||||
unresolved: unresolvedFilter.value,
|
grouping,
|
||||||
zoneBoundaries: zoneBoundaryToggle ? zoneBoundaryToggle.checked : true,
|
definitionSet,
|
||||||
zoneGrouping: zoneGroupSelect ? zoneGroupSelect.value || "deploymentEnvironment" : "deploymentEnvironment",
|
presentation: {
|
||||||
rules: filterRules.map((rule) => ({...rule})),
|
boundaries: visible,
|
||||||
manualOverrides: {...manualOverrides},
|
labels: true,
|
||||||
});
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentViewState = () => {
|
||||||
|
const zone = currentZoneViewState();
|
||||||
|
return {
|
||||||
|
search: searchInput.value,
|
||||||
|
mode: modeSelect.value || "full",
|
||||||
|
layout: layoutSelect.value || "cose",
|
||||||
|
labelMode: labelSelect.value || "auto",
|
||||||
|
nodeTypes: Array.from(selectedNodeTypes()),
|
||||||
|
edgeTypes: Array.from(selectedEdgeTypes()),
|
||||||
|
review: reviewFilter.value,
|
||||||
|
unresolved: unresolvedFilter.value,
|
||||||
|
zone,
|
||||||
|
zoneBoundaries: zone.visible,
|
||||||
|
zoneGrouping: zone.grouping,
|
||||||
|
zoneDefinitionSet: zone.definitionSet,
|
||||||
|
rules: filterRules.map((rule) => ({...rule})),
|
||||||
|
manualOverrides: {...manualOverrides},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const encodeStateBlob = (state) => {
|
const encodeStateBlob = (state) => {
|
||||||
const bytes = new TextEncoder().encode(JSON.stringify(state));
|
const bytes = new TextEncoder().encode(JSON.stringify(state));
|
||||||
@@ -1598,6 +1631,7 @@ def graph_explorer_page() -> str:
|
|||||||
if (params.has("unresolved")) state.unresolved = params.get("unresolved") || "";
|
if (params.has("unresolved")) state.unresolved = params.get("unresolved") || "";
|
||||||
if (params.has("zoneBoundaries")) state.zoneBoundaries = params.get("zoneBoundaries") !== "0";
|
if (params.has("zoneBoundaries")) state.zoneBoundaries = params.get("zoneBoundaries") !== "0";
|
||||||
if (params.has("zoneGrouping")) state.zoneGrouping = params.get("zoneGrouping") || "";
|
if (params.has("zoneGrouping")) state.zoneGrouping = params.get("zoneGrouping") || "";
|
||||||
|
if (params.has("zoneDefinitionSet")) state.zoneDefinitionSet = params.get("zoneDefinitionSet") || "";
|
||||||
if (params.has("profile")) state.profile = params.get("profile") || "";
|
if (params.has("profile")) state.profile = params.get("profile") || "";
|
||||||
if (params.has("state")) {
|
if (params.has("state")) {
|
||||||
try {
|
try {
|
||||||
@@ -1622,6 +1656,7 @@ def graph_explorer_page() -> str:
|
|||||||
if (state.unresolved) params.set("unresolved", state.unresolved);
|
if (state.unresolved) params.set("unresolved", state.unresolved);
|
||||||
if (state.zoneBoundaries === false) params.set("zoneBoundaries", "0");
|
if (state.zoneBoundaries === false) params.set("zoneBoundaries", "0");
|
||||||
if (state.zoneGrouping && state.zoneGrouping !== "deploymentEnvironment") params.set("zoneGrouping", state.zoneGrouping);
|
if (state.zoneGrouping && state.zoneGrouping !== "deploymentEnvironment") params.set("zoneGrouping", state.zoneGrouping);
|
||||||
|
if (state.zoneDefinitionSet && state.zoneDefinitionSet !== "fabric-default") params.set("zoneDefinitionSet", state.zoneDefinitionSet);
|
||||||
if (currentProfileId) params.set("profile", currentProfileId);
|
if (currentProfileId) params.set("profile", currentProfileId);
|
||||||
if (includeStateBlob || hasManualOverrides() || filterRules.length > 0) {
|
if (includeStateBlob || hasManualOverrides() || filterRules.length > 0) {
|
||||||
params.set("state", encodeStateBlob(state));
|
params.set("state", encodeStateBlob(state));
|
||||||
@@ -1638,6 +1673,31 @@ def graph_explorer_page() -> str:
|
|||||||
const optionExists = (select, value) =>
|
const optionExists = (select, value) =>
|
||||||
Array.from(select.options).some((option) => option.value === value);
|
Array.from(select.options).some((option) => option.value === value);
|
||||||
|
|
||||||
|
const normalizeZoneViewState = (state) => {
|
||||||
|
const nested = state.zone && typeof state.zone === "object" ? state.zone : {};
|
||||||
|
const presentation = nested.presentation && typeof nested.presentation === "object"
|
||||||
|
? nested.presentation
|
||||||
|
: {};
|
||||||
|
const grouping = nested.grouping || state.zoneGrouping || "deploymentEnvironment";
|
||||||
|
const definitionSet = zoneDefinitionSets[nested.definitionSet || state.zoneDefinitionSet]
|
||||||
|
? nested.definitionSet || state.zoneDefinitionSet
|
||||||
|
: "fabric-default";
|
||||||
|
const visible = "visible" in nested
|
||||||
|
? nested.visible !== false
|
||||||
|
: "zoneBoundaries" in state
|
||||||
|
? state.zoneBoundaries !== false
|
||||||
|
: true;
|
||||||
|
return {
|
||||||
|
visible,
|
||||||
|
grouping: zoneGroupSelect && optionExists(zoneGroupSelect, grouping) ? grouping : "deploymentEnvironment",
|
||||||
|
definitionSet,
|
||||||
|
presentation: {
|
||||||
|
boundaries: "boundaries" in presentation ? presentation.boundaries !== false : visible,
|
||||||
|
labels: "labels" in presentation ? presentation.labels !== false : true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const applyViewState = (state, options = {}) => {
|
const applyViewState = (state, options = {}) => {
|
||||||
if ("search" in state) searchInput.value = state.search || "";
|
if ("search" in state) searchInput.value = state.search || "";
|
||||||
if ("mode" in state) modeSelect.value = optionExists(modeSelect, state.mode) ? state.mode : "full";
|
if ("mode" in state) modeSelect.value = optionExists(modeSelect, state.mode) ? state.mode : "full";
|
||||||
@@ -1648,9 +1708,11 @@ def graph_explorer_page() -> str:
|
|||||||
if ("edgeTypes" in state) setCheckedValues(edgeTypeFilter, (state.edgeTypes || []).filter((value) => allEdgeTypes.includes(value)));
|
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 ("review" in state) reviewFilter.value = optionExists(reviewFilter, state.review) ? state.review : "";
|
||||||
if ("unresolved" in state) unresolvedFilter.value = optionExists(unresolvedFilter, state.unresolved) ? state.unresolved : "";
|
if ("unresolved" in state) unresolvedFilter.value = optionExists(unresolvedFilter, state.unresolved) ? state.unresolved : "";
|
||||||
if ("zoneBoundaries" in state && zoneBoundaryToggle) zoneBoundaryToggle.checked = state.zoneBoundaries !== false;
|
if ("zone" in state || "zoneBoundaries" in state || "zoneGrouping" in state || "zoneDefinitionSet" in state) {
|
||||||
if ("zoneGrouping" in state && zoneGroupSelect) {
|
const zone = normalizeZoneViewState(state);
|
||||||
zoneGroupSelect.value = optionExists(zoneGroupSelect, state.zoneGrouping) ? state.zoneGrouping : "deploymentEnvironment";
|
activeZoneDefinitionSet = zone.definitionSet;
|
||||||
|
if (zoneBoundaryToggle) zoneBoundaryToggle.checked = zone.visible;
|
||||||
|
if (zoneGroupSelect) zoneGroupSelect.value = zone.grouping;
|
||||||
}
|
}
|
||||||
if ("manualOverrides" in state && state.manualOverrides && typeof state.manualOverrides === "object") {
|
if ("manualOverrides" in state && state.manualOverrides && typeof state.manualOverrides === "object") {
|
||||||
manualOverrides = {...state.manualOverrides};
|
manualOverrides = {...state.manualOverrides};
|
||||||
@@ -2492,6 +2554,7 @@ def graph_explorer_page() -> str:
|
|||||||
zoneBoundaryToggle.checked = true;
|
zoneBoundaryToggle.checked = true;
|
||||||
zoneGroupSelect.value = "deploymentEnvironment";
|
zoneGroupSelect.value = "deploymentEnvironment";
|
||||||
activeZoneGrouping = "deploymentEnvironment";
|
activeZoneGrouping = "deploymentEnvironment";
|
||||||
|
activeZoneDefinitionSet = "fabric-default";
|
||||||
selectedZoneId = "";
|
selectedZoneId = "";
|
||||||
setCheckedValues(nodeTypeFilter);
|
setCheckedValues(nodeTypeFilter);
|
||||||
setCheckedValues(edgeTypeFilter);
|
setCheckedValues(edgeTypeFilter);
|
||||||
|
|||||||
@@ -422,6 +422,11 @@ def test_registry_serves_graph_explorer_exports(tmp_path: Path) -> None:
|
|||||||
assert "ZONE_NODE_SEEDED_BY_MULTIPLE_ZONES" in page
|
assert "ZONE_NODE_SEEDED_BY_MULTIPLE_ZONES" in page
|
||||||
assert "ZONE_EDGE_CROSSES_ZONE_BOUNDARY" in page
|
assert "ZONE_EDGE_CROSSES_ZONE_BOUNDARY" in page
|
||||||
assert "zone.diagnostics" in page
|
assert "zone.diagnostics" in page
|
||||||
|
assert "currentZoneViewState" in page
|
||||||
|
assert "normalizeZoneViewState" in page
|
||||||
|
assert "zoneDefinitionSet" in page
|
||||||
|
assert "zoneDefinitionSets" in page
|
||||||
|
assert "fabric-default" in page
|
||||||
assert 'normalize: "deploymentEnvironment"' in page
|
assert 'normalize: "deploymentEnvironment"' in page
|
||||||
assert "zoneBoundsForNodes" in page
|
assert "zoneBoundsForNodes" in page
|
||||||
assert "renderZoneOverlay" in page
|
assert "renderZoneOverlay" in page
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ resolver path.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAIL-FAB-WP-0022-T05
|
id: RAIL-FAB-WP-0022-T05
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "765caa50-f372-4ab4-adb4-87660e684c54"
|
state_hub_task_id: "765caa50-f372-4ab4-adb4-87660e684c54"
|
||||||
```
|
```
|
||||||
@@ -173,6 +173,13 @@ At minimum, preserve:
|
|||||||
Expected result: saved views restore the same zone mode and default definition
|
Expected result: saved views restore the same zone mode and default definition
|
||||||
set after reload.
|
set after reload.
|
||||||
|
|
||||||
|
Result: Added explicit nested zone state to graph explorer profiles with
|
||||||
|
`visible`, `grouping`, `definitionSet`, and `presentation` fields while keeping
|
||||||
|
legacy URL aliases for `zoneBoundaries`, `zoneGrouping`, and
|
||||||
|
`zoneDefinitionSet`. Saved and copied views now preserve the active zone
|
||||||
|
definition set and presentation state, and profile restore normalizes unknown
|
||||||
|
future definition sets back to the Fabric default.
|
||||||
|
|
||||||
## Task 6: Prototype Collapse-To-Zone-Node
|
## Task 6: Prototype Collapse-To-Zone-Node
|
||||||
|
|
||||||
```task
|
```task
|
||||||
|
|||||||
Reference in New Issue
Block a user