| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 |
- import { useFlag, useParams } from 'common'
- import { UseFormReturn } from 'react-hook-form'
- import type { CloudProvider } from 'shared-data'
- import {
- Badge,
- cn,
- FormField,
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectLabel,
- SelectSeparator,
- SelectTrigger,
- SelectValue,
- Tooltip,
- TooltipContent,
- TooltipTrigger,
- } from 'ui'
- import { Admonition } from 'ui-patterns/admonition'
- import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
- import { CreateProjectForm } from './ProjectCreation.schema'
- import { getAvailableRegions } from './ProjectCreation.utils'
- import AlertError from '@/components/ui/AlertError'
- import { InlineLink } from '@/components/ui/InlineLink'
- import Panel from '@/components/ui/Panel'
- 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'
- import { BASE_PATH, PROVIDERS } from '@/lib/constants'
- interface RegionSelectorProps {
- form: UseFormReturn<CreateProjectForm>
- instanceSize?: DesiredInstanceSize
- layout?: 'vertical' | 'horizontal'
- }
- // [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
- }
- export const RegionSelector = ({
- form,
- instanceSize,
- layout = 'horizontal',
- }: RegionSelectorProps) => {
- const { slug } = useParams()
- const cloudProvider = form.getValues('cloudProvider') as CloudProvider
- const smartRegionEnabled = useFlag('enableSmartRegion')
- const { data: statusData } = useIncidentStatusQuery()
- const { incidents = [] } = statusData ?? {}
- const { isPending: isLoadingDefaultRegion } = useDefaultRegionQuery(
- { cloudProvider },
- { enabled: !smartRegionEnabled }
- )
- const {
- data: availableRegionsData,
- isPending: isLoadingAvailableRegions,
- isError: isErrorAvailableRegions,
- error: errorAvailableRegions,
- } = useOrganizationAvailableRegionsQuery(
- { slug, cloudProvider, desiredInstanceSize: instanceSize },
- { enabled: smartRegionEnabled, staleTime: 1000 * 60 * 5 } // 5 minutes
- )
- const smartRegions = availableRegionsData?.all.smartGroup ?? []
- const allRegions = availableRegionsData?.all.specific ?? []
- const recommendedSmartRegions = new Set(
- [availableRegionsData?.recommendations.smartGroup.code].filter(Boolean)
- )
- const recommendedSpecificRegions = new Set(
- availableRegionsData?.recommendations.specific.map((region) => region.code)
- )
- const availableRegions = getAvailableRegions(PROVIDERS[cloudProvider].id)
- const regionsArray = Object.entries(availableRegions).map(([_key, value]) => {
- return {
- code: value.code,
- name: value.displayName,
- provider: cloudProvider,
- status: undefined,
- }
- })
- const regionOptions = smartRegionEnabled ? allRegions : regionsArray
- const isLoading = smartRegionEnabled ? isLoadingAvailableRegions : isLoadingDefaultRegion
- const showNonProdFields =
- process.env.NEXT_PUBLIC_ENVIRONMENT === 'local' ||
- process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
- const allSelectableRegions = [...smartRegions, ...regionOptions]
- 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 === field.value
- })
- 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
- layout={layout}
- label="Region"
- description={
- <>
- <p>Select the region closest to your users for the best performance.</p>
- {showNonProdFields && (
- <div className="mt-2 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>
- </div>
- )}
- </>
- }
- >
- <Select value={field.value} onValueChange={field.onChange} disabled={isLoading}>
- <SelectTrigger 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..'
- }
- >
- {field.value !== undefined && (
- <div className="flex items-center gap-x-3">
- {selectedRegion?.code && (
- <img
- alt="region icon"
- className="w-5 rounded-xs"
- src={`${BASE_PATH}/img/regions/${selectedRegion.code}.svg`}
- />
- )}
- <span className="text-foreground">
- {selectedRegion?.name
- ? getDisplayNameForSmartRegion(selectedRegion.name)
- : field.value}
- </span>
- </div>
- )}
- </SelectValue>
- </SelectTrigger>
- <SelectContent>
- {smartRegionEnabled && (
- <>
- <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">
- <img
- alt="region icon"
- className="w-5 rounded-xs"
- src={`${BASE_PATH}/img/regions/${value.code}.svg`}
- />
- <span className="text-foreground">
- {getDisplayNameForSmartRegion(value.name)}
- </span>
- </div>
- <div>
- {recommendedSmartRegions.has(value.code) && (
- <Badge variant="success" className="mr-1">
- Recommended
- </Badge>
- )}
- </div>
- </div>
- </SelectItem>
- )
- })}
- </SelectGroup>
- <SelectSeparator />
- </>
- )}
- <SelectGroup>
- <SelectLabel>Specific regions</SelectLabel>
- {regionOptions.map((value) => {
- return (
- <SelectItem
- key={value.code}
- value={value.name}
- className={cn(
- 'w-full [&>:nth-child(2)]:w-full',
- value.status !== undefined && 'pointer-events-auto!'
- )}
- disabled={value.status !== undefined}
- >
- <div className="flex flex-row items-center justify-between w-full gap-x-2">
- <div className="flex items-center gap-x-3">
- <img
- alt="region icon"
- className="w-5 rounded-xs"
- src={`${BASE_PATH}/img/regions/${value.code}.svg`}
- />
- <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>
- {recommendedSpecificRegions.has(value.code) && (
- <Badge variant="success" className="mr-1">
- Recommended
- </Badge>
- )}
- {value.status !== undefined && value.status === 'capacity' && (
- <Tooltip>
- <TooltipTrigger>
- <Badge variant="warning" className="mr-1">
- Unavailable
- </Badge>
- </TooltipTrigger>
- <TooltipContent>
- Temporarily unavailable due to this region being at capacity.
- </TooltipContent>
- </Tooltip>
- )}
- </div>
- </SelectItem>
- )
- })}
- </SelectGroup>
- </SelectContent>
- </Select>
- </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>
- )}
- </>
- )
- }}
- />
- </Panel.Content>
- )
- }
|