import { useEffect, useRef, useState } from "react"; import { useDashboardStore } from "../store"; interface PathFinderModalProps { isOpen: boolean; onClose: () => void; } export default function PathFinderModal({ isOpen, onClose }: PathFinderModalProps) { const graph = useDashboardStore((s) => s.graph); const selectNode = useDashboardStore((s) => s.selectNode); const [fromNodeId, setFromNodeId] = useState(""); const [toNodeId, setToNodeId] = useState(""); const [path, setPath] = useState(null); const [searching, setSearching] = useState(false); const modalRef = useRef(null); // Close on outside click useEffect(() => { if (!isOpen) return; const handleClickOutside = (e: MouseEvent) => { if (modalRef.current && !modalRef.current.contains(e.target as Node)) { onClose(); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [isOpen, onClose]); // Close on Escape useEffect(() => { if (!isOpen) return; const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape") { onClose(); } }; document.addEventListener("keydown", handleEscape); return () => document.removeEventListener("keydown", handleEscape); }, [isOpen, onClose]); if (!isOpen || !graph) return null; const nodes = graph.nodes; const edges = graph.edges; // BFS to find shortest path const findPath = () => { if (!fromNodeId || !toNodeId || fromNodeId === toNodeId) { setPath(null); return; } setSearching(true); // Build adjacency list (bidirectional traversal for path finding) const adjacency = new Map(); for (const edge of edges) { if (!adjacency.has(edge.source)) { adjacency.set(edge.source, []); } adjacency.get(edge.source)!.push(edge.target); // Also traverse in reverse so we can find paths through backward edges if (!adjacency.has(edge.target)) { adjacency.set(edge.target, []); } adjacency.get(edge.target)!.push(edge.source); } // BFS const queue: Array<{ nodeId: string; path: string[] }> = [ { nodeId: fromNodeId, path: [fromNodeId] }, ]; const visited = new Set([fromNodeId]); while (queue.length > 0) { const { nodeId, path: currentPath } = queue.shift()!; if (nodeId === toNodeId) { setPath(currentPath); setSearching(false); return; } const neighbors = adjacency.get(nodeId) ?? []; for (const neighbor of neighbors) { if (!visited.has(neighbor)) { visited.add(neighbor); queue.push({ nodeId: neighbor, path: [...currentPath, neighbor] }); } } } // No path found setPath([]); setSearching(false); }; const handleNodeClick = (nodeId: string) => { selectNode(nodeId); onClose(); }; const nodeMap = new Map(nodes.map((n) => [n.id, n])); return (
{/* Header */}

Dependency Path Finder

{/* Body */}

Find the shortest path between two nodes in the dependency graph.

{/* From Node */}
{/* To Node */}
{/* Find Path Button */} {/* Path Result */} {path !== null && (
{path.length === 0 ? (

No path found between these nodes.

) : (

Path Found ({path.length} nodes)

{path.map((nodeId, idx) => { const node = nodeMap.get(nodeId); if (!node) return null; const isLast = idx === path.length - 1; return (
{!isLast && (
)}
); })}
)}
)}
{/* Footer */}
); }