Skip to content

Commit b918e69

Browse files
committed
feat: add dashboard component and API integration for car metrics overview
1 parent f5275be commit b918e69

3 files changed

Lines changed: 570 additions & 1 deletion

File tree

dev-demo/api.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,63 @@ import { Express, Response } from "express";
22
import { IAdminForth, IAdminUserExpressRequest } from "adminforth";
33
import * as z from "zod";
44

5+
const DASHBOARD_CAR_SOURCES = [
6+
{ resourceId: 'cars_sl', label: 'SQLite' },
7+
{ resourceId: 'cars_mysql', label: 'MySQL' },
8+
{ resourceId: 'cars_pg', label: 'PostgreSQL' },
9+
{ resourceId: 'cars_mongo', label: 'MongoDB' },
10+
{ resourceId: 'cars_ch', label: 'ClickHouse' },
11+
] as const;
12+
13+
type DashboardCarRecord = {
14+
model: string;
15+
price: string | number;
16+
listed: boolean;
17+
mileage: string | number | null;
18+
production_year: number;
19+
engine_type: string;
20+
body_type: string;
21+
};
22+
23+
function toNumber(value: string | number | null): number {
24+
if (typeof value === 'number') {
25+
return value;
26+
}
27+
if (typeof value === 'string') {
28+
return Number(value);
29+
}
30+
return 0;
31+
}
32+
33+
function roundMetric(value: number): number {
34+
return Number(value.toFixed(0));
35+
}
36+
37+
function getBreakdown(records: DashboardCarRecord[], fieldName: 'engine_type' | 'body_type') {
38+
const counts: Record<string, number> = {};
39+
40+
records.forEach((record) => {
41+
counts[record[fieldName]] = (counts[record[fieldName]] ?? 0) + 1;
42+
});
43+
44+
return Object.entries(counts)
45+
.map(([label, amount]) => ({ label, amount }))
46+
.sort((left, right) => right.amount - left.amount || left.label.localeCompare(right.label));
47+
}
48+
49+
function getTopModels(records: DashboardCarRecord[]) {
50+
const counts: Record<string, number> = {};
51+
52+
records.forEach((record) => {
53+
counts[record.model] = (counts[record.model] ?? 0) + 1;
54+
});
55+
56+
return Object.entries(counts)
57+
.map(([x, count]) => ({ x, count }))
58+
.sort((left, right) => right.count - left.count || left.x.localeCompare(right.x))
59+
.slice(0, 6);
60+
}
61+
562
export function initApi(app: Express, admin: IAdminForth) {
663
app.get(`${admin.config.baseUrl}/api/hello/`,
764
admin.express.withSchema(
@@ -19,4 +76,100 @@ export function initApi(app: Express, admin: IAdminForth) {
1976
)
2077
)
2178
);
79+
80+
app.get(`${admin.config.baseUrl}/api/dashboard/`,
81+
admin.express.withSchema(
82+
{
83+
description: 'Returns aggregated car metrics used by the dev demo dashboard custom page.',
84+
response: z.object({
85+
summary: z.object({
86+
totalCars: z.number(),
87+
listedCars: z.number(),
88+
unlistedCars: z.number(),
89+
averagePrice: z.number(),
90+
averageMileage: z.number(),
91+
}),
92+
operations: z.object({
93+
adminUsers: z.number(),
94+
sessions: z.number(),
95+
backgroundJobs: z.number(),
96+
}),
97+
sourceTotals: z.array(
98+
z.object({
99+
source: z.string(),
100+
count: z.number(),
101+
listed: z.number(),
102+
avgPrice: z.number(),
103+
})
104+
),
105+
engineTypeBreakdown: z.array(
106+
z.object({
107+
label: z.string(),
108+
amount: z.number(),
109+
})
110+
),
111+
bodyTypeBreakdown: z.array(
112+
z.object({
113+
label: z.string(),
114+
amount: z.number(),
115+
})
116+
),
117+
topModels: z.array(
118+
z.object({
119+
x: z.string(),
120+
count: z.number(),
121+
})
122+
),
123+
}),
124+
},
125+
admin.express.authorize(
126+
async (_req: IAdminUserExpressRequest, res: Response) => {
127+
const [carsBySource, adminUsers, sessions, backgroundJobs] = await Promise.all([
128+
Promise.all(
129+
DASHBOARD_CAR_SOURCES.map(async ({ resourceId, label }) => {
130+
const records = await admin.resource(resourceId).list([]) as DashboardCarRecord[];
131+
132+
return { label, records };
133+
})
134+
),
135+
admin.resource('adminuser').count(),
136+
admin.resource('sessions').count(),
137+
admin.resource('jobs').count(),
138+
]);
139+
140+
const allCars = carsBySource.flatMap(({ records }) => records);
141+
const totalCars = allCars.length;
142+
const listedCars = allCars.filter((record) => record.listed).length;
143+
const totalPrice = allCars.reduce((sum, record) => sum + toNumber(record.price), 0);
144+
const totalMileage = allCars.reduce((sum, record) => sum + toNumber(record.mileage), 0);
145+
146+
res.json({
147+
summary: {
148+
totalCars,
149+
listedCars,
150+
unlistedCars: totalCars - listedCars,
151+
averagePrice: totalCars > 0 ? roundMetric(totalPrice / totalCars) : 0,
152+
averageMileage: totalCars > 0 ? roundMetric(totalMileage / totalCars) : 0,
153+
},
154+
operations: {
155+
adminUsers,
156+
sessions,
157+
backgroundJobs,
158+
},
159+
sourceTotals: carsBySource.map(({ label, records }) => ({
160+
source: label,
161+
count: records.length,
162+
listed: records.filter((record) => record.listed).length,
163+
avgPrice: records.length > 0
164+
? roundMetric(records.reduce((sum, record) => sum + toNumber(record.price), 0) / records.length)
165+
: 0,
166+
})),
167+
engineTypeBreakdown: getBreakdown(allCars, 'engine_type'),
168+
bodyTypeBreakdown: getBreakdown(allCars, 'body_type'),
169+
topModels: getTopModels(allCars),
170+
});
171+
}
172+
)
173+
)
174+
);
22175
}

0 commit comments

Comments
 (0)