Skip to content

Commit d74adf4

Browse files
committed
feat(webapp): add region override to the bulk replay action
When replaying runs in bulk from a deployed environment, you can now choose which region the replayed runs run in. The inspector shows an "Override region" dropdown that defaults to "Don't override", which keeps each run in its original region, so replaying a selection that spans multiple regions doesn't silently re-route anything. Pick a region and every matched run is replayed there instead.
1 parent 5667461 commit d74adf4

3 files changed

Lines changed: 89 additions & 8 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
Add an "Override region" option to the bulk replay action so replayed runs can be routed to a chosen region, defaulting to keeping each run in its original region.

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.bulkaction.tsx

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { InputGroup } from "~/components/primitives/InputGroup";
3939
import { Label } from "~/components/primitives/Label";
4040
import { Paragraph } from "~/components/primitives/Paragraph";
4141
import { RadioGroup, RadioGroupItem } from "~/components/primitives/RadioButton";
42+
import { Select, SelectItem } from "~/components/primitives/Select";
4243
import { type TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters";
4344
import { useEnvironment } from "~/hooks/useEnvironment";
4445
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
@@ -51,6 +52,7 @@ import { resolveOrgIdFromSlug } from "~/models/organization.server";
5152
import { findProjectBySlug } from "~/models/project.server";
5253
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
5354
import { CreateBulkActionPresenter } from "~/presenters/v3/CreateBulkActionPresenter.server";
55+
import { RegionsPresenter } from "~/presenters/v3/RegionsPresenter.server";
5456
import { RUNS_BULK_INSPECTOR_UI_SEARCH_PARAMS } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/shouldRevalidateRunsList";
5557
import { logger } from "~/services/logger.server";
5658
import { 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+
107120
export 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

apps/webapp/app/v3/services/bulk/BulkActionV2.server.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ export class BulkActionService extends BaseService {
3737
) {
3838
const filters = await getFilters(payload, request);
3939

40+
// Region is a replay-only override that re-routes the replayed runs. It's
41+
// stored alongside the run-list filters under a dedicated key so it isn't
42+
// mistaken for a `regions` selection filter when the params are parsed.
43+
const replayRegion = payload.action === "replay" ? payload.region : undefined;
44+
const params = replayRegion ? { ...filters, replayRegion } : filters;
45+
4046
// Count the runs that will be affected by the bulk action
4147
const clickhouse = await clickhouseFactory.getClickhouseForOrganization(organizationId, "standard");
4248
const runsRepository = new RunsRepository({
@@ -61,7 +67,7 @@ export class BulkActionService extends BaseService {
6167
userId,
6268
name: payload.title,
6369
type: payload.action === "cancel" ? BulkActionType.CANCEL : BulkActionType.REPLAY,
64-
params: filters,
70+
params,
6571
queryName: "bulk_action_v1",
6672
totalCount: count,
6773
completionNotification:
@@ -141,6 +147,10 @@ export class BulkActionService extends BaseService {
141147
// 2. Parse the params
142148
const rawParams = group.params && typeof group.params === "object" ? group.params : {};
143149
const finalizeRun = "finalizeRun" in rawParams && (rawParams as any).finalizeRun === true;
150+
const replayRegion =
151+
"replayRegion" in rawParams && typeof (rawParams as any).replayRegion === "string"
152+
? (rawParams as any).replayRegion
153+
: undefined;
144154
const filters = parseRunListInputOptions({
145155
organizationId: group.project.organizationId,
146156
projectId: group.projectId,
@@ -254,6 +264,7 @@ export class BulkActionService extends BaseService {
254264
replayService.call(run, {
255265
bulkActionId: bulkActionId,
256266
triggerSource: "dashboard",
267+
region: replayRegion,
257268
})
258269
);
259270
if (error) {

0 commit comments

Comments
 (0)