@@ -40,6 +40,7 @@ import { InputGroup } from "~/components/primitives/InputGroup";
4040import { Label } from "~/components/primitives/Label" ;
4141import { Paragraph } from "~/components/primitives/Paragraph" ;
4242import { RadioGroup , RadioGroupItem } from "~/components/primitives/RadioButton" ;
43+ import { Select , SelectItem } from "~/components/primitives/Select" ;
4344import { type TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters" ;
4445import { useEnvironment } from "~/hooks/useEnvironment" ;
4546import { useOptimisticLocation } from "~/hooks/useOptimisticLocation" ;
@@ -51,44 +52,56 @@ import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/m
5152import { findProjectBySlug } from "~/models/project.server" ;
5253import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server" ;
5354import { CreateBulkActionPresenter } from "~/presenters/v3/CreateBulkActionPresenter.server" ;
55+ import { RegionsPresenter } from "~/presenters/v3/RegionsPresenter.server" ;
5456import { RUNS_BULK_INSPECTOR_UI_SEARCH_PARAMS } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/shouldRevalidateRunsList" ;
5557import { logger } from "~/services/logger.server" ;
56- import { requireUserId } from "~/services/session.server" ;
58+ import { requireUser , requireUserId } from "~/services/session.server" ;
5759import { cn } from "~/utils/cn" ;
5860import { EnvironmentParamSchema , v3BulkActionPath } from "~/utils/pathBuilder" ;
5961import { BulkActionService } from "~/v3/services/bulk/BulkActionV2.server" ;
6062
6163export async function loader ( { request, params } : LoaderFunctionArgs ) {
62- const userId = await requireUserId ( request ) ;
64+ const user = await requireUser ( request ) ;
6365
6466 const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema . parse ( params ) ;
6567
66- const project = await findProjectBySlug ( organizationSlug , projectParam , userId ) ;
68+ const project = await findProjectBySlug ( organizationSlug , projectParam , user . id ) ;
6769 if ( ! project ) {
6870 throw new Response ( "Not Found" , { status : 404 } ) ;
6971 }
7072
71- const environment = await findEnvironmentBySlug ( project . id , envParam , userId ) ;
73+ const environment = await findEnvironmentBySlug ( project . id , envParam , user . id ) ;
7274 if ( ! environment ) {
7375 throw new Response ( "Not Found" , { status : 404 } ) ;
7476 }
7577
7678 const presenter = new CreateBulkActionPresenter ( ) ;
77- const data = await presenter . call ( {
78- organizationId : project . organizationId ,
79- projectId : project . id ,
80- environmentId : environment . id ,
81- request,
82- } ) ;
79+ const [ data , regionsResult ] = await Promise . all ( [
80+ presenter . call ( {
81+ organizationId : project . organizationId ,
82+ projectId : project . id ,
83+ environmentId : environment . id ,
84+ request,
85+ } ) ,
86+ new RegionsPresenter ( ) . call ( {
87+ userId : user . id ,
88+ projectSlug : projectParam ,
89+ isAdmin : user . admin || user . isImpersonating ,
90+ } ) ,
91+ ] ) ;
8392
84- return typedjson ( data ) ;
93+ return typedjson ( { ... data , regions : regionsResult . regions } ) ;
8594}
8695
8796export const CreateBulkActionSearchParams = z . object ( {
8897 mode : BulkActionMode . default ( "filter" ) ,
8998 action : BulkActionAction . default ( "cancel" ) ,
9099} ) ;
91100
101+ // Sentinel for the "Override region" dropdown meaning "keep each run's original
102+ // region". Normalized to `undefined` in the action so the service never sees it.
103+ const REPLAY_REGION_NO_OVERRIDE_VALUE = "__no_override__" ;
104+
92105export const CreateBulkActionPayload = z . discriminatedUnion ( "mode" , [
93106 z . object ( {
94107 mode : z . literal ( "selected" ) ,
@@ -99,13 +112,15 @@ export const CreateBulkActionPayload = z.discriminatedUnion("mode", [
99112 return [ ] ;
100113 } , z . array ( z . string ( ) ) ) ,
101114 title : z . string ( ) . optional ( ) ,
115+ region : z . string ( ) . optional ( ) ,
102116 failedRedirect : z . string ( ) ,
103117 emailNotification : z . preprocess ( ( value ) => value === "on" , z . boolean ( ) ) ,
104118 } ) ,
105119 z . object ( {
106120 mode : z . literal ( "filter" ) ,
107121 action : BulkActionAction ,
108122 title : z . string ( ) . optional ( ) ,
123+ region : z . string ( ) . optional ( ) ,
109124 failedRedirect : z . string ( ) ,
110125 emailNotification : z . preprocess ( ( value ) => value === "on" , z . boolean ( ) ) ,
111126 } ) ,
@@ -138,6 +153,12 @@ export async function action({ params, request }: ActionFunctionArgs) {
138153 return redirectWithErrorMessage ( "/" , request , "Invalid bulk action" ) ;
139154 }
140155
156+ // "Don't override" keeps each run's original region — drop it so it isn't
157+ // stored as a real override.
158+ if ( submission . value . region === REPLAY_REGION_NO_OVERRIDE_VALUE ) {
159+ submission . value . region = undefined ;
160+ }
161+
141162 const service = new BulkActionService ( ) ;
142163 const [ error , result ] = await tryCatch (
143164 service . create (
@@ -212,6 +233,21 @@ export function CreateBulkActionInspector({
212233 const impactedCountElement =
213234 mode === "selected" ? selectedItems . size : < EstimatedCount count = { data ?. count } /> ;
214235
236+ // Region is a replay-only override and only applies to deployed environments.
237+ // The default keeps each run in its original region so a bulk action spanning
238+ // multiple regions doesn't silently re-route runs.
239+ const regions = data ?. regions ?? [ ] ;
240+ const showRegion =
241+ action === "replay" && environment . type !== "DEVELOPMENT" && regions . length > 1 ;
242+ const regionItems = [
243+ { value : REPLAY_REGION_NO_OVERRIDE_VALUE , label : "Don't override" , isDefault : false } ,
244+ ...regions . map ( ( r ) => ( {
245+ value : r . name ,
246+ label : r . description ? `${ r . name } — ${ r . description } ` : r . name ,
247+ isDefault : r . isDefault ,
248+ } ) ) ,
249+ ] ;
250+
215251 return (
216252 < Form
217253 method = "post"
@@ -342,6 +378,34 @@ export function CreateBulkActionInspector({
342378 />
343379 </ RadioGroup >
344380 </ InputGroup >
381+ { showRegion && (
382+ < InputGroup >
383+ < Label htmlFor = "region" > Override region</ Label >
384+ { /* Our Select primitive uses Ariakit, which treats value={undefined}
385+ as uncontrolled and keeps stale state when switching environments.
386+ The key forces a remount so it reinitializes with the default value. */ }
387+ < Select
388+ key = { `bulk-region-${ environment . id } ` }
389+ name = "region"
390+ variant = "tertiary/medium"
391+ dropdownIcon
392+ items = { regionItems }
393+ defaultValue = { REPLAY_REGION_NO_OVERRIDE_VALUE }
394+ text = { ( value ) => regionItems . find ( ( r ) => r . value === value ) ?. label }
395+ >
396+ { regionItems . map ( ( r ) => (
397+ < SelectItem key = { r . value } value = { r . value } >
398+ { r . label }
399+ { r . isDefault ? " (default)" : "" }
400+ </ SelectItem >
401+ ) ) }
402+ </ Select >
403+ < Hint >
404+ By default each run is replayed in its original region. Select a region to run
405+ them all there instead.
406+ </ Hint >
407+ </ InputGroup >
408+ ) }
345409 < InputGroup >
346410 < Label > Preview</ Label >
347411 < BulkActionFilterSummary
0 commit comments