diff --git a/railiance_fabric/graph_explorer_ui.py b/railiance_fabric/graph_explorer_ui.py
index f9bd25f..6ca6e8a 100644
--- a/railiance_fabric/graph_explorer_ui.py
+++ b/railiance_fabric/graph_explorer_ui.py
@@ -121,6 +121,7 @@ def graph_explorer_page() -> str:
right: 12px;
z-index: 3;
display: flex;
+ align-items: end;
gap: 6px;
padding: 6px;
border: 1px solid var(--line);
@@ -129,6 +130,18 @@ def graph_explorer_page() -> str:
box-shadow: 0 12px 30px rgba(23, 32, 51, .12);
}
.map-controls button { min-width: 54px; }
+ .map-control-field {
+ display: grid;
+ gap: 2px;
+ color: var(--muted);
+ font-size: 11px;
+ line-height: 1;
+ }
+ .map-control-field select {
+ min-height: 30px;
+ min-width: 84px;
+ padding: 4px 6px;
+ }
.selection-anchor {
position: absolute;
z-index: 5;
@@ -248,6 +261,14 @@ def graph_explorer_page() -> str:
+
@@ -305,6 +326,7 @@ def graph_explorer_page() -> str:
const searchInput = document.getElementById("search");
const modeSelect = document.getElementById("mode-select");
const layoutSelect = document.getElementById("layout-select");
+ const labelSelect = document.getElementById("label-select");
const nodeTypeFilter = document.getElementById("node-type-filter");
const nodeTypeSummary = document.getElementById("node-type-summary");
const edgeTypeFilter = document.getElementById("edge-type-filter");
@@ -334,6 +356,7 @@ def graph_explorer_page() -> str:
let allNodeTypes = [];
let allEdgeTypes = [];
let activeMode = "full";
+ let activeLabelMode = "auto";
let profilePersistence = "none";
let profiles = [];
let currentProfileId = "";
@@ -462,10 +485,33 @@ def graph_explorer_page() -> str:
selectionAnchor.style.display = "block";
};
+ const isKeyLabelNode = (node) => {
+ const layer = node.data("layer");
+ return (
+ ["repository", "server", "deployment", "service"].includes(layer) ||
+ node.data("unresolved") === true ||
+ node.data("lifecycle") === "registered-only"
+ );
+ };
+
+ const updateLabelVisibility = () => {
+ if (!cy) return;
+ const visibleNodes = cy.nodes().filter((node) => node.style("display") !== "none");
+ const mode = activeLabelMode === "auto" && visibleNodes.length > 70 ? "key" : activeLabelMode;
+ cy.nodes().forEach((node) => {
+ let showLabel = mode === "all" || mode === "auto";
+ if (mode === "key") showLabel = isKeyLabelNode(node);
+ if (mode === "none") showLabel = false;
+ if (selected && node.id() === selected.id()) showLabel = false;
+ node.toggleClass("label-hidden", !showLabel);
+ });
+ };
+
const currentViewState = () => ({
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,
@@ -491,6 +537,7 @@ def graph_explorer_page() -> str:
if (params.has("search")) state.search = params.get("search") || "";
if (params.has("mode")) state.mode = params.get("mode") || "";
if (params.has("layout")) state.layout = params.get("layout") || "";
+ if (params.has("labelMode")) state.labelMode = params.get("labelMode") || "";
if (params.has("nodeTypes")) state.nodeTypes = parseListParam(params.get("nodeTypes"));
if (params.has("edgeTypes")) state.edgeTypes = parseListParam(params.get("edgeTypes"));
if (params.has("layer")) state.nodeTypes = parseListParam(params.get("layer"));
@@ -513,6 +560,7 @@ def graph_explorer_page() -> str:
if (state.search) params.set("search", state.search);
if (state.mode && state.mode !== "full") params.set("mode", state.mode);
if (state.layout && state.layout !== "cose") params.set("layout", state.layout);
+ if (state.labelMode && state.labelMode !== "auto") params.set("labelMode", state.labelMode);
if (state.nodeTypes.length !== allNodeTypes.length) params.set("nodeTypes", state.nodeTypes.join(","));
if (state.edgeTypes.length !== allEdgeTypes.length) params.set("edgeTypes", state.edgeTypes.join(","));
if (state.review) params.set("review", state.review);
@@ -535,6 +583,7 @@ def graph_explorer_page() -> str:
if ("search" in state) searchInput.value = state.search || "";
if ("mode" in state) modeSelect.value = optionExists(modeSelect, state.mode) ? state.mode : "full";
if ("layout" in state) layoutSelect.value = optionExists(layoutSelect, state.layout) ? state.layout : "cose";
+ if ("labelMode" in state) labelSelect.value = optionExists(labelSelect, state.labelMode) ? state.labelMode : "auto";
if ("nodeTypes" in state) setCheckedValues(nodeTypeFilter, (state.nodeTypes || []).filter((value) => allNodeTypes.includes(value)));
if ("layer" in state) setCheckedValues(nodeTypeFilter, allNodeTypes.includes(state.layer) ? [state.layer] : null);
if ("edgeTypes" in state) setCheckedValues(edgeTypeFilter, (state.edgeTypes || []).filter((value) => allEdgeTypes.includes(value)));
@@ -544,6 +593,7 @@ def graph_explorer_page() -> str:
manualOverrides = {...state.manualOverrides};
}
activeMode = modeSelect.value || "full";
+ activeLabelMode = labelSelect.value || "auto";
focusSet = null;
syncFilterSummaries();
applyFilters();
@@ -711,6 +761,7 @@ def graph_explorer_page() -> str:
const hidden = cy.elements().length - visibleNodes - visibleEdges;
counts.textContent = `${visibleNodes} nodes / ${visibleEdges} edges`;
hiddenSummary.textContent = `Hidden ${hidden}`;
+ updateLabelVisibility();
updateSelectionAnchor();
};
@@ -723,6 +774,7 @@ def graph_explorer_page() -> str:
detailList.innerHTML = "";
orientationTitle.textContent = "Select a service, interface, dependency, or registered-only repo.";
orientationList.innerHTML = "";
+ updateLabelVisibility();
updateSelectionAnchor();
return;
}
@@ -749,6 +801,7 @@ def graph_explorer_page() -> str:
.map(([key, value]) => `
${escapeHtml(key)} ${escapeHtml(value)}`)
.join("");
renderOrientation(element);
+ updateLabelVisibility();
updateSelectionAnchor();
};
@@ -866,9 +919,15 @@ def graph_explorer_page() -> str:
};
const renderLegend = (layers) => {
- legend.innerHTML = layers.map((layer) =>
+ const nodeTypes = layers.map((layer) =>
`${escapeHtml(layer.label)}`
- ).join("");
+ );
+ const statusItems = [
+ 'Unresolved',
+ 'Registered only',
+ 'Weak edge',
+ ];
+ legend.innerHTML = [...nodeTypes, ...statusItems].join("");
};
const boot = async () => {
@@ -949,7 +1008,8 @@ def graph_explorer_page() -> str:
"z-index": 2
}},
{selector: ".display-blur", style: {"opacity": .24, "label": ""}},
- {selector: ".display-blur:selected, .display-blur.hover", style: {"opacity": .78, "label": "data(label)"}},
+ {selector: ".display-blur.hover", style: {"opacity": .78, "label": "data(label)"}},
+ {selector: ".label-hidden", style: {"label": ""}},
{selector: ":selected", style: {"overlay-opacity": 0}}
]
});
@@ -1015,12 +1075,23 @@ def graph_explorer_page() -> str:
updateProfileSummary();
updateUrlState();
});
+ labelSelect.addEventListener("input", () => {
+ activeLabelMode = labelSelect.value || "auto";
+ currentProfileId = "";
+ profileSelect.value = "";
+ updateLabelVisibility();
+ updateProfileControls();
+ updateProfileSummary();
+ updateUrlState();
+ });
document.querySelector("[data-action='fit']").addEventListener("click", () => cy && cy.fit(cy.elements(":visible"), 48));
document.querySelector("[data-action='focus']").addEventListener("click", applyFocus);
document.querySelector("[data-action='clear']").addEventListener("click", () => {
searchInput.value = "";
modeSelect.value = "full";
activeMode = "full";
+ labelSelect.value = "auto";
+ activeLabelMode = "auto";
setCheckedValues(nodeTypeFilter);
setCheckedValues(edgeTypeFilter);
reviewFilter.value = "";
diff --git a/tests/test_graph_explorer.py b/tests/test_graph_explorer.py
index 1177a39..ffc62a1 100644
--- a/tests/test_graph_explorer.py
+++ b/tests/test_graph_explorer.py
@@ -206,12 +206,16 @@ def test_registry_serves_graph_explorer_exports(tmp_path: Path) -> None:
assert 'id="graph-canvas"' in page
assert 'id="mode-select"' in page
assert 'id="layout-select"' in page
+ assert 'id="label-select"' in page
assert 'id="node-type-filter"' in page
assert 'id="edge-type-filter"' in page
assert 'id="selection-anchor"' in page
assert "updateSelectionAnchor" in page
+ assert "updateLabelVisibility" in page
+ assert "label-hidden" in page
assert "Node Types" in page
assert "Edge Types" in page
+ assert "Registered only" in page
assert 'id="layer-filter"' not in page
assert '"border-width": 4' not in page
assert 'id="profile-select"' in page