// @ts-nocheck 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 { Admonition } from 'ui-patterns' import { FormFooterChangeBadge } from '../DataWarehouse/FormFooterChangeBadge' import { CreateDiskStorageSchema, DiskStorageSchemaType } from './DiskManagement.schema' import { DiskManagementMessage } from './DiskManagement.types' import { calculateDiskSizeRequiredForIopsWithGp3, mapComputeSizeNameToAddonVariantId, } from './DiskManagement.utils' import { DiskMangementRestartRequiredSection } from './DiskManagementRestartRequiredSection' import { DiskManagementReviewAndSubmitDialog } from './DiskManagementReviewAndSubmitDialog/DiskManagementReviewAndSubmitDialog' import { AutoScaleFields } from './fields/AutoScaleFields' import { ComputeSizeField } from './fields/ComputeSizeField' import { DiskSizeField } from './fields/DiskSizeField' import { IOPSField } from './fields/IOPSField' import { StorageTypeField } from './fields/StorageTypeField' import { ThroughputField } from './fields/ThroughputField' import { DiskCountdownRadial } from './ui/DiskCountdownRadial' import { DISK_LIMITS, DiskType, 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 { DocsButton } from '@/components/ui/DocsButton' import { RequestUpgradeToBillingOwners } from '@/components/ui/RequestUpgradeToBillingOwners' import { UpgradeToPro } from '@/components/ui/UpgradeToPro' import { useDiskAttributesQuery, useRemainingDurationForDiskAttributeUpdate, } from '@/data/config/disk-attributes-query' import { useUpdateDiskAttributesMutation } from '@/data/config/disk-attributes-update-mutation' import { useDiskAutoscaleCustomConfigQuery } from '@/data/config/disk-autoscale-config-query' import { useUpdateDiskAutoscaleConfigMutation } from '@/data/config/disk-autoscale-config-update-mutation' import { useDiskUtilizationQuery } from '@/data/config/disk-utilization-query' import { useSetProjectStatus } from '@/data/projects/project-detail-query' import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query' import { useProjectAddonUpdateMutation } from '@/data/subscriptions/project-addon-update-mutation' import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query' import { AddonVariantId } from '@/data/subscriptions/types' import { useResourceWarningsQuery } from '@/data/usage/resource-warnings-query' import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { useIsAwsCloudProvider, useIsAwsK8sCloudProvider, useIsAwsNimbusCloudProvider, useSelectedProjectQuery, } from '@/hooks/misc/useSelectedProject' import { DOCS_URL, GB, PROJECT_STATUS } from '@/lib/constants' export function DiskManagementForm() { const { ref: projectRef } = useParams() const { data: project, isPending: isProjectPending } = useSelectedProjectQuery() const { data: org } = useSelectedOrganizationQuery() const { setProjectStatus } = useSetProjectStatus() const advancedSettingsRef = useRef(null) const isSpendCapEnabled = org?.plan.id !== 'free' && !org?.usage_billing_enabled && project?.cloud_provider !== 'FLY' 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( (warning) => warning.project === project?.ref ) const isReadOnlyMode = projectResourceWarnings?.is_readonly_mode_enabled const isAws = useIsAwsCloudProvider() const isAwsK8s = useIsAwsK8sCloudProvider() const isAwsNimbus = useIsAwsNimbusCloudProvider() const { can: canUpdateDiskConfiguration, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(PermissionAction.UPDATE, 'projects', { resource: { project_id: project?.id, }, }) const { hasAccess, isSuccess: isEntitlementsLoaded } = useCheckEntitlements( 'instances.compute_update_available_sizes' ) const [isDialogOpen, setIsDialogOpen] = useState(false) const [refetchInterval, setRefetchInterval] = useState(false) const [message, setMessageState] = useState(null) const [advancedSettingsOpen, setAdvancedSettingsOpenState] = useState(false) const { data: databases, isSuccess: isReadReplicasSuccess } = useReadReplicasQuery({ projectRef }) const { data, isSuccess: isDiskAttributesSuccess } = useDiskAttributesQuery( { projectRef }, { refetchInterval, refetchOnWindowFocus: false, enabled: project != null && isAws, } ) const { isSuccess: isAddonsSuccess } = useProjectAddonsQuery({ projectRef }) const { isWithinCooldownWindow, isSuccess: isCooldownSuccess } = useRemainingDurationForDiskAttributeUpdate({ projectRef, enabled: project != null && isAws, }) const { data: diskUtil, isSuccess: isDiskUtilizationSuccess } = useDiskUtilizationQuery( { projectRef, }, { enabled: project != null && isAws } ) const { data: diskAutoscaleConfig, isSuccess: isDiskAutoscaleConfigSuccess } = useDiskAutoscaleCustomConfigQuery({ projectRef }, { enabled: project != null && isAws }) const computeSize = project?.infra_compute_size ? mapComputeSizeNameToAddonVariantId(project?.infra_compute_size) : undefined // @ts-ignore const { type, iops, throughput_mbps, size_gb } = data?.attributes ?? { size_gb: 0, iops: 0 } const { growth_percent, max_size_gb, min_increment_gb } = diskAutoscaleConfig ?? {} const defaultValues = { storageType: type ?? DiskType.GP3, provisionedIOPS: iops, throughput: throughput_mbps, totalSize: size_gb, computeSize: computeSize ?? 'ci_micro', growthPercent: growth_percent, minIncrementGb: min_increment_gb, maxSizeGb: max_size_gb, } const form = useForm({ resolver: zodResolver( CreateDiskStorageSchema({ defaultTotalSize: defaultValues.totalSize, cloudProvider: project?.cloud_provider as CloudProvider, isSpendCapEnabled, } as any) ), defaultValues, mode: 'onBlur', reValidateMode: 'onChange', }) const { computeSize: modifiedComputeSize } = form.watch() const isSuccess = isAddonsSuccess && isDiskAttributesSuccess && isDiskUtilizationSuccess && isReadReplicasSuccess && isDiskAutoscaleConfigSuccess && isCooldownSuccess const isRequestingChanges = data?.requested_modification !== undefined const readReplicas = (databases ?? []).filter((db) => db.identifier !== projectRef) const isPlanUpgradeRequired = !hasAccess const { formState } = form const errors = formState.errors const usedSize = Math.round(((diskUtil?.metrics.fs_used_bytes ?? 0) / GB) * 100) / 100 const totalSize = formState.defaultValues?.totalSize || 0 const usedPercentage = (usedSize / totalSize) * 100 const disableIopsThroughputConfig = modifiedComputeSize && !isSpendCapEnabled && RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3.includes(modifiedComputeSize) const watchedTotalSize = form.watch('totalSize') ?? 0 const watchedStorageType = form.watch('storageType') // Minimum disk size where the platform API will accept an IOPS payload (500 IOPS/GB rule). const minDiskSizeForCustomIops = calculateDiskSizeRequiredForIopsWithGp3( DISK_LIMITS[DiskType.GP3].minIops ) // Suggested target when prompting a resize, sits above the floor so users // aren't pinned at the minimum during the 4-hour disk-config cooldown. const suggestedDiskSizeForCustomIops = PLAN_DETAILS.pro.includedDiskGB.gp3 const isDiskTooSmallForCustomIops = watchedStorageType === 'gp3' && watchedTotalSize < minDiskSizeForCustomIops const isBranch = project?.parent_project_ref !== undefined const disableDiskSizeInput = isRequestingChanges || isPlanUpgradeRequired || isWithinCooldownWindow || !canUpdateDiskConfiguration || !isAws const disableDiskInputs = disableDiskSizeInput || isSpendCapEnabled const disableComputeInputs = isPlanUpgradeRequired const isDirty = !!Object.keys(form.formState.dirtyFields).length const isProjectResizing = project?.status === PROJECT_STATUS.RESIZING const isProjectRequestingDiskChanges = isRequestingChanges && !isProjectResizing const noPermissions = isPermissionsLoaded && !canUpdateDiskConfiguration const isDiskNoticeVisible = !isProjectPending && !(isAws || isAwsNimbus) const { mutateAsync: updateDiskConfiguration, isPending: isUpdatingDisk } = useUpdateDiskAttributesMutation({ // this is to suppress to toast message onError: () => {}, onSuccess: () => setRefetchInterval(2000), }) const { mutateAsync: updateSubscriptionAddon, isPending: isUpdatingCompute } = useProjectAddonUpdateMutation({ // this is to suppress to toast message onError: () => {}, onSuccess: () => { //Manually set project status to RESIZING, Project status should be RESIZING on next project status request. if (projectRef) setProjectStatus({ ref: projectRef, status: PROJECT_STATUS.RESIZING }) }, }) const { mutateAsync: updateDiskAutoscaleConfig, isPending: isUpdatingDiskAutoscaleConfig } = useUpdateDiskAutoscaleConfigMutation({ // this is to suppress to toast message onError: () => {}, }) const isUpdatingConfig = isUpdatingDisk || isUpdatingCompute || isUpdatingDiskAutoscaleConfig const onSubmit = async (data: DiskStorageSchemaType) => { let payload = data let willUpdateDiskConfiguration = false setMessageState(null) // [Joshen] Skip disk configuration related stuff for AWS Nimbus try { if ( !isAwsK8s && !isAwsNimbus && (payload.storageType !== form.formState.defaultValues?.storageType || payload.provisionedIOPS !== form.formState.defaultValues?.provisionedIOPS || payload.throughput !== form.formState.defaultValues?.throughput || payload.totalSize !== form.formState.defaultValues?.totalSize) ) { willUpdateDiskConfiguration = true await updateDiskConfiguration({ ref: projectRef, provisionedIOPS: payload.provisionedIOPS!, storageType: payload.storageType, totalSize: payload.totalSize!, throughput: payload.throughput, }) } if ( !isAwsK8s && !isAwsNimbus && (payload.growthPercent !== form.formState.defaultValues?.growthPercent || payload.minIncrementGb !== form.formState.defaultValues?.minIncrementGb || payload.maxSizeGb !== form.formState.defaultValues?.maxSizeGb) ) { await updateDiskAutoscaleConfig({ projectRef, growthPercent: payload.growthPercent, minIncrementGb: payload.minIncrementGb, maxSizeGb: payload.maxSizeGb, }) } if (payload.computeSize !== form.formState.defaultValues?.computeSize) { await updateSubscriptionAddon({ projectRef: projectRef, // cast variant to AddonVariantId to satisfy type variant: payload.computeSize as AddonVariantId, type: 'compute_instance', suppressToast: true, }) } setIsDialogOpen(false) form.reset(data as DiskStorageSchemaType) toast.success( `Successfully updated disk settings!${willUpdateDiskConfiguration ? ' The requested changes will be applied to your disk shortly.' : ''}` ) } catch (error: unknown) { setMessageState({ message: error instanceof Error ? error.message : 'An unknown error occurred', type: 'error', }) } } useEffect(() => { if (!isDiskAttributesSuccess) return // @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, computeSize: form.getValues('computeSize'), } if (!('requested_modification' in data)) { if (refetchInterval !== false) { form.reset(formValues) setRefetchInterval(false) toast.success('Disk configuration changes have been successfully applied!') } } else { setRefetchInterval(2000) } }, [data, isDiskAttributesSuccess, form, refetchInterval]) // We only support disk configurations for >=Large instances // If a customer downgrades back to { if (modifiedComputeSize && project?.infra_compute_size && isDialogOpen) { if (RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3.includes(modifiedComputeSize)) { form.setValue('storageType', DiskType.GP3) form.setValue('throughput', DISK_LIMITS['gp3'].minThroughput) form.setValue('provisionedIOPS', DISK_LIMITS['gp3'].minIops) } } }, [modifiedComputeSize, isDialogOpen, project]) useEffect(() => { // Initialize field values properly when data has been loaded, preserving any user changes if (isDiskAttributesSuccess || isSuccess) { form.reset(defaultValues, {}) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isSuccess, isDiskAttributesSuccess]) useEffect(() => { const fieldErrors = Object.keys(errors) if (fieldErrors.length > 0) { if ( fieldErrors.includes('throughput') || fieldErrors.includes('provisionedIOPS') || fieldErrors.includes('maxSizeGb') ) { setAdvancedSettingsOpenState(true) // [Joshen] The timeout is to let the collapsible open prior to scrolling const timeoutId = setTimeout(() => { advancedSettingsRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) }, 100) return () => clearTimeout(timeoutId) } } }, [errors]) return ( <> {isEntitlementsLoaded && isPlanUpgradeRequired && ( )} {(isProjectResizing || isProjectRequestingDiskChanges || (isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions)) && (
)}
{isDiskNoticeVisible && }
{isAws && ( <>
{!isReadOnlyMode && usedPercentage >= 90 && isWithinCooldownWindow && ( )} {isReadOnlyMode && ( )}
)}
{isAws && ( <> setAdvancedSettingsOpenState((prev) => !prev)} >
Advanced disk settings Specify additional settings for your disk, including autoscaling configuration, IOPS, throughput, and disk type.
{ form.setValue('computeSize', 'ci_large') }} > Change to LARGE Compute ) : ( ) } /> { form.setValue('totalSize', suggestedDiskSizeForCustomIops, { shouldDirty: true, shouldValidate: true, }) }} > Increase to {suggestedDiskSizeForCustomIops} GB ) : undefined } />
)}
{isDirty ? (
) : null}
) }