generated from coulomb/repo-seed
Add graph selection anchors
This commit is contained in:
@@ -129,6 +129,55 @@ def graph_explorer_page() -> str:
|
||||
box-shadow: 0 12px 30px rgba(23, 32, 51, .12);
|
||||
}
|
||||
.map-controls button { min-width: 54px; }
|
||||
.selection-anchor {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
transform: translate(-50%, -100%);
|
||||
}
|
||||
.selection-anchor::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -1px;
|
||||
top: -36px;
|
||||
width: 2px;
|
||||
height: 28px;
|
||||
border-radius: 999px;
|
||||
background: #111827;
|
||||
box-shadow: 0 1px 2px rgba(23, 32, 51, .28);
|
||||
}
|
||||
.selection-anchor::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -6px;
|
||||
top: -10px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 10px solid #111827;
|
||||
filter: drop-shadow(0 1px 1px rgba(23, 32, 51, .24));
|
||||
}
|
||||
.selection-anchor-label {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: -48px;
|
||||
max-width: 220px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, .96);
|
||||
box-shadow: 0 12px 30px rgba(23, 32, 51, .12);
|
||||
color: var(--text);
|
||||
font-size: 12px;
|
||||
line-height: 1.25;
|
||||
padding: 6px 8px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.button-row { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.meta { color: var(--muted); font-size: 12px; margin: 0; }
|
||||
.pill {
|
||||
@@ -203,6 +252,9 @@ def graph_explorer_page() -> str:
|
||||
<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="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>
|
||||
<aside class="side">
|
||||
@@ -248,6 +300,8 @@ def graph_explorer_page() -> str:
|
||||
const graphUrl = "/exports/graph-explorer";
|
||||
const canvas = document.getElementById("graph-canvas");
|
||||
const popup = document.getElementById("popup");
|
||||
const selectionAnchor = document.getElementById("selection-anchor");
|
||||
const selectionAnchorLabel = document.getElementById("selection-anchor-label");
|
||||
const searchInput = document.getElementById("search");
|
||||
const modeSelect = document.getElementById("mode-select");
|
||||
const layoutSelect = document.getElementById("layout-select");
|
||||
@@ -272,6 +326,7 @@ def graph_explorer_page() -> str:
|
||||
const profileStorageKey = "railiance.fabric.graphExplorer.profiles";
|
||||
let cy = null;
|
||||
let selected = null;
|
||||
let selectedAnchor = null;
|
||||
let focusSet = null;
|
||||
let manualOverrides = {};
|
||||
let layerColors = {};
|
||||
@@ -354,6 +409,59 @@ def graph_explorer_page() -> str:
|
||||
}).join("");
|
||||
};
|
||||
|
||||
const isFinitePoint = (position) =>
|
||||
position && Number.isFinite(position.x) && Number.isFinite(position.y);
|
||||
|
||||
const elementLabel = (element) => {
|
||||
if (!element) return "";
|
||||
const data = element.data();
|
||||
return data.name || data.label || data.edgeType || data.id;
|
||||
};
|
||||
|
||||
const renderedElementPosition = (element) => {
|
||||
if (!element || !cy) return null;
|
||||
if (element.isNode && element.isNode()) return element.renderedPosition();
|
||||
if (element.isEdge && element.isEdge()) {
|
||||
if (typeof element.renderedMidpoint === "function") {
|
||||
const midpoint = element.renderedMidpoint();
|
||||
if (isFinitePoint(midpoint)) return midpoint;
|
||||
}
|
||||
const source = cy.getElementById(element.data("source"));
|
||||
const target = cy.getElementById(element.data("target"));
|
||||
if (source.length > 0 && target.length > 0) {
|
||||
const sourcePosition = source.renderedPosition();
|
||||
const targetPosition = target.renderedPosition();
|
||||
if (isFinitePoint(sourcePosition) && isFinitePoint(targetPosition)) {
|
||||
return {
|
||||
x: (sourcePosition.x + targetPosition.x) / 2,
|
||||
y: (sourcePosition.y + targetPosition.y) / 2,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const updateSelectionAnchor = () => {
|
||||
if (!selected || !selectionAnchor) {
|
||||
selectedAnchor = null;
|
||||
selectionAnchor.style.display = "none";
|
||||
return;
|
||||
}
|
||||
const position = renderedElementPosition(selected);
|
||||
if (isFinitePoint(position)) {
|
||||
selectedAnchor = {x: position.x, y: position.y, label: elementLabel(selected)};
|
||||
}
|
||||
if (!selectedAnchor) {
|
||||
selectionAnchor.style.display = "none";
|
||||
return;
|
||||
}
|
||||
selectionAnchor.style.left = `${selectedAnchor.x}px`;
|
||||
selectionAnchor.style.top = `${selectedAnchor.y}px`;
|
||||
selectionAnchorLabel.textContent = selectedAnchor.label;
|
||||
selectionAnchor.style.display = "block";
|
||||
};
|
||||
|
||||
const currentViewState = () => ({
|
||||
search: searchInput.value,
|
||||
mode: modeSelect.value || "full",
|
||||
@@ -603,6 +711,7 @@ def graph_explorer_page() -> str:
|
||||
const hidden = cy.elements().length - visibleNodes - visibleEdges;
|
||||
counts.textContent = `${visibleNodes} nodes / ${visibleEdges} edges`;
|
||||
hiddenSummary.textContent = `Hidden ${hidden}`;
|
||||
updateSelectionAnchor();
|
||||
};
|
||||
|
||||
const showDetails = (element) => {
|
||||
@@ -614,6 +723,7 @@ def graph_explorer_page() -> str:
|
||||
detailList.innerHTML = "";
|
||||
orientationTitle.textContent = "Select a service, interface, dependency, or registered-only repo.";
|
||||
orientationList.innerHTML = "";
|
||||
updateSelectionAnchor();
|
||||
return;
|
||||
}
|
||||
const data = element.data();
|
||||
@@ -639,6 +749,7 @@ def graph_explorer_page() -> str:
|
||||
.map(([key, value]) => `<li><strong>${escapeHtml(key)}</strong> ${escapeHtml(value)}</li>`)
|
||||
.join("");
|
||||
renderOrientation(element);
|
||||
updateSelectionAnchor();
|
||||
};
|
||||
|
||||
const renderOrientation = (element) => {
|
||||
@@ -751,6 +862,7 @@ def graph_explorer_page() -> str:
|
||||
}
|
||||
: {name, padding: 48, animate: false};
|
||||
cy.layout(options).run();
|
||||
updateSelectionAnchor();
|
||||
};
|
||||
|
||||
const renderLegend = (layers) => {
|
||||
@@ -809,6 +921,16 @@ def graph_explorer_page() -> str:
|
||||
"text-max-width": 130,
|
||||
"width": "data(visualSize)"
|
||||
}},
|
||||
{selector: "node[layer = 'repository']", style: {"shape": "round-rectangle"}},
|
||||
{selector: "node[layer = 'server']", style: {"shape": "pentagon"}},
|
||||
{selector: "node[layer = 'deployment']", style: {"shape": "diamond"}},
|
||||
{selector: "node[layer = 'service']", style: {"shape": "hexagon"}},
|
||||
{selector: "node[layer = 'capability']", style: {"shape": "vee"}},
|
||||
{selector: "node[layer = 'interface']", style: {"shape": "rectangle"}},
|
||||
{selector: "node[layer = 'dependency']", style: {"shape": "triangle"}},
|
||||
{selector: "node[layer = 'binding']", style: {"shape": "rhomboid"}},
|
||||
{selector: "node[unresolved = true]", style: {"border-color": "#b45309", "border-style": "dashed", "border-width": 3}},
|
||||
{selector: "node[lifecycle = 'registered-only']", style: {"border-color": "#be123c", "border-style": "dashed", "border-width": 3}},
|
||||
{selector: "edge", style: {
|
||||
"curve-style": "bezier",
|
||||
"line-color": "#98a2b3",
|
||||
@@ -818,13 +940,23 @@ def graph_explorer_page() -> str:
|
||||
}},
|
||||
{selector: "edge[strength = 'strong']", style: {"line-color": "#344054", "target-arrow-color": "#344054"}},
|
||||
{selector: "edge[strength = 'weak']", style: {"line-style": "dotted"}},
|
||||
{selector: "edge.hover", style: {
|
||||
"label": "data(edgeType)",
|
||||
"font-size": 10,
|
||||
"text-background-color": "#ffffff",
|
||||
"text-background-opacity": .9,
|
||||
"text-background-padding": 2,
|
||||
"z-index": 2
|
||||
}},
|
||||
{selector: ".display-blur", style: {"opacity": .24, "label": ""}},
|
||||
{selector: ".display-blur:selected, .display-blur.hover", style: {"opacity": .78, "label": "data(label)"}},
|
||||
{selector: ":selected", style: {"border-width": 4, "border-color": "#111827", "line-color": "#111827", "target-arrow-color": "#111827"}}
|
||||
{selector: ":selected", style: {"overlay-opacity": 0}}
|
||||
]
|
||||
});
|
||||
cy.on("tap", "node, edge", (event) => showDetails(event.target));
|
||||
cy.on("tap", (event) => { if (event.target === cy) showDetails(null); });
|
||||
cy.on("pan zoom resize render layoutstop", updateSelectionAnchor);
|
||||
cy.on("position", "node", updateSelectionAnchor);
|
||||
cy.on("mouseover", "node, edge", (event) => {
|
||||
event.target.addClass("hover");
|
||||
const data = event.target.data();
|
||||
|
||||
@@ -208,9 +208,12 @@ def test_registry_serves_graph_explorer_exports(tmp_path: Path) -> None:
|
||||
assert 'id="layout-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 "Node Types" in page
|
||||
assert "Edge Types" in page
|
||||
assert 'id="layer-filter"' not in page
|
||||
assert '"border-width": 4' not in page
|
||||
assert 'id="profile-select"' in page
|
||||
assert 'id="profile-name"' in page
|
||||
assert 'id="orientation-list"' in page
|
||||
|
||||
Reference in New Issue
Block a user