Files
supabase/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.utils.ts
T
ec2657e4f7 fix: Filter the version selector in the new project form by the chosen project type (#51490)
This pull request enhances the project creation flow by introducing
support for filtering Postgres versions based on the OrioleDB engine.
The main changes involve updating the `PostgresVersionSelector`
component to allow filtering for OrioleDB-specific versions, and
integrating this logic into the internal-only configuration step of
project creation. This provides a clearer and more flexible experience
when selecting Postgres versions, especially for internal or advanced
users.

**Postgres Version Selection Enhancements:**

* Added a new `useOrioleDbForCreate` prop to `PostgresVersionSelector`,
allowing the component to filter available Postgres versions to only
OrioleDB-based options or exclude them, based on user selection. The
prop defaults to `undefined` for no filtering.
[[1]](diffhunk://#diff-1842d78cb2a4be2c2b6e1581981d8111304fd9aaf0ad45df273d8c001938bcd9R41-R50)
[[2]](diffhunk://#diff-1842d78cb2a4be2c2b6e1581981d8111304fd9aaf0ad45df273d8c001938bcd9R82-R85)
[[3]](diffhunk://#diff-1842d78cb2a4be2c2b6e1581981d8111304fd9aaf0ad45df273d8c001938bcd9L92-R114)
* Added a `description` prop to `PostgresVersionSelector` for improved
UI guidance, and passed this prop through the form.
[[1]](diffhunk://#diff-1842d78cb2a4be2c2b6e1581981d8111304fd9aaf0ad45df273d8c001938bcd9R41-R50)
[[2]](diffhunk://#diff-1842d78cb2a4be2c2b6e1581981d8111304fd9aaf0ad45df273d8c001938bcd9R82-R85)
[[3]](diffhunk://#diff-1842d78cb2a4be2c2b6e1581981d8111304fd9aaf0ad45df273d8c001938bcd9L139-R161)

**Integration with Project Creation Flow:**

* Updated `InternalOnlyConfiguration` to watch the `useOrioleDb` form
field and pass its value to `PostgresVersionSelector`, ensuring the
version list is filtered appropriately during project creation. Also,
conditionally set the description to guide users toward advanced
configuration for OrioleDB.
[[1]](diffhunk://#diff-f0fa51a1cc67bb1a2c38d2c595eb3cab3a45813a027a04942d684914f2c3de93R33)
[[2]](diffhunk://#diff-f0fa51a1cc67bb1a2c38d2c595eb3cab3a45813a027a04942d684914f2c3de93R60-R65)
* Adjusted the render order in `ProjectCreationForm` to ensure advanced
configuration and internal-only configuration are displayed in a logical
sequence.

**Other Improvements:**

* Imported the `isSuccess` utility from `effect/Result` in
`PostgresVersionSelector.tsx` to support the updated logic.

---------

Co-authored-by: Alaister Young <alaister@users.noreply.github.com>
Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-10-13 12:15:51 +02:00

238 lines
8.1 KiB
TypeScript

import type { CloudProvider, Region } from 'shared-data'
import { AWS_REGIONS } from 'shared-data'
import { SMART_REGION_TO_EXACT_REGION_MAP } from 'shared-data/regions'
import type { OrganizationAvailableRegionsData } from '@/data/organizations/organization-available-regions-query'
import {
DesiredInstanceSize,
instanceSizeSpecs,
PostgresEngine,
ReleaseChannel,
} from '@/data/projects/new-project.constants'
export function smartRegionToExactRegion(smartOrExactRegion: string) {
return SMART_REGION_TO_EXACT_REGION_MAP.get(smartOrExactRegion) ?? smartOrExactRegion
}
export function getAvailableRegions(
cloudProvider: CloudProvider,
environment = process.env.NEXT_PUBLIC_ENVIRONMENT
): Region {
switch (cloudProvider) {
case 'AWS':
case 'AWS_K8S':
return AWS_REGIONS
case 'AWS_NIMBUS':
if (environment !== 'prod') {
// Only allow Southeast Asia for Nimbus (local/staging)
return {
SOUTHEAST_ASIA: AWS_REGIONS.SOUTHEAST_ASIA,
}
}
// Only allow US East for Nimbus (prod)
return {
EAST_US: AWS_REGIONS.EAST_US,
}
default:
throw new Error('Invalid cloud provider')
}
}
type ResolveDefaultDbRegionArgs = {
cloudProvider: CloudProvider
isHighAvailabilityRestricted: boolean
highAvailabilityRegionName: string | undefined
isSmartRegionEnabled: boolean
recommendedSmartRegion: string | undefined
autoDefaultRegion: string | undefined
fixedDefaultRegion: string
environment?: string
}
export function resolveDefaultDbRegion({
cloudProvider,
isHighAvailabilityRestricted,
highAvailabilityRegionName,
isSmartRegionEnabled,
recommendedSmartRegion,
autoDefaultRegion,
fixedDefaultRegion,
environment = process.env.NEXT_PUBLIC_ENVIRONMENT,
}: ResolveDefaultDbRegionArgs): string | undefined {
if (isHighAvailabilityRestricted) return highAvailabilityRegionName
if (isSmartRegionEnabled) return recommendedSmartRegion
// The geolocated default is only usable if the provider actually offers that region
// (e.g. AWS_NIMBUS is restricted to a single region)
const isAutoDefaultRegionAvailable = Object.entries(
getAvailableRegions(cloudProvider, environment)
).some(([, region]) => region.displayName === autoDefaultRegion)
return isAutoDefaultRegionAvailable && autoDefaultRegion !== undefined
? autoDefaultRegion
: fixedDefaultRegion
}
/**
* When launching new projects, they only get assigned a compute size once successfully launched,
* this might assume wrong compute size, but only for projects being rapidly launched after one another on non-default compute sizes.
*
* Needs to be in the API in the future [kevin]
*/
export const monthlyInstancePrice = (instance: string | undefined): number => {
return instanceSizeSpecs[instance as DesiredInstanceSize]?.priceMonthly || 10
}
export const instanceLabel = (instance: string | undefined): string => {
return instanceSizeSpecs[instance as DesiredInstanceSize]?.label || 'Micro'
}
export const getHighAvailabilityRegionCode = (
environment = process.env.NEXT_PUBLIC_ENVIRONMENT
) => {
// Local dev stacks can run in any of the supported regions, so they're left unrestricted
if (environment === 'prod') return 'us-east-1'
if (environment === 'staging') return 'us-east-1'
if (environment === 'local') return 'eu-central-1'
return undefined
}
export const filterHighAvailabilityRegions = <T extends { code: string }>(
regions: T[],
highAvailability: boolean,
environment = process.env.NEXT_PUBLIC_ENVIRONMENT
) => {
const isLocal = environment === 'local'
const regionCode = getHighAvailabilityRegionCode(environment)
return highAvailability && !isLocal && regionCode !== undefined
? regions.filter((region) => region.code === regionCode)
: regions
}
export type FreeTierGeneralRegionExperimentVariant = 'test' | 'control'
type GetFreeTierGeneralRegionExperimentVariantArgs = {
isFreePlan: boolean
smartRegionEnabled: boolean
enrollmentFlag: boolean
selectionFlag: boolean
/** When true, another feature owns the region selector and the experiment is inactive */
isOverridden?: boolean
}
/**
* Org's arm in the freeTierGeneralRegionExperiment. The test arm only sees general regions.
* Returns undefined when the org isn't enrolled, or when the experiment is overridden.
*/
export function getFreeTierGeneralRegionExperimentVariant({
isFreePlan,
smartRegionEnabled,
enrollmentFlag,
selectionFlag,
isOverridden = false,
}: GetFreeTierGeneralRegionExperimentVariantArgs):
| FreeTierGeneralRegionExperimentVariant
| undefined {
const isInExperiment =
!isOverridden && isFreePlan && smartRegionEnabled && enrollmentFlag === true
if (!isInExperiment) return undefined
return selectionFlag === true ? 'test' : 'control'
}
export type AvailableRegions = NonNullable<OrganizationAvailableRegionsData>['all']
type GetRegionSelectionTypeArgs = {
dbRegion: string | undefined
availableRegions: AvailableRegions | undefined
/** Whether the user ever touched the region field. Defaults to true for untracked callers. */
isDbRegionDirty?: boolean
}
/**
* How the submitted region was arrived at: the auto-assigned default left untouched ('auto'),
* a smart group region picked by the user (e.g. Americas, 'general'), or an exact region picked
* by the user (e.g. us-east-1, 'specific'). Returns undefined if dbRegion matches neither list,
* regardless of whether the field was touched.
*/
export function getRegionSelectionType({
dbRegion,
availableRegions,
isDbRegionDirty = true,
}: GetRegionSelectionTypeArgs): 'general' | 'auto' | 'specific' | undefined {
if (!dbRegion || !availableRegions) return undefined
const isGeneral = availableRegions.smartGroup.some((region) => region.name === dbRegion)
const isSpecific = availableRegions.specific.some((region) => region.name === dbRegion)
if (!isGeneral && !isSpecific) return undefined
if (!isDbRegionDirty) return 'auto'
return isGeneral ? 'general' : 'specific'
}
export type SpecificRegionsLinkVariant = 'test' | 'control'
type GetSpecificRegionsLinkVariantArgs = {
isFreePlan: boolean
smartRegionEnabled: boolean
linkFlag: boolean
}
/**
* Org's arm in the projectCreationSpecificRegionsLink experiment. Returns undefined when the
* org isn't in the eligible population (paid plan, or smart regions disabled for the selected
* cloud provider) — the link can never show for them regardless of the flag.
*/
export function getSpecificRegionsLinkVariant({
isFreePlan,
smartRegionEnabled,
linkFlag,
}: GetSpecificRegionsLinkVariantArgs): SpecificRegionsLinkVariant | undefined {
if (!isFreePlan || !smartRegionEnabled) return undefined
return linkFlag === true ? 'test' : 'control'
}
export interface PostgresVersionDetails {
postgresEngine?: Exclude<PostgresEngine, '13' | '14'>
releaseChannel?: ReleaseChannel
}
export const extractPostgresVersionDetails = (value: string): PostgresVersionDetails => {
if (!value) {
return { postgresEngine: undefined, releaseChannel: undefined }
}
const [postgresEngine, releaseChannel] = value.split('|')
return { postgresEngine, releaseChannel } as PostgresVersionDetails
}
/**
* Picks the engine + release channel to submit on project creation. The selector's value is used
* only when it matches the selected Postgres Type in Advanced Config. Otherwise, OrioleDB falls
* back to the available OrioleDB image, and Postgres falls back to the API default
*/
export function resolvePostgresVersion({
postgresVersionSelection,
useOrioleDb,
availableOrioleVersion,
}: {
postgresVersionSelection: string
useOrioleDb: boolean
availableOrioleVersion?: {
postgres_engine: Exclude<PostgresEngine, '13' | '14'>
release_channel: ReleaseChannel
}
}): PostgresVersionDetails {
const selection = extractPostgresVersionDetails(postgresVersionSelection)
const isSelectionOriole = !!selection.postgresEngine?.endsWith('oriole')
if (useOrioleDb) {
if (isSelectionOriole) return selection
return {
postgresEngine: availableOrioleVersion?.postgres_engine,
releaseChannel: availableOrioleVersion?.release_channel,
}
}
if (isSelectionOriole) return {}
return selection
}