generated from coulomb/repo-seed
Add graph explorer bubble help
This commit is contained in:
@@ -67,6 +67,42 @@ def graph_explorer_page() -> str:
|
||||
}
|
||||
button.primary { background: var(--accent); border-color: var(--accent); color: #ffffff; }
|
||||
button:disabled { color: #98a2b3; cursor: default; }
|
||||
.field-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
.help-tip {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
min-height: 18px;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.help-tip:hover, .help-tip:focus-visible {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
outline: none;
|
||||
}
|
||||
.help-popup {
|
||||
position: fixed;
|
||||
z-index: 30;
|
||||
display: none;
|
||||
max-width: 300px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 16px 40px rgba(23, 32, 51, .16);
|
||||
color: var(--text);
|
||||
padding: 10px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.help-popup strong { display: block; margin-bottom: 4px; }
|
||||
.help-popup p { margin: 0; color: var(--muted); font-size: 12px; line-height: 1.35; }
|
||||
.filter-menu {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
@@ -122,7 +158,10 @@ def graph_explorer_page() -> str:
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
align-items: end;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
max-width: min(460px, calc(100% - 24px));
|
||||
padding: 6px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
@@ -220,48 +259,76 @@ def graph_explorer_page() -> str:
|
||||
}
|
||||
.popup strong { display: block; margin-bottom: 4px; }
|
||||
.notice { color: var(--warn); padding: 14px; }
|
||||
.canvas-notice {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--muted);
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.shell { min-height: 760px; }
|
||||
.toolbar { grid-template-columns: 1fr 1fr; }
|
||||
.main { grid-template-columns: 1fr; grid-template-rows: minmax(420px, 1fr) 330px; }
|
||||
.side { border-left: 0; border-top: 1px solid var(--line); }
|
||||
.map-controls { top: 8px; right: 8px; max-width: calc(100% - 16px); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<section class="shell">
|
||||
<div class="toolbar">
|
||||
<label>Search <input id="search" autocomplete="off"></label>
|
||||
<label>Mode <select id="mode-select"></select></label>
|
||||
<label>Layout <select id="layout-select">
|
||||
<div class="field">
|
||||
<span class="field-label">Search <button type="button" class="help-tip" aria-label="Search help" data-help-title="Search" data-help="Search matches labels, ids, repos, node types, edge types, and descriptions. It hides non-matching nodes and edges without redrawing the layout.">?</button></span>
|
||||
<input id="search" autocomplete="off" aria-label="Search graph">
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="field-label">Mode <button type="button" class="help-tip" aria-label="Mode help" data-help-title="Mode" data-help="Modes are predefined map views. Some modes use the selected item as context; changing modes changes which graph entities are visible, but keeps the layout controls separate.">?</button></span>
|
||||
<select id="mode-select" aria-label="Graph mode"></select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="field-label">Layout <button type="button" class="help-tip" aria-label="Layout help" data-help-title="Layout" data-help="Layout redraws the map arrangement. Cose uses relationship strength and repo affinity; circle, grid, concentric, and breadthfirst are simpler alternate arrangements.">?</button></span>
|
||||
<select id="layout-select" aria-label="Graph layout">
|
||||
<option value="cose">Cose</option>
|
||||
<option value="concentric">Concentric</option>
|
||||
<option value="circle">Circle</option>
|
||||
<option value="grid">Grid</option>
|
||||
<option value="breadthfirst">Breadthfirst</option>
|
||||
</select></label>
|
||||
<div class="field"><span>Node Types</span>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field"><span class="field-label">Node Types <button type="button" class="help-tip" aria-label="Node types help" data-help-title="Node Types" data-help="Nodes are the entities drawn on the map: repositories, services, deployments, servers, capabilities, interfaces, dependencies, bindings, and libraries. This filter hides unchecked node types while preserving the current layout.">?</button></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>
|
||||
<div class="field"><span class="field-label">Edge Types <button type="button" class="help-tip" aria-label="Edge types help" data-help-title="Edge Types" data-help="Edges are relationships between nodes, such as provides, consumes, runs_on, or binds. This filter hides unchecked relationship types while preserving the current layout.">?</button></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>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>
|
||||
<div class="field">
|
||||
<span class="field-label">Review <button type="button" class="help-tip" aria-label="Review help" data-help-title="Review" data-help="Review state applies only to entities that carry review metadata. Accepted means trusted data; candidate means registered or inferred data that still needs context.">?</button></span>
|
||||
<select id="review-filter" aria-label="Review state filter"><option value="">Any</option><option>accepted</option><option>candidate</option></select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="field-label">Unresolved <button type="button" class="help-tip" aria-label="Unresolved help" data-help-title="Unresolved" data-help="Unresolved marks missing provider bindings or registered repositories without accepted graph snapshots. It only applies to node and edge types that can express those gaps.">?</button></span>
|
||||
<select id="unresolved-filter" aria-label="Unresolved filter"><option value="">Any</option><option value="true">Only</option></select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="field-label">Profile <button type="button" class="help-tip" aria-label="Saved views help" data-help-title="Saved Views" data-help="Saved views store the current search, mode, layout, label density, filters, and manual visibility overrides in this browser. Copy State creates a shareable URL for the current view.">?</button></span>
|
||||
<select id="profile-select" aria-label="Saved view profile" disabled><option>Unsaved exploration</option></select>
|
||||
</div>
|
||||
<span id="counts" class="meta"></span>
|
||||
</div>
|
||||
<main class="main">
|
||||
<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" aria-live="polite"><p class="canvas-notice">Loading graph...</p></div>
|
||||
<div class="map-controls" aria-label="Map navigation controls">
|
||||
<label class="map-control-field">Labels
|
||||
<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>
|
||||
<option value="key">Key</option>
|
||||
@@ -269,14 +336,15 @@ def graph_explorer_page() -> str:
|
||||
<option value="none">None</option>
|
||||
</select>
|
||||
</label>
|
||||
<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>
|
||||
<button type="button" data-action="fit" title="Fit graph to view" data-help-title="Fit" data-help="Fit pans and zooms the current visible graph into the map viewport. It does not change filters, labels, or layout.">Fit</button>
|
||||
<button type="button" data-action="focus" title="Focus selected neighborhood" data-help-title="Focus" data-help="Focus narrows the map to the selected entity and its nearby context. It changes visibility, not the underlying registry data.">Focus</button>
|
||||
<button type="button" data-action="clear" class="primary" title="Reset map controls" data-help-title="Reset" data-help="Reset clears search, mode, node and edge type filters, label density, unresolved/review filters, focus, and manual overrides.">Reset</button>
|
||||
</div>
|
||||
<div id="selection-anchor" class="selection-anchor" aria-hidden="true">
|
||||
<span id="selection-anchor-label" class="selection-anchor-label"></span>
|
||||
</div>
|
||||
<div id="popup" class="popup"></div>
|
||||
<div id="help-popup" class="help-popup" role="tooltip"></div>
|
||||
</div>
|
||||
<aside class="side">
|
||||
<section class="section">
|
||||
@@ -291,24 +359,27 @@ def graph_explorer_page() -> str:
|
||||
</section>
|
||||
<section class="section">
|
||||
<div class="button-row">
|
||||
<button type="button" data-override="show">Show</button>
|
||||
<button type="button" data-override="blur">Blur</button>
|
||||
<button type="button" data-override="hide">Hide</button>
|
||||
<button type="button" data-action="reset-overrides">Clear Overrides</button>
|
||||
<button type="button" data-override="show" data-help-title="Show" data-help="Show pins the selected entity visible even when other filters would hide it. It does not redraw the layout.">Show</button>
|
||||
<button type="button" data-override="blur" data-help-title="Blur" data-help="Blur keeps the selected entity in place but reduces emphasis. It is useful for context that should stay visible without competing for attention.">Blur</button>
|
||||
<button type="button" data-override="hide" data-help-title="Hide" data-help="Hide makes the selected entity invisible while preserving layout context. Remove is reserved for the upcoming rule panel and will redraw the graph without the entity.">Hide</button>
|
||||
<button type="button" data-action="reset-overrides" data-help-title="Clear Overrides" data-help="Clear Overrides removes manual Show, Blur, and Hide choices. Filters, mode, layout, and label density stay as they are.">Clear Overrides</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section">
|
||||
<p id="profile-summary" class="meta">Profile persistence unavailable for this host.</p>
|
||||
<label>Profile Name <input id="profile-name" autocomplete="off" disabled></label>
|
||||
<div class="field">
|
||||
<span class="field-label">Profile Name <button type="button" class="help-tip" aria-label="Profile name help" data-help-title="Profile Name" data-help="Profile Name labels a saved browser-local map view. It does not rename graph entities or registry data.">?</button></span>
|
||||
<input id="profile-name" autocomplete="off" aria-label="Profile name" disabled>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button type="button" data-profile-action="save" disabled>Save</button>
|
||||
<button type="button" data-profile-action="duplicate" disabled>Duplicate</button>
|
||||
<button type="button" data-profile-action="delete" disabled>Delete</button>
|
||||
<button type="button" data-profile-action="copy" disabled>Copy State</button>
|
||||
<button type="button" data-profile-action="save" data-help-title="Save" data-help="Save stores the current browser-local view, including filters, layout, label density, and manual overrides." disabled>Save</button>
|
||||
<button type="button" data-profile-action="duplicate" data-help-title="Duplicate" data-help="Duplicate creates a new saved view from the currently loaded profile and current map state." disabled>Duplicate</button>
|
||||
<button type="button" data-profile-action="delete" data-help-title="Delete" data-help="Delete removes the selected browser-local saved view. It does not delete graph data." disabled>Delete</button>
|
||||
<button type="button" data-profile-action="copy" data-help-title="Copy State" data-help="Copy State copies a URL that can restore this map state, including filters and manual overrides." disabled>Copy State</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section">
|
||||
<p id="hidden-summary" class="meta">Hidden 0</p>
|
||||
<p class="meta"><span id="hidden-summary">Hidden 0</span> <button type="button" class="help-tip" aria-label="Hidden entities help" data-help-title="Hidden Entities" data-help="Hidden counts entities currently not shown because of filters, mode, or manual Hide overrides. Hidden keeps layout context; Remove will redraw without the entity in the rule panel.">?</button></p>
|
||||
<div id="legend"></div>
|
||||
</section>
|
||||
</aside>
|
||||
@@ -321,6 +392,7 @@ def graph_explorer_page() -> str:
|
||||
const graphUrl = "/exports/graph-explorer";
|
||||
const canvas = document.getElementById("graph-canvas");
|
||||
const popup = document.getElementById("popup");
|
||||
const helpPopup = document.getElementById("help-popup");
|
||||
const selectionAnchor = document.getElementById("selection-anchor");
|
||||
const selectionAnchorLabel = document.getElementById("selection-anchor-label");
|
||||
const searchInput = document.getElementById("search");
|
||||
@@ -365,6 +437,26 @@ def graph_explorer_page() -> str:
|
||||
.replaceAll("&", "&").replaceAll("<", "<")
|
||||
.replaceAll(">", ">").replaceAll('"', """);
|
||||
|
||||
const showHelp = (target) => {
|
||||
if (!helpPopup || !target?.dataset?.help) return;
|
||||
const bounds = target.getBoundingClientRect();
|
||||
const title = target.dataset.helpTitle || target.textContent || "Help";
|
||||
helpPopup.innerHTML = `<strong>${escapeHtml(title)}</strong><p>${escapeHtml(target.dataset.help)}</p>`;
|
||||
helpPopup.style.display = "block";
|
||||
const width = Math.min(helpPopup.offsetWidth || 300, 300);
|
||||
const left = Math.min(Math.max(12, bounds.left), window.innerWidth - width - 12);
|
||||
const top = bounds.bottom + 8 < window.innerHeight - 80
|
||||
? bounds.bottom + 8
|
||||
: Math.max(12, bounds.top - helpPopup.offsetHeight - 8);
|
||||
helpPopup.style.left = `${left}px`;
|
||||
helpPopup.style.top = `${top}px`;
|
||||
};
|
||||
|
||||
const hideHelp = () => {
|
||||
if (!helpPopup) return;
|
||||
helpPopup.style.display = "none";
|
||||
};
|
||||
|
||||
const elementText = (data) => [
|
||||
data.id, data.stableKey, data.label, data.name, data.description,
|
||||
data.repo, data.domain, data.kind, data.layer, data.edgeType
|
||||
@@ -960,6 +1052,13 @@ def graph_explorer_page() -> str:
|
||||
...element,
|
||||
data: {...element.data, color: layerColors[element.data.layer] || "#667085"}
|
||||
}));
|
||||
if (elements.length === 0) {
|
||||
canvas.innerHTML = "<p class='canvas-notice'>No graph entities were returned by the registry.</p>";
|
||||
counts.textContent = "0 nodes / 0 edges";
|
||||
hiddenSummary.textContent = "Hidden 0";
|
||||
return;
|
||||
}
|
||||
canvas.innerHTML = "";
|
||||
cy = cytoscape({
|
||||
container: canvas,
|
||||
elements,
|
||||
@@ -1158,12 +1257,21 @@ def graph_explorer_page() -> str:
|
||||
}
|
||||
});
|
||||
});
|
||||
document.querySelectorAll("[data-help]").forEach((target) => {
|
||||
target.addEventListener("mouseenter", () => showHelp(target));
|
||||
target.addEventListener("focus", () => showHelp(target));
|
||||
target.addEventListener("mouseleave", hideHelp);
|
||||
target.addEventListener("blur", hideHelp);
|
||||
});
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") hideHelp();
|
||||
});
|
||||
if (!window.cytoscape) {
|
||||
canvas.innerHTML = "<p class='notice'>Cytoscape.js could not be loaded.</p>";
|
||||
return;
|
||||
}
|
||||
boot().catch((error) => {
|
||||
canvas.innerHTML = `<p class='notice'>${escapeHtml(error.message)}</p>`;
|
||||
canvas.innerHTML = `<p class='canvas-notice'>Could not load graph explorer data: ${escapeHtml(error.message)}</p>`;
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -210,11 +210,19 @@ def test_registry_serves_graph_explorer_exports(tmp_path: Path) -> None:
|
||||
assert 'id="node-type-filter"' in page
|
||||
assert 'id="edge-type-filter"' in page
|
||||
assert 'id="selection-anchor"' in page
|
||||
assert 'id="help-popup"' in page
|
||||
assert "updateSelectionAnchor" in page
|
||||
assert "updateLabelVisibility" in page
|
||||
assert "showHelp" in page
|
||||
assert "label-hidden" in page
|
||||
assert "Loading graph..." in page
|
||||
assert "No graph entities were returned by the registry." in page
|
||||
assert "Could not load graph explorer data" in page
|
||||
assert "Node Types" in page
|
||||
assert "Edge Types" in page
|
||||
assert 'data-help-title="Node Types"' in page
|
||||
assert 'data-help-title="Saved Views"' in page
|
||||
assert "Remove will redraw" in page
|
||||
assert "Registered only" in page
|
||||
assert 'id="layer-filter"' not in page
|
||||
assert '"border-width": 4' not in page
|
||||
|
||||
@@ -123,7 +123,7 @@ Acceptance notes:
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0009-T03
|
||||
status: in_progress
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "3e60397c-c833-4bd7-ba1b-b754b203dade"
|
||||
```
|
||||
@@ -176,7 +176,7 @@ Acceptance notes:
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0009-T05
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "67a9cbc9-ebaa-4cb1-bec9-46bf250faf41"
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user