feat(dashboard): add node type category filter controls

Add nodeTypeFilters state and toggleNodeTypeFilter action to the Zustand
store, apply category-based filtering in GraphView's useLayerDetailTopology,
and render filter toggle buttons in the App header for Code, Config, Docs,
Infra, and Data categories with colored indicator dots.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lum1104
2026-03-28 18:48:08 +08:00
co-authored by Claude Opus 4.6
parent 78c8ce2602
commit 642626653c
3 changed files with 62 additions and 1 deletions
@@ -72,6 +72,8 @@ function Dashboard({ accessToken }: { accessToken: string }) {
const codeViewerOpen = useDashboardStore((s) => s.codeViewerOpen);
const closeCodeViewer = useDashboardStore((s) => s.closeCodeViewer);
const setDiffOverlay = useDashboardStore((s) => s.setDiffOverlay);
const nodeTypeFilters = useDashboardStore((s) => s.nodeTypeFilters);
const toggleNodeTypeFilter = useDashboardStore((s) => s.toggleNodeTypeFilter);
const [loadError, setLoadError] = useState<string | null>(null);
const [graphIssues, setGraphIssues] = useState<GraphIssue[]>([]);
const [showKeyboardHelp, setShowKeyboardHelp] = useState(false);
@@ -250,6 +252,35 @@ function Dashboard({ accessToken }: { accessToken: string }) {
</div>
<div className="flex items-center gap-4">
<DiffToggle />
<div className="flex items-center gap-1">
{([
{ key: "code", label: "Code", color: "var(--color-node-file)" },
{ key: "config", label: "Config", color: "var(--color-node-config)" },
{ key: "docs", label: "Docs", color: "var(--color-node-document)" },
{ key: "infra", label: "Infra", color: "var(--color-node-service)" },
{ key: "data", label: "Data", color: "var(--color-node-table)" },
] as const).map((cat) => (
<button
key={cat.key}
onClick={() => toggleNodeTypeFilter(cat.key)}
className={`text-[10px] font-semibold uppercase tracking-wider px-2 py-1 rounded border transition-colors flex items-center gap-1.5 ${
nodeTypeFilters[cat.key] !== false
? "border-border-medium bg-elevated text-text-secondary hover:text-text-primary"
: "border-transparent bg-transparent text-text-muted/40 line-through hover:text-text-muted"
}`}
title={`${nodeTypeFilters[cat.key] !== false ? "Hide" : "Show"} ${cat.label} nodes`}
>
<span
className="w-2 h-2 rounded-full shrink-0"
style={{
backgroundColor: cat.color,
opacity: nodeTypeFilters[cat.key] !== false ? 1 : 0.3,
}}
/>
{cat.label}
</button>
))}
</div>
<LayerLegend />
<ThemePicker />
<button
@@ -200,6 +200,7 @@ function useLayerDetailTopology() {
const changedNodeIds = useDashboardStore((s) => s.changedNodeIds);
const affectedNodeIds = useDashboardStore((s) => s.affectedNodeIds);
const focusNodeId = useDashboardStore((s) => s.focusNodeId);
const nodeTypeFilters = useDashboardStore((s) => s.nodeTypeFilters);
const drillIntoLayer = useDashboardStore((s) => s.drillIntoLayer);
const handleNodeSelect = useCallback(
@@ -218,6 +219,15 @@ function useLayerDetailTopology() {
const layerNodeIds = new Set(activeLayer.nodeIds);
// Map node types to filter categories
const nodeTypeToCategory: Record<string, string> = {
file: "code", function: "code", class: "code", module: "code", concept: "code",
config: "config",
document: "docs",
service: "infra", resource: "infra", pipeline: "infra",
table: "data", endpoint: "data", schema: "data",
};
// Non-technical persona only sees concept/module/file nodes
let filteredGraphNodes = persona === "non-technical"
? graph.nodes.filter(
@@ -225,6 +235,12 @@ function useLayerDetailTopology() {
)
: graph.nodes.filter((n) => layerNodeIds.has(n.id) && n.type === "file");
// Apply node type category filters
filteredGraphNodes = filteredGraphNodes.filter((n) => {
const category = nodeTypeToCategory[n.type] ?? "code";
return nodeTypeFilters[category] !== false;
});
let filteredNodeIds = new Set(filteredGraphNodes.map((n) => n.id));
let filteredGraphEdges = graph.edges.filter(
@@ -360,7 +376,7 @@ function useLayerDetailTopology() {
const laid = applyDagreLayout(allFlowNodes, allFlowEdges, "TB", dims);
return { nodes: laid.nodes, edges: laid.edges, portalNodes, portalEdges, filteredEdges: filteredGraphEdges };
}, [graph, activeLayerId, persona, handleNodeSelect, diffMode, changedNodeIds, affectedNodeIds, focusNodeId, drillIntoLayer]);
}, [graph, activeLayerId, persona, handleNodeSelect, diffMode, changedNodeIds, affectedNodeIds, focusNodeId, nodeTypeFilters, drillIntoLayer]);
}
/**
@@ -53,6 +53,10 @@ interface DashboardStore {
// Sidebar navigation history (stack of visited node IDs)
nodeHistory: string[];
// Node type category filters
nodeTypeFilters: Record<string, boolean>;
toggleNodeTypeFilter: (category: string) => void;
setGraph: (graph: KnowledgeGraph) => void;
selectNode: (nodeId: string | null) => void;
navigateToNode: (nodeId: string) => void;
@@ -125,6 +129,16 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
focusNodeId: null,
nodeHistory: [],
nodeTypeFilters: { code: true, config: true, docs: true, infra: true, data: true },
toggleNodeTypeFilter: (category) =>
set((state) => ({
nodeTypeFilters: {
...state.nodeTypeFilters,
[category]: !state.nodeTypeFilters[category],
},
})),
setGraph: (graph) => {
const searchEngine = new SearchEngine(graph.nodes);
const query = get().searchQuery;