mirror of
https://github.com/supabase/supabase.git
synced 2026-05-07 23:19:23 +08:00
315 lines
11 KiB
TypeScript
315 lines
11 KiB
TypeScript
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { Loader2 } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { useEffect, useState } from 'react'
|
|
import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Button } from 'ui'
|
|
import { Admonition } from 'ui-patterns/admonition'
|
|
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
|
|
|
import { PreviousRestoreItem } from './PreviousRestoreItem'
|
|
import { PITRForm } from '@/components/interfaces/Database/Backups/PITR/PITRForm'
|
|
import { BackupsList } from '@/components/interfaces/Database/Backups/RestoreToNewProject/BackupsList'
|
|
import { ConfirmRestoreDialog } from '@/components/interfaces/Database/Backups/RestoreToNewProject/ConfirmRestoreDialog'
|
|
import { CreateNewProjectDialog } from '@/components/interfaces/Database/Backups/RestoreToNewProject/CreateNewProjectDialog'
|
|
import { projectSpecToMonthlyPrice } from '@/components/interfaces/Database/Backups/RestoreToNewProject/RestoreToNewProject.utils'
|
|
import { DiskType } from '@/components/interfaces/DiskManagement/ui/DiskManagement.constants'
|
|
import { Markdown } from '@/components/interfaces/Markdown'
|
|
import AlertError from '@/components/ui/AlertError'
|
|
import { InlineLink } from '@/components/ui/InlineLink'
|
|
import NoPermission from '@/components/ui/NoPermission'
|
|
import Panel from '@/components/ui/Panel'
|
|
import { UpgradeToPro } from '@/components/ui/UpgradeToPro'
|
|
import { useDiskAttributesQuery } from '@/data/config/disk-attributes-query'
|
|
import { useCloneBackupsQuery } from '@/data/projects/clone-query'
|
|
import { useCloneStatusQuery } from '@/data/projects/clone-status-query'
|
|
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
|
|
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
|
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
|
import {
|
|
useIsAwsK8sCloudProvider,
|
|
useIsOrioleDb,
|
|
useSelectedProjectQuery,
|
|
} from '@/hooks/misc/useSelectedProject'
|
|
import { DOCS_URL, PROJECT_STATUS } from '@/lib/constants'
|
|
import { getDatabaseMajorVersion } from '@/lib/helpers'
|
|
|
|
export const RestoreToNewProject = () => {
|
|
const { data: project } = useSelectedProjectQuery()
|
|
const { data: organization } = useSelectedOrganizationQuery()
|
|
const { hasAccess: hasAccessToRestoreToNewProject, isLoading: isLoadingEntitlement } =
|
|
useCheckEntitlements('backup.restore_to_new_project')
|
|
const isOrioleDb = useIsOrioleDb()
|
|
const isAwsK8s = useIsAwsK8sCloudProvider()
|
|
|
|
const [refetchInterval, setRefetchInterval] = useState<number | false>(false)
|
|
const [selectedBackupId, setSelectedBackupId] = useState<number | null>(null)
|
|
const [showConfirmationDialog, setShowConfirmationDialog] = useState(false)
|
|
const [showNewProjectDialog, setShowNewProjectDialog] = useState(false)
|
|
const [recoveryTimeTarget, setRecoveryTimeTarget] = useState<number | null>(null)
|
|
|
|
const {
|
|
data: cloneBackups,
|
|
error,
|
|
isPending: cloneBackupsLoading,
|
|
isError,
|
|
} = useCloneBackupsQuery(
|
|
{ projectRef: project?.ref },
|
|
{ enabled: hasAccessToRestoreToNewProject }
|
|
)
|
|
|
|
const isActiveHealthy = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY
|
|
|
|
const { can: canReadPhysicalBackups, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(
|
|
PermissionAction.READ,
|
|
'physical_backups'
|
|
)
|
|
const { can: canTriggerPhysicalBackups } = useAsyncCheckPermissions(
|
|
PermissionAction.INFRA_EXECUTE,
|
|
'queue_job.restore.prepare'
|
|
)
|
|
const PITR_ENABLED = cloneBackups?.pitr_enabled
|
|
const PHYSICAL_BACKUPS_ENABLED = project?.is_physical_backups_enabled
|
|
const dbVersion = getDatabaseMajorVersion(project?.dbVersion ?? '')
|
|
const IS_PG15_OR_ABOVE = dbVersion >= 15
|
|
const targetVolumeSizeGb = cloneBackups?.target_volume_size_gb
|
|
const targetComputeSize = cloneBackups?.target_compute_size
|
|
const planId = organization?.plan?.id ?? 'free'
|
|
const { data } = useDiskAttributesQuery({ projectRef: project?.ref })
|
|
const storageType = data?.attributes?.type ?? 'gp3'
|
|
|
|
const {
|
|
data: cloneStatus,
|
|
refetch: refetchCloneStatus,
|
|
isPending: cloneStatusLoading,
|
|
isSuccess: isCloneStatusSuccess,
|
|
} = useCloneStatusQuery(
|
|
{
|
|
projectRef: project?.ref,
|
|
},
|
|
{
|
|
refetchInterval,
|
|
refetchOnWindowFocus: false,
|
|
enabled: PHYSICAL_BACKUPS_ENABLED || PITR_ENABLED,
|
|
}
|
|
)
|
|
const isLoading = !isPermissionsLoaded || cloneBackupsLoading || cloneStatusLoading
|
|
|
|
useEffect(() => {
|
|
if (!isCloneStatusSuccess) return
|
|
const hasTransientState = cloneStatus.clones.some((c) => c.status === 'IN_PROGRESS')
|
|
if (!hasTransientState) {
|
|
setRefetchInterval(false)
|
|
}
|
|
}, [cloneStatus?.clones, isCloneStatusSuccess])
|
|
|
|
const previousClones = cloneStatus?.clones
|
|
const isRestoring = previousClones?.some((c) => c.status === 'IN_PROGRESS')
|
|
const restoringClone = previousClones?.find((c) => c.status === 'IN_PROGRESS')
|
|
|
|
if (isLoadingEntitlement) {
|
|
return <GenericSkeletonLoader />
|
|
}
|
|
|
|
if (!hasAccessToRestoreToNewProject) {
|
|
return (
|
|
<UpgradeToPro
|
|
buttonText="Upgrade"
|
|
source="backupsRestoreToNewProject"
|
|
featureProposition="enable restoring to new project"
|
|
primaryText="Restore to a new project requires Pro Plan and above"
|
|
secondaryText="To restore to a new project, you need to upgrade to a Pro Plan and have physical backups enabled."
|
|
/>
|
|
)
|
|
}
|
|
|
|
if (isOrioleDb) {
|
|
return (
|
|
<Admonition
|
|
type="default"
|
|
title="Restoring to new projects are not available for OrioleDB"
|
|
description="OrioleDB is currently in public alpha and projects created are strictly ephemeral with no database backups"
|
|
/>
|
|
)
|
|
}
|
|
|
|
if (isAwsK8s) {
|
|
return (
|
|
<Admonition
|
|
type="default"
|
|
title="Restoring to new projects is temporarily not available for AWS (Revamped) projects"
|
|
/>
|
|
)
|
|
}
|
|
|
|
if (!canReadPhysicalBackups) {
|
|
return <NoPermission resourceText="view backups" />
|
|
}
|
|
|
|
if (!canTriggerPhysicalBackups) {
|
|
return <NoPermission resourceText="restore backups" />
|
|
}
|
|
|
|
if (!IS_PG15_OR_ABOVE) {
|
|
return (
|
|
<Admonition
|
|
type="default"
|
|
title="Restore to new project is not available for this database version"
|
|
>
|
|
<Markdown
|
|
className="max-w-full"
|
|
content={`Restore to new project is only available for Postgres 15 and above.
|
|
Go to [infrastructure settings](/project/${project?.ref}/settings/infrastructure)
|
|
to upgrade your database version.
|
|
`}
|
|
/>
|
|
</Admonition>
|
|
)
|
|
}
|
|
|
|
if (!PHYSICAL_BACKUPS_ENABLED) {
|
|
return (
|
|
<Admonition
|
|
type="default"
|
|
title="Physical backups are required"
|
|
description={
|
|
<>
|
|
Physical backups must be enabled to restore your database to a new project.{' '}
|
|
<InlineLink href={`${DOCS_URL}/guides/platform/backups`}>Learn more</InlineLink>
|
|
</>
|
|
}
|
|
/>
|
|
)
|
|
}
|
|
|
|
if (isLoading) {
|
|
return <GenericSkeletonLoader />
|
|
}
|
|
|
|
if (isError) {
|
|
return <AlertError error={error} subject="Failed to retrieve backups" />
|
|
}
|
|
|
|
if (!isActiveHealthy) {
|
|
return (
|
|
<Admonition
|
|
type="default"
|
|
title="Restore to new project is not available while project is offline"
|
|
description="Your project needs to be online to restore your database to a new project"
|
|
/>
|
|
)
|
|
}
|
|
|
|
if (
|
|
!isLoading &&
|
|
PITR_ENABLED &&
|
|
!cloneBackups?.physicalBackupData.earliestPhysicalBackupDateUnix
|
|
) {
|
|
return (
|
|
<Admonition
|
|
type="default"
|
|
title="No backups found"
|
|
description="PITR is enabled, but no backups were found. Check again in a few minutes."
|
|
/>
|
|
)
|
|
}
|
|
|
|
if (!isLoading && !PITR_ENABLED && cloneBackups?.backups.length === 0) {
|
|
return (
|
|
<>
|
|
<Admonition
|
|
type="default"
|
|
title="No backups found"
|
|
description="Backups are enabled, but no backups were found. Check again tomorrow."
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
const additionalMonthlySpend = projectSpecToMonthlyPrice({
|
|
targetVolumeSizeGb: targetVolumeSizeGb ?? 0,
|
|
targetComputeSize: targetComputeSize ?? 'nano',
|
|
planId: planId ?? 'free',
|
|
storageType: storageType as DiskType,
|
|
})
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
<ConfirmRestoreDialog
|
|
open={showConfirmationDialog}
|
|
onOpenChange={setShowConfirmationDialog}
|
|
onSelectContinue={() => {
|
|
setShowConfirmationDialog(false)
|
|
setShowNewProjectDialog(true)
|
|
}}
|
|
additionalMonthlySpend={additionalMonthlySpend}
|
|
/>
|
|
<CreateNewProjectDialog
|
|
open={showNewProjectDialog}
|
|
selectedBackupId={selectedBackupId}
|
|
recoveryTimeTarget={recoveryTimeTarget}
|
|
additionalMonthlySpend={additionalMonthlySpend}
|
|
hasAccess={hasAccessToRestoreToNewProject}
|
|
onOpenChange={setShowNewProjectDialog}
|
|
onCloneSuccess={() => {
|
|
refetchCloneStatus()
|
|
setRefetchInterval(5000)
|
|
setShowNewProjectDialog(false)
|
|
}}
|
|
/>
|
|
{isRestoring ? (
|
|
<Alert_Shadcn_ className="[&>svg]:bg-none! [&>svg]:text-foreground-light mb-6">
|
|
<Loader2 className="animate-spin" />
|
|
<AlertTitle_Shadcn_>Restoration in progress</AlertTitle_Shadcn_>
|
|
<AlertDescription_Shadcn_>
|
|
<p>
|
|
The new project {(restoringClone?.target_project as any)?.name || ''} is currently
|
|
being created. You'll be able to restore again once the project is ready.
|
|
</p>
|
|
<Button asChild type="default" className="mt-2">
|
|
<Link href={`/project/${restoringClone?.target_project?.ref ?? '_'}`}>
|
|
Go to new project
|
|
</Link>
|
|
</Button>
|
|
</AlertDescription_Shadcn_>
|
|
</Alert_Shadcn_>
|
|
) : null}
|
|
{previousClones?.length ? (
|
|
<div className="flex flex-col gap-2">
|
|
<h3 className="text-sm font-medium">Previous restorations</h3>
|
|
<Panel className="flex flex-col divide-y divide-border">
|
|
{previousClones?.map((c) => (
|
|
<PreviousRestoreItem key={c.inserted_at} clone={c} />
|
|
))}
|
|
</Panel>
|
|
</div>
|
|
) : null}
|
|
{PITR_ENABLED ? (
|
|
<>
|
|
<PITRForm
|
|
disabled={isRestoring}
|
|
onSubmit={(v) => {
|
|
setShowConfirmationDialog(true)
|
|
setRecoveryTimeTarget(v.recoveryTimeTargetUnix)
|
|
}}
|
|
earliestAvailableBackupUnix={
|
|
cloneBackups?.physicalBackupData.earliestPhysicalBackupDateUnix || 0
|
|
}
|
|
latestAvailableBackupUnix={
|
|
cloneBackups?.physicalBackupData.latestPhysicalBackupDateUnix || 0
|
|
}
|
|
/>
|
|
</>
|
|
) : (
|
|
<BackupsList
|
|
disabled={isRestoring}
|
|
hasAccess={hasAccessToRestoreToNewProject}
|
|
onSelectRestore={(id) => {
|
|
setSelectedBackupId(id)
|
|
setShowConfirmationDialog(true)
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|