Skip to content

Commit ddc5e0a

Browse files
committed
feat: add rollback functionality to WorkerRevisionsSection with confirmation dialog and update TRPC router for revision rollback
1 parent d1c2c96 commit ddc5e0a

File tree

3 files changed

+143
-27
lines changed

3 files changed

+143
-27
lines changed

src/client/components/worker/WorkerRevisionsSection.tsx

Lines changed: 77 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useEffect, useMemo, useState } from 'react';
22
import { useTranslation } from '@i18next-toolkit/react';
3-
import { trpc } from '@/api/trpc';
3+
import { trpc, defaultErrorHandler } from '@/api/trpc';
44
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
55
import { Button } from '@/components/ui/button';
66
import { Badge } from '@/components/ui/badge';
@@ -15,25 +15,45 @@ import {
1515
import { ScrollArea } from '@/components/ui/scroll-area';
1616
import { Loading } from '@/components/Loading';
1717
import { WorkerRevisionDiffViewer } from './WorkerRevisionDiffViewer';
18-
import { LuArrowLeftRight, LuArrowRight } from 'react-icons/lu';
18+
import { AlertConfirm } from '@/components/AlertConfirm';
19+
import {
20+
LuArrowLeftRight,
21+
LuArrowRight,
22+
LuUndo2,
23+
LuGitCompareArrows,
24+
LuDiff,
25+
} from 'react-icons/lu';
26+
import { SimpleTooltip } from '@/components/ui/tooltip';
1927
import dayjs from 'dayjs';
2028

2129
interface WorkerRevisionsSectionProps {
2230
workspaceId: string;
2331
workerId: string;
32+
onRollback?: () => void;
2433
}
2534

2635
export const WorkerRevisionsSection: React.FC<WorkerRevisionsSectionProps> = ({
2736
workspaceId,
2837
workerId,
38+
onRollback,
2939
}) => {
3040
const { t } = useTranslation();
31-
const { data: revisions = [], isLoading } = trpc.worker.getRevisions.useQuery(
32-
{
33-
workspaceId,
34-
workerId,
35-
}
36-
);
41+
const {
42+
data: revisions = [],
43+
isLoading,
44+
refetch: refetchRevisions,
45+
} = trpc.worker.getRevisions.useQuery({
46+
workspaceId,
47+
workerId,
48+
});
49+
50+
const rollbackMutation = trpc.worker.rollbackToRevision.useMutation({
51+
onError: defaultErrorHandler,
52+
onSuccess: () => {
53+
refetchRevisions();
54+
onRollback?.();
55+
},
56+
});
3757
const [baseRevisionId, setBaseRevisionId] = useState<string | null>(null);
3858
const [targetRevisionId, setTargetRevisionId] = useState<string | null>(null);
3959

@@ -186,25 +206,55 @@ export const WorkerRevisionsSection: React.FC<WorkerRevisionsSectionProps> = ({
186206
: ''}
187207
</div>
188208
</div>
189-
<div className="flex items-center gap-2">
190-
<Button
191-
type="button"
192-
size="sm"
193-
variant="outline"
194-
onClick={() => setBaseRevisionId(revision.id)}
195-
className="h-7 text-xs"
196-
>
197-
{t('Set base')}
198-
</Button>
199-
<Button
200-
type="button"
201-
size="sm"
202-
variant="outline"
203-
onClick={() => setTargetRevisionId(revision.id)}
204-
className="h-7 text-xs"
205-
>
206-
{t('Set compare')}
207-
</Button>
209+
<div className="flex items-center gap-1">
210+
<SimpleTooltip content={t('Set base')}>
211+
<Button
212+
type="button"
213+
size="icon"
214+
variant={isBase ? 'default' : 'ghost'}
215+
onClick={() => setBaseRevisionId(revision.id)}
216+
className="h-7 w-7"
217+
Icon={LuGitCompareArrows}
218+
/>
219+
</SimpleTooltip>
220+
<SimpleTooltip content={t('Set compare')}>
221+
<Button
222+
type="button"
223+
size="icon"
224+
variant={isTarget ? 'default' : 'ghost'}
225+
onClick={() => setTargetRevisionId(revision.id)}
226+
className="h-7 w-7"
227+
Icon={LuDiff}
228+
/>
229+
</SimpleTooltip>
230+
{!isLatest && (
231+
<AlertConfirm
232+
title={t('Rollback to Revision #{{num}}', {
233+
num: revision.revision,
234+
})}
235+
description={t(
236+
'Are you sure you want to rollback the worker code to this revision? This will create a new revision with the rolled-back code.'
237+
)}
238+
onConfirm={async () => {
239+
await rollbackMutation.mutateAsync({
240+
workspaceId,
241+
workerId,
242+
revisionId: revision.id,
243+
});
244+
}}
245+
>
246+
<SimpleTooltip content={t('Rollback')}>
247+
<Button
248+
type="button"
249+
size="icon"
250+
variant="ghost"
251+
className="h-7 w-7"
252+
Icon={LuUndo2}
253+
loading={rollbackMutation.isPending}
254+
/>
255+
</SimpleTooltip>
256+
</AlertConfirm>
257+
)}
208258
</div>
209259
</div>
210260
);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,7 @@ function PageComponent() {
391391
<WorkerRevisionsSection
392392
workspaceId={workspaceId}
393393
workerId={workerId}
394+
onRollback={refetch}
394395
/>
395396
</TabsContent>
396397

src/server/trpc/routers/worker.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,71 @@ export const workerRouter = router({
621621
return execution;
622622
}),
623623

624+
rollbackToRevision: workspaceAdminProcedure
625+
.meta(
626+
buildWorkerOpenapi({
627+
method: 'POST',
628+
path: '/{workerId}/rollback',
629+
summary: 'Rollback worker to a specific revision',
630+
})
631+
)
632+
.input(
633+
z.object({
634+
workerId: z.string().cuid2(),
635+
revisionId: z.string().cuid2(),
636+
})
637+
)
638+
.output(FunctionWorkerModelSchema)
639+
.mutation(async ({ input, ctx }) => {
640+
const { workerId, revisionId, workspaceId } = input;
641+
642+
const worker = await prisma.functionWorker.findUnique({
643+
where: {
644+
id: workerId,
645+
workspaceId,
646+
},
647+
});
648+
649+
if (!worker) {
650+
throw new Error('Worker not found');
651+
}
652+
653+
const revision = await prisma.functionWorkerRevision.findUnique({
654+
where: {
655+
id: revisionId,
656+
workerId,
657+
},
658+
});
659+
660+
if (!revision) {
661+
throw new Error('Revision not found');
662+
}
663+
664+
if (revision.code === worker.code) {
665+
return worker;
666+
}
667+
668+
const updatedWorker = await workerCronManager.upsert({
669+
id: workerId,
670+
workspaceId,
671+
name: worker.name,
672+
description: worker.description ?? undefined,
673+
code: revision.code,
674+
active: worker.active,
675+
enableCron: worker.enableCron,
676+
cronExpression: worker.cronExpression,
677+
});
678+
679+
await createAuditLog({
680+
workspaceId,
681+
relatedId: workerId,
682+
relatedType: WorkspaceAuditLogType.FunctionWorker,
683+
content: `Rolled back worker: ${worker.name} to revision #${revision.revision} by ${ctx.user?.username}(${ctx.user?.id})`,
684+
});
685+
686+
return updatedWorker;
687+
}),
688+
624689
getRevisions: workspaceProcedure
625690
.meta(
626691
buildWorkerOpenapi({

0 commit comments

Comments
 (0)