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:
Jordi Enric authored and Joshen Lim committed 2024-12-18 12:32:46 +08:00
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: {
@@ -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>
)
}