11import { BookOpenIcon , PlusIcon } from "@heroicons/react/20/solid" ;
2+ import { NoSymbolIcon } from "@heroicons/react/24/solid" ;
23import { Outlet , useParams , type MetaFunction } from "@remix-run/react" ;
34import { type LoaderFunctionArgs } from "@remix-run/server-runtime" ;
45import { tryCatch } from "@trigger.dev/core" ;
@@ -7,8 +8,9 @@ import { z } from "zod";
78import { AdminDebugTooltip } from "~/components/admin/debugTooltip" ;
89import { BulkActionsNone } from "~/components/BlankStatePanels" ;
910import { MainCenteredContainer , PageBody , PageContainer } from "~/components/layout/AppLayout" ;
10- import { LinkButton } from "~/components/primitives/Buttons" ;
11+ import { Button , LinkButton } from "~/components/primitives/Buttons" ;
1112import { DateTime } from "~/components/primitives/DateTime" ;
13+ import { Dialog , DialogTrigger } from "~/components/primitives/Dialog" ;
1214import { NavBar , PageAccessories , PageTitle } from "~/components/primitives/PageHeader" ;
1315import { PaginationControls } from "~/components/primitives/Pagination" ;
1416import { Paragraph } from "~/components/primitives/Paragraph" ;
@@ -24,11 +26,13 @@ import {
2426 TableBlankRow ,
2527 TableBody ,
2628 TableCell ,
29+ TableCellMenu ,
2730 TableHeader ,
2831 TableHeaderCell ,
2932 TableRow ,
3033} from "~/components/primitives/Table" ;
3134import { TruncatedCopyableValue } from "~/components/primitives/TruncatedCopyableValue" ;
35+ import { AbortBulkActionDialog } from "~/components/runs/v3/AbortBulkActionDialog" ;
3236import { BulkActionStatusCombo , BulkActionTypeCombo } from "~/components/runs/v3/BulkAction" ;
3337import { UserAvatar } from "~/components/UserProfilePhoto" ;
3438import { useEnvironment } from "~/hooks/useEnvironment" ;
@@ -40,6 +44,8 @@ import {
4044 type BulkActionListItem ,
4145 BulkActionListPresenter ,
4246} from "~/presenters/v3/BulkActionListPresenter.server" ;
47+ import { rbac } from "~/services/rbac.server" ;
48+ import { checkPermissions } from "~/services/routeBuilders/permissions.server" ;
4349import { requireUserId } from "~/services/session.server" ;
4450import { cn } from "~/utils/cn" ;
4551import {
@@ -92,7 +98,19 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
9298 throw new Error ( error . message ) ;
9399 }
94100
95- return typedjson ( data ) ;
101+ // Display flag for the row-menu Abort control. The abort action route
102+ // enforces write:runs independently. Permissive in OSS.
103+ const bulkAuth = await rbac . authenticateSession ( request , {
104+ userId,
105+ organizationId : project . organizationId ,
106+ } ) ;
107+ const { canAbort } = bulkAuth . ok
108+ ? checkPermissions ( bulkAuth . ability , {
109+ canAbort : { action : "write" , resource : { type : "runs" } } ,
110+ } )
111+ : { canAbort : true } ;
112+
113+ return typedjson ( { ...data , canAbort } ) ;
96114 } catch ( error ) {
97115 console . error ( error ) ;
98116 throw new Response ( undefined , {
@@ -108,6 +126,7 @@ export default function Page() {
108126 currentPage,
109127 totalPages,
110128 totalCount : _totalCount ,
129+ canAbort,
111130 } = useTypedLoaderData < typeof loader > ( ) ;
112131 const organization = useOrganization ( ) ;
113132 const project = useProject ( ) ;
@@ -164,7 +183,11 @@ export default function Page() {
164183 </ div >
165184 ) }
166185
167- < BulkActionsTable bulkActions = { bulkActions } totalPages = { totalPages } />
186+ < BulkActionsTable
187+ bulkActions = { bulkActions }
188+ totalPages = { totalPages }
189+ canAbort = { canAbort }
190+ />
168191 { totalPages > 1 && (
169192 < div
170193 className = { cn (
@@ -207,9 +230,11 @@ export default function Page() {
207230function BulkActionsTable ( {
208231 bulkActions,
209232 totalPages,
233+ canAbort,
210234} : {
211235 bulkActions : BulkActionListItem [ ] ;
212236 totalPages : number ;
237+ canAbort : boolean ;
213238} ) {
214239 const organization = useOrganization ( ) ;
215240 const project = useProject ( ) ;
@@ -260,11 +285,14 @@ function BulkActionsTable({
260285 < TableHeaderCell > User</ TableHeaderCell >
261286 < TableHeaderCell > Created</ TableHeaderCell >
262287 < TableHeaderCell > Completed</ TableHeaderCell >
288+ < TableHeaderCell >
289+ < span className = "sr-only" > Actions</ span >
290+ </ TableHeaderCell >
263291 </ TableRow >
264292 </ TableHeader >
265293 < TableBody >
266294 { bulkActions . length === 0 ? (
267- < TableBlankRow colSpan = { 8 } > There are no matching bulk actions</ TableBlankRow >
295+ < TableBlankRow colSpan = { 9 } > There are no matching bulk actions</ TableBlankRow >
268296 ) : (
269297 bulkActions . map ( ( bulkAction ) => {
270298 const path = v3BulkActionPath ( organization , project , environment , bulkAction ) ;
@@ -306,6 +334,7 @@ function BulkActionsTable({
306334 < TableCell to = { path } >
307335 { bulkAction . completedAt ? < DateTime date = { bulkAction . completedAt } /> : "–" }
308336 </ TableCell >
337+ < BulkActionActionsCell bulkAction = { bulkAction } path = { path } canAbort = { canAbort } />
309338 </ TableRow >
310339 ) ;
311340 } )
@@ -314,3 +343,50 @@ function BulkActionsTable({
314343 </ Table >
315344 ) ;
316345}
346+
347+ function BulkActionActionsCell ( {
348+ bulkAction,
349+ path,
350+ canAbort,
351+ } : {
352+ bulkAction : BulkActionListItem ;
353+ path : string ;
354+ canAbort : boolean ;
355+ } ) {
356+ // Abort is the only action, and only while the bulk action is still running.
357+ if ( bulkAction . status !== "PENDING" ) {
358+ return < TableCell to = { path } > { "" } </ TableCell > ;
359+ }
360+
361+ return (
362+ < TableCellMenu
363+ isSticky
364+ hiddenButtons = {
365+ canAbort ? (
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 >
378+ ) : (
379+ < Button
380+ variant = "minimal/small"
381+ LeadingIcon = { NoSymbolIcon }
382+ leadingIconClassName = "text-error"
383+ disabled
384+ tooltip = "You don't have permission to abort bulk actions"
385+ >
386+ < span className = "text-text-bright" > Abort…</ span >
387+ </ Button >
388+ )
389+ }
390+ />
391+ ) ;
392+ }
0 commit comments