| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- import dayjs from 'dayjs'
- import { HelpCircle } from 'lucide-react'
- import { useMemo, useState } from 'react'
- import { Calendar, cn } from 'ui'
- import { Timezone } from './PITR.types'
- import {
- constrainDateToRange,
- formatNumberToTwoDigits,
- getClientTimezone,
- getDatesBetweenRange,
- } from './PITR.utils'
- import TimeInput from './TimeInput'
- import { TimezoneSelection } from './TimezoneSelection'
- import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
- import { FormPanel } from '@/components/ui/Forms/FormPanel'
- import InformationBox from '@/components/ui/InformationBox'
- type Props = {
- onSubmit: (data: {
- recoveryTimeTargetUnix: number
- recoveryTimeString: string
- recoveryTimeStringUtc: string
- }) => void
- earliestAvailableBackupUnix: number
- latestAvailableBackupUnix: number
- disabled?: boolean
- }
- export function PITRForm({
- onSubmit,
- earliestAvailableBackupUnix,
- latestAvailableBackupUnix,
- disabled = false,
- }: Props) {
- const [selectedTimezone, setSelectedTimezone] = useState<Timezone>(getClientTimezone())
- const earliestAvailableBackup = dayjs
- .unix(earliestAvailableBackupUnix ?? 0)
- .tz(selectedTimezone.utc[0])
- const latestAvailableBackup = dayjs
- .unix(latestAvailableBackupUnix ?? 0)
- .tz(selectedTimezone.utc[0])
- const earliestAvailableBackupAsDate = earliestAvailableBackup.toDate()
- const latestAvailableBackupAsDate = latestAvailableBackup.toDate()
- const [selectedDateRaw, setSelectedDateRaw] = useState<Date>(latestAvailableBackup.toDate())
- const selectedDate = dayjs(selectedDateRaw).tz(selectedTimezone.utc[0], true)
- const isSelectedOnEarliestDay = selectedDate.isSame(earliestAvailableBackup, 'day')
- const isSelectedOnLatestDay = selectedDate.isSame(latestAvailableBackup, 'day')
- const availableDates = getDatesBetweenRange(earliestAvailableBackup, latestAvailableBackup)
- const selectedTime = {
- h: selectedDate.hour(),
- m: selectedDate.minute(),
- s: selectedDate.second(),
- }
- const earliestAvailableBackupTime = {
- h: earliestAvailableBackup.hour(),
- m: earliestAvailableBackup.minute(),
- s: earliestAvailableBackup.second(),
- }
- const latestAvailableBackupTime = {
- h: latestAvailableBackup.hour(),
- m: latestAvailableBackup.minute(),
- s: latestAvailableBackup.second(),
- }
- const recoveryTimeTargetUnix = selectedDate.unix()
- const recoveryTimeString = selectedDate.format('DD MMM YYYY HH:mm:ss')
- const recoveryTimeStringUtc = selectedDate.utc().format('DD MMM YYYY HH:mm:ss')
- const isWithinRange = useMemo(
- () =>
- (!!selectedDate && selectedDate.isSame(latestAvailableBackup)) ||
- selectedDate.isSame(earliestAvailableBackup) ||
- (selectedDate.isBefore(latestAvailableBackup) &&
- selectedDate.isAfter(earliestAvailableBackup)),
- [selectedDate, latestAvailableBackup, earliestAvailableBackup]
- )
- const onUpdateDate = (date: Date) => {
- setSelectedDateRaw(
- constrainDateToRange(
- dayjs(date).tz(selectedTimezone.utc[0], true),
- earliestAvailableBackup,
- latestAvailableBackup
- ).toDate()
- )
- }
- const handleSubmit = () => {
- onSubmit({
- recoveryTimeTargetUnix,
- recoveryTimeString,
- recoveryTimeStringUtc,
- })
- }
- return (
- <div>
- <FormPanel
- disabled={true}
- footer={
- <div className="flex items-center justify-end gap-3 p-6">
- <ButtonTooltip
- type="default"
- disabled={disabled || !selectedDate || !isWithinRange}
- onClick={handleSubmit}
- tooltip={{
- content: {
- hidden: !isWithinRange,
- side: 'bottom',
- text: !isWithinRange
- ? 'Selected date is out of range where backups are available'
- : undefined,
- },
- }}
- >
- Continue
- </ButtonTooltip>
- </div>
- }
- >
- <div className="flex justify-between px-4 md:px-10 py-6 space-x-10">
- <div className="w-1/3 space-y-2">
- <Calendar
- mode="single"
- required={true}
- selected={selectedDateRaw}
- onSelect={onUpdateDate}
- defaultMonth={latestAvailableBackupAsDate}
- startMonth={earliestAvailableBackupAsDate}
- endMonth={latestAvailableBackupAsDate}
- disabled={[
- { before: earliestAvailableBackupAsDate },
- { after: latestAvailableBackupAsDate },
- ]}
- classNames={{
- day: cn(
- '[&:not(:has(:disabled))]:border [&:not(:has(:disabled))]:border-stronger not-last:border-r-0 [&:not(:has(:disabled))]:bg-overlay-hover',
- 'rounded-none'
- ),
- selected: 'bg-brand-500!',
- }}
- />
- {availableDates.length > 1 && (
- <div className="flex items-center space-x-2">
- <div className="border w-4 h-4 border-stronger bg-overlay-hover" />
- <p className="text-xs text-foreground-light">Point in time back up available</p>
- </div>
- )}
- </div>
- <div className="w-2/3">
- {!selectedDate ? (
- <div className="h-full flex items-center justify-center">
- <div className="mx-2">
- <InformationBox
- defaultVisibility
- hideCollapse
- icon={<HelpCircle size={14} strokeWidth={2} />}
- title="Select a date which you'd like to restore your database to"
- />
- </div>
- </div>
- ) : (
- <div className="space-y-8 py-2">
- <div className="space-y-1">
- <p className="text-sm text-foreground-light">Date to restore to</p>
- <p className="text-3xl">
- <span>{dayjs(selectedDate).format('DD MMM YYYY')}</span>
- <span>
- , {formatNumberToTwoDigits(selectedTime.h)}:
- {formatNumberToTwoDigits(selectedTime.m)}:
- {formatNumberToTwoDigits(selectedTime.s)}
- </span>
- </p>
- </div>
- <div className="space-y-2">
- <div className="space-y-1">
- <p className="text-sm text-foreground-light">Time zone</p>
- <div className="w-[350px]">
- <TimezoneSelection
- selectedTimezone={selectedTimezone}
- onSelectTimezone={setSelectedTimezone}
- />
- </div>
- </div>
- <div>
- <div className="space-y-1">
- <p className="text-sm text-foreground-light">Recovery time</p>
- <TimeInput
- defaultTime={selectedTime}
- minimumTime={
- isSelectedOnEarliestDay ? earliestAvailableBackupTime : undefined
- }
- maximumTime={isSelectedOnLatestDay ? latestAvailableBackupTime : undefined}
- onChange={({ h, m, s }) => {
- const newDate = dayjs(selectedDateRaw)
- .set('hour', h)
- .set('minute', m)
- .set('second', s)
- setSelectedDateRaw(newDate.toDate())
- }}
- />
- </div>
- <p className="text-sm text-foreground-light mt-8">
- Enter a time within the available range to restore from. <br /> Backups are
- captured every 2 minutes, allowing you to enter a time and restore your
- database to the closest backup point. We'll match the time you enter to the
- closest backup within the 2-minute window
- </p>
- </div>
- <div className="mt-4! space-y-1">
- <h3 className="text-sm text-foreground-light"></h3>
- {isSelectedOnEarliestDay && (
- <p className="text-sm text-foreground-light">
- <strong>Earliest backup available for this date</strong>:{' '}
- {earliestAvailableBackup.format('HH:mm:ss')}
- </p>
- )}
- {isSelectedOnLatestDay && (
- <p className="text-sm text-foreground-light">
- <strong>Latest backup available for this date</strong>:{' '}
- {latestAvailableBackup.format('HH:mm:ss')}
- </p>
- )}
- </div>
- </div>
- </div>
- )}
- </div>
- </div>
- </FormPanel>
- </div>
- )
- }
|