From 794fe05a34b629eaaf90e837f291c211235f42d4 Mon Sep 17 00:00:00 2001 From: ShresthSamyak Date: Sun, 24 May 2026 10:50:16 +0530 Subject: [PATCH] fix(dashboard): restrict Path Finder "To" dropdown to reachable nodes The Dependency Path Finder modal listed every node in both the "From" and "To" selects, so users could pick a destination that was not reachable from the source and only learn "no path found" after clicking Find Path. Compute the reachable set from the chosen "From" node via the same undirected adjacency used by findPath (memoized on the graph), and filter the "To" dropdown to that set. Disable "To" until a source is picked, surface the reachable count, and clear a stale "To" selection when the source changes. Closes #188 --- .../src/components/PathFinderModal.tsx | 96 ++++++++++++++----- 1 file changed, 73 insertions(+), 23 deletions(-) diff --git a/understand-anything-plugin/packages/dashboard/src/components/PathFinderModal.tsx b/understand-anything-plugin/packages/dashboard/src/components/PathFinderModal.tsx index 3e0e3c01..a1db4b23 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/PathFinderModal.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/PathFinderModal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useDashboardStore } from "../store"; interface PathFinderModalProps { @@ -15,6 +15,50 @@ export default function PathFinderModal({ isOpen, onClose }: PathFinderModalProp const [searching, setSearching] = useState(false); const modalRef = useRef(null); + // Undirected adjacency, memoized per graph. Matches the bidirectional + // traversal used by findPath so reachability and path-finding agree. + const adjacency = useMemo(() => { + const adj = new Map(); + if (!graph) return adj; + for (const edge of graph.edges) { + if (!adj.has(edge.source)) adj.set(edge.source, []); + adj.get(edge.source)!.push(edge.target); + if (!adj.has(edge.target)) adj.set(edge.target, []); + adj.get(edge.target)!.push(edge.source); + } + return adj; + }, [graph]); + + // Set of node ids reachable from the selected "From" node (excluding itself). + // null when no source is selected — caller treats null as "no filtering yet". + const reachableFromSource = useMemo(() => { + if (!fromNodeId) return null; + const reachable = new Set(); + const queue: string[] = [fromNodeId]; + reachable.add(fromNodeId); + while (queue.length > 0) { + const nodeId = queue.shift()!; + const neighbors = adjacency.get(nodeId) ?? []; + for (const n of neighbors) { + if (!reachable.has(n)) { + reachable.add(n); + queue.push(n); + } + } + } + reachable.delete(fromNodeId); + return reachable; + }, [fromNodeId, adjacency]); + + // If "From" changes and the previously chosen "To" is no longer reachable, + // clear "To" so the select doesn't display a stale, invalid value. + useEffect(() => { + if (toNodeId && reachableFromSource && !reachableFromSource.has(toNodeId)) { + setToNodeId(""); + setPath(null); + } + }, [reachableFromSource, toNodeId]); + // Close on outside click useEffect(() => { if (!isOpen) return; @@ -46,7 +90,13 @@ export default function PathFinderModal({ isOpen, onClose }: PathFinderModalProp if (!isOpen || !graph) return null; const nodes = graph.nodes; - const edges = graph.edges; + + // Nodes shown in the "To" dropdown. Before a source is picked, show all + // nodes; once a source is picked, only show nodes reachable from it so + // the user can't select a destination with no path. + const toCandidates = reachableFromSource + ? nodes.filter((n) => reachableFromSource.has(n.id)) + : nodes; // BFS to find shortest path const findPath = () => { @@ -57,21 +107,7 @@ export default function PathFinderModal({ isOpen, onClose }: PathFinderModalProp 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 + // BFS using the memoized undirected adjacency const queue: Array<{ nodeId: string; path: string[] }> = [ { nodeId: fromNodeId, path: [fromNodeId] }, ]; @@ -166,19 +202,33 @@ export default function PathFinderModal({ isOpen, onClose }: PathFinderModalProp {/* To Node */}
- +
+ + {fromNodeId && ( + + {toCandidates.length} reachable + + )} +