import { PermissionAction } from '@supabase/shared-types/out/constants' import { ExternalLink } from 'lucide-react' 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 { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' 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' 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 { data: projectSettings } = useProjectSettingsV2Query({ projectRef }) 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, 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) && projectSettings?.is_sensitive 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 isFreePlan = subscription?.plan?.id === 'free' const selectedPitr = availableOptions.find((option) => option.identifier === selectedOption) const hasSufficientCompute = !!subscriptionCompute && subscriptionCompute.variant.identifier !== 'ci_micro' // These are illegal states. If they are true, we should block the user from saving them. const blockDowngradeDueToReadReplicas = hasChanges && hasReadReplicas && selectedCategory === 'off' && selectedOption === 'pitr_0' const blockDowngradeDueToHipaa = hasHipaaAddon && (selectedCategory !== 'on' || // If the project is HIPAA, we don't allow the user to downgrade below 28 days selectedPitr?.identifier !== 'pitr_28') 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 }) } } useEffect(() => { if (visible) { if (subscriptionPitr !== undefined) { setSelectedCategory('on') setSelectedOption(subscriptionPitr.variant.identifier) } else { setSelectedCategory('off') setSelectedOption('pitr_0') } } }, [visible, isLoading]) return (

Point in Time Recovery

} >

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 { if (hasHipaaAddon) { setSelectedOption('pitr_28') } 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. )} {blockDowngradeDueToHipaa ? ( PITR cannot be disabled on HIPAA projects PITR is enabled by default for all HIPAA projects and cannot be turned off. Contact support for further assistance.
) : blockDowngradeDueToReadReplicas ? ( Remove all read replicas before disabling PITR 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. ) : null} {selectedCategory === 'on' && (
{isFreePlan ? ( View available plans } > Upgrade your plan to change PITR for your project ) : !hasSufficientCompute ? ( 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