import { ChevronDown } from 'lucide-react' import Link from 'next/link' import { useEffect, useMemo, useState } from 'react' import { toast } from 'sonner' import { useParams } from 'common' import { calculateIOPSPrice, calculateThroughputPrice, } from 'components/interfaces/DiskManagement/DiskManagement.utils' import { DISK_PRICING, DiskType, } from 'components/interfaces/DiskManagement/ui/DiskManagement.constants' import { DocsButton } from 'components/ui/DocsButton' import { useDiskAttributesQuery } from 'data/config/disk-attributes-query' import { useEnablePhysicalBackupsMutation } from 'data/database/enable-physical-backups-mutation' import { useOverdueInvoicesQuery } from 'data/invoices/invoices-overdue-query' import { useProjectDetailQuery } from 'data/projects/project-detail-query' import { Region, useReadReplicaSetUpMutation } from 'data/read-replicas/replica-setup-mutation' import { MAX_REPLICAS_ABOVE_XL, MAX_REPLICAS_BELOW_XL, useReadReplicasQuery, } from 'data/read-replicas/replicas-query' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useIsAwsK8sCloudProvider, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { AWS_REGIONS_DEFAULT, BASE_PATH } from 'lib/constants' import { formatCurrency } from 'lib/helpers' import type { AWS_REGIONS_KEYS } from 'shared-data' import { AWS_REGIONS } from 'shared-data' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, CollapsibleContent_Shadcn_, CollapsibleTrigger_Shadcn_, Collapsible_Shadcn_, Listbox, SidePanel, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, WarningIcon, cn, } from 'ui' import { AVAILABLE_REPLICA_REGIONS } from './InstanceConfiguration.constants' // [Joshen] FYI this is purely for AWS only, need to update to support Fly eventually interface DeployNewReplicaPanelProps { visible: boolean selectedDefaultRegion?: AWS_REGIONS_KEYS onSuccess: () => void onClose: () => void } const DeployNewReplicaPanel = ({ visible, selectedDefaultRegion, onSuccess, onClose, }: DeployNewReplicaPanelProps) => { const { ref: projectRef } = useParams() const { data: project } = useSelectedProjectQuery() const { data: org } = useSelectedOrganizationQuery() const { data } = useReadReplicasQuery({ projectRef }) const { data: addons, isSuccess } = useProjectAddonsQuery({ projectRef }) const { data: diskConfiguration } = useDiskAttributesQuery({ projectRef }) const isNotOnTeamOrEnterprisePlan = useMemo( () => !['team', 'enterprise'].includes(org?.plan.id ?? ''), [org] ) const { data: allOverdueInvoices } = useOverdueInvoicesQuery({ enabled: isNotOnTeamOrEnterprisePlan, }) const overdueInvoices = (allOverdueInvoices ?? []).filter( (x) => x.organization_id === project?.organization_id ) const hasOverdueInvoices = overdueInvoices.length > 0 && isNotOnTeamOrEnterprisePlan const isAwsK8s = useIsAwsK8sCloudProvider() // Opting for useState temporarily as Listbox doesn't seem to work with react-hook-form yet const [defaultRegion] = Object.entries(AWS_REGIONS).find( ([_, name]) => name === AWS_REGIONS_DEFAULT ) ?? ['ap-southeast-1'] // Will be following the primary's compute size for the time being const defaultCompute = addons?.selected_addons.find((addon) => addon.type === 'compute_instance')?.variant .identifier ?? 'ci_micro' // @ts-ignore const { size_gb, type, throughput_mbps, iops } = diskConfiguration?.attributes ?? {} const showNewDiskManagementUI = project?.cloud_provider === 'AWS' const readReplicaDiskSizes = (size_gb ?? 0) * 1.25 const additionalCostDiskSize = readReplicaDiskSizes * (DISK_PRICING[type as DiskType]?.storage ?? 0) const additionalCostIOPS = calculateIOPSPrice({ oldStorageType: type as DiskType, newStorageType: type as DiskType, oldProvisionedIOPS: 0, newProvisionedIOPS: iops ?? 0, numReplicas: 0, }).newPrice const additionalCostThroughput = type === 'gp3' ? calculateThroughputPrice({ storageType: type as DiskType, newThroughput: throughput_mbps ?? 0, oldThroughput: 0, numReplicas: 0, }).newPrice : 0 const [refetchInterval, setRefetchInterval] = useState(false) const [selectedRegion, setSelectedRegion] = useState(defaultRegion) const [selectedCompute, setSelectedCompute] = useState(defaultCompute) useProjectDetailQuery( { ref: projectRef }, { refetchInterval, refetchOnWindowFocus: false, onSuccess: (data) => { if (data.is_physical_backups_enabled) setRefetchInterval(false) }, } ) const { mutate: enablePhysicalBackups, isLoading: isEnabling } = useEnablePhysicalBackupsMutation( { onSuccess: () => { toast.success( 'Physical backups are currently being enabled, please check back in a few minutes!' ) setRefetchInterval(5000) }, } ) const { mutate: setUpReplica, isLoading: isSettingUp } = useReadReplicaSetUpMutation({ onSuccess: () => { const region = AVAILABLE_REPLICA_REGIONS.find((r) => r.key === selectedRegion)?.name toast.success(`Spinning up new replica in ${region ?? ' Unknown'}...`) onSuccess() onClose() }, }) const currentPgVersion = Number( (project?.dbVersion ?? '').split('supabase-postgres-')[1]?.split('.')[0] ) const maxNumberOfReplicas = ['ci_micro', 'ci_small', 'ci_medium', 'ci_large'].includes( selectedCompute ) ? MAX_REPLICAS_BELOW_XL : MAX_REPLICAS_ABOVE_XL const reachedMaxReplicas = (data ?? []).filter((db) => db.identifier !== projectRef).length >= maxNumberOfReplicas const isFreePlan = org?.plan.id === 'free' const isAWSProvider = project?.cloud_provider === 'AWS' const isWalgEnabled = project?.is_physical_backups_enabled const currentComputeAddon = addons?.selected_addons.find( (addon) => addon.type === 'compute_instance' ) const isProWithSpendCapEnabled = org?.plan.id === 'pro' && !org.usage_billing_enabled const isMinimallyOnSmallCompute = currentComputeAddon?.variant.identifier !== undefined && currentComputeAddon?.variant.identifier !== 'ci_micro' const canDeployReplica = !reachedMaxReplicas && currentPgVersion >= 15 && isAWSProvider && !isFreePlan && isWalgEnabled && currentComputeAddon !== undefined && !hasOverdueInvoices && !isAwsK8s && !isProWithSpendCapEnabled const computeAddons = addons?.available_addons.find((addon) => addon.type === 'compute_instance')?.variants ?? [] const selectedComputeMeta = computeAddons.find((addon) => addon.identifier === selectedCompute) const estComputeMonthlyCost = Math.floor((selectedComputeMeta?.price ?? 0) * 730) // 730 hours in a month const availableRegions = process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging' ? AVAILABLE_REPLICA_REGIONS.filter((x) => ['SOUTHEAST_ASIA', 'CENTRAL_EU', 'EAST_US'].includes(x.key) ) : AVAILABLE_REPLICA_REGIONS const onSubmit = async () => { const regionKey = AWS_REGIONS[selectedRegion as AWS_REGIONS_KEYS].code if (!projectRef) return console.error('Project is required') if (!regionKey) return toast.error('Unable to deploy replica: Unsupported region selected') const primary = data?.find((db) => db.identifier === projectRef) setUpReplica({ projectRef, region: regionKey as Region, size: primary?.size ?? 't4g.small' }) } useEffect(() => { if (visible && isSuccess) { if (selectedDefaultRegion !== undefined) { setSelectedRegion(selectedDefaultRegion) } else if (defaultRegion) { setSelectedRegion(defaultRegion) } if (defaultCompute !== undefined) setSelectedCompute(defaultCompute) } }, [visible, isSuccess]) return ( onSubmit()} confirmText="Deploy replica" > {hasOverdueInvoices ? ( Your organization has overdue invoices Please resolve all outstanding invoices first before deploying a new read replica
) : !isAWSProvider ? ( Read replicas are only supported for projects provisioned via AWS Projects provisioned by other cloud providers currently will not be able to use read replicas ) : isAwsK8s ? ( Read replicas are not supported for AWS (Revamped) projects Projects provisioned by other cloud providers currently will not be able to use read replicas ) : currentPgVersion < 15 ? ( Read replicas can only be deployed with projects on Postgres version 15 and above If you'd like to use read replicas, please contact us via support ) : !isMinimallyOnSmallCompute ? ( Project required to at least be on a Small compute This is to ensure that read replicas can keep up with the primary databases' activities.
) : !isWalgEnabled ? ( {refetchInterval !== false ? 'Physical backups are currently being enabled' : 'Physical backups are required to deploy replicas'} {refetchInterval === false && ( Physical backups are used under the hood to spin up read replicas for your project. )} {refetchInterval !== false ? 'This warning will go away once physical backups have been enabled - check back in a few minutes!' : 'Enabling physical backups will take a few minutes, after which you will be able to deploy read replicas.'} {refetchInterval !== false ? ( You may start deploying read replicas thereafter once this is completed. ) : ( )} ) : isProWithSpendCapEnabled ? ( Spend cap needs to be disabled to deploy replicas Launching a replica incurs additional disk size that will exceed the plan's quota. Disable the spend cap first to allow overages before launching a replica.
) : reachedMaxReplicas ? ( You can only deploy up to {maxNumberOfReplicas} read replicas at once If you'd like to spin up another read replica, please drop an existing replica first. {maxNumberOfReplicas === MAX_REPLICAS_BELOW_XL && ( <> Alternatively, you may deploy up to{' '} {MAX_REPLICAS_ABOVE_XL} replicas if your project is on an XL compute or higher.
)}
) : null}
{availableRegions.map((region) => ( ( region icon )} > {region.name} ))}
{showNewDiskManagementUI ? ( <>

New replica will cost an additional{' '} {formatCurrency( estComputeMonthlyCost + additionalCostDiskSize + Number(additionalCostIOPS) + Number(additionalCostThroughput) )} /month

Read replicas will match the compute size of your primary database and will include 25% more disk size than the primary database to accommodate WAL files.

The additional cost for the replica breaks down to:

Item Description Cost (/month) Compute size {selectedComputeMeta?.name} {formatCurrency(estComputeMonthlyCost)} Disk size {((size_gb ?? 0) * 1.25).toLocaleString()} GB ({type}) {formatCurrency(additionalCostDiskSize)} IOPS {iops?.toLocaleString()} IOPS {formatCurrency(+additionalCostIOPS)} {type === 'gp3' && ( Throughput {throughput_mbps?.toLocaleString()} MB/s {formatCurrency(+additionalCostThroughput)} )}
) : (

Read replicas will be on the same compute size as your primary database. Deploying a read replica on the{' '} {selectedComputeMeta?.name} size incurs additional{' '} {selectedComputeMeta?.price_description} .

)}

Read more about{' '} billing {' '} for read replicas.

) } export default DeployNewReplicaPanel