feat: multiple options for region config

This commit is contained in:
kemal committed 2026-10-05 15:30:57 +01:00
1 parent 87681812a0
commit c834a5f220
8 files changed
+1218 -430

No files matched your search

@@ -1,4 +1,5 @@
import { useFlag } from 'common'
import type { ReactNode } from 'react'
import { UseFormReturn } from 'react-hook-form'
import {
Badge,
@@ -23,9 +24,21 @@ import { DOCS_URL } from '@/lib/constants'
interface AdvancedConfigurationProps {
form: UseFormReturn<CreateProjectForm>
/** Hidden when no OrioleDB image is available for the current selection. */
hasPostgresTypeOption?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
/** Extra fields rendered above the Postgres type field, e.g. the region picker in option B. */
children?: ReactNode
}
export const AdvancedConfiguration = ({ form }: AdvancedConfigurationProps) => {
export const AdvancedConfiguration = ({
form,
hasPostgresTypeOption = true,
open,
onOpenChange,
children,
}: AdvancedConfigurationProps) => {
const disableOrioleProjectCreation = useFlag('disableOrioleProjectCreation')
return (
@@ -33,86 +46,97 @@ export const AdvancedConfiguration = ({ form }: AdvancedConfigurationProps) => {
<CollapsibleCardSection
title="Advanced Configuration"
description="These settings cannot be changed after the project is created"
open={open}
onOpenChange={onOpenChange}
>
<FormField
name="useOrioleDb"
control={form.control}
render={({ field }) => (
<>
<FormItemLayout
layout="horizontal"
label="Postgres Type"
className="[&>div>label]:break-normal!"
>
<FormControl>
<RadioGroupStacked
// Due to radio group not supporting boolean values
// value is converted to boolean
onValueChange={(value) => field.onChange(value === 'true')}
defaultValue={field.value.toString()}
<div className="flex flex-col gap-y-6">
{children}
{hasPostgresTypeOption && (
<FormField
name="useOrioleDb"
control={form.control}
render={({ field }) => (
<>
<FormItemLayout
layout="horizontal"
label="Postgres Type"
className="[&>div>label]:break-normal!"
>
<FormItem asChild>
<FormControl>
<RadioGroupStackedItem
value="false"
// @ts-ignore
label={
<>
Postgres
<Badge>Default</Badge>
</>
}
description="Recommended for production workloads"
className="[&>div>div>p]:text-left [&>div>div>p]:text-xs [&>div>div>div]:flex [&>div>div>div]:items-center [&>div>div>div]:gap-x-2"
/>
</FormControl>
</FormItem>
<FormItem asChild>
<FormControl>
<Tooltip>
<TooltipTrigger asChild>
<FormControl>
<RadioGroupStacked
// Due to radio group not supporting boolean values
// value is converted to boolean
onValueChange={(value) => field.onChange(value === 'true')}
defaultValue={field.value.toString()}
>
<FormItem asChild>
<FormControl>
<RadioGroupStackedItem
value="true"
value="false"
// @ts-ignore
label={
<>
Postgres with OrioleDB
<Badge variant="warning">Beta</Badge>
Postgres
<Badge>Default</Badge>
</>
}
description="Not recommended for production workloads"
className={cn(
'[&>div>div>p]:text-left [&>div>div>p]:text-xs [&>div>div>div]:flex [&>div>div>div]:items-center [&>div>div>div]:gap-x-2',
form.getValues('useOrioleDb') ? 'rounded-b-none!' : ''
)}
disabled={disableOrioleProjectCreation}
description="Recommended for production workloads"
className="[&>div>div>p]:text-left [&>div>div>p]:text-xs [&>div>div>div]:flex [&>div>div>div]:items-center [&>div>div>div]:gap-x-2"
/>
</TooltipTrigger>
{disableOrioleProjectCreation && (
<TooltipContent side="right" className="w-60 text-center">
OrioleDB is temporarily disabled for new projects. Please try again
later.
</TooltipContent>
)}
</Tooltip>
</FormControl>
</FormItem>
</RadioGroupStacked>
</FormControl>
{form.getValues('useOrioleDb') && (
<Admonition
type="warning"
className="rounded-t-none [&>div]:text-xs"
title="OrioleDB is not production ready"
description="Postgres with OrioleDB extension is currently in Public Beta and not recommended for production usage yet."
>
<DocsButton className="mt-2" href={`${DOCS_URL}/guides/database/orioledb`} />
</Admonition>
)}
</FormItemLayout>
</>
</FormControl>
</FormItem>
<FormItem asChild>
<FormControl>
<Tooltip>
<TooltipTrigger asChild>
<RadioGroupStackedItem
value="true"
// @ts-ignore
label={
<>
Postgres with OrioleDB
<Badge variant="warning">Beta</Badge>
</>
}
description="Not recommended for production workloads"
className={cn(
'[&>div>div>p]:text-left [&>div>div>p]:text-xs [&>div>div>div]:flex [&>div>div>div]:items-center [&>div>div>div]:gap-x-2',
form.getValues('useOrioleDb') ? 'rounded-b-none!' : ''
)}
disabled={disableOrioleProjectCreation}
/>
</TooltipTrigger>
{disableOrioleProjectCreation && (
<TooltipContent side="right" className="w-60 text-center">
OrioleDB is temporarily disabled for new projects. Please try
again later.
</TooltipContent>
)}
</Tooltip>
</FormControl>
</FormItem>
</RadioGroupStacked>
</FormControl>
{form.getValues('useOrioleDb') && (
<Admonition
type="warning"
className="rounded-t-none [&>div]:text-xs"
title="OrioleDB is not production ready"
description="Postgres with OrioleDB extension is currently in Public Beta and not recommended for production usage yet."
>
<DocsButton
className="mt-2"
href={`${DOCS_URL}/guides/database/orioledb`}
/>
</Admonition>
)}
</FormItemLayout>
</>
)}
/>
)}
/>
</div>
</CollapsibleCardSection>
</Panel.Content>
)
@@ -0,0 +1,81 @@
import { describe, expect, test } from 'vitest'
import {
parseAdvancedRegionConfigVariant,
resolveAdvancedRegionConfigVariant,
} from './AdvancedRegionConfig'
describe('parseAdvancedRegionConfigVariant', () => {
test('returns known variants', () => {
expect(parseAdvancedRegionConfigVariant('option_a')).toBe('option_a')
expect(parseAdvancedRegionConfigVariant('control')).toBe('control')
})
test('returns undefined for anything else', () => {
expect(parseAdvancedRegionConfigVariant('option_d')).toBeUndefined()
expect(parseAdvancedRegionConfigVariant(true)).toBeUndefined()
expect(parseAdvancedRegionConfigVariant(undefined)).toBeUndefined()
})
})
describe('resolveAdvancedRegionConfigVariant', () => {
test('defaults to control', () => {
expect(
resolveAdvancedRegionConfigVariant({
queryParamValue: null,
flagValue: undefined,
isProduction: false,
})
).toBe('control')
})
test('falls back to control for an unknown flag value', () => {
expect(
resolveAdvancedRegionConfigVariant({
queryParamValue: null,
flagValue: 'option_z',
isProduction: false,
})
).toBe('control')
})
test('uses the remote flag when there is no override', () => {
expect(
resolveAdvancedRegionConfigVariant({
queryParamValue: null,
flagValue: 'option_b',
isProduction: true,
})
).toBe('option_b')
})
test('the query param wins outside production', () => {
expect(
resolveAdvancedRegionConfigVariant({
queryParamValue: 'option_c',
flagValue: 'option_b',
isProduction: false,
})
).toBe('option_c')
})
test('the query param is ignored in production', () => {
expect(
resolveAdvancedRegionConfigVariant({
queryParamValue: 'option_c',
flagValue: 'option_b',
isProduction: true,
})
).toBe('option_b')
})
test('an unknown query param falls through to the flag', () => {
expect(
resolveAdvancedRegionConfigVariant({
queryParamValue: 'nonsense',
flagValue: 'option_a',
isProduction: false,
})
).toBe('option_a')
})
})
@@ -0,0 +1,404 @@
import { ChevronRight } from 'lucide-react'
import { parseAsString, useQueryState } from 'nuqs'
import { useEffect, useState } from 'react'
import { UseFormReturn } from 'react-hook-form'
import {
Button,
FormControl,
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
} from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { CreateProjectForm } from './ProjectCreation.schema'
import {
GeneralRegionSelectItems,
RegionSelectTriggerValue,
SpecificRegionSelectItems,
useRegionSelectorOptions,
type RegionSelectProps,
} from './RegionSelector.shared'
import { SELECT_DIFFERENT_REGION } from './RegionSelector.utils'
import Panel from '@/components/ui/Panel'
import { RegionFlag } from '@/components/ui/RegionFlag'
import type { DesiredInstanceSize } from '@/data/projects/new-project.constants'
import { usePHFlag } from '@/hooks/ui/useFlag'
/**
* Advanced region config experiment (P-PROD-4259). Everything experiment-specific lives in this
* file: variant resolution, the instrumentation stubs, and the three treatments. Deleting this
* file plus its two call sites in RegionSelector/ProjectCreationForm removes the experiment.
*/
export const ADVANCED_REGION_CONFIG_VARIANTS = [
'control',
'option_a',
'option_b',
'option_c',
] as const
export type AdvancedRegionConfigVariant = (typeof ADVANCED_REGION_CONFIG_VARIANTS)[number]
export const ADVANCED_REGION_CONFIG_FLAG_KEY = 'advancedRegionConfig'
export const ADVANCED_REGION_CONFIG_EXPERIMENT_ID = 'advanced_region_config'
export const ADVANCED_REGION_CONFIG_QUERY_PARAM = 'regionVariant'
export function parseAdvancedRegionConfigVariant(
value: unknown
): AdvancedRegionConfigVariant | undefined {
return ADVANCED_REGION_CONFIG_VARIANTS.find((variant) => variant === value)
}
/**
* Resolution order, first match wins:
* 1. `?regionVariant=` — ignored in production so the override can't leak into the experiment
* 2. the remote PostHog flag value
* 3. `control`
*/
export function resolveAdvancedRegionConfigVariant({
queryParamValue,
flagValue,
isProduction,
}: {
queryParamValue: string | null | undefined
flagValue: string | boolean | undefined
isProduction: boolean
}): AdvancedRegionConfigVariant {
if (!isProduction) {
const override = parseAdvancedRegionConfigVariant(queryParamValue)
if (override !== undefined) return override
}
return parseAdvancedRegionConfigVariant(flagValue) ?? 'control'
}
/** Variant assignment. Unknown flag values and unknown query params both fall back to `control`. */
export function useAdvancedRegionConfigVariant(): AdvancedRegionConfigVariant {
const [queryParamValue] = useQueryState(ADVANCED_REGION_CONFIG_QUERY_PARAM, parseAsString)
const flagValue = usePHFlag<string>(ADVANCED_REGION_CONFIG_FLAG_KEY)
return resolveAdvancedRegionConfigVariant({
queryParamValue,
flagValue,
isProduction: process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod',
})
}
/**
* TODO(P-PROD-4259): prototype-only no-op instrumentation.
*
* Each handler below is where the real PostHog event belongs once a variant wins. Wire them up
* the way the existing project-creation experiments do:
* - exposure -> `useTrackExperimentExposure(ADVANCED_REGION_CONFIG_EXPERIMENT_ID, variant)` from
* `hooks/misc/useTrackExperimentExposure.ts`, which is what the plan-presentation experiment
* uses and what the Best-available region work would plug into
* - the two interaction events -> `useTrack()` from `lib/telemetry/track`, after adding the event
* names to `packages/common/telemetry-constants.ts`
* - carry the resolved variant as a property on every event, and add it to the existing
* `project_creation_simple_version_submitted` payload in ProjectCreationForm the same way
* `dataApiRevokeOnCreateDefaultEnabled` is attached there
*/
export function useAdvancedRegionConfigTelemetry(variant: AdvancedRegionConfigVariant) {
useEffect(() => {
// TODO(P-PROD-4259): variant exposure
void variant
}, [variant])
const trackDisclosureOpened = () => {
// TODO(P-PROD-4259): disclosure opened
void variant
}
const trackSpecificRegionSelected = (_regionCode: string | undefined) => {
// TODO(P-PROD-4259): specific region selected
void variant
}
return { trackDisclosureOpened, trackSpecificRegionSelected }
}
const TRIGGER_CLASS =
'[&>:nth-child(1)]:w-full [&>:nth-child(1)]:flex [&>:nth-child(1)]:items-start'
/**
* Option A — the specific regions stay in the same dropdown, but behind an inline disclosure at
* the bottom of the list.
*/
export const RegionSelectorOptionA = ({
options,
value,
onChange,
triggerRef,
onDisclosureOpened,
onSpecificRegionSelected,
}: RegionSelectProps) => {
const {
generalRegions,
specificRegions,
recommendedGeneralRegionCodes,
recommendedSpecificRegionCodes,
selectedRegion,
selectedRegionLabel,
specificRegionsLabel,
hasGeneralRegions,
isLoading,
} = options
const [hasOpenedDisclosure, setHasOpenedDisclosure] = useState(false)
const isSpecificRegionSelected = specificRegions.some((region) => region.name === value)
const areSpecificRegionsVisible =
hasOpenedDisclosure || isSpecificRegionSelected || !hasGeneralRegions
const handleOpenDisclosure = () => {
setHasOpenedDisclosure(true)
onDisclosureOpened()
}
const handleValueChange = (nextValue: string) => {
const specificRegion = specificRegions.find((region) => region.name === nextValue)
if (specificRegion !== undefined) onSpecificRegionSelected(specificRegion.code)
onChange(nextValue)
}
return (
<FormControl>
<Select value={value} onValueChange={handleValueChange} disabled={isLoading}>
<SelectTrigger ref={triggerRef} id="region" className={TRIGGER_CLASS}>
<SelectValue
placeholder={
isLoading ? 'Loading available regions...' : 'Select a region for your project..'
}
>
{value !== undefined && (
<RegionSelectTriggerValue
region={selectedRegion}
label={isLoading ? 'Loading available regions...' : selectedRegionLabel}
isLoading={isLoading}
/>
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{hasGeneralRegions && (
<>
<SelectGroup>
<SelectLabel>General regions</SelectLabel>
<GeneralRegionSelectItems
regions={generalRegions}
recommendedCodes={recommendedGeneralRegionCodes}
/>
</SelectGroup>
<SelectSeparator />
</>
)}
{!areSpecificRegionsVisible && (
<button
type="button"
onClick={handleOpenDisclosure}
className="w-full flex items-center gap-x-1 px-2 py-1.5 text-sm text-foreground-light hover:text-foreground rounded-sm hover:bg-overlay-hover transition"
>
<ChevronRight size={14} strokeWidth={1.5} />
Advanced: choose a specific region
</button>
)}
{areSpecificRegionsVisible && (
<SelectGroup>
<SelectLabel>{specificRegionsLabel}</SelectLabel>
<SpecificRegionSelectItems
regions={specificRegions}
recommendedCodes={recommendedSpecificRegionCodes}
/>
</SelectGroup>
)}
</SelectContent>
</Select>
</FormControl>
)
}
/**
* Option B — the main form only reports the current region as text; the picker itself is rendered
* inside the "Advanced Configuration" section by ProjectCreationForm.
*/
export const RegionSummaryRow = ({
form,
hasSelectedOrganization,
instanceSize,
onChangeRegionClick,
}: {
form: UseFormReturn<CreateProjectForm>
hasSelectedOrganization: boolean
instanceSize?: DesiredInstanceSize
onChangeRegionClick: () => void
}) => {
const { selectedRegion, selectedRegionLabel, selectedRestrictionCopy, isLoading } =
useRegionSelectorOptions({ form, hasSelectedOrganization, instanceSize })
return (
<Panel.Content>
<FormItemLayout layout="horizontal" label="Region" isReactForm={false}>
<div className="flex items-center gap-x-3 min-h-[38px]">
{!isLoading && selectedRegion?.code && (
<RegionFlag className="w-5" region={selectedRegion.code} />
)}
<span className="text-sm text-foreground">
{isLoading ? 'Loading available regions...' : (selectedRegionLabel ?? 'Not selected')}
</span>
<Button type="button" variant="link" size="tiny" onClick={onChangeRegionClick}>
Change in advanced configuration
</Button>
</div>
</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>
)
}
/**
* Sentinel for the "Choose a specific region" entry in the first dropdown. It is never written to
* `dbRegion` — picking it only reveals the second selector.
*/
const CHOOSE_SPECIFIC_REGION = '__choose_specific_region__'
/**
* Option C — two-step selector. The first dropdown offers the general regions plus an entry that
* reveals a second dropdown holding the specific regions.
*/
export const RegionSelectorOptionC = ({
options,
value,
onChange,
triggerRef,
onDisclosureOpened,
onSpecificRegionSelected,
}: RegionSelectProps) => {
const {
generalRegions,
specificRegions,
recommendedGeneralRegionCodes,
recommendedSpecificRegionCodes,
selectedRegion,
selectedRegionLabel,
specificRegionsLabel,
hasGeneralRegions,
isLoading,
} = options
const [isChoosingSpecificRegion, setIsChoosingSpecificRegion] = useState(false)
const isSpecificRegionSelected = specificRegions.some((region) => region.name === value)
const isSpecificSelectVisible =
isChoosingSpecificRegion || isSpecificRegionSelected || !hasGeneralRegions
const handleGeneralValueChange = (nextValue: string) => {
if (nextValue === CHOOSE_SPECIFIC_REGION) {
setIsChoosingSpecificRegion(true)
onDisclosureOpened()
return
}
// Picking a general region clears the specific choice by overwriting `dbRegion` outright.
setIsChoosingSpecificRegion(false)
onChange(nextValue)
}
const handleSpecificValueChange = (nextValue: string) => {
const specificRegion = specificRegions.find((region) => region.name === nextValue)
if (specificRegion !== undefined) onSpecificRegionSelected(specificRegion.code)
onChange(nextValue)
}
return (
<div className="flex flex-col gap-y-2">
<FormControl>
<Select
value={isSpecificSelectVisible ? CHOOSE_SPECIFIC_REGION : value}
onValueChange={handleGeneralValueChange}
disabled={isLoading}
>
<SelectTrigger ref={triggerRef} id="region" className={TRIGGER_CLASS}>
<SelectValue
placeholder={
isLoading ? 'Loading available regions...' : 'Select a region for your project..'
}
>
{isSpecificSelectVisible && (
<span className="text-foreground">Choose a specific region</span>
)}
{!isSpecificSelectVisible && value !== undefined && (
<RegionSelectTriggerValue
region={selectedRegion}
label={isLoading ? 'Loading available regions...' : selectedRegionLabel}
isLoading={isLoading}
/>
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{hasGeneralRegions && (
<>
<SelectGroup>
<SelectLabel>General regions</SelectLabel>
<GeneralRegionSelectItems
regions={generalRegions}
recommendedCodes={recommendedGeneralRegionCodes}
/>
</SelectGroup>
<SelectSeparator />
</>
)}
<SelectItem value={CHOOSE_SPECIFIC_REGION}>Choose a specific region</SelectItem>
</SelectContent>
</Select>
</FormControl>
{isSpecificSelectVisible && (
<Select
value={isSpecificRegionSelected ? value : ''}
onValueChange={handleSpecificValueChange}
disabled={isLoading}
>
<SelectTrigger id="specific-region" className={TRIGGER_CLASS}>
<SelectValue placeholder="Select a specific region...">
{isSpecificRegionSelected && (
<RegionSelectTriggerValue
region={selectedRegion}
label={selectedRegionLabel}
isLoading={isLoading}
/>
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{specificRegionsLabel}</SelectLabel>
<SpecificRegionSelectItems
regions={specificRegions}
recommendedCodes={recommendedSpecificRegionCodes}
/>
</SelectGroup>
</SelectContent>
</Select>
)}
</div>
)
}
@@ -14,6 +14,7 @@ import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { z } from 'zod'
import { AdvancedConfiguration } from './AdvancedConfiguration'
import { RegionSummaryRow, useAdvancedRegionConfigVariant } from './AdvancedRegionConfig'
import { ComputeSizeSelector } from './ComputeSizeSelector'
import { DatabasePasswordInput } from './DatabasePasswordInput'
import { DataSeeding } from './DataSeeding'
@@ -132,6 +133,10 @@ export const ProjectCreationForm = ({
'integrations.github_connections'
)
const showAdvancedConfig = useIsFeatureEnabled('project_creation:show_advanced_config')
const regionVariant = useAdvancedRegionConfigVariant()
// Option B only applies where the advanced section actually renders — otherwise the region
// picker would have nowhere to live.
const isRegionInAdvancedConfig = regionVariant === 'option_b' && showAdvancedConfig
const { hasAccess: hasAccessToGitHubIntegration } = useCheckEntitlements(
'integrations.github_connections'
)
@@ -157,6 +162,8 @@ export const ProjectCreationForm = ({
const [isComputeCostsConfirmationModalVisible, setIsComputeCostsConfirmationModalVisible] =
useState(false)
const [projectCreationError, setProjectCreationError] = useState<string>()
const [isAdvancedConfigOpen, setIsAdvancedConfigOpen] = useState(false)
const advancedConfigRef = useRef<HTMLDivElement>(null)
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
@@ -325,6 +332,15 @@ export const ProjectCreationForm = ({
{ enabled: currentOrg !== null }
)
const hasPostgresTypeOption =
showAdvancedConfig && !!availableOrioleVersion && highAvailability !== true
const isAdvancedConfigVisible = hasPostgresTypeOption || isRegionInAdvancedConfig
const handleChangeRegionClick = () => {
setIsAdvancedConfigOpen(true)
advancedConfigRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
const userPrimaryEmail = profile?.primary_email?.toLowerCase()
const isUserAtFreeProjectLimit = userPrimaryEmail
? membersExceededLimit.some(
@@ -777,11 +793,21 @@ export const ProjectCreationForm = ({
<DatabasePasswordInput form={form} />
<RegionSelector
form={form}
hasSelectedOrganization={hasSelectedOrganization}
instanceSize={instanceSize as DesiredInstanceSize}
/>
{isRegionInAdvancedConfig ? (
<RegionSummaryRow
form={form}
hasSelectedOrganization={hasSelectedOrganization}
instanceSize={instanceSize as DesiredInstanceSize}
onChangeRegionClick={handleChangeRegionClick}
/>
) : (
<RegionSelector
form={form}
hasSelectedOrganization={hasSelectedOrganization}
instanceSize={instanceSize as DesiredInstanceSize}
variant={regionVariant}
/>
)}
{isVercelIntegrationFlow && !!externalId && <DataSeeding form={form} />}
@@ -789,9 +815,25 @@ export const ProjectCreationForm = ({
{showInternalOnlyConfiguration && <InternalOnlyConfiguration form={form} />}
{showAdvancedConfig &&
!!availableOrioleVersion &&
highAvailability !== true && <AdvancedConfiguration form={form} />}
{isAdvancedConfigVisible && (
<div ref={advancedConfigRef}>
<AdvancedConfiguration
form={form}
hasPostgresTypeOption={hasPostgresTypeOption}
open={isAdvancedConfigOpen}
onOpenChange={setIsAdvancedConfigOpen}
>
{isRegionInAdvancedConfig && (
<RegionSelector
form={form}
hasSelectedOrganization={hasSelectedOrganization}
instanceSize={instanceSize as DesiredInstanceSize}
isEmbedded
/>
)}
</AdvancedConfiguration>
</div>
)}
{shouldShowFreeProjectInfo ? (
<Admonition
@@ -0,0 +1,294 @@
import { useFeatureFlags, useFlag, useParams } from 'common'
import { Loader2 } from 'lucide-react'
import { useEffect, useRef, type Ref } from 'react'
import { UseFormReturn } from 'react-hook-form'
import type { CloudProvider } from 'shared-data'
import { Badge, cn, SelectItem, Tooltip, TooltipContent, TooltipTrigger, useWatch } from 'ui'
import { CreateProjectForm } from './ProjectCreation.schema'
import {
filterHighAvailabilityRegions,
getAvailableRegions,
getHighAvailabilityRegionCode,
} from './ProjectCreation.utils'
import {
getRegionRestrictionCopy,
regionMatches,
type RegionRestrictionCopy,
} from './RegionSelector.utils'
import { useRegionRestriction } from './useRegionRestriction'
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'
import type { ResponseError } from '@/types'
export type SelectableRegion = { code: string; name: string; status?: string }
export type RestrictableRegion = SelectableRegion & { restriction: string | undefined }
/** Props every arm of the region picker takes. */
export type RegionSelectProps = {
options: RegionSelectorOptions
value: string | undefined
onChange: (value: string) => void
triggerRef: Ref<HTMLButtonElement>
onDisclosureOpened: () => void
onSpecificRegionSelected: (regionCode: string | undefined) => void
}
/** Backend region names that read better in the UI under a different label. */
export const getDisplayNameForGeneralRegion = (name: string): string =>
name === 'APAC' ? 'Asia-Pacific' : name
export type RegionSelectorOptions = {
dbRegion: string | undefined
generalRegions: SelectableRegion[]
specificRegions: RestrictableRegion[]
recommendedGeneralRegionCodes: Set<string | undefined>
recommendedSpecificRegionCodes: Set<string>
selectedRegion: SelectableRegion | undefined
selectedRegionLabel: string | undefined
selectedRestrictionCopy: RegionRestrictionCopy | undefined
specificRegionsLabel: string
hasGeneralRegions: boolean
isLoading: boolean
isError: boolean
error: ResponseError | null
affectingIncidents: unknown[]
isStatusPageEnabled: boolean
isHighAvailabilityRestricted: boolean
highAvailabilityRegionLabel: string | undefined
}
/**
* Region data, derived option lists and selection bookkeeping shared by every rendering of the
* region picker, so no experiment variant has to duplicate any of it.
*/
export function useRegionSelectorOptions({
form,
hasSelectedOrganization,
instanceSize,
}: {
form: UseFormReturn<CreateProjectForm>
hasSelectedOrganization: boolean
instanceSize?: DesiredInstanceSize
}): RegionSelectorOptions {
const { slug } = useParams()
const cloudProvider = useWatch({ control: form.control, name: '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,
error,
} = useOrganizationAvailableRegionsQuery(
{ slug, cloudProvider, desiredInstanceSize: instanceSize, highAvailability },
{ enabled: smartRegionEnabled && hasSelectedOrganization, staleTime: 1000 * 60 * 5 }
)
const isLocal = process.env.NEXT_PUBLIC_ENVIRONMENT === 'local'
const allSmartRegions = availableRegionsData?.all.smartGroup ?? []
const allRegions = availableRegionsData?.all.specific ?? []
const isHighAvailabilityRestricted =
highAvailability && !isLocal && highAvailabilityRegionCode !== undefined
const generalRegions = highAvailability ? [] : allSmartRegions
const recommendedGeneralRegionCodes = new Set(
[availableRegionsData?.recommendations.smartGroup.code].filter(Boolean)
)
const recommendedSpecificRegionCodes = new Set(
availableRegionsData?.recommendations.specific.map((region) => region.code)
)
const regionsArray = Object.entries(getAvailableRegions(cloudProvider)).map(([_key, value]) => ({
code: value.code,
name: value.displayName,
provider: cloudProvider,
status: undefined,
}))
const unfilteredRegionOptions = smartRegionEnabled ? allRegions : regionsArray
const regionOptions = filterHighAvailabilityRegions(
[...unfilteredRegionOptions],
highAvailability
)
const specificRegions: RestrictableRegion[] = regionOptions.map((region) => ({
...region,
restriction: getRegionRestriction(region),
}))
const isLoading = smartRegionEnabled ? isLoadingAvailableRegions : isLoadingDefaultRegion
const allSelectableRegions = [...generalRegions, ...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])
const selectedRegion = allSelectableRegions.find(
(region) => !!region.name && region.name === dbRegion
)
const selectedRestriction = specificRegions.find(
(region) => region.name === dbRegion
)?.restriction
return {
dbRegion,
generalRegions,
specificRegions,
recommendedGeneralRegionCodes,
recommendedSpecificRegionCodes,
selectedRegion,
selectedRegionLabel: selectedRegion?.name
? getDisplayNameForGeneralRegion(selectedRegion.name)
: dbRegion,
selectedRestrictionCopy:
selectedRestriction !== undefined ? getRegionRestrictionCopy(selectedRestriction) : undefined,
specificRegionsLabel: highAvailability ? 'High Availability Regions' : 'Specific regions',
hasGeneralRegions: smartRegionEnabled && !highAvailability,
isLoading,
isError,
error,
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)
)
}),
isStatusPageEnabled,
isHighAvailabilityRestricted,
highAvailabilityRegionLabel: regionOptions[0]?.name ?? highAvailabilityRegionCode,
}
}
export const RegionSelectTriggerValue = ({
region,
label,
isLoading,
}: {
region: SelectableRegion | undefined
label: string | undefined
isLoading: boolean
}) => (
<div className="flex items-center gap-x-3">
{isLoading && <Loader2 size={14} className="animate-spin" />}
{region?.code && <RegionFlag className="w-5" region={region.code} />}
<span className="text-foreground">{label}</span>
</div>
)
export const GeneralRegionSelectItems = ({
regions,
recommendedCodes,
}: {
regions: SelectableRegion[]
recommendedCodes: Set<string | undefined>
}) =>
regions.map((region) => (
<SelectItem key={region.code} value={region.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={region.code} />
<span className="text-foreground">{getDisplayNameForGeneralRegion(region.name)}</span>
</div>
<div>
{recommendedCodes.has(region.code) && (
<Badge variant="success" className="mr-1">
Recommended
</Badge>
)}
</div>
</div>
</SelectItem>
))
export const SpecificRegionSelectItems = ({
regions,
recommendedCodes,
}: {
regions: RestrictableRegion[]
recommendedCodes: Set<string>
}) =>
regions.map((region) => {
const restrictionCopy =
region.restriction !== undefined ? getRegionRestrictionCopy(region.restriction) : undefined
return (
<SelectItem
key={region.code}
value={region.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={region.code} />
<div className="flex items-center gap-x-2">
<span className="text-foreground">{region.name}</span>
<span className="text-xs text-foreground-lighter font-mono">{region.code}</span>
</div>
</div>
{recommendedCodes.has(region.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>
)
})
@@ -1,49 +1,38 @@
import { useFeatureFlags, useFlag, useParams } from 'common'
import { Loader2 } 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'
RegionSelectorOptionA,
RegionSelectorOptionC,
useAdvancedRegionConfigTelemetry,
type AdvancedRegionConfigVariant,
} from './AdvancedRegionConfig'
import { CreateProjectForm } from './ProjectCreation.schema'
import { ProjectCreationStatusAdmonition } from './ProjectCreationStatusAdmonition'
import {
getRegionRestrictionCopy,
regionMatches,
SELECT_DIFFERENT_REGION,
} from './RegionSelector.utils'
import { useRegionRestriction } from './useRegionRestriction'
GeneralRegionSelectItems,
RegionSelectTriggerValue,
SpecificRegionSelectItems,
useRegionSelectorOptions,
type RegionSelectorOptions,
type RegionSelectProps,
} from './RegionSelector.shared'
import { SELECT_DIFFERENT_REGION } from './RegionSelector.utils'
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 {
@@ -51,352 +40,199 @@ interface RegionSelectorProps {
hasSelectedOrganization: boolean
instanceSize?: DesiredInstanceSize
layout?: 'vertical' | 'horizontal'
/** Experiment arm (P-PROD-4259). `option_b` renders the control picker — it only moves place. */
variant?: AdvancedRegionConfigVariant
/** Set when rendered inside a section that already provides its own padding. */
isEmbedded?: boolean
}
// [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 isLocalEnvironment = process.env.NEXT_PUBLIC_ENVIRONMENT === 'local'
const showNonProdFields = isLocalEnvironment || process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
/** The region picker as it ships today — the `control` arm of the experiment. */
const RegionSelectControl = ({ options, value, onChange, triggerRef }: RegionSelectProps) => {
const {
generalRegions,
specificRegions,
recommendedGeneralRegionCodes,
recommendedSpecificRegionCodes,
selectedRegion,
selectedRegionLabel,
specificRegionsLabel,
hasGeneralRegions,
isLoading,
} = options
return (
<FormControl>
<Select value={value} onValueChange={onChange} disabled={isLoading}>
<SelectTrigger
ref={triggerRef}
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..'
}
>
{value !== undefined && (
<RegionSelectTriggerValue
region={selectedRegion}
label={isLoading ? 'Loading available regions...' : selectedRegionLabel}
isLoading={isLoading}
/>
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{hasGeneralRegions && (
<>
<SelectGroup>
<SelectLabel>General regions</SelectLabel>
<GeneralRegionSelectItems
regions={generalRegions}
recommendedCodes={recommendedGeneralRegionCodes}
/>
</SelectGroup>
<SelectSeparator />
</>
)}
<SelectGroup>
<SelectLabel>{specificRegionsLabel}</SelectLabel>
<SpecificRegionSelectItems
regions={specificRegions}
recommendedCodes={recommendedSpecificRegionCodes}
/>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
)
}
const isLocal = process.env.NEXT_PUBLIC_ENVIRONMENT === 'local'
const RegionSelectorDescription = ({ options }: { options: RegionSelectorOptions }) => {
if (options.isHighAvailabilityRestricted) {
return (
<div className="text-warning">
High Availability projects are currently limited to {options.highAvailabilityRegionLabel}.
</div>
)
}
if (!showNonProdFields) return null
return (
<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>
)
}
export const RegionSelector = ({
form,
hasSelectedOrganization,
instanceSize,
layout = 'horizontal',
variant = 'control',
isEmbedded = false,
}: 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 options = useRegionSelectorOptions({ form, hasSelectedOrganization, instanceSize })
const { trackDisclosureOpened, trackSpecificRegionSelected } =
useAdvancedRegionConfigTelemetry(variant)
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 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} />
if (options.isError) {
return <AlertError subject="Error loading available regions" error={options.error} />
}
return (
<Panel.Content>
<FormField
control={form.control}
name="dbRegion"
render={({ field }) => {
const selectedRegion = allSelectableRegions.find((region) => {
return !!region.name && region.name === dbRegion
})
const content = (
<FormField
control={form.control}
name="dbRegion"
render={({ field }) => {
const controlProps: RegionSelectProps = {
options,
value: options.dbRegion,
onChange: (value: string) => {
if (value === '') return
field.onChange(value)
},
triggerRef: field.ref,
onDisclosureOpened: trackDisclosureOpened,
onSpecificRegionSelected: trackSpecificRegionSelected,
}
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...' : selectedRegionLabel
const renderRegionControl = () => {
if (variant === 'option_a') return <RegionSelectorOptionA {...controlProps} />
if (variant === 'option_c') return <RegionSelectorOptionC {...controlProps} />
return <RegionSelectControl {...controlProps} />
}
const affectingIncidents = incidents.filter((incident) => {
const affectedRegions = incident.cache?.affected_regions ?? []
if (affectedRegions.length === 0 || selectedRegion?.code === undefined) return false
return (
<>
<FormItemLayout
id="region"
layout={layout}
label="Region"
description={<RegionSelectorDescription options={options} />}
>
{renderRegionControl()}
</FormItemLayout>
return affectedRegions.some((affectedRegion) =>
regionMatches(selectedRegion.code, affectedRegion)
)
})
{options.isStatusPageEnabled && (
<ProjectCreationStatusAdmonition selectedRegionCode={options.selectedRegion?.code} />
)}
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={dbRegion}
onValueChange={(value) => {
if (value === '') return
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" />}
{selectedRegion?.code && (
<RegionFlag className="w-5" region={selectedRegion.code} />
)}
<span className="text-foreground">{triggerLabel}</span>
</div>
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{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>
{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>
{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>
{!options.isStatusPageEnabled && options.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>
)}
{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>
{options.selectedRestrictionCopy !== undefined && (
<FormItemLayout layout="horizontal" isReactForm={false}>
<Admonition
type="warning"
title={options.selectedRestrictionCopy.title}
description={`${options.selectedRestrictionCopy.notice} ${SELECT_DIFFERENT_REGION}`}
className="mt-3"
/>
</FormItemLayout>
)}
</>
)
}}
/>
)
if (isEmbedded) return content
return <Panel.Content>{content}</Panel.Content>
}
+105 -2
View File
@@ -277,7 +277,9 @@ const DEFAULT_FLAGS = {
projectCreationRestrictedRegions: false,
}
async function renderWizard(options: { flags?: Record<string, boolean | string> } = {}) {
async function renderWizard(
options: { flags?: Record<string, boolean | string>; searchParams?: string } = {}
) {
const { default: Wizard } = await import('@/pages/new/[slug]')
return customRender(
<FeatureFlagContext.Provider
@@ -285,7 +287,10 @@ async function renderWizard(options: { flags?: Record<string, boolean | string>
>
<Wizard dehydratedState={undefined} />
</FeatureFlagContext.Provider>,
{ profileContext: createMockProfileContext() }
{
profileContext: createMockProfileContext(),
...(options.searchParams ? { nuqs: { searchParams: options.searchParams } } : {}),
}
)
}
@@ -1236,4 +1241,102 @@ describe('project creation wizard', () => {
)
)
})
// P-PROD-4259 — the variants only move the picker around; every arm must still submit the
// same region_selection payload for both a general and a specific region.
describe('advanced region config experiment', () => {
// Option B renders two "Region" labels (the summary row and the picker in advanced
// configuration), so the shared helper can't disambiguate them.
const getLastSelectTriggerByLabel = (labelText: string) => {
const labels = screen.getAllByText(labelText)
const row = labels[labels.length - 1].closest(
'[data-formlayout-id="labelContainer"]'
)?.parentElement
const trigger = row?.querySelector('[role="combobox"]')
if (!trigger) throw new Error(`No combobox trigger found near label "${labelText}"`)
return trigger as HTMLElement
}
const getTriggerByPlaceholder = (placeholder: string) => {
const trigger = screen.getByText(placeholder).closest('[role="combobox"]')
if (!trigger) throw new Error(`No combobox trigger found for "${placeholder}"`)
return trigger as HTMLElement
}
const renderVariant = async (variant: string) => {
mockWizardEndpoints()
const onRequest = vi.fn()
mockCreateProject(onRequest)
await renderWizard({ searchParams: `?regionVariant=${variant}` })
await fillProjectName(`${variant} project`)
await generateAndWaitForStrongPassword()
return onRequest
}
const submitAndReadRegion = async (onRequest: ReturnType<typeof vi.fn>) => {
fireEvent.click(screen.getByRole('button', { name: 'Create new project' }))
await waitFor(() => expect(onRequest).toHaveBeenCalled())
return onRequest.mock.calls[0][0].region_selection
}
test('option A submits a general region, and a specific region behind the inline disclosure', async () => {
const onRequest = await renderVariant('option_a')
await selectRegion(/Americas/)
expect(getSelectTriggerByLabel('Region')).toHaveTextContent('Americas')
await user.click(getSelectTriggerByLabel('Region'))
await user.click(
await screen.findByRole('button', { name: /Advanced: choose a specific region/ })
)
await user.click(await screen.findByRole('option', { name: /East US/ }))
expect(await submitAndReadRegion(onRequest)).toMatchObject({ code: 'us-east-1' })
})
test('option B keeps the picker in advanced configuration and mirrors it in the summary row', async () => {
const onRequest = await renderVariant('option_b')
await waitFor(() => expect(screen.getByText('Americas')).toBeInTheDocument())
expect(screen.queryByRole('combobox', { name: 'Region' })).not.toBeInTheDocument()
await user.click(screen.getByRole('button', { name: 'Change in advanced configuration' }))
await user.click(await waitFor(() => getLastSelectTriggerByLabel('Region')))
await user.click(await screen.findByRole('option', { name: /East US/ }))
await waitFor(() =>
expect(screen.getAllByText('East US (North Virginia)').length).toBeGreaterThan(1)
)
expect(await submitAndReadRegion(onRequest)).toMatchObject({ code: 'us-east-1' })
})
test('option C reveals a second selector for specific regions', async () => {
const onRequest = await renderVariant('option_c')
await selectRegion(/Americas/)
await user.click(getSelectTriggerByLabel('Region'))
await user.click(await screen.findByRole('option', { name: 'Choose a specific region' }))
await user.click(getTriggerByPlaceholder('Select a specific region...'))
await user.click(await screen.findByRole('option', { name: /East US/ }))
expect(await submitAndReadRegion(onRequest)).toMatchObject({ code: 'us-east-1' })
})
test('option C keeps the previous general region until a specific one is picked', async () => {
const onRequest = await renderVariant('option_c')
await selectRegion(/Americas/)
await user.click(getSelectTriggerByLabel('Region'))
await user.click(await screen.findByRole('option', { name: 'Choose a specific region' }))
expect(getTriggerByPlaceholder('Select a specific region...')).toBeInTheDocument()
expect(await submitAndReadRegion(onRequest)).toMatchObject({ code: 'americas' })
})
})
})
@@ -6,15 +6,19 @@ type CollapsibleCardSectionProps = PropsWithChildren<{
title: string
description?: ReactNode
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
}>
export const CollapsibleCardSection = ({
title,
description,
defaultOpen = false,
open,
onOpenChange,
children,
}: CollapsibleCardSectionProps) => (
<Collapsible defaultOpen={defaultOpen}>
<Collapsible defaultOpen={defaultOpen} open={open} onOpenChange={onOpenChange}>
<CollapsibleTrigger className="group/trigger font-mono uppercase tracking-widest text-xs flex items-center gap-1 text-foreground-lighter/75 hover:text-foreground-light transition data-open:text-foreground-light">
{title}
<ChevronRight