Skip to content

Commit 0f6e728

Browse files
committed
feat: add WorkerSparkline
1 parent bb48c5b commit 0f6e728

3 files changed

Lines changed: 154 additions & 7 deletions

File tree

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { trpc } from '@/api/trpc';
2+
import { useCurrentWorkspaceId } from '@/store/user';
3+
import { Sparkline } from '@/components/chart/Sparkline';
4+
import React from 'react';
5+
import { getDateArray } from '@tianji/shared';
6+
import dayjs from 'dayjs';
7+
8+
export const WorkerSparkline: React.FC<{ workerId: string }> = React.memo(
9+
({ workerId }) => {
10+
const workspaceId = useCurrentWorkspaceId();
11+
12+
// Use last 24 hours data
13+
const startDate = dayjs().subtract(24, 'hour').startOf('hour');
14+
const endDate = dayjs().endOf('hour');
15+
16+
const { data = [], isLoading } = trpc.worker.getExecutionTrend.useQuery(
17+
{
18+
workspaceId,
19+
workerId,
20+
},
21+
{
22+
select(data) {
23+
if (!data || data.length === 0) {
24+
// Return 8 empty data points for consistent display
25+
return [
26+
{ value: 0 },
27+
{ value: 0 },
28+
{ value: 0 },
29+
{ value: 0 },
30+
{ value: 0 },
31+
{ value: 0 },
32+
{ value: 0 },
33+
{ value: 0 },
34+
];
35+
}
36+
37+
return getDateArray(
38+
data.map((item) => ({
39+
date: item.date,
40+
value: item.value,
41+
})),
42+
startDate,
43+
endDate,
44+
'hour'
45+
);
46+
},
47+
refetchInterval: 5 * 60 * 1000, // Refresh every 5 minutes
48+
trpc: {
49+
context: {
50+
skipBatch: true,
51+
},
52+
},
53+
}
54+
);
55+
56+
if (isLoading) {
57+
return (
58+
<div className="flex h-6 w-20 items-center justify-center">
59+
<span className="text-muted-foreground text-xs">Loading...</span>
60+
</div>
61+
);
62+
}
63+
64+
if (!data || data.length === 0) {
65+
return (
66+
<div className="flex h-6 w-20 items-center justify-center">
67+
<span className="text-muted-foreground text-xs">No data</span>
68+
</div>
69+
);
70+
}
71+
72+
// Extract counts for sparkline
73+
const sparklineData = data.map((item) => item.value || 0);
74+
75+
return (
76+
<div className="flex items-center gap-2">
77+
<Sparkline
78+
data={sparklineData}
79+
width={80}
80+
height={24}
81+
strokeWidth={1.5}
82+
showGradient={true}
83+
/>
84+
</div>
85+
);
86+
}
87+
);
88+
WorkerSparkline.displayName = 'WorkerSparkline';

src/client/routes/worker.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { trpc } from '@/api/trpc';
22
import { CommonHeader } from '@/components/CommonHeader';
3-
import { CommonList } from '@/components/CommonList';
3+
import { CommonList, CommonListItem } from '@/components/CommonList';
44
import { CommonWrapper } from '@/components/CommonWrapper';
55
import { Button } from '@/components/ui/button';
66
import { Layout } from '@/components/layout';
@@ -16,6 +16,8 @@ import {
1616
import { LuPlus } from 'react-icons/lu';
1717
import { useEvent } from '@/hooks/useEvent';
1818
import { useDataReady } from '@/hooks/useDataReady';
19+
import { useMemo } from 'react';
20+
import { WorkerSparkline } from '@/components/worker/WorkerSparkline';
1921

2022
export const Route = createFileRoute('/worker')({
2123
beforeLoad: routeAuthBeforeLoad,
@@ -34,12 +36,16 @@ function PageComponent() {
3436
});
3537
const hasAdminPermission = useHasAdminPermission();
3638

37-
const items = data.map((item) => ({
38-
id: item.id,
39-
title: item.name,
40-
content: item.description || '',
41-
href: `/worker/${item.id}`,
42-
}));
39+
const items: CommonListItem[] = useMemo(() => {
40+
return data.length > 0
41+
? data.map((item) => ({
42+
id: item.id,
43+
title: item.name,
44+
href: `/worker/${item.id}`,
45+
content: <WorkerSparkline workerId={item.id} />,
46+
}))
47+
: [];
48+
}, [data]);
4349

4450
useDataReady(
4551
() => data.length > 0,
@@ -88,6 +94,7 @@ function PageComponent() {
8894
<CommonList
8995
hasSearch={true}
9096
items={items}
97+
direction="horizontal"
9198
isLoading={isLoading}
9299
emptyDescription={t(
93100
'No function workers yet. Create one to run JavaScript code in an isolated environment.'

src/server/trpc/routers/worker.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,58 @@ export const workerRouter = router({
379379
avgCpuTime: result.avgCpuTime,
380380
};
381381
}),
382+
383+
// Get execution trend for sparkline (last 24 hours, grouped by hour)
384+
getExecutionTrend: workspaceProcedure
385+
.input(
386+
z.object({
387+
workerId: z.cuid2(),
388+
})
389+
)
390+
.query(async ({ input }) => {
391+
const { workerId, workspaceId } = input;
392+
393+
// Verify worker exists and belongs to workspace
394+
const worker = await prisma.functionWorker.findUnique({
395+
where: {
396+
id: workerId,
397+
workspaceId,
398+
},
399+
});
400+
401+
if (!worker) {
402+
throw new Error('Worker not found');
403+
}
404+
405+
// Calculate time range for last 24 hours
406+
const endDate = new Date();
407+
const startDate = new Date();
408+
startDate.setHours(startDate.getHours() - 24);
409+
410+
// Query execution counts grouped by hour
411+
const trendData = await prisma.$queryRaw<
412+
Array<{
413+
hour: Date;
414+
count: bigint;
415+
}>
416+
>`
417+
SELECT
418+
DATE_TRUNC('hour', "createdAt") as hour,
419+
COUNT(*)::int as count
420+
FROM "FunctionWorkerExecution"
421+
WHERE "workerId" = ${workerId}
422+
AND "createdAt" >= ${startDate}
423+
AND "createdAt" <= ${endDate}
424+
GROUP BY DATE_TRUNC('hour', "createdAt")
425+
ORDER BY hour ASC
426+
`;
427+
428+
// Convert to the format expected by frontend
429+
return trendData.map((item) => ({
430+
date: item.hour.toISOString(),
431+
value: Number(item.count),
432+
}));
433+
}),
382434
testCode: workspaceAdminProcedure
383435
.input(
384436
z.object({

0 commit comments

Comments
 (0)