mirror of
https://github.com/supabase/supabase.git
synced 2026-06-14 23:25:16 +08:00
## Problem Now that we migrated old components to their new shadcn alternatives, we don't need the `_Shadcn_` suffix anymore. ## Solution Remove it <img width="659" height="609" alt="image" src="https://github.com/user-attachments/assets/2d7271a9-066a-4dcc-92fe-729b106d2c2f" />
337 lines
13 KiB
TypeScript
337 lines
13 KiB
TypeScript
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { useParams } from 'common'
|
|
import { useTheme } from 'next-themes'
|
|
import { useEffect, useState } from 'react'
|
|
import { toast } from 'sonner'
|
|
import {
|
|
Alert,
|
|
AlertDescription,
|
|
AlertTitle,
|
|
Button,
|
|
cn,
|
|
CriticalIcon,
|
|
RadioGroupCard,
|
|
RadioGroupCardItem,
|
|
SidePanel,
|
|
} from 'ui'
|
|
|
|
import { subscriptionHasHipaaAddon } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
|
|
import { TaxDisclaimer } from '@/components/interfaces/Billing/TaxDisclaimer'
|
|
import { SupportLink } from '@/components/interfaces/Support/SupportLink'
|
|
import { DocsButton } from '@/components/ui/DocsButton'
|
|
import { UpgradeToPro } from '@/components/ui/UpgradeToPro'
|
|
import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-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 { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
|
|
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
|
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
import { BASE_PATH, DOCS_URL } from '@/lib/constants'
|
|
import { formatCurrency } from '@/lib/helpers'
|
|
import { useAddonsPagePanel } from '@/state/addons-page'
|
|
|
|
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 { data: project } = useSelectedProjectQuery()
|
|
const { data: organization } = useSelectedOrganizationQuery()
|
|
const { data: projectSettings } = useProjectSettingsV2Query({ projectRef })
|
|
|
|
const [selectedCategory, setSelectedCategory] = useState<'on' | 'off'>('off')
|
|
const [selectedOption, setSelectedOption] = useState<string>('pitr_0')
|
|
|
|
const { can: canUpdatePitr } = useAsyncCheckPermissions(
|
|
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: addons, isPending: isLoading } = useProjectAddonsQuery({ projectRef })
|
|
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug })
|
|
const hasHipaaAddon = subscriptionHasHipaaAddon(subscription) && projectSettings?.is_sensitive
|
|
|
|
const { mutate: updateAddon, isPending: 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, isPending: 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 hasChanges = selectedOption !== (subscriptionPitr?.variant.identifier ?? 'pitr_0')
|
|
const { hasAccess: hasAccessToPitrVariants } = useCheckEntitlements('pitr.available_variants')
|
|
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 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 (
|
|
<SidePanel
|
|
size="xlarge"
|
|
visible={visible}
|
|
onCancel={closePanel}
|
|
onConfirm={onConfirm}
|
|
loading={isLoading || isSubmitting}
|
|
disabled={
|
|
!hasAccessToPitrVariants ||
|
|
isLoading ||
|
|
!hasChanges ||
|
|
isSubmitting ||
|
|
!canUpdatePitr ||
|
|
(!!selectedPitr && !hasSufficientCompute) ||
|
|
blockDowngradeDueToHipaa
|
|
}
|
|
tooltip={
|
|
blockDowngradeDueToHipaa
|
|
? 'Unable to disable PITR with HIPAA add-on'
|
|
: !hasAccessToPitrVariants
|
|
? 'Unable to enable point in time recovery on your Plan'
|
|
: !canUpdatePitr
|
|
? 'You do not have permission to update PITR'
|
|
: undefined
|
|
}
|
|
header={
|
|
<div className="flex w-full items-center justify-between">
|
|
<h4>Point in Time Recovery</h4>
|
|
<DocsButton href={`${DOCS_URL}/guides/platform/backups#point-in-time-recovery`} />
|
|
</div>
|
|
}
|
|
>
|
|
<SidePanel.Content>
|
|
<div className="py-6 space-y-4">
|
|
<p className="text-sm">
|
|
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.
|
|
</p>
|
|
|
|
<div className="mt-8! pb-4">
|
|
<div className="flex gap-3">
|
|
{PITR_CATEGORY_OPTIONS.map((option) => {
|
|
const isSelected = selectedCategory === option.id
|
|
return (
|
|
<div
|
|
key={option.id}
|
|
className={cn(
|
|
'col-span-3 group space-y-1',
|
|
!hasAccessToPitrVariants && 'opacity-75'
|
|
)}
|
|
onClick={() => {
|
|
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')
|
|
}
|
|
}
|
|
}}
|
|
>
|
|
<img
|
|
alt="Point-In-Time-Recovery"
|
|
className={cn(
|
|
'relative rounded-xl transition border bg-no-repeat bg-center bg-cover cursor-pointer w-[160px] h-[96px]',
|
|
isSelected
|
|
? 'border-foreground'
|
|
: 'border-foreground-muted opacity-50 group-hover:border-foreground-lighter group-hover:opacity-100'
|
|
)}
|
|
width={160}
|
|
height={96}
|
|
src={resolvedTheme?.includes('dark') ? option.imageUrl : option.imageUrlLight}
|
|
/>
|
|
|
|
<p
|
|
className={cn(
|
|
'text-sm transition',
|
|
isSelected ? 'text-foreground' : 'text-foreground-light'
|
|
)}
|
|
>
|
|
{option.name}
|
|
</p>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{selectedCategory === 'off' && subscriptionPitr !== undefined && isBranchingEnabled && (
|
|
<Alert variant="warning">
|
|
<CriticalIcon />
|
|
<AlertTitle>Are you sure you want to disable this while using Branching?</AlertTitle>
|
|
<AlertDescription>
|
|
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.
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{blockDowngradeDueToHipaa ? (
|
|
<Alert>
|
|
<AlertTitle>PITR cannot be disabled on HIPAA projects</AlertTitle>
|
|
<AlertDescription>
|
|
PITR is enabled by default for all HIPAA projects and cannot be turned off. Contact
|
|
support for further assistance.
|
|
</AlertDescription>
|
|
<div className="mt-4">
|
|
<Button type="default" asChild>
|
|
<SupportLink>Contact support</SupportLink>
|
|
</Button>
|
|
</div>
|
|
</Alert>
|
|
) : null}
|
|
|
|
{selectedCategory === 'on' && (
|
|
<div className="mt-8! pb-4">
|
|
{!hasAccessToPitrVariants ? (
|
|
<UpgradeToPro
|
|
className="mb-4"
|
|
addon="pitr"
|
|
primaryText="Changing your Point-In-Time-Recovery is only available on the Pro Plan"
|
|
secondaryText="Upgrade your plan to change PITR for your project."
|
|
featureProposition="enable PITR"
|
|
/>
|
|
) : !hasSufficientCompute ? (
|
|
<UpgradeToPro
|
|
className="mb-4"
|
|
addon="computeSize"
|
|
primaryText="Project needs to be at least on a Small compute size to enable PITR"
|
|
secondaryText="This ensures enough resources to execute PITR successfully."
|
|
featureProposition="enable PITR"
|
|
/>
|
|
) : null}
|
|
|
|
<label className="block text-sm text-foreground-light mb-4" htmlFor="pitr">
|
|
Choose the duration of recovery
|
|
</label>
|
|
<RadioGroupCard
|
|
id="pitr"
|
|
className="flex flex-wrap gap-3"
|
|
value={selectedOption}
|
|
onValueChange={(value) => setSelectedOption(value)}
|
|
disabled={!hasAccessToPitrVariants || subscriptionCompute === undefined}
|
|
>
|
|
{availableOptions.map((option) => (
|
|
<RadioGroupCardItem
|
|
key={option.identifier}
|
|
value={option.identifier}
|
|
id={option.identifier}
|
|
label={
|
|
<div className="w-full group">
|
|
<div className="border-b border-default px-4 py-2">
|
|
<p className="text-sm">{option.name}</p>
|
|
</div>
|
|
<div className="px-4 py-2">
|
|
<p className="text-foreground-light">
|
|
Allow database restorations to any time up to{' '}
|
|
{option.identifier.split('_')[1]} days ago
|
|
</p>
|
|
<div className="flex items-center space-x-1 mt-2">
|
|
<p className="text-foreground text-sm" translate="no">
|
|
{formatCurrency(option.price)}
|
|
</p>
|
|
<p className="text-foreground-light translate-y-px"> / month</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
showIndicator={false}
|
|
/>
|
|
))}
|
|
</RadioGroupCard>
|
|
<TaxDisclaimer className="mt-3" />
|
|
</div>
|
|
)}
|
|
|
|
{hasChanges && selectedOption !== 'pitr_0' && (
|
|
<p className="text-sm text-foreground-light">
|
|
There are no immediate charges. The add-on is billed at the end of your billing cycle
|
|
based on your usage and prorated to the hour.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</SidePanel.Content>
|
|
</SidePanel>
|
|
)
|
|
}
|
|
|
|
export default PITRSidePanel
|