11import { 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" ;
35import { typedjson , useTypedLoaderData } from "remix-typedjson" ;
46import { z } from "zod" ;
57import { CloudProviderIcon } from "~/assets/icons/CloudProviderIcon" ;
@@ -10,7 +12,7 @@ import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
1012import { InlineCode } from "~/components/code/InlineCode" ;
1113import { MainCenteredContainer , PageBody , PageContainer } from "~/components/layout/AppLayout" ;
1214import { Badge } from "~/components/primitives/Badge" ;
13- import { LinkButton } from "~/components/primitives/Buttons" ;
15+ import { Button , LinkButton } from "~/components/primitives/Buttons" ;
1416import { ClipboardField } from "~/components/primitives/ClipboardField" ;
1517import { CopyableText } from "~/components/primitives/CopyableText" ;
1618import { 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" ;
3033import { useOrganization } from "~/hooks/useOrganizations" ;
3134import { useProject } from "~/hooks/useProject" ;
35+ import { redirectWithErrorMessage , redirectWithSuccessMessage } from "~/models/message.server" ;
36+ import { findProjectBySlug } from "~/models/project.server" ;
3237import { RegionsPresenter } from "~/presenters/v3/RegionsPresenter.server" ;
3338import { logger } from "~/services/logger.server" ;
3439import { 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
3748export 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
67117export 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 >
0 commit comments