Skip to content

Commit 12c60f3

Browse files
committed
Switching regions is working. Some style improvements
1 parent 370b035 commit 12c60f3

File tree

3 files changed

+141
-35
lines changed

3 files changed

+141
-35
lines changed

apps/webapp/app/presenters/v3/RegionsPresenter.server.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,21 @@ export class RegionsPresenter extends BasePresenter {
107107
}
108108
}
109109

110+
// Default first
111+
const sorted = regions.sort((a, b) => {
112+
if (a.isDefault) return -1;
113+
if (b.isDefault) return 1;
114+
return a.name.localeCompare(b.name);
115+
});
116+
117+
// Remove later duplicates
118+
const unique = sorted.filter((region, index, self) => {
119+
const firstIndex = self.findIndex((t) => t.id === region.id);
120+
return index === firstIndex;
121+
});
122+
110123
return {
111-
regions,
124+
regions: unique.sort((a, b) => a.name.localeCompare(b.name)),
112125
};
113126
}
114127
}

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

Lines changed: 97 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { BookOpenIcon } from "@heroicons/react/24/solid";
2-
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
2+
import { Form } from "@remix-run/react";
3+
import { ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime";
4+
import { tryCatch } from "@trigger.dev/core";
35
import { typedjson, useTypedLoaderData } from "remix-typedjson";
46
import { z } from "zod";
57
import { CloudProviderIcon } from "~/assets/icons/CloudProviderIcon";
@@ -10,7 +12,7 @@ import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
1012
import { InlineCode } from "~/components/code/InlineCode";
1113
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
1214
import { Badge } from "~/components/primitives/Badge";
13-
import { LinkButton } from "~/components/primitives/Buttons";
15+
import { Button, LinkButton } from "~/components/primitives/Buttons";
1416
import { ClipboardField } from "~/components/primitives/ClipboardField";
1517
import { CopyableText } from "~/components/primitives/CopyableText";
1618
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
@@ -27,12 +29,21 @@ import {
2729
TableHeaderCell,
2830
TableRow,
2931
} from "~/components/primitives/Table";
32+
import { TextLink } from "~/components/primitives/TextLink";
3033
import { useOrganization } from "~/hooks/useOrganizations";
3134
import { useProject } from "~/hooks/useProject";
35+
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
36+
import { findProjectBySlug } from "~/models/project.server";
3237
import { RegionsPresenter } from "~/presenters/v3/RegionsPresenter.server";
3338
import { logger } from "~/services/logger.server";
3439
import { requireUserId } from "~/services/session.server";
35-
import { docsPath, ProjectParamSchema } from "~/utils/pathBuilder";
40+
import {
41+
docsPath,
42+
EnvironmentParamSchema,
43+
ProjectParamSchema,
44+
regionsPath,
45+
} from "~/utils/pathBuilder";
46+
import { SetDefaultRegionService } from "~/v3/services/setDefaultRegion.server";
3647

3748
export const RegionsOptions = z.object({
3849
search: z.string().optional(),
@@ -43,31 +54,68 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
4354
const userId = await requireUserId(request);
4455
const { projectParam } = ProjectParamSchema.parse(params);
4556

46-
const searchParams = new URL(request.url).searchParams;
47-
const parsedSearchParams = RegionsOptions.safeParse(Object.fromEntries(searchParams));
48-
const options = parsedSearchParams.success ? parsedSearchParams.data : {};
49-
50-
try {
51-
const presenter = new RegionsPresenter();
52-
const result = await presenter.call({
57+
const presenter = new RegionsPresenter();
58+
const [error, result] = await tryCatch(
59+
presenter.call({
5360
userId,
5461
projectSlug: projectParam,
55-
});
62+
})
63+
);
5664

57-
return typedjson(result);
58-
} catch (error) {
59-
logger.error("Error loading regions page", { error });
65+
if (error) {
6066
throw new Response(undefined, {
6167
status: 400,
62-
statusText: "Something went wrong, if this problem persists please contact support.",
68+
statusText: error.message,
6369
});
6470
}
71+
72+
return typedjson(result);
73+
};
74+
75+
const FormSchema = z.object({
76+
regionId: z.string(),
77+
});
78+
79+
export const action = async ({ request, params }: ActionFunctionArgs) => {
80+
const userId = await requireUserId(request);
81+
const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params);
82+
83+
const project = await findProjectBySlug(organizationSlug, projectParam, userId);
84+
85+
const redirectPath = regionsPath(
86+
{ slug: organizationSlug },
87+
{ slug: projectParam },
88+
{ slug: envParam }
89+
);
90+
91+
if (!project) {
92+
throw redirectWithErrorMessage(redirectPath, request, "Project not found");
93+
}
94+
95+
const formData = await request.formData();
96+
const parsedFormData = FormSchema.safeParse(Object.fromEntries(formData));
97+
98+
if (!parsedFormData.success) {
99+
throw redirectWithErrorMessage(redirectPath, request, "No region specified");
100+
}
101+
102+
const service = new SetDefaultRegionService();
103+
const [error, result] = await tryCatch(
104+
service.call({
105+
projectId: project.id,
106+
regionId: parsedFormData.data.regionId,
107+
})
108+
);
109+
110+
if (error) {
111+
return redirectWithErrorMessage(redirectPath, request, error.message);
112+
}
113+
114+
return redirectWithSuccessMessage(redirectPath, request, `Set ${result.name} as default`);
65115
};
66116

67117
export default function Page() {
68118
const { regions } = useTypedLoaderData<typeof loader>();
69-
const organization = useOrganization();
70-
const project = useProject();
71119

72120
return (
73121
<PageContainer>
@@ -112,11 +160,20 @@ export default function Page() {
112160
<TableHeaderCell>Cloud Provider</TableHeaderCell>
113161
<TableHeaderCell>Location</TableHeaderCell>
114162
<TableHeaderCell>Static IPs</TableHeaderCell>
115-
<TableHeaderCell>
116-
<span className="sr-only">Is default?</span>
117-
</TableHeaderCell>
118-
<TableHeaderCell>
119-
<span className="sr-only">Actions</span>
163+
<TableHeaderCell
164+
alignment="right"
165+
tooltip={
166+
<Paragraph variant="small">
167+
When you trigger a run it will execute in your default region, unless
168+
you{" "}
169+
<TextLink to={docsPath("triggering#region")}>
170+
specify a region when triggering
171+
</TextLink>
172+
.
173+
</Paragraph>
174+
}
175+
>
176+
Default region
120177
</TableHeaderCell>
121178
</TableRow>
122179
</TableHeader>
@@ -163,23 +220,29 @@ export default function Page() {
163220
variant={"secondary/small"}
164221
/>
165222
) : (
166-
"–"
167-
)}
168-
</TableCell>
169-
<TableCell>
170-
{region.isDefault ? (
171-
<Badge variant="outline-rounded" className="inline-grid">
172-
Default
173-
</Badge>
174-
) : (
175-
"–"
223+
"Not available"
176224
)}
177225
</TableCell>
178226
<TableCellMenu
179227
className="pl-32"
180228
isSticky
181-
popoverContent={
182-
<PopoverMenuItem to="#" title="View region details" />
229+
visibleButtons={
230+
region.isDefault ? (
231+
<Badge variant="outline-rounded" className="inline-grid">
232+
Default
233+
</Badge>
234+
) : (
235+
<Form method="post">
236+
<Button
237+
variant="secondary/small"
238+
type="submit"
239+
name="regionId"
240+
value={region.id}
241+
>
242+
Set as default...
243+
</Button>
244+
</Form>
245+
)
183246
}
184247
/>
185248
</TableRow>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { BaseService, ServiceValidationError } from "./baseService.server";
2+
3+
export class SetDefaultRegionService extends BaseService {
4+
public async call({ projectId, regionId }: { projectId: string; regionId: string }) {
5+
const workerGroup = await this._prisma.workerInstanceGroup.findFirst({
6+
where: {
7+
id: regionId,
8+
hidden: false,
9+
},
10+
});
11+
12+
if (!workerGroup) {
13+
throw new ServiceValidationError("Region not found or is hidden");
14+
}
15+
16+
await this._prisma.project.update({
17+
where: {
18+
id: projectId,
19+
},
20+
data: {
21+
defaultWorkerGroupId: regionId,
22+
},
23+
});
24+
25+
return {
26+
id: workerGroup.id,
27+
name: workerGroup.name,
28+
};
29+
}
30+
}

0 commit comments

Comments
 (0)