RegionSelector.tsx315 lines · main
1import { useFlag, useParams } from 'common'
2import { UseFormReturn } from 'react-hook-form'
3import type { CloudProvider } from 'shared-data'
4import {
5 Badge,
6 cn,
7 FormField,
8 Select,
9 SelectContent,
10 SelectGroup,
11 SelectItem,
12 SelectLabel,
13 SelectSeparator,
14 SelectTrigger,
15 SelectValue,
16 Tooltip,
17 TooltipContent,
18 TooltipTrigger,
19} from 'ui'
20import { Admonition } from 'ui-patterns/admonition'
21import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
22
23import { CreateProjectForm } from './ProjectCreation.schema'
24import { getAvailableRegions } from './ProjectCreation.utils'
25import AlertError from '@/components/ui/AlertError'
26import { InlineLink } from '@/components/ui/InlineLink'
27import Panel from '@/components/ui/Panel'
28import { useDefaultRegionQuery } from '@/data/misc/get-default-region-query'
29import { useOrganizationAvailableRegionsQuery } from '@/data/organizations/organization-available-regions-query'
30import { useIncidentStatusQuery } from '@/data/platform/incident-status-query'
31import type { DesiredInstanceSize } from '@/data/projects/new-project.constants'
32import { BASE_PATH, PROVIDERS } from '@/lib/constants'
33
34interface RegionSelectorProps {
35 form: UseFormReturn<CreateProjectForm>
36 instanceSize?: DesiredInstanceSize
37 layout?: 'vertical' | 'horizontal'
38}
39
40// [Joshen] Let's use a library to maintain the flag SVGs in the future
41// I tried using https://flagpack.xyz/docs/development/react/ but couldn't get it to render
42// ^ can try again next time
43
44// Maps smart region group codes to the specific-region code prefixes they contain.
45// Used to check whether an incident affecting specific regions also affects a smart region selection.
46const SMART_REGION_PREFIXES: Record<string, Array<string>> = {
47 americas: ['us-', 'ca-', 'sa-'],
48 emea: ['eu-', 'me-', 'af-'],
49 apac: ['ap-'],
50}
51
52function smartRegionMatchesSpecific(smartCode: string, specificCode: string): boolean {
53 return (SMART_REGION_PREFIXES[smartCode] ?? []).some((prefix) => specificCode.startsWith(prefix))
54}
55
56// Map backend region names to user-friendly display names
57const getDisplayNameForSmartRegion = (name: string): string => {
58 if (name === 'APAC') {
59 return 'Asia-Pacific'
60 }
61 return name
62}
63
64export const RegionSelector = ({
65 form,
66 instanceSize,
67 layout = 'horizontal',
68}: RegionSelectorProps) => {
69 const { slug } = useParams()
70 const cloudProvider = form.getValues('cloudProvider') as CloudProvider
71
72 const smartRegionEnabled = useFlag('enableSmartRegion')
73
74 const { data: statusData } = useIncidentStatusQuery()
75 const { incidents = [] } = statusData ?? {}
76
77 const { isPending: isLoadingDefaultRegion } = useDefaultRegionQuery(
78 { cloudProvider },
79 { enabled: !smartRegionEnabled }
80 )
81
82 const {
83 data: availableRegionsData,
84 isPending: isLoadingAvailableRegions,
85 isError: isErrorAvailableRegions,
86 error: errorAvailableRegions,
87 } = useOrganizationAvailableRegionsQuery(
88 { slug, cloudProvider, desiredInstanceSize: instanceSize },
89 { enabled: smartRegionEnabled, staleTime: 1000 * 60 * 5 } // 5 minutes
90 )
91
92 const smartRegions = availableRegionsData?.all.smartGroup ?? []
93 const allRegions = availableRegionsData?.all.specific ?? []
94
95 const recommendedSmartRegions = new Set(
96 [availableRegionsData?.recommendations.smartGroup.code].filter(Boolean)
97 )
98 const recommendedSpecificRegions = new Set(
99 availableRegionsData?.recommendations.specific.map((region) => region.code)
100 )
101
102 const availableRegions = getAvailableRegions(PROVIDERS[cloudProvider].id)
103 const regionsArray = Object.entries(availableRegions).map(([_key, value]) => {
104 return {
105 code: value.code,
106 name: value.displayName,
107 provider: cloudProvider,
108 status: undefined,
109 }
110 })
111
112 const regionOptions = smartRegionEnabled ? allRegions : regionsArray
113 const isLoading = smartRegionEnabled ? isLoadingAvailableRegions : isLoadingDefaultRegion
114
115 const showNonProdFields =
116 process.env.NEXT_PUBLIC_ENVIRONMENT === 'local' ||
117 process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
118
119 const allSelectableRegions = [...smartRegions, ...regionOptions]
120
121 if (isErrorAvailableRegions) {
122 return <AlertError subject="Error loading available regions" error={errorAvailableRegions} />
123 }
124
125 return (
126 <Panel.Content>
127 <FormField
128 control={form.control}
129 name="dbRegion"
130 render={({ field }) => {
131 const selectedRegion = allSelectableRegions.find((region) => {
132 return !!region.name && region.name === field.value
133 })
134
135 const affectingIncidents = incidents.filter((incident) => {
136 const affectedRegions = incident.cache?.affected_regions ?? []
137 if (affectedRegions.length === 0 || selectedRegion?.code === undefined) return false
138
139 // Specific region: direct code match
140 if (affectedRegions.includes(selectedRegion.code)) return true
141
142 // Smart region: match if any affected region falls within the smart group
143 return affectedRegions.some((specificCode) =>
144 smartRegionMatchesSpecific(selectedRegion.code, specificCode)
145 )
146 })
147
148 return (
149 <>
150 <FormItemLayout
151 layout={layout}
152 label="Region"
153 description={
154 <>
155 <p>Select the region closest to your users for the best performance.</p>
156 {showNonProdFields && (
157 <div className="mt-2 text-warning">
158 <p>Only these regions are supported for local/staging projects:</p>
159 <ul className="list-disc list-inside mt-1">
160 <li>East US (North Virginia)</li>
161 <li>Central EU (Frankfurt)</li>
162 <li>Southeast Asia (Singapore)</li>
163 </ul>
164 </div>
165 )}
166 </>
167 }
168 >
169 <Select value={field.value} onValueChange={field.onChange} disabled={isLoading}>
170 <SelectTrigger className="[&>:nth-child(1)]:w-full [&>:nth-child(1)]:flex [&>:nth-child(1)]:items-start">
171 <SelectValue
172 placeholder={
173 isLoading
174 ? 'Loading available regions...'
175 : 'Select a region for your project..'
176 }
177 >
178 {field.value !== undefined && (
179 <div className="flex items-center gap-x-3">
180 {selectedRegion?.code && (
181 <img
182 alt="region icon"
183 className="w-5 rounded-xs"
184 src={`${BASE_PATH}/img/regions/${selectedRegion.code}.svg`}
185 />
186 )}
187 <span className="text-foreground">
188 {selectedRegion?.name
189 ? getDisplayNameForSmartRegion(selectedRegion.name)
190 : field.value}
191 </span>
192 </div>
193 )}
194 </SelectValue>
195 </SelectTrigger>
196 <SelectContent>
197 {smartRegionEnabled && (
198 <>
199 <SelectGroup>
200 <SelectLabel>General regions</SelectLabel>
201 {smartRegions.map((value) => {
202 return (
203 <SelectItem
204 key={value.code}
205 value={value.name}
206 className="w-full [&>:nth-child(2)]:w-full"
207 >
208 <div className="flex flex-row items-center justify-between w-full">
209 <div className="flex items-center gap-x-3">
210 <img
211 alt="region icon"
212 className="w-5 rounded-xs"
213 src={`${BASE_PATH}/img/regions/${value.code}.svg`}
214 />
215 <span className="text-foreground">
216 {getDisplayNameForSmartRegion(value.name)}
217 </span>
218 </div>
219
220 <div>
221 {recommendedSmartRegions.has(value.code) && (
222 <Badge variant="success" className="mr-1">
223 Recommended
224 </Badge>
225 )}
226 </div>
227 </div>
228 </SelectItem>
229 )
230 })}
231 </SelectGroup>
232 <SelectSeparator />
233 </>
234 )}
235
236 <SelectGroup>
237 <SelectLabel>Specific regions</SelectLabel>
238 {regionOptions.map((value) => {
239 return (
240 <SelectItem
241 key={value.code}
242 value={value.name}
243 className={cn(
244 'w-full [&>:nth-child(2)]:w-full',
245 value.status !== undefined && 'pointer-events-auto!'
246 )}
247 disabled={value.status !== undefined}
248 >
249 <div className="flex flex-row items-center justify-between w-full gap-x-2">
250 <div className="flex items-center gap-x-3">
251 <img
252 alt="region icon"
253 className="w-5 rounded-xs"
254 src={`${BASE_PATH}/img/regions/${value.code}.svg`}
255 />
256 <div className="flex items-center gap-x-2">
257 <span className="text-foreground">{value.name}</span>
258 <span className="text-xs text-foreground-lighter font-mono">
259 {value.code}
260 </span>
261 </div>
262 </div>
263
264 {recommendedSpecificRegions.has(value.code) && (
265 <Badge variant="success" className="mr-1">
266 Recommended
267 </Badge>
268 )}
269 {value.status !== undefined && value.status === 'capacity' && (
270 <Tooltip>
271 <TooltipTrigger>
272 <Badge variant="warning" className="mr-1">
273 Unavailable
274 </Badge>
275 </TooltipTrigger>
276 <TooltipContent>
277 Temporarily unavailable due to this region being at capacity.
278 </TooltipContent>
279 </Tooltip>
280 )}
281 </div>
282 </SelectItem>
283 )
284 })}
285 </SelectGroup>
286 </SelectContent>
287 </Select>
288 </FormItemLayout>
289
290 {affectingIncidents.length > 0 && (
291 <FormItemLayout layout="horizontal">
292 <Admonition
293 type="warning"
294 title="Incident in progress for this region"
295 description={
296 <>
297 We're currently investigating an issue that may impact projects in this
298 region. Follow updates on{' '}
299 <InlineLink href="https://status.supabase.com">
300 status.supabase.com
301 </InlineLink>
302 .
303 </>
304 }
305 className="mt-3"
306 />
307 </FormItemLayout>
308 )}
309 </>
310 )
311 }}
312 />
313 </Panel.Content>
314 )
315}