diff --git a/apps/studio/components/interfaces/DiskManagement/BillingChangeBadge.tsx b/apps/studio/components/interfaces/DiskManagement/BillingChangeBadge.tsx index 9d9335d16a2..4a2431cf2a4 100644 --- a/apps/studio/components/interfaces/DiskManagement/BillingChangeBadge.tsx +++ b/apps/studio/components/interfaces/DiskManagement/BillingChangeBadge.tsx @@ -59,4 +59,4 @@ const BillingChangeBadge = ({ ) } -export default BillingChangeBadge +export { BillingChangeBadge } diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx index 00912bf5266..7642858ebf0 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx @@ -1,8 +1,6 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { components } from 'api-types' import { useParams } from 'common' -import DiskSpaceBar from 'components/interfaces/DiskManagement/DiskSpaceBar' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { MAX_WIDTH_CLASSES, PADDING_CLASSES, ScaffoldContainer } from 'components/layouts/Scaffold' import { @@ -13,19 +11,17 @@ import { useUpdateDiskAttributesMutation } from 'data/config/disk-attributes-upd import { useDiskUtilizationQuery } from 'data/config/disk-utilization-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 { AnimatePresence, motion } from 'framer-motion' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { useFlag } from 'hooks/ui/useFlag' -import { getCloudProviderArchitecture } from 'lib/cloudprovider-utils' -import { GB } from 'lib/constants' -import { ChevronRight, CpuIcon, Microchip } from 'lucide-react' -import { useEffect, useMemo, useState } from 'react' +import { ChevronRight } from 'lucide-react' +import { useEffect, useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { - Badge, Button, Card, CardContent, @@ -34,60 +30,23 @@ import { CollapsibleContent_Shadcn_, CollapsibleTrigger_Shadcn_, Form_Shadcn_, - FormControl_Shadcn_, - FormField_Shadcn_, - Input_Shadcn_ as Input, - RadioGroupCard, - RadioGroupCardItem, - Select_Shadcn_, - SelectContent_Shadcn_, - SelectItem_Shadcn_, - SelectTrigger_Shadcn_, - SelectValue_Shadcn_, Separator, } from 'ui' -import { ComputeBadge } from 'ui-patterns' -import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { FormFooterChangeBadge } from '../DataWarehouse/FormFooterChangeBadge' -import BillingChangeBadge from './BillingChangeBadge' -// import { DiskManagementFormLoading } from './BillingMangementForm.loading' -import { ComputeSizeReccomendationSection } from './ComputeSizeReccomendationSection' import { DiskCountdownRadial } from './DiskCountdownRadial' import { - COMPUTE_BASELINE_IOPS, - COMPUTE_BASELINE_THROUGHPUT, - COMPUTE_MAX_IOPS, - COMPUTE_MAX_THROUGHPUT, - DISK_TYPE_OPTIONS, DiskType, IOPS_RANGE, - PLAN_DETAILS, - RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3, RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3, - THROUGHPUT_RANGE, } from './DiskManagement.constants' -import { - calculateComputeSizePrice, - calculateComputeSizeRequiredForIops, - calculateDiskSizePrice, - calculateIOPSPrice, - calculateThroughputPrice, - getAvailableComputeOptions, -} from './DiskManagement.utils' import { CreateDiskStorageSchema, DiskStorageSchemaType } from './DiskManagementPanel.schema' import { DiskManagementPlanUpgradeRequired } from './DiskManagementPlanUpgradeRequired' -import { - DiskManagementDiskSizeReadReplicas, - DiskManagementIOPSReadReplicas, - DiskManagementThroughputReadReplicas, -} from './DiskManagementReadReplicas' import { DiskManagementReviewAndSubmitDialog } from './DiskManagementReviewAndSubmitDialog' -import { InputPostTab } from './InputPostTab' -import { InputResetButton } from './InputResetButton' -import { - updateSubscriptionAddon, - useProjectAddonUpdateMutation, -} from 'data/subscriptions/project-addon-update-mutation' +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' export function DiskManagementForm() { const showDiskAndComputeForm = useFlag('diskAndComputeForm') @@ -104,9 +63,7 @@ export function DiskManagementForm() { type: 'error' | 'success' } | null>(null) const [showRestartPending, setShowRestartPendingState] = useState(false) - const [advancedSettingsOpen, setAdvancedSettingsOpenState] = useState(false) - const canUpdateDiskConfiguration = useCheckPermissions(PermissionAction.UPDATE, 'projects', { resource: { project_id: project?.id, @@ -164,7 +121,6 @@ export function DiskManagementForm() { projectRef, }) const { - data: diskUtil, isLoading: isDiskUtilizationLoading, error: diskUtilError, isSuccess: isDiskUtilizationSuccess, @@ -183,17 +139,9 @@ export function DiskManagementForm() { /** * Handle compute instances */ - const availableAddons = useMemo(() => { - return addons?.available_addons ?? [] - }, [addons]) - const selectedAddons = addons?.selected_addons ?? [] const subscriptionCompute = selectedAddons.find((addon) => addon.type === 'compute_instance') - const availableOptions = useMemo(() => { - return getAvailableComputeOptions(availableAddons, project?.cloud_provider) - }, [availableAddons, project?.cloud_provider]) - /** * Handle default values */ @@ -236,21 +184,15 @@ export function DiskManagementForm() { const readReplicas = (databases ?? []).filter((db) => db.identifier !== projectRef) - const planId = subscription?.plan.id ?? 'free' + // const planId = subscription?.plan.id ?? 'free' const isPlanUpgradeRequired = subscription?.plan.id === 'pro' && !subscription.usage_billing_enabled - const mainDiskUsed = Math.round(((diskUtil?.metrics.fs_used_bytes ?? 0) / GB) * 100) / 100 - - const { watch, setValue, trigger, control, formState } = form - const { dirtyFields } = formState + const { watch, formState } = form const watchedComputeSize = watch('computeSize') - const watchedStorageType = watch('storageType') - const watchedTotalSize = watch('totalSize') - const watchedIOPS = watch('provisionedIOPS') - const isAllocatedStorageDirty = !!dirtyFields.totalSize + // const isAllocatedStorageDirty = !!dirtyFields.totalSize const disableInput = isRequestingChanges || @@ -258,66 +200,11 @@ export function DiskManagementForm() { isWithinCooldownWindow || !canUpdateDiskConfiguration - /** Disable IOPS if: - * 1. baseline IOPS of compute is lower than min required IOPS for storage type - * 2. compute is less than large - * - * */ - const disableIopsInput = - RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3.includes(watchedComputeSize) && watchedStorageType === 'gp3' - - const { includedDiskGB: includedDiskGBMeta } = - PLAN_DETAILS?.[planId as keyof typeof PLAN_DETAILS] ?? {} - const includedDiskGB = includedDiskGBMeta[watchedStorageType] - - /** - * Price calculations - */ - - const computeSizePrice = calculateComputeSizePrice({ - availableOptions: availableOptions, - oldComputeSize: form.formState.defaultValues?.computeSize || 'ci_micro', - newComputeSize: form.getValues('computeSize'), - }) - 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) - // @ts-nocheck }, [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 GB - const currentThroughput = form.getValues('throughput') - const { min, max } = THROUGHPUT_RANGE[DiskType.GP3] - if (!currentThroughput || currentThroughput < min || currentThroughput > max) { - setValue('throughput', min) // Reset to default if undefined or out of bounds - } - } - }, [watchedStorageType, watchedTotalSize, setValue, form]) - useEffect(() => { if (initialRemainingTime > 0) setRemainingTime(initialRemainingTime) }, [initialRemainingTime]) @@ -374,9 +261,6 @@ export function DiskManagementForm() { }) } - // toast.success( - // 'Successfully requested disk configuration changes! Your changes will be applied shortly' - // ) setIsDialogOpen(false) form.reset(data as DiskStorageSchemaType) } catch (error: unknown) { @@ -395,14 +279,14 @@ export function DiskManagementForm() { ) } - if (isLoading) { - // return - return
loading...
- } + // if (isLoading) { + // // return + // return
loading...
+ // } - if (error) { - return
Error: {error.message}
- } + // if (error) { + // return
Error: {error.message}
+ // } // if (planId === 'team') { // return ( @@ -445,8 +329,6 @@ export function DiskManagementForm() {
- {/* {showNewDiskManagementUI ? : null} */} - {showRestartPending && ( @@ -473,201 +355,13 @@ export function DiskManagementForm() { )} - - - ( - <> - - -

Hardware resources allocated to your postgres database

- - } - > - { - setValue('computeSize', value, { - shouldDirty: true, - shouldValidate: true, - }) - trigger('provisionedIOPS') - trigger('throughput') - }} - defaultValue={field.value} - value={field.value} - > - {availableOptions.map((compute: (typeof availableOptions)[number]) => { - const cpuArchitecture = getCloudProviderArchitecture( - project?.cloud_provider - ) - - return ( - -
- -
- - {/* Price needs to be exact here */}${compute.price} - - - {' '} - / {compute.price_interval === 'monthly' ? 'month' : 'hour'} - -
-
- {/* */} -
-
-
- - {compute.meta?.memory_gb ?? 0} GB memory -
-
- - - {compute.meta?.cpu_cores ?? 0}-core {cpuArchitecture} CPU - -
-
-
- - } - >
- ) - })} -
-
- - )} - /> - + - + - {isPlanUpgradeRequired && } - -
-
- ( - - - - {includedDiskGB > 0 && - `Your plan includes ${includedDiskGB} GB of disk size for ${watchedStorageType}.`} - -
- } - // labelOptional={} - // className=""/ - > -
- - - e.currentTarget.blur()} - onChange={(e) => { - setValue('totalSize', e.target.valueAsNumber, { - shouldDirty: true, - shouldValidate: true, - }) - trigger('provisionedIOPS') - trigger('throughput') - }} - min={includedDiskGB} - /> - - - { - form.resetField('totalSize') - trigger('provisionedIOPS') - }} - /> -
- - )} - /> -
-
- {/* You can add additional content in the remaining 4 columns if needed */} - - -
- - - - ( - - { - field.onChange(e) - // only trigger provisionedIOPS due to other input being hidden - await form.trigger('provisionedIOPS') - await form.trigger('totalSize') - - trigger('provisionedIOPS') - }} - defaultValue={field.value} - disabled={disableInput} - > - - - - - - - <> - {DISK_TYPE_OPTIONS.map((item) => ( - -
-
- {item.name}{' '} -
- - {item.type} - -
-
-

{item.description}

-
-
- ))} - -
-
-
- )} - /> - - { - const reccomendedComputeSize = calculateComputeSizeRequiredForIops(watchedIOPS) - return ( - - { - setValue( - 'computeSize', - `ci_${reccomendedComputeSize.toLocaleLowerCase()}` as components['schemas']['AddonVariantId'] - ) - trigger('provisionedIOPS') - }} - > - Update to {reccomendedComputeSize} - - } - /> - {!form.formState.errors.provisionedIOPS && ( - - )} - - } - labelOptional={ - <> - -

Input/output operations per second.

-

Higher IOPS for high-throughput apps.

- - } - > - - - { - setValue('provisionedIOPS', e.target.valueAsNumber, { - shouldDirty: true, - shouldValidate: true, - }) - }} - /> - - -
- ) - }} - /> - - - {form.getValues('storageType') === 'gp3' && ( - - ( - - ) - } - labelOptional={ - <> - -

Amount of data read/written to the disk per second.

-

- Higher throughput suits applications with high data transfer - needs. -

- - } - > - - - { - setValue('throughput', e.target.valueAsNumber, { - shouldDirty: true, - shouldValidate: true, - }) - }} - className="flex-grow font-mono rounded-r-none max-w-32" - disabled={ - disableInput || disableIopsInput || watchedStorageType === 'io2' - } - /> - - -
- )} - /> -
- )} -
+ + +
- {isDirty ? ( ) : null} - - {/* */} - {/* */}
diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx index 19814dde5a5..ce22ed7e7e8 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx @@ -28,7 +28,7 @@ import { TableRow, WarningIcon, } from 'ui' -import BillingChangeBadge from './BillingChangeBadge' +import { BillingChangeBadge } from './BillingChangeBadge' import { DiskType } from './DiskManagement.constants' import { calculateComputeSizePrice, diff --git a/apps/studio/components/interfaces/DiskManagement/fields/ComputeSizeField.tsx b/apps/studio/components/interfaces/DiskManagement/fields/ComputeSizeField.tsx new file mode 100644 index 00000000000..824b74aa9e7 --- /dev/null +++ b/apps/studio/components/interfaces/DiskManagement/fields/ComputeSizeField.tsx @@ -0,0 +1,156 @@ +import { components } from 'api-types' +import { useParams } from 'common' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' +import { getCloudProviderArchitecture } from 'lib/cloudprovider-utils' +import { CpuIcon, Microchip } from 'lucide-react' +import { useMemo } from 'react' +import { UseFormReturn } from 'react-hook-form' +import { FormField_Shadcn_, RadioGroupCard, RadioGroupCardItem } from 'ui' +import { ComputeBadge } from 'ui-patterns' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { BillingChangeBadge } from '../BillingChangeBadge' +import { calculateComputeSizePrice, getAvailableComputeOptions } from '../DiskManagement.utils' +import { DiskStorageSchemaType } from '../DiskManagementPanel.schema' + +type ComputeOption = { + identifier: components['schemas']['AddonVariantId'] + name: string + price: number + price_interval: 'monthly' | 'hourly' + meta?: { + memory_gb?: number + cpu_cores?: number + } +} + +type ComputeSizeFieldProps = { + form: UseFormReturn +} + +export function ComputeSizeField({ form }: ComputeSizeFieldProps) { + const project = useProjectContext() + const { ref } = useParams() + const { control, formState, setValue, trigger } = form + + const { + data: addons, + isLoading: isAddonsLoading, + error: addonsError, + isSuccess: isAddonsSuccess, + } = useProjectAddonsQuery({ projectRef: ref }) + + const availableAddons = useMemo(() => { + return addons?.available_addons ?? [] + }, [addons]) + + const availableOptions = useMemo(() => { + // @ts-expect-error + return getAvailableComputeOptions(availableAddons, project?.cloud_provider) + // @ts-expect-error + }, [availableAddons, project?.cloud_provider]) + + const computeSizePrice = calculateComputeSizePrice({ + availableOptions: availableOptions, + oldComputeSize: form.formState.defaultValues?.computeSize || 'ci_micro', + newComputeSize: form.getValues('computeSize'), + }) + + return ( + ( + + +

Hardware resources allocated to your postgres database

+ + } + > + { + setValue('computeSize', value, { + shouldDirty: true, + shouldValidate: true, + }) + trigger('provisionedIOPS') + trigger('throughput') + }} + defaultValue={field.value} + value={field.value} + > + {availableOptions.map((compute: ComputeOption) => { + // @ts-expect-error + const cpuArchitecture = getCloudProviderArchitecture(project?.cloud_provider) + + return ( + +
+ +
+ + ${compute.price} + + + {' '} + / {compute.price_interval === 'monthly' ? 'month' : 'hour'} + +
+
+
+
+
+ + {compute.meta?.memory_gb ?? 0} GB memory +
+
+ + + {compute.meta?.cpu_cores ?? 0}-core {cpuArchitecture} CPU + +
+
+
+ + } + /> + ) + })} +
+
+ )} + /> + ) +} diff --git a/apps/studio/components/interfaces/DiskManagement/fields/DiskSizeField.tsx b/apps/studio/components/interfaces/DiskManagement/fields/DiskSizeField.tsx new file mode 100644 index 00000000000..18d226f01aa --- /dev/null +++ b/apps/studio/components/interfaces/DiskManagement/fields/DiskSizeField.tsx @@ -0,0 +1,149 @@ +import { FormField_Shadcn_, FormControl_Shadcn_, Input_Shadcn_ } from 'ui' +import { ComputeBadge } from 'ui-patterns' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { BillingChangeBadge } from '../BillingChangeBadge' +import { InputPostTab } from '../InputPostTab' +import { InputResetButton } from '../InputResetButton' +import DiskSpaceBar from '../DiskSpaceBar' + +import { DiskType, PLAN_DETAILS } from '../DiskManagement.constants' +import { UseFormReturn } from 'react-hook-form' +import { DiskStorageSchemaType } from '../DiskManagementPanel.schema' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { calculateDiskSizePrice } from '../DiskManagement.utils' +import { DiskManagementDiskSizeReadReplicas } from '../DiskManagementReadReplicas' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' +import { useDiskUtilizationQuery } from 'data/config/disk-utilization-query' +import { useParams } from 'common' +import { GB } from 'lib/constants' + +type DiskSizeFieldProps = { + form: UseFormReturn + disableInput: boolean +} + +export function DiskSizeField({ form, disableInput }: DiskSizeFieldProps) { + const { ref: projectRef } = useParams() + const { control, formState, setValue, trigger, getValues, resetField, watch } = form + const { project } = useProjectContext() + const org = useSelectedOrganization() + + const { + data: subscription, + isLoading: isSubscriptionLoading, + error: subscriptionError, + } = useOrgSubscriptionQuery({ + orgSlug: org?.slug, + }) + const { + data: diskUtil, + isLoading: isDiskUtilizationLoading, + error: diskUtilError, + isSuccess: isDiskUtilizationSuccess, + } = useDiskUtilizationQuery({ + projectRef: projectRef, + }) + + const watchedStorageType = watch('storageType') + const watchedTotalSize = watch('totalSize') + + const planId = subscription?.plan.id ?? 'free' + + const { includedDiskGB: includedDiskGBMeta } = + PLAN_DETAILS?.[planId as keyof typeof PLAN_DETAILS] ?? {} + const includedDiskGB = includedDiskGBMeta[watchedStorageType] + + const diskSizePrice = calculateDiskSizePrice({ + planId, + oldSize: formState.defaultValues?.totalSize || 0, + oldStorageType: formState.defaultValues?.storageType as DiskType, + newSize: getValues('totalSize'), + newStorageType: getValues('storageType') as DiskType, + }) + + const isAllocatedStorageDirty = !!formState.dirtyFields.totalSize + const mainDiskUsed = Math.round(((diskUtil?.metrics.fs_used_bytes ?? 0) / GB) * 100) / 100 + + return ( +
+
+ ( + + + + {includedDiskGB > 0 && + `Your plan includes ${includedDiskGB} GB of disk size for ${watchedStorageType}.`} + +
+ } + > +
+ + + e.currentTarget.blur()} + onChange={(e) => { + setValue('totalSize', e.target.valueAsNumber, { + shouldDirty: true, + shouldValidate: true, + }) + trigger('provisionedIOPS') + trigger('throughput') + }} + min={includedDiskGB} + /> + + + { + resetField('totalSize') + trigger('provisionedIOPS') + }} + /> +
+ + )} + /> +
+
+ + +
+ + ) +} diff --git a/apps/studio/components/interfaces/DiskManagement/fields/IOPSField.tsx b/apps/studio/components/interfaces/DiskManagement/fields/IOPSField.tsx new file mode 100644 index 00000000000..586bab7b90f --- /dev/null +++ b/apps/studio/components/interfaces/DiskManagement/fields/IOPSField.tsx @@ -0,0 +1,128 @@ +import { components } from 'api-types' +import { UseFormReturn } from 'react-hook-form' +import { Button, FormControl_Shadcn_, FormField_Shadcn_, Input_Shadcn_ } from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { BillingChangeBadge } from '../BillingChangeBadge' +import { ComputeSizeReccomendationSection } from '../ComputeSizeReccomendationSection' +import { + COMPUTE_BASELINE_IOPS, + DiskType, + RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3, +} from '../DiskManagement.constants' +import { calculateComputeSizeRequiredForIops, calculateIOPSPrice } from '../DiskManagement.utils' +import { DiskStorageSchemaType } from '../DiskManagementPanel.schema' +import { DiskManagementIOPSReadReplicas } from '../DiskManagementReadReplicas' +import { InputPostTab } from '../InputPostTab' + +type IOPSFieldProps = { + form: UseFormReturn + disableInput: boolean +} + +export function IOPSField({ form, disableInput }: IOPSFieldProps) { + const { control, formState, setValue, trigger, getValues, watch } = form + + const watchedStorageType = watch('storageType') + const watchedComputeSize = watch('computeSize') + const watchedIOPS = watch('provisionedIOPS') + + const iopsPrice = calculateIOPSPrice({ + oldStorageType: formState.defaultValues?.storageType as DiskType, + oldProvisionedIOPS: formState.defaultValues?.provisionedIOPS || 0, + newStorageType: getValues('storageType') as DiskType, + newProvisionedIOPS: getValues('provisionedIOPS'), + }) + + const disableIopsInput = + RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3.includes(watchedComputeSize) && watchedStorageType === 'gp3' + + return ( + { + const reccomendedComputeSize = calculateComputeSizeRequiredForIops(watchedIOPS) + return ( + + { + setValue( + 'computeSize', + `ci_${reccomendedComputeSize.toLocaleLowerCase()}` as components['schemas']['AddonVariantId'] + ) + trigger('provisionedIOPS') + }} + > + Update to {reccomendedComputeSize} + + } + /> + {!formState.errors.provisionedIOPS && ( + + )} + + } + labelOptional={ + <> + +

Input/output operations per second.

+

Higher IOPS for high-throughput apps.

+ + } + > + + + { + setValue('provisionedIOPS', e.target.valueAsNumber, { + shouldDirty: true, + shouldValidate: true, + }) + }} + /> + + +
+ ) + }} + /> + ) +} diff --git a/apps/studio/components/interfaces/DiskManagement/fields/StorageTypeField.tsx b/apps/studio/components/interfaces/DiskManagement/fields/StorageTypeField.tsx new file mode 100644 index 00000000000..2d35d8872f7 --- /dev/null +++ b/apps/studio/components/interfaces/DiskManagement/fields/StorageTypeField.tsx @@ -0,0 +1,73 @@ +import { FormField_Shadcn_, FormControl_Shadcn_ } from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { + Select_Shadcn_, + SelectTrigger_Shadcn_, + SelectValue_Shadcn_, + SelectContent_Shadcn_, + SelectItem_Shadcn_, +} from 'ui' +import { Badge } from 'ui' +import { UseFormReturn } from 'react-hook-form' +import { DiskStorageSchemaType } from '../DiskManagementPanel.schema' +import { DISK_TYPE_OPTIONS } from '../DiskManagement.constants' + +type StorageTypeFieldProps = { + form: UseFormReturn + disableInput: boolean +} + +export function StorageTypeField({ form, disableInput }: StorageTypeFieldProps) { + const { control, trigger } = form + + return ( + ( + + { + field.onChange(e) + // only trigger provisionedIOPS due to other input being hidden + await trigger('provisionedIOPS') + await trigger('totalSize') + trigger('provisionedIOPS') + }} + defaultValue={field.value} + disabled={disableInput} + > + + + + + + + <> + {DISK_TYPE_OPTIONS.map((item) => ( + +
+
+ {item.name}{' '} +
+ + {item.type} + +
+
+

{item.description}

+
+
+ ))} + +
+
+
+ )} + /> + ) +} diff --git a/apps/studio/components/interfaces/DiskManagement/fields/ThroughputField.tsx b/apps/studio/components/interfaces/DiskManagement/fields/ThroughputField.tsx new file mode 100644 index 00000000000..e83b58070d4 --- /dev/null +++ b/apps/studio/components/interfaces/DiskManagement/fields/ThroughputField.tsx @@ -0,0 +1,130 @@ +import { AnimatePresence, motion } from 'framer-motion' +import { UseFormReturn } from 'react-hook-form' +import { FormControl_Shadcn_, FormField_Shadcn_, Input_Shadcn_ } from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { BillingChangeBadge } from '../BillingChangeBadge' +import { + COMPUTE_BASELINE_THROUGHPUT, + DiskType, + RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3, + THROUGHPUT_RANGE, +} from '../DiskManagement.constants' +import { calculateThroughputPrice } from '../DiskManagement.utils' +import { DiskStorageSchemaType } from '../DiskManagementPanel.schema' +import { DiskManagementThroughputReadReplicas } from '../DiskManagementReadReplicas' +import { InputPostTab } from '../InputPostTab' +import { useEffect } from 'react' + +type ThroughputFieldProps = { + form: UseFormReturn + disableInput: boolean +} + +export function ThroughputField({ form, disableInput }: ThroughputFieldProps) { + const { control, formState, setValue, getValues, watch } = form + + const watchedStorageType = watch('storageType') + const watchedTotalSize = watch('totalSize') + const watchedComputeSize = watch('computeSize') + const throughput_mbps = formState.defaultValues?.throughput + + const throughputPrice = calculateThroughputPrice({ + storageType: form.getValues('storageType') as DiskType, + newThroughput: form.getValues('throughput') || 0, + oldThroughput: form.formState.defaultValues?.throughput || 0, + }) + + const disableIopsInput = + RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3.includes(watchedComputeSize) && watchedStorageType === 'gp3' + + // 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 GB + const currentThroughput = form.getValues('throughput') + const { min, max } = THROUGHPUT_RANGE[DiskType.GP3] + if (!currentThroughput || currentThroughput < min || currentThroughput > max) { + setValue('throughput', min) // Reset to default if undefined or out of bounds + } + } + }, [watchedStorageType, watchedTotalSize, setValue, form]) + + return ( + + {getValues('storageType') === 'gp3' && ( + + ( + + ) + } + labelOptional={ + <> + +

Amount of data read/written to the disk per second.

+

Higher throughput suits applications with high data transfer needs.

+ + } + > + + + { + setValue('throughput', e.target.valueAsNumber, { + shouldDirty: true, + shouldValidate: true, + }) + }} + className="flex-grow font-mono rounded-r-none max-w-32" + disabled={disableInput || disableIopsInput || watchedStorageType === 'io2'} + /> + + +
+ )} + /> +
+ )} +
+ ) +}