mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 18:05:11 +03:00
Resolves [FE-4144](https://linear.app/supabase/issue/FE-4144/restore-flow-shows-completion-before-restore-is-actually-done) ## Problem `RestoringState` treated any `ACTIVE_HEALTHY` reading from the project status endpoint as "restore finished". Right after a restore is triggered the backend still reports the pre-restore status, so the first poll could land on `ACTIVE_HEALTHY` and flip the UI to "Restoration complete!" seconds into a restore that had barely started. `isCompleted` was local state nothing reset and polling stopped on that first reading, so the screen never self-corrected — "Return to project" then hung until a manual refresh. ## Changes - Gate completion on having observed the project leave the healthy state, so a stale pre-restore reading is no longer mistaken for a finished restore. - Keep polling through an unconfirmed healthy reading instead of stopping on it. - `onConfirm` clears its loading flag rather than relying on the layout to unmount the component. - Component tests covering both the premature completion and the stuck button. ## Needs validation Not yet verified against a real restore — please confirm on staging before merging. Worth checking in particular that a restore which completes normally still reaches the completion screen. There is one residual edge case left in place deliberately: if the details endpoint reports `RESTORING` while the status endpoint reports `ACTIVE_HEALTHY`, the UI now stays on "Restoration in progress" until the details query catches up. Fixing that properly needs an authoritative "restore initiated at" timestamp from the API, which does not exist today. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Improved project restoration tracking to prevent completion from being reported prematurely. - Restoration now correctly detects failures and stops polling when appropriate. - Restore status and saved transition information are cleared after successful completion or failure. - Confirmation actions now remain reliable while project details refresh. - Restoring controls become usable again after the process finishes. - Improved the restore menu trigger behavior for more consistent interaction. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
175 lines
6.7 KiB
TypeScript
175 lines
6.7 KiB
TypeScript
import { PermissionAction, SupportCategories } from '@supabase/shared-types/out/constants'
|
|
import { useParams } from 'common'
|
|
import { Download, MoreVertical, Trash } from 'lucide-react'
|
|
import { useState } from 'react'
|
|
import {
|
|
Button,
|
|
CriticalIcon,
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogSection,
|
|
DialogSectionSeparator,
|
|
DialogTitle,
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuTrigger,
|
|
} from 'ui'
|
|
|
|
import { DeleteProjectModal } from '@/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectModal'
|
|
import { SupportLink } from '@/components/interfaces/Support/SupportLink'
|
|
import { LogicalBackupCliInstructions } from '@/components/layouts/ProjectLayout/LogicalBackupCliInstructions'
|
|
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
|
import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip'
|
|
import { InlineLink } from '@/components/ui/InlineLink'
|
|
import { useBackupDownloadMutation } from '@/data/database/backup-download-mutation'
|
|
import { useDownloadableBackupQuery } from '@/data/database/backup-query'
|
|
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
|
|
export const RestoreFailedState = () => {
|
|
const { ref } = useParams()
|
|
const { data: project } = useSelectedProjectQuery()
|
|
const [visible, setVisible] = useState(false)
|
|
const [showCliBackup, setShowCliBackup] = useState(false)
|
|
|
|
const { can: canDeleteProject } = useAsyncCheckPermissions(PermissionAction.UPDATE, 'projects', {
|
|
resource: { project_id: project?.id },
|
|
})
|
|
|
|
const { data, isPending: isLoadingBackups } = useDownloadableBackupQuery({ projectRef: ref })
|
|
const backups = data?.backups ?? []
|
|
|
|
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 (backups.length === 0 || data?.status === 'physical-backups-enabled')
|
|
return setShowCliBackup(true)
|
|
downloadBackup({ ref, backup: backups[0] })
|
|
}
|
|
|
|
const downloadBackupTooltipText = isLoadingBackups
|
|
? undefined
|
|
: data?.status === 'physical-backups-enabled'
|
|
? 'Project uses physical backups — click to see CLI backup instructions'
|
|
: backups.length === 0
|
|
? 'No downloadable backup available — click to see CLI backup instructions'
|
|
: undefined
|
|
|
|
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">
|
|
<div className="space-y-6 pt-6">
|
|
<div className="flex px-8 space-x-8">
|
|
<div className="mt-1">
|
|
<CriticalIcon className="w-5 h-5" />
|
|
</div>
|
|
<div className="space-y-1">
|
|
<p>Something went wrong while restoring your project</p>
|
|
<p className="text-sm text-foreground-light">
|
|
Your project's data is intact, but your project is inaccessible due to a
|
|
restoration failure. Database backups for this project can still be accessed{' '}
|
|
<InlineLink href={`/project/${ref}/database/backups/scheduled`}>here</InlineLink>.
|
|
</p>
|
|
<p className="text-sm text-foreground-light">
|
|
Please contact support for assistance.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border-t border-overlay flex items-center justify-end py-4 px-8 gap-x-2">
|
|
<Button asChild variant="default">
|
|
<SupportLink
|
|
queryParams={{
|
|
category: SupportCategories.DATABASE_UNRESPONSIVE,
|
|
projectRef: project?.ref,
|
|
subject: 'Restoration failed for project',
|
|
}}
|
|
>
|
|
Contact support
|
|
</SupportLink>
|
|
</Button>
|
|
|
|
<ButtonTooltip
|
|
variant="default"
|
|
icon={<Download />}
|
|
disabled={isLoadingBackups}
|
|
loading={isDownloading || isLoadingBackups}
|
|
tooltip={{
|
|
content: {
|
|
side: 'bottom',
|
|
text: downloadBackupTooltipText,
|
|
},
|
|
}}
|
|
onClick={onClickDownloadBackup}
|
|
>
|
|
Download backup
|
|
</ButtonTooltip>
|
|
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="default" className="w-7" icon={<MoreVertical />} />
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent className="w-72" align="end">
|
|
<DropdownMenuItemTooltip
|
|
onClick={() => setVisible(true)}
|
|
className="items-start gap-x-2"
|
|
disabled={!canDeleteProject}
|
|
tooltip={{
|
|
content: {
|
|
side: 'right',
|
|
text: !canDeleteProject
|
|
? 'You need additional permissions to delete this project'
|
|
: undefined,
|
|
},
|
|
}}
|
|
>
|
|
<div className="translate-y-0.5">
|
|
<Trash size={14} />
|
|
</div>
|
|
<div className="">
|
|
<p>Delete project</p>
|
|
<p className="text-foreground-lighter">
|
|
Project cannot be restored once it is deleted
|
|
</p>
|
|
</div>
|
|
</DropdownMenuItemTooltip>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Dialog open={showCliBackup} onOpenChange={setShowCliBackup}>
|
|
<DialogContent size="medium" onOpenAutoFocus={(e) => e.preventDefault()}>
|
|
<DialogHeader>
|
|
<DialogTitle>Back up your database with the Supabase CLI</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<DialogSectionSeparator />
|
|
|
|
<DialogSection>
|
|
<LogicalBackupCliInstructions showResetPassword={false} />
|
|
</DialogSection>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<DeleteProjectModal visible={visible} onClose={() => setVisible(false)} />
|
|
</>
|
|
)
|
|
}
|