mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
## Summary - Adds a new project-creation-form incident warning path behind the `incidentIoStatusPage` ConfigCat flag (default off — no visible behavior change until the flag is enabled) - When the flag is on, reads the new `/api/status-page` endpoint (added earlier in this stack) instead of the legacy `/api/incident-status` endpoint, and only fetches the legacy endpoint when the flag is off - Extracts region-matching logic into `RegionSelector.utils.ts` (`regionMatches`, `getItemsAffectingProjectCreation`) with unit test coverage See Linear FE-4057 for full context and design. ## Test plan - [x] `pnpm --filter studio run typecheck` - [x] `pnpm --filter studio run lint:ratchet` - [x] `pnpm knip --workspace apps/studio` - [x] `pnpm test:prettier` - [x] `pnpm --filter studio exec vitest run components/interfaces/ProjectCreation/RegionSelector.utils.test.ts` (35/35 passing) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Project creation displays a warning and links to the status page when an incident or maintenance event may affect the selected region. * Status notices match exact regions and broader smart-region groupings; global project-creation notices are also shown. * The AI assistant can report visible, active incidents and in-progress maintenance, including affected components and update details. When there are no active events, it reports that status. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
481 lines
20 KiB
TypeScript
481 lines
20 KiB
TypeScript
import { useFeatureFlags, useFlag, 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 { ProjectCreationStatusAdmonition } from './ProjectCreationStatusAdmonition'
|
|
import {
|
|
getRegionRestrictionCopy,
|
|
regionMatches,
|
|
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>
|
|
hasSelectedOrganization: boolean
|
|
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
|
|
|
|
// 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,
|
|
hasSelectedOrganization,
|
|
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 isStatusPageEnabled = useFlag('incidentIoStatusPage') === true
|
|
|
|
const { getRegionRestriction } = useRegionRestriction()
|
|
|
|
const { data: statusData } = useIncidentStatusQuery({ enabled: !isStatusPageEnabled })
|
|
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, highAvailability },
|
|
{
|
|
enabled: smartRegionEnabled && hasSelectedOrganization,
|
|
staleTime: 1000 * 60 * 5,
|
|
}
|
|
)
|
|
|
|
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
|
|
|
|
return affectedRegions.some((affectedRegion) =>
|
|
regionMatches(selectedRegion.code, affectedRegion)
|
|
)
|
|
})
|
|
|
|
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>
|
|
|
|
{isStatusPageEnabled ? (
|
|
<ProjectCreationStatusAdmonition selectedRegionCode={selectedRegion?.code} />
|
|
) : (
|
|
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>
|
|
)
|
|
}
|