merge compute and disk

This commit is contained in:
Saxon Fletcher committed 2026-06-29 15:19:54 +10:00
1 parent 20290c71bd
commit 799e8d69d6
34 files changed
+1179 -1431

No files matched your search

-1
View File
@@ -276,7 +276,6 @@ These are the layout-only TanStack files. Most hold a single product layout comp
- [x] A `routes/project/$ref/settings/general.tsx` ← `pages/project/[ref]/settings/general.tsx`
- [x] A `routes/project/$ref/settings/addons.tsx` ← `pages/project/[ref]/settings/addons.tsx`
- [x] A `routes/project/$ref/settings/api.tsx` ← `pages/project/[ref]/settings/api.tsx` (sets `skipSettingsLayout: true` — page is a useEffect redirect)
- [x] A `routes/project/$ref/settings/compute-and-disk.tsx` ← `pages/project/[ref]/settings/compute-and-disk.tsx`
- [x] A `routes/project/$ref/settings/dashboard.tsx` ← `pages/project/[ref]/settings/dashboard.tsx`
- [x] A `routes/project/$ref/settings/infrastructure.tsx` ← `pages/project/[ref]/settings/infrastructure.tsx`
- [x] A `routes/project/$ref/settings/integrations.tsx` ← `pages/project/[ref]/settings/integrations.tsx`
@@ -522,9 +522,9 @@ export const CreateBranchModal = () => {
<InlineLink
onClick={() => setShowCreateBranchModal(false)}
className="pointer-events-auto"
href={`/project/${ref}/settings/compute-and-disk`}
href={`/project/${ref}/settings/infrastructure`}
>
Compute and Disk
Infrastructure
</InlineLink>
</p>
</TooltipContent>
@@ -13,7 +13,7 @@ export const AdditionalMonthlySpend = ({
be able to update the compute size and increase the disk size after the new project is
created in{' '}
<span className="font-mono text-xs tracking-tighter text-foreground-light">
Project Settings &gt; Compute and Disk
Project Settings &gt; Infrastructure
</span>
</p>
<div className="flex flex-col gap-2 text-foreground mt-4">
@@ -0,0 +1,374 @@
import { useParams } from 'common'
import dayjs from 'dayjs'
import { Activity, BarChart2, Database, ExternalLink } from 'lucide-react'
import { useMemo } from 'react'
import type { ChartConfig } from 'ui'
import { cn } from 'ui'
import {
Chart,
ChartActions,
ChartCard,
ChartContent,
ChartEmptyState,
ChartHeader,
ChartLine,
ChartLoadingState,
ChartMetric,
type ChartLineTick,
} from 'ui-patterns/Chart'
import type { InfraMonitoringAttribute } from '@/data/analytics/infra-monitoring-query'
import { useInfraMonitoringAttributesQuery } from '@/data/analytics/infra-monitoring-query'
import { formatBytes } from '@/lib/helpers'
type UsageChartDatum = {
timestamp: string
maxCpuUsage?: number
ramUsage?: number
diskIoConsumption?: number
databaseBytes?: number
walBytes?: number
systemBytes?: number
diskSizeBytes?: number
diskUsagePercent?: number
}
const USAGE_ATTRIBUTES = [
'max_cpu_usage',
'ram_usage',
'disk_io_consumption',
'disk_fs_used_system',
'disk_fs_used_wal',
'pg_database_size',
'disk_fs_size',
] as InfraMonitoringAttribute[]
const COMPUTE_CHART_CONFIG = {
maxCpuUsage: {
label: 'CPU',
color: 'hsl(var(--brand-default))',
},
ramUsage: {
label: 'Memory',
color: 'hsl(var(--warning-default))',
},
diskIoConsumption: {
label: 'Disk IO',
color: 'hsl(var(--destructive-500))',
},
} satisfies ChartConfig
const DISK_CHART_CONFIG = {
databaseUsagePercent: {
label: 'Database',
color: 'hsl(var(--brand-default))',
},
walUsagePercent: {
label: 'WAL',
color: 'hsl(var(--warning-default))',
},
systemUsagePercent: {
label: 'System',
color: 'hsl(var(--destructive-500))',
},
} satisfies ChartConfig
const PERCENTAGE_Y_AXIS_PROPS = {
domain: [0, 100] as [number, number],
allowDataOverflow: true,
ticks: [0, 25, 50, 75, 100],
tickFormatter: (value: number) => `${Math.round(Number(value))}%`,
width: 64,
}
const formatUsagePercent = (value: number | undefined) =>
value === undefined ? '—' : `${value.toFixed(0)}%`
const getUsageMetricStatus = (value: number | undefined): 'default' | 'warning' | 'negative' => {
if (value === undefined) return 'default'
if (value >= 90) return 'negative'
if (value >= 75) return 'warning'
return 'default'
}
const getWorstUsageMetricStatus = (
...values: Array<number | undefined>
): 'default' | 'warning' | 'negative' => {
const statuses = values.map(getUsageMetricStatus)
if (statuses.includes('negative')) return 'negative'
if (statuses.includes('warning')) return 'warning'
return 'default'
}
const getUsageCardClassName = (status: 'default' | 'warning' | 'negative') =>
cn(
'h-full flex flex-col transition-colors',
status === 'warning' && 'border-warning-400 bg-warning-200/30',
status === 'negative' && 'border-destructive-400 bg-destructive-200/30'
)
const toNumber = (value: string | number | undefined) => {
const parsedValue = Number(value)
return Number.isFinite(parsedValue) ? parsedValue : 0
}
const clampPercentage = (value: number) => Math.min(Math.max(value, 0), 100)
const getPeakChartValue = (data: UsageChartDatum[], dataKey: keyof UsageChartDatum) => {
if (data.length === 0) return undefined
const values = data
.map((point) => point[dataKey])
.filter((value): value is number => typeof value === 'number')
if (values.length === 0) return undefined
return Math.max(...values)
}
export const ComputeAndDiskUsageCharts = ({ className }: { className?: string }) => {
const { ref: projectRef } = useParams()
// Intentionally anchored to mount time so the query key stays stable across re-renders.
const { startDate, endDate } = useMemo(() => {
const now = dayjs()
return {
startDate: now.subtract(7, 'day').toISOString(),
endDate: now.toISOString(),
}
}, [])
const {
data: usageData,
isLoading,
isError,
} = useInfraMonitoringAttributesQuery({
projectRef,
attributes: USAGE_ATTRIBUTES,
startDate,
endDate,
interval: '1d',
})
const { computeChartData, diskChartData } = useMemo(() => {
if (!usageData || !('series' in usageData)) {
return {
computeChartData: [],
diskChartData: [],
}
}
const computeChartData = usageData.data.map((point) => ({
timestamp: point.period_start,
maxCpuUsage: clampPercentage(toNumber(point.values.max_cpu_usage)),
ramUsage: clampPercentage(toNumber(point.values.ram_usage)),
diskIoConsumption: clampPercentage(toNumber(point.values.disk_io_consumption)),
}))
const diskChartData = usageData.data.flatMap((point) => {
const databaseBytes = toNumber(point.values.pg_database_size)
const walBytes = toNumber(point.values.disk_fs_used_wal)
const systemBytes = toNumber(point.values.disk_fs_used_system)
const totalBytes = toNumber(point.values.disk_fs_size)
if (totalBytes <= 0) return []
return {
timestamp: point.period_start,
databaseBytes,
walBytes,
systemBytes,
diskSizeBytes: totalBytes,
databaseUsagePercent: clampPercentage((databaseBytes / totalBytes) * 100),
walUsagePercent: clampPercentage((walBytes / totalBytes) * 100),
systemUsagePercent: clampPercentage((systemBytes / totalBytes) * 100),
}
})
return { computeChartData, diskChartData }
}, [usageData])
const peakCpuUsage = getPeakChartValue(computeChartData, 'maxCpuUsage')
const peakMemoryUsage = getPeakChartValue(computeChartData, 'ramUsage')
const peakDiskIoUsage = getPeakChartValue(computeChartData, 'diskIoConsumption')
const computePeaks = [peakCpuUsage, peakMemoryUsage, peakDiskIoUsage].filter(
(value): value is number => value !== undefined
)
const peakComputeUsage = computePeaks.length > 0 ? Math.max(...computePeaks) : undefined
const computeUsageStatus = getWorstUsageMetricStatus(
peakCpuUsage,
peakMemoryUsage,
peakDiskIoUsage
)
const latestDiskDataPoint = diskChartData[diskChartData.length - 1]
const latestDiskUsedBytes =
(latestDiskDataPoint?.databaseBytes ?? 0) +
(latestDiskDataPoint?.walBytes ?? 0) +
(latestDiskDataPoint?.systemBytes ?? 0)
const latestDiskSizeBytes = latestDiskDataPoint?.diskSizeBytes ?? 0
const latestDiskUsage =
latestDiskSizeBytes > 0
? clampPercentage((latestDiskUsedBytes / latestDiskSizeBytes) * 100)
: undefined
const diskUsageStatus = getUsageMetricStatus(latestDiskUsage)
const databaseReportUrl = `/project/${projectRef}/observability/database`
const chartActions = [
{
label: 'View database report',
href: databaseReportUrl,
icon: <ExternalLink size={12} />,
},
]
const isComputeEmpty = computeChartData.length === 0
const isDiskEmpty = diskChartData.length === 0
return (
<div className={cn('grid grid-cols-1 gap-4 @xl:grid-cols-2 @xl:items-stretch', className)}>
<div id="cpu" className="scroll-mt-24 h-full min-h-0">
<Chart isLoading={isLoading} isErrored={isError} className="h-full">
<ChartCard className={getUsageCardClassName(computeUsageStatus)}>
<ChartHeader align="start" className="min-h-[5.5rem]">
<ChartMetric
label="Compute utilization"
tooltip="Peak CPU, memory, and disk IO usage over the last 7 days. Sustained high usage may require a larger compute size."
value={formatUsagePercent(peakComputeUsage)}
status={computeUsageStatus}
/>
<div className="flex flex-wrap items-center gap-6">
<ChartMetric
label="CPU"
tooltip="Highest daily CPU usage"
value={formatUsagePercent(peakCpuUsage)}
status={getUsageMetricStatus(peakCpuUsage)}
align="end"
/>
<ChartMetric
label="Memory"
tooltip="Highest daily memory usage"
value={formatUsagePercent(peakMemoryUsage)}
status={getUsageMetricStatus(peakMemoryUsage)}
align="end"
/>
<ChartMetric
label="Disk IO"
tooltip="Highest daily disk IO consumption"
value={formatUsagePercent(peakDiskIoUsage)}
status={getUsageMetricStatus(peakDiskIoUsage)}
align="end"
/>
<ChartActions actions={chartActions} />
</div>
</ChartHeader>
<ChartContent
className="p-0"
isEmpty={isComputeEmpty}
loadingState={<ChartLoadingState className="h-48 border-x-0 border-b-0" />}
errorState={
<ChartEmptyState
className="h-48 border-x-0 border-b-0"
icon={<Activity size={16} />}
title="Failed to load usage data"
/>
}
emptyState={
<ChartEmptyState
className="h-48 border-x-0 border-b-0"
icon={<Activity size={16} />}
title="No compute data"
description="Usage data may take a few minutes to appear."
/>
}
>
<div className="h-48 px-card pb-4 pt-2">
<ChartLine
data={computeChartData as ChartLineTick[]}
dataKey="maxCpuUsage"
dataKeys={['maxCpuUsage', 'ramUsage', 'diskIoConsumption']}
DateTimeFormat="MMM D"
isFullHeight
showGrid
showYAxis
config={COMPUTE_CHART_CONFIG}
YAxisProps={PERCENTAGE_Y_AXIS_PROPS}
/>
</div>
</ChartContent>
</ChartCard>
</Chart>
</div>
<div id="disk" className="scroll-mt-24 h-full min-h-0">
<Chart isLoading={isLoading} isErrored={isError} className="h-full">
<ChartCard className={getUsageCardClassName(diskUsageStatus)}>
<ChartHeader align="start" className="min-h-[5.5rem]">
<ChartMetric
label="Disk usage"
tooltip="Provisioned disk and used disk split by database, WAL, and system usage over the last 7 days."
value={formatUsagePercent(latestDiskUsage)}
status={diskUsageStatus}
/>
<div className="flex flex-wrap items-center gap-6">
<ChartMetric
label="Database"
tooltip="Database storage used"
value={formatBytes(latestDiskDataPoint?.databaseBytes ?? 0, 1)}
align="end"
/>
<ChartMetric
label="WAL"
tooltip="Write-ahead log storage used"
value={formatBytes(latestDiskDataPoint?.walBytes ?? 0, 1)}
align="end"
/>
<ChartMetric
label="System"
tooltip="System storage used"
value={formatBytes(latestDiskDataPoint?.systemBytes ?? 0, 1)}
align="end"
/>
<ChartActions actions={chartActions} />
</div>
</ChartHeader>
<ChartContent
className="p-0"
isEmpty={isDiskEmpty}
loadingState={<ChartLoadingState className="h-48 border-x-0 border-b-0" />}
errorState={
<ChartEmptyState
className="h-48 border-x-0 border-b-0"
icon={<BarChart2 size={16} />}
title="Failed to load usage data"
/>
}
emptyState={
<ChartEmptyState
className="h-48 border-x-0 border-b-0"
icon={<Database size={16} />}
title="No disk data"
description="Disk metrics may take a few minutes to appear."
/>
}
>
<div className="h-48 px-card pb-4 pt-2">
<ChartLine
data={diskChartData as ChartLineTick[]}
dataKey="databaseUsagePercent"
dataKeys={['databaseUsagePercent', 'walUsagePercent', 'systemUsagePercent']}
DateTimeFormat="MMM D"
isFullHeight
showGrid
showYAxis
config={DISK_CHART_CONFIG}
YAxisProps={PERCENTAGE_Y_AXIS_PROPS}
/>
</div>
</ChartContent>
</ChartCard>
</Chart>
</div>
</div>
)
}
@@ -2,24 +2,24 @@ import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { AnimatePresence, motion } from 'framer-motion'
import { ChevronRight } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { useForm } from 'react-hook-form'
import { CloudProvider } from 'shared-data'
import { toast } from 'sonner'
import {
Button,
cn,
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
DialogSectionSeparator,
Form,
Separator,
} from 'ui'
import { Button, Card, CardContent, cn, Form } from 'ui'
import { Admonition } from 'ui-patterns'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
PageSection,
PageSectionAside,
PageSectionContent,
PageSectionDescription,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import { FormFooterChangeBadge } from '../DataWarehouse/FormFooterChangeBadge'
import { ComputeAndDiskUsageCharts } from './ComputeAndDiskUsageCharts'
import { CreateDiskStorageSchema, DiskStorageSchemaType } from './DiskManagement.schema'
import { DiskManagementMessage } from './DiskManagement.types'
import {
@@ -28,12 +28,18 @@ import {
} from './DiskManagement.utils'
import { DiskMangementRestartRequiredSection } from './DiskManagementRestartRequiredSection'
import { DiskManagementReviewAndSubmitDialog } from './DiskManagementReviewAndSubmitDialog/DiskManagementReviewAndSubmitDialog'
import { useDiskManagementReviewChanges } from './DiskManagementReviewAndSubmitDialog/DiskManagementReviewAndSubmitDialog.hooks'
import { AutoScaleFields } from './fields/AutoScaleFields'
import { ComputeSizeField } from './fields/ComputeSizeField'
import {
ComputeSectionBillingBadge,
ComputeSizeField,
ComputeSizeFieldMeta,
} from './fields/ComputeSizeField'
import { DiskSizeField } from './fields/DiskSizeField'
import { IOPSField } from './fields/IOPSField'
import { StorageTypeField } from './fields/StorageTypeField'
import { ThroughputField } from './fields/ThroughputField'
import { BillingChangeBadge } from './ui/BillingChangeBadge'
import { DiskCountdownRadial } from './ui/DiskCountdownRadial'
import {
DISK_LIMITS,
@@ -41,12 +47,9 @@ import {
PLAN_DETAILS,
RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3,
} from './ui/DiskManagement.constants'
import { NoticeBar } from './ui/NoticeBar'
import { SpendCapDisabledSection } from './ui/SpendCapDisabledSection'
import {
MAX_WIDTH_CLASSES,
PADDING_CLASSES,
ScaffoldContainer,
} from '@/components/layouts/Scaffold'
import { PADDING_CLASSES } from '@/components/layouts/Scaffold'
import { DocsButton } from '@/components/ui/DocsButton'
import { RequestUpgradeToBillingOwners } from '@/components/ui/RequestUpgradeToBillingOwners'
import { UpgradeToPro } from '@/components/ui/UpgradeToPro'
@@ -75,13 +78,14 @@ import {
} from '@/hooks/misc/useSelectedProject'
import { DOCS_URL, GB, PROJECT_STATUS } from '@/lib/constants'
export function DiskManagementForm() {
export function DiskManagementForm({ chartsClassName }: { chartsClassName?: string } = {}) {
const { ref: projectRef } = useParams()
const { data: project, isPending: isProjectPending } = useSelectedProjectQuery()
const { data: org } = useSelectedOrganizationQuery()
const { setProjectStatus } = useSetProjectStatus()
const advancedSettingsRef = useRef<HTMLDivElement>(null)
const autoscaleSettingsRef = useRef<HTMLDivElement>(null)
const storageSettingsRef = useRef<HTMLDivElement>(null)
const isSpendCapEnabled =
org?.plan.id !== 'free' && !org?.usage_billing_enabled && project?.cloud_provider !== 'FLY'
@@ -110,7 +114,6 @@ export function DiskManagementForm() {
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false)
const [refetchInterval, setRefetchInterval] = useState<number | false>(false)
const [message, setMessageState] = useState<DiskManagementMessage | null>(null)
const [advancedSettingsOpen, setAdvancedSettingsOpenState] = useState(false)
const { data: databases, isSuccess: isReadReplicasSuccess } = useReadReplicasQuery({ projectRef })
const { data, isSuccess: isDiskAttributesSuccess } = useDiskAttributesQuery(
@@ -183,6 +186,18 @@ export function DiskManagementForm() {
const readReplicas = (databases ?? []).filter((db) => db.identifier !== projectRef)
const isPlanUpgradeRequired = !hasAccess
const {
computeSizePrice,
diskSizePrice,
totalBeforePrice,
totalAfterPrice,
advancedBeforePrice,
advancedAfterPrice,
showComputeBillingBadge,
showDiskBillingBadge,
showAdvancedBillingBadge,
} = useDiskManagementReviewChanges(form, readReplicas.length)
const { formState } = form
const errors = formState.errors
const usedSize = Math.round(((diskUtil?.metrics.fs_used_bytes ?? 0) / GB) * 100) / 100
@@ -345,7 +360,7 @@ export function DiskManagementForm() {
form.setValue('provisionedIOPS', DISK_LIMITS['gp3'].minIops)
}
}
}, [modifiedComputeSize, isDialogOpen, project])
}, [modifiedComputeSize, form, isDialogOpen, project])
useEffect(() => {
// Initialize field values properly when data has been loaded, preserving any user changes
@@ -357,218 +372,247 @@ export function DiskManagementForm() {
useEffect(() => {
const fieldErrors = Object.keys(errors)
if (fieldErrors.length > 0) {
if (
fieldErrors.includes('throughput') ||
fieldErrors.includes('provisionedIOPS') ||
fieldErrors.includes('maxSizeGb')
) {
setAdvancedSettingsOpenState(true)
if (fieldErrors.length === 0) return
// [Joshen] The timeout is to let the collapsible open prior to scrolling
const timeoutId = setTimeout(() => {
advancedSettingsRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}, 100)
const scrollTarget =
fieldErrors.includes('maxSizeGb') ||
fieldErrors.includes('growthPercent') ||
fieldErrors.includes('minIncrementGb')
? autoscaleSettingsRef
: fieldErrors.includes('throughput') || fieldErrors.includes('provisionedIOPS')
? storageSettingsRef
: null
return () => clearTimeout(timeoutId)
}
}
if (!scrollTarget) return
const timeoutId = setTimeout(() => {
scrollTarget.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}, 100)
return () => clearTimeout(timeoutId)
}, [errors])
return (
<>
<ScaffoldContainer className="relative flex flex-col gap-10" bottomPadding>
{isEntitlementsLoaded && isPlanUpgradeRequired && (
<UpgradeToPro
featureProposition="configure compute and disk"
primaryText="Only available on Pro Plan and above"
secondaryText="Upgrade to the Pro Plan to configure compute and disk settings."
/>
)}
<Form {...form}>
<form id="disk-compute-form" onSubmit={form.handleSubmit(onSubmit)}>
<PageContainer size="default" className="pb-16">
<ComputeAndDiskUsageCharts className={chartsClassName} />
{(isProjectResizing ||
isProjectRequestingDiskChanges ||
(isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions)) && (
<div className="relative flex flex-col gap-10">
<DiskMangementRestartRequiredSection
visible={isProjectResizing}
title="Your project will now automatically restart."
description="Your project will be unavailable for up to 2 mins."
{isEntitlementsLoaded && isPlanUpgradeRequired && (
<UpgradeToPro
featureProposition="configure compute and disk"
primaryText="Only available on Pro Plan and above"
secondaryText="Upgrade to the Pro Plan to configure compute and disk settings."
/>
{isProjectRequestingDiskChanges && (
<Admonition
)}
{(isProjectResizing ||
isProjectRequestingDiskChanges ||
(isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions)) && (
<div className="relative flex flex-col gap-10">
<DiskMangementRestartRequiredSection
visible={isProjectResizing}
title="Your project will now automatically restart."
description="Your project will be unavailable for up to 2 mins."
/>
<NoticeBar
type="default"
layout="horizontal"
visible={isProjectRequestingDiskChanges}
title="Disk configuration changes have been requested"
description="The requested changes will be applied to your disk shortly"
/>
)}
{isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions && (
<Admonition
<NoticeBar
type="default"
layout="horizontal"
visible={isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions}
title="You do not have permission to update disk configuration"
description="Please contact your organization administrator to update your disk configuration"
/>
)}
</div>
)}
<Separator />
</ScaffoldContainer>
<Form {...form}>
<form
id="disk-compute-form"
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-8"
>
<ScaffoldContainer className="relative flex flex-col gap-10" bottomPadding>
<ComputeSizeField form={form} disabled={disableComputeInputs} />
{isDiskNoticeVisible && <Separator />}
<SpendCapDisabledSection currentDiskSizeGb={defaultValues.totalSize} />
<div className="flex flex-col gap-y-4">
{isDiskNoticeVisible && (
<Admonition
type="default"
layout="horizontal"
title="Disk configuration is only available for projects in the AWS cloud provider"
description={
isAwsK8s
? 'Configuring your disk for AWS (Revamped) projects is unavailable for now.'
: isBranch
? 'Delete and recreate your Preview Branch to configure disk size. It was deployed on an older branching infrastructure.'
: 'The Fly Postgres offering is deprecated - please migrate your instance to the AWS cloud prov to configure your disk.'
}
/>
)}
{isAws && (
<>
<div className="flex flex-col gap-y-3">
<DiskCountdownRadial />
{!isReadOnlyMode && usedPercentage >= 90 && isWithinCooldownWindow && (
<Admonition
type="destructive"
title="Database size is currently over 90% of disk size"
description="Your project will enter read-only mode once you reach 95% of the disk space to prevent your database from exceeding the disk limitations"
>
<DocsButton
abbrev={false}
className="mt-2"
href={`${DOCS_URL}/guides/platform/database-size#read-only-mode`}
/>
</Admonition>
)}
{isReadOnlyMode && (
<Admonition
type="destructive"
title="Project is currently in read-only mode"
description="You will need to manually override read-only mode and reduce the database size to below 95% of the disk size"
>
<DocsButton
abbrev={false}
className="mt-2"
href={`${DOCS_URL}/guides/platform/database-size#disabling-read-only-mode`}
/>
</Admonition>
)}
</div>
<DiskSizeField
form={form}
disableInput={disableDiskSizeInput}
setAdvancedSettingsOpenState={setAdvancedSettingsOpenState}
/>
</>
)}
</div>
)}
{isAws && (
<>
<Separator />
<Collapsible
open={advancedSettingsOpen}
onOpenChange={() => setAdvancedSettingsOpenState((prev) => !prev)}
>
<CollapsibleTrigger className="px-card py-3 w-full border flex items-center gap-6 rounded-t data-closed:rounded-b group justify-between">
<div className="flex flex-col items-start">
<span className="text-sm text-foreground">Advanced disk settings</span>
<span className="text-sm text-foreground-light text-left">
Specify additional settings for your disk, including autoscaling
configuration, IOPS, throughput, and disk type.
</span>
</div>
<ChevronRight
size={16}
className="text-foreground-light transition-all group-data-open:rotate-90"
strokeWidth={1}
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Scaling</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<PageSection className="pt-0">
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle className="heading-default text-base">
Compute size
</PageSectionTitle>
<PageSectionDescription>
<ComputeSizeFieldMeta />
</PageSectionDescription>
</PageSectionSummary>
<PageSectionAside>
<ComputeSectionBillingBadge
form={form}
show={showComputeBillingBadge}
beforePrice={Number(computeSizePrice.oldPrice)}
afterPrice={Number(computeSizePrice.newPrice)}
/>
</CollapsibleTrigger>
<CollapsibleContent
ref={advancedSettingsRef}
className={cn(
'transition-all rounded-b',
'border border-t-0 data-closed:animate-collapsible-up data-open:animate-collapsible-down'
)}
>
<div className="flex flex-col gap-y-8 py-8">
<div className="px-card flex flex-col gap-y-8">
<AutoScaleFields form={form} />
</div>
<DialogSectionSeparator />
<div className="px-card flex flex-col gap-y-8">
{!!disableIopsThroughputConfig && (
<DocsButton href={`${DOCS_URL}/guides/platform/compute-and-disk`} />
</PageSectionAside>
</PageSectionMeta>
<PageSectionContent>
<ComputeSizeField form={form} disabled={disableComputeInputs} />
</PageSectionContent>
</PageSection>
<PageSection id="disk-size">
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle className="heading-default text-base">Disk</PageSectionTitle>
<PageSectionDescription>
Configure provisioned storage for your primary database.
</PageSectionDescription>
</PageSectionSummary>
<PageSectionAside>
<BillingChangeBadge
show={showDiskBillingBadge}
beforePrice={Number(diskSizePrice.oldPrice)}
afterPrice={Number(diskSizePrice.newPrice)}
/>
<DocsButton href={`${DOCS_URL}/guides/platform/database-size`} />
</PageSectionAside>
</PageSectionMeta>
<PageSectionContent className="flex flex-col gap-4">
<SpendCapDisabledSection currentDiskSizeGb={defaultValues.totalSize} />
<NoticeBar
type="default"
visible={isDiskNoticeVisible}
title="Disk configuration is only available for projects in the AWS cloud provider"
description={
isAwsK8s
? 'Configuring your disk for AWS (Revamped) projects is unavailable for now.'
: isBranch
? 'Delete and recreate your Preview Branch to configure disk size. It was deployed on an older branching infrastructure.'
: 'The Fly Postgres offering is deprecated - please migrate your instance to the AWS cloud prov to configure your disk.'
}
/>
{isAws && (
<>
<div className="flex flex-col gap-y-3">
<DiskCountdownRadial />
{!isReadOnlyMode && usedPercentage >= 90 && isWithinCooldownWindow && (
<Admonition
type="default"
title="Adjusting disk configuration requires LARGE Compute size or above"
description={`Increase your compute size to adjust your disk's storage type, ${form.getValues('storageType') === 'gp3' ? 'IOPS, ' : ''} and throughput`}
actions={
canUpdateDiskConfiguration ? (
<Button
variant="default"
onClick={() => {
form.setValue('computeSize', 'ci_large')
}}
>
Change to LARGE Compute
</Button>
) : (
<RequestUpgradeToBillingOwners
addon="computeSize"
featureProposition="adjust disk configuration"
/>
)
}
/>
)}
{isDiskTooSmallForCustomIops &&
!disableIopsThroughputConfig &&
!disableDiskInputs && (
<Admonition
type="default"
title="Increase disk size to adjust IOPS or throughput"
description={`This disk is too small to update IOPS or throughput, since gp3 volumes are capped at 500 IOPS per GB with a 3,000 IOPS minimum. Resizing to ${suggestedDiskSizeForCustomIops} GB unlocks custom IOPS and throughput, and leaves headroom for further adjustments (disk config changes are locked for 4 hours after each resize).`}
actions={
!disableDiskSizeInput ? (
<Button
variant="default"
onClick={() => {
form.setValue('totalSize', suggestedDiskSizeForCustomIops, {
shouldDirty: true,
shouldValidate: true,
})
}}
>
Increase to {suggestedDiskSizeForCustomIops} GB
</Button>
) : undefined
}
type="destructive"
title="Database size is currently over 90% of disk size"
description="Your project will enter read-only mode once you reach 95% of the disk space to prevent your database from exceeding the disk limitations"
>
<DocsButton
abbrev={false}
className="mt-2"
href={`${DOCS_URL}/guides/platform/database-size#read-only-mode`}
/>
)}
</Admonition>
)}
{isReadOnlyMode && (
<Admonition
type="destructive"
title="Project is currently in read-only mode"
description="You will need to manually override read-only mode and reduce the database size to below 95% of the disk size"
>
<DocsButton
abbrev={false}
className="mt-2"
href={`${DOCS_URL}/guides/platform/database-size#disabling-read-only-mode`}
/>
</Admonition>
)}
</div>
<Card>
<CardContent>
<DiskSizeField form={form} disableInput={disableDiskSizeInput} />
</CardContent>
</Card>
</>
)}
</PageSectionContent>
</PageSection>
{isAws && (
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle className="heading-default text-base">
Advanced
</PageSectionTitle>
<PageSectionDescription>
Configure autoscaling, storage type, IOPS, and throughput.
</PageSectionDescription>
</PageSectionSummary>
<PageSectionAside>
<BillingChangeBadge
show={showAdvancedBillingBadge}
beforePrice={advancedBeforePrice}
afterPrice={advancedAfterPrice}
/>
</PageSectionAside>
</PageSectionMeta>
<PageSectionContent className="flex flex-col gap-4">
<Card ref={autoscaleSettingsRef} className="scroll-mt-24">
<CardContent className="flex flex-col gap-y-8">
<AutoScaleFields form={form} />
</CardContent>
</Card>
<Card ref={storageSettingsRef} className="scroll-mt-24">
<CardContent className="flex flex-col gap-y-8">
<NoticeBar
type="default"
visible={!!disableIopsThroughputConfig}
title="Adjusting disk configuration requires LARGE Compute size or above"
description={`Increase your compute size to adjust your disk's storage type, ${form.getValues('storageType') === 'gp3' ? 'IOPS, ' : ''} and throughput`}
actions={
canUpdateDiskConfiguration ? (
<Button
variant="default"
onClick={() => {
form.setValue('computeSize', 'ci_large')
}}
>
Change to LARGE Compute
</Button>
) : (
<RequestUpgradeToBillingOwners
addon="computeSize"
featureProposition="adjust disk configuration"
/>
)
}
/>
<NoticeBar
type="default"
visible={
isDiskTooSmallForCustomIops &&
!disableIopsThroughputConfig &&
!disableDiskInputs
}
title={`Increase disk size to adjust IOPS or throughput`}
description={`This disk is too small to update IOPS or throughput, since gp3 volumes are capped at 500 IOPS per GB with a 3,000 IOPS minimum. Resizing to ${suggestedDiskSizeForCustomIops} GB unlocks custom IOPS and throughput, and leaves headroom for further adjustments (disk config changes are locked for 4 hours after each resize).`}
actions={
!disableDiskSizeInput ? (
<Button
variant="default"
onClick={() => {
form.setValue('totalSize', suggestedDiskSizeForCustomIops, {
shouldDirty: true,
shouldValidate: true,
})
}}
>
Increase to {suggestedDiskSizeForCustomIops} GB
</Button>
) : undefined
}
/>
<StorageTypeField
form={form}
disableInput={disableIopsThroughputConfig || disableDiskInputs}
@@ -589,55 +633,59 @@ export function DiskManagementForm() {
isDiskTooSmallForCustomIops
}
/>
</div>
</div>
</CollapsibleContent>
</Collapsible>
</>
)}
</ScaffoldContainer>
</CardContent>
</Card>
</PageSectionContent>
</PageSection>
)}
</PageSectionContent>
</PageSection>
</PageContainer>
<AnimatePresence>
{isDirty ? (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{ duration: 0.1, delay: 0.2 }}
className="z-10 w-full left-0 right-0 sticky bottom-0 bg-surface-100 border-t h-16 items-center flex"
<AnimatePresence>
{isDirty ? (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{ duration: 0.1, delay: 0.2 }}
className="z-10 w-full left-0 right-0 sticky bottom-0 bg-surface-100 border-t h-16 items-center flex"
>
<div
className={cn(
'mx-auto w-full max-w-[1200px]',
PADDING_CLASSES,
'flex items-center justify-end gap-3'
)}
>
<div
className={cn(
MAX_WIDTH_CLASSES,
PADDING_CLASSES,
'flex items-center gap-3 justify-end'
)}
<BillingChangeBadge
show={isDirty}
beforePrice={totalBeforePrice}
afterPrice={totalAfterPrice}
/>
<Button
variant="default"
onClick={() => form.reset()}
disabled={!isDirty}
size="medium"
>
<FormFooterChangeBadge formState={formState} />
<Button
variant="default"
onClick={() => form.reset()}
disabled={!isDirty}
size="medium"
>
Cancel
</Button>
<DiskManagementReviewAndSubmitDialog
loading={isUpdatingConfig}
disabled={noPermissions}
form={form}
numReplicas={readReplicas.length}
isDialogOpen={isDialogOpen}
onSubmit={onSubmit}
setIsDialogOpen={setIsDialogOpen}
message={message}
/>
</div>
</motion.div>
) : null}
</AnimatePresence>
</form>
</Form>
</>
Cancel
</Button>
<DiskManagementReviewAndSubmitDialog
loading={isUpdatingConfig}
disabled={noPermissions}
form={form}
numReplicas={readReplicas.length}
isDialogOpen={isDialogOpen}
onSubmit={onSubmit}
setIsDialogOpen={setIsDialogOpen}
message={message}
/>
</div>
</motion.div>
) : null}
</AnimatePresence>
</form>
</Form>
)
}
@@ -30,11 +30,11 @@ export function DiskManagementPanelForm() {
type="default"
layout="responsive"
title="Disk Management has moved"
description="Disk configuration is now managed alongside Project Compute on the new Compute and Disk page."
description="Disk configuration is now managed alongside Project Compute on the Infrastructure page."
actions={
<Button variant="default" asChild>
<Link href={`/project/${projectRef}/settings/compute-and-disk`}>
Go to Compute and Disk
<Link href={`/project/${projectRef}/settings/infrastructure`}>
Go to Infrastructure
</Link>
</Button>
}
@@ -22,6 +22,8 @@ export function useDiskManagementReviewChanges(
form: UseFormReturn<DiskStorageSchemaType>,
numReplicas: number
) {
form.watch(['computeSize', 'totalSize', 'storageType', 'provisionedIOPS', 'throughput'])
const { data: project } = useSelectedProjectQuery()
const { data: org } = useSelectedOrganizationQuery()
const isAwsNimbus = useIsAwsNimbusCloudProvider()
@@ -78,6 +80,24 @@ export function useDiskManagementReviewChanges(
Number(iopsPrice.newPrice) +
Number(throughputPrice.newPrice)
const advancedBeforePrice = Number(iopsPrice.oldPrice) + Number(throughputPrice.oldPrice)
const advancedAfterPrice = Number(iopsPrice.newPrice) + Number(throughputPrice.newPrice)
const { isDirty, dirtyFields, errors } = form.formState
const showComputeBillingBadge = isDirty && !!dirtyFields.computeSize && !errors.computeSize
const showDiskBillingBadge =
isDirty &&
!errors.totalSize &&
Number(diskSizePrice.oldPrice) !== Number(diskSizePrice.newPrice)
const showAdvancedBillingBadge =
isDirty &&
advancedBeforePrice !== advancedAfterPrice &&
!errors.provisionedIOPS &&
!errors.throughput
// --- Change flags ---
const hasComputeChanges =
@@ -166,6 +186,11 @@ export function useDiskManagementReviewChanges(
throughputPrice,
totalBeforePrice,
totalAfterPrice,
advancedBeforePrice,
advancedAfterPrice,
showComputeBillingBadge,
showDiskBillingBadge,
showAdvancedBillingBadge,
// change flags
hasComputeChanges,
hasTotalSizeChanges,
@@ -55,7 +55,7 @@ export const AutoScaleFields = ({ form }: AutoScaleFieldProps) => {
render={({ field }) => {
return (
<FormItemLayout
layout="horizontal"
layout="flex-row-reverse"
label="Autoscale growth percent"
id={field.name}
labelOptional="Percentage of current disk size to grow"
@@ -101,7 +101,7 @@ export const AutoScaleFields = ({ form }: AutoScaleFieldProps) => {
render={({ field }) => {
return (
<FormItemLayout
layout="horizontal"
layout="flex-row-reverse"
label="Minimum increment"
id={field.name}
labelOptional="Minimum value to autoscale disk size by"
@@ -147,7 +147,7 @@ export const AutoScaleFields = ({ form }: AutoScaleFieldProps) => {
render={({ field }) => {
return (
<FormItemLayout
layout="horizontal"
layout="flex-row-reverse"
label="Maximum disk size"
id={field.name}
labelOptional="Maximum size that the disk can grow to"
@@ -1,12 +1,14 @@
import { SupportCategories } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { ChevronRight, CpuIcon, Lock, Microchip } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
import { CpuIcon, Lock, Microchip } from 'lucide-react'
import { useMemo } from 'react'
import { UseFormReturn } from 'react-hook-form'
import {
Button,
cn,
FormField,
HoverCard,
HoverCardContent,
HoverCardTrigger,
RadioGroupCard,
RadioGroupCardItem,
Skeleton,
@@ -14,20 +16,14 @@ import {
TooltipContent,
TooltipTrigger,
} from 'ui'
import { Admonition, ComputeBadge } from 'ui-patterns'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { ComputeBadge } from 'ui-patterns'
import { DiskStorageSchemaType } from '../DiskManagement.schema'
import { ComputeInstanceAddonVariantId, InfraInstanceSize } from '../DiskManagement.types'
import {
calculateComputeSizePrice,
ComputeAddonVariant,
getAvailableComputeOptions,
} from '../DiskManagement.utils'
import { ComputeAddonVariant, getAvailableComputeOptions } from '../DiskManagement.utils'
import { BillingChangeBadge } from '../ui/BillingChangeBadge'
import FormMessage from '../ui/FormMessage'
import { SupportLink } from '@/components/interfaces/Support/SupportLink'
import { DocsButton } from '@/components/ui/DocsButton'
import { InlineLink } from '@/components/ui/InlineLink'
import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
@@ -35,7 +31,6 @@ import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { getCloudProviderArchitecture } from '@/lib/cloudprovider-utils'
import { DOCS_URL } from '@/lib/constants'
const INITIALLY_VISIBLE_COUNT = 6
@@ -47,6 +42,42 @@ type ComputeSizeFieldProps = {
disabled?: boolean
}
export function ComputeSizeFieldMeta() {
return <p>Hardware resources allocated to your Postgres database</p>
}
type ComputeSectionBillingBadgeProps = {
form: UseFormReturn<DiskStorageSchemaType>
show: boolean
beforePrice: number
afterPrice: number
}
export function ComputeSectionBillingBadge({
form,
show,
beforePrice,
afterPrice,
}: ComputeSectionBillingBadgeProps) {
const { data: project } = useSelectedProjectQuery()
const { hasAccess: entitledUpdateCompute } = useCheckEntitlements(
'instances.compute_update_available_sizes'
)
const computeSize = form.watch('computeSize')
const projectComputeSize = project?.infra_compute_size ?? 'nano'
const showUpgradeBadge = entitledUpdateCompute && projectComputeSize === 'nano'
return (
<BillingChangeBadge
show={show}
beforePrice={beforePrice}
afterPrice={afterPrice}
free={showUpgradeBadge && computeSize === 'ci_micro'}
/>
)
}
export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
const { ref } = useParams()
const { data: org } = useSelectedOrganizationQuery()
@@ -57,8 +88,6 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
const showComputePrice = useIsFeatureEnabled('project_addons:show_compute_price')
const { computeSize } = form.watch()
const {
data: addons,
isPending: isAddonsLoading,
@@ -67,7 +96,7 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
const isLoading = isProjectLoading || isAddonsLoading || isEntitlementLoading
const { control, formState, setValue, trigger } = form
const { control, setValue, trigger } = form
const availableAddons = useMemo(() => {
return addons?.available_addons ?? []
@@ -81,44 +110,11 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
return getAvailableComputeOptions(availableAddons, project?.cloud_provider)
}, [availableAddons, project?.cloud_provider])
// Expand by default if the project's current compute size is beyond the initial visible set
const [showAllSizes, setShowAllSizes] = useState(() => {
const idx = availableOptions.findIndex((o) => o.identifier === computeSize)
return idx >= INITIALLY_VISIBLE_COUNT
})
// Expand whenever the selected size falls outside the visible set — covers both initial data
// load (availableOptions starts empty) and computeSize changes after mount (e.g. form reset)
useEffect(() => {
const idx = availableOptions.findIndex((o) => o.identifier === computeSize)
if (idx >= INITIALLY_VISIBLE_COUNT) {
setShowAllSizes(true)
}
}, [computeSize, availableOptions])
const subscriptionPitr = addons?.selected_addons.find((addon) => addon.type === 'pitr')
const computeSizePrice = calculateComputeSizePrice({
availableOptions: availableOptions,
oldComputeSize: form.formState.defaultValues?.computeSize || 'ci_micro',
newComputeSize: form.getValues('computeSize'),
plan: org?.plan.id ?? 'free',
})
const projectComputeSize = project?.infra_compute_size ?? 'nano'
const showUpgradeBadge = entitledUpdateCompute && projectComputeSize === 'nano'
const selectedOptionIndex = availableOptions.findIndex((o) => o.identifier === computeSize)
const selectedOptionIsHidden = selectedOptionIndex >= INITIALLY_VISIBLE_COUNT
// Always show all options if the selected one would be outside the visible slice,
// so the active card is never hidden from the user.
const visibleOptions =
showAllSizes || selectedOptionIsHidden
? availableOptions
: availableOptions.slice(0, INITIALLY_VISIBLE_COUNT)
const hasHiddenOptions = availableOptions.length > INITIALLY_VISIBLE_COUNT
return (
<FormField
name="computeSize"
@@ -137,267 +133,224 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
defaultValue={field.value}
disabled={disabled}
>
<FormItemLayout
layout="horizontal"
label="Compute size"
id={field.name}
className="gap-5"
labelOptional={
<>
<BillingChangeBadge
className="mb-2"
show={
formState.isDirty &&
formState.dirtyFields.computeSize &&
!formState.errors.computeSize
}
beforePrice={Number(computeSizePrice.oldPrice)}
afterPrice={Number(computeSizePrice.newPrice)}
free={showUpgradeBadge && computeSize === 'ci_micro' ? true : false}
/>
<p className="text-foreground-lighter">
Hardware resources allocated to your Postgres database
</p>
<div className="mt-3">
<DocsButton
abbrev={false}
href={`${DOCS_URL}/guides/platform/compute-and-disk`}
/>
</div>
{showUpgradeBadge && form.watch('computeSize') === 'ci_nano' && (
<Admonition
showIcon={false}
type="default"
className="mt-3 border-violet-900 bg-violet-200 [&_h5]:text-violet-1100"
title="Upgrade to Micro Compute"
description="This Project is already paying for Micro Compute. You can upgrade to Micro Compute at any time when convenient."
/>
)}
</>
<div
className={
!addonsError
? 'grid gap-4 grid-cols-[repeat(auto-fit,minmax(min(100%,13em),1fr))]'
: ''
}
>
<div
className={
!addonsError
? 'grid gap-4 grid-cols-[repeat(auto-fit,minmax(min(100%,13em),1fr))]'
: ''
}
>
{isLoading ? (
Array(INITIALLY_VISIBLE_COUNT)
.fill(0)
.map((_, i) => <Skeleton key={i} className="w-full h-[110px] rounded-md" />)
) : addonsError ? (
<FormMessage message={'Failed to load Compute size options'} type="error">
<p>{addonsError?.message}</p>
</FormMessage>
) : (
<>
{visibleOptions.map((compute) => {
const cpuArchitecture = getCloudProviderArchitecture(project?.cloud_provider)
{isLoading ? (
Array(INITIALLY_VISIBLE_COUNT)
.fill(0)
.map((_, i) => <Skeleton key={i} className="w-full h-[110px] rounded-md" />)
) : addonsError ? (
<FormMessage message={'Failed to load Compute size options'} type="error">
<p>{addonsError?.message}</p>
</FormMessage>
) : (
<>
{availableOptions.map((compute) => {
const cpuArchitecture = getCloudProviderArchitecture(project?.cloud_provider)
const lockedMicroDueToPITR =
compute.identifier === 'ci_micro' && !!subscriptionPitr
const lockedNanoDueToPlan =
org?.plan.id !== 'free' &&
project?.infra_compute_size !== 'nano' &&
compute.identifier === 'ci_nano'
const lockedMicroDueToPITR =
compute.identifier === 'ci_micro' && !!subscriptionPitr
const lockedNanoDueToPlan =
org?.plan.id !== 'free' &&
project?.infra_compute_size !== 'nano' &&
compute.identifier === 'ci_nano'
const lockedOption = lockedNanoDueToPlan || lockedMicroDueToPITR
const lockedOption = lockedNanoDueToPlan || lockedMicroDueToPITR
const price =
org?.plan.id !== 'free' &&
project?.infra_compute_size === 'nano' &&
compute.identifier === 'ci_nano'
? availableOptions.find(
(option: ComputeAddonVariant) => option.identifier === 'ci_micro'
)?.price
: compute.price
const price =
org?.plan.id !== 'free' &&
project?.infra_compute_size === 'nano' &&
compute.identifier === 'ci_nano'
? availableOptions.find(
(option: ComputeAddonVariant) => option.identifier === 'ci_micro'
)?.price
: compute.price
const cpuLabel = (() => {
const cpuCores = compute.meta?.cpu_cores
if (typeof cpuCores === 'number') {
return `${cpuCores}-core ${cpuArchitecture} CPU`
}
if (cpuCores) {
return `${cpuCores} CPU`
}
return 'CPU'
})()
const cpuLabel = (() => {
const cpuCores = compute.meta?.cpu_cores
if (typeof cpuCores === 'number') {
return `${cpuCores}-core ${cpuArchitecture} CPU`
}
if (cpuCores) {
return `${cpuCores} CPU`
}
return 'CPU'
})()
return (
<RadioGroupCardItem
showIndicator={false}
id={compute.identifier}
key={compute.identifier}
value={compute.identifier}
className={cn(
'relative text-sm text-left flex flex-col gap-0 px-0 py-3 [&_label]:w-full group w-full h-[110px]',
lockedOption && 'opacity-50'
)}
disabled={disabled || lockedOption}
label={
<Tooltip>
<TooltipTrigger asChild>
<div>
{showUpgradeBadge && compute.identifier === 'ci_micro' && (
<div className="absolute -top-4 -right-3 text-violet-1100 flex items-center gap-1 bg-surface-75 py-0.5 px-2 rounded-full border border-violet-900">
<span>No additional charge</span>
</div>
)}
<div className="w-full flex flex-col gap-3 justify-between">
<div className="relative px-3 opacity-50 group-data-checked:opacity-100 flex justify-between">
<ComputeBadge
className="inline-flex font-semibold"
infraComputeSize={compute.name as InfraInstanceSize}
/>
<div className="flex items-center space-x-1">
{lockedOption ? (
<div className="bg border rounded-lg h-7 w-7 flex items-center justify-center">
<Lock size={14} />
</div>
) : (
showComputePrice && (
<>
<span
className="text-foreground text-sm font-semibold"
translate="no"
>
${price}
</span>
<span className="text-foreground-light translate-y-px">
{' '}
/{' '}
{compute.price_interval === 'monthly'
? 'month'
: 'hour'}
</span>
</>
)
)}
</div>
</div>
<div className="w-full">
<div className="px-3 text-sm flex flex-col gap-1">
<div className="text-foreground-light flex gap-2 items-center">
<Microchip
strokeWidth={1}
size={14}
className="text-foreground-lighter"
/>
<span>
{compute.identifier === 'ci_nano' && 'Up to '}
{compute.meta?.memory_gb ?? 0} GB memory
</span>
</div>
<div className="text-foreground-light flex gap-2 items-center">
<CpuIcon
strokeWidth={1}
size={14}
className="text-foreground-lighter"
/>
<span>{cpuLabel}</span>
</div>
</div>
</div>
</div>
</div>
</TooltipTrigger>
{lockedMicroDueToPITR && (
<TooltipContent side="bottom" className="w-64 text-center">
Project has PITR enabled which requires a minimum of Small compute.
Please{' '}
<InlineLink href="/project/_/settings/addons?panel=pitr">
disable PITR
</InlineLink>{' '}
first before selecting Micro
</TooltipContent>
)}
</Tooltip>
}
/>
)
})}
{showAllSizes && (
return (
<RadioGroupCardItem
id="larger-compute"
key="larger-compute"
showIndicator={false}
value="larger-compute"
onClick={(e) => e.preventDefault()}
id={compute.identifier}
key={compute.identifier}
value={compute.identifier}
className={cn(
'relative text-sm text-left flex flex-col gap-0 px-0 py-3 [&_label]:w-full group w-full h-[110px]'
'relative text-sm text-left flex flex-col gap-0 px-0 py-3 [&_label]:w-full group w-full h-[110px]',
lockedOption && 'opacity-50'
)}
disabled={disabled || lockedOption}
label={
<SupportLink
queryParams={{
projectRef: ref,
category: SupportCategories.SALES_ENQUIRY,
subject: 'Enquiry about larger instance sizes',
}}
>
<div className="w-full flex flex-col gap-3 justify-between">
<div className="relative px-3 flex justify-between">
<ComputeBadge infraComputeSize=">16XL" />
<Tooltip>
<TooltipTrigger asChild>
<div>
{showUpgradeBadge && compute.identifier === 'ci_micro' && (
<HoverCard openDelay={200}>
<HoverCardTrigger asChild>
<div
className="absolute -top-4 -right-3 text-violet-1100 flex items-center gap-1 bg-surface-75 py-0.5 px-2 rounded-full border border-violet-900 cursor-default"
onClick={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>
<span>Free Upgrade</span>
</div>
</HoverCardTrigger>
<HoverCardContent
side="top"
align="end"
className="w-72"
onClick={(e) => e.stopPropagation()}
>
<p className="text-sm font-medium text-foreground">
Upgrade to Micro Compute
</p>
<p className="text-sm text-foreground-light">
This Project is already paying for Micro Compute. You can
upgrade to Micro Compute at any time when convenient.
</p>
</HoverCardContent>
</HoverCard>
)}
<div className="w-full flex flex-col gap-3 justify-between">
<div className="relative px-3 opacity-50 group-data-checked:opacity-100 flex justify-between">
<ComputeBadge
className="inline-flex font-semibold"
infraComputeSize={compute.name as InfraInstanceSize}
/>
<div className="flex items-center space-x-1">
{lockedOption ? (
<div className="bg border rounded-lg h-7 w-7 flex items-center justify-center">
<Lock size={14} />
</div>
) : (
showComputePrice && (
<>
<span
className="text-foreground text-sm font-semibold"
translate="no"
>
${price}
</span>
<span className="text-foreground-light translate-y-px">
{' '}
/{' '}
{compute.price_interval === 'monthly'
? 'month'
: 'hour'}
</span>
</>
)
)}
</div>
</div>
<div className="flex items-center space-x-1 opacity-50 ">
<span className="text-foreground-light text-sm">Contact Us</span>
</div>
</div>
<div className="w-full">
<div className="px-3 text-sm flex flex-col gap-1">
<div className="text-foreground-light flex gap-2 items-center">
<Microchip
strokeWidth={1}
size={14}
className="text-foreground-lighter"
/>
<span>Custom memory</span>
</div>
<div className="text-foreground-light flex gap-2 items-center">
<CpuIcon
strokeWidth={1}
size={14}
className="text-foreground-lighter"
/>
<span>Custom CPU</span>
<div className="w-full">
<div className="px-3 text-sm flex flex-col gap-1">
<div className="text-foreground-light flex gap-2 items-center">
<Microchip
strokeWidth={1}
size={14}
className="text-foreground-lighter"
/>
<span>
{compute.identifier === 'ci_nano' && 'Up to '}
{compute.meta?.memory_gb ?? 0} GB memory
</span>
</div>
<div className="text-foreground-light flex gap-2 items-center">
<CpuIcon
strokeWidth={1}
size={14}
className="text-foreground-lighter"
/>
<span>{cpuLabel}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</SupportLink>
</TooltipTrigger>
{lockedMicroDueToPITR && (
<TooltipContent side="bottom" className="w-64 text-center">
Project has PITR enabled which requires a minimum of Small compute.
Please{' '}
<InlineLink href="/project/_/settings/addons?panel=pitr">
disable PITR
</InlineLink>{' '}
first before selecting Micro
</TooltipContent>
)}
</Tooltip>
}
/>
)}
</>
)}
</div>
)
})}
{!isLoading && !addonsError && hasHiddenOptions && (
<Button
variant="default"
size="tiny"
className="mt-4"
aria-expanded={showAllSizes}
// Prevent collapsing when the selected size would become hidden
disabled={showAllSizes && selectedOptionIsHidden}
onClick={() => setShowAllSizes((prev) => !prev)}
icon={
<ChevronRight
size={14}
strokeWidth={1.5}
className={cn('transition-transform', showAllSizes && '-rotate-90')}
/>
}
>
{showAllSizes ? 'Show fewer sizes' : 'Show all sizes'}
</Button>
<RadioGroupCardItem
id="larger-compute"
key="larger-compute"
showIndicator={false}
value="larger-compute"
onClick={(e) => e.preventDefault()}
className={cn(
'relative text-sm text-left flex flex-col gap-0 px-0 py-3 [&_label]:w-full group w-full h-[110px]'
)}
label={
<SupportLink
queryParams={{
projectRef: ref,
category: SupportCategories.SALES_ENQUIRY,
subject: 'Enquiry about larger instance sizes',
}}
>
<div className="w-full flex flex-col gap-3 justify-between">
<div className="relative px-3 flex justify-between">
<ComputeBadge infraComputeSize=">16XL" />
<div className="flex items-center space-x-1 opacity-50 ">
<span className="text-foreground-light text-sm">Contact Us</span>
</div>
</div>
<div className="w-full">
<div className="px-3 text-sm flex flex-col gap-1">
<div className="text-foreground-light flex gap-2 items-center">
<Microchip
strokeWidth={1}
size={14}
className="text-foreground-lighter"
/>
<span>Custom memory</span>
</div>
<div className="text-foreground-light flex gap-2 items-center">
<CpuIcon
strokeWidth={1}
size={14}
className="text-foreground-lighter"
/>
<span>Custom CPU</span>
</div>
</div>
</div>
</div>
</SupportLink>
}
/>
</>
)}
</FormItemLayout>
</div>
</RadioGroupCard>
)}
/>
@@ -15,31 +15,22 @@ import {
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { DiskStorageSchemaType } from '../DiskManagement.schema'
import { calculateDiskSizePrice } from '../DiskManagement.utils'
import { BillingChangeBadge } from '../ui/BillingChangeBadge'
import { DiskType, PLAN_DETAILS } from '../ui/DiskManagement.constants'
import { DiskManagementDiskSizeReadReplicas } from '../ui/DiskManagementReadReplicas'
import { DiskSpaceBar } from '../ui/DiskSpaceBar'
import { DiskTypeRecommendationSection } from '../ui/DiskTypeRecommendationSection'
import FormMessage from '../ui/FormMessage'
import { DocsButton } from '@/components/ui/DocsButton'
import { useDiskAttributesQuery } from '@/data/config/disk-attributes-query'
import { useDiskUtilizationQuery } from '@/data/config/disk-utilization-query'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { DOCS_URL, GB } from '@/lib/constants'
import { GB } from '@/lib/constants'
type DiskSizeFieldProps = {
form: UseFormReturn<DiskStorageSchemaType>
disableInput: boolean
setAdvancedSettingsOpenState: (state: boolean) => void
}
export function DiskSizeField({
form,
disableInput,
setAdvancedSettingsOpenState,
}: DiskSizeFieldProps) {
export function DiskSizeField({ form, disableInput }: DiskSizeFieldProps) {
const { ref: projectRef } = useParams()
const { control, formState, setValue, trigger, getValues, resetField, watch } = form
const { data: org } = useSelectedOrganizationQuery()
@@ -79,125 +70,107 @@ export function DiskSizeField({
PLAN_DETAILS?.[planId as keyof typeof PLAN_DETAILS] ?? {}
const includedDiskGB = includedDiskGBMeta[watchedStorageType]
const { defaultValues, dirtyFields, isDirty, errors } = formState
const diskSizePrice = calculateDiskSizePrice({
planId,
oldSize: defaultValues?.totalSize || 0,
oldStorageType: defaultValues?.storageType as DiskType,
newSize: getValues('totalSize'),
newStorageType: getValues('storageType') as DiskType,
})
const { defaultValues, dirtyFields } = formState
const mainDiskUsed = Math.round(((diskUtil?.metrics.fs_used_bytes ?? 0) / GB) * 100) / 100
return (
<div id="disk-size" className="grid @xl:grid-cols-12 gap-5">
<div className="col-span-4">
<FormField
name="totalSize"
control={control}
render={({ field, fieldState: { isDirty } }) => (
<FormItemLayout label="Disk Size" layout="vertical" id={field.name}>
<FormControl className="max-w-32">
<InputGroup>
<FormInputGroupInput
type="number"
id={field.name}
{...field}
disabled={disableInput || isError}
onWheel={(e) => e.currentTarget.blur()}
onChange={(e) => {
setValue('totalSize', e.target.valueAsNumber, {
shouldDirty: true,
shouldValidate: true,
})
<FormField
name="totalSize"
control={control}
render={({ field, fieldState: { isDirty } }) => (
<FormItemLayout
label="Disk size"
layout="flex-row-reverse"
id={field.name}
description={
<div className="flex flex-col gap-y-3">
{includedDiskGB > 0 && org?.plan.id && (
<p>
Your plan includes up to {includedDiskGB} GB of {watchedStorageType} storage.
</p>
)}
<DiskTypeRecommendationSection
form={form}
actions={
<Button
variant="default"
onClick={() => {
setValue('storageType', 'io2')
trigger('provisionedIOPS')
trigger('throughput')
trigger('totalSize')
}}
min={includedDiskGB}
/>
<InputGroupAddon align="inline-end">
<InputGroupText>GB</InputGroupText>
{isDirty ? (
<InputGroupButton
type="button"
variant="default"
size="tiny"
className="px-2 text-foreground-light"
onClick={() => {
resetField('totalSize')
trigger('provisionedIOPS')
}}
title="Reset"
>
<RotateCcw className="h-4 w-4" aria-hidden="true" />
</InputGroupButton>
) : null}
</InputGroupAddon>
</InputGroup>
</FormControl>
</FormItemLayout>
)}
/>
<div className="flex flex-col gap-1">
<BillingChangeBadge
className="mt-1"
beforePrice={Number(diskSizePrice.oldPrice)}
afterPrice={Number(diskSizePrice.newPrice)}
show={isDirty && !errors.totalSize && diskSizePrice.oldPrice !== diskSizePrice.newPrice}
/>
<span className="text-foreground-lighter text-sm">
{includedDiskGB > 0 &&
org?.plan.id &&
`Your plan includes up to ${includedDiskGB} GB of ${watchedStorageType} storage.`}
>
Change to High Performance SSD
</Button>
}
/>
<div className="mt-3">
<DocsButton abbrev={false} href={`${DOCS_URL}/guides/platform/database-size`} />
{isProjectNew ? (
<FormMessage
message="Disk size data is not available for ~10 minutes after project creation"
type="error"
/>
) : (
error && (
<FormMessage message="Failed to load disk size data" type="error">
{error?.message}
</FormMessage>
)
)}
<DiskManagementDiskSizeReadReplicas
isDirty={dirtyFields.totalSize !== undefined}
totalSize={(defaultValues?.totalSize || 0) * 1.25}
usedSize={mainDiskUsed}
newTotalSize={watchedTotalSize * 1.25}
oldStorageType={defaultValues?.storageType as DiskType}
newStorageType={getValues('storageType') as DiskType}
/>
</div>
</span>
<DiskTypeRecommendationSection
form={form}
actions={
<Button
variant="default"
onClick={() => {
setValue('storageType', 'io2')
}
>
<FormControl className="max-w-32">
<InputGroup>
<FormInputGroupInput
type="number"
id={field.name}
{...field}
disabled={disableInput || isError}
onWheel={(e) => e.currentTarget.blur()}
onChange={(e) => {
setValue('totalSize', e.target.valueAsNumber, {
shouldDirty: true,
shouldValidate: true,
})
trigger('provisionedIOPS')
trigger('totalSize')
setAdvancedSettingsOpenState(true)
trigger('throughput')
}}
>
Change to High Performance SSD
</Button>
}
/>
</div>
</div>
<div className="col-span-8">
<DiskSpaceBar form={form} />
{isProjectNew ? (
<FormMessage
message="Disk size data is not available for ~10 minutes after project creation"
type="error"
/>
) : (
error && (
<FormMessage message="Failed to load disk size data" type="error">
{error?.message}
</FormMessage>
)
)}
<DiskManagementDiskSizeReadReplicas
isDirty={dirtyFields.totalSize !== undefined}
totalSize={(defaultValues?.totalSize || 0) * 1.25}
usedSize={mainDiskUsed}
newTotalSize={watchedTotalSize * 1.25}
oldStorageType={defaultValues?.storageType as DiskType}
newStorageType={getValues('storageType') as DiskType}
/>
</div>
</div>
min={includedDiskGB}
/>
<InputGroupAddon align="inline-end">
<InputGroupText>GB</InputGroupText>
{isDirty ? (
<InputGroupButton
type="button"
variant="default"
size="tiny"
className="px-2 text-foreground-light"
onClick={() => {
resetField('totalSize')
trigger('provisionedIOPS')
}}
title="Reset"
>
<RotateCcw className="h-4 w-4" aria-hidden="true" />
</InputGroupButton>
) : null}
</InputGroupAddon>
</InputGroup>
</FormControl>
</FormItemLayout>
)}
/>
)
}
@@ -13,10 +13,8 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { DiskStorageSchemaType } from '../DiskManagement.schema'
import {
calculateComputeSizeRequiredForIops,
calculateIOPSPrice,
mapAddOnVariantIdToComputeSize,
} from '../DiskManagement.utils'
import { BillingChangeBadge } from '../ui/BillingChangeBadge'
import { ComputeSizeRecommendationSection } from '../ui/ComputeSizeRecommendationSection'
import { DiskType, RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3 } from '../ui/DiskManagement.constants'
import { DiskManagementIOPSReadReplicas } from '../ui/DiskManagementReadReplicas'
@@ -37,13 +35,6 @@ export function IOPSField({ form, disableInput }: IOPSFieldProps) {
const { isError } = useDiskAttributesQuery({ projectRef })
const iopsPrice = calculateIOPSPrice({
oldStorageType: formState.defaultValues?.storageType as DiskType,
oldProvisionedIOPS: formState.defaultValues?.provisionedIOPS || 0,
newStorageType: getValues('storageType') as DiskType,
newProvisionedIOPS: getValues('provisionedIOPS'),
})
const disableIopsInput =
RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3.includes(watchedComputeSize) && watchedStorageType === 'gp3'
@@ -55,7 +46,7 @@ export function IOPSField({ form, disableInput }: IOPSFieldProps) {
const reccomendedComputeSize = calculateComputeSizeRequiredForIops(watchedIOPS)
return (
<FormItemLayout
layout="horizontal"
layout="flex-row-reverse"
label="IOPS"
id={field.name}
description={
@@ -87,21 +78,7 @@ export function IOPSField({ form, disableInput }: IOPSFieldProps) {
</span>
}
labelOptional={
<>
<BillingChangeBadge
show={
(watchedStorageType !== formState.defaultValues?.storageType ||
(watchedStorageType === 'gp3' &&
field.value !== formState.defaultValues?.provisionedIOPS)) &&
!formState.errors.provisionedIOPS &&
!disableIopsInput
}
beforePrice={Number(iopsPrice.oldPrice)}
afterPrice={Number(iopsPrice.newPrice)}
className="mb-2"
/>
<p className="text-foreground-lighter">Input/output operations per second.</p>
</>
<p className="text-foreground-lighter">Input/output operations per second.</p>
}
>
<FormControl className="max-w-32">
@@ -45,7 +45,7 @@ export function StorageTypeField({ form, disableInput }: StorageTypeFieldProps)
name="storageType"
control={control}
render={({ field }) => (
<FormItemLayout layout="horizontal" label="Storage type">
<FormItemLayout layout="flex-row-reverse" label="Storage type" id={field.name}>
<Select
{...field}
onValueChange={async (e: DiskType) => {
@@ -13,8 +13,6 @@ import {
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { DiskStorageSchemaType } from '../DiskManagement.schema'
import { calculateThroughputPrice } from '../DiskManagement.utils'
import { BillingChangeBadge } from '../ui/BillingChangeBadge'
import {
DISK_LIMITS,
DiskType,
@@ -40,12 +38,6 @@ export function ThroughputField({ form, disableInput }: ThroughputFieldProps) {
useDiskAttributesQuery({ projectRef })
const throughputPrice = calculateThroughputPrice({
storageType: form.getValues('storageType') as DiskType,
newThroughput: form.getValues('throughput') || 0,
oldThroughput: form.formState.defaultValues?.throughput || 0,
})
const disableIopsInput =
RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3.includes(watchedComputeSize) && watchedStorageType === 'gp3'
@@ -84,7 +76,7 @@ export function ThroughputField({ form, disableInput }: ThroughputFieldProps) {
render={({ field }) => (
<FormItemLayout
label="Throughput"
layout="horizontal"
layout="flex-row-reverse"
description={
<span className="flex flex-col gap-y-2">
<p>Higher throughput suits applications with high data transfer needs.</p>
@@ -100,21 +92,7 @@ export function ThroughputField({ form, disableInput }: ThroughputFieldProps) {
</span>
}
labelOptional={
<>
<BillingChangeBadge
show={
formState.isDirty &&
formState.dirtyFields.throughput &&
!formState.errors.throughput
}
beforePrice={Number(throughputPrice.oldPrice)}
afterPrice={Number(throughputPrice.newPrice)}
className="mb-2"
/>
<p className="text-foreground-lighter">
Amount of data read/written per second.
</p>
</>
<p className="text-foreground-lighter">Amount of data read/written per second.</p>
}
>
<FormControl className="max-w-32">
@@ -0,0 +1,38 @@
import { AnimatePresence, motion } from 'framer-motion'
import { type ComponentProps, type ReactNode } from 'react'
import { cn } from 'ui'
import { Admonition } from 'ui-patterns'
/**
* @deprecated Use Admonition from ui-patterns instead
* Pass actions as a prop to the Admonition component
*/
interface NoticeBarProps extends Omit<ComponentProps<typeof Admonition>, 'description'> {
title?: string
description?: string
icon?: ReactNode
visible: boolean
actions?: ReactNode
}
export function NoticeBar({ visible, description, actions, ...props }: NoticeBarProps) {
return (
<AnimatePresence>
{visible && (
<motion.div
initial={{ opacity: 0, height: 0, y: 4 }}
animate={{ opacity: 1, height: 'auto', y: 0 }}
exit={{ opacity: 0, height: 0, y: 4 }}
transition={{ duration: 0.15 }}
>
<Admonition {...props} className={cn(props.className, 'mb-0')}>
{description}
<div className="flex flex-col gap-2">
{actions && <div className="mt-2">{actions}</div>}
</div>
</Admonition>
</motion.div>
)}
</AnimatePresence>
)
}
@@ -175,7 +175,7 @@ export const DiskUsage = ({
{project.name}
</span>
<Button asChild variant="default" size={'tiny'}>
<Link href={`/project/${project.ref}/settings/compute-and-disk`}>
<Link href={`/project/${project.ref}/settings/infrastructure`}>
Manage Disk
</Link>
</Button>
@@ -1,6 +1,8 @@
import { useParams } from 'common'
import {
Badge,
Card,
CardContent,
Input,
InputGroup,
InputGroupAddon,
@@ -11,21 +13,22 @@ import {
} from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import {
PageSection,
PageSectionContent,
PageSectionDescription,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { ProjectUpgradeAlert } from '../General/Infrastructure/ProjectUpgradeAlert'
import { ProjectUpgradeAlert } from '../../General/Infrastructure/ProjectUpgradeAlert'
import {
ReadReplicasWarning,
ValidationErrorsWarning,
ValidationWarningsAdmonition,
} from './UpgradeWarnings'
import {
ScaffoldContainer,
ScaffoldDivider,
ScaffoldSection,
ScaffoldSectionContent,
ScaffoldSectionDetail,
} from '@/components/layouts/Scaffold'
} from '../../Infrastructure/UpgradeWarnings'
import AlertError from '@/components/ui/AlertError'
import { useProjectUpgradeEligibilityQuery } from '@/data/config/project-upgrade-eligibility-query'
import { useProjectServiceVersionsQuery } from '@/data/projects/project-service-versions'
@@ -33,7 +36,7 @@ import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { useIsOrioleDb, useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
export const InfrastructureInfo = () => {
export const ServiceVersionsSection = () => {
const { ref } = useParams()
const { data: project } = useSelectedProjectQuery()
@@ -82,18 +85,18 @@ export const InfrastructureInfo = () => {
const hasReadReplicas = (databases ?? []).length > 1
return (
<>
<ScaffoldDivider />
<ScaffoldContainer>
<ScaffoldSection>
<ScaffoldSectionDetail>
<h4 className="text-base capitalize m-0">Service versions</h4>
<p className="text-foreground-light text-sm pr-8 mt-1">
Service versions and upgrade eligibility for your provisioned instance.
</p>
</ScaffoldSectionDetail>
<ScaffoldSectionContent>
<PageSection id="service-versions">
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Service versions</PageSectionTitle>
<PageSectionDescription>
Service versions and upgrade eligibility for your provisioned instance.
</PageSectionDescription>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Card>
<CardContent className="flex flex-col gap-6">
{isInactive ? (
<Admonition
type="note"
@@ -103,7 +106,6 @@ export const InfrastructureInfo = () => {
/>
) : (
<>
{/* [Joshen] Double check why we need this waterfall loading behaviour here */}
{isLoadingUpgradeEligibility && <GenericSkeletonLoader />}
{isErrorUpgradeEligibility && (
<AlertError error={error} subject="Failed to retrieve Postgres version" />
@@ -214,9 +216,9 @@ export const InfrastructureInfo = () => {
)}
</>
)}
</ScaffoldSectionContent>
</ScaffoldSection>
</ScaffoldContainer>
</>
</CardContent>
</Card>
</PageSectionContent>
</PageSection>
)
}
@@ -1,109 +0,0 @@
import { DOCS_URL } from '@/lib/constants'
export interface Attribute {
key: string
name?: string
color: 'white' | 'blue' | 'green' | 'yellow' | 'orange'
}
export interface CategoryAttribute {
anchor: string
key: string // Property from organization usage
attributes: Attribute[] // For querying against stats-daily / infra-monitoring
name: string
unit: 'bytes' | 'absolute' | 'percentage'
links: {
name: string
url: string
}[]
description: string
chartPrefix?: 'Max' | 'Average'
chartDescription: string
}
export interface CategoryMeta {
key: string
name: string
description: string
attributes: CategoryAttribute[]
}
export const INFRA_ACTIVITY_METRICS: CategoryMeta[] = [
{
key: 'infra',
name: 'Infrastructure',
description: 'Usage statistics related to your server instance',
attributes: [
{
anchor: 'cpu',
key: 'max_cpu_usage',
attributes: [{ key: 'max_cpu_usage', color: 'white' }],
name: 'CPU',
unit: 'percentage',
description: 'Max CPU usage of your server.',
chartDescription: '',
links: [
{
name: 'Compute Add-Ons',
url: `${DOCS_URL}/guides/platform/compute-add-ons`,
},
{
name: 'High CPU Usage',
url: `${DOCS_URL}/guides/troubleshooting/high-cpu-usage`,
},
{
name: 'Metrics',
url: `${DOCS_URL}/guides/platform/metrics`,
},
],
},
{
anchor: 'ram',
key: 'ram_usage',
attributes: [{ key: 'ram_usage', color: 'white' }],
name: 'Memory',
unit: 'percentage',
description:
'Memory usage of your server.\nYou might observe elevated memory usage, even with little to no load. Besides Postgres, a wide range of services are running under the hood resulting in an elevated base memory usage.',
chartDescription: '',
links: [
{
name: 'Compute Add-Ons',
url: `${DOCS_URL}/guides/platform/compute-add-ons`,
},
{
name: 'High RAM Usage',
url: `${DOCS_URL}/guides/troubleshooting/exhaust-ram`,
},
{
name: 'Metrics',
url: `${DOCS_URL}/guides/platform/metrics`,
},
],
},
{
anchor: 'disk_io',
key: 'disk_io_consumption',
attributes: [{ key: 'disk_io_consumption', color: 'white' }],
name: 'Disk IO Bandwidth',
unit: 'percentage',
links: [
{
name: 'Disk Throughput and IOPS',
url: `${DOCS_URL}/guides/platform/compute-add-ons#disk-throughput-and-iops`,
},
{
name: 'High Disk I/O',
url: `${DOCS_URL}/guides/troubleshooting/exhaust-disk-io`,
},
{
name: 'Metrics',
url: `${DOCS_URL}/guides/platform/metrics`,
},
],
description:
'The disk performance of your workload is determined by the Disk IO bandwidth.\nSmaller compute instances (below 4XL) can burst above their baseline disk throughput and IOPS for short periods of time. Beyond that, the performance reverts to the baseline.',
chartDescription: '',
},
],
},
]
@@ -1,457 +0,0 @@
import { useParams } from 'common'
import dayjs from 'dayjs'
import { capitalize } from 'lodash'
import { BarChart2, ChartLine, ExternalLink } from 'lucide-react'
import Link from 'next/link'
import { Fragment, useMemo, useState } from 'react'
import { Button } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { INFRA_ACTIVITY_METRICS } from './Infrastructure.constants'
import { getAddons } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
import { CPUWarnings } from '@/components/interfaces/Billing/Usage/UsageWarningAlerts/CPUWarnings'
import { DiskIOBandwidthWarnings } from '@/components/interfaces/Billing/Usage/UsageWarningAlerts/DiskIOBandwidthWarnings'
import { RAMWarnings } from '@/components/interfaces/Billing/Usage/UsageWarningAlerts/RAMWarnings'
import UsageBarChart from '@/components/interfaces/Organization/Usage/UsageBarChart'
import {
ScaffoldContainer,
ScaffoldDivider,
ScaffoldSection,
ScaffoldSectionContent,
ScaffoldSectionDetail,
} from '@/components/layouts/Scaffold'
import { DatabaseSelector } from '@/components/ui/DatabaseSelector'
import { DateRangePicker } from '@/components/ui/DateRangePicker'
import { DocsButton } from '@/components/ui/DocsButton'
import Panel from '@/components/ui/Panel'
import { DataPoint } from '@/data/analytics/constants'
import { mapMultiResponseToAnalyticsData } from '@/data/analytics/infra-monitoring-queries'
import {
InfraMonitoringAttribute,
useInfraMonitoringAttributesQuery,
} from '@/data/analytics/infra-monitoring-query'
import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
import { useResourceWarningsQuery } from '@/data/usage/resource-warnings-query'
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { DOCS_URL, INSTANCE_MICRO_SPECS, INSTANCE_NANO_SPECS, InstanceSpecs } from '@/lib/constants'
import { TIME_PERIODS_BILLING, TIME_PERIODS_REPORTS } from '@/lib/constants/metrics'
import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
const NON_DEDICATED_IO_RESOURCES = [
'ci_micro',
'ci_small',
'ci_medium',
'ci_large',
'ci_xlarge',
'ci_2xlarge',
]
const INFRA_ATTRIBUTES: InfraMonitoringAttribute[] = [
'max_cpu_usage',
'ram_usage',
'disk_io_consumption',
]
export const InfrastructureActivity = () => {
const { ref: projectRef } = useParams()
const { data: project } = useSelectedProjectQuery()
const { data: organization } = useSelectedOrganizationQuery()
const state = useDatabaseSelectorStateSnapshot()
const [dateRange, setDateRange] = useState<any>()
const { data: subscription, isPending: isLoadingSubscription } = useOrgSubscriptionQuery({
orgSlug: organization?.slug,
})
const { hasAccess: hasAccessToComputeSizes } = useCheckEntitlements(
'instances.compute_update_available_sizes'
)
const { data: resourceWarnings } = useResourceWarningsQuery({ ref: projectRef })
// [Joshen Cleanup] JFYI this client side filtering can be cleaned up once BE changes are live which will only return the warnings based on the provided ref
const projectResourceWarnings = resourceWarnings?.find((x) => x.project === projectRef)
const { data: addons } = useProjectAddonsQuery({ projectRef })
const selectedAddons = addons?.selected_addons ?? []
const { computeInstance } = getAddons(selectedAddons)
const hasDedicatedIOResources =
computeInstance !== undefined &&
!NON_DEDICATED_IO_RESOURCES.includes(computeInstance.variant.identifier)
function getCurrentComputeInstanceSpecs() {
if (computeInstance?.variant.meta) {
// If user has a compute instance (called addons) return that
return computeInstance?.variant.meta as InstanceSpecs
} else {
// Otherwise, return the default specs
return project?.infra_compute_size === 'nano' ? INSTANCE_NANO_SPECS : INSTANCE_MICRO_SPECS
}
}
const currentComputeInstanceSpecs = getCurrentComputeInstanceSpecs()
const currentBillingCycleSelected = useMemo(() => {
// Selected by default
if (!dateRange?.period_start || !dateRange?.period_end || !subscription) return true
const { current_period_start, current_period_end } = subscription
return (
dayjs(dateRange.period_start.date).isSame(new Date(current_period_start * 1000)) &&
dayjs(dateRange.period_end.date).isSame(new Date(current_period_end * 1000))
)
}, [dateRange, subscription])
const upgradeUrl =
organization === undefined
? `/`
: hasAccessToComputeSizes
? `/org/${organization?.slug ?? '[slug]'}/billing#subscription`
: `/project/${projectRef}/settings/addons`
const categoryMeta = INFRA_ACTIVITY_METRICS.find((category) => category.key === 'infra')
const startDate = useMemo(() => {
if (dateRange?.period_start?.date === 'Invalid Date') return undefined
// If end date is in future, set end date to now
if (!dateRange?.period_start?.date) {
return undefined
} else {
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
return new Date(dateRange?.period_start?.date ?? 0).toISOString().slice(0, -5) + 'Z'
}
}, [dateRange])
const endDate = useMemo(() => {
if (dateRange?.period_end?.date === 'Invalid Date') return undefined
// If end date is in future, set end date to end of current day
if (dateRange?.period_end?.date && dayjs(dateRange.period_end.date).isAfter(dayjs())) {
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
// In order to have full days from Prometheus metrics when using 1d interval,
// the time needs to be greater or equal than the time of the start date
return dayjs().endOf('day').toISOString().slice(0, -5) + 'Z'
} else if (dateRange?.period_end?.date) {
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
return new Date(dateRange.period_end.date ?? 0).toISOString().slice(0, -5) + 'Z'
}
}, [dateRange])
// Switch to hourly interval, if the timeframe is <48 hours
let interval: '1d' | '1h' = '1d'
let dateFormat = 'DD MMM'
if (startDate && endDate) {
const diffInHours = dayjs(endDate).diff(startDate, 'hours')
if (diffInHours <= 48) {
interval = '1h'
dateFormat = 'h a'
}
}
const { data: infraMonitoringData, isPending: isLoadingInfraData } =
useInfraMonitoringAttributesQuery({
projectRef,
attributes: INFRA_ATTRIBUTES,
interval,
startDate,
endDate,
databaseIdentifier: state.selectedDatabaseId,
})
const transformedData = useMemo(() => {
if (!infraMonitoringData) return undefined
return mapMultiResponseToAnalyticsData(infraMonitoringData, INFRA_ATTRIBUTES, dateFormat)
}, [infraMonitoringData, dateFormat])
const cpuUsageData = transformedData?.max_cpu_usage
const memoryUsageData = transformedData?.ram_usage
const ioBudgetData = transformedData?.disk_io_consumption
const hasLatest = dayjs(endDate!).isAfter(dayjs().startOf('day'))
const latestIoBudgetConsumption =
hasLatest && ioBudgetData?.data?.slice(-1)?.[0]
? Number(ioBudgetData.data.slice(-1)[0].disk_io_consumption)
: 0
const highestIoBudgetConsumption = (ioBudgetData?.data || [])
.map((x) => Number(x.disk_io_consumption) ?? 0)
.reduce((a, b) => Math.max(a, b), 0)
const chartMeta: { [key: string]: { data: DataPoint[]; isLoading: boolean } } = {
max_cpu_usage: {
isLoading: isLoadingInfraData,
data: cpuUsageData?.data ?? [],
},
ram_usage: {
isLoading: isLoadingInfraData,
data: memoryUsageData?.data ?? [],
},
disk_io_consumption: {
isLoading: isLoadingInfraData,
data: ioBudgetData?.data ?? [],
},
}
return (
<>
<ScaffoldContainer id="infrastructure-activity">
<div className="mx-auto flex flex-col gap-10 pt-6">
<div>
<p className="text-xl">Infrastructure Activity</p>
<p className="text-sm text-foreground-light">
Activity statistics related to your server instance
</p>
</div>
</div>
</ScaffoldContainer>
<ScaffoldContainer className="sticky top-0 py-6 border-b bg-studio z-10">
<div className="flex items-center gap-x-4">
<DatabaseSelector />
{!isLoadingSubscription && (
<>
<DateRangePicker
onChange={setDateRange}
value={TIME_PERIODS_REPORTS[0].key}
options={[...TIME_PERIODS_BILLING, ...TIME_PERIODS_REPORTS]}
loading={isLoadingSubscription}
currentBillingPeriodStart={subscription?.current_period_start}
currentBillingPeriodEnd={subscription?.current_period_end}
/>
<p className="text-sm text-foreground-light">
{dayjs(startDate).format('DD MMM YYYY')} - {dayjs(endDate).format('DD MMM YYYY')}
</p>
</>
)}
</div>
</ScaffoldContainer>
{categoryMeta?.attributes.map((attribute) => {
const chartData = chartMeta[attribute.key]?.data ?? []
return (
<Fragment key={attribute.key}>
<ScaffoldContainer id={attribute.anchor}>
<ScaffoldSection>
<ScaffoldSectionDetail>
<div className="sticky top-32 space-y-6">
<div className="space-y-1">
<div className="flex items-center space-x-2">
<h4 className="text-base capitalize m-0">{attribute.name}</h4>
</div>
<div className="grid gap-4">
{attribute.description.split('\n').map((value, idx) => (
<p key={`desc-${idx}`} className="text-sm text-foreground-light pr-8 m-0">
{value}
</p>
))}
</div>
</div>
<div className="space-y-2">
<p className="text-sm text-foreground mb-2">More information</p>
{attribute.links.map((link) => (
<div key={link.url}>
<Link href={link.url} target="_blank" rel="noreferrer">
<div className="flex items-center space-x-2 opacity-50 hover:opacity-100 transition">
<p className="text-sm m-0">{link.name}</p>
<ExternalLink size={16} strokeWidth={1.5} />
</div>
</Link>
</div>
))}
</div>
</div>
</ScaffoldSectionDetail>
<ScaffoldSectionContent>
{attribute.key === 'disk_io_consumption' && (
<>
<DiskIOBandwidthWarnings
upgradeUrl={upgradeUrl}
hasAccessToComputeSizes={hasAccessToComputeSizes}
hasLatest={hasLatest}
currentBillingCycleSelected={currentBillingCycleSelected}
latestIoBudgetConsumption={latestIoBudgetConsumption}
highestIoBudgetConsumption={highestIoBudgetConsumption}
/>
<div className="space-y-1">
<p>Disk IO Bandwidth</p>
{currentComputeInstanceSpecs.baseline_disk_io_mbs ===
currentComputeInstanceSpecs.max_disk_io_mbs ? (
<p className="text-sm text-foreground-light">
Your current compute has a baseline and maximum disk throughput of{' '}
{currentComputeInstanceSpecs.max_disk_io_mbs?.toLocaleString()} Mbps.
</p>
) : (
<p className="text-sm text-foreground-light">
Your current compute can burst above the baseline disk throughput of{' '}
{currentComputeInstanceSpecs.baseline_disk_io_mbs?.toLocaleString()}{' '}
Mbps for short periods of time.
</p>
)}
</div>
<div>
<p className="text-sm mb-2">Overview</p>
<div className="flex items-center justify-between border-b py-1">
<p className="text-xs text-foreground-light">Current compute instance</p>
<p className="text-xs">
{computeInstance?.variant?.name ??
capitalize(project?.infra_compute_size) ??
'Micro'}
</p>
</div>
<div className="flex items-center justify-between border-b py-1">
<p className="text-xs text-foreground-light">Baseline IO Bandwidth</p>
<p className="text-xs">
{currentComputeInstanceSpecs.baseline_disk_io_mbs?.toLocaleString()}{' '}
Mbps
</p>
</div>
<div className="flex items-center justify-between border-b py-1">
<p className="text-xs text-foreground-light">
Maximum IO Bandwidth (burst limit)
</p>
<p className="text-xs">
{currentComputeInstanceSpecs.max_disk_io_mbs?.toLocaleString()} Mbps
</p>
</div>
{currentComputeInstanceSpecs.max_disk_io_mbs !==
currentComputeInstanceSpecs?.baseline_disk_io_mbs && (
<div className="flex items-center justify-between py-1">
<p className="text-xs text-foreground-light">Daily burst time limit</p>
<p className="text-xs">30 mins</p>
</div>
)}
</div>
</>
)}
{attribute.key === 'max_cpu_usage' && (
<CPUWarnings
upgradeUrl={upgradeUrl}
hasAccessToComputeSizes={hasAccessToComputeSizes}
severity={projectResourceWarnings?.cpu_exhaustion}
/>
)}
{attribute.key === 'ram_usage' && (
<RAMWarnings
upgradeUrl={upgradeUrl}
hasAccessToComputeSizes={hasAccessToComputeSizes}
severity={projectResourceWarnings?.memory_and_swap_exhaustion}
/>
)}
<div className="space-y-1">
<div className="flex flex-row justify-between">
{attribute.key === 'disk_io_consumption' ? (
<p>Disk IO consumed per {interval === '1d' ? 'day' : 'hour'}</p>
) : (
<p>
Max{' '}
<span className={attribute.key === 'ram_usage' ? 'lowercase' : ''}>
{attribute.name}
</span>{' '}
utilization per {interval === '1d' ? 'day' : 'hour'}
</p>
)}
</div>
{attribute.key === 'ram_usage' && (
<div className="text-sm text-foreground-light">
<p>
Your compute instance has {currentComputeInstanceSpecs.memory_gb} GB of
memory.
</p>
{currentComputeInstanceSpecs.memory_gb === 1 && (
<p>
As your project is running on the smallest compute instance, it is not
unusual for your project to have a base memory usage of ~50%.
</p>
)}
</div>
)}
{attribute.key === 'max_cpu_usage' && (
<p className="text-sm text-foreground-light">
Your compute instance has {currentComputeInstanceSpecs.cpu_cores} CPU cores.
</p>
)}
{attribute.chartDescription.split('\n').map((paragraph, idx) => (
<p key={`para-${idx}`} className="text-sm text-foreground-light">
{paragraph}
</p>
))}
</div>
{attribute.key === 'disk_io_consumption' && hasDedicatedIOResources ? (
<>
<Admonition
type="note"
title={`Your compute instance of ${computeInstance.variant.name} comes with dedicated I/O resources`}
description="Your project thus does not rely on I/O balance or burst capacity as larger
add-ons are designed for sustained, high performance with specific IOPS and
throughput limits without needing to burst."
>
<DocsButton
abbrev={false}
href={`${DOCS_URL}/guides/platform/compute-add-ons#disk-throughput-and-iops`}
/>
</Admonition>
</>
) : chartMeta[attribute.key].isLoading ? (
<div className="space-y-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
) : chartData.length ? (
<UsageBarChart
name={`${attribute.chartPrefix || ''} ${attribute.name}`}
unit={attribute.unit}
attributes={attribute.attributes}
data={chartData}
yFormatter={(value) => `${Math.round(Number(value))}%`}
tooltipFormatter={(value) => `${value}%`}
yLimit={100}
/>
) : (
<Panel>
<Panel.Content>
<div className="flex flex-col items-center justify-center space-y-2">
<BarChart2 size={18} className="text-foreground-light mb-2" />
<p className="text-sm">No data in period</p>
<p className="text-sm text-foreground-light">
May take a few minutes to show
</p>
</div>
</Panel.Content>
</Panel>
)}
{attribute.key === 'disk_io_consumption' && !hasDedicatedIOResources && (
<Admonition
type="default"
title="Looking for actual disk activity?"
description="The chart above shows your remaining burst budget, not real disk throughput. For detailed read/write IOPS and throughput charts, head to the Database Observability page."
>
<Button asChild variant="default" icon={<ChartLine size={14} />}>
<Link href={`/project/${projectRef}/observability/database`}>
View detailed IOPS and throughput
</Link>
</Button>
</Admonition>
)}
</ScaffoldSectionContent>
</ScaffoldSection>
</ScaffoldContainer>
<ScaffoldDivider />
</Fragment>
)
})}
</>
)
}
@@ -301,7 +301,7 @@ const InstanceConfigurationUI = ({ diagramOnly = false }: InstanceConfigurationU
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-52 *:space-x-2">
<DropdownMenuItem asChild>
<Link href={`/project/${projectRef}/settings/compute-and-disk`}>
<Link href={`/project/${projectRef}/settings/infrastructure`}>
Resize databases
</Link>
</DropdownMenuItem>
@@ -139,11 +139,11 @@ const StorageListV2MigrationDialog = () => {
<p className="text-sm">
Ensure that your database instance has not{' '}
<InlineLink href={`/project/${ref}/settings/compute-and-disk#disk-size`}>
<InlineLink href={`/project/${ref}/settings/infrastructure#disk-size`}>
scaled disk
</InlineLink>{' '}
within the last 6h and you have at least 60%{' '}
<InlineLink href={`/project/${ref}/settings/infrastructure#infrastructure-activity`}>
<InlineLink href={`/project/${ref}/observability/database`}>
CPU capacity
</InlineLink>{' '}
before proceeding.
@@ -189,10 +189,8 @@ const IssueSuggestion = ({ category, projectRef }: { category: string; projectRe
title="Have you checked your project's infrastructure activity?"
>
High memory or low disk IO bandwidth may be slowing down your database. Verify by checking
the infrastructure activity of your project{' '}
<InlineLink href={`${baseUrl}/settings/infrastructure#infrastructure-activity`}>
here
</InlineLink>
your project's database observability reports{' '}
<InlineLink href={`${baseUrl}/observability/database`}>here</InlineLink>
.
</Admonition>
)
@@ -205,11 +203,8 @@ const IssueSuggestion = ({ category, projectRef }: { category: string; projectRe
className={className}
title="Have you checked the Query Performance Advisor?"
>
Identify slow running queries and get actionable insights on how to optimize them with the
Query Performance Advisor{' '}
<InlineLink href={`${baseUrl}/settings/infrastructure#infrastructure-activity`}>
here
</InlineLink>
Identify slow running queries and get actionable insights on how to optimize them with Query
Performance <InlineLink href={`${baseUrl}/observability/query-performance`}>here</InlineLink>
.
</Admonition>
)
@@ -106,7 +106,6 @@ describe('useGenerateSettingsMenu (self-hosted)', () => {
const { result } = renderHook(() => useGenerateSettingsMenu())
const configGroup = result.current.find((group) => group.title === 'Configuration')
expect(configGroup?.items.some((item) => item.key === 'compute-and-disk')).toBe(false)
expect(configGroup?.items.some((item) => item.key === 'infrastructure')).toBe(false)
expect(configGroup?.items.some((item) => item.key === 'addons')).toBe(false)
})
@@ -147,9 +147,6 @@ describe('useGenerateSettingsMenu', () => {
)
expect(shortcutByKey.get('general')).toBe(SHORTCUT_IDS.NAV_PROJECT_SETTINGS_GENERAL)
expect(shortcutByKey.get('compute-and-disk')).toBe(
SHORTCUT_IDS.NAV_PROJECT_SETTINGS_COMPUTE_AND_DISK
)
expect(shortcutByKey.get('infrastructure')).toBe(
SHORTCUT_IDS.NAV_PROJECT_SETTINGS_INFRASTRUCTURE
)
@@ -106,15 +106,6 @@ export const useGenerateSettingsMenu = () => {
items: [],
shortcutId: SHORTCUT_IDS.NAV_PROJECT_SETTINGS_GENERAL,
},
{
name: 'Compute and Disk',
key: 'compute-and-disk',
url: `/project/${ref}/settings/compute-and-disk`,
items: [],
disabled: !isProjectActive,
isLoading: isPending,
shortcutId: SHORTCUT_IDS.NAV_PROJECT_SETTINGS_COMPUTE_AND_DISK,
},
{
name: 'Infrastructure',
key: 'infrastructure',
@@ -58,7 +58,7 @@ export const BannerFreeMicroUpgrade = () => {
track('free_micro_upgrade_banner_cta_clicked')
}}
>
<Link href={`/project/${ref}/settings/compute-and-disk?upgrade=micro`}>
<Link href={`/project/${ref}/settings/infrastructure?upgrade=micro`}>
Upgrade for free
</Link>
</Button>
@@ -201,7 +201,7 @@ export const ComputeBadgeWrapper = ({
})
}}
>
<Link href={`/project/${projectRef}/settings/compute-and-disk`}>
<Link href={`/project/${projectRef}/settings/infrastructure`}>
Upgrade compute
</Link>
</Button>
@@ -115,11 +115,11 @@ export const ResourceExhaustionWarningBanner = () => {
const correctionUrlVariants = {
undefined: undefined,
null: '/project/[ref]/settings/[infra-path]',
disk_space: '/project/[ref]/settings/compute-and-disk',
read_only: '/project/[ref]/settings/compute-and-disk',
disk_io: '/project/[ref]/settings/compute-and-disk',
cpu: '/project/[ref]/settings/compute-and-disk',
ram: '/project/[ref]/settings/compute-and-disk',
disk_space: '/project/[ref]/settings/infrastructure',
read_only: '/project/[ref]/settings/infrastructure',
disk_io: '/project/[ref]/settings/infrastructure',
cpu: '/project/[ref]/settings/infrastructure',
ram: '/project/[ref]/settings/infrastructure',
auth_email_rate_limit: '/project/[ref]/auth/rate-limits',
auth_restricted_email_sending: '/project/[ref]/auth/smtp',
default: (metric: string) => `/project/[ref]/settings/[infra-path]#${metric}`,
@@ -146,7 +146,7 @@ export const ResourceExhaustionWarningBanner = () => {
return `/org/${organization?.slug ?? '_'}/billing?panel=subscriptionPlan&source=resource_exhaustion_banner`
}
if (isComputeUpgradeMetric && activeWarnings.length > 1) {
return `/project/${ref ?? 'default'}/settings/compute-and-disk`
return `/project/${ref ?? 'default'}/settings/infrastructure`
}
return getCorrectionUrl(metric)
?.replace('[ref]', ref ?? 'default')
@@ -182,15 +182,15 @@ export const ResourceExhaustionWarningBanner = () => {
warningContent === undefined || (!warningContent?.title && !warningContent?.description)
const isUsageOrInfraPage =
router.pathname.endsWith('/usage') || router.pathname.endsWith('/infrastructure')
// Compute warnings now link to compute-and-disk, so they should remain visible on infrastructure
// Compute warnings now link to infrastructure, so they should remain visible on usage.
const onUsageOrInfraAndNotInReadOnlyMode =
isUsageOrInfraPage &&
!activeWarnings.includes('is_readonly_mode_enabled') &&
!isComputeUpgradeMetric
// Suppress when already on the target page (no-op CTA). Paid-plan compute warnings link to
// compute-and-disk; free-plan links to billing instead, so we keep the banner visible for them.
// infrastructure; free-plan links to billing instead, so we keep the banner visible for them.
const onDatabaseSettingsAndInReadOnlyMode =
router.pathname.endsWith('settings/compute-and-disk') &&
router.pathname.endsWith('settings/infrastructure') &&
(activeWarnings.includes('is_readonly_mode_enabled') || (isComputeUpgradeMetric && !isFreePlan))
// these take precedence over each other, so there's only one active warning to check
@@ -71,7 +71,7 @@ export const UpgradePlanButton = ({
? `/org/${slug ?? '_'}/billing?panel=costControl&source=${source}`
: isOnPaidPlanAndRequestingToPurchaseAddon
? addon === 'computeSize'
? `/project/${ref ?? '_'}/settings/compute-and-disk`
? `/project/${ref ?? '_'}/settings/infrastructure`
: `/project/${ref ?? '_'}/settings/addons?panel=${addon}&source=${source}`
: `/org/${slug ?? '_'}/billing?panel=subscriptionPlan&source=${source}`
@@ -20,6 +20,7 @@ import { DiskSizeConfiguration } from '@/components/interfaces/Settings/Database
import { JitDbAccessConfiguration } from '@/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccessConfiguration'
import { NetworkRestrictions } from '@/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions'
import { PoolingModesModal } from '@/components/interfaces/Settings/Database/PoolingModesModal'
import { ServiceVersionsSection } from '@/components/interfaces/Settings/Database/ServiceVersions/ServiceVersionsSection'
import { SettingsDatabaseEmptyStateLocal } from '@/components/interfaces/Settings/Database/SettingsDatabaseEmptyStateLocal'
import { SSLConfiguration } from '@/components/interfaces/Settings/Database/SSLConfiguration'
import DatabaseLayout from '@/components/layouts/DatabaseLayout/DatabaseLayout'
@@ -57,8 +58,9 @@ const DatabaseSettings: NextPageWithLayout = () => {
{jitDbAccessEnabled && <JitDbAccessConfiguration />}
<ConnectionPooling />
<SSLConfiguration />
<ServiceVersionsSection />
{showNewDiskManagementUI ? (
// This form is hidden if Disk and Compute form is enabled, new form is on ./settings/compute-and-disk
// This form is hidden if Disk and Compute form is enabled, new form is on ./settings/infrastructure
<DiskManagementPanelForm />
) : (
<DiskSizeConfiguration />
@@ -373,7 +373,7 @@ const DatabaseUsage = () => {
<div className="ml-auto">
{project?.cloud_provider === 'AWS' ? (
<Button asChild variant="default">
<Link href={`/project/${ref}/settings/compute-and-disk`}>
<Link href={`/project/${ref}/settings/infrastructure`}>
Increase disk size
</Link>
</Button>
@@ -1,33 +0,0 @@
import { DiskManagementForm } from '@/components/interfaces/DiskManagement/DiskManagementForm'
import DefaultLayout from '@/components/layouts/DefaultLayout'
import SettingsLayout from '@/components/layouts/ProjectSettingsLayout/SettingsLayout'
import {
ScaffoldContainer,
ScaffoldDescription,
ScaffoldHeader,
ScaffoldTitle,
} from '@/components/layouts/Scaffold'
import type { NextPageWithLayout } from '@/types'
const AuthSettings: NextPageWithLayout = () => {
return (
<>
<ScaffoldContainer>
<ScaffoldHeader>
<ScaffoldTitle>Compute and Disk</ScaffoldTitle>
<ScaffoldDescription>
Configure the compute and disk settings for your project.
</ScaffoldDescription>
</ScaffoldHeader>
</ScaffoldContainer>
<DiskManagementForm />
</>
)
}
AuthSettings.getLayout = (page) => (
<DefaultLayout>
<SettingsLayout title="Compute and Disk">{page}</SettingsLayout>
</DefaultLayout>
)
export default AuthSettings
@@ -1,38 +1,37 @@
import { InfrastructureActivity } from '@/components/interfaces/Settings/Infrastructure/InfrastructureActivity'
import { InfrastructureInfo } from '@/components/interfaces/Settings/Infrastructure/InfrastructureInfo'
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
import SettingsLayout from '@/components/layouts/ProjectSettingsLayout/SettingsLayout'
import {
ScaffoldContainer,
ScaffoldDescription,
ScaffoldDivider,
ScaffoldHeader,
ScaffoldTitle,
} from '@/components/layouts/Scaffold'
PageHeader,
PageHeaderDescription,
PageHeaderMeta,
PageHeaderSummary,
PageHeaderTitle,
} from 'ui-patterns/PageHeader'
import { DiskManagementForm } from '@/components/interfaces/DiskManagement/DiskManagementForm'
import DefaultLayout from '@/components/layouts/DefaultLayout'
import SettingsLayout from '@/components/layouts/ProjectSettingsLayout/SettingsLayout'
import type { NextPageWithLayout } from '@/types'
const ProjectInfrastructure: NextPageWithLayout = () => {
const InfrastructureSettings: NextPageWithLayout = () => {
return (
<>
<ScaffoldContainer>
<ScaffoldHeader>
<ScaffoldTitle>Infrastructure</ScaffoldTitle>
<ScaffoldDescription>
General information regarding your server instance
</ScaffoldDescription>
</ScaffoldHeader>
</ScaffoldContainer>
<InfrastructureInfo />
<ScaffoldDivider />
<InfrastructureActivity />
<PageHeader size="default" className="pb-12">
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>Infrastructure</PageHeaderTitle>
<PageHeaderDescription>
View and configure compute and disk for your project.
</PageHeaderDescription>
</PageHeaderSummary>
</PageHeaderMeta>
</PageHeader>
<DiskManagementForm />
</>
)
}
ProjectInfrastructure.getLayout = (page) => (
InfrastructureSettings.getLayout = (page) => (
<DefaultLayout>
<SettingsLayout title="Infrastructure">{page}</SettingsLayout>
</DefaultLayout>
)
export default ProjectInfrastructure
export default InfrastructureSettings
+6 -1
View File
@@ -109,6 +109,11 @@ export const SHARED_REDIRECTS: StudioRedirect[] = [
destination: '/org/_/billing?panel=subscriptionPlan',
permanent: true,
},
{
source: '/project/:ref/settings/compute-and-disk',
destination: '/project/:ref/settings/infrastructure',
permanent: true,
},
{
source: '/project/:ref/settings/billing/subscription',
has: [{ type: 'query', key: 'panel', value: 'pitr' }],
@@ -118,7 +123,7 @@ export const SHARED_REDIRECTS: StudioRedirect[] = [
{
source: '/project/:ref/settings/billing/subscription',
has: [{ type: 'query', key: 'panel', value: 'computeInstance' }],
destination: '/project/:ref/settings/compute-and-disk',
destination: '/project/:ref/settings/infrastructure',
permanent: true,
},
{
@@ -10,7 +10,6 @@ import { RegistryDefinations } from '../types'
*/
export const PROJECT_SETTINGS_NAV_SHORTCUT_IDS = {
NAV_PROJECT_SETTINGS_GENERAL: 'nav.project-settings-general',
NAV_PROJECT_SETTINGS_COMPUTE_AND_DISK: 'nav.project-settings-compute-and-disk',
NAV_PROJECT_SETTINGS_INFRASTRUCTURE: 'nav.project-settings-infrastructure',
NAV_PROJECT_SETTINGS_INTEGRATIONS: 'nav.project-settings-integrations',
NAV_PROJECT_SETTINGS_WEBHOOKS: 'nav.project-settings-webhooks',
@@ -32,13 +31,6 @@ export const projectSettingsNavRegistry: RegistryDefinations<ProjectSettingsNavS
showInSettings: false,
referenceGroup: SHORTCUT_REFERENCE_GROUPS.NAVIGATION_PROJECT_SETTINGS,
},
[PROJECT_SETTINGS_NAV_SHORTCUT_IDS.NAV_PROJECT_SETTINGS_COMPUTE_AND_DISK]: {
id: PROJECT_SETTINGS_NAV_SHORTCUT_IDS.NAV_PROJECT_SETTINGS_COMPUTE_AND_DISK,
label: 'Go to Compute and Disk',
sequence: ['S', 'C'],
showInSettings: false,
referenceGroup: SHORTCUT_REFERENCE_GROUPS.NAVIGATION_PROJECT_SETTINGS,
},
[PROJECT_SETTINGS_NAV_SHORTCUT_IDS.NAV_PROJECT_SETTINGS_INFRASTRUCTURE]: {
id: PROJECT_SETTINGS_NAV_SHORTCUT_IDS.NAV_PROJECT_SETTINGS_INFRASTRUCTURE,
label: 'Go to Infrastructure',