Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Index a repo first. The architecture tab is the full typed graph plus `loadpath.

### Impact graph

Toggle **This review** (impact subgraph) vs **Indexed architecture** (the repo map). Dashed edges are inferred (URL/Zod overlap); solid edges are extracted or generated-client stitches. 2D is the default; 3D is available when WebGL is.
Toggle **This review** (impact subgraph) vs **Indexed architecture** (the repo map). Dashed edges are inferred (URL/Zod overlap); solid edges are extracted or generated-client stitches. 2D is the default. 3D uses the same layout algorithms, puts bounded context on the depth axis, and is available when WebGL is.

![Impact graph, this review](docs/screenshots/graph.png)

Expand Down
4,116 changes: 0 additions & 4,116 deletions src/loadpath/static/assets/LayeredGraph3D-BCjRMvyT.js

This file was deleted.

4,116 changes: 4,116 additions & 0 deletions src/loadpath/static/assets/LayeredGraph3D-DIGrA9CU.js

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions src/loadpath/static/assets/index-CcW4ogPp.js

Large diffs are not rendered by default.

62 changes: 0 additions & 62 deletions src/loadpath/static/assets/index-DuTMxUNT.js

This file was deleted.

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/loadpath/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
<script type="module" crossorigin src="./assets/index-DuTMxUNT.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-CCz64vFJ.css">
<script type="module" crossorigin src="./assets/index-CcW4ogPp.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-Q9TpSg-b.css">
</head>
<body>
<div id="root"></div>
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/test_ui_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ def test_ui_index_review_graph_copy_and_workspace(live_app, browser_page):
page.get_by_test_id("graph-view-3d").click()
assert page.get_by_test_id("graph-view-3d").get_attribute("aria-pressed") == "true"
page.get_by_test_id("graph-3d").wait_for(timeout=15_000)
assert page.get_by_test_id("graph-layout").count() == 0
page.get_by_test_id("graph-layout").wait_for()
page.locator("[data-testid='graph-3d-canvas'], [data-testid='graph-3d-fallback']").first.wait_for(timeout=20_000)
page.get_by_test_id("graph-view-2d").click()
page.locator(".react-flow__node").first.wait_for(timeout=15_000)
Expand Down
4 changes: 3 additions & 1 deletion tests/e2e/test_ui_screenshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ def test_ui_review_graph_prs_settings(live_app, tmp_path: Path, browser_page):
page.wait_for_timeout(800)
_shot(page, dest, "architecture.png")

page.get_by_test_id("btn-review").wait_for(state="visible")
page.wait_for_function("() => !document.querySelector('[data-testid=\"btn-review\"]')?.disabled")
with page.expect_response(
lambda r: "/api/review" in r.url and r.request.method == "POST",
timeout=60_000,
Expand All @@ -113,8 +115,8 @@ def test_ui_review_graph_prs_settings(live_app, tmp_path: Path, browser_page):
error = page.locator(".error")
if error.count() and error.inner_text().strip():
pytest.fail(error.inner_text())
page.get_by_test_id("brief").locator(".level").wait_for(timeout=30_000)
page.get_by_test_id("merge-box").wait_for(timeout=15_000)
page.get_by_test_id("brief").locator(".level").wait_for(timeout=15_000)
brief = page.get_by_test_id("brief").inner_text()
assert "MEDIUM" in brief or "LOW" in brief or "HIGH" in brief
_wait_graph(page)
Expand Down
133 changes: 93 additions & 40 deletions ui/src/ImpactGraph.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { lazy, Suspense, useEffect, useMemo, useState } from "react";
import { Component, lazy, Suspense, useEffect, useMemo, useState, type ReactNode } from "react";
import {
Background,
BaseEdge,
Expand All @@ -22,13 +22,15 @@ import {
GRAPH_LAYOUTS,
defaultDetail,
defaultProjection,
effectiveProjection,
familyFor,
isolatePathIds,
layoutGraph,
layoutUsesColumns,
readGraphLayout,
searchNodes,
visibleGraph,
webglAvailable,
writeGraphLayout,
type GraphDetail,
type GraphFamily,
Expand All @@ -50,6 +52,22 @@ const LayeredGraph3D = lazy(() =>
import("./LayeredGraph3D").then((mod) => ({ default: mod.LayeredGraph3D })),
);

class GraphBoundary extends Component<{ children: ReactNode; fallback: ReactNode }, { failed: boolean }> {
state = { failed: false };
static getDerivedStateFromError() {
return { failed: true };
}
render() {
return this.state.failed ? this.props.fallback : this.props.children;
}
}

const WEBGL_FALLBACK = (
<p className="muted graph-3d-hint" data-testid="graph-3d-fallback">
WebGL is unavailable in this browser, so the 3D view cannot start. Switch back to 2D map.
</p>
);

const WEIGHT_COLOR: Record<string, string> = {
cheap: "var(--edge-cheap)",
expensive: "var(--edge-expensive)",
Expand Down Expand Up @@ -455,7 +473,7 @@ function InspectorLinks({
);
}

export function ImpactGraph({
function ImpactGraphView({
nodes,
edges,
onWhatIf,
Expand Down Expand Up @@ -499,10 +517,22 @@ export function ImpactGraph({
const [neighborhoodOnly, setNeighborhoodOnly] = useState(false);
const [query, setQuery] = useState("");
const [hitsOpen, setHitsOpen] = useState(false);
const [webgl, setWebgl] = useState<boolean | null>(null);
const reduceMotion =
typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;

const view = projection ?? defaultProjection(nodes.length);
useEffect(() => {
const probe = () => setWebgl(webglAvailable());
if (typeof window.requestIdleCallback === "function") {
const idle = window.requestIdleCallback(probe);
return () => window.cancelIdleCallback(idle);
}
const timer = window.setTimeout(probe, 0);
return () => window.clearTimeout(timer);
}, []);

const preferred = projection ?? defaultProjection(nodes.length);
const view = effectiveProjection(preferred, projection, webgl);
const level = detail ?? defaultDetail(nodes.length);
const neighborhoodFocus = neighborhoodOnly ? selectedId : null;
const isolated = useMemo(
Expand Down Expand Up @@ -656,29 +686,27 @@ export function ImpactGraph({
</button>
))}
</div>
{view === "2d" ? (
<label className="graph-layout">
Layout
<select
id="graph-layout"
data-testid="graph-layout"
value={layout}
aria-label="2D layout algorithm"
onChange={(event) => {
const next = GRAPH_LAYOUTS.find((item) => item.id === event.target.value)?.id;
if (!next) return;
setLayout(next);
writeGraphLayout(next);
}}
>
{GRAPH_LAYOUTS.map((item) => (
<option key={item.id} value={item.id}>
{item.label}
</option>
))}
</select>
</label>
) : null}
<label className="graph-layout">
Layout
<select
id="graph-layout"
data-testid="graph-layout"
value={layout}
aria-label="Graph layout algorithm"
onChange={(event) => {
const next = GRAPH_LAYOUTS.find((item) => item.id === event.target.value)?.id;
if (!next) return;
setLayout(next);
writeGraphLayout(next);
}}
>
{GRAPH_LAYOUTS.map((item) => (
<option key={item.id} value={item.id}>
{item.label}
</option>
))}
</select>
</label>
<button
type="button"
className={neighborhoodOnly ? "chip-btn active" : "chip-btn"}
Expand Down Expand Up @@ -749,21 +777,32 @@ export function ImpactGraph({
) : view === "3d" ? (
<div className="graph-3d" data-testid="graph-3d">
<p className="graph-3d-hint">
Architecture layers are stacked in depth (Django → stitch → React). Drag to orbit, scroll to
zoom, click a node to inspect it.
Same layout as the 2D map, with bounded context on the depth axis. Dashed edges are inferred.
Drag to orbit, scroll to zoom, click a node to inspect it.
</p>
<Suspense fallback={<p className="muted graph-3d-hint">Loading 3D layers…</p>}>
<LayeredGraph3D
nodes={visible.nodes}
edges={visible.edges}
selectedId={selectedId}
neighborIds={neighborhoodFocus ? visible.neighborIds : NO_NEIGHBORS}
onSelect={(id) => {
setSelectedId(id);
if (!id) setNeighborhoodOnly(false);
}}
/>
</Suspense>
{webgl === false ? (
WEBGL_FALLBACK
) : webgl === null ? (
<p className="muted graph-3d-hint">Loading 3D layers…</p>
) : (
<GraphBoundary fallback={WEBGL_FALLBACK}>
<Suspense fallback={<p className="muted graph-3d-hint">Loading 3D layers…</p>}>
<LayeredGraph3D
nodes={visible.nodes}
edges={visible.edges}
selectedId={selectedId}
neighborIds={neighborhoodFocus ? visible.neighborIds : NO_NEIGHBORS}
layout={layout}
nodeRoles={nodeRoles}
testOverlay={testOverlay}
onSelect={(id) => {
setSelectedId(id);
if (!id) setNeighborhoodOnly(false);
}}
/>
</Suspense>
</GraphBoundary>
)}
{inspector}
</div>
) : (
Expand Down Expand Up @@ -808,3 +847,17 @@ export function ImpactGraph({
</div>
);
}

export function ImpactGraph(props: Parameters<typeof ImpactGraphView>[0]) {
return (
<GraphBoundary
fallback={
<p className="muted graph-3d-hint" data-testid="graph-crash-fallback">
The graph failed to render. Switch to 2D map or another layout.
</p>
}
>
<ImpactGraphView {...props} />
</GraphBoundary>
);
}
Loading
Loading