mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
* o11y: mirror and sanitize breadcrumbs Mirror Sentry breadcrumbs as the basis for our own support logging. Also adds more sanitization to breadcrumbs. * feat(support form): toggle for attaching dashboard logs Add a toggle to the support form when the category is "Dashboard bug", to attach recent dashboard logs. Users can preview the attached logs and opt out. * feat(support links): dedicated support link component Add a new component for support links, which: - Uses the serializer for support link params to ensure serialization/deserialization pairs correctly - Snapshots breadcrumbs so the attached log on the support form will be cut off at the support link click (otherwise we will get support form actions cluttering up the log) * tests(support form): extend timeout on flaky test * Minor clean up * fix(support form): allow url to specifically indicate no specified project * minor nits * Fix tests * Fix tests --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
569 lines
23 KiB
TypeScript
569 lines
23 KiB
TypeScript
import { ChevronDown } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { useEffect, useMemo, useState } from 'react'
|
|
import { toast } from 'sonner'
|
|
|
|
import { SupportCategories } from '@supabase/shared-types/out/constants'
|
|
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 { SupportLink } from 'components/interfaces/Support/SupportLink'
|
|
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, DOCS_URL } 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<number | false>(false)
|
|
const [selectedRegion, setSelectedRegion] = useState<string>(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 (
|
|
<SidePanel
|
|
visible={visible}
|
|
onCancel={onClose}
|
|
loading={isSettingUp}
|
|
disabled={!canDeployReplica}
|
|
className={cn(showNewDiskManagementUI ? 'max-w-[500px]' : '')}
|
|
header="Deploy a new read replica"
|
|
onConfirm={() => onSubmit()}
|
|
confirmText="Deploy replica"
|
|
>
|
|
<SidePanel.Content className="flex flex-col py-4 gap-y-4">
|
|
{hasOverdueInvoices ? (
|
|
<Alert_Shadcn_>
|
|
<WarningIcon />
|
|
<AlertTitle_Shadcn_>Your organization has overdue invoices</AlertTitle_Shadcn_>
|
|
<AlertDescription_Shadcn_>
|
|
<span>
|
|
Please resolve all outstanding invoices first before deploying a new read replica
|
|
</span>
|
|
<div className="mt-3">
|
|
<Button asChild type="default">
|
|
<Link href={`/org/${org?.slug}/billing#invoices`}>View invoices</Link>
|
|
</Button>
|
|
</div>
|
|
</AlertDescription_Shadcn_>
|
|
</Alert_Shadcn_>
|
|
) : !isAWSProvider ? (
|
|
<Alert_Shadcn_>
|
|
<WarningIcon />
|
|
<AlertTitle_Shadcn_>
|
|
Read replicas are only supported for projects provisioned via AWS
|
|
</AlertTitle_Shadcn_>
|
|
<AlertDescription_Shadcn_>
|
|
<span>
|
|
Projects provisioned by other cloud providers currently will not be able to use read
|
|
replicas
|
|
</span>
|
|
<DocsButton
|
|
abbrev={false}
|
|
className="mt-3"
|
|
href={`${DOCS_URL}/guides/platform/read-replicas#prerequisites`}
|
|
/>
|
|
</AlertDescription_Shadcn_>
|
|
</Alert_Shadcn_>
|
|
) : isAwsK8s ? (
|
|
<Alert_Shadcn_>
|
|
<WarningIcon />
|
|
<AlertTitle_Shadcn_>
|
|
Read replicas are not supported for AWS (Revamped) projects
|
|
</AlertTitle_Shadcn_>
|
|
<AlertDescription_Shadcn_>
|
|
<span>
|
|
Projects provisioned by other cloud providers currently will not be able to use read
|
|
replicas
|
|
</span>
|
|
</AlertDescription_Shadcn_>
|
|
</Alert_Shadcn_>
|
|
) : currentPgVersion < 15 ? (
|
|
<Alert_Shadcn_>
|
|
<WarningIcon />
|
|
<AlertTitle_Shadcn_>
|
|
Read replicas can only be deployed with projects on Postgres version 15 and above
|
|
</AlertTitle_Shadcn_>
|
|
<AlertDescription_Shadcn_>
|
|
If you'd like to use read replicas, please contact us via support
|
|
</AlertDescription_Shadcn_>
|
|
<AlertDescription_Shadcn_ className="mt-2">
|
|
<Button type="default">
|
|
<SupportLink
|
|
queryParams={{
|
|
projectRef,
|
|
category: SupportCategories.SALES_ENQUIRY,
|
|
subject: 'Enquiry on read replicas',
|
|
message: `Project DB version: ${project?.dbVersion}`,
|
|
}}
|
|
>
|
|
Contact support
|
|
</SupportLink>
|
|
</Button>
|
|
</AlertDescription_Shadcn_>
|
|
</Alert_Shadcn_>
|
|
) : !isMinimallyOnSmallCompute ? (
|
|
<Alert_Shadcn_>
|
|
<WarningIcon />
|
|
<AlertTitle_Shadcn_>
|
|
Project required to at least be on a Small compute
|
|
</AlertTitle_Shadcn_>
|
|
<AlertDescription_Shadcn_>
|
|
<span>
|
|
This is to ensure that read replicas can keep up with the primary databases'
|
|
activities.
|
|
</span>
|
|
<div className="flex items-center gap-x-2 mt-3">
|
|
<Button asChild type="default">
|
|
<Link
|
|
href={
|
|
isFreePlan
|
|
? `/org/${org?.slug}/billing?panel=subscriptionPlan&source=deployNewReplicaPanelSmallCompute`
|
|
: `/project/${projectRef}/settings/compute-and-disk`
|
|
}
|
|
>
|
|
{isFreePlan ? 'Upgrade to Pro' : 'Change compute size'}
|
|
</Link>
|
|
</Button>
|
|
<DocsButton
|
|
abbrev={false}
|
|
href={`${DOCS_URL}/guides/platform/read-replicas#prerequisites`}
|
|
/>
|
|
</div>
|
|
</AlertDescription_Shadcn_>
|
|
</Alert_Shadcn_>
|
|
) : !isWalgEnabled ? (
|
|
<Alert_Shadcn_>
|
|
<WarningIcon />
|
|
<AlertTitle_Shadcn_>
|
|
{refetchInterval !== false
|
|
? 'Physical backups are currently being enabled'
|
|
: 'Physical backups are required to deploy replicas'}
|
|
</AlertTitle_Shadcn_>
|
|
{refetchInterval === false && (
|
|
<AlertDescription_Shadcn_ className="mb-2">
|
|
Physical backups are used under the hood to spin up read replicas for your project.
|
|
</AlertDescription_Shadcn_>
|
|
)}
|
|
<AlertDescription_Shadcn_>
|
|
{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.'}
|
|
</AlertDescription_Shadcn_>
|
|
{refetchInterval !== false ? (
|
|
<AlertDescription_Shadcn_ className="mt-2">
|
|
You may start deploying read replicas thereafter once this is completed.
|
|
</AlertDescription_Shadcn_>
|
|
) : (
|
|
<AlertDescription_Shadcn_ className="flex items-center gap-x-2 mt-3">
|
|
<Button
|
|
type="default"
|
|
loading={isEnabling}
|
|
disabled={isEnabling}
|
|
onClick={() => {
|
|
if (projectRef) enablePhysicalBackups({ ref: projectRef })
|
|
}}
|
|
>
|
|
Enable physical backups
|
|
</Button>
|
|
<DocsButton
|
|
abbrev={false}
|
|
href={`${DOCS_URL}/guides/platform/read-replicas#how-are-read-replicas-made`}
|
|
/>
|
|
</AlertDescription_Shadcn_>
|
|
)}
|
|
</Alert_Shadcn_>
|
|
) : isProWithSpendCapEnabled ? (
|
|
<Alert_Shadcn_>
|
|
<WarningIcon />
|
|
<AlertTitle_Shadcn_>
|
|
Spend cap needs to be disabled to deploy replicas
|
|
</AlertTitle_Shadcn_>
|
|
<AlertDescription_Shadcn_>
|
|
<span>
|
|
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.
|
|
</span>
|
|
<div className="flex items-center gap-x-2 mt-3">
|
|
<Button asChild type="default">
|
|
<Link href={`/org/${org?.slug}/billing?panel=costControl`}>
|
|
Disable spend cap
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
</AlertDescription_Shadcn_>
|
|
</Alert_Shadcn_>
|
|
) : reachedMaxReplicas ? (
|
|
<Alert_Shadcn_>
|
|
<WarningIcon />
|
|
<AlertTitle_Shadcn_>
|
|
You can only deploy up to {maxNumberOfReplicas} read replicas at once
|
|
</AlertTitle_Shadcn_>
|
|
<AlertDescription_Shadcn_>
|
|
If you'd like to spin up another read replica, please drop an existing replica first.
|
|
</AlertDescription_Shadcn_>
|
|
{maxNumberOfReplicas === MAX_REPLICAS_BELOW_XL && (
|
|
<>
|
|
<AlertDescription_Shadcn_>
|
|
<span>
|
|
Alternatively, you may deploy up to{' '}
|
|
<span className="text-foreground">{MAX_REPLICAS_ABOVE_XL}</span> replicas if
|
|
your project is on an XL compute or higher.
|
|
</span>
|
|
<div className="flex items-center gap-x-2 mt-3">
|
|
<Button asChild type="default">
|
|
<Link
|
|
href={
|
|
isFreePlan
|
|
? `/org/${org?.slug}/billing?panel=subscriptionPlan&source=deployNewReplicaPanelMaxReplicas`
|
|
: `/project/${projectRef}/settings/compute-and-disk`
|
|
}
|
|
>
|
|
Upgrade compute size
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
</AlertDescription_Shadcn_>
|
|
</>
|
|
)}
|
|
</Alert_Shadcn_>
|
|
) : null}
|
|
|
|
<div className="flex flex-col gap-y-6 mt-2">
|
|
<Listbox
|
|
size="small"
|
|
id="region"
|
|
name="region"
|
|
disabled={!canDeployReplica}
|
|
value={selectedRegion}
|
|
onChange={setSelectedRegion}
|
|
label="Select a region to deploy your read replica in"
|
|
>
|
|
{availableRegions.map((region) => (
|
|
<Listbox.Option
|
|
key={region.key}
|
|
label={region.name}
|
|
value={region.key}
|
|
addOnBefore={() => (
|
|
<img
|
|
alt="region icon"
|
|
className="w-5 rounded-sm"
|
|
src={`${BASE_PATH}/img/regions/${region.region}.svg`}
|
|
/>
|
|
)}
|
|
>
|
|
<p className="flex items-center gap-x-2">
|
|
<span>{region.name}</span>
|
|
<span className="text-xs text-foreground-lighter font-mono">{region.region}</span>
|
|
</p>
|
|
</Listbox.Option>
|
|
))}
|
|
</Listbox>
|
|
|
|
<div className="flex flex-col gap-y-2">
|
|
{showNewDiskManagementUI ? (
|
|
<>
|
|
<Collapsible_Shadcn_>
|
|
<CollapsibleTrigger_Shadcn_ className="w-full flex items-center justify-between [&[data-state=open]>svg]:!-rotate-180">
|
|
<p className="text-sm text-left">
|
|
New replica will cost an additional{' '}
|
|
<span translate="no">
|
|
{formatCurrency(
|
|
estComputeMonthlyCost +
|
|
additionalCostDiskSize +
|
|
Number(additionalCostIOPS) +
|
|
Number(additionalCostThroughput)
|
|
)}
|
|
/month
|
|
</span>
|
|
</p>
|
|
<ChevronDown size={14} className="transition" />
|
|
</CollapsibleTrigger_Shadcn_>
|
|
<CollapsibleContent_Shadcn_ className="flex flex-col gap-y-1 mt-1">
|
|
<p className="text-foreground-light text-sm">
|
|
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.
|
|
</p>
|
|
<p className="text-foreground-light text-sm">
|
|
The additional cost for the replica breaks down to:
|
|
</p>
|
|
<Table>
|
|
<TableHeader className="font-mono uppercase text-xs [&_th]:h-auto [&_th]:pb-2 [&_th]:pt-4">
|
|
<TableRow>
|
|
<TableHead className="w-[140px] pl-0">Item</TableHead>
|
|
<TableHead>Description</TableHead>
|
|
<TableHead className="text-right pr-0">Cost (/month)</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody className="[&_td]:py-0 [&_tr]:h-[50px] [&_tr]:border-dotted">
|
|
<TableRow>
|
|
<TableCell className="pl-0">Compute size</TableCell>
|
|
<TableCell>{selectedComputeMeta?.name}</TableCell>
|
|
<TableCell className="text-right font-mono pr-0" translate="no">
|
|
{formatCurrency(estComputeMonthlyCost)}
|
|
</TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableCell className="pl-0">Disk size</TableCell>
|
|
<TableCell>
|
|
{((size_gb ?? 0) * 1.25).toLocaleString()} GB ({type})
|
|
</TableCell>
|
|
<TableCell className="text-right font-mono pr-0" translate="no">
|
|
{formatCurrency(additionalCostDiskSize)}
|
|
</TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableCell className="pl-0">IOPS</TableCell>
|
|
<TableCell>{iops?.toLocaleString()} IOPS</TableCell>
|
|
<TableCell className="text-right font-mono pr-0" translate="no">
|
|
{formatCurrency(+additionalCostIOPS)}
|
|
</TableCell>
|
|
</TableRow>
|
|
{type === 'gp3' && (
|
|
<TableRow>
|
|
<TableCell className="pl-0">Throughput</TableCell>
|
|
<TableCell>{throughput_mbps?.toLocaleString()} MB/s</TableCell>
|
|
<TableCell className="text-right font-mono pr-0">
|
|
{formatCurrency(+additionalCostThroughput)}
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</CollapsibleContent_Shadcn_>
|
|
</Collapsible_Shadcn_>
|
|
</>
|
|
) : (
|
|
<p className="text-foreground-light text-sm">
|
|
Read replicas will be on the same compute size as your primary database. Deploying a
|
|
read replica on the{' '}
|
|
<span className="text-foreground">{selectedComputeMeta?.name}</span> size incurs
|
|
additional{' '}
|
|
<span className="text-foreground" translate="no">
|
|
{selectedComputeMeta?.price_description}
|
|
</span>
|
|
.
|
|
</p>
|
|
)}
|
|
|
|
<p className="text-foreground-light text-sm">
|
|
Read more about{' '}
|
|
<Link
|
|
href={`${DOCS_URL}/guides/platform/manage-your-usage/read-replicas`}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="underline hover:text-foreground transition"
|
|
>
|
|
billing
|
|
</Link>{' '}
|
|
for read replicas.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</SidePanel.Content>
|
|
</SidePanel>
|
|
)
|
|
}
|
|
|
|
export default DeployNewReplicaPanel
|