({ project }: GraphTabProps)
| 64 | } |
| 65 | |
| 66 | export function GraphTab({ project }: GraphTabProps) { |
| 67 | const { data, loading, error, progress, fetchOverview } = useGraphData(); |
| 68 | const [highlightedIds, setHighlightedIds] = useState<Set<number> | null>(null); |
| 69 | const [selectedPath, setSelectedPath] = useState<string | null>(null); |
| 70 | const [selectedNode, setSelectedNode] = useState<GraphNode | null>(null); |
| 71 | const [cameraTarget, setCameraTarget] = useState<CameraTarget | null>(null); |
| 72 | const [repoInfo, setRepoInfo] = useState<RepoInfo | null>(null); |
| 73 | const [showLabels, setShowLabels] = useState(true); |
| 74 | const [display, setDisplay] = useState<DisplaySettings>(() => |
| 75 | loadDisplaySettings(), |
| 76 | ); |
| 77 | const updateDisplay = useCallback((next: DisplaySettings) => { |
| 78 | setDisplay(next); |
| 79 | saveDisplaySettings(next); |
| 80 | }, []); |
| 81 | const [leftWidth, setLeftWidth] = useState(() => loadWidth("cbm-left-w", 260)); |
| 82 | const [rightWidth, setRightWidth] = useState(() => loadWidth("cbm-right-w", 280)); |
| 83 | const limitNotice = formatGraphLimitNotice(data); |
| 84 | |
| 85 | /* Node budget — keyed to its project so switching projects re-reads the |
| 86 | * persisted value and triggers exactly one fetch. */ |
| 87 | const [budget, setBudget] = useState<{ project: string | null; value: number }>( |
| 88 | { project: null, value: GRAPH_RENDER_NODE_LIMIT }, |
| 89 | ); |
| 90 | const [budgetDraft, setBudgetDraft] = useState(String(GRAPH_RENDER_NODE_LIMIT)); |
| 91 | |
| 92 | const commitBudget = useCallback(() => { |
| 93 | const parsed = clampNodeBudget(parseInt(budgetDraft, 10)); |
| 94 | setBudgetDraft(String(parsed)); |
| 95 | if (project && parsed !== budget.value) { |
| 96 | saveNodeBudget(project, parsed); |
| 97 | setBudget({ project, value: parsed }); |
| 98 | } |
| 99 | }, [budgetDraft, project, budget.value]); |
| 100 | |
| 101 | /* Filter state — all enabled by default */ |
| 102 | const [enabledLabels, setEnabledLabels] = useState<Set<string>>(new Set()); |
| 103 | const [enabledEdgeTypes, setEnabledEdgeTypes] = useState<Set<string>>(new Set()); |
| 104 | |
| 105 | /* Dead-code view: recolor by status + status-based filters */ |
| 106 | const [deadCodeView, setDeadCodeView] = useState(false); |
| 107 | const [showOnlyDead, setShowOnlyDead] = useState(false); |
| 108 | const [hideEntryPoints, setHideEntryPoints] = useState(false); |
| 109 | const [hideTests, setHideTests] = useState(false); |
| 110 | |
| 111 | /* Initialize filters when data loads */ |
| 112 | useEffect(() => { |
| 113 | if (!data) return; |
| 114 | const labels = new Set(data.nodes.map((n) => n.label)); |
| 115 | const types = new Set(data.edges.map((e) => e.type)); |
| 116 | for (const lp of data.linked_projects ?? []) { |
| 117 | for (const n of lp.nodes) labels.add(n.label); |
| 118 | for (const e of lp.edges) types.add(e.type); |
| 119 | for (const e of lp.cross_edges) types.add(e.type); |
| 120 | } |
| 121 | setEnabledLabels(labels); |
| 122 | setEnabledEdgeTypes(types); |
| 123 | }, [data]); |
nothing calls this directly
no test coverage detected