import { PermissionAction } from '@supabase/shared-types/out/constants' import { ChevronRight } from 'lucide-react' import { useMemo } from 'react' import { UseFormReturn } from 'react-hook-form' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useIsAwsNimbusCloudProvider, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { formatCurrency } from 'lib/helpers' import { Alert_Shadcn_, AlertTitle_Shadcn_, Badge, Button, ButtonProps, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogSection, DialogSectionSeparator, DialogTitle, DialogTrigger, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, WarningIcon, } from 'ui' import { DiskStorageSchemaType } from './DiskManagement.schema' import { DiskManagementMessage } from './DiskManagement.types' import { calculateComputeSizePrice, calculateDiskSizePrice, calculateIOPSPrice, calculateThroughputPrice, getAvailableComputeOptions, mapAddOnVariantIdToComputeSize, } from './DiskManagement.utils' import { DiskMangementRestartRequiredSection } from './DiskManagementRestartRequiredSection' import { BillingChangeBadge } from './ui/BillingChangeBadge' import { DISK_AUTOSCALE_CONFIG_DEFAULTS, DiskType } from './ui/DiskManagement.constants' import { DiskMangementCoolDownSection } from './ui/DiskManagementCoolDownSection' const TableHeaderRow = () => ( Disk attribute - Unit Price Change ) interface TableDataRowProps { attribute: string defaultValue: string | number newValue: string | number unit: string beforePrice: number afterPrice: number hidePrice?: boolean priceTooltip?: string upgradeIncluded?: boolean } const TableDataRow = ({ attribute, defaultValue, newValue, unit, beforePrice, afterPrice, hidePrice = false, priceTooltip, upgradeIncluded = false, }: TableDataRowProps) => ( {attribute} {defaultValue !== newValue ? ( {defaultValue.toString()} {newValue.toString()} ) : ( {defaultValue} )} {unit} {!upgradeIncluded && hidePrice ? ( - ) : beforePrice !== afterPrice || upgradeIncluded ? ( ) : ( {formatCurrency(beforePrice)} )} ) interface DiskSizeMeterProps { loading: boolean form: UseFormReturn numReplicas: number isDialogOpen: boolean disabled?: boolean setIsDialogOpen: (isOpen: boolean) => void onSubmit: (values: DiskStorageSchemaType) => Promise buttonSize?: ButtonProps['size'] message?: DiskManagementMessage | null } export const DiskManagementReviewAndSubmitDialog = ({ isDialogOpen, setIsDialogOpen, disabled, form, numReplicas, loading, onSubmit, message, buttonSize = 'medium', }: DiskSizeMeterProps) => { const { data: project } = useSelectedProjectQuery() const { data: org } = useSelectedOrganizationQuery() const isAwsNimbus = useIsAwsNimbusCloudProvider() const isAwsK8sProject = project?.cloud_provider === 'AWS_K8S' const { formState, getValues } = form const { can: canUpdateDiskConfiguration } = useAsyncCheckPermissions( PermissionAction.UPDATE, 'projects', { resource: { project_id: project?.id, }, } ) /** * Queries * */ const { data: addons, // request deps in Form handle errors and loading } = useProjectAddonsQuery({ projectRef: project?.ref }) const planId = org?.plan.id ?? 'free' const isDirty = !!Object.keys(form.formState.dirtyFields).length const replicaTooltipText = `Price change includes primary database and ${numReplicas} replica${numReplicas > 1 ? 's' : ''}` const availableAddons = useMemo(() => { return addons?.available_addons ?? [] }, [addons]) const availableOptions = useMemo(() => { return getAvailableComputeOptions(availableAddons, project?.cloud_provider) }, [availableAddons, project?.cloud_provider]) const computeSizePrice = calculateComputeSizePrice({ availableOptions, oldComputeSize: form.formState.defaultValues?.computeSize || 'ci_micro', newComputeSize: form.getValues('computeSize'), plan: org?.plan.id ?? 'free', }) const diskSizePrice = calculateDiskSizePrice({ planId: planId, oldSize: formState.defaultValues?.totalSize || 0, oldStorageType: formState.defaultValues?.storageType as DiskType, newSize: getValues('totalSize'), newStorageType: getValues('storageType') as DiskType, numReplicas, }) 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'), numReplicas, }) const throughputPrice = calculateThroughputPrice({ storageType: form.getValues('storageType') as DiskType, newThroughput: form.getValues('throughput') || 0, oldThroughput: form.formState.defaultValues?.throughput || 0, numReplicas, }) const hasComputeChanges = form.formState.defaultValues?.computeSize !== form.getValues('computeSize') const hasTotalSizeChanges = !isAwsK8sProject && !isAwsNimbus && form.formState.defaultValues?.totalSize !== form.getValues('totalSize') const hasStorageTypeChanges = !isAwsK8sProject && !isAwsNimbus && form.formState.defaultValues?.storageType !== form.getValues('storageType') const hasThroughputChanges = !isAwsK8sProject && !isAwsNimbus && form.formState.defaultValues?.throughput !== form.getValues('throughput') const hasIOPSChanges = !isAwsK8sProject && !isAwsNimbus && form.formState.defaultValues?.provisionedIOPS !== form.getValues('provisionedIOPS') const hasGrowthPercentChanges = !isAwsK8sProject && !isAwsNimbus && form.formState.defaultValues?.growthPercent !== form.getValues('growthPercent') const hasMinIncrementChanges = !isAwsK8sProject && !isAwsNimbus && form.formState.defaultValues?.minIncrementGb !== form.getValues('minIncrementGb') const hasMaxSizeChanges = !isAwsK8sProject && !isAwsNimbus && form.formState.defaultValues?.maxSizeGb !== form.getValues('maxSizeGb') const hasDiskConfigChanges = hasIOPSChanges || (hasThroughputChanges && form.getValues('storageType') === 'gp3') || hasTotalSizeChanges return ( { 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 Review changes Changes will be applied shortly once confirmed. {(hasComputeChanges || hasDiskConfigChanges) && ( <> > )} {hasComputeChanges && ( )} {hasStorageTypeChanges && ( )} {(hasIOPSChanges || hasStorageTypeChanges) && ( 0 ? replicaTooltipText : undefined} /> )} {form.getValues('storageType') === 'gp3' && hasThroughputChanges && ( 0 ? replicaTooltipText : undefined} /> )} {(hasTotalSizeChanges || hasStorageTypeChanges) && ( 0 ? replicaTooltipText : undefined} /> )} {hasGrowthPercentChanges && ( )} {hasMinIncrementChanges && ( )} {hasMaxSizeChanges && ( )} {numReplicas > 0 && ( {replicaTooltipText} )} setIsDialogOpen(false)}> Cancel { await onSubmit(form.getValues()) }} > Confirm changes {message && ( <> {message.message} > )} ) }