PITRForm.tsx 8.9 KB

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