Skip to content

Commit cf17a48

Browse files
Add support for stopping flow to runs table (#1236)
1 parent d8b2de8 commit cf17a48

File tree

6 files changed

+143
-30
lines changed

6 files changed

+143
-30
lines changed

packages/react-ui/public/locales/en/translation.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@
162162
"Error, please try again.": "Error, please try again.",
163163
"Duration": "Duration",
164164
"Run Details": "Run Details",
165+
"Are you sure you want to stop this run?": "Are you sure you want to stop this run?",
166+
"Stopping this workflow may leave some steps unfinished. Consider whether stopping the execution is necessary.": "Stopping this workflow may leave some steps unfinished. Consider whether stopping the execution is necessary.",
167+
"Stop Run": "Stop Run",
165168
"There are no logs captured for this run.": "There are no logs captured for this run.",
166169
"Logs are kept for {days} days after execution and then deleted.": "Logs are kept for {days} days after execution and then deleted.",
167170
"Run Succeeded": "Run Succeeded",
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {
2+
Button,
3+
Dialog,
4+
DialogContent,
5+
DialogDescription,
6+
DialogFooter,
7+
DialogHeader,
8+
DialogTitle,
9+
DialogTrigger,
10+
} from '@openops/components/ui';
11+
import { t } from 'i18next';
12+
import { ReactNode } from 'react';
13+
14+
type StopRunDialogProps = {
15+
isStopDialogOpen: boolean;
16+
setIsStopDialogOpen: (isStopDialogOpen: boolean) => void;
17+
stopRun: () => void;
18+
children: ReactNode;
19+
};
20+
21+
const StopRunDialog = ({
22+
isStopDialogOpen,
23+
setIsStopDialogOpen,
24+
stopRun,
25+
children,
26+
}: StopRunDialogProps) => {
27+
return (
28+
<Dialog open={isStopDialogOpen} onOpenChange={setIsStopDialogOpen}>
29+
<DialogTrigger asChild>{children}</DialogTrigger>
30+
<DialogContent>
31+
<DialogHeader className="mb-0">
32+
<DialogTitle className="text-primary text-[22px]">
33+
{t('Are you sure you want to stop this run?')}
34+
</DialogTitle>
35+
<DialogDescription className="text-primary text-[16px]">
36+
{t(
37+
'Stopping this workflow may leave some steps unfinished. Consider whether stopping the execution is necessary.',
38+
)}
39+
</DialogDescription>
40+
</DialogHeader>
41+
<DialogFooter>
42+
<Button
43+
size="lg"
44+
variant="outline"
45+
onClick={() => setIsStopDialogOpen(false)}
46+
>
47+
{t('Cancel')}
48+
</Button>
49+
<Button
50+
size="lg"
51+
onClick={() => {
52+
stopRun();
53+
}}
54+
>
55+
{t('Stop Run')}
56+
</Button>
57+
</DialogFooter>
58+
</DialogContent>
59+
</Dialog>
60+
);
61+
};
62+
63+
StopRunDialog.displayName = 'StopRunDialog';
64+
export { StopRunDialog };

packages/react-ui/src/app/features/flow-runs/hooks/useRunsTableColumns.tsx

Lines changed: 62 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,50 @@
1+
import { flagsHooks } from '@/app/common/hooks/flags-hooks';
2+
import { flowRunsApi } from '@/app/features/flow-runs/lib/flow-runs-api';
3+
import { formatUtils } from '@/app/lib/utils';
14
import {
25
DataTableColumnHeader,
36
DropdownMenu,
47
DropdownMenuContent,
58
DropdownMenuItem,
69
DropdownMenuTrigger,
710
INTERNAL_ERROR_TOAST,
8-
PermissionNeededTooltip,
911
RowDataWithActions,
1012
StatusIconWithText,
1113
toast,
1214
} from '@openops/components/ui';
13-
import { useMutation } from '@tanstack/react-query';
14-
import { ColumnDef } from '@tanstack/react-table';
15-
import { t } from 'i18next';
16-
import { EllipsisVertical, RefreshCw, RotateCcw } from 'lucide-react';
17-
import { useMemo } from 'react';
18-
19-
import { useAuthorization } from '@/app/common/hooks/authorization-hooks';
20-
import { flagsHooks } from '@/app/common/hooks/flags-hooks';
21-
import { flowRunsApi } from '@/app/features/flow-runs/lib/flow-runs-api';
22-
import { formatUtils } from '@/app/lib/utils';
2315
import {
2416
FlagId,
2517
FlowRetryStrategy,
2618
FlowRun,
19+
FlowRunStatus,
2720
FlowRunTriggerSource,
2821
isFailedState,
29-
Permission,
22+
isRunningState,
3023
} from '@openops/shared';
24+
import { useMutation } from '@tanstack/react-query';
25+
import { ColumnDef } from '@tanstack/react-table';
26+
import { t } from 'i18next';
27+
import {
28+
CircleStop,
29+
EllipsisVertical,
30+
RefreshCw,
31+
RotateCcw,
32+
} from 'lucide-react';
33+
import { useMemo, useState } from 'react';
3134

3235
import { RunType } from '@/app/features/flow-runs/components/run-type';
36+
import { StopRunDialog } from '@/app/features/flow-runs/components/stop-run-dialog';
3337
import { flowRunUtils } from '../lib/flow-run-utils';
3438

3539
type Column = ColumnDef<RowDataWithActions<FlowRun>> & {
3640
accessorKey: string;
3741
};
3842

3943
export const useRunsTableColumns = (): Column[] => {
40-
const { checkAccess } = useAuthorization();
4144
const durationEnabled = flagsHooks.useFlag<boolean>(
4245
FlagId.SHOW_DURATION,
4346
).data;
4447

45-
const userHasPermissionToRetryRun = checkAccess(Permission.RETRY_RUN);
4648
const { mutate } = useMutation<
4749
FlowRun,
4850
Error,
@@ -58,6 +60,22 @@ export const useRunsTableColumns = (): Column[] => {
5860
toast(INTERNAL_ERROR_TOAST);
5961
},
6062
});
63+
64+
const { mutate: stopRun } = useMutation<
65+
void,
66+
Error,
67+
{ row: RowDataWithActions<FlowRun> }
68+
>({
69+
mutationFn: (data) => flowRunsApi.abort(data.row.id),
70+
onSuccess: (_, { row }) => {
71+
row.update({ ...row, status: FlowRunStatus.STOPPED });
72+
},
73+
onError: (error) => {
74+
console.error(error);
75+
toast(INTERNAL_ERROR_TOAST);
76+
},
77+
});
78+
6179
return useMemo(
6280
() =>
6381
[
@@ -136,10 +154,16 @@ export const useRunsTableColumns = (): Column[] => {
136154
header: () => null,
137155
cell: ({ row }) => {
138156
const isFailed = isFailedState(row.original.status);
157+
const isRunning = isRunningState(row.original.status);
158+
const isStopped = row.original.status === FlowRunStatus.STOPPED;
159+
160+
// eslint-disable-next-line react-hooks/rules-of-hooks
161+
const [isStopDialogOpen, setIsStopDialogOpen] = useState(false);
139162

140163
if (
141-
!isFailed ||
142-
row.original.triggerSource === FlowRunTriggerSource.TEST_RUN
164+
((isFailed || isStopped) &&
165+
row.original.triggerSource === FlowRunTriggerSource.TEST_RUN) ||
166+
(!isFailed && !isRunning && !isStopped)
143167
) {
144168
return <div className="h-10"></div>;
145169
}
@@ -157,11 +181,8 @@ export const useRunsTableColumns = (): Column[] => {
157181
<EllipsisVertical className="h-10 w-10" />
158182
</DropdownMenuTrigger>
159183
<DropdownMenuContent>
160-
<PermissionNeededTooltip
161-
hasPermission={userHasPermissionToRetryRun}
162-
>
184+
{(isFailed || isStopped) && (
163185
<DropdownMenuItem
164-
disabled={!userHasPermissionToRetryRun}
165186
onClick={() =>
166187
mutate({
167188
row: row.original,
@@ -174,13 +195,9 @@ export const useRunsTableColumns = (): Column[] => {
174195
<span>{t('Retry on latest version')}</span>
175196
</div>
176197
</DropdownMenuItem>
177-
</PermissionNeededTooltip>
178-
179-
<PermissionNeededTooltip
180-
hasPermission={userHasPermissionToRetryRun}
181-
>
198+
)}
199+
{isFailed && (
182200
<DropdownMenuItem
183-
disabled={!userHasPermissionToRetryRun}
184201
onClick={() =>
185202
mutate({
186203
row: row.original,
@@ -193,7 +210,24 @@ export const useRunsTableColumns = (): Column[] => {
193210
<span>{t('Retry from failed step')}</span>
194211
</div>
195212
</DropdownMenuItem>
196-
</PermissionNeededTooltip>
213+
)}
214+
{isRunning && (
215+
<StopRunDialog
216+
isStopDialogOpen={isStopDialogOpen}
217+
setIsStopDialogOpen={setIsStopDialogOpen}
218+
stopRun={() => {
219+
stopRun({ row: row.original });
220+
setIsStopDialogOpen(false);
221+
}}
222+
>
223+
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
224+
<div className="flex flex-row gap-2 items-center">
225+
<CircleStop className="h-4 w-4" />
226+
<span>{t('Stop Run')}</span>
227+
</div>
228+
</DropdownMenuItem>
229+
</StopRunDialog>
230+
)}
197231
</DropdownMenuContent>
198232
</DropdownMenu>
199233
</div>
@@ -203,6 +237,6 @@ export const useRunsTableColumns = (): Column[] => {
203237
].filter(
204238
(column) => durationEnabled || column.accessorKey !== 'duration',
205239
),
206-
[mutate, userHasPermissionToRetryRun],
240+
[mutate, stopRun],
207241
);
208242
};

packages/react-ui/src/app/features/flow-runs/lib/flow-run-utils.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
Check,
33
CircleCheck,
4+
CircleStop,
45
CircleX,
56
PauseCircleIcon,
67
PauseIcon,
@@ -78,8 +79,8 @@ export const flowRunUtils = {
7879
};
7980
case FlowRunStatus.STOPPED:
8081
return {
81-
variant: 'success',
82-
Icon: Check,
82+
variant: 'default',
83+
Icon: CircleStop,
8384
};
8485
case FlowRunStatus.FAILED:
8586
return {

packages/react-ui/src/app/features/flow-runs/lib/flow-runs-api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,7 @@ export const flowRunsApi = {
1818
retry(flowRunId: string, request: RetryFlowRequestBody): Promise<FlowRun> {
1919
return api.post<FlowRun>(`/v1/flow-runs/${flowRunId}/retry`, request);
2020
},
21+
abort(flowRunId: string): Promise<void> {
22+
return api.post<void>(`/v1/flow-runs/${flowRunId}/stop`);
23+
},
2124
};

packages/shared/src/lib/flow-run/execution/flow-execution.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,11 @@ export const isFailedState = (status: FlowRunStatus): boolean => {
8686
status === FlowRunStatus.TIMEOUT
8787
);
8888
};
89+
90+
export const isRunningState = (status: FlowRunStatus): boolean => {
91+
return (
92+
status === FlowRunStatus.RUNNING ||
93+
status === FlowRunStatus.PAUSED ||
94+
status === FlowRunStatus.SCHEDULED
95+
);
96+
};

0 commit comments

Comments
 (0)