Skip to content

Commit 59b6eb9

Browse files
authored
feat(webapp): add region selector to test and replay task (#3082)
Adds a region selector to the Test task page and Replay run dialog, so users can override the region from the dashboard. Disabled with a placeholder for dev environments. Closes #3016
1 parent b793f33 commit 59b6eb9

File tree

6 files changed

+184
-19
lines changed

6 files changed

+184
-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: 133 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,45 @@ 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+
{/* Our Select primitive uses Ariakit under the hood, which treats
1189+
value={undefined} as uncontrolled, keeping stale internal state when
1190+
switching environments. The key forces a remount so it reinitializes
1191+
with the correct defaultValue. */}
1192+
<Select
1193+
key={`region-${environment.id}`}
1194+
{...conform.select(region)}
1195+
variant="tertiary/small"
1196+
placeholder={isDev ? "–" : undefined}
1197+
dropdownIcon
1198+
items={regionItems}
1199+
defaultValue={isDev ? undefined : defaultRegion?.name}
1200+
value={isDev ? undefined : regionValue}
1201+
setValue={isDev ? undefined : (e) => {
1202+
if (Array.isArray(e)) return;
1203+
setRegionValue(e);
1204+
}}
1205+
disabled={isDev}
1206+
>
1207+
{regionItems.map((r) => (
1208+
<SelectItem key={r.value} value={r.value}>
1209+
{r.label}
1210+
{r.isDefault ? " (default)" : ""}
1211+
</SelectItem>
1212+
))}
1213+
</Select>
1214+
{isDev ? (
1215+
<Hint>Region is not available in the development environment.</Hint>
1216+
) : (
1217+
<Hint>Overrides the region for this run.</Hint>
1218+
)}
1219+
<FormError id={region.errorId}>{region.error}</FormError>
1220+
</InputGroup>
1221+
)}
11041222
<InputGroup>
11051223
<Label htmlFor={queue.id} variant="small">
11061224
Queue

0 commit comments

Comments
 (0)