feat: support for multiple pg versions (#29473)

* feat: support multiple pg versions

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
Paul CioancaandJoshen Lim authored and Crispy1975 committed 2024-10-16 12:54:48 +01:00
1 parent 1ef75abc02
commit d255e15efd
15 files changed
+728 -217

No files matched your search

@@ -0,0 +1,102 @@
import { useEffect } from 'react'
import { ControllerRenderProps, UseFormReturn } from 'react-hook-form'
import { components } from 'api-types'
import {
ProjectCreationPostgresVersion,
useProjectCreationPostgresVersionsQuery,
} from 'data/config/project-creation-postgres-versions-query'
import type { CloudProvider } from 'shared-data'
import {
Badge,
SelectContent_Shadcn_,
SelectGroup_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
Select_Shadcn_,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
type ReleaseChannel = components['schemas']['ReleaseChannel']
type PostgresEngine = components['schemas']['PostgresEngine']
interface PostgresVersionDetails {
postgresEngine: PostgresEngine | undefined
releaseChannel: ReleaseChannel | undefined
}
interface PostgresVersionSelectorProps {
cloudProvider: CloudProvider
dbRegion: string
organizationSlug: string | undefined
field: ControllerRenderProps<any, 'postgresVersionSelection'>
form: UseFormReturn<any>
}
const formatValue = ({ postgres_engine, release_channel }: ProjectCreationPostgresVersion) => {
return `${postgres_engine}|${release_channel}`
}
export const extractPostgresVersionDetails = (value: string): PostgresVersionDetails => {
if (!value) {
return { postgresEngine: undefined, releaseChannel: undefined }
}
const [postgresEngine, releaseChannel] = value.split('|')
return { postgresEngine, releaseChannel } as PostgresVersionDetails
}
export const PostgresVersionSelector = ({
cloudProvider,
dbRegion,
organizationSlug,
field,
form,
}: PostgresVersionSelectorProps) => {
const { data, isLoading: isLoadingProjectVersions } = useProjectCreationPostgresVersionsQuery({
cloudProvider,
dbRegion,
organizationSlug,
})
useEffect(() => {
const defaultValue = data?.available_versions?.[0]
? formatValue(data.available_versions[0])
: undefined
form.setValue('postgresVersionSelection', defaultValue)
}, [data, form])
return (
<FormItemLayout layout="horizontal" label="Postgres Version">
<Select_Shadcn_
value={field.value}
onValueChange={field.onChange}
disabled={isLoadingProjectVersions}
>
<SelectTrigger_Shadcn_>
<SelectValue_Shadcn_ placeholder="Select a Postgres version for your project" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
{(data?.available_versions || [])?.map((value) => {
const postgresVersion = value.version.split('supabase-postgres-')[1]
return (
<SelectItem_Shadcn_ key={formatValue(value)} value={formatValue(value)}>
<div className="flex items-center gap-3">
<span className="text-foreground">{postgresVersion}</span>
{value.release_channel !== 'ga' && (
<Badge variant="warning" className="mr-1 capitalize">
{value.release_channel}
</Badge>
)}
</div>
</SelectItem_Shadcn_>
)
})}
</SelectGroup_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
</FormItemLayout>
)
}
@@ -1,28 +1,62 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { useQueryClient } from '@tanstack/react-query'
import { AlertCircle, AlertTriangle } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { z } from 'zod'
import { useParams } from 'common'
import { useProjectUpgradeEligibilityQuery } from 'data/config/project-upgrade-eligibility-query'
import { useProjectUpgradeMutation } from 'data/projects/project-upgrade-mutation'
import {
ProjectUpgradeTargetVersion,
useProjectUpgradeEligibilityQuery,
} from 'data/config/project-upgrade-eligibility-query'
import { ReleaseChannel, useProjectUpgradeMutation } from 'data/projects/project-upgrade-mutation'
import { setProjectStatus } from 'data/projects/projects-query'
import { useFlag } from 'hooks/ui/useFlag'
import { PROJECT_STATUS } from 'lib/constants'
import { Admonition } from 'ui-patterns/admonition'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
Badge,
Button,
Form,
Listbox,
FormControl_Shadcn_,
FormField_Shadcn_,
Form_Shadcn_,
Modal,
SelectContent_Shadcn_,
SelectGroup_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
Select_Shadcn_,
TooltipContent_Shadcn_,
TooltipTrigger_Shadcn_,
Tooltip_Shadcn_,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
interface PostgresVersionDetails {
postgresEngine: string
releaseChannel: ReleaseChannel
}
const formatValue = ({ postgres_version, release_channel }: ProjectUpgradeTargetVersion) => {
return `${postgres_version}|${release_channel}`
}
export const extractPostgresVersionDetails = (value: string): PostgresVersionDetails | null => {
if (!value) {
return null
}
const [postgresEngine, releaseChannel] = value.split('|')
return { postgresEngine, releaseChannel } as PostgresVersionDetails
}
const ProjectUpgradeAlert = () => {
const router = useRouter()
@@ -32,7 +66,6 @@ const ProjectUpgradeAlert = () => {
const projectUpgradeDisabled = useFlag('disableProjectUpgrade')
const formId = 'project-upgrade-form'
const { data } = useProjectUpgradeEligibilityQuery({ projectRef: ref })
const currentPgVersion = (data?.current_app_version ?? '').split('supabase-postgres-')[1]
const latestPgVersion = (data?.latest_app_version ?? '').split('supabase-postgres-')[1]
@@ -40,20 +73,48 @@ const ProjectUpgradeAlert = () => {
const durationEstimateHours = data?.duration_estimate_hours || 1
const legacyAuthCustomRoles = data?.legacy_auth_custom_roles || []
const initialValues = { version: data?.target_upgrade_versions?.[0]?.postgres_version ?? 0 }
const { mutate: upgradeProject, isLoading: isUpgrading } = useProjectUpgradeMutation({
onSuccess: (res, variables) => {
setProjectStatus(queryClient, variables.ref, PROJECT_STATUS.UPGRADING)
toast.success('Upgrading project')
router.push(`/project/${variables.ref}?upgradeInitiated=true`)
router.push(`/project/${variables.ref}?upgradeInitiated=true&trackingId=${res.tracking_id}`)
},
})
const onConfirmUpgrade = async (values: any) => {
const onConfirmUpgrade = async (values: z.infer<typeof FormSchema>) => {
if (!ref) return toast.error('Project ref not found')
upgradeProject({ ref, target_version: values.version })
const { postgresVersionSelection } = values
const versionDetails = extractPostgresVersionDetails(postgresVersionSelection)
if (!versionDetails) return toast.error('Invalid Postgres version')
upgradeProject({
ref,
target_version: versionDetails.postgresEngine,
release_channel: versionDetails.releaseChannel,
})
}
const FormSchema = z.object({
postgresVersionSelection: z.string(),
})
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
mode: 'onChange',
defaultValues: {
postgresVersionSelection: '',
},
})
useEffect(() => {
const defaultValue = data?.target_upgrade_versions?.[0]
? formatValue(data.target_upgrade_versions[0])
: ''
form.setValue('postgresVersionSelection', defaultValue)
}, [data, form])
return (
<>
<Alert_Shadcn_ title="Your project can be upgraded to the latest version of Postgres">
@@ -86,144 +147,146 @@ const ProjectUpgradeAlert = () => {
<Modal
hideFooter
size="small"
visible={showUpgradeModal}
onCancel={() => setShowUpgradeModal(false)}
header={<h3>Upgrade Postgres</h3>}
header="Confirm to upgrade Postgres version"
>
<Form id={formId} initialValues={initialValues} onSubmit={onConfirmUpgrade}>
{({ values }: { values: any }) => {
const selectedVersion = (data?.target_upgrade_versions ?? []).find(
(x) => x.postgres_version === values.version
)
<Form_Shadcn_ {...form}>
<form onSubmit={form.handleSubmit(onConfirmUpgrade)}>
<Admonition
type="warning"
className="border-x-0 border-t-0 rounded-none"
title={`Your project will be offline for up to ${durationEstimateHours} hour${durationEstimateHours === 1 ? '' : 's'}`}
description="It is advised to upgrade at a time when there will be minimal impact for your application."
/>
<Modal.Content>
<div className="space-y-4">
<p className="text-sm">
All services will be offline and you will not be able to downgrade back to
Postgres {currentPgVersion}.
</p>
{(data?.potential_breaking_changes ?? []).length > 0 && (
<Alert_Shadcn_ variant="destructive" title="Breaking changes">
<AlertCircle className="h-4 w-4" strokeWidth={2} />
<AlertTitle_Shadcn_>Breaking changes</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_ className="flex flex-col gap-3">
<p>
Your project will be upgraded across major versions of Postgres. This may
involve breaking changes.
</p>
return (
<>
<Modal.Content>
<div className="space-y-4">
<p className="text-sm">All services are going offline.</p>
<p className="text-sm">
You will not be able to downgrade back to Postgres {currentPgVersion}.
</p>
<Alert_Shadcn_ title="Your project will be offline while the upgrade is in progress">
<AlertCircle className="h-4 w-4" strokeWidth={2} />
<AlertTitle_Shadcn_>
Your project will be offline for up to {durationEstimateHours} hour
{durationEstimateHours === 1 ? '' : 's'}
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
<p>
It is advised to upgrade at a time when there will be minimal impact for
your application.
</p>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
{(data?.potential_breaking_changes ?? []).length > 0 && (
<Alert_Shadcn_ variant="destructive" title="Breaking changes">
<AlertCircle className="h-4 w-4" strokeWidth={2} />
<AlertTitle_Shadcn_>Breaking changes</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_ className="flex flex-col gap-3">
<p>
Your project will be upgraded across major versions of Postgres. This
may involve breaking changes.
</p>
<div>
<Button size="tiny" type="default" asChild>
<Link
href="https://supabase.com/docs/guides/platform/migrating-and-upgrading-projects#caveats"
target="_blank"
rel="noreferrer"
>
View docs
</Link>
</Button>
</div>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
)}
{legacyAuthCustomRoles.length > 0 && (
<Alert_Shadcn_
variant="warning"
title="Custom Postgres roles using md5 authentication have been detected"
>
<AlertTriangle className="h-4 w-4" strokeWidth={2} />
<AlertTitle_Shadcn_>
Custom Postgres roles will not work automatically after upgrade
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_ className="flex flex-col gap-3">
<p>You must run a series of commands after upgrading.</p>
<p>
This is because new Postgres versions use scram-sha-256 authentication
by default and do not support md5, as it has been deprecated.
</p>
<div>
<p className="mb-1">Run the following commands after the upgrade:</p>
<div className="flex items-baseline gap-2">
<code className="text-xs">
{legacyAuthCustomRoles.map((role) => (
<div key={role} className="pb-1">
ALTER ROLE <span className="text-brand">{role}</span> WITH
PASSWORD '<span className="text-brand">newpassword</span>';
</div>
))}
</code>
</div>
</div>
<div>
<Button size="tiny" type="default" asChild>
<Link
href="https://supabase.com/docs/guides/platform/migrating-and-upgrading-projects#caveats"
target="_blank"
rel="noreferrer"
>
View docs
</Link>
</Button>
</div>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
)}
<Listbox
id="version"
name="version"
label="Select the version of Postgres to upgrade to"
descriptionText={`Postgres will be upgraded from ${currentPgVersion} to ${
selectedVersion?.app_version?.split('supabase-postgres-')[1] ??
values.version
}`}
>
{data?.target_upgrade_versions.map((version) => (
<Listbox.Option
key={version.postgres_version}
value={version.postgres_version}
label={`${version.postgres_version} (${
version.app_version.split('supabase-postgres-')[1]
})`}
>
{version.postgres_version} (
{version.app_version.split('supabase-postgres-')[1]})
</Listbox.Option>
))}
</Listbox>
</div>
</Modal.Content>
<Modal.Separator />
<Modal.Content className="flex items-center space-x-2 justify-end">
<Button
type="default"
onClick={() => setShowUpgradeModal(false)}
disabled={isUpgrading}
<div>
<Button size="tiny" type="default" asChild>
<Link
href="https://supabase.com/docs/guides/platform/migrating-and-upgrading-projects#caveats"
target="_blank"
rel="noreferrer"
>
View docs
</Link>
</Button>
</div>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
)}
{legacyAuthCustomRoles.length > 0 && (
<Alert_Shadcn_
variant="warning"
title="Custom Postgres roles using md5 authentication have been detected"
>
Cancel
</Button>
<Button htmlType="submit" disabled={isUpgrading} loading={isUpgrading}>
Confirm upgrade
</Button>
</Modal.Content>
</>
)
}}
</Form>
<AlertTriangle className="h-4 w-4" strokeWidth={2} />
<AlertTitle_Shadcn_>
Custom Postgres roles will not work automatically after upgrade
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_ className="flex flex-col gap-3">
<p>You must run a series of commands after upgrading.</p>
<p>
This is because new Postgres versions use scram-sha-256 authentication by
default and do not support md5, as it has been deprecated.
</p>
<div>
<p className="mb-1">Run the following commands after the upgrade:</p>
<div className="flex items-baseline gap-2">
<code className="text-xs">
{legacyAuthCustomRoles.map((role) => (
<div key={role} className="pb-1">
ALTER ROLE <span className="text-brand">{role}</span> WITH PASSWORD
'<span className="text-brand">newpassword</span>';
</div>
))}
</code>
</div>
</div>
<div>
<Button size="tiny" type="default" asChild>
<Link
href="https://supabase.com/docs/guides/platform/migrating-and-upgrading-projects#caveats"
target="_blank"
rel="noreferrer"
>
View docs
</Link>
</Button>
</div>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
)}
<FormField_Shadcn_
control={form.control}
name="postgresVersionSelection"
render={({ field }) => (
<FormItemLayout label="Select the version of Postgres to upgrade to">
<FormControl_Shadcn_>
<Select_Shadcn_ value={field.value} onValueChange={field.onChange}>
<SelectTrigger_Shadcn_>
<SelectValue_Shadcn_ placeholder="Select a Postgres version" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
{(data?.target_upgrade_versions || [])?.map((value) => {
const postgresVersion =
value.app_version.split('supabase-postgres-')[1]
return (
<SelectItem_Shadcn_
key={formatValue(value)}
value={formatValue(value)}
>
<div className="flex items-center gap-3">
<span className="text-foreground">{postgresVersion}</span>
{value.release_channel !== 'ga' && (
<Badge variant="warning" className="mr-1 capitalize">
{value.release_channel}
</Badge>
)}
</div>
</SelectItem_Shadcn_>
)
})}
</SelectGroup_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</div>
</Modal.Content>
<Modal.Separator />
<Modal.Content className="flex items-center space-x-2 justify-end">
<Button
type="default"
onClick={() => setShowUpgradeModal(false)}
disabled={isUpgrading}
>
Cancel
</Button>
<Button htmlType="submit" disabled={isUpgrading} loading={isUpgrading}>
Confirm upgrade
</Button>
</Modal.Content>
</form>
</Form_Shadcn_>
</Modal>
</>
)
@@ -42,9 +42,14 @@ const InfrastructureInfo = () => {
projectRef: ref,
})
const { data: databases } = useReadReplicasQuery({ projectRef: ref })
const { current_app_version, latest_app_version } = data || {}
const { current_app_version, current_app_version_release_channel, latest_app_version } =
data || {}
const isOnLatestVersion = current_app_version === latest_app_version
const currentPgVersion = (current_app_version ?? '').split('supabase-postgres-')[1]
const isOnNonGenerallyAvailableReleaseChannel =
current_app_version_release_channel && current_app_version_release_channel !== 'ga'
? current_app_version_release_channel
: undefined
const latestPgVersion = (latest_app_version ?? '').split('supabase-postgres-')[1]
const isInactive = project?.status === 'INACTIVE'
@@ -106,6 +111,19 @@ const InfrastructureInfo = () => {
value={currentPgVersion}
label="Postgres version"
actions={[
isOnNonGenerallyAvailableReleaseChannel && (
<Tooltip_Shadcn_>
<TooltipTrigger_Shadcn_>
<Badge variant="warning" className="mr-1 capitalize">
{isOnNonGenerallyAvailableReleaseChannel}
</Badge>
</TooltipTrigger_Shadcn_>
<TooltipContent_Shadcn_ side="bottom" className="w-44 text-center">
This project uses a {isOnNonGenerallyAvailableReleaseChannel} database
version release
</TooltipContent_Shadcn_>
</Tooltip_Shadcn_>
),
isOnLatestVersion && (
<Tooltip_Shadcn_>
<TooltipTrigger_Shadcn_>
@@ -1,14 +1,23 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useQueryClient } from '@tanstack/react-query'
import dayjs from 'dayjs'
import { ExternalLink, PauseCircle } from 'lucide-react'
import Link from 'next/link'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { z } from 'zod'
import { useParams } from 'common'
import AlertError from 'components/ui/AlertError'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import {
PostgresEngine,
ProjectUnpausePostgresVersion,
ReleaseChannel,
useProjectUnpausePostgresVersionsQuery,
} from 'data/config/project-unpause-postgres-versions-query'
import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query'
import { useProjectPauseStatusQuery } from 'data/projects/project-pause-status-query'
import { useProjectRestoreMutation } from 'data/projects/project-restore-mutation'
@@ -18,8 +27,24 @@ import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useFlag } from 'hooks/ui/useFlag'
import { PROJECT_STATUS } from 'lib/constants'
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, Modal } from 'ui'
import ConfirmModal from 'ui-patterns/Dialogs/ConfirmDialog'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
Badge,
Button,
FormControl_Shadcn_,
FormField_Shadcn_,
Form_Shadcn_,
Modal,
SelectContent_Shadcn_,
SelectGroup_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
Select_Shadcn_,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { PauseDisabledState } from './PauseDisabledState'
import { RestorePaidPlanProjectNotice } from '../RestorePaidPlanProjectNotice'
@@ -29,12 +54,27 @@ export interface ProjectPausedStateProps {
product?: string
}
interface PostgresVersionDetails {
postgresEngine: PostgresEngine
releaseChannel: ReleaseChannel
}
const formatValue = ({ postgres_engine, release_channel }: ProjectUnpausePostgresVersion) => {
return `${postgres_engine}|${release_channel}`
}
export const extractPostgresVersionDetails = (value: string): PostgresVersionDetails => {
const [postgresEngine, releaseChannel] = value.split('|')
return { postgresEngine, releaseChannel } as PostgresVersionDetails
}
export const ProjectPausedState = ({ product }: ProjectPausedStateProps) => {
const { ref } = useParams()
const queryClient = useQueryClient()
const { project } = useProjectContext()
const selectedOrganization = useSelectedOrganization()
const enforceNinetyDayUnpauseExpiry = useFlag('enforceNinetyDayUnpauseExpiry')
const projectVersionSelectionDisabled = useFlag('disableProjectVersionSelection')
const orgSlug = selectedOrganization?.slug
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug })
@@ -64,11 +104,15 @@ export const ProjectPausedState = ({ product }: ProjectPausedStateProps) => {
{ enabled: isFreePlan }
)
const { data: availablePostgresVersions } = useProjectUnpausePostgresVersionsQuery({
projectRef: project?.ref,
})
const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0
const [showConfirmRestore, setShowConfirmRestore] = useState(false)
const [showFreeProjectLimitWarning, setShowFreeProjectLimitWarning] = useState(false)
const { mutate: restoreProject } = useProjectRestoreMutation({
const { mutate: restoreProject, isLoading: isRestoring } = useProjectRestoreMutation({
onSuccess: (_, variables) => {
setProjectStatus(queryClient, variables.ref, PROJECT_STATUS.RESTORING)
toast.success('Restoring project')
@@ -87,13 +131,45 @@ export const ProjectPausedState = ({ product }: ProjectPausedStateProps) => {
else setShowConfirmRestore(true)
}
const onConfirmRestore = () => {
const onConfirmRestore = async (values: z.infer<typeof FormSchema>) => {
if (!project) {
return toast.error('Unable to restore: project is required')
}
restoreProject({ ref: project.ref })
if (projectVersionSelectionDisabled) {
restoreProject({ ref: project.ref })
} else {
const { postgresVersionSelection } = values
const postgresVersionDetails = extractPostgresVersionDetails(postgresVersionSelection)
restoreProject({
ref: project.ref,
releaseChannel: postgresVersionDetails.releaseChannel,
postgresEngine: postgresVersionDetails.postgresEngine,
})
}
}
const FormSchema = z.object({
postgresVersionSelection: z.string(),
})
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
mode: 'onChange',
defaultValues: {
postgresVersionSelection: '',
},
})
useEffect(() => {
const defaultValue = availablePostgresVersions?.available_versions[0]
? formatValue(availablePostgresVersions?.available_versions[0])
: ''
form.setValue('postgresVersionSelection', defaultValue)
}, [availablePostgresVersions, form])
return (
<>
<div className="space-y-4">
@@ -224,15 +300,78 @@ export const ProjectPausedState = ({ product }: ProjectPausedStateProps) => {
</div>
</div>
<ConfirmModal
<Modal
hideFooter
visible={showConfirmRestore}
size={'small'}
title="Restore this project"
description="Confirm to restore this project? Your project's data will be restored to when it was initially paused."
buttonLabel="Restore project"
buttonLoadingLabel="Restoring project"
onSelectCancel={() => setShowConfirmRestore(false)}
onSelectConfirm={onConfirmRestore}
/>
onCancel={() => setShowConfirmRestore(false)}
header={'Restore this project'}
>
<Form_Shadcn_ {...form}>
<form onSubmit={form.handleSubmit(onConfirmRestore)}>
{!projectVersionSelectionDisabled && (
<Modal.Content>
<div className="space-y-2">
<FormField_Shadcn_
control={form.control}
name="postgresVersionSelection"
render={({ field }) => (
<FormItemLayout label="Select the version of Postgres to restore to">
<FormControl_Shadcn_>
<Select_Shadcn_ value={field.value} onValueChange={field.onChange}>
<SelectTrigger_Shadcn_>
<SelectValue_Shadcn_ placeholder="Select a Postgres version" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
{(availablePostgresVersions?.available_versions || [])?.map(
(value) => {
const postgresVersion =
value.version.split('supabase-postgres-')[1]
return (
<SelectItem_Shadcn_
key={formatValue(value)}
value={formatValue(value)}
>
<div className="flex items-center gap-3">
<span className="text-foreground">{postgresVersion}</span>
{value.release_channel !== 'ga' && (
<Badge variant="warning" className="mr-1 capitalize">
{value.release_channel}
</Badge>
)}
</div>
</SelectItem_Shadcn_>
)
}
)}
</SelectGroup_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</div>
</Modal.Content>
)}
<Modal.Content className="flex items-center space-x-2 justify-end">
<Button
type="default"
disabled={isRestoring}
onClick={() => setShowConfirmRestore(false)}
>
Cancel
</Button>
<Button htmlType="submit" loading={isRestoring}>
Confirm upgrade
</Button>
</Modal.Content>
</form>
</Form_Shadcn_>
</Modal>
<Modal
hideFooter
@@ -22,9 +22,11 @@ import {
Loader,
Check,
} from 'lucide-react'
import { useSearchParams } from 'next/navigation'
const UpgradingState = () => {
const { ref } = useParams()
const queryParams = useSearchParams()
const queryClient = useQueryClient()
const { project } = useProjectContext()
const [loading, setLoading] = useState(false)
@@ -33,6 +35,7 @@ const UpgradingState = () => {
{
projectRef: ref,
projectStatus: project?.status,
trackingId: queryParams.get('trackingId'),
},
{
enabled: IS_PLATFORM,
+8
View File
@@ -15,4 +15,12 @@ export const configKeys = {
['projects', projectRef, 'disk-attributes'] as const,
diskUtilization: (projectRef: string | undefined) =>
['projects', projectRef, 'disk-utilization'] as const,
projectCreationPostgresVersions: (
organizationSlug: string | undefined,
cloudProvider: string,
dbRegion: string
) =>
['projects', organizationSlug, cloudProvider, dbRegion, 'available-creation-versions'] as const,
projectUnpausePostgresVersions: (projectRef: string | undefined) =>
['projects', projectRef, 'available-unpause-versions'] as const,
}
@@ -0,0 +1,62 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { components } from 'api-types'
import { handleError, post } from 'data/fetchers'
import { CloudProvider } from 'shared-data'
import type { ResponseError } from 'types'
import { configKeys } from './keys'
export type ProjectCreationPostgresVersionsVariables = {
cloudProvider: CloudProvider
dbRegion: string
organizationSlug: string | undefined
}
export type ProjectCreationPostgresVersion = components['schemas']['ProjectCreationVersionInfo']
export type ProjectCreationPostgresVersionsResponse = {
available_versions: ProjectCreationPostgresVersion[]
}
export async function getPostgresCreationVersions(
{ cloudProvider, dbRegion, organizationSlug }: ProjectCreationPostgresVersionsVariables,
signal?: AbortSignal
) {
if (!organizationSlug) throw new Error('organizationSlug is required')
const { data, error } = await post('/platform/organizations/{slug}/available-versions', {
params: { path: { slug: organizationSlug } },
body: { provider: cloudProvider, region: dbRegion },
signal,
})
if (error) handleError(error)
return data as ProjectCreationPostgresVersionsResponse
}
export type ProjectCreationPostgresVersionData = Awaited<
ReturnType<typeof getPostgresCreationVersions>
>
export type ProjectCreationPostgresVersionError = ResponseError
export const useProjectCreationPostgresVersionsQuery = <TData = ProjectCreationPostgresVersionData>(
{ cloudProvider, dbRegion, organizationSlug }: ProjectCreationPostgresVersionsVariables,
{
enabled = true,
...options
}: UseQueryOptions<
ProjectCreationPostgresVersionData,
ProjectCreationPostgresVersionError,
TData
> = {}
) => {
return useQuery<ProjectCreationPostgresVersionData, ProjectCreationPostgresVersionError, TData>(
configKeys.projectCreationPostgresVersions(organizationSlug, cloudProvider, dbRegion),
({ signal }) =>
getPostgresCreationVersions({ organizationSlug, cloudProvider, dbRegion }, signal),
{
enabled: enabled && typeof organizationSlug !== 'undefined',
...options,
}
)
}
@@ -0,0 +1,59 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { get, handleError } from 'data/fetchers'
import { configKeys } from './keys'
import { components } from 'api-types'
import { ResponseError } from 'types'
export type ProjectUnpausePostgresVersionsVariables = {
projectRef?: string
}
export type ProjectUnpausePostgresVersion = components['schemas']['ProjectUnpauseVersionInfo']
export type ReleaseChannel = components['schemas']['ReleaseChannel']
export type PostgresEngine = components['schemas']['PostgresEngine']
export type ProjectUnpausePostgresVersionsResponse = {
available_versions: ProjectUnpausePostgresVersion[]
}
export async function getPostgresUnpauseVersions(
{ projectRef }: ProjectUnpausePostgresVersionsVariables,
signal?: AbortSignal
) {
if (!projectRef) throw new Error('projectRef is required')
const { data, error } = await get('/platform/projects/{ref}/restore/versions', {
params: { path: { ref: projectRef } },
signal,
})
if (error) handleError(error)
return data as ProjectUnpausePostgresVersionsResponse
}
export type ProjectUnpausePostgresVersionData = Awaited<
ReturnType<typeof getPostgresUnpauseVersions>
>
export type ProjectUnpausePostgresVersionError = ResponseError
export const useProjectUnpausePostgresVersionsQuery = <TData = ProjectUnpausePostgresVersionData>(
{ projectRef }: ProjectUnpausePostgresVersionsVariables,
{
enabled = true,
...options
}: UseQueryOptions<
ProjectUnpausePostgresVersionData,
ProjectUnpausePostgresVersionError,
TData
> = {}
) => {
return useQuery<ProjectUnpausePostgresVersionData, ProjectUnpausePostgresVersionError, TData>(
configKeys.projectUnpausePostgresVersions(projectRef),
({ signal }) => getPostgresUnpauseVersions({ projectRef }, signal),
{
enabled: enabled && typeof projectRef !== 'undefined',
...options,
}
)
}
@@ -8,6 +8,7 @@ import type { ResponseError } from 'types'
import { configKeys } from './keys'
import { components } from 'api-types'
export type ProjectUpgradeTargetVersion = components['schemas']['ProjectVersion']
export type ProjectUpgradeEligibilityVariables = { projectRef?: string }
export type ProjectUpgradeEligibilityResponse =
components['schemas']['ProjectUpgradeEligibilityResponse']
@@ -7,16 +7,22 @@ import { configKeys } from './keys'
export type ProjectUpgradingStatusVariables = {
projectRef?: string
projectStatus?: string
trackingId?: string | null
}
export async function getProjectUpgradingStatus(
{ projectRef }: ProjectUpgradingStatusVariables,
{ projectRef, trackingId }: ProjectUpgradingStatusVariables,
signal?: AbortSignal
) {
if (!projectRef) throw new Error('projectRef is required')
const queryParams: Record<string, string> = {}
if (trackingId) {
queryParams['tracking_id'] = trackingId
}
const { data, error } = await get(`/v1/projects/{ref}/upgrade/status`, {
params: { path: { ref: projectRef } },
params: { path: { ref: projectRef }, query: queryParams },
signal,
})
if (error) handleError(error)
@@ -28,7 +34,7 @@ export type ProjectUpgradingStatusData = Awaited<ReturnType<typeof getProjectUpg
export type ProjectUpgradingStatusError = unknown
export const useProjectUpgradingStatusQuery = <TData = ProjectUpgradingStatusData>(
{ projectRef, projectStatus }: ProjectUpgradingStatusVariables,
{ projectRef, projectStatus, trackingId }: ProjectUpgradingStatusVariables,
{
enabled = true,
...options
@@ -38,7 +44,7 @@ export const useProjectUpgradingStatusQuery = <TData = ProjectUpgradingStatusDat
return useQuery<ProjectUpgradingStatusData, ProjectUpgradingStatusError, TData>(
configKeys.upgradeStatus(projectRef),
({ signal }) => getProjectUpgradingStatus({ projectRef }, signal),
({ signal }) => getProjectUpgradingStatus({ projectRef, trackingId }, signal),
{
enabled: enabled && typeof projectRef !== 'undefined',
refetchInterval(data) {
@@ -18,6 +18,8 @@ const WHITELIST_ERRORS = [
]
export type DbInstanceSize = components['schemas']['DesiredInstanceSize']
export type ReleaseChannel = components['schemas']['ReleaseChannel']
export type PostgresEngine = components['schemas']['PostgresEngine']
export type ProjectCreateVariables = {
name: string
@@ -32,6 +34,8 @@ export type ProjectCreateVariables = {
dbInstanceSize?: DbInstanceSize
dataApiExposedSchemas?: string[]
dataApiUseApiSchema?: boolean
postgresEngine?: PostgresEngine
releaseChannel?: ReleaseChannel
}
export async function createProject({
@@ -46,6 +50,8 @@ export async function createProject({
dbInstanceSize,
dataApiExposedSchemas,
dataApiUseApiSchema,
postgresEngine,
releaseChannel,
}: ProjectCreateVariables) {
const body: components['schemas']['CreateProjectBody'] = {
cloud_provider: cloudProvider,
@@ -61,6 +67,8 @@ export async function createProject({
desired_instance_size: dbInstanceSize,
data_api_exposed_schemas: dataApiExposedSchemas,
data_api_use_api_schema: dataApiUseApiSchema,
postgres_engine: postgresEngine,
release_channel: releaseChannel,
}
const { data, error } = await post(`/platform/projects`, {
@@ -3,16 +3,28 @@ import { toast } from 'sonner'
import { handleError, post } from 'data/fetchers'
import type { ResponseError } from 'types'
import { components } from 'api-types'
export type ReleaseChannel = components['schemas']['ReleaseChannel']
export type PostgresEngine = components['schemas']['PostgresEngine']
export type ProjectRestoreVariables = {
ref: string
postgresEngine?: PostgresEngine
releaseChannel?: ReleaseChannel
}
export async function restoreProject({ ref }: ProjectRestoreVariables) {
export async function restoreProject({
ref,
postgresEngine,
releaseChannel,
}: ProjectRestoreVariables) {
const { data, error } = await post('/platform/projects/{ref}/restore', {
params: { path: { ref } },
// @ts-ignore
body: {},
body: {
postgres_engine: postgresEngine,
release_channel: releaseChannel,
},
})
if (error) handleError(error)
return data
@@ -3,17 +3,24 @@ import { toast } from 'sonner'
import { handleError, post } from 'data/fetchers'
import type { ResponseError } from 'types'
import { components } from 'api-types'
export type ReleaseChannel = components['schemas']['ReleaseChannel']
export type ProjectUpgradeVariables = {
ref: string
target_version: number
target_version: string
release_channel: ReleaseChannel
}
export async function upgradeProject({ ref, target_version }: ProjectUpgradeVariables) {
export async function upgradeProject({
ref,
target_version,
release_channel,
}: ProjectUpgradeVariables) {
const { data, error } = await post('/v1/projects/{ref}/upgrade', {
params: { path: { ref } },
// @ts-expect-error release_channel param is optional on API end, but not typed properly from generated types
body: { target_version: target_version.toString() },
body: { target_version: target_version.toString(), release_channel },
})
if (error) handleError(error)
return data
+1 -1
View File
@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
// see https://nextjs.org/docs/basic-features/typescript for more information.
+72 -49
View File
@@ -17,6 +17,10 @@ import {
FreeProjectLimitWarning,
NotOrganizationOwnerWarning,
} from 'components/interfaces/Organization/NewProject'
import {
PostgresVersionSelector,
extractPostgresVersionDetails,
} from 'components/interfaces/ProjectCreation/PostgresVersionSelector'
import { RegionSelector } from 'components/interfaces/ProjectCreation/RegionSelector'
import { WizardLayoutWithoutAuth } from 'components/layouts/WizardLayout'
import DisabledWarningDueToIncident from 'components/ui/DisabledWarningDueToIncident'
@@ -194,6 +198,7 @@ const Wizard: NextPageWithLayout = () => {
const { slug, projectName } = useParams()
const projectCreationDisabled = useFlag('disableProjectCreationAndUpdate')
const projectVersionSelectionDisabled = useFlag('disableProjectVersionSelection')
const cloudProviderEnabled = useFlag('enableFlyCloudProvider')
const { data: membersExceededLimit, isLoading: isLoadingFreeProjectLimitCheck } =
useFreeProjectLimitCheckQuery({ slug })
@@ -301,6 +306,7 @@ const Wizard: NextPageWithLayout = () => {
instanceSize: z.string(),
dataApi: z.boolean(),
useApiSchema: z.boolean(),
postgresVersionSelection: z.string(),
})
.superRefine(({ dbPassStrength }, refinementContext) => {
if (dbPassStrength < DEFAULT_MINIMUM_PASSWORD_STRENGTH) {
@@ -326,6 +332,7 @@ const Wizard: NextPageWithLayout = () => {
instanceSize: sizes[0],
dataApi: true,
useApiSchema: false,
postgresVersionSelection: '',
},
})
@@ -368,8 +375,12 @@ const Wizard: NextPageWithLayout = () => {
instanceSize,
dataApi,
useApiSchema,
postgresVersionSelection,
} = values
const { postgresEngine, releaseChannel } =
extractPostgresVersionDetails(postgresVersionSelection)
const data: ProjectCreateVariables = {
cloudProvider: cloudProvider,
organizationId: currentOrg.id,
@@ -383,6 +394,8 @@ const Wizard: NextPageWithLayout = () => {
orgSubscription?.plan.id === 'free' ? undefined : (instanceSize as DesiredInstanceSize),
dataApiExposedSchemas: !dataApi ? [] : undefined,
dataApiUseApiSchema: !dataApi ? false : useApiSchema,
postgresEngine: postgresEngine,
releaseChannel: releaseChannel,
}
if (postgresVersion) {
if (!postgresVersion.match(/1[2-9]\..*/)) {
@@ -546,30 +559,6 @@ const Wizard: NextPageWithLayout = () => {
/>
</Panel.Content>
{showNonProdFields && (
<Panel.Content>
<FormField_Shadcn_
control={form.control}
name="postgresVersion"
render={({ field }) => (
<FormItemLayout
label="Postgres version"
layout="horizontal"
description="Specify a custom version of Postgres (Defaults to the latest). This is only applicable for local/staging projects"
>
<FormControl_Shadcn_>
<Input_Shadcn_
placeholder="Postgres version"
{...field}
autoComplete="off"
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</Panel.Content>
)}
{cloudProviderEnabled && showNonProdFields && (
<Panel.Content>
<FormField_Shadcn_
@@ -623,19 +612,30 @@ const Wizard: NextPageWithLayout = () => {
<FormItemLayout
layout="horizontal"
label={
<div className="space-y-4">
<div className="flex flex-col gap-y-4">
<span>Compute Size</span>
<div className="flex flex-col space-y-2">
<div className="flex flex-col gap-y-2">
<Link
href="https://supabase.com/docs/guides/platform/compute-add-ons"
target="_blank"
rel="noopener noreferrer"
href="https://supabase.com/docs/guides/platform/compute-add-ons"
>
<div className="flex items-center space-x-2 opacity-75 hover:opacity-100 transition">
<p className="text-sm m-0">Compute Add-Ons</p>
<ExternalLink size={16} strokeWidth={1.5} />
</div>
</Link>
<Link
target="_blank"
rel="noopener noreferrer"
href="https://supabase.com/docs/guides/platform/org-based-billing#billing-for-compute-compute-hours"
>
<div className="flex items-center space-x-2 opacity-75 hover:opacity-100 transition">
<p className="text-sm m-0">Compute Billing</p>
<ExternalLink size={16} strokeWidth={1.5} />
</div>
</Link>
</div>
</div>
}
@@ -698,26 +698,7 @@ const Wizard: NextPageWithLayout = () => {
</FormItemLayout>
)}
/>
<FormItemLayout
className="pt-4"
label={
<div className="space-y-4">
<span>Compute Billing</span>
<div className="flex flex-col space-y-2">
<Link
href="https://supabase.com/docs/guides/platform/org-based-billing#billing-for-compute-compute-hours"
target="_blank"
>
<div className="flex items-center space-x-2 opacity-75 hover:opacity-100 transition">
<p className="text-sm m-0">Docs</p>
<ExternalLink size={16} strokeWidth={1.5} />
</div>
</Link>
</div>
</div>
}
layout="horizontal"
>
<FormItemLayout layout="horizontal">
<div className="flex justify-between mr-2">
<span>Additional Monthly Compute Costs</span>
<div className="text-brand flex gap-1 items-center">
@@ -782,7 +763,7 @@ const Wizard: NextPageWithLayout = () => {
</TableBody>
</Table>
<PopoverSeparator />
<Table className="mt-3">
<Table>
<TableHeader className="[&_th]:h-7">
<TableRow>
<TableHead colSpan={2}>Compute Credits</TableHead>
@@ -813,7 +794,7 @@ const Wizard: NextPageWithLayout = () => {
</TableBody>
</Table>
<div className="p-4 text-xs text-foreground-light mt-2 space-y-1">
<div className="p-4 text-xs text-foreground-light space-y-1">
<p>
Compute is charged usage-based whenever your billing cycle
resets. Given compute charges are hourly, your invoice will
@@ -913,6 +894,48 @@ const Wizard: NextPageWithLayout = () => {
/>
</Panel.Content>
{!projectVersionSelectionDisabled && (
<Panel.Content>
<FormField_Shadcn_
control={form.control}
name="postgresVersionSelection"
render={({ field }) => (
<PostgresVersionSelector
field={field}
form={form}
cloudProvider={form.getValues('cloudProvider') as CloudProvider}
organizationSlug={slug}
dbRegion={form.getValues('dbRegion')}
/>
)}
/>
</Panel.Content>
)}
{showNonProdFields && (
<Panel.Content>
<FormField_Shadcn_
control={form.control}
name="postgresVersion"
render={({ field }) => (
<FormItemLayout
label="Custom Postgres version"
layout="horizontal"
description="Specify a custom version of Postgres (Defaults to the latest). This is only applicable for local/staging projects"
>
<FormControl_Shadcn_>
<Input_Shadcn_
placeholder="Postgres version"
{...field}
autoComplete="off"
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</Panel.Content>
)}
<Panel.Content>
<Collapsible_Shadcn_>
<CollapsibleTrigger_Shadcn_ className="group/advanced-trigger font-mono uppercase tracking-widest text-xs flex items-center gap-1 text-foreground-lighter/75 hover:text-foreground-light transition data-[state=open]:text-foreground-light">