From d1e3adcb3c96f07e79cf5fbdb2866886df558327 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Mon, 2 Feb 2026 19:20:53 +0100 Subject: [PATCH] Fix blank portal: add root element heights, use dagre for graph layout - Set explicit height on html/body/#root so React Flow container renders - Replace @dagrejs/dagre (broken CJS dynamic require) with dagre v0.8.5 - Hierarchical top-down layout with dagre, arrow markers on edges - Move React Flow controls to top-right to avoid floating button overlap --- portal/index.html | 6 +-- portal/package-lock.json | 33 ++++++++++++ portal/package.json | 6 ++- portal/src/App.tsx | 4 +- portal/src/components/GraphView.tsx | 81 +++++++++++++++++++++-------- 5 files changed, 101 insertions(+), 29 deletions(-) diff --git a/portal/index.html b/portal/index.html index 4e8b875..3d92ea6 100644 --- a/portal/index.html +++ b/portal/index.html @@ -1,12 +1,12 @@ - + Cortex Portal - -
+ +
diff --git a/portal/package-lock.json b/portal/package-lock.json index 3607ab1..7f2f138 100644 --- a/portal/package-lock.json +++ b/portal/package-lock.json @@ -9,7 +9,9 @@ "version": "1.0.0", "dependencies": { "@tanstack/react-query": "^5.62.0", + "@types/dagre": "^0.7.53", "@xyflow/react": "^12.4.0", + "dagre": "^0.8.5", "react": "^19.0.0", "react-dom": "^19.0.0" }, @@ -1546,6 +1548,12 @@ "@types/d3-selection": "*" } }, + "node_modules/@types/dagre": { + "version": "0.7.53", + "resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.53.tgz", + "integrity": "sha512-f4gkWqzPZvYmKhOsDnhq/R8mO4UMcKdxZo+i5SCkOU1wvGeHJeUXGIHeE9pnwGyPMDof1Vx5ZQo4nxpeg2TTVQ==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1816,6 +1824,16 @@ "node": ">=12" } }, + "node_modules/dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "license": "MIT", + "dependencies": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1967,6 +1985,15 @@ "dev": true, "license": "ISC" }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -2271,6 +2298,12 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", diff --git a/portal/package.json b/portal/package.json index c07ac44..6284b42 100644 --- a/portal/package.json +++ b/portal/package.json @@ -10,7 +10,9 @@ }, "dependencies": { "@tanstack/react-query": "^5.62.0", + "@types/dagre": "^0.7.53", "@xyflow/react": "^12.4.0", + "dagre": "^0.8.5", "react": "^19.0.0", "react-dom": "^19.0.0" }, @@ -18,9 +20,9 @@ "@tailwindcss/vite": "^4.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.0", "tailwindcss": "^4.0.0", "typescript": "^5.9.0", - "vite": "^6.0.0", - "@vitejs/plugin-react": "^4.3.0" + "vite": "^6.0.0" } } diff --git a/portal/src/App.tsx b/portal/src/App.tsx index 68588e3..afc96ce 100644 --- a/portal/src/App.tsx +++ b/portal/src/App.tsx @@ -44,7 +44,9 @@ export default function App() { return (
{/* Full-screen graph */} - +
+ +
{/* Floating action buttons — bottom-left */}
diff --git a/portal/src/components/GraphView.tsx b/portal/src/components/GraphView.tsx index d294b81..1a22bed 100644 --- a/portal/src/components/GraphView.tsx +++ b/portal/src/components/GraphView.tsx @@ -8,9 +8,12 @@ import { useEdgesState, type Node as FlowNode, type Edge as FlowEdge, + Position, + MarkerType, } from '@xyflow/react'; +import Dagre from 'dagre'; import { useGraph } from '../hooks/useGraph'; -import type { CortexNode } from '../types'; +import type { CortexNode, CortexEdge } from '../types'; const KIND_COLORS: Record = { memory: '#6366f1', @@ -19,23 +22,43 @@ const KIND_COLORS: Record = { decision: '#ef4444', }; -function layoutNodes(nodes: CortexNode[]): FlowNode[] { - const cols = Math.max(Math.ceil(Math.sqrt(nodes.length)), 1); - return nodes.map((n, i) => ({ - id: n.id, - position: { x: (i % cols) * 280 + 50, y: Math.floor(i / cols) * 140 + 50 }, - data: { label: n.title, kind: n.kind, status: n.status }, - style: { - background: KIND_COLORS[n.kind] || '#6b7280', - color: '#fff', - border: 'none', - borderRadius: 8, - padding: '12px 16px', - fontSize: 13, - fontWeight: 500, - minWidth: 160, - }, - })); +const NODE_WIDTH = 180; +const NODE_HEIGHT = 60; + +function layoutGraph(nodes: CortexNode[], edges: CortexEdge[]): FlowNode[] { + const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); + g.setGraph({ rankdir: 'TB', nodesep: 60, ranksep: 100, marginx: 40, marginy: 40 }); + + for (const n of nodes) { + g.setNode(n.id, { width: NODE_WIDTH, height: NODE_HEIGHT }); + } + for (const e of edges) { + g.setEdge(e.fromId, e.toId); + } + + Dagre.layout(g); + + return nodes.map((n) => { + const pos = g.node(n.id); + return { + id: n.id, + position: { x: pos.x - NODE_WIDTH / 2, y: pos.y - NODE_HEIGHT / 2 }, + sourcePosition: Position.Bottom, + targetPosition: Position.Top, + data: { label: n.title, kind: n.kind, status: n.status }, + style: { + background: KIND_COLORS[n.kind] || '#6b7280', + color: '#fff', + border: 'none', + borderRadius: 8, + padding: '10px 16px', + fontSize: 13, + fontWeight: 500, + width: NODE_WIDTH, + textAlign: 'center' as const, + }, + }; + }); } export default function GraphView({ @@ -47,7 +70,11 @@ export default function GraphView({ }) { const { data, isLoading } = useGraph(); - const initialNodes = useMemo(() => (data ? layoutNodes(data.nodes) : []), [data]); + const initialNodes = useMemo( + () => (data ? layoutGraph(data.nodes, data.edges) : []), + [data], + ); + const initialEdges = useMemo( () => data @@ -57,8 +84,12 @@ export default function GraphView({ target: e.toId, label: e.type, animated: e.type === 'blocked_by', - style: { stroke: '#64748b' }, - labelStyle: { fill: '#94a3b8', fontSize: 11 }, + style: { stroke: '#64748b', strokeWidth: 1.5 }, + labelStyle: { fill: '#94a3b8', fontSize: 10 }, + labelBgStyle: { fill: '#1e293b', fillOpacity: 0.9 }, + labelBgPadding: [6, 3] as [number, number], + labelBgBorderRadius: 4, + markerEnd: { type: MarkerType.ArrowClosed, color: '#64748b', width: 16, height: 16 }, })) : [], [data], @@ -67,7 +98,6 @@ export default function GraphView({ const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); - // Sync when data changes useMemo(() => { setNodes(initialNodes); setEdges(initialEdges); @@ -94,11 +124,16 @@ export default function GraphView({ onEdgesChange={onEdgesChange} onNodeClick={onNodeClick} fitView + fitViewOptions={{ padding: 0.3 }} className="bg-gray-950" > - + KIND_COLORS[(n.data as any)?.kind] || '#6b7280'} className="!bg-gray-900 !border-gray-700" />