import { zodResolver } from '@hookform/resolvers/zod' import { AnimatePresence, motion } from 'framer-motion' import { InfoIcon, RotateCcw } from 'lucide-react' import Link from 'next/link' import { useEffect, useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { useParams } from 'common' import DiskSpaceBar from 'components/interfaces/DiskManagement/DiskSpaceBar' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { FormHeader } from 'components/ui/Forms/FormHeader' import { useDiskAttributesQuery, useRemainingDurationForDiskAttributeUpdate, } from 'data/config/disk-attributes-query' import { useUpdateDiskAttributesMutation } from 'data/config/disk-attributes-update-mutation' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { GB } from 'lib/constants' import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Badge, Button, Card, CardContent, Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, FormItem_Shadcn_, Input_Shadcn_ as Input, RadioGroupCard, RadioGroupCardItem, Separator, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { FormFooterChangeBadge } from '../DataWarehouse/FormFooterChangeBadge' import BillingChangeBadge from './BillingChangeBadge' import { DiskCountdownRadial } from './DiskCountdownRadial' import { DiskType, PLAN_DETAILS } from './DiskManagement.constants' import { calculateDiskSizePrice, calculateIOPSPrice, calculateThroughputPrice, } from './DiskManagement.utils' import { DiskManagementDiskSizeReadReplicas } from './DiskManagementDiskSizeReadReplicas' import { DiskStorageSchema, DiskStorageSchemaType } from './DiskManagementPanelSchema' import { DiskManagementPlanUpgradeRequired } from './DiskManagementPlanUpgradeRequired' import { DiskManagementReviewAndSubmitDialog } from './DiskManagementReviewAndSubmitDialog' import { useDiskUtilizationQuery } from 'data/config/disk-utilization-query' export function DiskManagementPanelForm() { const org = useSelectedOrganization() const { project } = useProjectContext() const { ref: projectRef } = useParams() const [isDialogOpen, setIsDialogOpen] = useState(false) const [remainingTime, setRemainingTime] = useState(0) const [refetchInterval, setRefetchInterval] = useState(false) const { data, isSuccess } = useDiskAttributesQuery( { projectRef }, { refetchInterval, refetchOnWindowFocus: false, onSuccess: (data) => { // @ts-ignore const { type, iops, throughput_mbps, size_gb } = data?.attributes ?? { size_gb: 0 } const formValues = { storageType: type, provisionedIOPS: iops, throughput: throughput_mbps, totalSize: size_gb, } if (!('requested_modification' in data)) { if (refetchInterval !== false) { form.reset(formValues) setRefetchInterval(false) toast.success('Disk configuration changes have been successfully applied!') } } }, } ) // @ts-ignore [Joshen TODO] check whats happening here const { type, iops, throughput_mbps, size_gb } = data?.attributes ?? { size_gb: 0 } const isRequestingChanges = data?.requested_modification !== undefined const { remainingDuration: initialRemainingTime, isWithinCooldownWindow } = useRemainingDurationForDiskAttributeUpdate({ projectRef, }) const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: org?.slug, }) const planId = subscription?.plan.id ?? '' const isPlanUpgradeRequired = subscription?.plan.id === 'pro' && !subscription.usage_billing_enabled const { data: diskUtil } = useDiskUtilizationQuery({ projectRef }) const mainDiskUsed = Math.round(((diskUtil?.metrics.fs_used_bytes ?? 0) / GB) * 100) / 100 const { mutate: updateDiskConfigurationRQ, isLoading: isUpdatingDiskConfiguration } = useUpdateDiskAttributesMutation({ onSuccess: (_, vars) => { toast.success( 'Successfully requested disk configuration changes! Your changes will be applied shortly' ) const { ref, ...formData } = vars setIsDialogOpen(false) setRefetchInterval(3000) form.reset(formData as DiskStorageSchemaType) }, }) const defaultValues = { storageType: type, provisionedIOPS: iops, throughput: throughput_mbps, totalSize: size_gb, } const form = useForm({ resolver: zodResolver(DiskStorageSchema), defaultValues, mode: 'onBlur', reValidateMode: 'onChange', }) const { watch, setValue, trigger, control, formState } = form const watchedStorageType = watch('storageType') const watchedTotalSize = watch('totalSize') const watchedIOPS = watch('provisionedIOPS') const { dirtyFields } = formState // Destructure dirtyFields from formState const isAllocatedStorageDirty = !!dirtyFields.totalSize // Check if 'allocatedStorage' is dirty const disableInput = isRequestingChanges || isPlanUpgradeRequired || isWithinCooldownWindow const { includedDiskGB: includedDiskGBMeta } = PLAN_DETAILS?.[planId as keyof typeof PLAN_DETAILS] ?? {} const includedDiskGB = includedDiskGBMeta[watchedStorageType] const maxIOPS = watchedStorageType === 'gp3' ? Math.min(500 * watchedTotalSize, 16000) : Math.min(1000 * watchedTotalSize, 256000) const maxThroughput = watchedStorageType === 'gp3' ? Math.min(0.25 * watchedIOPS, 1000) : undefined const onSubmit = async (data: DiskStorageSchemaType) => { if (projectRef === undefined) return console.error('Project ref is required') updateDiskConfigurationRQ({ ref: projectRef, ...data }) } const diskSizePrice = calculateDiskSizePrice({ planId, oldSize: form.formState.defaultValues?.totalSize || 0, oldStorageType: form.formState.defaultValues?.storageType as DiskType, newSize: form.getValues('totalSize'), newStorageType: form.getValues('storageType') as DiskType, }) const iopsPrice = calculateIOPSPrice({ oldStorageType: form.formState.defaultValues?.storageType as DiskType, oldProvisionedIOPS: form.formState.defaultValues?.provisionedIOPS || 0, newStorageType: form.getValues('storageType') as DiskType, newProvisionedIOPS: form.getValues('provisionedIOPS'), }) const throughputPrice = calculateThroughputPrice({ storageType: form.getValues('storageType') as DiskType, newThroughput: form.getValues('throughput') || 0, oldThroughput: form.formState.defaultValues?.throughput || 0, }) useEffect(() => { // Initialize field values properly when data has been loaded if (isSuccess) form.reset(defaultValues) }, [isSuccess]) // Watch storageType and allocatedStorage to adjust constraints dynamically useEffect(() => { if (watchedStorageType === 'io2') { setValue('throughput', undefined) // Throughput is not configurable for 'io2' } else if (watchedStorageType === 'gp3') { // Ensure throughput is within the allowed range if it's greater than or equal to 400 GiB const currentThroughput = form.getValues('throughput') if (!currentThroughput || currentThroughput < 125 || currentThroughput > 1000) { setValue('throughput', 125) // Reset to default if undefined or out of bounds } } }, [watchedStorageType, watchedTotalSize, setValue, form]) useEffect(() => { if (initialRemainingTime > 0) setRemainingTime(initialRemainingTime) }, [initialRemainingTime]) useEffect(() => { if (remainingTime <= 0) return const timer = setInterval(() => { setRemainingTime(Math.max(0, remainingTime - 1)) }, 1000) return () => clearInterval(timer) }, [remainingTime]) if (planId === 'free') { return (
Disk size configuration is not available for projects on the Free Plan

If you are intending to use more than 500MB of disk space, then you will need to upgrade to at least the Pro Plan.

) } return (
( { field.onChange(e) // only trigger provisionedIOPS due to other input being hidden await form.trigger('provisionedIOPS') }} defaultValue={field.value} disabled={disableInput} >
General Purpose SSD{' '}
gp3

gp3 provides a balance between price and performance

} />
Provisioned IOPS SSD{' '}
io2

io2 offers high IOPS for mission-critical applications.

} /> )} /> ( For io2 storage type, IOPS must be{' '} {watchedTotalSize >= 8 ? `between 100 and ${maxIOPS}.` : `at least 100`} ) : ( <> For gp3 storage type, IOPS must be{' '} {watchedTotalSize >= 8 ? `between 3,000 and ${maxIOPS.toLocaleString()}.` : `at least 3,000`} ) } labelOptional="Input/output operations per second. Higher IOPS is suitable for applications requiring high throughput." >
{ setValue('provisionedIOPS', e.target.valueAsNumber, { shouldDirty: true, shouldValidate: true, }) }} />
IOPS
)} /> {form.getValues('storageType') === 'gp3' && ( ( For gp3 storage type, throughput must be between 125 and {maxThroughput} MiBps } >
{ setValue('throughput', e.target.valueAsNumber, { shouldDirty: true, shouldValidate: true, }) }} className="flex-grow font-mono rounded-r-none max-w-32" disabled={disableInput || watchedStorageType === 'io2'} />
MiBps
)} />
)}
(
e.currentTarget.blur()} onChange={(e) => { setValue('totalSize', e.target.valueAsNumber, { shouldDirty: true, shouldValidate: true, }) trigger('provisionedIOPS') trigger('throughput') }} min={includedDiskGB} />
GiB
{isAllocatedStorageDirty && ( )} {includedDiskGB > 0 && (
Your plan includes {includedDiskGB} GiB of disk size for{' '} {watchedStorageType}.
)}
)} />
{/* You can add additional content in the remaining 4 columns if needed */}
{/* Additional content or information can go here */}
{isRequestingChanges ? (

Disk configuration changes have been requested

The requested changes will be applied to your disk shortly

) : ( )} {isPlanUpgradeRequired && }
) }