RestoringState.tsx192 lines · main
1import { SupportCategories } from '@supabase/shared-types/out/constants'
2import { LOCAL_STORAGE_KEYS, useParams } from 'common'
3import { CheckCircle, Download, Loader } from 'lucide-react'
4import { useEffect, useState } from 'react'
5import { Button } from 'ui'
6import { Admonition } from 'ui-patterns/admonition'
7
8import { SupportLink } from '@/components/interfaces/Support/SupportLink'
9import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
10import { useBackupDownloadMutation } from '@/data/database/backup-download-mutation'
11import { useDownloadableBackupQuery } from '@/data/database/backup-query'
12import { useInvalidateProjectDetailsQuery } from '@/data/projects/project-detail-query'
13import { useProjectStatusQuery } from '@/data/projects/project-status-query'
14import { useLongRunningTransitionState } from '@/hooks/misc/useLongRunningTransitionState'
15import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
16import { PROJECT_STATUS } from '@/lib/constants'
17import {
18 clearPersistedTransitionStartTime,
19 minutesToMilliseconds,
20} from '@/lib/project-transition-state'
21import { getRestoreLongRunningThresholdMinutes } from '@/lib/restore-estimate'
22
23export const RestoringState = () => {
24 const { ref } = useParams()
25 const { data: project } = useSelectedProjectQuery()
26
27 const [loading, setLoading] = useState(false)
28 const [isCompleted, setIsCompleted] = useState(false)
29 const restoreStateStartStorageKey = ref
30 ? LOCAL_STORAGE_KEYS.PROJECT_RESTORING_STARTED_AT(ref)
31 : null
32
33 const { data } = useDownloadableBackupQuery({ projectRef: ref })
34 const backups = data?.backups ?? []
35 const logicalBackups = backups.filter((b) => !b.isPhysicalBackup)
36 const longRunningThresholdMinutes = getRestoreLongRunningThresholdMinutes(project?.volumeSizeGb)
37 const longRunningThresholdMs = minutesToMilliseconds(longRunningThresholdMinutes)
38 const isTakingLongerThanExpected = useLongRunningTransitionState({
39 storageKey: restoreStateStartStorageKey,
40 thresholdMs: longRunningThresholdMs,
41 })
42
43 const { invalidateProjectDetailsQuery } = useInvalidateProjectDetailsQuery()
44
45 const { data: projectStatusData, isSuccess: isProjectStatusSuccess } = useProjectStatusQuery(
46 { projectRef: ref },
47 {
48 enabled: project?.status !== PROJECT_STATUS.ACTIVE_HEALTHY,
49 refetchInterval: (query) => {
50 const data = query.state.data
51 return data?.status === PROJECT_STATUS.ACTIVE_HEALTHY ||
52 data?.status === PROJECT_STATUS.RESTORE_FAILED
53 ? false
54 : 4000
55 },
56 }
57 )
58
59 const { mutate: downloadBackup, isPending: isDownloading } = useBackupDownloadMutation({
60 onSuccess: (res) => {
61 const { fileUrl } = res
62
63 // Trigger browser download by create,trigger and remove tempLink
64 const tempLink = document.createElement('a')
65 tempLink.href = fileUrl
66 document.body.appendChild(tempLink)
67 tempLink.click()
68 document.body.removeChild(tempLink)
69 },
70 })
71
72 const onClickDownloadBackup = () => {
73 if (!ref) return console.error('Project ref is required')
74 if (logicalBackups.length === 0) return console.error('No available backups to download')
75
76 downloadBackup({ ref, backup: logicalBackups[0] })
77 }
78
79 const onConfirm = async () => {
80 if (!project) return console.error('Project is required')
81 setLoading(true)
82 if (ref) await invalidateProjectDetailsQuery(ref)
83 }
84
85 useEffect(() => {
86 if (!isProjectStatusSuccess) return
87
88 if (projectStatusData.status === PROJECT_STATUS.ACTIVE_HEALTHY) {
89 if (restoreStateStartStorageKey) {
90 clearPersistedTransitionStartTime(restoreStateStartStorageKey)
91 }
92 setIsCompleted(true)
93 } else if (projectStatusData.status === PROJECT_STATUS.RESTORE_FAILED) {
94 if (restoreStateStartStorageKey) {
95 clearPersistedTransitionStartTime(restoreStateStartStorageKey)
96 }
97 if (ref) void invalidateProjectDetailsQuery(ref)
98 }
99 }, [
100 isProjectStatusSuccess,
101 projectStatusData,
102 restoreStateStartStorageKey,
103 ref,
104 invalidateProjectDetailsQuery,
105 ])
106
107 return (
108 <div className="flex items-center justify-center h-full">
109 <div className="bg-surface-100 border border-overlay rounded-md w-3/4 lg:w-1/2">
110 {isCompleted ? (
111 <div className="space-y-6 pt-6">
112 <div className="flex px-8 space-x-8">
113 <div className="mt-1">
114 <CheckCircle className="text-brand" size={18} strokeWidth={2} />
115 </div>
116 <div className="space-y-1">
117 <p>Restoration complete!</p>
118 <p className="text-sm text-foreground-light">
119 Your project has been successfully restored and is now back online.
120 </p>
121 </div>
122 </div>
123 <div className="border-t border-overlay flex items-center justify-end py-4 px-8">
124 <Button disabled={loading} loading={loading} onClick={onConfirm}>
125 Return to project
126 </Button>
127 </div>
128 </div>
129 ) : (
130 <>
131 <div className="space-y-6 py-6">
132 <div className="flex px-8 space-x-8">
133 <div className="mt-1">
134 <Loader className="animate-spin" size={18} />
135 </div>
136 <div className="space-y-1">
137 <p>Restoration in progress</p>
138 <p className="text-sm text-foreground-light">
139 Restoration can take from a few minutes up to several hours depending on the
140 size of your database. Your project will be offline while the restoration is
141 running.
142 </p>
143 {isTakingLongerThanExpected && (
144 <Admonition
145 type="warning"
146 title="This is taking longer than usual"
147 layout="responsive"
148 description="Contact support if this project remains in a restoring state."
149 actions={
150 <Button asChild type="default">
151 <SupportLink
152 queryParams={{
153 category: SupportCategories.DATABASE_UNRESPONSIVE,
154 projectRef: project?.ref ?? ref,
155 subject: 'Project stuck in restoring state',
156 message: `Project "${project?.name ?? 'Unknown project'}" (ref: ${project?.ref ?? ref ?? 'unknown'}) has remained in a restoring state for over ${longRunningThresholdMinutes} minutes.`,
157 }}
158 >
159 Contact support
160 </SupportLink>
161 </Button>
162 }
163 className="mt-5!"
164 />
165 )}
166 </div>
167 </div>
168 </div>
169 <div className="border-t border-overlay flex items-center justify-end py-4 px-8 gap-x-2">
170 <ButtonTooltip
171 type="default"
172 icon={<Download />}
173 loading={isDownloading}
174 disabled={logicalBackups.length === 0}
175 tooltip={{
176 content: {
177 side: 'bottom',
178 text:
179 logicalBackups.length === 0 ? 'No available backups to download' : undefined,
180 },
181 }}
182 onClick={onClickDownloadBackup}
183 >
184 Download latest backup
185 </ButtonTooltip>
186 </div>
187 </>
188 )}
189 </div>
190 </div>
191 )
192}