diff --git a/apps/studio/components/interfaces/ProjectCreation/PostgresVersionSelector.tsx b/apps/studio/components/interfaces/ProjectCreation/PostgresVersionSelector.tsx new file mode 100644 index 00000000000..0056c482d01 --- /dev/null +++ b/apps/studio/components/interfaces/ProjectCreation/PostgresVersionSelector.tsx @@ -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 + form: UseFormReturn +} + +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 ( + + + + + + + + {(data?.available_versions || [])?.map((value) => { + const postgresVersion = value.version.split('supabase-postgres-')[1] + return ( + +
+ {postgresVersion} + {value.release_channel !== 'ga' && ( + + {value.release_channel} + + )} +
+
+ ) + })} +
+
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Settings/General/Infrastructure/ProjectUpgradeAlert/ProjectUpgradeAlert.tsx b/apps/studio/components/interfaces/Settings/General/Infrastructure/ProjectUpgradeAlert/ProjectUpgradeAlert.tsx index ed5daf18d15..ec6d0647d19 100644 --- a/apps/studio/components/interfaces/Settings/General/Infrastructure/ProjectUpgradeAlert/ProjectUpgradeAlert.tsx +++ b/apps/studio/components/interfaces/Settings/General/Infrastructure/ProjectUpgradeAlert/ProjectUpgradeAlert.tsx @@ -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) => { 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>({ + 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 ( <> @@ -86,144 +147,146 @@ const ProjectUpgradeAlert = () => { setShowUpgradeModal(false)} - header={

Upgrade Postgres

} + header="Confirm to upgrade Postgres version" > -
- {({ values }: { values: any }) => { - const selectedVersion = (data?.target_upgrade_versions ?? []).find( - (x) => x.postgres_version === values.version - ) + + + + +
+

+ All services will be offline and you will not be able to downgrade back to + Postgres {currentPgVersion}. +

+ {(data?.potential_breaking_changes ?? []).length > 0 && ( + + + Breaking changes + +

+ Your project will be upgraded across major versions of Postgres. This may + involve breaking changes. +

- return ( - <> - -
-

All services are going offline.

-

- You will not be able to downgrade back to Postgres {currentPgVersion}. -

- - - - Your project will be offline for up to {durationEstimateHours} hour - {durationEstimateHours === 1 ? '' : 's'} - - -

- It is advised to upgrade at a time when there will be minimal impact for - your application. -

-
-
- {(data?.potential_breaking_changes ?? []).length > 0 && ( - - - Breaking changes - -

- Your project will be upgraded across major versions of Postgres. This - may involve breaking changes. -

- -
- -
-
-
- )} - {legacyAuthCustomRoles.length > 0 && ( - - - - Custom Postgres roles will not work automatically after upgrade - - -

You must run a series of commands after upgrading.

-

- This is because new Postgres versions use scram-sha-256 authentication - by default and do not support md5, as it has been deprecated. -

-
-

Run the following commands after the upgrade:

-
- - {legacyAuthCustomRoles.map((role) => ( -
- ALTER ROLE {role} WITH - PASSWORD 'newpassword'; -
- ))} -
-
-
-
- -
-
-
- )} - - {data?.target_upgrade_versions.map((version) => ( - - {version.postgres_version} ( - {version.app_version.split('supabase-postgres-')[1]}) - - ))} - -
-
- - - +
+ + + )} + {legacyAuthCustomRoles.length > 0 && ( + - Cancel - - -
- - ) - }} - + + + Custom Postgres roles will not work automatically after upgrade + + +

You must run a series of commands after upgrading.

+

+ This is because new Postgres versions use scram-sha-256 authentication by + default and do not support md5, as it has been deprecated. +

+
+

Run the following commands after the upgrade:

+
+ + {legacyAuthCustomRoles.map((role) => ( +
+ ALTER ROLE {role} WITH PASSWORD + 'newpassword'; +
+ ))} +
+
+
+
+ +
+
+
+ )} + ( + + + + + + + + + {(data?.target_upgrade_versions || [])?.map((value) => { + const postgresVersion = + value.app_version.split('supabase-postgres-')[1] + return ( + +
+ {postgresVersion} + {value.release_channel !== 'ga' && ( + + {value.release_channel} + + )} +
+
+ ) + })} +
+
+
+
+
+ )} + /> + + + + + + + + + ) diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx index b3b7e5932ba..7b460d9804f 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx @@ -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 && ( + + + + {isOnNonGenerallyAvailableReleaseChannel} + + + + This project uses a {isOnNonGenerallyAvailableReleaseChannel} database + version release + + + ), isOnLatestVersion && ( diff --git a/apps/studio/components/layouts/ProjectLayout/PausedState/ProjectPausedState.tsx b/apps/studio/components/layouts/ProjectLayout/PausedState/ProjectPausedState.tsx index 9e5998bae80..ce40709d502 100644 --- a/apps/studio/components/layouts/ProjectLayout/PausedState/ProjectPausedState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/PausedState/ProjectPausedState.tsx @@ -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) => { 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>({ + 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 ( <>
@@ -224,15 +300,78 @@ export const ProjectPausedState = ({ product }: ProjectPausedStateProps) => {
- setShowConfirmRestore(false)} - onSelectConfirm={onConfirmRestore} - /> + onCancel={() => setShowConfirmRestore(false)} + header={'Restore this project'} + > + +
+ {!projectVersionSelectionDisabled && ( + +
+ ( + + + + + + + + + {(availablePostgresVersions?.available_versions || [])?.map( + (value) => { + const postgresVersion = + value.version.split('supabase-postgres-')[1] + return ( + +
+ {postgresVersion} + {value.release_channel !== 'ga' && ( + + {value.release_channel} + + )} +
+
+ ) + } + )} +
+
+
+
+
+ )} + /> +
+
+ )} + + + + +
+
+ { 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, diff --git a/apps/studio/data/config/keys.ts b/apps/studio/data/config/keys.ts index c0b37ff5210..6f4b61da217 100644 --- a/apps/studio/data/config/keys.ts +++ b/apps/studio/data/config/keys.ts @@ -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, } diff --git a/apps/studio/data/config/project-creation-postgres-versions-query.ts b/apps/studio/data/config/project-creation-postgres-versions-query.ts new file mode 100644 index 00000000000..a54d42beb5e --- /dev/null +++ b/apps/studio/data/config/project-creation-postgres-versions-query.ts @@ -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 +> +export type ProjectCreationPostgresVersionError = ResponseError + +export const useProjectCreationPostgresVersionsQuery = ( + { cloudProvider, dbRegion, organizationSlug }: ProjectCreationPostgresVersionsVariables, + { + enabled = true, + ...options + }: UseQueryOptions< + ProjectCreationPostgresVersionData, + ProjectCreationPostgresVersionError, + TData + > = {} +) => { + return useQuery( + configKeys.projectCreationPostgresVersions(organizationSlug, cloudProvider, dbRegion), + ({ signal }) => + getPostgresCreationVersions({ organizationSlug, cloudProvider, dbRegion }, signal), + { + enabled: enabled && typeof organizationSlug !== 'undefined', + ...options, + } + ) +} diff --git a/apps/studio/data/config/project-unpause-postgres-versions-query.ts b/apps/studio/data/config/project-unpause-postgres-versions-query.ts new file mode 100644 index 00000000000..e569fa09f3b --- /dev/null +++ b/apps/studio/data/config/project-unpause-postgres-versions-query.ts @@ -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 +> +export type ProjectUnpausePostgresVersionError = ResponseError + +export const useProjectUnpausePostgresVersionsQuery = ( + { projectRef }: ProjectUnpausePostgresVersionsVariables, + { + enabled = true, + ...options + }: UseQueryOptions< + ProjectUnpausePostgresVersionData, + ProjectUnpausePostgresVersionError, + TData + > = {} +) => { + return useQuery( + configKeys.projectUnpausePostgresVersions(projectRef), + ({ signal }) => getPostgresUnpauseVersions({ projectRef }, signal), + { + enabled: enabled && typeof projectRef !== 'undefined', + ...options, + } + ) +} diff --git a/apps/studio/data/config/project-upgrade-eligibility-query.ts b/apps/studio/data/config/project-upgrade-eligibility-query.ts index 34410e665bc..0da7ddc7440 100644 --- a/apps/studio/data/config/project-upgrade-eligibility-query.ts +++ b/apps/studio/data/config/project-upgrade-eligibility-query.ts @@ -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'] diff --git a/apps/studio/data/config/project-upgrade-status-query.ts b/apps/studio/data/config/project-upgrade-status-query.ts index fcaba8cba71..a4880d81502 100644 --- a/apps/studio/data/config/project-upgrade-status-query.ts +++ b/apps/studio/data/config/project-upgrade-status-query.ts @@ -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 = {} + 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( - { projectRef, projectStatus }: ProjectUpgradingStatusVariables, + { projectRef, projectStatus, trackingId }: ProjectUpgradingStatusVariables, { enabled = true, ...options @@ -38,7 +44,7 @@ export const useProjectUpgradingStatusQuery = ( configKeys.upgradeStatus(projectRef), - ({ signal }) => getProjectUpgradingStatus({ projectRef }, signal), + ({ signal }) => getProjectUpgradingStatus({ projectRef, trackingId }, signal), { enabled: enabled && typeof projectRef !== 'undefined', refetchInterval(data) { diff --git a/apps/studio/data/projects/project-create-mutation.ts b/apps/studio/data/projects/project-create-mutation.ts index 5e9a11d8d3d..5859bfd5033 100644 --- a/apps/studio/data/projects/project-create-mutation.ts +++ b/apps/studio/data/projects/project-create-mutation.ts @@ -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`, { diff --git a/apps/studio/data/projects/project-restore-mutation.ts b/apps/studio/data/projects/project-restore-mutation.ts index 5747e34b332..6b74010e400 100644 --- a/apps/studio/data/projects/project-restore-mutation.ts +++ b/apps/studio/data/projects/project-restore-mutation.ts @@ -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 diff --git a/apps/studio/data/projects/project-upgrade-mutation.ts b/apps/studio/data/projects/project-upgrade-mutation.ts index 0cf93bb57a7..483e94fbc61 100644 --- a/apps/studio/data/projects/project-upgrade-mutation.ts +++ b/apps/studio/data/projects/project-upgrade-mutation.ts @@ -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 diff --git a/apps/studio/next-env.d.ts b/apps/studio/next-env.d.ts index a4a7b3f5cfa..4f11a03dc6c 100644 --- a/apps/studio/next-env.d.ts +++ b/apps/studio/next-env.d.ts @@ -2,4 +2,4 @@ /// // 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. diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx index 7399098ecf5..cf59f410810 100644 --- a/apps/studio/pages/new/[slug].tsx +++ b/apps/studio/pages/new/[slug].tsx @@ -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 = () => { /> - {showNonProdFields && ( - - ( - - - - - - )} - /> - - )} - {cloudProviderEnabled && showNonProdFields && ( { +
Compute Size -
+

Compute Add-Ons

+ +
+

Compute Billing

+ +
+
} @@ -698,26 +698,7 @@ const Wizard: NextPageWithLayout = () => { )} /> - - Compute Billing -
- -
-

Docs

- -
- -
-
- } - layout="horizontal" - > +
Additional Monthly Compute Costs
@@ -782,7 +763,7 @@ const Wizard: NextPageWithLayout = () => { - +
Compute Credits @@ -813,7 +794,7 @@ const Wizard: NextPageWithLayout = () => {
-
+

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 = () => { /> + {!projectVersionSelectionDisabled && ( + + ( + + )} + /> + + )} + + {showNonProdFields && ( + + ( + + + + + + )} + /> + + )} +