PITRSelection.tsx180 lines · main
1import { useParams } from 'common'
2import Link from 'next/link'
3import { useRouter } from 'next/router'
4import { useState } from 'react'
5import { Alert, AlertDescription, AlertTitle, Button, Modal, WarningIcon } from 'ui'
6
7import { BackupsEmpty } from '../BackupsEmpty'
8import { BackupsStorageAlert } from '../BackupsStorageAlert'
9import type { Timezone } from './PITR.types'
10import { getClientTimezone } from './PITR.utils'
11import { PITRForm } from './PITRForm'
12import PITRStatus from './PITRStatus'
13import { FormHeader } from '@/components/ui/Forms/FormHeader'
14import { useBackupsQuery } from '@/data/database/backups-query'
15import { usePitrRestoreMutation } from '@/data/database/pitr-restore-mutation'
16import { useSetProjectStatus } from '@/data/projects/project-detail-query'
17import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
18import { PROJECT_STATUS } from '@/lib/constants'
19
20export const PITRSelection = () => {
21 const router = useRouter()
22 const { ref } = useParams()
23
24 const { data: backups } = useBackupsQuery({ projectRef: ref })
25 const { data: databases } = useReadReplicasQuery({ projectRef: ref })
26 const { setProjectStatus } = useSetProjectStatus()
27
28 const [showConfiguration, setShowConfiguration] = useState(false)
29 const [showConfirmation, setShowConfirmation] = useState(false)
30 const [selectedTimezone, setSelectedTimezone] = useState<Timezone>(getClientTimezone())
31 const [selectedRecoveryPoint, setSelectedRecoveryPoint] = useState<{
32 recoveryTimeTargetUnix: number
33 recoveryTimeString: string
34 recoveryTimeStringUtc: string
35 }>()
36
37 const hasReadReplicas = (databases ?? []).length > 1
38
39 const {
40 mutate: restoreFromPitr,
41 isPending: isRestoring,
42 isSuccess: isSuccessPITR,
43 } = usePitrRestoreMutation({
44 onSuccess: (_, variables) => {
45 setTimeout(() => {
46 setShowConfirmation(false)
47 setProjectStatus({ ref: variables.ref, status: PROJECT_STATUS.RESTORING })
48 router.push(`/project/${variables.ref}`)
49 }, 3000)
50 },
51 })
52
53 const { earliestPhysicalBackupDateUnix, latestPhysicalBackupDateUnix } =
54 backups?.physicalBackupData ?? {}
55 const hasNoBackupsAvailable = !earliestPhysicalBackupDateUnix || !latestPhysicalBackupDateUnix
56
57 const onConfirmRestore = async () => {
58 if (!ref) return console.error('Project ref is required')
59 if (!selectedRecoveryPoint?.recoveryTimeTargetUnix)
60 return console.error('Recovery time target unix is required')
61
62 restoreFromPitr({
63 ref,
64 recovery_time_target_unix: selectedRecoveryPoint.recoveryTimeTargetUnix,
65 })
66 }
67
68 return (
69 <>
70 <FormHeader
71 title="Restore your database from a backup"
72 description="Database changes are watched and recorded, so that you can restore your database to any point in time"
73 />
74 <BackupsStorageAlert />
75 {hasNoBackupsAvailable ? (
76 <BackupsEmpty />
77 ) : (
78 <>
79 {hasReadReplicas && (
80 <Alert variant="warning">
81 <WarningIcon />
82 <AlertTitle>
83 Unable to restore from PITR as project has read replicas enabled
84 </AlertTitle>
85 <AlertDescription>
86 You will need to remove all read replicas first from your project's infrastructure
87 settings prior to starting a PITR restore.
88 </AlertDescription>
89 <div className="flex items-center gap-x-2 mt-2">
90 {/* [Joshen] Ideally we have some links to a docs to explain why so */}
91 <Button type="default">
92 <Link href={`/project/${ref}/settings/infrastructure`}>
93 Infrastructure settings
94 </Link>
95 </Button>
96 </div>
97 </Alert>
98 )}
99 {!showConfiguration ? (
100 <PITRStatus
101 selectedTimezone={selectedTimezone}
102 onUpdateTimezone={setSelectedTimezone}
103 onSetConfiguration={() => setShowConfiguration(true)}
104 />
105 ) : (
106 <PITRForm
107 earliestAvailableBackupUnix={earliestPhysicalBackupDateUnix}
108 latestAvailableBackupUnix={latestPhysicalBackupDateUnix}
109 onSubmit={(recoveryPoint) => {
110 setSelectedRecoveryPoint(recoveryPoint)
111 setShowConfirmation(true)
112 }}
113 />
114 )}
115 </>
116 )}
117
118 <Modal
119 size="medium"
120 visible={showConfirmation}
121 onCancel={() => setShowConfirmation(false)}
122 header="Point in time recovery review"
123 customFooter={
124 <div className="flex items-center justify-end space-x-2">
125 <Button
126 type="default"
127 disabled={isRestoring || isSuccessPITR}
128 onClick={() => setShowConfirmation(false)}
129 >
130 Cancel
131 </Button>
132 <Button
133 type="warning"
134 disabled={isRestoring || isSuccessPITR}
135 loading={isRestoring || isSuccessPITR}
136 onClick={onConfirmRestore}
137 >
138 I understand, begin restore
139 </Button>
140 </div>
141 }
142 >
143 <Modal.Content>
144 <div className="py-2 space-y-1">
145 <p className="text-sm text-foreground-light">Your database will be restored to:</p>
146 </div>
147 <div className="py-2 flex flex-col gap-3">
148 <div>
149 <p className="text-sm font-mono text-foreground-lighter">Local Time</p>
150 <p className="text-2xl">{selectedRecoveryPoint?.recoveryTimeString}</p>
151 </div>
152 <div>
153 <p className="text-sm font-mono text-foreground-lighter">(UTC+00:00)</p>
154 <p className="text-2xl">{selectedRecoveryPoint?.recoveryTimeStringUtc}</p>
155 </div>
156 </div>
157 </Modal.Content>
158 <Modal.Separator />
159 <Modal.Content>
160 <Alert variant="warning">
161 <WarningIcon />
162 <AlertTitle>This action cannot be undone, not canceled once started</AlertTitle>
163 <AlertDescription>
164 Any changes made to your database after this point in time will be lost. This includes
165 any changes to your project's storage and authentication.
166 </AlertDescription>
167 </Alert>
168 </Modal.Content>
169 <Modal.Separator />
170 <Modal.Content>
171 <p className="text-sm text-foreground-light">
172 Restores may take from a few minutes up to several hours depending on the size of your
173 database. During this period, your project will not be available, until the restoration
174 is completed.
175 </p>
176 </Modal.Content>
177 </Modal>
178 </>
179 )
180}