Skip to content

Commit 61a7bbc

Browse files
committed
feature: added a sketch of the new engine
1 parent b2afde6 commit 61a7bbc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1735
-1220
lines changed

Pipfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[[source]]
2+
url = "https://pypi.org/simple"
3+
verify_ssl = true
4+
name = "pypi"
5+
6+
[packages]
7+
8+
[dev-packages]
9+
10+
[requires]
11+
python_version = "3.13"

frontend/src/App.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const nav = [
3333
{ to: "/", label: "Overview" },
3434
{ to: "/authors", label: "Authors" },
3535
{ to: "/commits", label: "Commits" },
36+
{ to: "/codebase", label: "Codebase" },
3637
];
3738
</script>
3839

frontend/src/api.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/** Backend statistics (e.g. http://localhost:8000/api/statistics when served by git-analytics). */
2+
export const API_STATISTICS = "/api/statistics";

frontend/src/router/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ const routes = [
1616
name: "commits",
1717
component: () => import("../views/CommitsView.vue"),
1818
},
19+
{
20+
path: "/codebase",
21+
name: "codebase",
22+
component: () => import("../views/CodebaseView.vue"),
23+
},
1924
];
2025

2126
export default createRouter({

frontend/src/views/AuthorsView.vue

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import * as echarts from "echarts";
33
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
44
5+
import { API_STATISTICS } from "../api.js";
6+
57
const loading = ref(true);
68
const error = ref(null);
79
const stats = ref(null);
@@ -13,7 +15,7 @@ async function load() {
1315
loading.value = true;
1416
error.value = null;
1517
try {
16-
const r = await fetch("/api/statistics");
18+
const r = await fetch(API_STATISTICS);
1719
if (!r.ok) {
1820
throw new Error(r.statusText || String(r.status));
1921
}
@@ -25,7 +27,22 @@ async function load() {
2527
}
2628
}
2729
28-
const authorsStats = computed(() => stats.value?.authors_statistics?.authors ?? {});
30+
/** New API: activity.authors_statistics is author -> stats; legacy: authors_statistics.authors */
31+
const authorsStats = computed(() => {
32+
const root = stats.value;
33+
const fromActivity = root?.activity?.authors_statistics;
34+
const legacy = root?.authors_statistics;
35+
if (fromActivity != null) {
36+
if (fromActivity.authors != null && typeof fromActivity.authors === "object") {
37+
return fromActivity.authors;
38+
}
39+
return fromActivity;
40+
}
41+
if (legacy?.authors != null) {
42+
return legacy.authors;
43+
}
44+
return {};
45+
});
2946
3047
/** Pie slices: commit count per author (desc). */
3148
const commitsByAuthor = computed(() => {
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
<script setup>
2+
import * as echarts from "echarts";
3+
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
4+
5+
import { API_STATISTICS } from "../api.js";
6+
7+
const loading = ref(true);
8+
const error = ref(null);
9+
const stats = ref(null);
10+
11+
const linesChartRef = ref(null);
12+
let linesChartInstance = null;
13+
14+
const filesChartRef = ref(null);
15+
let filesChartInstance = null;
16+
17+
async function load() {
18+
loading.value = true;
19+
error.value = null;
20+
try {
21+
const r = await fetch(API_STATISTICS);
22+
if (!r.ok) {
23+
throw new Error(r.statusText || String(r.status));
24+
}
25+
stats.value = await r.json();
26+
} catch (e) {
27+
error.value = e instanceof Error ? e.message : "Failed to load statistics";
28+
} finally {
29+
loading.value = false;
30+
}
31+
}
32+
33+
const codebase = computed(() => {
34+
const cb = stats.value?.codebase;
35+
return cb && typeof cb === "object" ? cb : null;
36+
});
37+
38+
const linesByExtension = computed(() => codebase.value?.lines_by_extension ?? {});
39+
const filesByExtension = computed(() => codebase.value?.files_by_extension ?? {});
40+
41+
/**
42+
* @param {Record<string, number>} raw
43+
* @param {{ valueLabel: string; seriesName: string }} labels
44+
*/
45+
function buildHorizontalBarOption(raw, { valueLabel, seriesName }) {
46+
const entries = Object.entries(raw).sort((a, b) => b[1] - a[1]);
47+
if (entries.length === 0) {
48+
return null;
49+
}
50+
51+
const categories = entries.map(([ext]) => ext);
52+
const values = entries.map(([, n]) => n);
53+
54+
const barCount = categories.length;
55+
const gridBottom = barCount > 12 ? "14%" : "8%";
56+
57+
return {
58+
tooltip: {
59+
trigger: "axis",
60+
axisPointer: { type: "shadow" },
61+
formatter: (params) => {
62+
const p = Array.isArray(params) ? params[0] : params;
63+
if (!p) {
64+
return "";
65+
}
66+
return `${p.name}<br/>${p.value.toLocaleString()} ${valueLabel}`;
67+
},
68+
},
69+
grid: {
70+
left: "3%",
71+
right: "12%",
72+
top: "3%",
73+
bottom: gridBottom,
74+
containLabel: true,
75+
},
76+
xAxis: {
77+
type: "value",
78+
minInterval: 1,
79+
},
80+
yAxis: {
81+
type: "category",
82+
data: categories,
83+
inverse: true,
84+
axisLabel: {
85+
width: 120,
86+
overflow: "truncate",
87+
},
88+
},
89+
series: [
90+
{
91+
name: seriesName,
92+
type: "bar",
93+
data: values,
94+
emphasis: { focus: "series" },
95+
label: {
96+
show: barCount <= 24,
97+
position: "right",
98+
formatter: (p) => Number(p.value).toLocaleString(),
99+
},
100+
},
101+
],
102+
};
103+
}
104+
105+
function chartHeightPx(keyCount) {
106+
return Math.min(560, Math.max(280, keyCount * 28));
107+
}
108+
109+
function syncLinesChart() {
110+
const raw = linesByExtension.value;
111+
if (Object.keys(raw).length === 0) {
112+
linesChartInstance?.dispose();
113+
linesChartInstance = null;
114+
return;
115+
}
116+
if (!linesChartRef.value) {
117+
return;
118+
}
119+
const opt = buildHorizontalBarOption(raw, {
120+
valueLabel: "lines",
121+
seriesName: "Lines",
122+
});
123+
if (!opt) {
124+
linesChartInstance?.clear();
125+
return;
126+
}
127+
if (!linesChartInstance) {
128+
linesChartInstance = echarts.init(linesChartRef.value);
129+
}
130+
linesChartInstance.setOption(opt, true);
131+
}
132+
133+
function syncFilesChart() {
134+
const raw = filesByExtension.value;
135+
if (Object.keys(raw).length === 0) {
136+
filesChartInstance?.dispose();
137+
filesChartInstance = null;
138+
return;
139+
}
140+
if (!filesChartRef.value) {
141+
return;
142+
}
143+
const opt = buildHorizontalBarOption(raw, {
144+
valueLabel: "files",
145+
seriesName: "Files",
146+
});
147+
if (!opt) {
148+
filesChartInstance?.clear();
149+
return;
150+
}
151+
if (!filesChartInstance) {
152+
filesChartInstance = echarts.init(filesChartRef.value);
153+
}
154+
filesChartInstance.setOption(opt, true);
155+
}
156+
157+
function syncCharts() {
158+
syncLinesChart();
159+
syncFilesChart();
160+
}
161+
162+
function onResize() {
163+
linesChartInstance?.resize();
164+
filesChartInstance?.resize();
165+
}
166+
167+
watch(
168+
[stats, linesByExtension, filesByExtension],
169+
async () => {
170+
await nextTick();
171+
syncCharts();
172+
},
173+
{ deep: true, flush: "post" },
174+
);
175+
176+
onMounted(() => {
177+
load();
178+
window.addEventListener("resize", onResize);
179+
});
180+
181+
onUnmounted(() => {
182+
window.removeEventListener("resize", onResize);
183+
linesChartInstance?.dispose();
184+
linesChartInstance = null;
185+
filesChartInstance?.dispose();
186+
filesChartInstance = null;
187+
});
188+
189+
const totalLines = computed(() => stats.value?.codebase?.total_lines);
190+
const totalFiles = computed(() => stats.value?.codebase?.total_files);
191+
</script>
192+
193+
<template>
194+
<div class="space-y-6">
195+
<h1 class="text-3xl font-bold">Codebase</h1>
196+
197+
<div v-if="loading" class="flex justify-center py-16">
198+
<span class="loading loading-lg loading-spinner text-primary" />
199+
</div>
200+
201+
<div v-else-if="error" role="alert" class="alert alert-error">
202+
<span>{{ error }}</span>
203+
</div>
204+
205+
<div v-else class="space-y-4">
206+
<p class="text-base-content/70 max-w-2xl text-sm">
207+
Files and lines by extension (current tree scan).
208+
</p>
209+
210+
<div
211+
v-if="totalLines != null || totalFiles != null"
212+
class="text-sm text-base-content/80 flex flex-wrap gap-4"
213+
>
214+
<span v-if="totalFiles != null"
215+
><span class="font-semibold text-base-content">Files:</span>
216+
{{ totalFiles.toLocaleString() }}</span
217+
>
218+
<span v-if="totalLines != null"
219+
><span class="font-semibold text-base-content">Lines:</span>
220+
{{ totalLines.toLocaleString() }}</span
221+
>
222+
</div>
223+
224+
<div class="card card-bordered bg-base-100 shadow-sm">
225+
<div class="border-b border-base-300 px-4 py-3 flex items-center gap-2">
226+
<span class="font-semibold">Lines by extension</span>
227+
<div
228+
class="tooltip tooltip-right before:max-w-xs before:text-left before:whitespace-normal"
229+
data-tip="Number of text lines counted per file extension in the repository (excluding ignored paths and binary files)."
230+
>
231+
<button
232+
type="button"
233+
class="btn btn-ghost btn-xs btn-circle min-h-0 h-6 w-6 p-0"
234+
aria-label="About lines by extension"
235+
>
236+
<svg
237+
xmlns="http://www.w3.org/2000/svg"
238+
class="h-4 w-4 opacity-60"
239+
fill="none"
240+
viewBox="0 0 24 24"
241+
stroke="currentColor"
242+
stroke-width="2"
243+
>
244+
<path
245+
stroke-linecap="round"
246+
stroke-linejoin="round"
247+
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
248+
/>
249+
</svg>
250+
</button>
251+
</div>
252+
</div>
253+
<div class="card-body p-4">
254+
<div
255+
v-if="Object.keys(linesByExtension).length === 0"
256+
class="text-sm text-base-content/60 py-12 text-center"
257+
>
258+
No extension data in codebase report.
259+
</div>
260+
<div
261+
v-else
262+
ref="linesChartRef"
263+
class="w-full"
264+
:style="{ minHeight: `${chartHeightPx(Object.keys(linesByExtension).length)}px` }"
265+
/>
266+
</div>
267+
</div>
268+
269+
<div class="card card-bordered bg-base-100 shadow-sm">
270+
<div class="border-b border-base-300 px-4 py-3 flex items-center gap-2">
271+
<span class="font-semibold">Files by extension</span>
272+
<div
273+
class="tooltip tooltip-right before:max-w-xs before:text-left before:whitespace-normal"
274+
data-tip="Number of files per extension in the repository (excluding ignored paths and binary files)."
275+
>
276+
<button
277+
type="button"
278+
class="btn btn-ghost btn-xs btn-circle min-h-0 h-6 w-6 p-0"
279+
aria-label="About files by extension"
280+
>
281+
<svg
282+
xmlns="http://www.w3.org/2000/svg"
283+
class="h-4 w-4 opacity-60"
284+
fill="none"
285+
viewBox="0 0 24 24"
286+
stroke="currentColor"
287+
stroke-width="2"
288+
>
289+
<path
290+
stroke-linecap="round"
291+
stroke-linejoin="round"
292+
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
293+
/>
294+
</svg>
295+
</button>
296+
</div>
297+
</div>
298+
<div class="card-body p-4">
299+
<div
300+
v-if="Object.keys(filesByExtension).length === 0"
301+
class="text-sm text-base-content/60 py-12 text-center"
302+
>
303+
No file count data in codebase report.
304+
</div>
305+
<div
306+
v-else
307+
ref="filesChartRef"
308+
class="w-full"
309+
:style="{ minHeight: `${chartHeightPx(Object.keys(filesByExtension).length)}px` }"
310+
/>
311+
</div>
312+
</div>
313+
</div>
314+
</div>
315+
</template>

0 commit comments

Comments
 (0)