RestoringState.tsx 7.6 KB

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