Files
Joshen Lim fea8b8b41d Update copy RE disk configuration changes and cooldown (#50844)
## Context

Reverts copy changes from the following PRs:
- Docs: https://github.com/supabase/supabase/pull/42184
- FE: https://github.com/supabase/supabase/pull/47646

Our platform's disk management configuration limitation still follows
the 4 hour cooldown at the moment, doesn't align with AWS's 4 changes in
24 hours rule just yet. This is just to prevent any confusion for now,
and we'll need to update the copy again once behaviour matches AWS on
our BE

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

* **Updates**
* Disk changes are now subject to an approximately four-hour cooldown
after each modification, replacing the previous limit of four changes in
a rolling 24-hour period.
* Disk management screens now show the cooldown status, remaining wait
time, and next available update time.
* Updated platform guides and troubleshooting instructions to reflect
the cooldown and explain available recovery options.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-29 23:08:31 +08:00

317 lines
12 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { AlertTriangle, ArrowRight } from 'lucide-react'
import { UseFormReturn } from 'react-hook-form'
import {
Alert,
AlertTitle,
Button,
ButtonProps,
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogSection,
DialogSectionSeparator,
DialogTitle,
DialogTrigger,
WarningIcon,
} from 'ui'
import { DiskStorageSchemaType } from '../DiskManagement.schema'
import { DiskManagementMessage } from '../DiskManagement.types'
import { DISK_AUTOSCALE_CONFIG_DEFAULTS } from '../ui/DiskManagement.constants'
import {
BreakdownRow,
PriceDelta,
ValueChange,
} from './DiskManagementReviewAndSubmitDialog.components'
import { useDiskManagementReviewChanges } from './DiskManagementReviewAndSubmitDialog.hooks'
import { TaxDisclaimer } from '@/components/interfaces/Billing/TaxDisclaimer'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { formatCurrency } from '@/lib/helpers'
interface DiskManagementReviewAndSubmitDialogProps {
loading: boolean
form: UseFormReturn<DiskStorageSchemaType>
numReplicas: number
isDialogOpen: boolean
disabled?: boolean
setIsDialogOpen: (isOpen: boolean) => void
onSubmit: (values: DiskStorageSchemaType) => Promise<void>
buttonSize?: ButtonProps['size']
message?: DiskManagementMessage | null
}
export const DiskManagementReviewAndSubmitDialog = ({
isDialogOpen,
setIsDialogOpen,
disabled,
form,
numReplicas,
loading,
onSubmit,
message,
buttonSize = 'medium',
}: DiskManagementReviewAndSubmitDialogProps) => {
const { data: project } = useSelectedProjectQuery()
const { can: canUpdateDiskConfiguration } = useAsyncCheckPermissions(
PermissionAction.UPDATE,
'projects',
{ resource: { project_id: project?.id } }
)
const isDirty = !!Object.keys(form.formState.dirtyFields).length
const {
computeSizePrice,
diskSizePrice,
iopsPrice,
throughputPrice,
totalBeforePrice,
totalAfterPrice,
hasComputeChanges,
hasTotalSizeChanges,
hasStorageTypeChanges,
hasIOPSChanges,
hasGrowthPercentChanges,
hasMinIncrementChanges,
hasMaxSizeChanges,
anyBillableDiskChange,
anyDiskAttributeChange,
showThroughputRow,
hasAnyBreakdownRows,
hasExtendedDowntimeRisk,
oldComputeLabel,
newComputeLabel,
} = useDiskManagementReviewChanges(form, numReplicas)
return (
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<ButtonTooltip
size={buttonSize}
type="submit"
variant="primary"
onClick={async (e) => {
e.preventDefault()
const isValid = await form.trigger()
if (isValid) setIsDialogOpen(true)
}}
disabled={disabled || !isDirty}
tooltip={{
content: {
side: 'bottom',
text: !canUpdateDiskConfiguration
? 'You need additional permissions to update disk configuration'
: disabled
? 'Current form values are invalid'
: undefined,
},
}}
>
Review changes
</ButtonTooltip>
</DialogTrigger>
<DialogContent className="min-w-[560px] p-0 gap-0 overflow-hidden">
<DialogHeader className="px-5 py-4">
<DialogTitle>Review changes</DialogTitle>
<DialogDescription>Changes will be applied shortly after confirmation.</DialogDescription>
</DialogHeader>
<DialogSectionSeparator />
{(hasComputeChanges || anyBillableDiskChange) && (
<>
<div className="relative flex border-b">
<div className="flex-1 flex flex-col items-center gap-2 py-6 px-4 border-r bg-linear-to-t from-[var(--background-surface-100)] to-transparent">
<span className="text-xs uppercase tracking-widest font-mono text-foreground-lighter">
Before
</span>
<span className="text-3xl text-foreground-light tabular-nums" translate="no">
{formatCurrency(totalBeforePrice)}
</span>
<span className="text-xs uppercase tracking-widest font-mono text-foreground-lighter">
per month
</span>
</div>
<div className="animate-badge-pulse absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-9 h-9 rounded-full bg-dash-sidebar border border-brand-500 flex items-center justify-center z-10 overflow-hidden">
<span className="absolute inset-0 bg-brand-default/10 rounded-full" />
<span className="animate-badge-shimmer pointer-events-none absolute inset-0 bg-linear-to-br from-transparent via-brand-default/20 to-transparent blur-md" />
<ArrowRight size={16} className="text-brand-600 relative z-10" strokeWidth={2.5} />
</div>
<div className="flex-1 flex flex-col items-center gap-2 py-6 px-4 bg-linear-to-t from-[var(--background-surface-100)] to-transparent">
<span className="text-xs uppercase tracking-widest font-mono text-foreground-lighter">
After
</span>
<span className="text-3xl text-foreground tabular-nums" translate="no">
{formatCurrency(totalAfterPrice)}
</span>
<span className="text-xs uppercase tracking-widest font-mono text-foreground-lighter">
per month
</span>
</div>
</div>
<TaxDisclaimer className="px-5 py-2 text-center border-b" />
</>
)}
{hasAnyBreakdownRows && (
<div className="py-0.5 px-5">
{hasComputeChanges && (
<BreakdownRow
label="Compute size"
description="Project will restart automatically on confirmation."
>
<div className="flex flex-col items-end gap-0.5">
<ValueChange from={oldComputeLabel} to={newComputeLabel} />
<PriceDelta
delta={Number(computeSizePrice.newPrice) - Number(computeSizePrice.oldPrice)}
/>
</div>
</BreakdownRow>
)}
{hasStorageTypeChanges && (
<BreakdownRow label="Storage type">
<ValueChange
from={(form.formState.defaultValues?.storageType ?? '').toUpperCase()}
to={form.getValues('storageType').toUpperCase()}
/>
</BreakdownRow>
)}
{(hasIOPSChanges || hasStorageTypeChanges) && (
<BreakdownRow
label="IOPS"
description={
anyDiskAttributeChange && !hasTotalSizeChanges && !hasStorageTypeChanges
? 'Disk attributes, including IOPS and disk size, will be locked for 4 hours after this change.'
: undefined
}
>
<div className="flex flex-col items-end gap-0.5">
<ValueChange
from={(form.formState.defaultValues?.provisionedIOPS ?? 0).toLocaleString()}
to={(form.getValues('provisionedIOPS') ?? 0).toLocaleString()}
/>
<PriceDelta delta={Number(iopsPrice.newPrice) - Number(iopsPrice.oldPrice)} />
</div>
</BreakdownRow>
)}
{showThroughputRow && (
<BreakdownRow label="Throughput">
<div className="flex flex-col items-end gap-0.5">
<ValueChange
from={`${(form.formState.defaultValues?.throughput ?? 0).toLocaleString()} MB/s`}
to={`${(form.getValues('throughput') ?? 0).toLocaleString()} MB/s`}
/>
<PriceDelta
delta={Number(throughputPrice.newPrice) - Number(throughputPrice.oldPrice)}
/>
</div>
</BreakdownRow>
)}
{(hasTotalSizeChanges || hasStorageTypeChanges) && (
<BreakdownRow
label="Disk size"
description="For 4 hours after changes you will not be able to modify disk attributes."
>
<div className="flex flex-col items-end gap-0.5">
<ValueChange
from={`${(form.formState.defaultValues?.totalSize ?? 0).toLocaleString()} GB`}
to={`${(form.getValues('totalSize') ?? 0).toLocaleString()} GB`}
/>
<PriceDelta
delta={Number(diskSizePrice.newPrice) - Number(diskSizePrice.oldPrice)}
/>
</div>
</BreakdownRow>
)}
{hasGrowthPercentChanges && (
<BreakdownRow label="Growth percent">
<ValueChange
from={`${form.formState.defaultValues?.growthPercent ?? DISK_AUTOSCALE_CONFIG_DEFAULTS.growthPercent}%`}
to={`${form.getValues('growthPercent') ?? DISK_AUTOSCALE_CONFIG_DEFAULTS.growthPercent}%`}
/>
</BreakdownRow>
)}
{hasMinIncrementChanges && (
<BreakdownRow label="Min increment">
<ValueChange
from={`${form.formState.defaultValues?.minIncrementGb ?? DISK_AUTOSCALE_CONFIG_DEFAULTS.minIncrementSize} GB`}
to={`${form.getValues('minIncrementGb') ?? DISK_AUTOSCALE_CONFIG_DEFAULTS.minIncrementSize} GB`}
/>
</BreakdownRow>
)}
{hasMaxSizeChanges && (
<BreakdownRow label="Max disk size">
<ValueChange
from={(
form.formState.defaultValues?.maxSizeGb ??
DISK_AUTOSCALE_CONFIG_DEFAULTS.maxSizeGb
).toLocaleString()}
to={(
form.getValues('maxSizeGb') ?? DISK_AUTOSCALE_CONFIG_DEFAULTS.maxSizeGb
).toLocaleString()}
/>
</BreakdownRow>
)}
{numReplicas > 0 && (
<div className="py-2 text-xs text-foreground-muted">
Price change includes primary database and {numReplicas} replica
{numReplicas > 1 ? 's' : ''}
</div>
)}
</div>
)}
{hasExtendedDowntimeRisk && (
<>
<DialogSectionSeparator />
<DialogSection>
<Alert variant="warning">
<AlertTriangle />
<AlertTitle>
Resizes may require more downtime than normal on this project.
</AlertTitle>
</Alert>
</DialogSection>
</>
)}
<DialogFooter className="px-5 py-4">
<Button block size="large" onClick={() => setIsDialogOpen(false)}>
Cancel
</Button>
<Button
block
variant="primary"
size="large"
type="submit"
loading={loading}
onClick={async () => {
await onSubmit(form.getValues())
}}
>
Confirm changes
</Button>
</DialogFooter>
{message && (
<>
<DialogSectionSeparator />
<DialogSection>
<Alert variant={message.type === 'error' ? 'destructive' : 'default'}>
<WarningIcon />
<AlertTitle>{message.message}</AlertTitle>
</Alert>
</DialogSection>
</>
)}
</DialogContent>
</Dialog>
)
}