import { useMemo } from 'react'; import type { GraphNode, GraphEdge } from '../types'; const LABEL_COLORS: Record = { Customer: '#6366f1', SalesOrder: '#10b981', Delivery: '#8b5cf6', Invoice: '#14b8a6', JournalEntry: '#3b82f6', Payment: '#84cc16', Product: '#f59e0b', Plant: '#6366f1', }; interface Props { nodes: GraphNode[]; edges: GraphEdge[]; queryUsed: string; selectedNodeId?: string | null; selectedEdgeKey?: string | null; onNodeSelect: (nodeId: string) => void; onEdgeSelect: (edge: GraphEdge) => void; onClose: () => void; } function edgeKey(edge: GraphEdge): string { return `${edge.source}|${edge.target}|${edge.type}`; } function extractPathLines(queryUsed: string): string[] { return queryUsed .split('\n') .map((l) => l.trim()) .filter((l) => /^MATCH\b/i.test(l) || /^OPTIONAL MATCH\b/i.test(l)); } /** Get the primary display value for a node. */ function displayValue(node: GraphNode): string { const p = node.properties; if (typeof p.entity === 'string' && p.entity) { return p.entity; } return ( p.businessPartnerName || p.plantName || p.productGroup || p.salesOrder || p.deliveryDocument || p.billingDocument || p.accountingDocument || p.product || p[Object.keys(p)[0]] || node.id ); } type BFSLevel = { level: number; nodes: GraphNode[]; }; function buildBFSLevels(nodes: GraphNode[], edges: GraphEdge[]): BFSLevel[] { if (nodes.length === 0) return []; const nodeById = new Map(nodes.map((n) => [n.id, n])); const outgoing = new Map(); const indegree = new Map(); for (const n of nodes) { indegree.set(n.id, 0); } for (const e of edges) { if (!nodeById.has(e.source) || !nodeById.has(e.target)) continue; const list = outgoing.get(e.source) || []; list.push(e.target); outgoing.set(e.source, list); indegree.set(e.target, (indegree.get(e.target) || 0) + 1); } const roots = nodes .filter((n) => (indegree.get(n.id) || 0) === 0) .map((n) => n.id); const queue = roots.length > 0 ? [...roots] : [nodes[0].id]; const levelById = new Map(); for (const root of queue) { levelById.set(root, 0); } while (queue.length > 0) { const current = queue.shift() as string; const currentLevel = levelById.get(current) || 0; for (const next of outgoing.get(current) || []) { if (levelById.has(next)) continue; levelById.set(next, currentLevel + 1); queue.push(next); } } // Disconnected leftovers let tailLevel = Math.max(...Array.from(levelById.values()), 0) + 1; for (const n of nodes) { if (!levelById.has(n.id)) { levelById.set(n.id, tailLevel); tailLevel += 1; } } const grouped = new Map(); for (const n of nodes) { const lvl = levelById.get(n.id) || 0; const list = grouped.get(lvl) || []; list.push(n); grouped.set(lvl, list); } return Array.from(grouped.entries()) .sort((a, b) => a[0] - b[0]) .map(([level, levelNodes]) => ({ level, nodes: levelNodes })); } export default function QueryPathPanel({ nodes, edges, queryUsed, selectedNodeId, selectedEdgeKey, onNodeSelect, onEdgeSelect, onClose, }: Props) { const pathLines = useMemo(() => extractPathLines(queryUsed), [queryUsed]); const bfsLevels = useMemo(() => buildBFSLevels(nodes, edges), [nodes, edges]); return (
{/* Header */}

Query Path

{/* Summary */}

{nodes.length} node{nodes.length !== 1 ? 's' : ''},{' '} {edges.length} edge{edges.length !== 1 ? 's' : ''}

{/* Path lines first for debugging focus */}

Execution Path

{pathLines.length > 0 ? (
{pathLines.map((line, idx) => (
{line}
))}
) : (

No MATCH path detected.

)}
{/* BFS traversal view */}
{bfsLevels.map((level) => (
Level {level.level} ({level.nodes.length})
{level.nodes.map((node) => ( ))}
))} {/* Edge flow */} {edges.length > 0 && (
Relationships
{edges.map((edge, i) => { const srcNode = nodes.find((n) => n.id === edge.source); const tgtNode = nodes.find((n) => n.id === edge.target); const isSelected = selectedEdgeKey === edgeKey(edge); return ( ); })}
)}
{/* Cypher query */} {queryUsed && (

Cypher

            {queryUsed}
          
)}
); }