generated from coulomb/repo-seed
Refine graph explorer controls
This commit is contained in:
@@ -124,7 +124,7 @@ def fabric_graph_explorer_manifest(base_url: str = "") -> dict[str, Any]:
|
|||||||
"actions": list(DISPLAY_STATES),
|
"actions": list(DISPLAY_STATES),
|
||||||
"fields": [
|
"fields": [
|
||||||
{"id": "kind", "label": "Kind", "type": "string"},
|
{"id": "kind", "label": "Kind", "type": "string"},
|
||||||
{"id": "layer", "label": "Layer", "type": "string"},
|
{"id": "layer", "label": "Node Type", "type": "string"},
|
||||||
{"id": "repo", "label": "Repo", "type": "string"},
|
{"id": "repo", "label": "Repo", "type": "string"},
|
||||||
{"id": "domain", "label": "Domain", "type": "string"},
|
{"id": "domain", "label": "Domain", "type": "string"},
|
||||||
{"id": "environment", "label": "Environment", "type": "string"},
|
{"id": "environment", "label": "Environment", "type": "string"},
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ def graph_explorer_page() -> str:
|
|||||||
.shell { display: grid; grid-template-rows: auto 1fr; height: 100vh; min-height: 620px; }
|
.shell { display: grid; grid-template-rows: auto 1fr; height: 100vh; min-height: 620px; }
|
||||||
.toolbar {
|
.toolbar {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(180px, 1.2fr) repeat(6, minmax(110px, .55fr)) auto auto auto;
|
grid-template-columns: minmax(180px, 1.2fr) repeat(7, minmax(108px, .55fr)) auto;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
@@ -47,7 +47,7 @@ def graph_explorer_page() -> str:
|
|||||||
.side h1 { font-size: 17px; line-height: 1.2; margin: 0 0 10px; }
|
.side h1 { font-size: 17px; line-height: 1.2; margin: 0 0 10px; }
|
||||||
.section { border-top: 1px solid var(--line); padding-top: 12px; margin-top: 12px; }
|
.section { border-top: 1px solid var(--line); padding-top: 12px; margin-top: 12px; }
|
||||||
.section:first-child { border-top: 0; padding-top: 0; margin-top: 0; }
|
.section:first-child { border-top: 0; padding-top: 0; margin-top: 0; }
|
||||||
label { color: var(--muted); display: grid; gap: 4px; font-size: 12px; }
|
label, .field { color: var(--muted); display: grid; gap: 4px; font-size: 12px; }
|
||||||
input, select, button {
|
input, select, button {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -67,6 +67,68 @@ def graph_explorer_page() -> str:
|
|||||||
}
|
}
|
||||||
button.primary { background: var(--accent); border-color: var(--accent); color: #ffffff; }
|
button.primary { background: var(--accent); border-color: var(--accent); color: #ffffff; }
|
||||||
button:disabled { color: #98a2b3; cursor: default; }
|
button:disabled { color: #98a2b3; cursor: default; }
|
||||||
|
.filter-menu {
|
||||||
|
position: relative;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.filter-menu summary {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
min-height: 34px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 6px 8px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
.filter-menu summary::-webkit-details-marker { display: none; }
|
||||||
|
.filter-menu[open] summary { border-color: var(--accent); }
|
||||||
|
.check-list {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 8;
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
width: max-content;
|
||||||
|
min-width: 210px;
|
||||||
|
max-width: 280px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow: auto;
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 16px 40px rgba(23, 32, 51, .14);
|
||||||
|
}
|
||||||
|
.check-list label {
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 13px;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 26px;
|
||||||
|
}
|
||||||
|
.check-list input {
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.map-controls {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
z-index: 3;
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, .94);
|
||||||
|
box-shadow: 0 12px 30px rgba(23, 32, 51, .12);
|
||||||
|
}
|
||||||
|
.map-controls button { min-width: 54px; }
|
||||||
.button-row { display: flex; flex-wrap: wrap; gap: 8px; }
|
.button-row { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||||
.meta { color: var(--muted); font-size: 12px; margin: 0; }
|
.meta { color: var(--muted); font-size: 12px; margin: 0; }
|
||||||
.pill {
|
.pill {
|
||||||
@@ -116,18 +178,31 @@ def graph_explorer_page() -> str:
|
|||||||
<option value="grid">Grid</option>
|
<option value="grid">Grid</option>
|
||||||
<option value="breadthfirst">Breadthfirst</option>
|
<option value="breadthfirst">Breadthfirst</option>
|
||||||
</select></label>
|
</select></label>
|
||||||
<label>Layer <select id="layer-filter"><option value="">Any</option></select></label>
|
<div class="field"><span>Node Types</span>
|
||||||
|
<details id="node-type-menu" class="filter-menu">
|
||||||
|
<summary id="node-type-summary">All node types</summary>
|
||||||
|
<div id="node-type-filter" class="check-list"></div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
<div class="field"><span>Edge Types</span>
|
||||||
|
<details id="edge-type-menu" class="filter-menu">
|
||||||
|
<summary id="edge-type-summary">All edge types</summary>
|
||||||
|
<div id="edge-type-filter" class="check-list"></div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
<label>Review <select id="review-filter"><option value="">Any</option><option>accepted</option><option>candidate</option></select></label>
|
<label>Review <select id="review-filter"><option value="">Any</option><option>accepted</option><option>candidate</option></select></label>
|
||||||
<label>Unresolved <select id="unresolved-filter"><option value="">Any</option><option value="true">Only</option></select></label>
|
<label>Unresolved <select id="unresolved-filter"><option value="">Any</option><option value="true">Only</option></select></label>
|
||||||
<label>Profile <select id="profile-select" disabled><option>Unsaved exploration</option></select></label>
|
<label>Profile <select id="profile-select" disabled><option>Unsaved exploration</option></select></label>
|
||||||
<button type="button" data-action="fit">Fit</button>
|
|
||||||
<button type="button" data-action="focus">Focus</button>
|
|
||||||
<button type="button" data-action="clear" class="primary">Reset</button>
|
|
||||||
<span id="counts" class="meta"></span>
|
<span id="counts" class="meta"></span>
|
||||||
</div>
|
</div>
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<div class="canvas-wrap">
|
<div class="canvas-wrap">
|
||||||
<div id="graph-canvas" role="img" aria-label="Interactive Fabric graph"></div>
|
<div id="graph-canvas" role="img" aria-label="Interactive Fabric graph"></div>
|
||||||
|
<div class="map-controls" aria-label="Map navigation controls">
|
||||||
|
<button type="button" data-action="fit" title="Fit graph to view">Fit</button>
|
||||||
|
<button type="button" data-action="focus" title="Focus selected neighborhood">Focus</button>
|
||||||
|
<button type="button" data-action="clear" class="primary" title="Reset map controls">Reset</button>
|
||||||
|
</div>
|
||||||
<div id="popup" class="popup"></div>
|
<div id="popup" class="popup"></div>
|
||||||
</div>
|
</div>
|
||||||
<aside class="side">
|
<aside class="side">
|
||||||
@@ -176,7 +251,10 @@ def graph_explorer_page() -> str:
|
|||||||
const searchInput = document.getElementById("search");
|
const searchInput = document.getElementById("search");
|
||||||
const modeSelect = document.getElementById("mode-select");
|
const modeSelect = document.getElementById("mode-select");
|
||||||
const layoutSelect = document.getElementById("layout-select");
|
const layoutSelect = document.getElementById("layout-select");
|
||||||
const layerFilter = document.getElementById("layer-filter");
|
const nodeTypeFilter = document.getElementById("node-type-filter");
|
||||||
|
const nodeTypeSummary = document.getElementById("node-type-summary");
|
||||||
|
const edgeTypeFilter = document.getElementById("edge-type-filter");
|
||||||
|
const edgeTypeSummary = document.getElementById("edge-type-summary");
|
||||||
const reviewFilter = document.getElementById("review-filter");
|
const reviewFilter = document.getElementById("review-filter");
|
||||||
const unresolvedFilter = document.getElementById("unresolved-filter");
|
const unresolvedFilter = document.getElementById("unresolved-filter");
|
||||||
const profileSelect = document.getElementById("profile-select");
|
const profileSelect = document.getElementById("profile-select");
|
||||||
@@ -197,6 +275,9 @@ def graph_explorer_page() -> str:
|
|||||||
let focusSet = null;
|
let focusSet = null;
|
||||||
let manualOverrides = {};
|
let manualOverrides = {};
|
||||||
let layerColors = {};
|
let layerColors = {};
|
||||||
|
let nodeTypeLabels = {};
|
||||||
|
let allNodeTypes = [];
|
||||||
|
let allEdgeTypes = [];
|
||||||
let activeMode = "full";
|
let activeMode = "full";
|
||||||
let profilePersistence = "none";
|
let profilePersistence = "none";
|
||||||
let profiles = [];
|
let profiles = [];
|
||||||
@@ -220,11 +301,65 @@ def graph_explorer_page() -> str:
|
|||||||
|
|
||||||
const supportsLocalProfiles = () => profilePersistence === "local";
|
const supportsLocalProfiles = () => profilePersistence === "local";
|
||||||
|
|
||||||
|
const humanize = (value) => String(value || "")
|
||||||
|
.replaceAll("_", " ")
|
||||||
|
.replaceAll(":", ": ")
|
||||||
|
.replace(/\\b\\w/g, (letter) => letter.toUpperCase());
|
||||||
|
|
||||||
|
const parseListParam = (value) => String(value || "")
|
||||||
|
.split(",")
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const checkedValues = (container) => new Set(
|
||||||
|
Array.from(container.querySelectorAll("input[type='checkbox']"))
|
||||||
|
.filter((input) => input.checked)
|
||||||
|
.map((input) => input.value)
|
||||||
|
);
|
||||||
|
|
||||||
|
const setCheckedValues = (container, values = null) => {
|
||||||
|
const explicit = Array.isArray(values);
|
||||||
|
const selectedValues = new Set(values || []);
|
||||||
|
container.querySelectorAll("input[type='checkbox']").forEach((input) => {
|
||||||
|
input.checked = explicit ? selectedValues.has(input.value) : true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedNodeTypes = () => checkedValues(nodeTypeFilter);
|
||||||
|
|
||||||
|
const selectedEdgeTypes = () => checkedValues(edgeTypeFilter);
|
||||||
|
|
||||||
|
const summarizeSelection = (selected, allValues, labels, noun) => {
|
||||||
|
if (selected.size === 0) return `No ${noun}`;
|
||||||
|
if (selected.size === allValues.length) return `All ${noun}`;
|
||||||
|
const names = allValues
|
||||||
|
.filter((value) => selected.has(value))
|
||||||
|
.map((value) => labels[value] || humanize(value));
|
||||||
|
return names.length <= 2 ? names.join(", ") : `${names.length} ${noun}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncFilterSummaries = () => {
|
||||||
|
nodeTypeSummary.textContent = summarizeSelection(
|
||||||
|
selectedNodeTypes(), allNodeTypes, nodeTypeLabels, "node types"
|
||||||
|
);
|
||||||
|
edgeTypeSummary.textContent = summarizeSelection(
|
||||||
|
selectedEdgeTypes(), allEdgeTypes, {}, "edge types"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderChecklist = (container, values, labels, name) => {
|
||||||
|
container.innerHTML = values.map((value) => {
|
||||||
|
const label = labels[value] || humanize(value);
|
||||||
|
return `<label><input type="checkbox" name="${escapeHtml(name)}" value="${escapeHtml(value)}" checked> ${escapeHtml(label)}</label>`;
|
||||||
|
}).join("");
|
||||||
|
};
|
||||||
|
|
||||||
const currentViewState = () => ({
|
const currentViewState = () => ({
|
||||||
search: searchInput.value,
|
search: searchInput.value,
|
||||||
mode: modeSelect.value || "full",
|
mode: modeSelect.value || "full",
|
||||||
layout: layoutSelect.value || "cose",
|
layout: layoutSelect.value || "cose",
|
||||||
layer: layerFilter.value,
|
nodeTypes: Array.from(selectedNodeTypes()),
|
||||||
|
edgeTypes: Array.from(selectedEdgeTypes()),
|
||||||
review: reviewFilter.value,
|
review: reviewFilter.value,
|
||||||
unresolved: unresolvedFilter.value,
|
unresolved: unresolvedFilter.value,
|
||||||
manualOverrides: {...manualOverrides},
|
manualOverrides: {...manualOverrides},
|
||||||
@@ -248,7 +383,9 @@ def graph_explorer_page() -> str:
|
|||||||
if (params.has("search")) state.search = params.get("search") || "";
|
if (params.has("search")) state.search = params.get("search") || "";
|
||||||
if (params.has("mode")) state.mode = params.get("mode") || "";
|
if (params.has("mode")) state.mode = params.get("mode") || "";
|
||||||
if (params.has("layout")) state.layout = params.get("layout") || "";
|
if (params.has("layout")) state.layout = params.get("layout") || "";
|
||||||
if (params.has("layer")) state.layer = params.get("layer") || "";
|
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"));
|
||||||
if (params.has("review")) state.review = params.get("review") || "";
|
if (params.has("review")) state.review = params.get("review") || "";
|
||||||
if (params.has("unresolved")) state.unresolved = params.get("unresolved") || "";
|
if (params.has("unresolved")) state.unresolved = params.get("unresolved") || "";
|
||||||
if (params.has("profile")) state.profile = params.get("profile") || "";
|
if (params.has("profile")) state.profile = params.get("profile") || "";
|
||||||
@@ -268,7 +405,8 @@ def graph_explorer_page() -> str:
|
|||||||
if (state.search) params.set("search", state.search);
|
if (state.search) params.set("search", state.search);
|
||||||
if (state.mode && state.mode !== "full") params.set("mode", state.mode);
|
if (state.mode && state.mode !== "full") params.set("mode", state.mode);
|
||||||
if (state.layout && state.layout !== "cose") params.set("layout", state.layout);
|
if (state.layout && state.layout !== "cose") params.set("layout", state.layout);
|
||||||
if (state.layer) params.set("layer", state.layer);
|
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);
|
if (state.review) params.set("review", state.review);
|
||||||
if (state.unresolved) params.set("unresolved", state.unresolved);
|
if (state.unresolved) params.set("unresolved", state.unresolved);
|
||||||
if (currentProfileId) params.set("profile", currentProfileId);
|
if (currentProfileId) params.set("profile", currentProfileId);
|
||||||
@@ -289,7 +427,9 @@ def graph_explorer_page() -> str:
|
|||||||
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";
|
||||||
if ("layout" in state) layoutSelect.value = optionExists(layoutSelect, state.layout) ? state.layout : "cose";
|
if ("layout" in state) layoutSelect.value = optionExists(layoutSelect, state.layout) ? state.layout : "cose";
|
||||||
if ("layer" in state) layerFilter.value = optionExists(layerFilter, state.layer) ? state.layer : "";
|
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)));
|
||||||
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 ("manualOverrides" in state && state.manualOverrides && typeof state.manualOverrides === "object") {
|
if ("manualOverrides" in state && state.manualOverrides && typeof state.manualOverrides === "object") {
|
||||||
@@ -297,6 +437,7 @@ def graph_explorer_page() -> str:
|
|||||||
}
|
}
|
||||||
activeMode = modeSelect.value || "full";
|
activeMode = modeSelect.value || "full";
|
||||||
focusSet = null;
|
focusSet = null;
|
||||||
|
syncFilterSummaries();
|
||||||
applyFilters();
|
applyFilters();
|
||||||
if (!options.skipLayout) runLayout();
|
if (!options.skipLayout) runLayout();
|
||||||
if (!options.skipUrl) updateUrlState();
|
if (!options.skipUrl) updateUrlState();
|
||||||
@@ -403,7 +544,8 @@ def graph_explorer_page() -> str:
|
|||||||
if (modeSet && !modeSet.has(element.id())) return false;
|
if (modeSet && !modeSet.has(element.id())) return false;
|
||||||
const text = searchInput.value.trim().toLowerCase();
|
const text = searchInput.value.trim().toLowerCase();
|
||||||
if (text && !elementText(data).includes(text)) return false;
|
if (text && !elementText(data).includes(text)) return false;
|
||||||
if (layerFilter.value && data.layer !== layerFilter.value) return false;
|
if (element.isNode() && !selectedNodeTypes().has(data.layer)) return false;
|
||||||
|
if (element.isEdge() && !selectedEdgeTypes().has(data.edgeType)) return false;
|
||||||
if (reviewFilter.value && data.reviewState !== reviewFilter.value) return false;
|
if (reviewFilter.value && data.reviewState !== reviewFilter.value) return false;
|
||||||
if (unresolvedFilter.value === "true" && data.unresolved !== true) return false;
|
if (unresolvedFilter.value === "true" && data.unresolved !== true) return false;
|
||||||
if (focusSet && !focusSet.has(element.id())) return false;
|
if (focusSet && !focusSet.has(element.id())) return false;
|
||||||
@@ -436,6 +578,7 @@ def graph_explorer_page() -> str:
|
|||||||
|
|
||||||
const applyFilters = () => {
|
const applyFilters = () => {
|
||||||
if (!cy) return;
|
if (!cy) return;
|
||||||
|
syncFilterSummaries();
|
||||||
const hiddenNodes = new Set();
|
const hiddenNodes = new Set();
|
||||||
cy.nodes().forEach((node) => {
|
cy.nodes().forEach((node) => {
|
||||||
let state = matchesFilters(node) ? "show" : "hide";
|
let state = matchesFilters(node) ? "show" : "hide";
|
||||||
@@ -476,8 +619,9 @@ def graph_explorer_page() -> str:
|
|||||||
const data = element.data();
|
const data = element.data();
|
||||||
detailTitle.textContent = data.name || data.label || data.id;
|
detailTitle.textContent = data.name || data.label || data.id;
|
||||||
detailSummary.textContent = data.description || data.id;
|
detailSummary.textContent = data.description || data.id;
|
||||||
detailPills.innerHTML = ["kind", "layer", "repo", "reviewState", "displayState"]
|
const nodeType = data.layer ? nodeTypeLabels[data.layer] || humanize(data.layer) : "";
|
||||||
.map((field) => data[field] ? `<span class="pill">${escapeHtml(data[field])}</span>` : "")
|
detailPills.innerHTML = [data.kind, nodeType, data.repo, data.reviewState, data.displayState]
|
||||||
|
.map((value) => value ? `<span class="pill">${escapeHtml(value)}</span>` : "")
|
||||||
.join("");
|
.join("");
|
||||||
const links = data.deepLinks || {};
|
const links = data.deepLinks || {};
|
||||||
const refs = data.sourceReferences || [];
|
const refs = data.sourceReferences || [];
|
||||||
@@ -628,12 +772,16 @@ def graph_explorer_page() -> str:
|
|||||||
});
|
});
|
||||||
profilePersistence = manifest.profile_persistence || "none";
|
profilePersistence = manifest.profile_persistence || "none";
|
||||||
layerColors = Object.fromEntries((manifest.layers || []).map((layer) => [layer.id, layer.color]));
|
layerColors = Object.fromEntries((manifest.layers || []).map((layer) => [layer.id, layer.color]));
|
||||||
(manifest.layers || []).forEach((layer) => {
|
nodeTypeLabels = Object.fromEntries((manifest.layers || []).map((layer) => [layer.id, layer.label]));
|
||||||
const option = document.createElement("option");
|
allNodeTypes = (manifest.layers || []).map((layer) => layer.id);
|
||||||
option.value = layer.id;
|
allEdgeTypes = Array.from(new Set((payload.elements || [])
|
||||||
option.textContent = layer.label;
|
.map((element) => element.data || {})
|
||||||
layerFilter.appendChild(option);
|
.filter((data) => data.source && data.target && data.edgeType)
|
||||||
});
|
.map((data) => data.edgeType)
|
||||||
|
)).sort((left, right) => left.localeCompare(right));
|
||||||
|
renderChecklist(nodeTypeFilter, allNodeTypes, nodeTypeLabels, "node-type");
|
||||||
|
renderChecklist(edgeTypeFilter, allEdgeTypes, {}, "edge-type");
|
||||||
|
syncFilterSummaries();
|
||||||
renderLegend(manifest.layers || []);
|
renderLegend(manifest.layers || []);
|
||||||
profiles = loadProfiles();
|
profiles = loadProfiles();
|
||||||
renderProfiles();
|
renderProfiles();
|
||||||
@@ -680,7 +828,8 @@ def graph_explorer_page() -> str:
|
|||||||
cy.on("mouseover", "node, edge", (event) => {
|
cy.on("mouseover", "node, edge", (event) => {
|
||||||
event.target.addClass("hover");
|
event.target.addClass("hover");
|
||||||
const data = event.target.data();
|
const data = event.target.data();
|
||||||
popup.innerHTML = `<strong>${escapeHtml(data.name || data.label || data.id)}</strong><span class="meta">${escapeHtml(data.kind)} / ${escapeHtml(data.layer)}</span>`;
|
const nodeType = data.layer ? nodeTypeLabels[data.layer] || humanize(data.layer) : "";
|
||||||
|
popup.innerHTML = `<strong>${escapeHtml(data.name || data.label || data.id)}</strong><span class="meta">${escapeHtml(data.kind)} / ${escapeHtml(nodeType || data.edgeType || "")}</span>`;
|
||||||
popup.style.left = `${Math.min(event.renderedPosition.x + 14, canvas.clientWidth - 270)}px`;
|
popup.style.left = `${Math.min(event.renderedPosition.x + 14, canvas.clientWidth - 270)}px`;
|
||||||
popup.style.top = `${Math.max(10, event.renderedPosition.y + 14)}px`;
|
popup.style.top = `${Math.max(10, event.renderedPosition.y + 14)}px`;
|
||||||
popup.style.display = "block";
|
popup.style.display = "block";
|
||||||
@@ -702,17 +851,20 @@ def graph_explorer_page() -> str:
|
|||||||
updateUrlState();
|
updateUrlState();
|
||||||
};
|
};
|
||||||
|
|
||||||
[searchInput, layerFilter, reviewFilter, unresolvedFilter].forEach((control) => {
|
const handleFilterChange = () => {
|
||||||
control.addEventListener("input", () => {
|
focusSet = null;
|
||||||
focusSet = null;
|
currentProfileId = "";
|
||||||
currentProfileId = "";
|
profileSelect.value = "";
|
||||||
profileSelect.value = "";
|
applyFilters();
|
||||||
applyFilters();
|
updateProfileControls();
|
||||||
updateProfileControls();
|
updateProfileSummary();
|
||||||
updateProfileSummary();
|
updateUrlState();
|
||||||
updateUrlState();
|
};
|
||||||
});
|
[searchInput, reviewFilter, unresolvedFilter].forEach((control) => {
|
||||||
|
control.addEventListener("input", handleFilterChange);
|
||||||
});
|
});
|
||||||
|
nodeTypeFilter.addEventListener("change", handleFilterChange);
|
||||||
|
edgeTypeFilter.addEventListener("change", handleFilterChange);
|
||||||
modeSelect.addEventListener("input", () => {
|
modeSelect.addEventListener("input", () => {
|
||||||
activeMode = modeSelect.value || "full";
|
activeMode = modeSelect.value || "full";
|
||||||
focusSet = null;
|
focusSet = null;
|
||||||
@@ -737,7 +889,8 @@ def graph_explorer_page() -> str:
|
|||||||
searchInput.value = "";
|
searchInput.value = "";
|
||||||
modeSelect.value = "full";
|
modeSelect.value = "full";
|
||||||
activeMode = "full";
|
activeMode = "full";
|
||||||
layerFilter.value = "";
|
setCheckedValues(nodeTypeFilter);
|
||||||
|
setCheckedValues(edgeTypeFilter);
|
||||||
reviewFilter.value = "";
|
reviewFilter.value = "";
|
||||||
unresolvedFilter.value = "";
|
unresolvedFilter.value = "";
|
||||||
focusSet = null;
|
focusSet = null;
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ def test_graph_explorer_manifest_and_payload_validate() -> None:
|
|||||||
assert manifest["profile_persistence"] == "local"
|
assert manifest["profile_persistence"] == "local"
|
||||||
assert manifest["shareable_state"]["profile_id"] is True
|
assert manifest["shareable_state"]["profile_id"] is True
|
||||||
assert {layer["id"] for layer in manifest["layers"]} >= {"server", "deployment"}
|
assert {layer["id"] for layer in manifest["layers"]} >= {"server", "deployment"}
|
||||||
|
filter_labels = {field["id"]: field["label"] for field in manifest["filter"]["fields"]}
|
||||||
|
assert filter_labels["layer"] == "Node Type"
|
||||||
nodes = [element for element in payload["elements"] if "source" not in element["data"]]
|
nodes = [element for element in payload["elements"] if "source" not in element["data"]]
|
||||||
edges = [element for element in payload["elements"] if "source" in element["data"]]
|
edges = [element for element in payload["elements"] if "source" in element["data"]]
|
||||||
registered_only = next(
|
registered_only = next(
|
||||||
@@ -204,6 +206,11 @@ def test_registry_serves_graph_explorer_exports(tmp_path: Path) -> None:
|
|||||||
assert 'id="graph-canvas"' in page
|
assert 'id="graph-canvas"' in page
|
||||||
assert 'id="mode-select"' in page
|
assert 'id="mode-select"' in page
|
||||||
assert 'id="layout-select"' in page
|
assert 'id="layout-select"' in page
|
||||||
|
assert 'id="node-type-filter"' in page
|
||||||
|
assert 'id="edge-type-filter"' in page
|
||||||
|
assert "Node Types" in page
|
||||||
|
assert "Edge Types" in page
|
||||||
|
assert 'id="layer-filter"' not in page
|
||||||
assert 'id="profile-select"' in page
|
assert 'id="profile-select"' in page
|
||||||
assert 'id="profile-name"' in page
|
assert 'id="profile-name"' in page
|
||||||
assert 'id="orientation-list"' in page
|
assert 'id="orientation-list"' in page
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ Acceptance notes:
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAIL-FAB-WP-0009-T02
|
id: RAIL-FAB-WP-0009-T02
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "751a2fc5-2263-48b0-9ef2-d55e37a0735c"
|
state_hub_task_id: "751a2fc5-2263-48b0-9ef2-d55e37a0735c"
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user