Files
supabase/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.sections.tsx
Saxon Fletcher 618e003136 Modernize compute and disk configuration (#48368)
## Summary

This is a precursor to unifying "compute and disk" and "infrastructure
pages". First step is just updating the existing compute and disk page
to make use of standard page and form patterns.

- Reorganizes the existing Compute and Disk form into the established
settings layout pattern, with Scaling, Compute, Disk, and Advanced
sections.
- Moves billing deltas to section headers while retaining the sticky
review footer.
- Adds animated notices, validation-error scrolling, and a responsive
compute-size selector (2 columns by default, 3 from 680px, and 4 from
900px).
- Preserves permissions, entitlements, cooldowns, read-only warnings,
replica pricing, GP3 validation, Nano/PITR locks, and the free Micro
upgrade treatment.
- Keeps the redesigned experience on `/settings/compute-and-disk` so it
can be reviewed independently of the route cutover.

## Stack

1. #48368 (this PR)
2. #48369
3. #48370

## How to test

1. Check out `chore/infra-compute-1-config` and start Studio with `pnpm
dev:studio`.
2. Open `/project/<ref>/settings/compute-and-disk`.
3. Confirm the page header and Scaling, Compute, Disk, and Advanced
sections follow the standard settings-page spacing, with the plan notice
directly below the Scaling header.
4. Change the compute size and disk configuration. Confirm billing
deltas appear in the relevant section headers and the sticky review
footer summarizes and applies the pending changes.
5. Enter invalid GP3 IOPS or throughput values and submit. Confirm the
validation message is shown and the first invalid field is scrolled into
view.
6. Exercise representative project states: insufficient permissions,
read-only mode, resize cooldown, Nano with PITR, a project with read
replicas, and a free Micro upgrade. Confirm the existing locks,
warnings, pricing, and upgrade treatment remain intact.
7. Resize the viewport and confirm the compute cards use 2 columns by
default, 3 columns from 680px, and no more than 4 columns from 900px.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Reworked the Compute and Disk settings UI into clearer sectioned
pages, including smoother navigation to the first validation error.
- Added a transitional in-form NoticeBar for consistent alert rendering.
- Improved billing change badges to reflect accurate before/after
totals.
- **Bug Fixes**
  - Fixed disk IOPS/throughput pricing to include replica-based charges.
- Strengthened disk sizing validation (GP3 limits, legacy/size edge
cases, spend-cap behavior, and provider-specific constraints).
- **Refactor**
  - Updated key form field layouts for improved readability.
- **Tests**
  - Expanded schema validation and pricing/billing badge test coverage.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
2026-07-29 10:56:19 +10:00

328 lines
11 KiB
TypeScript

import { type RefObject } from 'react'
import { type UseFormReturn } from 'react-hook-form'
import { Button, Card, CardContent } from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import {
PageSection,
PageSectionAside,
PageSectionContent,
PageSectionDescription,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import { DiskStorageSchemaType } from './DiskManagement.schema'
import { AutoScaleFields } from './fields/AutoScaleFields'
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 { DiskSpaceBar } from './ui/DiskSpaceBar'
import { NoticeBar } from './ui/NoticeBar'
import { SpendCapDisabledSection } from './ui/SpendCapDisabledSection'
import { DocsButton } from '@/components/ui/DocsButton'
import { RequestUpgradeToBillingOwners } from '@/components/ui/RequestUpgradeToBillingOwners'
import { DOCS_URL } from '@/lib/constants'
interface ComputeSectionProps {
form: UseFormReturn<DiskStorageSchemaType>
settingsRef: RefObject<HTMLDivElement | null>
showBillingBadge: boolean
beforePrice: number
afterPrice: number
disabled: boolean
}
export function ComputeSection({
form,
settingsRef,
showBillingBadge,
beforePrice,
afterPrice,
disabled,
}: ComputeSectionProps) {
return (
<PageSection className="pt-0">
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle className="heading-default text-base">Compute size</PageSectionTitle>
<PageSectionDescription>
<ComputeSizeFieldMeta />
</PageSectionDescription>
</PageSectionSummary>
<PageSectionAside>
<ComputeSectionBillingBadge
form={form}
show={showBillingBadge}
beforePrice={beforePrice}
afterPrice={afterPrice}
/>
<DocsButton href={`${DOCS_URL}/guides/platform/compute-and-disk`} />
</PageSectionAside>
</PageSectionMeta>
<PageSectionContent ref={settingsRef} className="scroll-mt-24">
<ComputeSizeField form={form} disabled={disabled} />
</PageSectionContent>
</PageSection>
)
}
interface DiskSectionProps {
form: UseFormReturn<DiskStorageSchemaType>
settingsRef: RefObject<HTMLDivElement | null>
showBillingBadge: boolean
beforePrice: number
afterPrice: number
isAws: boolean
isAwsK8s: boolean
isBranch: boolean
isNoticeVisible: boolean
isReadOnlyMode: boolean
usedPercentage: number
isWithinCooldownWindow: boolean
currentDiskSizeGb?: number
disableDiskSizeInput: boolean
}
function getDiskNoticeDescription({
isAwsK8s,
isBranch,
}: Pick<DiskSectionProps, 'isAwsK8s' | 'isBranch'>) {
if (isAwsK8s) {
return 'Configuring your disk for AWS (Revamped) projects is unavailable for now.'
}
if (isBranch) {
return 'Delete and recreate your Preview Branch to configure disk size. It was deployed on an older branching infrastructure.'
}
return 'The Fly Postgres offering is deprecated - please migrate your instance to the AWS cloud provider to configure your disk.'
}
export function DiskSection({
form,
settingsRef,
showBillingBadge,
beforePrice,
afterPrice,
isAws,
isAwsK8s,
isBranch,
isNoticeVisible,
isReadOnlyMode,
usedPercentage,
isWithinCooldownWindow,
currentDiskSizeGb,
disableDiskSizeInput,
}: DiskSectionProps) {
const noticeDescription = getDiskNoticeDescription({ isAwsK8s, isBranch })
return (
<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={showBillingBadge}
beforePrice={beforePrice}
afterPrice={afterPrice}
/>
<DocsButton href={`${DOCS_URL}/guides/platform/database-size`} />
</PageSectionAside>
</PageSectionMeta>
<PageSectionContent ref={settingsRef} className="flex flex-col gap-4 scroll-mt-24">
{isAws && <DiskSpaceBar form={form} />}
<SpendCapDisabledSection currentDiskSizeGb={currentDiskSizeGb} />
<NoticeBar
type="default"
visible={isNoticeVisible}
title="Disk configuration is only available for projects in the AWS cloud provider"
description={noticeDescription}
/>
{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>
<Card>
<CardContent>
<DiskSizeField form={form} disableInput={disableDiskSizeInput} />
</CardContent>
</Card>
</>
)}
</PageSectionContent>
</PageSection>
)
}
interface AdvancedSectionProps {
form: UseFormReturn<DiskStorageSchemaType>
autoscaleSettingsRef: RefObject<HTMLDivElement | null>
storageSettingsRef: RefObject<HTMLDivElement | null>
showBillingBadge: boolean
beforePrice: number
afterPrice: number
disableIopsThroughputConfig?: boolean
canUpdateDiskConfiguration: boolean
isDiskTooSmallForCustomIops: boolean
disableDiskInputs: boolean
disableDiskSizeInput: boolean
suggestedDiskSizeForCustomIops: number
}
export function AdvancedSection({
form,
autoscaleSettingsRef,
storageSettingsRef,
showBillingBadge,
beforePrice,
afterPrice,
disableIopsThroughputConfig,
canUpdateDiskConfiguration,
isDiskTooSmallForCustomIops,
disableDiskInputs,
disableDiskSizeInput,
suggestedDiskSizeForCustomIops,
}: AdvancedSectionProps) {
return (
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle className="heading-default text-base">Advanced</PageSectionTitle>
<PageSectionDescription>
Configure autoscaling, storage type, IOPS, and throughput.
</PageSectionDescription>
</PageSectionSummary>
<PageSectionAside>
<BillingChangeBadge
show={showBillingBadge}
beforePrice={beforePrice}
afterPrice={afterPrice}
/>
</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', {
shouldDirty: true,
shouldValidate: true,
})
form.trigger('provisionedIOPS')
form.trigger('throughput')
}}
>
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 limited to 4 within a rolling 24-hour window).`}
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}
/>
<IOPSField
form={form}
disableInput={
!!disableIopsThroughputConfig || disableDiskInputs || isDiskTooSmallForCustomIops
}
/>
<ThroughputField
form={form}
disableInput={
!!disableIopsThroughputConfig || disableDiskInputs || isDiskTooSmallForCustomIops
}
/>
</CardContent>
</Card>
</PageSectionContent>
</PageSection>
)
}