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' || (selectedPitr?.price ?? 0) < (subscriptionPitr?.variant.price ?? 0) ? subscription?.billing_via_partner === false && // Old addon billing with upfront payment subscription.usage_based_billing_project_addons === false && (

Upon clicking confirm, the add-on is removed immediately and any unused time in the current billing cycle is added as prorated credits to your organization and used in subsequent billing cycles.

) : !subscription?.billing_via_partner && (

{subscription?.usage_based_billing_project_addons === false ? ( Upon clicking confirm, the amount of{' '} {formatCurrency(selectedPitr?.price)} {' '} will be added to your monthly invoice. The addon is prepaid per month and in case of a downgrade, you get credits for the remaining time. For the current billing cycle you're immediately charged a prorated amount for the remaining days. ) : ( 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. )}

)} { // Billed via partner subscription?.billing_via_partner && // Project addons are still billed the old way (upfront payment) subscription?.usage_based_billing_project_addons === false && // Scheduled billing plan change subscription.scheduled_plan_change?.target_plan !== undefined && ( You have a scheduled subscription change that will be canceled if you change your PITR add on. ) } )}
) } export default PITRSidePanel