Skip to content

Commit c9e78ec

Browse files
committed
feat: add region selector to test and replay task UI
Allow users to select which region to run a task in when testing or replaying from the webapp dashboard. Previously this was only possible via the SDK's trigger() options. The region selector appears in the options panel alongside machine preset, and only shows when multiple regions are available to the project. Closes #3016
1 parent 4c986ad commit c9e78ec

File tree

6 files changed

+178
-19
lines changed

6 files changed

+178
-19
lines changed

apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ function ReplayForm({
201201
tags,
202202
version,
203203
machine,
204+
region,
204205
prioritySeconds,
205206
},
206207
] = useForm({
@@ -357,6 +358,35 @@ function ReplayForm({
357358
)}
358359
<FormError id={version.errorId}>{version.error}</FormError>
359360
</InputGroup>
361+
{replayData.regions.length > 1 && (
362+
<InputGroup>
363+
<Label htmlFor={region.id} variant="small">
364+
Region
365+
</Label>
366+
<Select
367+
{...conform.select(region)}
368+
variant="tertiary/small"
369+
placeholder={replayData.disableVersionSelection ? "–" : undefined}
370+
dropdownIcon
371+
items={replayData.regions}
372+
defaultValue={replayData.region ?? undefined}
373+
disabled={replayData.disableVersionSelection}
374+
>
375+
{replayData.regions.map((r) => (
376+
<SelectItem key={r.name} value={r.name}>
377+
{r.description ? `${r.name}${r.description}` : r.name}
378+
{r.isDefault ? " (default)" : ""}
379+
</SelectItem>
380+
))}
381+
</Select>
382+
{replayData.disableVersionSelection ? (
383+
<Hint>Region is not available in the development environment.</Hint>
384+
) : (
385+
<Hint>Overrides the region for this run.</Hint>
386+
)}
387+
<FormError id={region.errorId}>{region.error}</FormError>
388+
</InputGroup>
389+
)}
360390
<InputGroup>
361391
<Label htmlFor={queue.id} variant="small">
362392
Queue

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx

Lines changed: 127 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ import {
5454
TestTaskPresenter,
5555
} from "~/presenters/v3/TestTaskPresenter.server";
5656
import { logger } from "~/services/logger.server";
57-
import { requireUserId } from "~/services/session.server";
57+
import { requireUser } from "~/services/session.server";
5858
import { cn } from "~/utils/cn";
5959
import { docsPath, v3RunSpanPath, v3TaskParamsSchema, v3TestPath } from "~/utils/pathBuilder";
6060
import { TestTaskService } from "~/v3/services/testTask.server";
@@ -75,22 +75,23 @@ import { DialogClose, DialogDescription } from "@radix-ui/react-dialog";
7575
import { FormButtons } from "~/components/primitives/FormButtons";
7676
import { $replica } from "~/db.server";
7777
import { clickhouseClient } from "~/services/clickhouseInstance.server";
78+
import { RegionsPresenter, type Region } from "~/presenters/v3/RegionsPresenter.server";
7879

7980
type FormAction = "create-template" | "delete-template" | "run-scheduled" | "run-standard";
8081

8182
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
82-
const userId = await requireUserId(request);
83+
const user = await requireUser(request);
8384
const { projectParam, organizationSlug, envParam, taskParam } = v3TaskParamsSchema.parse(params);
8485

85-
const project = await findProjectBySlug(organizationSlug, projectParam, userId);
86+
const project = await findProjectBySlug(organizationSlug, projectParam, user.id);
8687
if (!project) {
8788
throw new Response(undefined, {
8889
status: 404,
8990
statusText: "Project not found",
9091
});
9192
}
9293

93-
const environment = await findEnvironmentBySlug(project.id, envParam, userId);
94+
const environment = await findEnvironmentBySlug(project.id, envParam, user.id);
9495
if (!environment) {
9596
throw new Response(undefined, {
9697
status: 404,
@@ -100,14 +101,21 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
100101

101102
const presenter = new TestTaskPresenter($replica, clickhouseClient);
102103
try {
103-
const result = await presenter.call({
104-
userId,
105-
projectId: project.id,
106-
taskIdentifier: taskParam,
107-
environment: environment,
108-
});
109-
110-
return typedjson(result);
104+
const [result, regionsResult] = await Promise.all([
105+
presenter.call({
106+
userId: user.id,
107+
projectId: project.id,
108+
taskIdentifier: taskParam,
109+
environment: environment,
110+
}),
111+
new RegionsPresenter().call({
112+
userId: user.id,
113+
projectSlug: projectParam,
114+
isAdmin: user.admin || user.isImpersonating,
115+
}),
116+
]);
117+
118+
return typedjson({ ...result, regions: regionsResult.regions });
111119
} catch (error) {
112120
return redirectWithErrorMessage(
113121
v3TestPath({ slug: organizationSlug }, { slug: projectParam }, environment),
@@ -118,15 +126,15 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
118126
};
119127

120128
export const action: ActionFunction = async ({ request, params }) => {
121-
const userId = await requireUserId(request);
129+
const user = await requireUser(request);
122130
const { organizationSlug, projectParam, envParam } = v3TaskParamsSchema.parse(params);
123131

124-
const project = await findProjectBySlug(organizationSlug, projectParam, userId);
132+
const project = await findProjectBySlug(organizationSlug, projectParam, user.id);
125133
if (!project) {
126134
return redirectBackWithErrorMessage(request, "Project not found");
127135
}
128136

129-
const environment = await findEnvironmentBySlug(project.id, envParam, userId);
137+
const environment = await findEnvironmentBySlug(project.id, envParam, user.id);
130138

131139
if (!environment) {
132140
return redirectBackWithErrorMessage(request, "Environment not found");
@@ -290,6 +298,7 @@ export default function Page() {
290298
templates={result.taskRunTemplates}
291299
disableVersionSelection={result.disableVersionSelection}
292300
allowArbitraryQueues={result.allowArbitraryQueues}
301+
regions={result.regions}
293302
/>
294303
);
295304
}
@@ -304,6 +313,7 @@ export default function Page() {
304313
possibleTimezones={result.possibleTimezones}
305314
disableVersionSelection={result.disableVersionSelection}
306315
allowArbitraryQueues={result.allowArbitraryQueues}
316+
regions={result.regions}
307317
/>
308318
);
309319
}
@@ -324,6 +334,7 @@ function StandardTaskForm({
324334
templates,
325335
disableVersionSelection,
326336
allowArbitraryQueues,
337+
regions,
327338
}: {
328339
task: StandardTaskResult["task"];
329340
queues: Required<StandardTaskResult>["queue"][];
@@ -332,6 +343,7 @@ function StandardTaskForm({
332343
templates: RunTemplate[];
333344
disableVersionSelection: boolean;
334345
allowArbitraryQueues: boolean;
346+
regions: Region[];
335347
}) {
336348
const environment = useEnvironment();
337349
const { value, replace } = useSearchParams();
@@ -373,6 +385,12 @@ function StandardTaskForm({
373385
);
374386
const [queueValue, setQueueValue] = useState<string | undefined>(lastRun?.queue);
375387
const [machineValue, setMachineValue] = useState<string | undefined>(lastRun?.machinePreset);
388+
const isDev = environment.type === "DEVELOPMENT";
389+
const defaultRegion = regions.find((r) => r.isDefault);
390+
const [regionValue, setRegionValue] = useState<string | undefined>(
391+
isDev ? undefined : defaultRegion?.name
392+
);
393+
376394
const [maxAttemptsValue, setMaxAttemptsValue] = useState<number | undefined>(
377395
lastRun?.maxAttempts
378396
);
@@ -381,6 +399,12 @@ function StandardTaskForm({
381399
);
382400
const [tagsValue, setTagsValue] = useState<string[]>(lastRun?.runTags ?? []);
383401

402+
const regionItems = regions.map((r) => ({
403+
value: r.name,
404+
label: r.description ? `${r.name}${r.description}` : r.name,
405+
isDefault: r.isDefault,
406+
}));
407+
384408
const queueItems = queues.map((q) => ({
385409
value: q.type === "task" ? `task/${q.name}` : q.name,
386410
label: q.name,
@@ -409,6 +433,7 @@ function StandardTaskForm({
409433
tags,
410434
version,
411435
machine,
436+
region,
412437
prioritySeconds,
413438
},
414439
] = useForm({
@@ -580,6 +605,45 @@ function StandardTaskForm({
580605
)}
581606
<FormError id={version.errorId}>{version.error}</FormError>
582607
</InputGroup>
608+
{regionItems.length > 1 && (
609+
<InputGroup>
610+
<Label htmlFor={region.id} variant="small">
611+
Region
612+
</Label>
613+
{/* Our Select primitive uses Ariakit under the hood, which treats
614+
value={undefined} as uncontrolled, keeping stale internal state when
615+
switching environments. The key forces a remount so it reinitializes
616+
with the correct defaultValue. */}
617+
<Select
618+
key={`region-${environment.id}`}
619+
{...conform.select(region)}
620+
variant="tertiary/small"
621+
placeholder={isDev ? "–" : undefined}
622+
dropdownIcon
623+
items={regionItems}
624+
defaultValue={isDev ? undefined : defaultRegion?.name}
625+
value={isDev ? undefined : regionValue}
626+
setValue={isDev ? undefined : (e) => {
627+
if (Array.isArray(e)) return;
628+
setRegionValue(e);
629+
}}
630+
disabled={isDev}
631+
>
632+
{regionItems.map((r) => (
633+
<SelectItem key={r.value} value={r.value}>
634+
{r.label}
635+
{r.isDefault ? " (default)" : ""}
636+
</SelectItem>
637+
))}
638+
</Select>
639+
{isDev ? (
640+
<Hint>Region is not available in the development environment.</Hint>
641+
) : (
642+
<Hint>Overrides the region for this run.</Hint>
643+
)}
644+
<FormError id={region.errorId}>{region.error}</FormError>
645+
</InputGroup>
646+
)}
583647
<InputGroup>
584648
<Label htmlFor={queue.id} variant="small">
585649
Queue
@@ -803,6 +867,7 @@ function ScheduledTaskForm({
803867
templates,
804868
disableVersionSelection,
805869
allowArbitraryQueues,
870+
regions,
806871
}: {
807872
task: ScheduledTaskResult["task"];
808873
runs: ScheduledRun[];
@@ -812,6 +877,7 @@ function ScheduledTaskForm({
812877
templates: RunTemplate[];
813878
disableVersionSelection: boolean;
814879
allowArbitraryQueues: boolean;
880+
regions: Region[];
815881
}) {
816882
const environment = useEnvironment();
817883

@@ -833,6 +899,12 @@ function ScheduledTaskForm({
833899
);
834900
const [queueValue, setQueueValue] = useState<string | undefined>(lastRun?.queue);
835901
const [machineValue, setMachineValue] = useState<string | undefined>(lastRun?.machinePreset);
902+
const isDev = environment.type === "DEVELOPMENT";
903+
const defaultRegion = regions.find((r) => r.isDefault);
904+
const [regionValue, setRegionValue] = useState<string | undefined>(
905+
isDev ? undefined : defaultRegion?.name
906+
);
907+
836908
const [maxAttemptsValue, setMaxAttemptsValue] = useState<number | undefined>(
837909
lastRun?.maxAttempts
838910
);
@@ -843,6 +915,12 @@ function ScheduledTaskForm({
843915

844916
const [showTemplateCreatedSuccessMessage, setShowTemplateCreatedSuccessMessage] = useState(false);
845917

918+
const regionItems = regions.map((r) => ({
919+
value: r.name,
920+
label: r.description ? `${r.name}${r.description}` : r.name,
921+
isDefault: r.isDefault,
922+
}));
923+
846924
const queueItems = queues.map((q) => ({
847925
value: q.type === "task" ? `task/${q.name}` : q.name,
848926
label: q.name,
@@ -879,6 +957,7 @@ function ScheduledTaskForm({
879957
tags,
880958
version,
881959
machine,
960+
region,
882961
prioritySeconds,
883962
},
884963
] = useForm({
@@ -1101,6 +1180,39 @@ function ScheduledTaskForm({
11011180
)}
11021181
<FormError id={version.errorId}>{version.error}</FormError>
11031182
</InputGroup>
1183+
{regionItems.length > 1 && (
1184+
<InputGroup>
1185+
<Label htmlFor={region.id} variant="small">
1186+
Region
1187+
</Label>
1188+
<Select
1189+
{...conform.select(region)}
1190+
variant="tertiary/small"
1191+
dropdownIcon
1192+
items={regionItems}
1193+
defaultValue={defaultRegion?.name}
1194+
value={regionValue}
1195+
setValue={(e) => {
1196+
if (Array.isArray(e)) return;
1197+
setRegionValue(e);
1198+
}}
1199+
disabled={isDev}
1200+
>
1201+
{regionItems.map((r) => (
1202+
<SelectItem key={r.value} value={r.value}>
1203+
{r.label}
1204+
{r.isDefault ? " (default)" : ""}
1205+
</SelectItem>
1206+
))}
1207+
</Select>
1208+
{isDev ? (
1209+
<Hint>Region is not available in the development environment.</Hint>
1210+
) : (
1211+
<Hint>Overrides the region for this run.</Hint>
1212+
)}
1213+
<FormError id={region.errorId}>{region.error}</FormError>
1214+
</InputGroup>
1215+
)}
11041216
<InputGroup>
11051217
<Label htmlFor={queue.id} variant="small">
11061218
Queue

0 commit comments

Comments
 (0)