mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
## 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 -->
317 lines
12 KiB
TypeScript
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>
|
|
)
|
|
}
|