From ef1f220782ea5f6a11a6ba739e095251e8992e0b Mon Sep 17 00:00:00 2001 From: Sreeram Date: Fri, 27 Mar 2026 03:07:30 +0530 Subject: [PATCH] refactor(dashboard): remove swim-lane flow view The swim-lane layout needs more design work before shipping. Remove the flow view toggle, useFlowViewGraph hook, applySwimLaneLayout, and related store/breadcrumb code. Hierarchical lens navigation is retained. --- .../dashboard/src/components/Breadcrumb.tsx | 56 +----- .../dashboard/src/components/GraphView.tsx | 177 +----------------- .../packages/dashboard/src/store.ts | 18 +- .../packages/dashboard/src/utils/layout.ts | 150 --------------- 4 files changed, 10 insertions(+), 391 deletions(-) diff --git a/understand-anything-plugin/packages/dashboard/src/components/Breadcrumb.tsx b/understand-anything-plugin/packages/dashboard/src/components/Breadcrumb.tsx index f35de41..4db7629 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/Breadcrumb.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/Breadcrumb.tsx @@ -1,29 +1,22 @@ import { useCallback, useEffect } from "react"; import { useDashboardStore } from "../store"; -import type { ViewMode } from "../store"; export default function Breadcrumb() { const navigationLevel = useDashboardStore((s) => s.navigationLevel); const activeLayerId = useDashboardStore((s) => s.activeLayerId); - const viewMode = useDashboardStore((s) => s.viewMode); const graph = useDashboardStore((s) => s.graph); const navigateToOverview = useDashboardStore((s) => s.navigateToOverview); - const setViewMode = useDashboardStore((s) => s.setViewMode); const activeLayer = graph?.layers.find((l) => l.id === activeLayerId); - // Escape key to go back to overview (only in graph mode) + // Escape key to go back to overview const handleKeyDown = useCallback( (e: KeyboardEvent) => { - if ( - e.key === "Escape" && - viewMode === "graph" && - navigationLevel === "layer-detail" - ) { + if (e.key === "Escape" && navigationLevel === "layer-detail") { navigateToOverview(); } }, - [viewMode, navigationLevel, navigateToOverview], + [navigationLevel, navigateToOverview], ); useEffect(() => { @@ -31,48 +24,15 @@ export default function Breadcrumb() { return () => document.removeEventListener("keydown", handleKeyDown); }, [handleKeyDown]); - const handleViewToggle = useCallback( - (mode: ViewMode) => { - setViewMode(mode); - }, - [setViewMode], - ); - return (
- {/* View mode toggle */} -
- -
- -
- - {/* Navigation breadcrumb (only in graph mode) */} - {viewMode === "graph" && navigationLevel === "overview" && ( + {navigationLevel === "overview" && (
Project Overview
)} - {viewMode === "graph" && navigationLevel === "layer-detail" && ( + {navigationLevel === "layer-detail" && (
)} - - {viewMode === "flow" && ( -
- Request Flow -
- )}
); } diff --git a/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx b/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx index 5efb517..f57ec8f 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx @@ -23,7 +23,6 @@ import Breadcrumb from "./Breadcrumb"; import { useDashboardStore } from "../store"; import { applyDagreLayout, - applySwimLaneLayout, NODE_WIDTH, NODE_HEIGHT, LAYER_CLUSTER_WIDTH, @@ -416,169 +415,12 @@ function useLayerDetailGraph() { ]); } -// ── Flow (swim-lane) view: all layers as columns ────────────────────── - -function useFlowViewGraph() { - const graph = useDashboardStore((s) => s.graph); - const selectedNodeId = useDashboardStore((s) => s.selectedNodeId); - const searchResults = useDashboardStore((s) => s.searchResults); - const selectNode = useDashboardStore((s) => s.selectNode); - const tourHighlightedNodeIds = useDashboardStore( - (s) => s.tourHighlightedNodeIds, - ); - const diffMode = useDashboardStore((s) => s.diffMode); - const changedNodeIds = useDashboardStore((s) => s.changedNodeIds); - const affectedNodeIds = useDashboardStore((s) => s.affectedNodeIds); - - const handleNodeSelect = useCallback( - (nodeId: string) => { - selectNode(nodeId); - }, - [selectNode], - ); - - return useMemo(() => { - if (!graph || graph.layers.length === 0) - return { nodes: [] as Node[], edges: [] as Edge[] }; - - const allLayerNodeIds = new Set( - graph.layers.flatMap((l) => l.nodeIds), - ); - - const fileGraphNodes = graph.nodes.filter( - (n) => n.type === "file" && allLayerNodeIds.has(n.id), - ); - - const fileNodeIds = new Set(fileGraphNodes.map((n) => n.id)); - const neighborNodeIds = new Set(); - if (selectedNodeId) { - for (const edge of graph.edges) { - if (edge.source === selectedNodeId && fileNodeIds.has(edge.target)) - neighborNodeIds.add(edge.target); - if (edge.target === selectedNodeId && fileNodeIds.has(edge.source)) - neighborNodeIds.add(edge.source); - } - neighborNodeIds.add(selectedNodeId); - } - - const diffNodeIds = diffMode - ? new Set([...changedNodeIds, ...affectedNodeIds]) - : new Set(); - - const flowNodes: CustomFlowNode[] = fileGraphNodes.map((node) => { - const matchResult = searchResults.find((r) => r.nodeId === node.id); - const hasSelection = !!selectedNodeId; - return { - id: node.id, - type: "custom" as const, - position: { x: 0, y: 0 }, - data: { - label: node.name ?? node.filePath?.split("/").pop() ?? node.id, - nodeType: node.type, - summary: node.summary, - complexity: node.complexity, - isHighlighted: !!matchResult, - searchScore: matchResult?.score, - isSelected: selectedNodeId === node.id, - isTourHighlighted: tourHighlightedNodeIds.includes(node.id), - isDiffChanged: diffMode && changedNodeIds.has(node.id), - isDiffAffected: diffMode && affectedNodeIds.has(node.id), - isDiffFaded: - diffMode && - !changedNodeIds.has(node.id) && - !affectedNodeIds.has(node.id), - isNeighbor: - hasSelection && - neighborNodeIds.has(node.id) && - selectedNodeId !== node.id, - isSelectionFaded: hasSelection && !neighborNodeIds.has(node.id), - onNodeClick: handleNodeSelect, - }, - }; - }); - - const flowEdges: Edge[] = []; - let edgeIdx = 0; - for (const edge of graph.edges) { - if (!fileNodeIds.has(edge.source) || !fileNodeIds.has(edge.target)) - continue; - - const isSelectedEdge = - !!selectedNodeId && - (edge.source === selectedNodeId || edge.target === selectedNodeId); - const hasSelection = !!selectedNodeId; - const sourceInDiff = diffMode && diffNodeIds.has(edge.source); - const targetInDiff = diffMode && diffNodeIds.has(edge.target); - const isImpacted = diffMode && (sourceInDiff || targetInDiff); - - let edgeStyle: React.CSSProperties; - let edgeLabelStyle: React.CSSProperties; - let edgeAnimated: boolean; - - if (isImpacted) { - edgeStyle = { - stroke: - sourceInDiff && targetInDiff - ? "rgba(224, 82, 82, 0.7)" - : "rgba(212, 160, 48, 0.5)", - strokeWidth: 2.5, - }; - edgeLabelStyle = { fill: "#a39787", fontSize: 10 }; - edgeAnimated = true; - } else if (diffMode) { - edgeStyle = { stroke: "rgba(212,165,116,0.08)", strokeWidth: 1 }; - edgeLabelStyle = { fill: "rgba(163,151,135,0.3)", fontSize: 10 }; - edgeAnimated = false; - } else if (isSelectedEdge) { - edgeStyle = { stroke: "rgba(212,165,116,0.8)", strokeWidth: 2.5 }; - edgeLabelStyle = { fill: "#d4a574", fontSize: 11, fontWeight: 600 }; - edgeAnimated = true; - } else if (hasSelection) { - edgeStyle = { stroke: "rgba(212,165,116,0.08)", strokeWidth: 1 }; - edgeLabelStyle = { fill: "rgba(163,151,135,0.2)", fontSize: 10 }; - edgeAnimated = false; - } else { - edgeStyle = { stroke: "rgba(212,165,116,0.25)", strokeWidth: 1 }; - edgeLabelStyle = { fill: "#a39787", fontSize: 9 }; - edgeAnimated = false; - } - - flowEdges.push({ - id: `fe-${edgeIdx++}`, - source: edge.source, - target: edge.target, - label: edge.type, - animated: edgeAnimated, - style: edgeStyle, - labelStyle: edgeLabelStyle, - }); - } - - const result = applySwimLaneLayout( - graph, - flowNodes as unknown as Node[], - flowEdges, - ); - return { nodes: result.nodes, edges: result.edges }; - }, [ - graph, - selectedNodeId, - searchResults, - tourHighlightedNodeIds, - handleNodeSelect, - diffMode, - changedNodeIds, - affectedNodeIds, - ]); -} - // ── Main inner component (must be inside ReactFlowProvider) ──────────── function GraphViewInner() { const graph = useDashboardStore((s) => s.graph); const navigationLevel = useDashboardStore((s) => s.navigationLevel); const activeLayerId = useDashboardStore((s) => s.activeLayerId); - const viewMode = useDashboardStore((s) => s.viewMode); const selectNode = useDashboardStore((s) => s.selectNode); const drillIntoLayer = useDashboardStore((s) => s.drillIntoLayer); const focusNodeId = useDashboardStore((s) => s.focusNodeId); @@ -586,14 +428,9 @@ function GraphViewInner() { const overviewGraph = useOverviewGraph(); const detailGraph = useLayerDetailGraph(); - const flowGraph = useFlowViewGraph(); const { nodes: initialNodes, edges: initialEdges } = - viewMode === "flow" - ? flowGraph - : navigationLevel === "overview" - ? overviewGraph - : detailGraph; + navigationLevel === "overview" ? overviewGraph : detailGraph; const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); @@ -608,22 +445,16 @@ function GraphViewInner() { setEdges(initialEdges); }, [initialEdges, setEdges]); - // Fit view on level/layer/view-mode transitions + // Fit view on level/layer transitions useEffect(() => { const timer = setTimeout(() => { fitView({ duration: 400, padding: 0.2 }); }, 50); return () => clearTimeout(timer); - }, [navigationLevel, activeLayerId, viewMode, fitView]); + }, [navigationLevel, activeLayerId, fitView]); const onNodeClick = useCallback( (_: React.MouseEvent, node: { id: string }) => { - // In flow view, all clicks are selections (no drill-in) - if (viewMode === "flow") { - if (node.id.startsWith("lane:")) return; - selectNode(node.id); - return; - } if (navigationLevel === "overview") { drillIntoLayer(node.id); } else if (node.id.startsWith("portal:")) { @@ -633,7 +464,7 @@ function GraphViewInner() { selectNode(node.id); } }, - [viewMode, navigationLevel, drillIntoLayer, selectNode], + [navigationLevel, drillIntoLayer, selectNode], ); const onPaneClick = useCallback(() => { diff --git a/understand-anything-plugin/packages/dashboard/src/store.ts b/understand-anything-plugin/packages/dashboard/src/store.ts index 4354e69..ae6a064 100644 --- a/understand-anything-plugin/packages/dashboard/src/store.ts +++ b/understand-anything-plugin/packages/dashboard/src/store.ts @@ -8,7 +8,7 @@ import type { export type Persona = "non-technical" | "junior" | "experienced"; export type NavigationLevel = "overview" | "layer-detail"; -export type ViewMode = "graph" | "flow"; + /** Find which layer a node belongs to. Returns layerId or null. */ function findNodeLayer(graph: KnowledgeGraph, nodeId: string): string | null { @@ -33,9 +33,6 @@ interface DashboardStore { navigationLevel: NavigationLevel; activeLayerId: string | null; - // View mode: graph (default free-form) or flow (swim-lane) - viewMode: ViewMode; - codeViewerOpen: boolean; codeViewerNodeId: string | null; @@ -65,7 +62,6 @@ interface DashboardStore { goBackNode: () => void; drillIntoLayer: (layerId: string) => void; navigateToOverview: () => void; - setViewMode: (mode: ViewMode) => void; setFocusNode: (nodeId: string | null) => void; setSearchQuery: (query: string) => void; toggleLayers: () => void; @@ -117,8 +113,6 @@ export const useDashboardStore = create()((set, get) => ({ navigationLevel: "overview", activeLayerId: null, - viewMode: "graph", - codeViewerOpen: false, codeViewerNodeId: null, @@ -243,16 +237,6 @@ export const useDashboardStore = create()((set, get) => ({ codeViewerNodeId: null, }), - setViewMode: (mode) => - set({ - viewMode: mode, - // When switching to flow view, go to overview level (flow shows all layers) - navigationLevel: mode === "flow" ? "overview" : get().navigationLevel, - activeLayerId: mode === "flow" ? null : get().activeLayerId, - selectedNodeId: null, - focusNodeId: null, - }), - setFocusNode: (nodeId) => set({ focusNodeId: nodeId, selectedNodeId: nodeId }), setSearchMode: (mode) => set({ searchMode: mode }), setSearchQuery: (query) => { diff --git a/understand-anything-plugin/packages/dashboard/src/utils/layout.ts b/understand-anything-plugin/packages/dashboard/src/utils/layout.ts index 548ebfc..b084db8 100644 --- a/understand-anything-plugin/packages/dashboard/src/utils/layout.ts +++ b/understand-anything-plugin/packages/dashboard/src/utils/layout.ts @@ -1,6 +1,5 @@ import dagre from "@dagrejs/dagre"; import type { Node, Edge } from "@xyflow/react"; -import type { KnowledgeGraph } from "@understand-anything/core/types"; import type { LayoutMessage, LayoutResult } from "./layout.worker"; export const NODE_WIDTH = 280; @@ -10,12 +9,6 @@ export const LAYER_CLUSTER_HEIGHT = 180; export const PORTAL_NODE_WIDTH = 240; export const PORTAL_NODE_HEIGHT = 80; -// Swim-lane constants -export const LANE_WIDTH = 320; -export const LANE_GAP = 40; -export const LANE_PADDING = 30; -export const LANE_HEADER_HEIGHT = 40; - /** * Synchronous dagre layout — used for small graphs. */ @@ -144,146 +137,3 @@ export function applyDagreLayoutAsync( }); } -// ── Swim-lane layout ─────────────────────────────────────────────────── - -/** - * Preferred order of layers for the swim-lane flow view. - * Reflects a typical request lifecycle: entry → middleware → logic → data → external. - */ -const LAYER_FLOW_ORDER = [ - "API Layer", - "UI Layer", - "Middleware Layer", - "Service Layer", - "External Services", - "Data Layer", - "Background Tasks", - "Utility Layer", - "Configuration Layer", - "Test Layer", -]; - -function getLayerSortIndex(layerName: string): number { - const idx = LAYER_FLOW_ORDER.indexOf(layerName); - return idx >= 0 ? idx : LAYER_FLOW_ORDER.length; -} - -export interface SwimLaneResult { - nodes: Node[]; - edges: Edge[]; - lanes: Array<{ layerId: string; layerName: string; columnIndex: number }>; -} - -/** - * Swim-lane layout: each layer becomes a vertical column (lane), ordered - * by the request lifecycle. Within each lane, dagre handles vertical - * positioning. File nodes are children of their lane group node. - */ -export function applySwimLaneLayout( - graph: KnowledgeGraph, - fileNodes: Node[], - allEdges: Edge[], -): SwimLaneResult { - const sortedLayers = [...graph.layers].sort( - (a, b) => getLayerSortIndex(a.name) - getLayerSortIndex(b.name), - ); - - const nodeToLayerId = new Map(); - for (const layer of sortedLayers) { - for (const nid of layer.nodeIds) { - nodeToLayerId.set(nid, layer.id); - } - } - - const nodesByLayer = new Map(); - for (const layer of sortedLayers) { - nodesByLayer.set(layer.id, []); - } - for (const node of fileNodes) { - const lid = nodeToLayerId.get(node.id); - if (lid && nodesByLayer.has(lid)) { - nodesByLayer.get(lid)!.push(node); - } - } - - const laneHeights = new Map(); - - for (const [layerId, nodes] of nodesByLayer) { - if (nodes.length === 0) { - laneHeights.set(layerId, LANE_HEADER_HEIGHT + LANE_PADDING * 2); - continue; - } - - const g = new dagre.graphlib.Graph(); - g.setDefaultEdgeLabel(() => ({})); - g.setGraph({ rankdir: "TB", nodesep: 20, ranksep: 40, marginx: 0, marginy: 0 }); - - const laneNodeIds = new Set(nodes.map((n) => n.id)); - for (const node of nodes) { - g.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT }); - } - - for (const edge of allEdges) { - if (laneNodeIds.has(edge.source) && laneNodeIds.has(edge.target)) { - g.setEdge(edge.source, edge.target); - } - } - - dagre.layout(g); - - let maxY = 0; - for (const node of nodes) { - const pos = g.node(node.id); - if (pos) { - (node as Node & { _laneY: number })._laneY = pos.y - NODE_HEIGHT / 2; - maxY = Math.max(maxY, pos.y + NODE_HEIGHT / 2); - } - } - - laneHeights.set(layerId, LANE_HEADER_HEIGHT + maxY + LANE_PADDING * 2); - } - - const maxLaneHeight = Math.max(200, ...Array.from(laneHeights.values())); - - const resultNodes: Node[] = []; - const lanes: SwimLaneResult["lanes"] = []; - - sortedLayers.forEach((layer, colIdx) => { - const laneX = colIdx * (LANE_WIDTH + LANE_GAP); - const laneId = `lane:${layer.id}`; - - lanes.push({ layerId: layer.id, layerName: layer.name, columnIndex: colIdx }); - - resultNodes.push({ - id: laneId, - type: "group", - position: { x: laneX, y: 0 }, - data: { label: layer.name }, - style: { - width: LANE_WIDTH, - height: maxLaneHeight, - backgroundColor: "rgba(212,165,116,0.03)", - borderRadius: 12, - border: "1px solid rgba(212,165,116,0.1)", - padding: 0, - }, - }); - - const nodes = nodesByLayer.get(layer.id) ?? []; - for (const node of nodes) { - const laneY = (node as Node & { _laneY?: number })._laneY ?? 0; - resultNodes.push({ - ...node, - parentId: laneId, - extent: "parent" as const, - position: { - x: (LANE_WIDTH - NODE_WIDTH) / 2, - y: LANE_HEADER_HEIGHT + LANE_PADDING + laneY, - }, - }); - delete (node as Node & { _laneY?: number })._laneY; - } - }); - - return { nodes: resultNodes, edges: allEdges, lanes }; -}