import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQueryClient } from '@tanstack/react-query' import { format } from 'date-fns' import dayjs from 'dayjs' import { ChevronLeft, ChevronRight, HelpCircle } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { useState } from 'react' import DatePicker from 'react-datepicker' import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { FormHeader } from 'components/ui/Forms/FormHeader' import { FormPanel } from 'components/ui/Forms/FormPanel' import InformationBox from 'components/ui/InformationBox' import { useBackupsQuery } from 'data/database/backups-query' import { usePitrRestoreMutation } from 'data/database/pitr-restore-mutation' import { setProjectStatus } from 'data/projects/projects-query' import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { PROJECT_STATUS } from 'lib/constants' import { Alert, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, Modal, WarningIcon, } from 'ui' import BackupsEmpty from '../BackupsEmpty' import BackupsStorageAlert from '../BackupsStorageAlert' import type { Timezone } from './PITR.types' import { constrainDateToRange, formatNumberToTwoDigits, getClientTimezone, getDatesBetweenRange, } from './PITR.utils' import PITRStatus from './PITRStatus' import TimeInput from './TimeInput' import { TimezoneSelection } from './TimezoneSelection' const PITRSelection = () => { const router = useRouter() const { ref } = useParams() const queryClient = useQueryClient() const { data: backups } = useBackupsQuery({ projectRef: ref }) const { data: databases } = useReadReplicasQuery({ projectRef: ref }) const [showConfiguration, setShowConfiguration] = useState(false) const [showConfirmation, setShowConfirmation] = useState(false) const [selectedTimezone, setSelectedTimezone] = useState(getClientTimezone()) const canTriggerPhysicalBackups = useCheckPermissions( PermissionAction.INFRA_EXECUTE, 'queue_job.restore.prepare' ) const hasReadReplicas = (databases ?? []).length > 1 const { mutate: restoreFromPitr, isLoading: isRestoring, isSuccess: isSuccessPITR, } = usePitrRestoreMutation({ onSuccess: (res, variables) => { setTimeout(() => { setShowConfirmation(false) setProjectStatus(queryClient, variables.ref, PROJECT_STATUS.RESTORING) router.push(`/project/${variables.ref}`) }, 3000) }, }) const { earliestPhysicalBackupDateUnix, latestPhysicalBackupDateUnix } = backups?.physicalBackupData ?? {} const hasNoBackupsAvailable = !earliestPhysicalBackupDateUnix || !latestPhysicalBackupDateUnix const earliestAvailableBackup = dayjs .unix(earliestPhysicalBackupDateUnix ?? 0) .tz(selectedTimezone.utc[0]) const latestAvailableBackup = dayjs .unix(latestPhysicalBackupDateUnix ?? 0) .tz(selectedTimezone.utc[0]) const [selectedDateRaw, setSelectedDateRaw] = useState(latestAvailableBackup.toDate()) const selectedDate = dayjs(selectedDateRaw).tz(selectedTimezone.utc[0], true) // true to keep local time and just change +whatever 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(), } // This will be the actual unix timestamp for the backup const recoveryTimeTargetUnix = selectedDate.unix() // Formatting from the unix again just to double check correctness const recoveryTimeString = selectedDate.format('DD MMM YYYY HH:mm:ss') const recoveryTimeStringUtc = selectedDate.utc().format('DD MMM YYYY HH:mm:ss') const isSelectedOutOfRange = selectedDate && (selectedDate.isBefore(earliestAvailableBackup) || selectedDate.isAfter(latestAvailableBackup)) const onUpdateDate = (date: Date) => { setSelectedDateRaw( constrainDateToRange( dayjs(date).tz(selectedTimezone.utc[0], true), earliestAvailableBackup, latestAvailableBackup ).toDate() ) } const onCancel = () => { setShowConfiguration(false) setSelectedTimezone(getClientTimezone()) } const onConfirmRestore = async () => { if (!ref) return console.error('Project ref is required') if (!recoveryTimeTargetUnix) return console.error('Recovery time target unix is required') restoreFromPitr({ ref, recovery_time_target_unix: recoveryTimeTargetUnix }) } return ( <> {hasNoBackupsAvailable ? ( ) : ( <> {hasReadReplicas && ( Unable to restore from PITR as project has read replicas enabled You will need to remove all read replicas first from your project's infrastructure settings prior to starting a PITR restore.
{/* [Joshen] Ideally we have some links to a docs to explain why so */} {/* */}
)} {!showConfiguration ? ( setShowConfiguration(true)} /> ) : ( setShowConfirmation(true)} tooltip={{ content: { side: 'bottom', text: isSelectedOutOfRange ? 'Selected date is out of range where backups are available' : !canTriggerPhysicalBackups ? 'You need additional permissions to trigger a restore' : undefined, }, }} > Review restore details } >
'cursor-pointer'} minDate={earliestAvailableBackup.toDate()} maxDate={latestAvailableBackup.toDate()} highlightDates={availableDates.map((date) => date.toDate())} renderCustomHeader={({ date, decreaseMonth, increaseMonth, prevMonthButtonDisabled, nextMonthButtonDisabled, }) => (
{format(date, 'MMMM yyyy')}
)} />

Point in time back up available

{!selectedDate ? (
} title="Select a date which you'd like to restore your database to" />
) : (

Restore database to

{dayjs(selectedDate).format('DD MMM YYYY')} , {formatNumberToTwoDigits(selectedTime.h)}: {formatNumberToTwoDigits(selectedTime.m)}: {formatNumberToTwoDigits(selectedTime.s)}

Time zone

Recovery time

{ const newDate = dayjs(selectedDateRaw) .set('hour', h) .set('minute', m) .set('second', s) setSelectedDateRaw(newDate.toDate()) }} />

Enter a time within the available range to restore from.
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

{isSelectedOnEarliestDay && (

Earliest backup available for this date:{' '} {earliestAvailableBackup.format('HH:mm:ss')}

)} {isSelectedOnLatestDay && (

Latest backup available for this date:{' '} {latestAvailableBackup.format('HH:mm:ss')}

)}
)}
)} )} setShowConfirmation(false)} header="Point in time recovery review" customFooter={
} >

Your database will be restored to:

{selectedTimezone?.text}

{recoveryTimeString}

(UTC+00:00)

{recoveryTimeStringUtc}

Any changes made to your database after this point in time will be lost. This includes any changes to your project's storage and authentication.

Restores may take from a few minutes up to several hours depending on the size of your database. During this period, your project will not be available, until the restoration is completed.

) } export default PITRSelection