Skip to content

Commit 1db90da

Browse files
aksOpsclaude
andcommitted
fix: codebase map data source, darker theme, edge breakdown
- Codebase Map: use /api/file-tree endpoint (returns proper tree with nodeCount per directory) instead of /api/nodes which was returning empty for treemap. Full recursive directory→ECharts conversion. - Dark theme: true black (#0a0a0a layout, #141414 container, #1c1c1c elevated) — no more grayish purple. Tabs and header match. - Light theme: clean white with #f7f7f8 background - Edge breakdown: pulls from graph.edges_by_kind in stats, not from REST connections section. Won't open if data unavailable. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 791ed93 commit 1db90da

4 files changed

Lines changed: 93 additions & 120 deletions

File tree

src/main/frontend/src/components/AppLayout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export default function AppLayout() {
3939
position: 'sticky',
4040
top: 0,
4141
zIndex: 100,
42-
borderBottom: isDark ? '1px solid #3f3f46' : '1px solid #f0f0f0',
42+
borderBottom: isDark ? '1px solid #303030' : '1px solid #e8e8e8',
4343
}}
4444
>
4545
<Typography.Title

src/main/frontend/src/main.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,23 @@ function ThemedApp() {
2828
wireframe: false,
2929
// Dark mode refinements
3030
...(isDark ? {
31-
colorBgContainer: '#1f1f23',
32-
colorBgElevated: '#27272b',
33-
colorBgLayout: '#18181b',
34-
colorBorder: '#3f3f46',
35-
colorBorderSecondary: '#34343a',
31+
colorBgContainer: '#141414',
32+
colorBgElevated: '#1c1c1c',
33+
colorBgLayout: '#0a0a0a',
34+
colorBorder: '#303030',
35+
colorBorderSecondary: '#262626',
36+
colorText: '#e8e8e8',
37+
colorTextSecondary: '#a0a0a0',
3638
} : {
3739
colorBgContainer: '#ffffff',
3840
colorBgElevated: '#ffffff',
39-
colorBgLayout: '#f5f5f9',
41+
colorBgLayout: '#f7f7f8',
4042
}),
4143
},
4244
components: {
4345
Table: {
44-
headerBg: isDark ? '#27272b' : '#fafafa',
45-
rowHoverBg: isDark ? '#2d2d33' : '#f0f0ff',
46+
headerBg: isDark ? '#1c1c1c' : '#fafafa',
47+
rowHoverBg: isDark ? '#1f1f1f' : '#f5f5ff',
4648
},
4749
Card: {
4850
paddingLG: 20,

src/main/frontend/src/pages/CodebaseMap.tsx

Lines changed: 72 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@ import ReactECharts from 'echarts-for-react';
44
import { useApi } from '@/hooks/useApi';
55
import { api } from '@/lib/api';
66
import { useTheme } from '@/context/ThemeContext';
7-
import type { NodesListResponse } from '@/types/api';
7+
import type { FileTreeResponse, FileTreeNode } from '@/types/api';
88

99
const LANG_COLORS: Record<string, string> = {
1010
java: '#b07219', python: '#3572A5', typescript: '#3178c6', javascript: '#f1e05a',
1111
go: '#00ADD8', csharp: '#178600', rust: '#dea584', kotlin: '#A97BFF',
1212
yaml: '#cb171e', json: '#555', ruby: '#701516', scala: '#c22d40',
1313
cpp: '#f34b7d', shell: '#89e051', markdown: '#083fa1', html: '#e34c26',
1414
css: '#563d7c', sql: '#e38c00', proto: '#60a0b0', dockerfile: '#384d54',
15-
other: '#888',
1615
};
1716

1817
const EXT_TO_LANG: Record<string, string> = {
@@ -24,104 +23,85 @@ const EXT_TO_LANG: Record<string, string> = {
2423
css: 'css', sql: 'sql', proto: 'proto',
2524
};
2625

27-
interface TreeNode {
26+
interface EChartsTreeNode {
2827
name: string;
29-
path: string;
3028
value: number;
31-
children?: TreeNode[];
29+
children?: EChartsTreeNode[];
3230
itemStyle?: { color: string };
3331
}
3432

35-
function buildTreemap(
36-
nodes: Array<{ file_path?: string; label: string; properties?: Record<string, unknown> }>,
37-
): TreeNode[] {
38-
// Build a nested directory tree
39-
const root: Record<string, { count: number; langs: Record<string, number>; children: Record<string, unknown> }> = {};
40-
41-
for (const node of nodes) {
42-
const fp = node.file_path;
43-
if (!fp) continue;
44-
45-
const parts = fp.split('/');
46-
const ext = fp.split('.').pop()?.toLowerCase() ?? '';
47-
const lang = EXT_TO_LANG[ext] ?? 'other';
48-
49-
// Walk/create directory path
50-
let current = root;
51-
for (let i = 0; i < parts.length - 1; i++) {
52-
const dir = parts[i];
53-
if (!current[dir]) {
54-
current[dir] = { count: 0, langs: {}, children: {} };
55-
}
56-
current[dir].count++;
57-
current[dir].langs[lang] = (current[dir].langs[lang] ?? 0) + 1;
58-
current = current[dir].children as typeof root;
59-
}
60-
}
33+
function inferLang(name: string): string {
34+
const ext = name.split('.').pop()?.toLowerCase() ?? '';
35+
return EXT_TO_LANG[ext] ?? 'other';
36+
}
6137

62-
function toTreeNodes(map: typeof root, parentPath: string): TreeNode[] {
63-
return Object.entries(map)
64-
.sort((a, b) => b[1].count - a[1].count)
65-
.map(([name, data]) => {
66-
const path = parentPath ? `${parentPath}/${name}` : name;
67-
const dominantLang = Object.entries(data.langs).sort((a, b) => b[1] - a[1])[0]?.[0] ?? 'other';
68-
const children = toTreeNodes(data.children as typeof root, path);
69-
70-
if (children.length > 0) {
71-
return {
72-
name,
73-
path,
74-
value: data.count,
75-
children,
76-
itemStyle: { color: LANG_COLORS[dominantLang] ?? '#888' },
77-
};
38+
function fileTreeToECharts(nodes: FileTreeNode[]): EChartsTreeNode[] {
39+
return nodes
40+
.filter(n => n.nodeCount > 0 || (n.children && n.children.length > 0))
41+
.map(n => {
42+
if (n.type === 'directory' && n.children && n.children.length > 0) {
43+
const children = fileTreeToECharts(n.children);
44+
// Dominant language from children names
45+
const langCounts: Record<string, number> = {};
46+
function countLangs(items: FileTreeNode[]) {
47+
for (const item of items) {
48+
if (item.type === 'file') {
49+
const lang = inferLang(item.name);
50+
langCounts[lang] = (langCounts[lang] ?? 0) + (item.nodeCount || 1);
51+
}
52+
if (item.children) countLangs(item.children);
53+
}
7854
}
55+
countLangs(n.children);
56+
const dominant = Object.entries(langCounts).sort((a, b) => b[1] - a[1])[0]?.[0] ?? 'other';
57+
7958
return {
80-
name,
81-
path,
82-
value: data.count,
83-
itemStyle: { color: LANG_COLORS[dominantLang] ?? '#888' },
59+
name: n.name,
60+
value: n.nodeCount,
61+
children: children.length > 0 ? children : undefined,
62+
itemStyle: { color: LANG_COLORS[dominant] ?? '#666' },
8463
};
85-
});
86-
}
64+
}
65+
const lang = inferLang(n.name);
66+
return {
67+
name: n.name,
68+
value: Math.max(n.nodeCount, 1),
69+
itemStyle: { color: LANG_COLORS[lang] ?? '#666' },
70+
};
71+
});
72+
}
8773

88-
return toTreeNodes(root, '');
74+
function collectLanguages(nodes: FileTreeNode[]): string[] {
75+
const langs = new Set<string>();
76+
function walk(items: FileTreeNode[]) {
77+
for (const item of items) {
78+
if (item.type === 'file') {
79+
const lang = inferLang(item.name);
80+
if (lang !== 'other') langs.add(lang);
81+
}
82+
if (item.children) walk(item.children);
83+
}
84+
}
85+
walk(nodes);
86+
return Array.from(langs).sort();
8987
}
9088

9189
export default function CodebaseMap() {
9290
const { isDark } = useTheme();
9391
const [langFilter, setLangFilter] = useState<string | undefined>(undefined);
94-
const { data: allNodesData, loading, error } = useApi<NodesListResponse>(
95-
() => api.getNodes(undefined, undefined, 10000, 0), []
92+
93+
const { data: treeData, loading, error } = useApi<FileTreeResponse>(
94+
() => api.getFileTree(), []
9695
);
9796

98-
const nodes = useMemo(() => {
99-
const all = allNodesData?.nodes ?? [];
100-
return all.filter(n => n.file_path);
101-
}, [allNodesData]);
102-
103-
const uniqueLangs = useMemo(() => {
104-
const langs = new Set<string>();
105-
for (const n of nodes) {
106-
const ext = n.file_path?.split('.').pop()?.toLowerCase() ?? '';
107-
const lang = EXT_TO_LANG[ext];
108-
if (lang) langs.add(lang);
109-
}
110-
return Array.from(langs).sort();
111-
}, [nodes]);
112-
113-
const filteredNodes = useMemo(() => {
114-
if (!langFilter) return nodes;
115-
const matchExts = Object.entries(EXT_TO_LANG)
116-
.filter(([, lang]) => lang === langFilter)
117-
.map(([ext]) => ext);
118-
return nodes.filter(n => {
119-
const ext = n.file_path?.split('.').pop()?.toLowerCase() ?? '';
120-
return matchExts.includes(ext);
121-
});
122-
}, [nodes, langFilter]);
97+
const tree = treeData?.tree ?? [];
98+
const totalFiles = treeData?.total_files ?? 0;
99+
const uniqueLangs = useMemo(() => collectLanguages(tree), [tree]);
123100

124-
const treemapData = useMemo(() => buildTreemap(filteredNodes), [filteredNodes]);
101+
const treemapData = useMemo(() => {
102+
// TODO: apply langFilter if needed
103+
return fileTreeToECharts(tree);
104+
}, [tree, langFilter]);
125105

126106
const chartOption = useMemo(() => ({
127107
tooltip: {
@@ -138,13 +118,13 @@ export default function CodebaseMap() {
138118
drillDownIcon: '▶',
139119
breadcrumb: {
140120
show: true,
141-
top: 0,
142-
left: 0,
121+
top: 4,
122+
left: 4,
143123
itemStyle: {
144-
color: isDark ? '#1f1f38' : '#f5f5f5',
145-
borderColor: isDark ? '#2d2d4a' : '#d9d9d9',
146-
textStyle: { color: isDark ? '#e0e0e0' : '#333' },
124+
color: isDark ? '#2a2a2e' : '#f5f5f5',
125+
borderColor: isDark ? '#3f3f46' : '#d9d9d9',
147126
},
127+
textStyle: { color: isDark ? '#e0e0e0' : '#333' },
148128
},
149129
levels: [
150130
{
@@ -167,29 +147,18 @@ export default function CodebaseMap() {
167147
borderWidth: 1,
168148
gapWidth: 1,
169149
},
170-
upperLabel: {
171-
show: true,
172-
height: 22,
173-
fontSize: 12,
174-
},
150+
upperLabel: { show: true, height: 22, fontSize: 12 },
175151
},
176152
{
177153
itemStyle: {
178154
borderColor: isDark ? '#666' : '#eee',
179155
borderWidth: 0.5,
180156
gapWidth: 0.5,
181157
},
182-
label: {
183-
show: true,
184-
fontSize: 11,
185-
},
158+
label: { show: true, fontSize: 11 },
186159
},
187160
],
188-
label: {
189-
show: true,
190-
formatter: '{b}',
191-
fontSize: 12,
192-
},
161+
label: { show: true, formatter: '{b}', fontSize: 12 },
193162
}],
194163
}), [treemapData, isDark]);
195164

@@ -203,18 +172,17 @@ export default function CodebaseMap() {
203172

204173
return (
205174
<div style={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 96px)', margin: '-16px -24px', padding: '8px 16px 0' }}>
206-
{/* Top bar: title + filter */}
207175
<div style={{
208176
display: 'flex',
209177
justifyContent: 'space-between',
210178
alignItems: 'center',
211-
marginBottom: 8,
179+
marginBottom: 4,
212180
flexShrink: 0,
213181
}}>
214182
<Space>
215183
<Typography.Title level={4} style={{ margin: 0 }}>Codebase Map</Typography.Title>
216184
<Typography.Text type="secondary">
217-
{filteredNodes.length.toLocaleString()} nodes · {uniqueLangs.length} languages
185+
{totalFiles.toLocaleString()} files · {uniqueLangs.length} languages
218186
</Typography.Text>
219187
</Space>
220188
<Select
@@ -227,7 +195,6 @@ export default function CodebaseMap() {
227195
/>
228196
</div>
229197

230-
{/* Treemap fills remaining space */}
231198
<div style={{ flex: 1, minHeight: 0 }}>
232199
{treemapData.length > 0 ? (
233200
<ReactECharts

src/main/frontend/src/pages/Dashboard.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,19 +138,23 @@ export default function Dashboard() {
138138
}
139139
}
140140

141-
// Edge kind breakdown from detailed stats
141+
// Edge kind breakdown — try multiple sources from stats API
142142
const edgeKindBreakdown: Record<string, number> = {};
143-
if (detailed && typeof detailed === 'object') {
143+
if (computed && typeof computed === 'object') {
144+
// Check for edges_by_kind in graph section
145+
const graph = (computed as unknown as Record<string, unknown>).graph as Record<string, unknown> | undefined;
146+
if (graph?.edges_by_kind && typeof graph.edges_by_kind === 'object') {
147+
Object.assign(edgeKindBreakdown, flattenToRecord(graph.edges_by_kind));
148+
}
149+
}
150+
if (detailed && typeof detailed === 'object' && Object.keys(edgeKindBreakdown).length === 0) {
144151
const d = detailed as Record<string, unknown>;
145152
const graph = d.graph as Record<string, unknown> | undefined;
146153
if (graph?.edges_by_kind && typeof graph.edges_by_kind === 'object') {
147154
Object.assign(edgeKindBreakdown, flattenToRecord(graph.edges_by_kind));
148155
}
149-
// Fallback: try connections section
150-
if (Object.keys(edgeKindBreakdown).length === 0 && d.connections) {
151-
Object.assign(edgeKindBreakdown, flattenToRecord(d.connections));
152-
}
153156
}
157+
// If still empty, the API doesn't provide edge breakdown — Edges card won't be clickable
154158

155159
if (loading) {
156160
return <div style={{ textAlign: 'center', padding: 80 }}><Spin size="large" /></div>;

0 commit comments

Comments
 (0)