mirror of
https://github.com/supabase/supabase.git
synced 2026-10-16 22:59:11 +03:00
Allow multiple r2np (#31063)
* add previous restores section * add target project name * fix link to og project * typecast to fix type err until api types are fixed * cleanup * improve IN PROGRESS state, shows restores list and disables buttons * fix typerr with isUTC * disable PITR form if theres a restoration happening * rm commented out code
This commit is contained in:
1 parent
d9b3a709c0
commit
71b08c0b0c
3 files changed
+151
-98
No files matched your search
@@ -24,12 +24,14 @@ type Props = {
|
||||
}) => void
|
||||
earliestAvailableBackupUnix: number
|
||||
latestAvailableBackupUnix: number
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export function PITRForm({
|
||||
onSubmit,
|
||||
earliestAvailableBackupUnix,
|
||||
latestAvailableBackupUnix,
|
||||
disabled = false,
|
||||
}: Props) {
|
||||
const [selectedTimezone, setSelectedTimezone] = useState<Timezone>(getClientTimezone())
|
||||
const earliestAvailableBackup = dayjs
|
||||
@@ -103,7 +105,7 @@ export function PITRForm({
|
||||
<div className="flex items-center justify-end gap-3 p-6">
|
||||
<ButtonTooltip
|
||||
type="default"
|
||||
disabled={!selectedDate || !isWithinRange}
|
||||
disabled={disabled || !selectedDate || !isWithinRange}
|
||||
onClick={handleSubmit}
|
||||
tooltip={{
|
||||
content: {
|
||||
|
||||
+36
-30
@@ -9,9 +9,10 @@ import { TimestampInfo } from 'ui-patterns'
|
||||
|
||||
interface BackupsListProps {
|
||||
onSelectRestore: (id: number) => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export const BackupsList = ({ onSelectRestore }: BackupsListProps) => {
|
||||
export const BackupsList = ({ onSelectRestore, disabled }: BackupsListProps) => {
|
||||
const { project } = useProjectContext()
|
||||
const organization = useSelectedOrganization()
|
||||
|
||||
@@ -24,36 +25,41 @@ export const BackupsList = ({ onSelectRestore }: BackupsListProps) => {
|
||||
)
|
||||
|
||||
return (
|
||||
<Panel>
|
||||
{cloneBackups?.backups.length === 0 ? (
|
||||
<>
|
||||
<div className="flex flex-col gap-2">
|
||||
<h3 className="text-sm font-medium">Available Backups</h3>
|
||||
<Panel>
|
||||
{cloneBackups?.backups.length === 0 ? (
|
||||
<BackupsEmpty />
|
||||
</>
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
{/* <pre>{JSON.stringify({ cloneStatus }, null, 2)}</pre> */}
|
||||
{cloneBackups?.backups.map((backup) => {
|
||||
if (!backup.isPhysicalBackup) return null
|
||||
return (
|
||||
<div className="flex p-4 gap-4" key={backup.id}>
|
||||
<div>
|
||||
<TimestampInfo value={backup.inserted_at} />
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
{cloneBackups?.backups.map((backup) => {
|
||||
if (!backup.isPhysicalBackup) return null
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-4 items-center p-4" key={backup.id}>
|
||||
<div>
|
||||
<TimestampInfo value={backup.inserted_at} />
|
||||
</div>
|
||||
<div>
|
||||
<Badge>{JSON.stringify(backup.status).replaceAll('"', '')}</Badge>
|
||||
</div>
|
||||
<div className="col-span-2 flex justify-end">
|
||||
{(backup.status as any) === 'COMPLETED' && (
|
||||
<Button
|
||||
className="ml-auto"
|
||||
type="outline"
|
||||
onClick={() => onSelectRestore(backup.id)}
|
||||
disabled={disabled}
|
||||
>
|
||||
Restore
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Badge>{JSON.stringify(backup.status).replaceAll('"', '')}</Badge>
|
||||
{(backup.status as any) === 'COMPLETED' && (
|
||||
<Button
|
||||
className="ml-auto"
|
||||
type="outline"
|
||||
onClick={() => onSelectRestore(backup.id)}
|
||||
>
|
||||
Restore
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { ChevronRightIcon, Loader2 } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
|
||||
import BackupsEmpty from 'components/interfaces/Database/Backups/BackupsEmpty'
|
||||
import DatabaseBackupsNav from 'components/interfaces/Database/Backups/DatabaseBackupsNav'
|
||||
import { PITRForm } from 'components/interfaces/Database/Backups/PITR/pitr-form'
|
||||
import { BackupsList } from 'components/interfaces/Database/Backups/RestoreToNewProject/BackupsList'
|
||||
@@ -21,7 +20,6 @@ import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import UpgradeToPro from 'components/ui/UpgradeToPro'
|
||||
import { useCloneBackupsQuery } from 'data/projects/clone-query'
|
||||
import { useCloneStatusQuery } from 'data/projects/clone-status-query'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
|
||||
import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
|
||||
@@ -29,8 +27,9 @@ import { useIsOrioleDb } from 'hooks/misc/useSelectedProject'
|
||||
import { PROJECT_STATUS } from 'lib/constants'
|
||||
import { getDatabaseMajorVersion } from 'lib/helpers'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Button } from 'ui'
|
||||
import { Admonition } from 'ui-patterns'
|
||||
import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Badge, Button } from 'ui'
|
||||
import { Admonition, TimestampInfo } from 'ui-patterns'
|
||||
import Panel from 'components/ui/Panel'
|
||||
|
||||
const RestoreToNewProjectPage: NextPageWithLayout = () => {
|
||||
return (
|
||||
@@ -67,8 +66,6 @@ const RestoreToNewProject = () => {
|
||||
const [showNewProjectDialog, setShowNewProjectDialog] = useState(false)
|
||||
const [recoveryTimeTarget, setRecoveryTimeTarget] = useState<number | null>(null)
|
||||
|
||||
const { data: projects } = useProjectsQuery()
|
||||
|
||||
const {
|
||||
data: cloneBackups,
|
||||
error,
|
||||
@@ -108,13 +105,79 @@ const RestoreToNewProject = () => {
|
||||
},
|
||||
}
|
||||
)
|
||||
const lastClone = cloneStatus?.clones?.[cloneStatus?.clones.length - 1]
|
||||
const IS_CLONED_PROJECT = (cloneStatus?.cloned_from?.source_project as any)?.ref ? true : false
|
||||
|
||||
const isLoading = !isPermissionsLoaded || cloneBackupsLoading || cloneStatusLoading
|
||||
const clonedProject = projects?.find(
|
||||
(p) => p.ref === cloneStatus?.clones?.[0]?.target_project.ref
|
||||
)
|
||||
|
||||
const previousClones = cloneStatus?.clones
|
||||
const isRestoring = previousClones?.some((c) => c.status === 'IN_PROGRESS')
|
||||
const restoringClone = previousClones?.find((c) => c.status === 'IN_PROGRESS')
|
||||
|
||||
const StatusBadge = ({
|
||||
status,
|
||||
}: {
|
||||
status: NonNullable<typeof previousClones>[number]['status']
|
||||
}) => {
|
||||
const statusTextMap = {
|
||||
IN_PROGRESS: 'RESTORING',
|
||||
COMPLETED: 'COMPLETED',
|
||||
REMOVED: 'REMOVED',
|
||||
FAILED: 'FAILED',
|
||||
}
|
||||
|
||||
if (status === 'IN_PROGRESS') {
|
||||
return <Badge variant="warning">{statusTextMap[status]}</Badge>
|
||||
}
|
||||
|
||||
if (status === 'FAILED') {
|
||||
return <Badge variant="destructive">{statusTextMap[status]}</Badge>
|
||||
}
|
||||
|
||||
return <Badge>{statusTextMap[status]}</Badge>
|
||||
}
|
||||
|
||||
const PreviousRestoreItem = ({
|
||||
clone,
|
||||
}: {
|
||||
clone: NonNullable<typeof previousClones>[number]
|
||||
}) => {
|
||||
if (clone.status === 'REMOVED') {
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-2 text-sm p-4 group">
|
||||
<div className="min-w-24 truncate">{(clone.target_project as any).name}</div>
|
||||
<div>
|
||||
<StatusBadge status={clone.status} />
|
||||
</div>
|
||||
<div>
|
||||
<TimestampInfo
|
||||
className="font-mono text-xs text-foreground-lighter"
|
||||
value={clone.inserted_at ?? ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Link
|
||||
href={`/project/${clone.target_project.ref}`}
|
||||
className="grid grid-cols-4 gap-2 text-sm p-4 group"
|
||||
>
|
||||
<div className="min-w-24 truncate">{(clone.target_project as any).name}</div>
|
||||
<div>
|
||||
<StatusBadge status={clone.status} />
|
||||
</div>
|
||||
<div>
|
||||
<TimestampInfo
|
||||
className="font-mono text-xs text-foreground-lighter"
|
||||
value={clone.inserted_at ?? ''}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-end text-foreground-lighter group-hover:text-foreground">
|
||||
<ChevronRightIcon className="w-4 h-4" />
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isOrioleDb) {
|
||||
return (
|
||||
@@ -199,9 +262,7 @@ const RestoreToNewProject = () => {
|
||||
If you need to restore a project to multiple other projects, please reach out via [support](/support/new?ref=${project?.ref}).`}
|
||||
/>
|
||||
<Button asChild type="default">
|
||||
<Link
|
||||
href={`/dashboard/project/${(cloneStatus?.cloned_from?.source_project as any)?.ref || ''}`}
|
||||
>
|
||||
<Link href={`/project/${(cloneStatus?.cloned_from?.source_project as any)?.ref || ''}`}>
|
||||
Go to original project
|
||||
</Link>
|
||||
</Button>
|
||||
@@ -223,55 +284,6 @@ const RestoreToNewProject = () => {
|
||||
)
|
||||
}
|
||||
|
||||
if (lastClone?.status === 'FAILED') {
|
||||
return (
|
||||
<Admonition type="destructive" title="Failed to restore to new project">
|
||||
<Markdown content="Sorry! The new project failed to be created, please reach out to support for assistance." />
|
||||
<Button asChild type="default">
|
||||
<Link
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href={`/support/new?category=dashboard_bug&subject=Failed%20to%20restore%20to%20new%20project&message=Target%20project%20reference:%20${clonedProject?.ref ?? 'unknown'}`}
|
||||
>
|
||||
Contact support
|
||||
</Link>
|
||||
</Button>
|
||||
</Admonition>
|
||||
)
|
||||
}
|
||||
|
||||
if (lastClone?.status === 'COMPLETED') {
|
||||
return (
|
||||
<Admonition type="default" title="Restoration completed">
|
||||
<Markdown
|
||||
className="max-w-full"
|
||||
content={`The new project${!!clonedProject ? ` ${clonedProject.name}` : ''} has been created. A project can only be restored to another project once.`}
|
||||
/>
|
||||
<Button asChild type="default">
|
||||
<Link href={`/project/${lastClone?.target_project.ref}`}>Go to new project</Link>
|
||||
</Button>
|
||||
</Admonition>
|
||||
)
|
||||
}
|
||||
|
||||
if (lastClone?.status === 'IN_PROGRESS') {
|
||||
return (
|
||||
<Alert_Shadcn_ className="[&>svg]:bg-none! [&>svg]:text-foreground-light">
|
||||
<Loader2 className="animate-spin" />
|
||||
<AlertTitle_Shadcn_>Restoration in progress</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
<p>
|
||||
The new project{!!clonedProject ? ` ${clonedProject.name}` : ''} is currently being
|
||||
created
|
||||
</p>
|
||||
<Button asChild type="default" className="mt-2">
|
||||
<Link href={`/project/${lastClone?.target_project.ref}`}>Go to new project</Link>
|
||||
</Button>
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
!isLoading &&
|
||||
hasPITREnabled &&
|
||||
@@ -287,11 +299,19 @@ const RestoreToNewProject = () => {
|
||||
}
|
||||
|
||||
if (!isLoading && !hasPITREnabled && cloneBackups?.backups.length === 0) {
|
||||
return <BackupsEmpty />
|
||||
return (
|
||||
<>
|
||||
<Admonition
|
||||
type="default"
|
||||
title="No backups found"
|
||||
description="Backups are enabled, but no backups were found. Check again tomorrow."
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-4">
|
||||
<ConfirmRestoreDialog
|
||||
open={showConfirmationDialog}
|
||||
onOpenChange={setShowConfirmationDialog}
|
||||
@@ -311,9 +331,33 @@ const RestoreToNewProject = () => {
|
||||
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}
|
||||
{hasPITREnabled ? (
|
||||
<>
|
||||
<PITRForm
|
||||
disabled={isRestoring}
|
||||
onSubmit={(v) => {
|
||||
setShowConfirmationDialog(true)
|
||||
setRecoveryTimeTarget(v.recoveryTimeTargetUnix)
|
||||
@@ -328,13 +372,14 @@ const RestoreToNewProject = () => {
|
||||
</>
|
||||
) : (
|
||||
<BackupsList
|
||||
disabled={isRestoring}
|
||||
onSelectRestore={(id) => {
|
||||
setSelectedBackupId(id)
|
||||
setShowConfirmationDialog(true)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user