-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat(webapp): add region override to the bulk replay action #4022
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| area: webapp | ||
| type: feature | ||
| --- | ||
|
|
||
| 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. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -39,6 +39,7 @@ import { InputGroup } from "~/components/primitives/InputGroup"; | |||||||||||||||||||||||||||
| import { Label } from "~/components/primitives/Label"; | ||||||||||||||||||||||||||||
| import { Paragraph } from "~/components/primitives/Paragraph"; | ||||||||||||||||||||||||||||
| import { RadioGroup, RadioGroupItem } from "~/components/primitives/RadioButton"; | ||||||||||||||||||||||||||||
| import { Select, SelectItem } from "~/components/primitives/Select"; | ||||||||||||||||||||||||||||
| import { type TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters"; | ||||||||||||||||||||||||||||
| import { useEnvironment } from "~/hooks/useEnvironment"; | ||||||||||||||||||||||||||||
| import { useOptimisticLocation } from "~/hooks/useOptimisticLocation"; | ||||||||||||||||||||||||||||
|
|
@@ -51,6 +52,7 @@ import { resolveOrgIdFromSlug } from "~/models/organization.server"; | |||||||||||||||||||||||||||
| import { findProjectBySlug } from "~/models/project.server"; | ||||||||||||||||||||||||||||
| import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; | ||||||||||||||||||||||||||||
| import { CreateBulkActionPresenter } from "~/presenters/v3/CreateBulkActionPresenter.server"; | ||||||||||||||||||||||||||||
| import { RegionsPresenter } from "~/presenters/v3/RegionsPresenter.server"; | ||||||||||||||||||||||||||||
| import { RUNS_BULK_INSPECTOR_UI_SEARCH_PARAMS } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/shouldRevalidateRunsList"; | ||||||||||||||||||||||||||||
| import { logger } from "~/services/logger.server"; | ||||||||||||||||||||||||||||
| import { dashboardAction, dashboardLoader } from "~/services/routeBuilders/dashboardBuilder"; | ||||||||||||||||||||||||||||
|
|
@@ -82,20 +84,27 @@ export const loader = dashboardLoader( | |||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const presenter = new CreateBulkActionPresenter(); | ||||||||||||||||||||||||||||
| const data = await presenter.call({ | ||||||||||||||||||||||||||||
| organizationId: project.organizationId, | ||||||||||||||||||||||||||||
| projectId: project.id, | ||||||||||||||||||||||||||||
| environmentId: environment.id, | ||||||||||||||||||||||||||||
| request, | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
| const [data, regionsResult] = await Promise.all([ | ||||||||||||||||||||||||||||
| presenter.call({ | ||||||||||||||||||||||||||||
| organizationId: project.organizationId, | ||||||||||||||||||||||||||||
| projectId: project.id, | ||||||||||||||||||||||||||||
| environmentId: environment.id, | ||||||||||||||||||||||||||||
| request, | ||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||
| new RegionsPresenter().call({ | ||||||||||||||||||||||||||||
| userId: user.id, | ||||||||||||||||||||||||||||
| projectSlug: projectParam, | ||||||||||||||||||||||||||||
| isAdmin: user.admin || user.isImpersonating, | ||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Display flag for the inspector's Cancel/Replay controls — the action | ||||||||||||||||||||||||||||
| // below enforces write:runs independently. | ||||||||||||||||||||||||||||
| const { canCreateBulkAction } = checkPermissions(ability, { | ||||||||||||||||||||||||||||
| canCreateBulkAction: { action: "write", resource: { type: "runs" } }, | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return typedjson({ ...data, canCreateBulkAction }); | ||||||||||||||||||||||||||||
| return typedjson({ ...data, regions: regionsResult.regions, canCreateBulkAction }); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
@@ -104,6 +113,10 @@ export const CreateBulkActionSearchParams = z.object({ | |||||||||||||||||||||||||||
| action: BulkActionAction.default("cancel"), | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Sentinel for the "Override region" dropdown meaning "keep each run's original | ||||||||||||||||||||||||||||
| // region". Normalized to `undefined` in the action so the service never sees it. | ||||||||||||||||||||||||||||
| const REPLAY_REGION_NO_OVERRIDE_VALUE = "__no_override__"; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| export const CreateBulkActionPayload = z.discriminatedUnion("mode", [ | ||||||||||||||||||||||||||||
| z.object({ | ||||||||||||||||||||||||||||
| mode: z.literal("selected"), | ||||||||||||||||||||||||||||
|
|
@@ -114,13 +127,15 @@ export const CreateBulkActionPayload = z.discriminatedUnion("mode", [ | |||||||||||||||||||||||||||
| return []; | ||||||||||||||||||||||||||||
| }, z.array(z.string())), | ||||||||||||||||||||||||||||
| title: z.string().optional(), | ||||||||||||||||||||||||||||
| region: z.string().optional(), | ||||||||||||||||||||||||||||
| failedRedirect: z.string(), | ||||||||||||||||||||||||||||
| emailNotification: z.preprocess((value) => value === "on", z.boolean()), | ||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||
| z.object({ | ||||||||||||||||||||||||||||
| mode: z.literal("filter"), | ||||||||||||||||||||||||||||
| action: BulkActionAction, | ||||||||||||||||||||||||||||
| title: z.string().optional(), | ||||||||||||||||||||||||||||
| region: z.string().optional(), | ||||||||||||||||||||||||||||
| failedRedirect: z.string(), | ||||||||||||||||||||||||||||
| emailNotification: z.preprocess((value) => value === "on", z.boolean()), | ||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||
|
|
@@ -160,6 +175,12 @@ export const action = dashboardAction( | |||||||||||||||||||||||||||
| return redirectWithErrorMessage("/", request, "Invalid bulk action"); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // "Don't override" keeps each run's original region — drop it so it isn't | ||||||||||||||||||||||||||||
| // stored as a real override. | ||||||||||||||||||||||||||||
| if (submission.value.region === REPLAY_REGION_NO_OVERRIDE_VALUE) { | ||||||||||||||||||||||||||||
| submission.value.region = undefined; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const service = new BulkActionService(); | ||||||||||||||||||||||||||||
| const [error, result] = await tryCatch( | ||||||||||||||||||||||||||||
| service.create( | ||||||||||||||||||||||||||||
|
|
@@ -238,6 +259,21 @@ export function CreateBulkActionInspector({ | |||||||||||||||||||||||||||
| const impactedCountElement = | ||||||||||||||||||||||||||||
| mode === "selected" ? selectedItems.size : <EstimatedCount count={data?.count} />; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Region is a replay-only override and only applies to deployed environments. | ||||||||||||||||||||||||||||
| // The default keeps each run in its original region so a bulk action spanning | ||||||||||||||||||||||||||||
| // multiple regions doesn't silently re-route runs. | ||||||||||||||||||||||||||||
| const regions = data?.regions ?? []; | ||||||||||||||||||||||||||||
| const showRegion = | ||||||||||||||||||||||||||||
| action === "replay" && environment.type !== "DEVELOPMENT" && regions.length > 1; | ||||||||||||||||||||||||||||
| const regionItems = [ | ||||||||||||||||||||||||||||
| { value: REPLAY_REGION_NO_OVERRIDE_VALUE, label: "Don't override", isDefault: false }, | ||||||||||||||||||||||||||||
| ...regions.map((r) => ({ | ||||||||||||||||||||||||||||
| value: r.name, | ||||||||||||||||||||||||||||
| label: r.description ? `${r.name} — ${r.description}` : r.name, | ||||||||||||||||||||||||||||
|
Comment on lines
+268
to
+272
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Use the region routing key ( Line 245 currently posts Suggested fix const regionItems = [
{ value: REPLAY_REGION_NO_OVERRIDE_VALUE, label: "Don't override", isDefault: false },
...regions.map((r) => ({
- value: r.name,
+ value: r.masterQueue,
label: r.description ? `${r.name} — ${r.description}` : r.name,
isDefault: r.isDefault,
})),
];📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||
| isDefault: r.isDefault, | ||||||||||||||||||||||||||||
| })), | ||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||
| <Form | ||||||||||||||||||||||||||||
| method="post" | ||||||||||||||||||||||||||||
|
|
@@ -368,6 +404,34 @@ export function CreateBulkActionInspector({ | |||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||
| </RadioGroup> | ||||||||||||||||||||||||||||
| </InputGroup> | ||||||||||||||||||||||||||||
| {showRegion && ( | ||||||||||||||||||||||||||||
| <InputGroup> | ||||||||||||||||||||||||||||
| <Label htmlFor="region">Override region</Label> | ||||||||||||||||||||||||||||
| {/* Our Select primitive uses Ariakit, which treats value={undefined} | ||||||||||||||||||||||||||||
| as uncontrolled and keeps stale state when switching environments. | ||||||||||||||||||||||||||||
| The key forces a remount so it reinitializes with the default value. */} | ||||||||||||||||||||||||||||
| <Select | ||||||||||||||||||||||||||||
| key={`bulk-region-${environment.id}`} | ||||||||||||||||||||||||||||
| name="region" | ||||||||||||||||||||||||||||
| variant="tertiary/medium" | ||||||||||||||||||||||||||||
| dropdownIcon | ||||||||||||||||||||||||||||
| items={regionItems} | ||||||||||||||||||||||||||||
| defaultValue={REPLAY_REGION_NO_OVERRIDE_VALUE} | ||||||||||||||||||||||||||||
| text={(value) => regionItems.find((r) => r.value === value)?.label} | ||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||
| {regionItems.map((r) => ( | ||||||||||||||||||||||||||||
| <SelectItem key={r.value} value={r.value}> | ||||||||||||||||||||||||||||
| {r.label} | ||||||||||||||||||||||||||||
| {r.isDefault ? " (default)" : ""} | ||||||||||||||||||||||||||||
| </SelectItem> | ||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||
| </Select> | ||||||||||||||||||||||||||||
| <Hint> | ||||||||||||||||||||||||||||
| By default each run is replayed in its original region. Select a region to run | ||||||||||||||||||||||||||||
| them all there instead. | ||||||||||||||||||||||||||||
| </Hint> | ||||||||||||||||||||||||||||
| </InputGroup> | ||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||
| <InputGroup> | ||||||||||||||||||||||||||||
| <Label>Preview</Label> | ||||||||||||||||||||||||||||
| <BulkActionFilterSummary | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,6 +37,12 @@ export class BulkActionService extends BaseService { | |
| ) { | ||
| const filters = await getFilters(payload, request); | ||
|
|
||
| // Region is a replay-only override that re-routes the replayed runs. It's | ||
| // stored alongside the run-list filters under a dedicated key so it isn't | ||
| // mistaken for a `regions` selection filter when the params are parsed. | ||
| const replayRegion = payload.action === "replay" ? payload.region : undefined; | ||
| const params = replayRegion ? { ...filters, replayRegion } : filters; | ||
|
|
||
| // Count the runs that will be affected by the bulk action | ||
| const clickhouse = await clickhouseFactory.getClickhouseForOrganization(organizationId, "standard"); | ||
| const runsRepository = new RunsRepository({ | ||
|
|
@@ -61,7 +67,7 @@ export class BulkActionService extends BaseService { | |
| userId, | ||
| name: payload.title, | ||
| type: payload.action === "cancel" ? BulkActionType.CANCEL : BulkActionType.REPLAY, | ||
| params: filters, | ||
| params, | ||
| queryName: "bulk_action_v1", | ||
| totalCount: count, | ||
| completionNotification: | ||
|
|
@@ -141,6 +147,10 @@ export class BulkActionService extends BaseService { | |
| // 2. Parse the params | ||
| const rawParams = group.params && typeof group.params === "object" ? group.params : {}; | ||
| const finalizeRun = "finalizeRun" in rawParams && (rawParams as any).finalizeRun === true; | ||
| const replayRegion = | ||
| "replayRegion" in rawParams && typeof (rawParams as any).replayRegion === "string" | ||
| ? (rawParams as any).replayRegion | ||
| : undefined; | ||
| const filters = parseRunListInputOptions({ | ||
| organizationId: group.project.organizationId, | ||
| projectId: group.projectId, | ||
|
|
@@ -254,6 +264,7 @@ export class BulkActionService extends BaseService { | |
| replayService.call(run, { | ||
| bulkActionId: bulkActionId, | ||
| triggerSource: "dashboard", | ||
| region: replayRegion, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 Region value uses workerInstanceGroup.name — verify consistency with TriggerTaskService The region Select sends Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| }) | ||
| ); | ||
| if (error) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚩 No server-side validation that the submitted region is in the allowed list
The action handler at lines 180-182 normalizes the
__no_override__sentinel but does not validate that a non-sentinel region value is actually in the user's allowed region list. A crafted form submission could pass an arbitrary region string. However, this is consistent with the single-run replay path (resources.taskruns.$runParam.replay.ts:430) which also passessubmission.value.regiondirectly toReplayTaskRunServicewithout validation. The downstreamTriggerTaskServicelikely rejects invalid regions at queue resolution time, but adding an explicit check in the action would be a defense-in-depth improvement.Was this helpful? React with 👍 or 👎 to provide feedback.