Skip to content

Commit eee5f84

Browse files
committed
feat: add worker execution replay feature
1 parent e4b61b2 commit eee5f84

6 files changed

Lines changed: 87 additions & 21 deletions

File tree

src/client/components/worker/WorkerEditForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ const formSchema = z
6060
}
6161
);
6262

63-
const defaultCode = `async function fetch(params) {
63+
const defaultCode = `async function fetch(payload, ctx) {
6464
return 'Hello, World!';
6565
}
6666
`;

src/client/components/worker/WorkerExecutionDetail.tsx

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import React from 'react';
22
import { useTranslation } from '@i18next-toolkit/react';
33
import { Badge } from '@/components/ui/badge';
4+
import { Button } from '@/components/ui/button';
45
import { ScrollArea } from '@/components/ui/scroll-area';
56
import { SheetDataSection } from '@/components/ui/sheet';
67
import dayjs from 'dayjs';
78
import { cn } from '@/utils/style';
89
import { formatDate } from '@/utils/date';
910
import { DataRender } from '../DataRender';
1011
import { drop } from 'lodash-es';
12+
import { LuPlay } from 'react-icons/lu';
13+
import { useEventWithLoading } from '@/hooks/useEvent';
14+
import { AlertConfirm } from '../AlertConfirm';
1115

1216
interface WorkerExecutionDetailProps {
1317
vertical?: boolean;
@@ -23,13 +27,20 @@ interface WorkerExecutionDetailProps {
2327
responsePayload?: unknown;
2428
logs?: (string | number)[][] | null;
2529
};
30+
onReplay?: (payload: unknown) => Promise<void>;
2631
}
2732

2833
export const WorkerExecutionDetail: React.FC<WorkerExecutionDetailProps> =
2934
React.memo((props) => {
30-
const { vertical = false, execution } = props;
35+
const { vertical = false, execution, onReplay } = props;
3136
const { t } = useTranslation();
3237

38+
const [handleReplay, isReplaying] = useEventWithLoading(async () => {
39+
if (onReplay) {
40+
await onReplay(execution.requestPayload);
41+
}
42+
});
43+
3344
return (
3445
<ScrollArea className="h-full">
3546
<div className="space-y-4 p-1">
@@ -76,11 +87,34 @@ export const WorkerExecutionDetail: React.FC<WorkerExecutionDetailProps> =
7687

7788
{execution.requestPayload !== null &&
7889
execution.requestPayload !== undefined && (
79-
<SheetDataSection label={t('Request')}>
80-
<div className="mt-1 max-h-[400px] overflow-auto rounded-md bg-gray-100 p-3 text-gray-900 dark:bg-gray-900 dark:text-gray-100">
81-
<DataRender type="json" value={execution.requestPayload} />
82-
</div>
83-
</SheetDataSection>
90+
<>
91+
<SheetDataSection label={t('Request')}>
92+
<div className="mt-1 max-h-[400px] overflow-auto rounded-md bg-gray-100 p-3 text-gray-900 dark:bg-gray-900 dark:text-gray-100">
93+
<DataRender type="json" value={execution.requestPayload} />
94+
</div>
95+
</SheetDataSection>
96+
97+
{onReplay && (
98+
<div className="flex justify-end">
99+
<AlertConfirm
100+
title={t('Replay Worker Execution')}
101+
description={t(
102+
'Are you sure you want to replay this execution with the same payload?'
103+
)}
104+
onConfirm={handleReplay}
105+
>
106+
<Button
107+
size="sm"
108+
Icon={LuPlay}
109+
loading={isReplaying}
110+
disabled={isReplaying}
111+
>
112+
{t('Replay')}
113+
</Button>
114+
</AlertConfirm>
115+
</div>
116+
)}
117+
</>
84118
)}
85119

86120
{execution.responsePayload !== null &&

src/client/routes/worker/$workerId/index.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,20 @@ function PageComponent() {
167167
refetchExecutions();
168168
});
169169

170+
const handleReplay = useEvent(async (payload: unknown) => {
171+
if (!worker) {
172+
return;
173+
}
174+
175+
await executeMutation.mutateAsync({
176+
workspaceId,
177+
workerId: worker.id,
178+
payload: payload as Record<string, any> | undefined,
179+
});
180+
refetchExecutions();
181+
setSelectedExecutionIndex(-1);
182+
});
183+
170184
const handleEdit = useEvent(() => {
171185
navigate({
172186
to: '/worker/$workerId/edit',
@@ -542,6 +556,7 @@ function PageComponent() {
542556
<WorkerExecutionDetail
543557
vertical={true}
544558
execution={selectedExecution}
559+
onReplay={handleReplay}
545560
/>
546561
)}
547562
</SheetContent>

src/client/routes/worker_/$workerId/editor.tsx

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { WorkerExecutionsTable } from '@/components/worker/WorkerExecutionsTable
1111
import { WorkerExecutionDetail } from '@/components/worker/WorkerExecutionDetail';
1212
import { WorkerApiPreview } from '@/components/worker/WorkerApiPreview';
1313
import { NavigationBlocker } from '@/components/NavigationBlocker';
14-
import { AppRouterOutput, trpc } from '@/api/trpc';
14+
import { AppRouterOutput, defaultErrorHandler, trpc } from '@/api/trpc';
1515
import { useEvent } from '@/hooks/useEvent';
1616
import { Loading } from '@/components/Loading';
1717
import { ErrorTip } from '@/components/ErrorTip';
@@ -79,13 +79,11 @@ function PageComponent() {
7979
});
8080

8181
const updateMutation = trpc.worker.upsert.useMutation({
82-
onError: (error) => {
83-
toast.error(error.message);
84-
},
85-
onSuccess: () => {
86-
toast.success(t('Worker updated successfully'));
87-
refetchWorker();
88-
},
82+
onError: defaultErrorHandler,
83+
});
84+
85+
const executeMutation = trpc.worker.execute.useMutation({
86+
onError: defaultErrorHandler,
8987
});
9088

9189
useEffect(() => {
@@ -136,6 +134,8 @@ function PageComponent() {
136134
enableCron: worker.enableCron,
137135
cronExpression: worker.cronExpression ?? undefined,
138136
});
137+
toast.success(t('Worker updated successfully'));
138+
refetchWorker();
139139
});
140140

141141
const handleLogRefresh = useEvent(() => {
@@ -157,6 +157,21 @@ function PageComponent() {
157157
});
158158
});
159159

160+
const handleReplay = useEvent(async (payload: unknown) => {
161+
if (!worker) {
162+
return;
163+
}
164+
165+
await executeMutation.mutateAsync({
166+
workspaceId,
167+
workerId: worker.id,
168+
payload: payload as Record<string, any> | undefined,
169+
});
170+
toast.success(t('Worker executed successfully'));
171+
refetchExecutions();
172+
setSelectedExecutionIndex(-1);
173+
});
174+
160175
useEffect(() => {
161176
if (currentPage > 1 && pagination && currentPage > pagination.totalPages) {
162177
setCurrentPage(Math.max(1, pagination.totalPages));
@@ -314,6 +329,7 @@ function PageComponent() {
314329
<WorkerExecutionDetail
315330
vertical={true}
316331
execution={selectedExecution}
332+
onReplay={handleReplay}
317333
/>
318334
) : (
319335
<div className="text-muted-foreground flex flex-1 items-center justify-center text-center text-sm">

src/server/model/worker/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export async function execWorker(
5656
cpuTime,
5757
requestPayload,
5858
responsePayload: result,
59-
error: String(error),
59+
error: error ? String(error) : undefined,
6060
logs: Array.isArray(logger) ? logger : [],
6161
};
6262

src/server/trpc/routers/worker.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -266,10 +266,11 @@ export const workerRouter = router({
266266
.input(
267267
z.object({
268268
workerId: z.cuid2(),
269+
payload: z.record(z.string(), z.any()).optional(),
269270
})
270271
)
271272
.mutation(async ({ input, ctx }) => {
272-
const { workerId, workspaceId } = input;
273+
const { workerId, workspaceId, payload = undefined } = input;
273274

274275
if (!env.enableFunctionWorker) {
275276
throw new Error('Function worker is not enabled');
@@ -287,7 +288,7 @@ export const workerRouter = router({
287288
throw new Error('Worker not found');
288289
}
289290

290-
const execution = await execWorker(worker.code, workerId, undefined, {
291+
const execution = await execWorker(worker.code, workerId, payload, {
291292
type: 'manual',
292293
});
293294

@@ -420,9 +421,9 @@ export const workerRouter = router({
420421
totalExecutions: Number(result.totalExecutions),
421422
successExecutions: Number(result.successExecutions),
422423
failedExecutions: Number(result.failedExecutions),
423-
avgDuration: result.avgDuration,
424-
avgMemoryUsed: result.avgMemoryUsed,
425-
avgCpuTime: result.avgCpuTime,
424+
avgDuration: Number(result.avgDuration),
425+
avgMemoryUsed: Number(result.avgMemoryUsed),
426+
avgCpuTime: Number(result.avgCpuTime),
426427
};
427428
}),
428429

0 commit comments

Comments
 (0)