Files
supabase/apps/studio/components/layouts/ProjectLayout/RestoringState.tsx
T
Joshen Lim 00ca386e24 Chore/add get restore initiated event endpoint (#43485)
## Context

Resolves FE-2709
Dependent on API PR: https://github.com/supabase/platform/pull/30564

While a project is in restoring, we should a "Contact support" CTA in
the footer, but this has led to a high number of tickets coming in
despite the project's status being `ACTIVE_HEALTHY` when we check on our
end.

Will be adjusting the behaviour of this CTA to only show up if the
project's status is RESTORING and it's `updated_at` is at than 30
minutes ago, and update the CTA a bit as well

<img width="765" height="243" alt="image"
src="https://github.com/user-attachments/assets/69fe6fa3-d46f-4268-ad1b-46cffb3cc6ba"
/>

<img width="566" height="273" alt="image"
src="https://github.com/user-attachments/assets/2d5f31c2-6ef2-444b-8aee-fd94139cf713"
/>
2026-03-19 00:49:01 +08:00

185 lines
7.0 KiB
TypeScript

import { SupportCategories } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { SupportLink } from 'components/interfaces/Support/SupportLink'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useBackupDownloadMutation } from 'data/database/backup-download-mutation'
import { useDownloadableBackupQuery } from 'data/database/backup-query'
import { useInvalidateProjectDetailsQuery } from 'data/projects/project-detail-query'
import { useProjectStatusQuery } from 'data/projects/project-status-query'
import dayjs from 'dayjs'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { PROJECT_STATUS } from 'lib/constants'
import { CheckCircle, Download, Loader } from 'lucide-react'
import { useEffect, useState } from 'react'
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogSection,
DialogSectionSeparator,
DialogTitle,
DialogTrigger,
} from 'ui'
export const RestoringState = () => {
const { ref } = useParams()
const { data: project } = useSelectedProjectQuery()
const [loading, setLoading] = useState(false)
const [isCompleted, setIsCompleted] = useState(false)
const { data } = useDownloadableBackupQuery({ projectRef: ref })
const backups = data?.backups ?? []
const logicalBackups = backups.filter((b) => !b.isPhysicalBackup)
const { invalidateProjectDetailsQuery } = useInvalidateProjectDetailsQuery()
const { data: projectStatusData, isSuccess: isProjectStatusSuccess } = useProjectStatusQuery(
{ projectRef: ref },
{
enabled: project?.status !== PROJECT_STATUS.ACTIVE_HEALTHY,
refetchInterval: (query) => {
const data = query.state.data
return data?.status === PROJECT_STATUS.ACTIVE_HEALTHY ? false : 4000
},
}
)
const restoreInitiatedSinceMinutes = dayjs().diff(dayjs.utc(project?.updated_at), 'minute')
const showSupportCta = restoreInitiatedSinceMinutes >= 30
const { mutate: downloadBackup, isPending: isDownloading } = useBackupDownloadMutation({
onSuccess: (res) => {
const { fileUrl } = res
// Trigger browser download by create,trigger and remove tempLink
const tempLink = document.createElement('a')
tempLink.href = fileUrl
document.body.appendChild(tempLink)
tempLink.click()
document.body.removeChild(tempLink)
},
})
const onClickDownloadBackup = () => {
if (!ref) return console.error('Project ref is required')
if (logicalBackups.length === 0) return console.error('No available backups to download')
downloadBackup({ ref, backup: logicalBackups[0] })
}
const onConfirm = async () => {
if (!project) return console.error('Project is required')
setLoading(true)
if (ref) await invalidateProjectDetailsQuery(ref)
}
useEffect(() => {
if (isProjectStatusSuccess && projectStatusData.status === PROJECT_STATUS.ACTIVE_HEALTHY) {
setIsCompleted(true)
}
}, [isProjectStatusSuccess, projectStatusData])
return (
<div className="flex items-center justify-center h-full">
<div className="bg-surface-100 border border-overlay rounded-md w-3/4 lg:w-1/2">
{isCompleted ? (
<div className="space-y-6 pt-6">
<div className="flex px-8 space-x-8">
<div className="mt-1">
<CheckCircle className="text-brand" size={18} strokeWidth={2} />
</div>
<div className="space-y-1">
<p>Restoration complete!</p>
<p className="text-sm text-foreground-light">
Your project has been successfully restored and is now back online.
</p>
</div>
</div>
<div className="border-t border-overlay flex items-center justify-end py-4 px-8">
<Button disabled={loading} loading={loading} onClick={onConfirm}>
Return to project
</Button>
</div>
</div>
) : (
<>
<div className="space-y-6 py-6">
<div className="flex px-8 space-x-8">
<div className="mt-1">
<Loader className="animate-spin" size={18} />
</div>
<div className="space-y-1">
<p>Restoration in progress</p>
<p className="text-sm text-foreground-light">
Restoration can take from a few minutes up to several hours depending on the
size of your database. Your project will be offline while the restoration is
running.
</p>
</div>
</div>
</div>
<div className="border-t border-overlay flex items-center justify-end py-4 px-8 gap-x-2">
{showSupportCta && (
<Dialog>
<DialogTrigger>
<Button type="text" className="text-foreground-light">
Taking longer than expected?
</Button>
</DialogTrigger>
<DialogContent aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>Restoration taking longer than expected?</DialogTitle>
</DialogHeader>
<DialogSectionSeparator />
<DialogSection>
<p className="text-sm">
Restores may take from a minutes up to several hours depending on the size
of your database. However, if the restoration process is taking far longer
than expected for the size of your database, you may reach out to us via
Support for help.
</p>
</DialogSection>
<DialogFooter>
<Button asChild type="default">
<SupportLink
queryParams={{
category: SupportCategories.DATABASE_UNRESPONSIVE,
projectRef: project?.ref,
subject: 'Ongoing restoration for project',
}}
>
Contact support
</SupportLink>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
<ButtonTooltip
type="default"
icon={<Download />}
loading={isDownloading}
disabled={logicalBackups.length === 0}
tooltip={{
content: {
side: 'bottom',
text:
logicalBackups.length === 0 ? 'No available backups to download' : undefined,
},
}}
onClick={onClickDownloadBackup}
>
Download latest backup
</ButtonTooltip>
</div>
</>
)}
</div>
</div>
)
}