import { PermissionAction } from '@supabase/shared-types/out/constants' import { useTheme } from 'next-themes' import Link from 'next/link' import { useEffect, useState } from 'react' import { toast } from 'sonner' import { useParams } from 'common' import { subscriptionHasHipaaAddon } from 'components/interfaces/Billing/Subscription/Subscription.utils' import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useProjectAddonRemoveMutation } from 'data/subscriptions/project-addon-remove-mutation' import { useProjectAddonUpdateMutation } from 'data/subscriptions/project-addon-update-mutation' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import type { AddonVariantId } from 'data/subscriptions/types' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { useSelectedProject } from 'hooks/misc/useSelectedProject' import { BASE_PATH } from 'lib/constants' import { formatCurrency } from 'lib/helpers' import { useAddonsPagePanel } from 'state/addons-page' import { Alert, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, CriticalIcon, Radio, SidePanel, WarningIcon, cn, } from 'ui' import { ExternalLink, AlertTriangle } from 'lucide-react' const PITR_CATEGORY_OPTIONS: { id: 'off' | 'on' name: string imageUrl: string imageUrlLight: string }[] = [ { id: 'off', name: 'Disable PITR', imageUrl: `${BASE_PATH}/img/pitr-off.svg?v=2`, imageUrlLight: `${BASE_PATH}/img/pitr-off--light.svg?v=2`, }, { id: 'on', name: 'Enable PITR', imageUrl: `${BASE_PATH}/img/pitr-on.svg?v=2`, imageUrlLight: `${BASE_PATH}/img/pitr-on--light.svg?v=2`, }, ] const PITRSidePanel = () => { const { ref: projectRef } = useParams() const { resolvedTheme } = useTheme() const project = useSelectedProject() const organization = useSelectedOrganization() const [selectedCategory, setSelectedCategory] = useState<'on' | 'off'>('off') const [selectedOption, setSelectedOption] = useState('pitr_0') const canUpdatePitr = useCheckPermissions(PermissionAction.BILLING_WRITE, 'stripe.subscriptions') const isBranchingEnabled = project?.is_branch_enabled === true || project?.parent_project_ref !== undefined const { panel, setPanel, closePanel } = useAddonsPagePanel() const visible = panel === 'pitr' const { data: databases } = useReadReplicasQuery({ projectRef }) const { data: addons, isLoading } = useProjectAddonsQuery({ projectRef }) const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) const hasHipaaAddon = subscriptionHasHipaaAddon(subscription) const { mutate: updateAddon, isLoading: isUpdating } = useProjectAddonUpdateMutation({ onSuccess: () => { toast.success(`Successfully updated point in time recovery duration`) closePanel() }, onError: (error) => { toast.error(`Unable to update PITR: ${error.message}`) }, }) const { mutate: removeAddon, isLoading: isRemoving } = useProjectAddonRemoveMutation({ onSuccess: () => { toast.success(`Successfully disabled point in time recovery`) closePanel() }, onError: (error) => { toast.error(`Unable to disable PITR: ${error.message}`) }, }) const isSubmitting = isUpdating || isRemoving const selectedAddons = addons?.selected_addons ?? [] const availableAddons = addons?.available_addons ?? [] const subscriptionCompute = selectedAddons.find((addon) => addon.type === 'compute_instance') const subscriptionPitr = selectedAddons.find((addon) => addon.type === 'pitr') const availableOptions = availableAddons.find((addon) => addon.type === 'pitr')?.variants ?? [] const hasReadReplicas = (databases ?? []).length > 1 const hasChanges = selectedOption !== (subscriptionPitr?.variant.identifier ?? 'pitr_0') const selectedPitr = availableOptions.find((option) => option.identifier === selectedOption) const isFreePlan = subscription?.plan?.id === 'free' const blockDowngradeDueToReadReplicas = hasChanges && hasReadReplicas && selectedCategory === 'off' && selectedOption === 'pitr_0' useEffect(() => { if (visible) { if (subscriptionPitr !== undefined) { setSelectedCategory('on') setSelectedOption(subscriptionPitr.variant.identifier) } else { setSelectedCategory('off') setSelectedOption('pitr_0') } } }, [visible, isLoading]) const onConfirm = async () => { if (!projectRef) return console.error('Project ref is required') if (selectedOption === 'pitr_0' && subscriptionPitr !== undefined) { removeAddon({ projectRef, variant: subscriptionPitr.variant.identifier }) } else { updateAddon({ projectRef, type: 'pitr', variant: selectedOption as AddonVariantId }) } } return (

Point in Time Recovery

} > {hasHipaaAddon && ( PITR cannot be changed with HIPAA All projects should have PITR enabled by default and cannot be changed with HIPAA enabled. Contact support for further assistance.
)}

Point-in-Time Recovery (PITR) allows a project to be backed up at much shorter intervals. This provides users an option to restore to any chosen point of up to seconds in granularity.

{PITR_CATEGORY_OPTIONS.map((option) => { const isSelected = selectedCategory === option.id return (
{ setSelectedCategory(option.id) if (option.id === 'off') { setSelectedOption('pitr_0') } else if (subscriptionPitr?.variant.identifier !== undefined) { setSelectedOption(subscriptionPitr.variant.identifier) } else { setSelectedOption('pitr_7') } }} > Point-In-Time-Recovery

{option.name}

) })}
{selectedCategory === 'off' && subscriptionPitr !== undefined && isBranchingEnabled && ( Are you sure you want to disable this while using Branching? Without PITR, you might not be able to recover lost data if you accidentally merge a branch that deletes a column or user data. We don't recommend this. )} {blockDowngradeDueToReadReplicas && ( Remove all read replicas before downgrading You currently have active read replicas. The minimum compute size for using read replicas is the Small Compute. You need to remove all read replicas before downgrading Compute as it requires at least a Small compute instance. )} {selectedCategory === 'on' && (
{isFreePlan ? ( View available plans } > Upgrade your plan to change PITR for your project ) : subscriptionCompute === undefined ? ( setPanel('computeInstance')} > Change compute size , ]} > This is to ensure that your project has enough resources to execute PITR successfully ) : null} Choose the duration of recovery

} onChange={(event: any) => setSelectedOption(event.target.value)} > {availableOptions.map((option) => ( {option.name}} value={option.identifier} >

{option.name}

Allow database restorations to any time up to{' '} {option.identifier.split('_')[1]} days ago

{formatCurrency(option.price)}

/ month

))}
)} {hasChanges && !blockDowngradeDueToReadReplicas && selectedOption !== 'pitr_0' && (

There are no immediate charges. The addon is billed at the end of your billing cycle based on your usage and prorated to the hour.

)}
) } export default PITRSidePanel