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() {
>
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'}
+ />
+
+
+
+ )}
+ />
+
+ )}
+
+ )
+}