PITRForm.tsx242 lines · main
1import dayjs from 'dayjs'
2import { HelpCircle } from 'lucide-react'
3import { useMemo, useState } from 'react'
4import { Calendar, cn } from 'ui'
5
6import { Timezone } from './PITR.types'
7import {
8 constrainDateToRange,
9 formatNumberToTwoDigits,
10 getClientTimezone,
11 getDatesBetweenRange,
12} from './PITR.utils'
13import TimeInput from './TimeInput'
14import { TimezoneSelection } from './TimezoneSelection'
15import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
16import { FormPanel } from '@/components/ui/Forms/FormPanel'
17import InformationBox from '@/components/ui/InformationBox'
18
19type Props = {
20 onSubmit: (data: {
21 recoveryTimeTargetUnix: number
22 recoveryTimeString: string
23 recoveryTimeStringUtc: string
24 }) => void
25 earliestAvailableBackupUnix: number
26 latestAvailableBackupUnix: number
27 disabled?: boolean
28}
29
30export function PITRForm({
31 onSubmit,
32 earliestAvailableBackupUnix,
33 latestAvailableBackupUnix,
34 disabled = false,
35}: Props) {
36 const [selectedTimezone, setSelectedTimezone] = useState<Timezone>(getClientTimezone())
37 const earliestAvailableBackup = dayjs
38 .unix(earliestAvailableBackupUnix ?? 0)
39 .tz(selectedTimezone.utc[0])
40 const latestAvailableBackup = dayjs
41 .unix(latestAvailableBackupUnix ?? 0)
42 .tz(selectedTimezone.utc[0])
43 const earliestAvailableBackupAsDate = earliestAvailableBackup.toDate()
44 const latestAvailableBackupAsDate = latestAvailableBackup.toDate()
45
46 const [selectedDateRaw, setSelectedDateRaw] = useState<Date>(latestAvailableBackup.toDate())
47
48 const selectedDate = dayjs(selectedDateRaw).tz(selectedTimezone.utc[0], true)
49 const isSelectedOnEarliestDay = selectedDate.isSame(earliestAvailableBackup, 'day')
50 const isSelectedOnLatestDay = selectedDate.isSame(latestAvailableBackup, 'day')
51 const availableDates = getDatesBetweenRange(earliestAvailableBackup, latestAvailableBackup)
52
53 const selectedTime = {
54 h: selectedDate.hour(),
55 m: selectedDate.minute(),
56 s: selectedDate.second(),
57 }
58
59 const earliestAvailableBackupTime = {
60 h: earliestAvailableBackup.hour(),
61 m: earliestAvailableBackup.minute(),
62 s: earliestAvailableBackup.second(),
63 }
64
65 const latestAvailableBackupTime = {
66 h: latestAvailableBackup.hour(),
67 m: latestAvailableBackup.minute(),
68 s: latestAvailableBackup.second(),
69 }
70
71 const recoveryTimeTargetUnix = selectedDate.unix()
72 const recoveryTimeString = selectedDate.format('DD MMM YYYY HH:mm:ss')
73 const recoveryTimeStringUtc = selectedDate.utc().format('DD MMM YYYY HH:mm:ss')
74
75 const isWithinRange = useMemo(
76 () =>
77 (!!selectedDate && selectedDate.isSame(latestAvailableBackup)) ||
78 selectedDate.isSame(earliestAvailableBackup) ||
79 (selectedDate.isBefore(latestAvailableBackup) &&
80 selectedDate.isAfter(earliestAvailableBackup)),
81 [selectedDate, latestAvailableBackup, earliestAvailableBackup]
82 )
83
84 const onUpdateDate = (date: Date) => {
85 setSelectedDateRaw(
86 constrainDateToRange(
87 dayjs(date).tz(selectedTimezone.utc[0], true),
88 earliestAvailableBackup,
89 latestAvailableBackup
90 ).toDate()
91 )
92 }
93
94 const handleSubmit = () => {
95 onSubmit({
96 recoveryTimeTargetUnix,
97 recoveryTimeString,
98 recoveryTimeStringUtc,
99 })
100 }
101
102 return (
103 <div>
104 <FormPanel
105 disabled={true}
106 footer={
107 <div className="flex items-center justify-end gap-3 p-6">
108 <ButtonTooltip
109 type="default"
110 disabled={disabled || !selectedDate || !isWithinRange}
111 onClick={handleSubmit}
112 tooltip={{
113 content: {
114 hidden: !isWithinRange,
115 side: 'bottom',
116 text: !isWithinRange
117 ? 'Selected date is out of range where backups are available'
118 : undefined,
119 },
120 }}
121 >
122 Continue
123 </ButtonTooltip>
124 </div>
125 }
126 >
127 <div className="flex justify-between px-4 md:px-10 py-6 space-x-10">
128 <div className="w-1/3 space-y-2">
129 <Calendar
130 mode="single"
131 required={true}
132 selected={selectedDateRaw}
133 onSelect={onUpdateDate}
134 defaultMonth={latestAvailableBackupAsDate}
135 startMonth={earliestAvailableBackupAsDate}
136 endMonth={latestAvailableBackupAsDate}
137 disabled={[
138 { before: earliestAvailableBackupAsDate },
139 { after: latestAvailableBackupAsDate },
140 ]}
141 classNames={{
142 day: cn(
143 '[&:not(:has(:disabled))]:border [&:not(:has(:disabled))]:border-stronger not-last:border-r-0 [&:not(:has(:disabled))]:bg-overlay-hover',
144 'rounded-none'
145 ),
146 selected: 'bg-brand-500!',
147 }}
148 />
149 {availableDates.length > 1 && (
150 <div className="flex items-center space-x-2">
151 <div className="border w-4 h-4 border-stronger bg-overlay-hover" />
152 <p className="text-xs text-foreground-light">Point in time back up available</p>
153 </div>
154 )}
155 </div>
156
157 <div className="w-2/3">
158 {!selectedDate ? (
159 <div className="h-full flex items-center justify-center">
160 <div className="mx-2">
161 <InformationBox
162 defaultVisibility
163 hideCollapse
164 icon={<HelpCircle size={14} strokeWidth={2} />}
165 title="Select a date which you'd like to restore your database to"
166 />
167 </div>
168 </div>
169 ) : (
170 <div className="space-y-8 py-2">
171 <div className="space-y-1">
172 <p className="text-sm text-foreground-light">Date to restore to</p>
173 <p className="text-3xl">
174 <span>{dayjs(selectedDate).format('DD MMM YYYY')}</span>
175 <span>
176 , {formatNumberToTwoDigits(selectedTime.h)}:
177 {formatNumberToTwoDigits(selectedTime.m)}:
178 {formatNumberToTwoDigits(selectedTime.s)}
179 </span>
180 </p>
181 </div>
182 <div className="space-y-2">
183 <div className="space-y-1">
184 <p className="text-sm text-foreground-light">Time zone</p>
185 <div className="w-[350px]">
186 <TimezoneSelection
187 selectedTimezone={selectedTimezone}
188 onSelectTimezone={setSelectedTimezone}
189 />
190 </div>
191 </div>
192 <div>
193 <div className="space-y-1">
194 <p className="text-sm text-foreground-light">Recovery time</p>
195 <TimeInput
196 defaultTime={selectedTime}
197 minimumTime={
198 isSelectedOnEarliestDay ? earliestAvailableBackupTime : undefined
199 }
200 maximumTime={isSelectedOnLatestDay ? latestAvailableBackupTime : undefined}
201 onChange={({ h, m, s }) => {
202 const newDate = dayjs(selectedDateRaw)
203 .set('hour', h)
204 .set('minute', m)
205 .set('second', s)
206
207 setSelectedDateRaw(newDate.toDate())
208 }}
209 />
210 </div>
211
212 <p className="text-sm text-foreground-light mt-8">
213 Enter a time within the available range to restore from. <br /> Backups are
214 captured every 2 minutes, allowing you to enter a time and restore your
215 database to the closest backup point. We'll match the time you enter to the
216 closest backup within the 2-minute window
217 </p>
218 </div>
219 <div className="mt-4! space-y-1">
220 <h3 className="text-sm text-foreground-light"></h3>
221 {isSelectedOnEarliestDay && (
222 <p className="text-sm text-foreground-light">
223 <strong>Earliest backup available for this date</strong>:{' '}
224 {earliestAvailableBackup.format('HH:mm:ss')}
225 </p>
226 )}
227 {isSelectedOnLatestDay && (
228 <p className="text-sm text-foreground-light">
229 <strong>Latest backup available for this date</strong>:{' '}
230 {latestAvailableBackup.format('HH:mm:ss')}
231 </p>
232 )}
233 </div>
234 </div>
235 </div>
236 )}
237 </div>
238 </div>
239 </FormPanel>
240 </div>
241 )
242}