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);
|
box-shadow: 0 12px 30px rgba(23, 32, 51, .12);
|
||||||
}
|
}
|
||||||
.map-controls button { min-width: 54px; }
|
.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; }
|
.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 {
|
||||||
@@ -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="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="clear" class="primary" title="Reset map controls">Reset</button>
|
||||||
</div>
|
</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="popup" class="popup"></div>
|
||||||
</div>
|
</div>
|
||||||
<aside class="side">
|
<aside class="side">
|
||||||
@@ -248,6 +300,8 @@ def graph_explorer_page() -> str:
|
|||||||
const graphUrl = "/exports/graph-explorer";
|
const graphUrl = "/exports/graph-explorer";
|
||||||
const canvas = document.getElementById("graph-canvas");
|
const canvas = document.getElementById("graph-canvas");
|
||||||
const popup = document.getElementById("popup");
|
const popup = document.getElementById("popup");
|
||||||
|
const selectionAnchor = document.getElementById("selection-anchor");
|
||||||
|
const selectionAnchorLabel = document.getElementById("selection-anchor-label");
|
||||||
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");
|
||||||
@@ -272,6 +326,7 @@ def graph_explorer_page() -> str:
|
|||||||
const profileStorageKey = "railiance.fabric.graphExplorer.profiles";
|
const profileStorageKey = "railiance.fabric.graphExplorer.profiles";
|
||||||
let cy = null;
|
let cy = null;
|
||||||
let selected = null;
|
let selected = null;
|
||||||
|
let selectedAnchor = null;
|
||||||
let focusSet = null;
|
let focusSet = null;
|
||||||
let manualOverrides = {};
|
let manualOverrides = {};
|
||||||
let layerColors = {};
|
let layerColors = {};
|
||||||
@@ -354,6 +409,59 @@ def graph_explorer_page() -> str:
|
|||||||
}).join("");
|
}).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 = () => ({
|
const currentViewState = () => ({
|
||||||
search: searchInput.value,
|
search: searchInput.value,
|
||||||
mode: modeSelect.value || "full",
|
mode: modeSelect.value || "full",
|
||||||
@@ -603,6 +711,7 @@ def graph_explorer_page() -> str:
|
|||||||
const hidden = cy.elements().length - visibleNodes - visibleEdges;
|
const hidden = cy.elements().length - visibleNodes - visibleEdges;
|
||||||
counts.textContent = `${visibleNodes} nodes / ${visibleEdges} edges`;
|
counts.textContent = `${visibleNodes} nodes / ${visibleEdges} edges`;
|
||||||
hiddenSummary.textContent = `Hidden ${hidden}`;
|
hiddenSummary.textContent = `Hidden ${hidden}`;
|
||||||
|
updateSelectionAnchor();
|
||||||
};
|
};
|
||||||
|
|
||||||
const showDetails = (element) => {
|
const showDetails = (element) => {
|
||||||
@@ -614,6 +723,7 @@ def graph_explorer_page() -> str:
|
|||||||
detailList.innerHTML = "";
|
detailList.innerHTML = "";
|
||||||
orientationTitle.textContent = "Select a service, interface, dependency, or registered-only repo.";
|
orientationTitle.textContent = "Select a service, interface, dependency, or registered-only repo.";
|
||||||
orientationList.innerHTML = "";
|
orientationList.innerHTML = "";
|
||||||
|
updateSelectionAnchor();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = element.data();
|
const data = element.data();
|
||||||
@@ -639,6 +749,7 @@ def graph_explorer_page() -> str:
|
|||||||
.map(([key, value]) => `<li><strong>${escapeHtml(key)}</strong> ${escapeHtml(value)}</li>`)
|
.map(([key, value]) => `<li><strong>${escapeHtml(key)}</strong> ${escapeHtml(value)}</li>`)
|
||||||
.join("");
|
.join("");
|
||||||
renderOrientation(element);
|
renderOrientation(element);
|
||||||
|
updateSelectionAnchor();
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderOrientation = (element) => {
|
const renderOrientation = (element) => {
|
||||||
@@ -751,6 +862,7 @@ def graph_explorer_page() -> str:
|
|||||||
}
|
}
|
||||||
: {name, padding: 48, animate: false};
|
: {name, padding: 48, animate: false};
|
||||||
cy.layout(options).run();
|
cy.layout(options).run();
|
||||||
|
updateSelectionAnchor();
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderLegend = (layers) => {
|
const renderLegend = (layers) => {
|
||||||
@@ -809,6 +921,16 @@ def graph_explorer_page() -> str:
|
|||||||
"text-max-width": 130,
|
"text-max-width": 130,
|
||||||
"width": "data(visualSize)"
|
"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: {
|
{selector: "edge", style: {
|
||||||
"curve-style": "bezier",
|
"curve-style": "bezier",
|
||||||
"line-color": "#98a2b3",
|
"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 = 'strong']", style: {"line-color": "#344054", "target-arrow-color": "#344054"}},
|
||||||
{selector: "edge[strength = 'weak']", style: {"line-style": "dotted"}},
|
{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", style: {"opacity": .24, "label": ""}},
|
||||||
{selector: ".display-blur:selected, .display-blur.hover", style: {"opacity": .78, "label": "data(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", "node, edge", (event) => showDetails(event.target));
|
||||||
cy.on("tap", (event) => { if (event.target === cy) showDetails(null); });
|
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) => {
|
cy.on("mouseover", "node, edge", (event) => {
|
||||||
event.target.addClass("hover");
|
event.target.addClass("hover");
|
||||||
const data = event.target.data();
|
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="layout-select"' in page
|
||||||
assert 'id="node-type-filter"' in page
|
assert 'id="node-type-filter"' in page
|
||||||
assert 'id="edge-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 "Node Types" in page
|
||||||
assert "Edge Types" in page
|
assert "Edge Types" in page
|
||||||
assert 'id="layer-filter"' not 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-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
|
||||||
|
|||||||
Reference in New Issue
Block a user