Skip to content

Commit d3daaca

Browse files
committed
feat(webapp): confirm before aborting a bulk action
Aborting a bulk action from the list row button or the inspector now opens a confirmation dialog (matching the "Cancel this run?" flow) instead of firing immediately, so it's harder to abort by accident. Both entry points share a new AbortBulkActionDialog that posts to the existing abort action.
1 parent ac634e7 commit d3daaca

3 files changed

Lines changed: 107 additions & 25 deletions

File tree

  • apps/webapp/app
    • components/runs/v3
    • routes
      • _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions.$bulkActionParam
      • _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { NoSymbolIcon } from "@heroicons/react/24/solid";
2+
import { DialogClose } from "@radix-ui/react-dialog";
3+
import { Form, useNavigation } from "@remix-run/react";
4+
import { Button } from "~/components/primitives/Buttons";
5+
import { DialogContent, DialogHeader } from "~/components/primitives/Dialog";
6+
import { FormButtons } from "~/components/primitives/FormButtons";
7+
import { Paragraph } from "~/components/primitives/Paragraph";
8+
import { SpinnerWhite } from "~/components/primitives/Spinner";
9+
10+
type AbortBulkActionDialogProps = {
11+
// The abort action route to POST to (the bulk action detail path).
12+
formAction: string;
13+
// Fired on submit so a parent controlling the Radix Dialog can close it
14+
// without wrapping the submit button in `DialogClose` — that wrapper races
15+
// submit (close fires first, unmounts the form, and the abort POST never
16+
// lands). Optional so uncontrolled call sites still type-check.
17+
onAbortSubmitted?: () => void;
18+
};
19+
20+
export function AbortBulkActionDialog({ formAction, onAbortSubmitted }: AbortBulkActionDialogProps) {
21+
const navigation = useNavigation();
22+
23+
const isLoading = navigation.formAction === formAction && navigation.formMethod === "POST";
24+
25+
return (
26+
<DialogContent key="abort">
27+
<DialogHeader>Abort this bulk action?</DialogHeader>
28+
<div className="flex flex-col gap-3 pt-3">
29+
<Paragraph>
30+
Aborting stops this bulk action from processing any remaining runs. Runs it has already
31+
processed won't be affected.
32+
</Paragraph>
33+
<FormButtons
34+
confirmButton={
35+
<Form action={formAction} method="post" onSubmit={() => onAbortSubmitted?.()}>
36+
<Button
37+
type="submit"
38+
variant="danger/medium"
39+
LeadingIcon={isLoading ? SpinnerWhite : NoSymbolIcon}
40+
disabled={isLoading}
41+
shortcut={{ modifiers: ["mod"], key: "enter" }}
42+
>
43+
{isLoading ? "Aborting..." : "Abort bulk action"}
44+
</Button>
45+
</Form>
46+
}
47+
cancelButton={
48+
<DialogClose asChild>
49+
<Button variant={"tertiary/medium"}>Close</Button>
50+
</DialogClose>
51+
}
52+
/>
53+
</div>
54+
</DialogContent>
55+
);
56+
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions.$bulkActionParam/route.tsx

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
import { ArrowPathIcon } from "@heroicons/react/20/solid";
2-
import { Form } from "@remix-run/react";
2+
import { NoSymbolIcon } from "@heroicons/react/24/solid";
33
import { tryCatch } from "@trigger.dev/core";
44
import type { BulkActionType } from "@trigger.dev/database";
55
import { motion } from "framer-motion";
6+
import { useState } from "react";
67
import { typedjson, useTypedLoaderData } from "remix-typedjson";
78
import { z } from "zod";
89
import { ExitIcon } from "~/assets/icons/ExitIcon";
910
import { RunsIcon } from "~/assets/icons/RunsIcon";
1011
import { BulkActionFilterSummary } from "~/components/BulkActionFilterSummary";
11-
import { LinkButton } from "~/components/primitives/Buttons";
12+
import { Button, LinkButton } from "~/components/primitives/Buttons";
1213
import { CopyableText } from "~/components/primitives/CopyableText";
1314
import { DateTime } from "~/components/primitives/DateTime";
15+
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
1416
import { Header2 } from "~/components/primitives/Headers";
1517
import { Paragraph } from "~/components/primitives/Paragraph";
16-
import { PermissionButton } from "~/components/primitives/PermissionButton";
1718
import * as Property from "~/components/primitives/PropertyTable";
19+
import { AbortBulkActionDialog } from "~/components/runs/v3/AbortBulkActionDialog";
1820
import { BulkActionStatusCombo, BulkActionTypeCombo } from "~/components/runs/v3/BulkAction";
1921
import { UserAvatar } from "~/components/UserProfilePhoto";
2022
import { env } from "~/env.server";
@@ -183,16 +185,10 @@ export default function Page() {
183185
<div className="flex items-center justify-between gap-2 border-b border-grid-dimmed px-3 text-sm">
184186
<BulkActionStatusCombo status={bulkAction.status} />
185187
{bulkAction.status === "PENDING" ? (
186-
<Form method="post">
187-
<PermissionButton
188-
type="submit"
189-
variant="danger/small"
190-
hasPermission={canAbort}
191-
noPermissionTooltip="You don't have permission to abort bulk actions"
192-
>
193-
Abort bulk action
194-
</PermissionButton>
195-
</Form>
188+
<ControlledAbortBulkActionDialog
189+
canAbort={canAbort}
190+
formAction={v3BulkActionPath(organization, project, environment, bulkAction)}
191+
/>
196192
) : null}
197193
</div>
198194
<div className="overflow-y-scroll scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
@@ -358,3 +354,28 @@ function typeText(type: BulkActionType) {
358354
return "replayed";
359355
}
360356
}
357+
358+
function ControlledAbortBulkActionDialog({
359+
canAbort,
360+
formAction,
361+
}: {
362+
canAbort: boolean;
363+
formAction: string;
364+
}) {
365+
const [open, setOpen] = useState(false);
366+
return (
367+
<Dialog open={open} onOpenChange={setOpen}>
368+
<DialogTrigger asChild>
369+
<Button
370+
variant="danger/small"
371+
LeadingIcon={NoSymbolIcon}
372+
disabled={!canAbort}
373+
tooltip={canAbort ? undefined : "You don't have permission to abort bulk actions"}
374+
>
375+
Abort bulk action
376+
</Button>
377+
</DialogTrigger>
378+
<AbortBulkActionDialog formAction={formAction} onAbortSubmitted={() => setOpen(false)} />
379+
</Dialog>
380+
);
381+
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions/route.tsx

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { BookOpenIcon, NoSymbolIcon, PlusIcon } from "@heroicons/react/20/solid";
2-
import { Form, Outlet, useParams, type MetaFunction } from "@remix-run/react";
1+
import { BookOpenIcon, PlusIcon } from "@heroicons/react/20/solid";
2+
import { NoSymbolIcon } from "@heroicons/react/24/solid";
3+
import { Outlet, useParams, type MetaFunction } from "@remix-run/react";
34
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
45
import { tryCatch } from "@trigger.dev/core";
56
import { typedjson, useTypedLoaderData } from "remix-typedjson";
@@ -9,6 +10,7 @@ import { BulkActionsNone } from "~/components/BlankStatePanels";
910
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
1011
import { Button, LinkButton } from "~/components/primitives/Buttons";
1112
import { DateTime } from "~/components/primitives/DateTime";
13+
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
1214
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
1315
import { PaginationControls } from "~/components/primitives/Pagination";
1416
import { Paragraph } from "~/components/primitives/Paragraph";
@@ -30,6 +32,7 @@ import {
3032
TableRow,
3133
} from "~/components/primitives/Table";
3234
import { TruncatedCopyableValue } from "~/components/primitives/TruncatedCopyableValue";
35+
import { AbortBulkActionDialog } from "~/components/runs/v3/AbortBulkActionDialog";
3336
import { BulkActionStatusCombo, BulkActionTypeCombo } from "~/components/runs/v3/BulkAction";
3437
import { UserAvatar } from "~/components/UserProfilePhoto";
3538
import { useEnvironment } from "~/hooks/useEnvironment";
@@ -360,16 +363,18 @@ function BulkActionActionsCell({
360363
isSticky
361364
hiddenButtons={
362365
canAbort ? (
363-
<Form method="post" action={path}>
364-
<Button
365-
type="submit"
366-
variant="minimal/small"
367-
LeadingIcon={NoSymbolIcon}
368-
leadingIconClassName="text-error"
369-
>
370-
<span className="text-text-bright">Abort</span>
371-
</Button>
372-
</Form>
366+
<Dialog>
367+
<DialogTrigger asChild>
368+
<Button
369+
variant="minimal/small"
370+
LeadingIcon={NoSymbolIcon}
371+
leadingIconClassName="text-error"
372+
>
373+
<span className="text-text-bright">Abort</span>
374+
</Button>
375+
</DialogTrigger>
376+
<AbortBulkActionDialog formAction={path} />
377+
</Dialog>
373378
) : (
374379
<Button
375380
variant="minimal/small"

0 commit comments

Comments
 (0)