@@ -39,6 +39,7 @@ import { InputGroup } from "~/components/primitives/InputGroup";
3939import { Label } from "~/components/primitives/Label" ;
4040import { Paragraph } from "~/components/primitives/Paragraph" ;
4141import { RadioGroup , RadioGroupItem } from "~/components/primitives/RadioButton" ;
42+ import { Select , SelectItem } from "~/components/primitives/Select" ;
4243import { type TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters" ;
4344import { useEnvironment } from "~/hooks/useEnvironment" ;
4445import { useOptimisticLocation } from "~/hooks/useOptimisticLocation" ;
@@ -51,6 +52,7 @@ import { resolveOrgIdFromSlug } from "~/models/organization.server";
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" ;
5658import { dashboardAction , dashboardLoader } from "~/services/routeBuilders/dashboardBuilder" ;
@@ -82,20 +84,27 @@ export const loader = dashboardLoader(
8284 }
8385
8486 const presenter = new CreateBulkActionPresenter ( ) ;
85- const data = await presenter . call ( {
86- organizationId : project . organizationId ,
87- projectId : project . id ,
88- environmentId : environment . id ,
89- request,
90- } ) ;
87+ const [ data , regionsResult ] = await Promise . all ( [
88+ presenter . call ( {
89+ organizationId : project . organizationId ,
90+ projectId : project . id ,
91+ environmentId : environment . id ,
92+ request,
93+ } ) ,
94+ new RegionsPresenter ( ) . call ( {
95+ userId : user . id ,
96+ projectSlug : projectParam ,
97+ isAdmin : user . admin || user . isImpersonating ,
98+ } ) ,
99+ ] ) ;
91100
92101 // Display flag for the inspector's Cancel/Replay controls — the action
93102 // below enforces write:runs independently.
94103 const { canCreateBulkAction } = checkPermissions ( ability , {
95104 canCreateBulkAction : { action : "write" , resource : { type : "runs" } } ,
96105 } ) ;
97106
98- return typedjson ( { ...data , canCreateBulkAction } ) ;
107+ return typedjson ( { ...data , regions : regionsResult . regions , canCreateBulkAction } ) ;
99108 }
100109) ;
101110
@@ -104,6 +113,10 @@ export const CreateBulkActionSearchParams = z.object({
104113 action : BulkActionAction . default ( "cancel" ) ,
105114} ) ;
106115
116+ // Sentinel for the "Override region" dropdown meaning "keep each run's original
117+ // region". Normalized to `undefined` in the action so the service never sees it.
118+ const REPLAY_REGION_NO_OVERRIDE_VALUE = "__no_override__" ;
119+
107120export const CreateBulkActionPayload = z . discriminatedUnion ( "mode" , [
108121 z . object ( {
109122 mode : z . literal ( "selected" ) ,
@@ -114,13 +127,15 @@ export const CreateBulkActionPayload = z.discriminatedUnion("mode", [
114127 return [ ] ;
115128 } , z . array ( z . string ( ) ) ) ,
116129 title : z . string ( ) . optional ( ) ,
130+ region : z . string ( ) . optional ( ) ,
117131 failedRedirect : z . string ( ) ,
118132 emailNotification : z . preprocess ( ( value ) => value === "on" , z . boolean ( ) ) ,
119133 } ) ,
120134 z . object ( {
121135 mode : z . literal ( "filter" ) ,
122136 action : BulkActionAction ,
123137 title : z . string ( ) . optional ( ) ,
138+ region : z . string ( ) . optional ( ) ,
124139 failedRedirect : z . string ( ) ,
125140 emailNotification : z . preprocess ( ( value ) => value === "on" , z . boolean ( ) ) ,
126141 } ) ,
@@ -160,6 +175,12 @@ export const action = dashboardAction(
160175 return redirectWithErrorMessage ( "/" , request , "Invalid bulk action" ) ;
161176 }
162177
178+ // "Don't override" keeps each run's original region — drop it so it isn't
179+ // stored as a real override.
180+ if ( submission . value . region === REPLAY_REGION_NO_OVERRIDE_VALUE ) {
181+ submission . value . region = undefined ;
182+ }
183+
163184 const service = new BulkActionService ( ) ;
164185 const [ error , result ] = await tryCatch (
165186 service . create (
@@ -238,6 +259,21 @@ export function CreateBulkActionInspector({
238259 const impactedCountElement =
239260 mode === "selected" ? selectedItems . size : < EstimatedCount count = { data ?. count } /> ;
240261
262+ // Region is a replay-only override and only applies to deployed environments.
263+ // The default keeps each run in its original region so a bulk action spanning
264+ // multiple regions doesn't silently re-route runs.
265+ const regions = data ?. regions ?? [ ] ;
266+ const showRegion =
267+ action === "replay" && environment . type !== "DEVELOPMENT" && regions . length > 1 ;
268+ const regionItems = [
269+ { value : REPLAY_REGION_NO_OVERRIDE_VALUE , label : "Don't override" , isDefault : false } ,
270+ ...regions . map ( ( r ) => ( {
271+ value : r . name ,
272+ label : r . description ? `${ r . name } — ${ r . description } ` : r . name ,
273+ isDefault : r . isDefault ,
274+ } ) ) ,
275+ ] ;
276+
241277 return (
242278 < Form
243279 method = "post"
@@ -368,6 +404,34 @@ export function CreateBulkActionInspector({
368404 />
369405 </ RadioGroup >
370406 </ InputGroup >
407+ { showRegion && (
408+ < InputGroup >
409+ < Label htmlFor = "region" > Override region</ Label >
410+ { /* Our Select primitive uses Ariakit, which treats value={undefined}
411+ as uncontrolled and keeps stale state when switching environments.
412+ The key forces a remount so it reinitializes with the default value. */ }
413+ < Select
414+ key = { `bulk-region-${ environment . id } ` }
415+ name = "region"
416+ variant = "tertiary/medium"
417+ dropdownIcon
418+ items = { regionItems }
419+ defaultValue = { REPLAY_REGION_NO_OVERRIDE_VALUE }
420+ text = { ( value ) => regionItems . find ( ( r ) => r . value === value ) ?. label }
421+ >
422+ { regionItems . map ( ( r ) => (
423+ < SelectItem key = { r . value } value = { r . value } >
424+ { r . label }
425+ { r . isDefault ? " (default)" : "" }
426+ </ SelectItem >
427+ ) ) }
428+ </ Select >
429+ < Hint >
430+ By default each run is replayed in its original region. Select a region to run
431+ them all there instead.
432+ </ Hint >
433+ </ InputGroup >
434+ ) }
371435 < InputGroup >
372436 < Label > Preview</ Label >
373437 < BulkActionFilterSummary
0 commit comments