11import { ArrowPathIcon } from "@heroicons/react/20/solid" ;
22import { Form } from "@remix-run/react" ;
3- import { type ActionFunctionArgs , type LoaderFunctionArgs } from "@remix-run/server-runtime" ;
43import { tryCatch } from "@trigger.dev/core" ;
54import type { BulkActionType } from "@trigger.dev/database" ;
65import { motion } from "framer-motion" ;
@@ -10,6 +9,7 @@ import { ExitIcon } from "~/assets/icons/ExitIcon";
109import { RunsIcon } from "~/assets/icons/RunsIcon" ;
1110import { BulkActionFilterSummary } from "~/components/BulkActionFilterSummary" ;
1211import { Button , LinkButton } from "~/components/primitives/Buttons" ;
12+ import { PermissionButton } from "~/components/primitives/PermissionButton" ;
1313import { CopyableText } from "~/components/primitives/CopyableText" ;
1414import { DateTime } from "~/components/primitives/DateTime" ;
1515import { Header2 } from "~/components/primitives/Headers" ;
@@ -26,8 +26,10 @@ import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/m
2626import { findProjectBySlug } from "~/models/project.server" ;
2727import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server" ;
2828import { BulkActionPresenter } from "~/presenters/v3/BulkActionPresenter.server" ;
29+ import { $replica } from "~/db.server" ;
2930import { logger } from "~/services/logger.server" ;
30- import { requireUserId } from "~/services/session.server" ;
31+ import { dashboardAction , dashboardLoader } from "~/services/routeBuilders/dashboardBuilder" ;
32+ import { checkPermissions } from "~/services/routeBuilders/permissions.server" ;
3133import { cn } from "~/utils/cn" ;
3234import { formatNumber } from "~/utils/numberFormatter" ;
3335import {
@@ -43,96 +45,121 @@ const BulkActionParamSchema = EnvironmentParamSchema.extend({
4345 bulkActionParam : z . string ( ) ,
4446} ) ;
4547
46- export const loader = async ( { request, params } : LoaderFunctionArgs ) => {
47- const userId = await requireUserId ( request ) ;
48+ async function resolveOrgIdFromSlug ( slug : string ) : Promise < string | null > {
49+ const org = await $replica . organization . findFirst ( { where : { slug } , select : { id : true } } ) ;
50+ return org ?. id ?? null ;
51+ }
4852
49- const { organizationSlug, projectParam, envParam, bulkActionParam } =
50- BulkActionParamSchema . parse ( params ) ;
53+ export const loader = dashboardLoader (
54+ {
55+ params : BulkActionParamSchema ,
56+ context : async ( params ) => {
57+ const organizationId = await resolveOrgIdFromSlug ( params . organizationSlug ) ;
58+ return organizationId ? { organizationId } : { } ;
59+ } ,
60+ authorization : { action : "read" , resource : { type : "runs" } } ,
61+ } ,
62+ async ( { params, user, ability } ) => {
63+ const { organizationSlug, projectParam, envParam, bulkActionParam } = params ;
5164
52- const project = await findProjectBySlug ( organizationSlug , projectParam , userId ) ;
53- if ( ! project ) {
54- throw new Response ( "Not Found" , { status : 404 } ) ;
55- }
65+ const project = await findProjectBySlug ( organizationSlug , projectParam , user . id ) ;
66+ if ( ! project ) {
67+ throw new Response ( "Not Found" , { status : 404 } ) ;
68+ }
5669
57- const environment = await findEnvironmentBySlug ( project . id , envParam , userId ) ;
58- if ( ! environment ) {
59- throw new Response ( "Not Found" , { status : 404 } ) ;
60- }
70+ const environment = await findEnvironmentBySlug ( project . id , envParam , user . id ) ;
71+ if ( ! environment ) {
72+ throw new Response ( "Not Found" , { status : 404 } ) ;
73+ }
6174
62- try {
63- const presenter = new BulkActionPresenter ( ) ;
64- const [ error , data ] = await tryCatch (
65- presenter . call ( {
66- environmentId : environment . id ,
67- bulkActionId : bulkActionParam ,
68- } )
69- ) ;
75+ try {
76+ const presenter = new BulkActionPresenter ( ) ;
77+ const [ error , data ] = await tryCatch (
78+ presenter . call ( {
79+ environmentId : environment . id ,
80+ bulkActionId : bulkActionParam ,
81+ } )
82+ ) ;
7083
71- if ( error ) {
72- throw new Error ( error . message ) ;
73- }
84+ if ( error ) {
85+ throw new Error ( error . message ) ;
86+ }
87+
88+ const autoReloadPollIntervalMs = env . BULK_ACTION_AUTORELOAD_POLL_INTERVAL_MS ;
7489
75- const autoReloadPollIntervalMs = env . BULK_ACTION_AUTORELOAD_POLL_INTERVAL_MS ;
90+ // Display flag for the Abort button — the action enforces write:runs.
91+ const { canAbort } = checkPermissions ( ability , {
92+ canAbort : { action : "write" , resource : { type : "runs" } } ,
93+ } ) ;
7694
77- return typedjson ( { bulkAction : data , autoReloadPollIntervalMs } ) ;
78- } catch ( error ) {
79- console . error ( error ) ;
80- throw new Response ( undefined , {
81- status : 400 ,
82- statusText : "Something went wrong, if this problem persists please contact support." ,
83- } ) ;
95+ return typedjson ( { bulkAction : data , autoReloadPollIntervalMs, canAbort } ) ;
96+ } catch ( error ) {
97+ console . error ( error ) ;
98+ throw new Response ( undefined , {
99+ status : 400 ,
100+ statusText : "Something went wrong, if this problem persists please contact support." ,
101+ } ) ;
102+ }
84103 }
85- } ;
104+ ) ;
86105
87- export const action = async ( { request, params } : ActionFunctionArgs ) => {
88- const userId = await requireUserId ( request ) ;
89- const { organizationSlug, projectParam, envParam, bulkActionParam } =
90- BulkActionParamSchema . parse ( params ) ;
106+ export const action = dashboardAction (
107+ {
108+ params : BulkActionParamSchema ,
109+ context : async ( params ) => {
110+ const organizationId = await resolveOrgIdFromSlug ( params . organizationSlug ) ;
111+ return organizationId ? { organizationId } : { } ;
112+ } ,
113+ authorization : { action : "write" , resource : { type : "runs" } } ,
114+ } ,
115+ async ( { request, params, user } ) => {
116+ const { organizationSlug, projectParam, envParam, bulkActionParam } = params ;
91117
92- const project = await findProjectBySlug ( organizationSlug , projectParam , userId ) ;
93- if ( ! project ) {
94- throw new Response ( "Not Found" , { status : 404 } ) ;
95- }
118+ const project = await findProjectBySlug ( organizationSlug , projectParam , user . id ) ;
119+ if ( ! project ) {
120+ throw new Response ( "Not Found" , { status : 404 } ) ;
121+ }
96122
97- const environment = await findEnvironmentBySlug ( project . id , envParam , userId ) ;
98- if ( ! environment ) {
99- throw new Response ( "Not Found" , { status : 404 } ) ;
100- }
123+ const environment = await findEnvironmentBySlug ( project . id , envParam , user . id ) ;
124+ if ( ! environment ) {
125+ throw new Response ( "Not Found" , { status : 404 } ) ;
126+ }
127+
128+ const service = new BulkActionService ( ) ;
129+ const [ error , result ] = await tryCatch ( service . abort ( bulkActionParam , environment . id ) ) ;
101130
102- const service = new BulkActionService ( ) ;
103- const [ error , result ] = await tryCatch ( service . abort ( bulkActionParam , environment . id ) ) ;
131+ if ( error ) {
132+ logger . error ( "Failed to abort bulk action" , {
133+ error,
134+ } ) ;
104135
105- if ( error ) {
106- logger . error ( "Failed to abort bulk action" , {
107- error,
108- } ) ;
136+ return redirectWithErrorMessage (
137+ v3BulkActionPath (
138+ { slug : organizationSlug } ,
139+ { slug : projectParam } ,
140+ { slug : envParam } ,
141+ { friendlyId : bulkActionParam }
142+ ) ,
143+ request ,
144+ `Failed to abort bulk action: ${ error . message } `
145+ ) ;
146+ }
109147
110- return redirectWithErrorMessage (
148+ return redirectWithSuccessMessage (
111149 v3BulkActionPath (
112150 { slug : organizationSlug } ,
113151 { slug : projectParam } ,
114152 { slug : envParam } ,
115153 { friendlyId : bulkActionParam }
116154 ) ,
117155 request ,
118- `Failed to abort bulk action: ${ error . message } `
156+ "Bulk action aborted"
119157 ) ;
120158 }
121-
122- return redirectWithSuccessMessage (
123- v3BulkActionPath (
124- { slug : organizationSlug } ,
125- { slug : projectParam } ,
126- { slug : envParam } ,
127- { friendlyId : bulkActionParam }
128- ) ,
129- request ,
130- "Bulk action aborted"
131- ) ;
132- } ;
159+ ) ;
133160
134161export default function Page ( ) {
135- const { bulkAction, autoReloadPollIntervalMs } = useTypedLoaderData < typeof loader > ( ) ;
162+ const { bulkAction, autoReloadPollIntervalMs, canAbort } = useTypedLoaderData < typeof loader > ( ) ;
136163 const organization = useOrganization ( ) ;
137164 const project = useProject ( ) ;
138165 const environment = useEnvironment ( ) ;
@@ -162,9 +189,14 @@ export default function Page() {
162189 < BulkActionStatusCombo status = { bulkAction . status } />
163190 { bulkAction . status === "PENDING" ? (
164191 < Form method = "post" >
165- < Button type = "submit" variant = "danger/small" >
192+ < PermissionButton
193+ type = "submit"
194+ variant = "danger/small"
195+ hasPermission = { canAbort }
196+ noPermissionTooltip = "You don't have permission to abort bulk actions"
197+ >
166198 Abort bulk action
167- </ Button >
199+ </ PermissionButton >
168200 </ Form >
169201 ) : null }
170202 </ div >
0 commit comments