import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQueryClient } from '@tanstack/react-query' import { AnimatePresence, motion } from 'framer-motion' import { ChevronRight } 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 { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { MAX_WIDTH_CLASSES, PADDING_CLASSES, ScaffoldContainer } from 'components/layouts/Scaffold' import { useDiskAttributesQuery, useRemainingDurationForDiskAttributeUpdate, } from 'data/config/disk-attributes-query' import { useUpdateDiskAttributesMutation } from 'data/config/disk-attributes-update-mutation' import { useDiskUtilizationQuery } from 'data/config/disk-utilization-query' import { setProjectStatus } from 'data/projects/projects-query' import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-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 { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { PROJECT_STATUS } from 'lib/constants' import { Button, cn, Collapsible_Shadcn_, CollapsibleContent_Shadcn_, CollapsibleTrigger_Shadcn_, Form_Shadcn_, Separator, } from 'ui' import { FormFooterChangeBadge } from '../DataWarehouse/FormFooterChangeBadge' import { CreateDiskStorageSchema, DiskStorageSchemaType } from './DiskManagement.schema' import { mapComputeSizeNameToAddonVariantId, showMicroUpgrade } from './DiskManagement.utils' import { DiskMangementRestartRequiredSection } from './DiskManagementRestartRequiredSection' import { DiskManagementReviewAndSubmitDialog } from './DiskManagementReviewAndSubmitDialog' 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 { DiskType, IOPS_RANGE, RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3, } from './ui/DiskManagement.constants' import { NoticeBar } from './ui/NoticeBar' import { SpendCapDisabledSection } from './ui/SpendCapDisabledSection' import { DiskManagementMessage } from './DiskManagement.types' export function DiskManagementForm() { const { project, // isLoading is used to avoid a useCheckPermissions() race condition isLoading: isProjectLoading, } = useProjectContext() const org = useSelectedOrganization() const { ref: projectRef } = useParams() const queryClient = useQueryClient() /** * Permissions */ const isPermissionsLoaded = usePermissionsLoaded() const canUpdateDiskConfiguration = useCheckPermissions(PermissionAction.UPDATE, 'projects', { resource: { project_id: project?.id, }, }) /** * Component States */ const [isDialogOpen, setIsDialogOpen] = useState(false) const [refetchInterval, setRefetchInterval] = useState(false) const [message, setMessageState] = useState(null) const [advancedSettingsOpen, setAdvancedSettingsOpenState] = useState(false) /** * Fetch form data */ const { data: databases, // isLoading: isReadReplicasLoading, // error: readReplicasError, isSuccess: isReadReplicasSuccess, } = useReadReplicasQuery({ projectRef }) const { data, // isLoading: isDiskAttributesLoading, // error: diskAttributesError, isSuccess: isDiskAttributesSuccess, } = 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!') } } }, } ) const { isSuccess: isAddonsSuccess } = useProjectAddonsQuery({ projectRef }) const { isWithinCooldownWindow, isSuccess: isCooldownSuccess } = useRemainingDurationForDiskAttributeUpdate({ projectRef, }) const { isSuccess: isDiskUtilizationSuccess } = useDiskUtilizationQuery({ projectRef, }) const { data: subscription, isSuccess: isSubscriptionSuccess } = useOrgSubscriptionQuery({ orgSlug: org?.slug, }) /** * Handle default values */ // @ts-ignore [Joshen TODO] check whats happening here const { type, iops, throughput_mbps, size_gb } = data?.attributes ?? { size_gb: 0 } const defaultValues = { storageType: type ?? DiskType.GP3, provisionedIOPS: iops, throughput: throughput_mbps, totalSize: size_gb, computeSize: project?.infra_compute_size ? mapComputeSizeNameToAddonVariantId(project?.infra_compute_size) : undefined, } const form = useForm({ resolver: zodResolver(CreateDiskStorageSchema(defaultValues.totalSize)), defaultValues, mode: 'onBlur', reValidateMode: 'onChange', }) /** * State handling */ const isSuccess = isAddonsSuccess && isDiskAttributesSuccess && isDiskUtilizationSuccess && isReadReplicasSuccess && isSubscriptionSuccess && isCooldownSuccess const isRequestingChanges = data?.requested_modification !== undefined const readReplicas = (databases ?? []).filter((db) => db.identifier !== projectRef) const isPlanUpgradeRequired = subscription?.plan.id === 'free' const { formState } = form const isFlyArchitecture = project?.cloud_provider === 'FLY' const disableDiskInputs = isRequestingChanges || isPlanUpgradeRequired || isWithinCooldownWindow || !canUpdateDiskConfiguration || isFlyArchitecture const disableComputeInputs = isPlanUpgradeRequired useEffect(() => { // Initialize field values properly when data has been loaded, preserving any user changes if (isSuccess) { form.reset(defaultValues, {}) } }, [isSuccess]) const isDirty = !!Object.keys(form.formState.dirtyFields).length const isProjectResizing = project?.status === PROJECT_STATUS.RESIZING const isProjectRequestingDiskChanges = isRequestingChanges && !isProjectResizing const noPermissions = isPermissionsLoaded && !canUpdateDiskConfiguration && !isProjectLoading const { mutateAsync: updateDiskConfiguration, isLoading: isUpdatingDisk } = useUpdateDiskAttributesMutation({ // this is to suppress to toast message onError: () => {}, }) const { mutateAsync: updateSubscriptionAddon, isLoading: 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. */ setProjectStatus(queryClient, projectRef!, PROJECT_STATUS.RESIZING) }, }) const isUpdatingConfig = isUpdatingDisk || isUpdatingCompute const onSubmit = async (data: DiskStorageSchemaType) => { let payload = data setMessageState(null) try { if ( payload.storageType !== form.formState.defaultValues?.storageType || payload.provisionedIOPS !== form.formState.defaultValues?.provisionedIOPS || payload.throughput !== form.formState.defaultValues?.throughput || payload.totalSize !== form.formState.defaultValues?.totalSize ) { if (RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3.includes(payload.computeSize)) { payload.provisionedIOPS = IOPS_RANGE[DiskType.GP3].min } await updateDiskConfiguration({ ref: projectRef, provisionedIOPS: payload.provisionedIOPS, storageType: payload.storageType, totalSize: payload.totalSize, throughput: payload.throughput, }) } 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) } catch (error: unknown) { setMessageState({ message: error instanceof Error ? error.message : 'An unknown error occurred', type: 'error', }) } } return (
Upgrade plan } description="You will need to upgrade to at least the Pro Plan to configure compute and disk" /> {isProjectResizing || isProjectRequestingDiskChanges || noPermissions ? (
) : null} {!isFlyArchitecture && ( <> setAdvancedSettingsOpenState((prev) => !prev)} >
Advanced disk settings Specify additional settings for your disk, including IOPS, throughput, and disk type.
{ form.setValue('computeSize', 'ci_large') }} > Change to LARGE Compute } />
)}
{isDirty ? (
) : null}
) }