mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 10:25:06 +03:00
The region selector's "Best available region" option now reads "Auto" and carries the Recommended badge. Also removes the "Select the region closest to your users for the best performance." description, which was misleading for the auto selection. Fixes FE-4468 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Updates** * The recommended region is now labeled “Auto” in both the selected value and the options list; its “Recommended” badge remains. * Removed general advice to choose a region closest to users. High-availability and local/staging notices are unchanged. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
482 lines
20 KiB
TypeScript
482 lines
20 KiB
TypeScript
import { useFeatureFlags, useParams } from 'common'
|
|
import { Loader2, ThumbsUp } from 'lucide-react'
|
|
import { useEffect, useRef } from 'react'
|
|
import { UseFormReturn } from 'react-hook-form'
|
|
import type { CloudProvider } from 'shared-data'
|
|
import {
|
|
Badge,
|
|
cn,
|
|
FormControl,
|
|
FormField,
|
|
Select,
|
|
SelectContent,
|
|
SelectGroup,
|
|
SelectItem,
|
|
SelectLabel,
|
|
SelectSeparator,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
useWatch,
|
|
} from 'ui'
|
|
import { Admonition } from 'ui-patterns/Admonition'
|
|
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
|
|
|
import { CreateProjectForm } from './ProjectCreation.schema'
|
|
import {
|
|
filterHighAvailabilityRegions,
|
|
getAvailableRegions,
|
|
getHighAvailabilityRegionCode,
|
|
} from './ProjectCreation.utils'
|
|
import { getRegionRestrictionCopy, SELECT_DIFFERENT_REGION } from './RegionSelector.utils'
|
|
import { useRegionRestriction } from './useRegionRestriction'
|
|
import { AlertError } from '@/components/ui/AlertError'
|
|
import { InlineLink } from '@/components/ui/InlineLink'
|
|
import Panel from '@/components/ui/Panel'
|
|
import { RegionFlag } from '@/components/ui/RegionFlag'
|
|
import { useDefaultRegionQuery } from '@/data/misc/get-default-region-query'
|
|
import { useOrganizationAvailableRegionsQuery } from '@/data/organizations/organization-available-regions-query'
|
|
import { useIncidentStatusQuery } from '@/data/platform/incident-status-query'
|
|
import type { DesiredInstanceSize } from '@/data/projects/new-project.constants'
|
|
|
|
interface RegionSelectorProps {
|
|
form: UseFormReturn<CreateProjectForm>
|
|
instanceSize?: DesiredInstanceSize
|
|
layout?: 'vertical' | 'horizontal'
|
|
showBestAvailableRegionOption: boolean
|
|
isBestAvailableSelected: boolean
|
|
onBestAvailableSelectedChange: (value: boolean) => void
|
|
}
|
|
|
|
// [Joshen] Let's use a library to maintain the flag SVGs in the future
|
|
// I tried using https://flagpack.xyz/docs/development/react/ but couldn't get it to render
|
|
// ^ can try again next time
|
|
|
|
// Maps smart region group codes to the specific-region code prefixes they contain.
|
|
// Used to check whether an incident affecting specific regions also affects a smart region selection.
|
|
const SMART_REGION_PREFIXES: Record<string, Array<string>> = {
|
|
americas: ['us-', 'ca-', 'sa-'],
|
|
emea: ['eu-', 'me-', 'af-'],
|
|
apac: ['ap-'],
|
|
}
|
|
|
|
function smartRegionMatchesSpecific(smartCode: string, specificCode: string): boolean {
|
|
return (SMART_REGION_PREFIXES[smartCode] ?? []).some((prefix) => specificCode.startsWith(prefix))
|
|
}
|
|
|
|
// Map backend region names to user-friendly display names
|
|
const getDisplayNameForSmartRegion = (name: string): string => {
|
|
if (name === 'APAC') {
|
|
return 'Asia-Pacific'
|
|
}
|
|
return name
|
|
}
|
|
|
|
const isLocal = process.env.NEXT_PUBLIC_ENVIRONMENT === 'local'
|
|
|
|
const BestAvailableRegionIcon = () => (
|
|
<div className="w-5 border border-brand-500 h-4.5 rounded-[3px] bg-brand-300 flex items-center justify-center">
|
|
<ThumbsUp size={10} className="text-brand" />
|
|
</div>
|
|
)
|
|
|
|
export const RegionSelector = ({
|
|
form,
|
|
instanceSize,
|
|
layout = 'horizontal',
|
|
showBestAvailableRegionOption,
|
|
isBestAvailableSelected,
|
|
onBestAvailableSelectedChange,
|
|
}: RegionSelectorProps) => {
|
|
const { slug } = useParams()
|
|
const cloudProvider = form.getValues('cloudProvider') as CloudProvider
|
|
const highAvailability = useWatch({ control: form.control, name: 'highAvailability' })
|
|
const dbRegion = useWatch({ control: form.control, name: 'dbRegion' })
|
|
const highAvailabilityRegionCode = getHighAvailabilityRegionCode()
|
|
|
|
const { hasLoaded: flagsLoaded } = useFeatureFlags()
|
|
const smartRegionEnabled = cloudProvider !== 'AWS_NIMBUS'
|
|
|
|
const { getRegionRestriction } = useRegionRestriction()
|
|
|
|
const { data: statusData } = useIncidentStatusQuery()
|
|
const { incidents = [] } = statusData ?? {}
|
|
|
|
const { isPending: isLoadingDefaultRegion } = useDefaultRegionQuery(
|
|
{ cloudProvider },
|
|
{ enabled: flagsLoaded && !smartRegionEnabled }
|
|
)
|
|
|
|
const {
|
|
data: availableRegionsData,
|
|
isPending: isLoadingAvailableRegions,
|
|
isError: isErrorAvailableRegions,
|
|
error: errorAvailableRegions,
|
|
} = useOrganizationAvailableRegionsQuery(
|
|
{ slug, cloudProvider, desiredInstanceSize: instanceSize },
|
|
{ enabled: smartRegionEnabled, staleTime: 1000 * 60 * 5 } // 5 minutes
|
|
)
|
|
|
|
const allSmartRegions = availableRegionsData?.all.smartGroup ?? []
|
|
const allRegions = availableRegionsData?.all.specific ?? []
|
|
const restrictHighAvailabilityRegion =
|
|
highAvailability && !isLocal && highAvailabilityRegionCode !== undefined
|
|
const smartRegions = highAvailability ? [] : allSmartRegions
|
|
|
|
const recommendedSmartRegions = new Set(
|
|
[availableRegionsData?.recommendations.smartGroup.code].filter(Boolean)
|
|
)
|
|
const recommendedSmartRegion = smartRegions.find(
|
|
(x) => x.code === recommendedSmartRegions.values().next().value
|
|
)
|
|
|
|
const isBestAvailableOptionShown = showBestAvailableRegionOption && !!recommendedSmartRegion
|
|
const isBestAvailableActive = isBestAvailableSelected && isBestAvailableOptionShown
|
|
|
|
// Defaults free orgs to the "Best available region" shortcut once the recommendation loads,
|
|
// unless the user has already interacted with the region field themselves.
|
|
const hasUserSelectedRegionRef = useRef(false)
|
|
useEffect(() => {
|
|
if (hasUserSelectedRegionRef.current) return
|
|
if (isBestAvailableOptionShown) {
|
|
onBestAvailableSelectedChange(true)
|
|
}
|
|
}, [isBestAvailableOptionShown, onBestAvailableSelectedChange])
|
|
|
|
// Keeps dbRegion following the recommendation while "Best available" is active, so a
|
|
// recommendation change (e.g. after a refetch) doesn't leave the form submitting a stale
|
|
// region behind a label that still reads "Auto".
|
|
useEffect(() => {
|
|
if (!isBestAvailableSelected || !recommendedSmartRegion) return
|
|
if (dbRegion !== recommendedSmartRegion.name) {
|
|
form.setValue('dbRegion', recommendedSmartRegion.name)
|
|
}
|
|
}, [isBestAvailableSelected, recommendedSmartRegion, dbRegion, form])
|
|
|
|
const recommendedSpecificRegions = new Set(
|
|
availableRegionsData?.recommendations.specific.map((region) => region.code)
|
|
)
|
|
|
|
const availableRegions = getAvailableRegions(cloudProvider)
|
|
const regionsArray = Object.entries(availableRegions).map(([_key, value]) => {
|
|
return {
|
|
code: value.code,
|
|
name: value.displayName,
|
|
provider: cloudProvider,
|
|
status: undefined,
|
|
}
|
|
})
|
|
|
|
const unfilteredRegionOptions = smartRegionEnabled ? allRegions : regionsArray
|
|
const regionOptions = filterHighAvailabilityRegions(
|
|
[...unfilteredRegionOptions],
|
|
highAvailability
|
|
)
|
|
const regionOptionsWithRestriction = regionOptions.map((region) => ({
|
|
...region,
|
|
restriction: getRegionRestriction(region),
|
|
}))
|
|
const isLoading = smartRegionEnabled ? isLoadingAvailableRegions : isLoadingDefaultRegion
|
|
|
|
const isLocalEnvironment = process.env.NEXT_PUBLIC_ENVIRONMENT === 'local'
|
|
const showNonProdFields = isLocalEnvironment || process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
|
|
|
|
const allSelectableRegions = [...smartRegions, ...regionOptions]
|
|
|
|
// react-hook-form intermittently drops this field's value when its Controller
|
|
// remounts (e.g. a sibling section mounting/unmounting in the same update, such as
|
|
// toggling high availability), so a one-shot effect isn't enough. Instead this effect
|
|
// re-asserts off the watched value: a region present in the current list is kept (and
|
|
// remembered in lastValidRegionRef), and when it's missing or cleared out from under us
|
|
// it restores the last valid region. allSelectableRegions is intentionally omitted from
|
|
// deps — it's a new array every render, and comparing it by reference would defeat the
|
|
// point of reacting to genuine content changes on every render where they occur.
|
|
const lastValidRegionRef = useRef<string | undefined>(undefined)
|
|
useEffect(() => {
|
|
if (allSelectableRegions.length === 0) return
|
|
const isRegionAvailable = (name: string | undefined) =>
|
|
!!name && allSelectableRegions.some((region) => region.name === name)
|
|
|
|
if (isRegionAvailable(dbRegion)) {
|
|
lastValidRegionRef.current = dbRegion
|
|
return
|
|
}
|
|
|
|
const lastValidRegion = lastValidRegionRef.current
|
|
if (lastValidRegion !== undefined && isRegionAvailable(lastValidRegion)) {
|
|
form.setValue('dbRegion', lastValidRegion)
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [dbRegion, form])
|
|
|
|
if (isErrorAvailableRegions) {
|
|
return <AlertError subject="Error loading available regions" error={errorAvailableRegions} />
|
|
}
|
|
|
|
return (
|
|
<Panel.Content>
|
|
<FormField
|
|
control={form.control}
|
|
name="dbRegion"
|
|
render={({ field }) => {
|
|
const selectedRegion = allSelectableRegions.find((region) => {
|
|
return !!region.name && region.name === dbRegion
|
|
})
|
|
|
|
const selectedRegionLabel = selectedRegion?.name
|
|
? getDisplayNameForSmartRegion(selectedRegion.name)
|
|
: dbRegion
|
|
const selectedRestriction = regionOptionsWithRestriction.find(
|
|
(region) => region.name === dbRegion
|
|
)?.restriction
|
|
const selectedRestrictionCopy =
|
|
selectedRestriction !== undefined
|
|
? getRegionRestrictionCopy(selectedRestriction)
|
|
: undefined
|
|
const triggerLabel = isLoading
|
|
? 'Loading available regions...'
|
|
: isBestAvailableActive
|
|
? 'Auto'
|
|
: selectedRegionLabel
|
|
|
|
const affectingIncidents = incidents.filter((incident) => {
|
|
const affectedRegions = incident.cache?.affected_regions ?? []
|
|
if (affectedRegions.length === 0 || selectedRegion?.code === undefined) return false
|
|
|
|
// Specific region: direct code match
|
|
if (affectedRegions.includes(selectedRegion.code)) return true
|
|
|
|
// Smart region: match if any affected region falls within the smart group
|
|
return affectedRegions.some((specificCode) =>
|
|
smartRegionMatchesSpecific(selectedRegion.code, specificCode)
|
|
)
|
|
})
|
|
|
|
return (
|
|
<>
|
|
<FormItemLayout
|
|
id="region"
|
|
layout={layout}
|
|
label="Region"
|
|
description={
|
|
restrictHighAvailabilityRegion ? (
|
|
<div className="text-warning">
|
|
High Availability projects are currently limited to{' '}
|
|
{regionOptions[0]?.name ?? highAvailabilityRegionCode}.
|
|
</div>
|
|
) : showNonProdFields ? (
|
|
<div className="text-warning">
|
|
<p>Only these regions are supported for local/staging projects:</p>
|
|
<ul className="list-disc list-inside mt-1">
|
|
<li>East US (North Virginia)</li>
|
|
<li>Central EU (Frankfurt)</li>
|
|
<li>Southeast Asia (Singapore)</li>
|
|
</ul>
|
|
{isLocalEnvironment && (
|
|
<p className="mt-1">
|
|
Use Central EU (Frankfurt) unless you're on a personal dev stack.
|
|
</p>
|
|
)}
|
|
</div>
|
|
) : undefined
|
|
}
|
|
>
|
|
<FormControl>
|
|
<Select
|
|
value={isBestAvailableActive ? 'best_available' : dbRegion}
|
|
onValueChange={(value) => {
|
|
if (value === '') return
|
|
hasUserSelectedRegionRef.current = true
|
|
if (value === 'best_available') {
|
|
if (!recommendedSmartRegion) return
|
|
onBestAvailableSelectedChange(true)
|
|
field.onChange(recommendedSmartRegion.name)
|
|
} else {
|
|
onBestAvailableSelectedChange(false)
|
|
field.onChange(value)
|
|
}
|
|
}}
|
|
disabled={isLoading}
|
|
>
|
|
<SelectTrigger
|
|
ref={field.ref}
|
|
id="region"
|
|
className="[&>:nth-child(1)]:w-full [&>:nth-child(1)]:flex [&>:nth-child(1)]:items-start"
|
|
>
|
|
<SelectValue
|
|
placeholder={
|
|
isLoading
|
|
? 'Loading available regions...'
|
|
: 'Select a region for your project..'
|
|
}
|
|
>
|
|
{dbRegion !== undefined && (
|
|
<div className="flex items-center gap-x-3">
|
|
{isLoading && <Loader2 size={14} className="animate-spin" />}
|
|
{isBestAvailableActive ? (
|
|
<BestAvailableRegionIcon />
|
|
) : (
|
|
selectedRegion?.code && (
|
|
<RegionFlag className="w-5" region={selectedRegion.code} />
|
|
)
|
|
)}
|
|
<span className="text-foreground">{triggerLabel}</span>
|
|
</div>
|
|
)}
|
|
</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{isBestAvailableOptionShown && (
|
|
<>
|
|
<SelectGroup>
|
|
<SelectLabel>Recommendation</SelectLabel>
|
|
<SelectItem
|
|
value="best_available"
|
|
className="w-full [&>:nth-child(2)]:w-full"
|
|
>
|
|
<div className="flex flex-row items-center justify-between w-full">
|
|
<div className="flex items-center gap-x-3">
|
|
<BestAvailableRegionIcon />
|
|
<span className="text-foreground">Auto</span>
|
|
</div>
|
|
<Badge variant="success" className="mr-1">
|
|
Recommended
|
|
</Badge>
|
|
</div>
|
|
</SelectItem>
|
|
</SelectGroup>
|
|
<SelectSeparator />
|
|
</>
|
|
)}
|
|
{smartRegionEnabled && !highAvailability && (
|
|
<>
|
|
<SelectGroup>
|
|
<SelectLabel>General regions</SelectLabel>
|
|
{smartRegions.map((value) => {
|
|
return (
|
|
<SelectItem
|
|
key={value.code}
|
|
value={value.name}
|
|
className="w-full [&>:nth-child(2)]:w-full"
|
|
>
|
|
<div className="flex flex-row items-center justify-between w-full">
|
|
<div className="flex items-center gap-x-3">
|
|
<RegionFlag className="w-5" region={value.code} />
|
|
<span className="text-foreground">
|
|
{getDisplayNameForSmartRegion(value.name)}
|
|
</span>
|
|
</div>
|
|
|
|
<div>
|
|
{!isBestAvailableOptionShown &&
|
|
recommendedSmartRegions.has(value.code) && (
|
|
<Badge variant="success" className="mr-1">
|
|
Recommended
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</SelectItem>
|
|
)
|
|
})}
|
|
</SelectGroup>
|
|
<SelectSeparator />
|
|
</>
|
|
)}
|
|
|
|
<SelectGroup>
|
|
<SelectLabel>
|
|
{highAvailability ? 'High Availability Regions' : 'Specific regions'}
|
|
</SelectLabel>
|
|
{regionOptionsWithRestriction.map((value) => {
|
|
const restrictionCopy =
|
|
value.restriction !== undefined
|
|
? getRegionRestrictionCopy(value.restriction)
|
|
: undefined
|
|
return (
|
|
<SelectItem
|
|
key={value.code}
|
|
value={value.name}
|
|
className={cn(
|
|
'w-full [&>:nth-child(2)]:w-full',
|
|
restrictionCopy !== undefined && 'pointer-events-auto!'
|
|
)}
|
|
>
|
|
<div className="flex flex-row items-center justify-between w-full gap-x-2">
|
|
<div className="flex items-center gap-x-3">
|
|
<RegionFlag className="w-5" region={value.code} />
|
|
<div className="flex items-center gap-x-2">
|
|
<span className="text-foreground">{value.name}</span>
|
|
<span className="text-xs text-foreground-lighter font-mono">
|
|
{value.code}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{!isBestAvailableOptionShown &&
|
|
recommendedSpecificRegions.has(value.code) && (
|
|
<Badge variant="success" className="mr-1">
|
|
Recommended
|
|
</Badge>
|
|
)}
|
|
|
|
{restrictionCopy !== undefined && (
|
|
<Tooltip>
|
|
<TooltipTrigger>
|
|
<Badge variant="warning" className="mr-1">
|
|
{restrictionCopy.badge}
|
|
</Badge>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{restrictionCopy.tooltip}</TooltipContent>
|
|
</Tooltip>
|
|
)}
|
|
</div>
|
|
</SelectItem>
|
|
)
|
|
})}
|
|
</SelectGroup>
|
|
</SelectContent>
|
|
</Select>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
|
|
{affectingIncidents.length > 0 && (
|
|
<FormItemLayout layout="horizontal">
|
|
<Admonition
|
|
type="warning"
|
|
title="Incident in progress for this region"
|
|
description={
|
|
<>
|
|
We're currently investigating an issue that may impact projects in this
|
|
region. Follow updates on{' '}
|
|
<InlineLink href="https://status.supabase.com">
|
|
status.supabase.com
|
|
</InlineLink>
|
|
.
|
|
</>
|
|
}
|
|
className="mt-3"
|
|
/>
|
|
</FormItemLayout>
|
|
)}
|
|
|
|
{selectedRestrictionCopy !== undefined && (
|
|
<FormItemLayout layout="horizontal" isReactForm={false}>
|
|
<Admonition
|
|
type="warning"
|
|
title={selectedRestrictionCopy.title}
|
|
description={`${selectedRestrictionCopy.notice} ${SELECT_DIFFERENT_REGION}`}
|
|
className="mt-3"
|
|
/>
|
|
</FormItemLayout>
|
|
)}
|
|
</>
|
|
)
|
|
}}
|
|
/>
|
|
</Panel.Content>
|
|
)
|
|
}
|