Skip to content

Commit 59791ea

Browse files
feat: add confirmation alert for stopping runs (#785)
1 parent 77537a7 commit 59791ea

File tree

7 files changed

+220
-78
lines changed

7 files changed

+220
-78
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
1212
"prepare": "husky",
1313
"preview": "vite preview",
14+
"security-refresh": "rm pnpm-lock.yaml && pnpm install && pnpm audit",
1415
"test:e2e": "playwright test",
1516
"test:unit": "vitest run"
1617
},

src/components/form/FieldHint.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function FieldHint({ schema, fieldName }: Props) {
2020
return (
2121
<TooltipProvider>
2222
<Tooltip>
23-
<TooltipTrigger>
23+
<TooltipTrigger type="button">
2424
<Info className="size-4 shrink-0 fill-theme-text-secondary" />
2525
</TooltipTrigger>
2626
<TooltipContent className="z-20 max-w-[400px] bg-theme-surface-primary p-5 text-text-sm text-theme-text-primary">
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import {
2+
AlertDialog,
3+
AlertDialogCancel,
4+
AlertDialogContent,
5+
AlertDialogDescription,
6+
AlertDialogFooter,
7+
AlertDialogHeader,
8+
AlertDialogTitle
9+
} from "@zenml-io/react-component-library/components/client";
10+
import { Button } from "@zenml-io/react-component-library/components/server";
11+
import { Dispatch, PropsWithChildren, SetStateAction } from "react";
12+
import { useStopRun } from "./use-stop-run";
13+
14+
type Props = {
15+
handleStop: () => void;
16+
isPending: boolean;
17+
open: boolean;
18+
setOpen: Dispatch<SetStateAction<boolean>>;
19+
};
20+
21+
function StopBaseAlert({
22+
open,
23+
setOpen,
24+
handleStop,
25+
isPending,
26+
children
27+
}: PropsWithChildren<Props>) {
28+
return (
29+
<AlertDialog open={open} onOpenChange={setOpen}>
30+
<AlertDialogContent>
31+
<AlertDialogHeader>
32+
<AlertDialogTitle>Stop Run</AlertDialogTitle>
33+
</AlertDialogHeader>
34+
<AlertDialogDescription className="overflow-hidden p-5">{children}</AlertDialogDescription>
35+
<AlertDialogFooter className="gap-[10px]">
36+
<AlertDialogCancel asChild>
37+
<Button size="sm" intent="secondary">
38+
Cancel
39+
</Button>
40+
</AlertDialogCancel>
41+
<Button
42+
onClick={handleStop}
43+
className="flex items-center gap-1"
44+
disabled={isPending}
45+
intent="primary"
46+
type="submit"
47+
>
48+
{isPending && (
49+
<div
50+
role="alert"
51+
aria-busy="true"
52+
className="full h-[20px] w-[20px] animate-spin rounded-rounded border-2 border-theme-text-negative border-b-theme-text-brand"
53+
></div>
54+
)}
55+
Stop
56+
</Button>
57+
</AlertDialogFooter>
58+
</AlertDialogContent>
59+
</AlertDialog>
60+
);
61+
}
62+
63+
type StopAlertProps = {
64+
open: boolean;
65+
setOpen: Dispatch<SetStateAction<boolean>>;
66+
runId: string;
67+
};
68+
export function StopGracefullyAlert({ open, setOpen, runId }: StopAlertProps) {
69+
const {
70+
stopRunQuery: { mutate: stopPipelineRun, isPending }
71+
} = useStopRun(() => setOpen(false));
72+
73+
function handleStop() {
74+
stopPipelineRun({
75+
runId,
76+
params: { graceful: true }
77+
});
78+
}
79+
80+
return (
81+
<StopBaseAlert open={open} setOpen={setOpen} handleStop={handleStop} isPending={isPending}>
82+
<div className="space-y-1">
83+
<div>This will stop the run gracefully and allow the current step to finish.</div>
84+
<div>Are you sure you want to stop the run?</div>
85+
</div>
86+
</StopBaseAlert>
87+
);
88+
}
89+
90+
export function StopImmediatelyAlert({ open, setOpen, runId }: StopAlertProps) {
91+
const {
92+
stopRunQuery: { mutate: stopPipelineRun, isPending }
93+
} = useStopRun(() => setOpen(false));
94+
95+
function handleStop() {
96+
stopPipelineRun({
97+
runId,
98+
params: { graceful: false }
99+
});
100+
}
101+
102+
return (
103+
<StopBaseAlert open={open} setOpen={setOpen} handleStop={handleStop} isPending={isPending}>
104+
<div className="space-y-1">
105+
<div>This will stop the run immediately and won't allow the current step to finish.</div>
106+
107+
<div>Are you sure you want to stop the run right now?</div>
108+
</div>
109+
</StopBaseAlert>
110+
);
111+
}
Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,30 @@
11
import StopCircle from "@/assets/icons/stop-circle.svg?react";
22
import { Button } from "@zenml-io/react-component-library";
3-
import { useStopRun } from "./use-stop-run";
3+
import { useState } from "react";
4+
import { StopGracefullyAlert } from "./stop-alert";
45

56
type Props = {
67
runId: string;
78
isActive: boolean;
89
};
910

1011
export function StopRunButton({ runId, isActive }: Props) {
11-
const {
12-
stopRunQuery: { mutate: stopPipelineRun, isPending }
13-
} = useStopRun();
14-
15-
function handleStop() {
16-
stopPipelineRun({
17-
runId,
18-
params: { graceful: false }
19-
});
20-
}
12+
const [isStopAlertOpen, setIsStopAlertOpen] = useState(false);
2113

2214
return (
23-
<Button
24-
data-tour-id="stop-run-button"
25-
disabled={!isActive || isPending}
26-
intent="secondary"
27-
size="sm"
28-
className="group whitespace-nowrap rounded-r-sharp"
29-
onClick={handleStop}
30-
emphasis="subtle"
31-
>
32-
<StopCircle className="h-4 w-4 shrink-0 fill-theme-text-primary transition-all duration-200 group-disabled:fill-neutral-300" />
33-
Stop
34-
</Button>
15+
<>
16+
<Button
17+
disabled={!isActive}
18+
intent="secondary"
19+
size="sm"
20+
className="group whitespace-nowrap rounded-r-sharp"
21+
onClick={() => setIsStopAlertOpen(true)}
22+
emphasis="subtle"
23+
>
24+
<StopCircle className="h-4 w-4 shrink-0 fill-theme-text-primary transition-all duration-200 group-disabled:fill-neutral-300" />
25+
Stop
26+
</Button>
27+
<StopGracefullyAlert open={isStopAlertOpen} setOpen={setIsStopAlertOpen} runId={runId} />
28+
</>
3529
);
3630
}

src/components/runs/stop-group/stop-dropdown.tsx

Lines changed: 39 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -10,61 +10,53 @@ import {
1010
DropdownMenuTrigger
1111
} from "@zenml-io/react-component-library";
1212
import { useState } from "react";
13-
import { useStopRun } from "./use-stop-run";
13+
import { StopImmediatelyAlert } from "./stop-alert";
1414

1515
type Props = {
1616
runId: string;
1717
isActive: boolean;
1818
};
1919
export function RunStopDropdown({ runId, isActive }: Props) {
2020
const [isOpen, setIsOpen] = useState(false);
21-
22-
const {
23-
stopRunQuery: { mutate: stopPipelineRun, isPending }
24-
} = useStopRun();
25-
26-
function handleStop() {
27-
stopPipelineRun({
28-
runId,
29-
params: { graceful: true }
30-
});
31-
}
21+
const [isStopAlertOpen, setIsStopAlertOpen] = useState(false);
3222

3323
return (
34-
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
35-
<DropdownMenuTrigger asChild>
36-
<Button
37-
disabled={!isActive || isPending}
38-
intent="secondary"
39-
emphasis="subtle"
40-
size="sm"
41-
className="group flex aspect-square items-center justify-center rounded-l-sharp border-l-0 p-0"
42-
>
43-
<ChevronDown
44-
width={20}
45-
height={20}
46-
className="size-4 shrink-0 fill-theme-text-primary transition-all duration-200 group-disabled:fill-neutral-300"
47-
/>
48-
<span className="sr-only">Open run refresh menu</span>
49-
</Button>
50-
</DropdownMenuTrigger>
51-
<DropdownMenuContent className="max-w-[100px] lg:max-w-[320px]">
52-
<DropdownMenuItem
53-
className="hover:cursor-pointer"
54-
icon={<StopCircle width={24} height={24} className="size-5 shrink-0" />}
55-
onSelect={(e) => {
56-
e.preventDefault();
57-
handleStop();
58-
}}
59-
>
60-
<div className="space-y-0.25">
61-
<p>Stop gracefully</p>
62-
<p className="text-text-xs text-theme-text-secondary">
63-
Stops the run gracefully. This will allow the current step to finish.
64-
</p>
65-
</div>
66-
</DropdownMenuItem>
67-
</DropdownMenuContent>
68-
</DropdownMenu>
24+
<>
25+
<StopImmediatelyAlert open={isStopAlertOpen} setOpen={setIsStopAlertOpen} runId={runId} />
26+
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
27+
<DropdownMenuTrigger asChild>
28+
<Button
29+
disabled={!isActive}
30+
intent="secondary"
31+
emphasis="subtle"
32+
size="sm"
33+
className="group flex aspect-square items-center justify-center rounded-l-sharp border-l-0 p-0"
34+
>
35+
<ChevronDown
36+
width={20}
37+
height={20}
38+
className="size-4 shrink-0 fill-theme-text-primary transition-all duration-200 group-disabled:fill-neutral-300"
39+
/>
40+
<span className="sr-only">Open stop run menu</span>
41+
</Button>
42+
</DropdownMenuTrigger>
43+
<DropdownMenuContent className="max-w-[100px] lg:max-w-[320px]">
44+
<DropdownMenuItem
45+
className="hover:cursor-pointer"
46+
icon={<StopCircle width={24} height={24} className="size-5 shrink-0" />}
47+
onSelect={() => {
48+
setIsStopAlertOpen(true);
49+
}}
50+
>
51+
<div className="space-y-0.25">
52+
<p>Stop immediately</p>
53+
<p className="text-text-xs text-theme-text-secondary">
54+
Stops the run immediately. This won't allow the current step to finish.
55+
</p>
56+
</div>
57+
</DropdownMenuItem>
58+
</DropdownMenuContent>
59+
</DropdownMenu>
60+
</>
6961
);
7062
}

src/components/runs/stop-group/use-stop-run.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import { useStopPipelineRun } from "@/data/pipeline-runs/stop-run";
22
import { useQueryClient } from "@tanstack/react-query";
33
import { useToast } from "@zenml-io/react-component-library";
44

5-
export function useStopRun() {
5+
export function useStopRun(closeAlert: () => void) {
66
const { toast } = useToast();
77
const queryClient = useQueryClient();
88

99
const stopRunQuery = useStopPipelineRun({
1010
onSuccess: () => {
1111
queryClient.invalidateQueries({ queryKey: ["runs"] });
12+
closeAlert();
1213
},
1314
onError: (err) => {
1415
toast({
@@ -17,6 +18,7 @@ export function useStopRun() {
1718
emphasis: "subtle",
1819
description: <div>{err.message}</div>
1920
});
21+
closeAlert();
2022
}
2123
});
2224

0 commit comments

Comments
 (0)