From 71b08c0b0cc17bd254fa65e2fa2fe466936567b5 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Tue, 17 Dec 2024 11:33:07 +0100 Subject: [PATCH] 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 --- .../Database/Backups/PITR/pitr-form.tsx | 4 +- .../RestoreToNewProject/BackupsList.tsx | 66 ++++--- .../backups/restore-to-new-project.tsx | 179 +++++++++++------- 3 files changed, 151 insertions(+), 98 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Backups/PITR/pitr-form.tsx b/apps/studio/components/interfaces/Database/Backups/PITR/pitr-form.tsx index 5bd537845cb..69207a2e0aa 100644 --- a/apps/studio/components/interfaces/Database/Backups/PITR/pitr-form.tsx +++ b/apps/studio/components/interfaces/Database/Backups/PITR/pitr-form.tsx @@ -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(getClientTimezone()) const earliestAvailableBackup = dayjs @@ -103,7 +105,7 @@ export function PITRForm({
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 ( - - {cloneBackups?.backups.length === 0 ? ( - <> +
+

Available Backups

+ + {cloneBackups?.backups.length === 0 ? ( - - ) : ( -
- {/*
{JSON.stringify({ cloneStatus }, null, 2)}
*/} - {cloneBackups?.backups.map((backup) => { - if (!backup.isPhysicalBackup) return null - return ( -
-
- + ) : ( +
+ {cloneBackups?.backups.map((backup) => { + if (!backup.isPhysicalBackup) return null + return ( +
+
+ +
+
+ {JSON.stringify(backup.status).replaceAll('"', '')} +
+
+ {(backup.status as any) === 'COMPLETED' && ( + + )} +
- {JSON.stringify(backup.status).replaceAll('"', '')} - {(backup.status as any) === 'COMPLETED' && ( - - )} -
- ) - })} -
- )} - + ) + })} +
+ )} + +
) } diff --git a/apps/studio/pages/project/[ref]/database/backups/restore-to-new-project.tsx b/apps/studio/pages/project/[ref]/database/backups/restore-to-new-project.tsx index 9fa1a73b3ea..c9fc727a6f1 100644 --- a/apps/studio/pages/project/[ref]/database/backups/restore-to-new-project.tsx +++ b/apps/studio/pages/project/[ref]/database/backups/restore-to-new-project.tsx @@ -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(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[number]['status'] + }) => { + const statusTextMap = { + IN_PROGRESS: 'RESTORING', + COMPLETED: 'COMPLETED', + REMOVED: 'REMOVED', + FAILED: 'FAILED', + } + + if (status === 'IN_PROGRESS') { + return {statusTextMap[status]} + } + + if (status === 'FAILED') { + return {statusTextMap[status]} + } + + return {statusTextMap[status]} + } + + const PreviousRestoreItem = ({ + clone, + }: { + clone: NonNullable[number] + }) => { + if (clone.status === 'REMOVED') { + return ( +
+
{(clone.target_project as any).name}
+
+ +
+
+ +
+
+ ) + } else { + return ( + +
{(clone.target_project as any).name}
+
+ +
+
+ +
+
+ +
+ + ) + } + } 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}).`} /> @@ -223,55 +284,6 @@ const RestoreToNewProject = () => { ) } - if (lastClone?.status === 'FAILED') { - return ( - - - - - ) - } - - if (lastClone?.status === 'COMPLETED') { - return ( - - - - - ) - } - - if (lastClone?.status === 'IN_PROGRESS') { - return ( - - - Restoration in progress - -

- The new project{!!clonedProject ? ` ${clonedProject.name}` : ''} is currently being - created -

- -
-
- ) - } - if ( !isLoading && hasPITREnabled && @@ -287,11 +299,19 @@ const RestoreToNewProject = () => { } if (!isLoading && !hasPITREnabled && cloneBackups?.backups.length === 0) { - return + return ( + <> + + + ) } return ( - <> +
{ setShowNewProjectDialog(false) }} /> + {isRestoring ? ( + + + Restoration in progress + +

+ 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. +

+ +
+
+ ) : null} + {previousClones?.length ? ( +
+

Previous restorations

+ + {previousClones?.map((c) => )} + +
+ ) : null} {hasPITREnabled ? ( <> { setShowConfirmationDialog(true) setRecoveryTimeTarget(v.recoveryTimeTargetUnix) @@ -328,13 +372,14 @@ const RestoreToNewProject = () => { ) : ( { setSelectedBackupId(id) setShowConfirmationDialog(true) }} /> )} - +
) }