Add modal graph node inspection
This commit is contained in:
parent
5f3d938adc
commit
4c82e1643d
13 changed files with 179 additions and 25 deletions
|
|
@ -16,7 +16,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|||
from .errors import DocForgeError
|
||||
from .index import APPLICATION_ID, INDEX_SCHEMA_VERSION, ProjectIndex, re_tokenize
|
||||
|
||||
VISUALIZATION_TEMPLATE = "graph-browser@2"
|
||||
VISUALIZATION_TEMPLATE = "graph-browser@3"
|
||||
DEFAULT_EDGE_LIMIT = 100
|
||||
MAX_EDGE_LIMIT = 400
|
||||
|
||||
|
|
@ -711,6 +711,34 @@ _GRAPH_BROWSER_HTML = r"""<!doctype html>
|
|||
padding: 12px; border: 1px solid var(--line); border-radius: 9px;
|
||||
background: #07111c; color: #d6e6f5;
|
||||
}
|
||||
dialog {
|
||||
width: min(760px, calc(100vw - 32px)); max-height: min(780px, calc(100vh - 32px));
|
||||
padding: 0; overflow: hidden; border: 1px solid #36536e; border-radius: 14px;
|
||||
background: var(--panel); color: var(--text);
|
||||
box-shadow: 0 24px 80px rgba(0, 0, 0, .6);
|
||||
}
|
||||
dialog::backdrop { background: rgba(2, 8, 14, .78); backdrop-filter: blur(3px); }
|
||||
.dialog-shell {
|
||||
display: grid; grid-template-rows: auto minmax(0, 1fr) auto; max-height: inherit;
|
||||
}
|
||||
.dialog-head {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
||||
padding: 12px 16px; border-bottom: 1px solid var(--line); background: var(--panel-2);
|
||||
}
|
||||
.dialog-head strong { font-size: 15px; }
|
||||
.dialog-close {
|
||||
width: 34px; height: 34px; border: 1px solid var(--line); border-radius: 8px;
|
||||
background: #102b3d; color: var(--text); font-size: 21px; line-height: 1;
|
||||
}
|
||||
.dialog-close:hover, .dialog-close:focus-visible {
|
||||
border-color: var(--accent); outline: 2px solid transparent;
|
||||
}
|
||||
.dialog-body { min-height: 0; overflow: auto; padding: 18px; }
|
||||
.dialog-body pre { max-height: none; }
|
||||
.dialog-actions {
|
||||
display: flex; justify-content: flex-end; gap: 8px; padding: 12px 16px;
|
||||
border-top: 1px solid var(--line); background: var(--panel-2);
|
||||
}
|
||||
.error { color: #ff9aac; }
|
||||
@media (max-width: 980px) {
|
||||
.layout { grid-template-columns: 240px 1fr; }
|
||||
|
|
@ -761,13 +789,31 @@ _GRAPH_BROWSER_HTML = r"""<!doctype html>
|
|||
<svg id="graph" viewBox="-600 -410 1200 820"
|
||||
role="img" aria-label="Node neighborhood"></svg>
|
||||
<div class="empty" id="empty">Search for a node to inspect its neighborhood.</div>
|
||||
<div class="viewport-hint">Mouse wheel to zoom · left-drag to pan</div>
|
||||
<div class="viewport-hint">
|
||||
Click node to inspect · mouse wheel to zoom · left-drag to pan
|
||||
</div>
|
||||
</main>
|
||||
<aside class="right">
|
||||
<div id="details"><p class="summary">Choose a search result or graph node.</p></div>
|
||||
<div id="details">
|
||||
<p class="summary">Choose a search result to load its neighborhood.</p>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
<dialog id="node-dialog" aria-labelledby="node-dialog-label">
|
||||
<div class="dialog-shell">
|
||||
<div class="dialog-head">
|
||||
<strong id="node-dialog-label">Inspect node</strong>
|
||||
<button class="dialog-close" id="close-node-dialog" type="button"
|
||||
aria-label="Close node inspection">×</button>
|
||||
</div>
|
||||
<div class="dialog-body" id="node-dialog-details"></div>
|
||||
<div class="dialog-actions">
|
||||
<button class="button" id="explore-node" type="button">Explore neighborhood</button>
|
||||
<button class="button" id="dismiss-node-dialog" type="button">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<script>
|
||||
const base = location.pathname.replace(/\/?$/, "/");
|
||||
const defaultViewport = Object.freeze({x: -600, y: -410, width: 1200, height: 820});
|
||||
|
|
@ -780,6 +826,7 @@ _GRAPH_BROWSER_HTML = r"""<!doctype html>
|
|||
viewport: {...defaultViewport},
|
||||
pointer: null,
|
||||
suppressClick: false,
|
||||
inspectedNode: null,
|
||||
};
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const api = async (path) => {
|
||||
|
|
@ -934,17 +981,19 @@ _GRAPH_BROWSER_HTML = r"""<!doctype html>
|
|||
family.textContent = short(node.family, 22);
|
||||
group.append(title, family);
|
||||
group.addEventListener("click", () => {
|
||||
if (!state.suppressClick) loadNode(node.node_id);
|
||||
if (!state.suppressClick) inspectNode(node.node_id);
|
||||
});
|
||||
group.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter" || event.key === " ") loadNode(node.node_id);
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
inspectNode(node.node_id);
|
||||
}
|
||||
});
|
||||
nodeLayer.append(group);
|
||||
}
|
||||
svg.append(edgeLayer, nodeLayer);
|
||||
}
|
||||
function renderDetails(node, data) {
|
||||
const details = $("details");
|
||||
function renderDetails(details, node, data) {
|
||||
details.replaceChildren();
|
||||
const heading = document.createElement("div");
|
||||
heading.className = "detail-head";
|
||||
|
|
@ -981,6 +1030,26 @@ _GRAPH_BROWSER_HTML = r"""<!doctype html>
|
|||
content.textContent = node.content;
|
||||
details.append(heading, badges, summary, dl, content);
|
||||
}
|
||||
function closeNodeDialog() {
|
||||
const dialog = $("node-dialog");
|
||||
if (dialog.open) dialog.close();
|
||||
else state.inspectedNode = null;
|
||||
}
|
||||
async function inspectNode(nodeId) {
|
||||
try {
|
||||
setStatus(`Inspecting ${nodeId}…`);
|
||||
const params = new URLSearchParams({id: nodeId, depth: String(state.depth), limit: "100"});
|
||||
const data = await api(`node?${params}`);
|
||||
state.inspectedNode = nodeId;
|
||||
renderDetails($("node-dialog-details"), data.node, data);
|
||||
const dialog = $("node-dialog");
|
||||
if (!dialog.open) dialog.showModal();
|
||||
$("close-node-dialog").focus();
|
||||
setStatus(`Inspecting ${nodeId}`);
|
||||
} catch (error) {
|
||||
setStatus(error.message, true);
|
||||
}
|
||||
}
|
||||
async function search() {
|
||||
const params = new URLSearchParams({
|
||||
q: $("search").value.trim(),
|
||||
|
|
@ -1002,7 +1071,7 @@ _GRAPH_BROWSER_HTML = r"""<!doctype html>
|
|||
const params = new URLSearchParams({id: nodeId, depth: String(state.depth), limit: "100"});
|
||||
const data = await api(`node?${params}`);
|
||||
renderGraph(data);
|
||||
renderDetails(data.node, data);
|
||||
renderDetails($("details"), data.node, data);
|
||||
setStatus(`${data.nodes.length} nodes · ${data.edges.length} edges in neighborhood`);
|
||||
history.replaceState(null, "", `?node=${encodeURIComponent(nodeId)}&depth=${state.depth}`);
|
||||
} catch (error) {
|
||||
|
|
@ -1014,6 +1083,28 @@ _GRAPH_BROWSER_HTML = r"""<!doctype html>
|
|||
$("zoom-in").addEventListener("click", () => zoomAt(.8));
|
||||
$("zoom-out").addEventListener("click", () => zoomAt(1.25));
|
||||
$("reset-view").addEventListener("click", resetViewport);
|
||||
$("close-node-dialog").addEventListener("click", closeNodeDialog);
|
||||
$("dismiss-node-dialog").addEventListener("click", closeNodeDialog);
|
||||
$("explore-node").addEventListener("click", async () => {
|
||||
const nodeId = state.inspectedNode;
|
||||
closeNodeDialog();
|
||||
if (nodeId) await loadNode(nodeId);
|
||||
});
|
||||
$("node-dialog").addEventListener("click", (event) => {
|
||||
if (event.target !== $("node-dialog")) return;
|
||||
const bounds = $("node-dialog").getBoundingClientRect();
|
||||
const inside = event.clientX >= bounds.left && event.clientX <= bounds.right
|
||||
&& event.clientY >= bounds.top && event.clientY <= bounds.bottom;
|
||||
if (!inside) closeNodeDialog();
|
||||
});
|
||||
$("node-dialog").addEventListener("close", () => {
|
||||
state.inspectedNode = null;
|
||||
if (state.graph) {
|
||||
const nodeCount = state.graph.nodes.length;
|
||||
const edgeCount = state.graph.edges.length;
|
||||
setStatus(`${nodeCount} nodes · ${edgeCount} edges in neighborhood`);
|
||||
}
|
||||
});
|
||||
$("graph").addEventListener("wheel", (event) => {
|
||||
event.preventDefault();
|
||||
zoomAt(event.deltaY < 0 ? .85 : 1.18, event.clientX, event.clientY);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue