diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagement.test.ts b/apps/studio/components/interfaces/DiskManagement/DiskManagement.test.ts
index c7e44d07d1ee4..74a67404a0620 100644
--- a/apps/studio/components/interfaces/DiskManagement/DiskManagement.test.ts
+++ b/apps/studio/components/interfaces/DiskManagement/DiskManagement.test.ts
@@ -1,5 +1,6 @@
import { describe, expect, test } from 'vitest'
+import { CreateDiskStorageSchema } from './DiskManagement.schema'
import {
calculateBaselineIopsForComputeSize,
calculateComputeSizeRequiredForIops,
@@ -147,6 +148,18 @@ describe('DiskManagement.utils.ts:calculateIOPSPrice', () => {
expect(result.oldPrice).toBe('357.00')
expect(result.newPrice).toBe('595.00')
})
+ test('includes IOPS charges for read replicas', () => {
+ const result = calculateIOPSPrice({
+ oldStorageType: DiskType.GP3,
+ oldProvisionedIOPS: 3000,
+ newStorageType: DiskType.GP3,
+ newProvisionedIOPS: 5000,
+ numReplicas: 2,
+ })
+
+ expect(result.oldPrice).toBe('0.00')
+ expect(result.newPrice).toBe('144.00')
+ })
})
describe('DiskManagement.utils.ts:calculateThroughputPrice', () => {
@@ -168,4 +181,109 @@ describe('DiskManagement.utils.ts:calculateThroughputPrice', () => {
expect(result.oldPrice).toBe('0.00')
expect(result.newPrice).toBe('0.00')
})
+ test('includes throughput charges for read replicas', () => {
+ const result = calculateThroughputPrice({
+ storageType: DiskType.GP3,
+ oldThroughput: 125,
+ newThroughput: 150,
+ numReplicas: 2,
+ })
+
+ expect(result.oldPrice).toBe('0.00')
+ expect(result.newPrice).toBe('7.13')
+ })
+})
+
+describe('CreateDiskStorageSchema', () => {
+ const validGp3Config = {
+ storageType: DiskType.GP3,
+ totalSize: 8,
+ provisionedIOPS: 3000,
+ throughput: 125,
+ computeSize: 'ci_large' as const,
+ growthPercent: null,
+ minIncrementGb: null,
+ maxSizeGb: null,
+ }
+
+ test('enforces the GP3 500 IOPS per GB limit', () => {
+ const schema = CreateDiskStorageSchema({
+ defaultTotalSize: 8,
+ cloudProvider: 'AWS',
+ isSpendCapEnabled: false,
+ })
+
+ const result = schema.safeParse({
+ ...validGp3Config,
+ provisionedIOPS: 6000,
+ })
+
+ expect(result.success).toBe(false)
+ if (result.success) return
+
+ expect(result.error.issues).toContainEqual(
+ expect.objectContaining({
+ path: ['provisionedIOPS'],
+ message: 'Larger Disk size of at least 12 GB required. Current max is 4,000 IOPS.',
+ })
+ )
+ })
+
+ test('allows a legacy disk below 8 GB when its size is unchanged', () => {
+ const schema = CreateDiskStorageSchema({
+ defaultTotalSize: 2,
+ cloudProvider: 'AWS',
+ isSpendCapEnabled: false,
+ })
+
+ expect(
+ schema.safeParse({
+ ...validGp3Config,
+ totalSize: 2,
+ }).success
+ ).toBe(true)
+ })
+
+ test('prevents disk growth above 8 GB while spend cap is enabled', () => {
+ const schema = CreateDiskStorageSchema({
+ defaultTotalSize: 8,
+ cloudProvider: 'AWS',
+ isSpendCapEnabled: true,
+ })
+
+ const result = schema.safeParse({
+ ...validGp3Config,
+ totalSize: 10,
+ })
+
+ expect(result.success).toBe(false)
+ if (result.success) return
+
+ expect(result.error.issues).toContainEqual(
+ expect.objectContaining({
+ path: ['totalSize'],
+ message: 'Disable spend cap to increase disk above 8 GB.',
+ })
+ )
+ })
+
+ test.each(['FLY', 'AWS_NIMBUS', 'AWS_K8S'] as const)(
+ 'skips platform disk constraints for %s projects',
+ (cloudProvider) => {
+ const schema = CreateDiskStorageSchema({
+ defaultTotalSize: 8,
+ cloudProvider,
+ isSpendCapEnabled: true,
+ })
+
+ expect(
+ schema.safeParse({
+ ...validGp3Config,
+ totalSize: 1,
+ provisionedIOPS: 100_000,
+ throughput: 10_000,
+ }).success
+ ).toBe(true)
+ }
+ )
})
diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.sections.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.sections.tsx
new file mode 100644
index 0000000000000..8acdbfccf6523
--- /dev/null
+++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.sections.tsx
@@ -0,0 +1,327 @@
+import { type RefObject } from 'react'
+import { type UseFormReturn } from 'react-hook-form'
+import { Button, Card, CardContent } from 'ui'
+import { Admonition } from 'ui-patterns/Admonition'
+import {
+ PageSection,
+ PageSectionAside,
+ PageSectionContent,
+ PageSectionDescription,
+ PageSectionMeta,
+ PageSectionSummary,
+ PageSectionTitle,
+} from 'ui-patterns/PageSection'
+
+import { DiskStorageSchemaType } from './DiskManagement.schema'
+import { AutoScaleFields } from './fields/AutoScaleFields'
+import {
+ ComputeSectionBillingBadge,
+ ComputeSizeField,
+ ComputeSizeFieldMeta,
+} from './fields/ComputeSizeField'
+import { DiskSizeField } from './fields/DiskSizeField'
+import { IOPSField } from './fields/IOPSField'
+import { StorageTypeField } from './fields/StorageTypeField'
+import { ThroughputField } from './fields/ThroughputField'
+import { BillingChangeBadge } from './ui/BillingChangeBadge'
+import { DiskCountdownRadial } from './ui/DiskCountdownRadial'
+import { DiskSpaceBar } from './ui/DiskSpaceBar'
+import { NoticeBar } from './ui/NoticeBar'
+import { SpendCapDisabledSection } from './ui/SpendCapDisabledSection'
+import { DocsButton } from '@/components/ui/DocsButton'
+import { RequestUpgradeToBillingOwners } from '@/components/ui/RequestUpgradeToBillingOwners'
+import { DOCS_URL } from '@/lib/constants'
+
+interface ComputeSectionProps {
+ form: UseFormReturn
+ settingsRef: RefObject
+ showBillingBadge: boolean
+ beforePrice: number
+ afterPrice: number
+ disabled: boolean
+}
+
+export function ComputeSection({
+ form,
+ settingsRef,
+ showBillingBadge,
+ beforePrice,
+ afterPrice,
+ disabled,
+}: ComputeSectionProps) {
+ return (
+
+
+
+ Compute size
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+interface DiskSectionProps {
+ form: UseFormReturn
+ settingsRef: RefObject
+ showBillingBadge: boolean
+ beforePrice: number
+ afterPrice: number
+ isAws: boolean
+ isAwsK8s: boolean
+ isBranch: boolean
+ isNoticeVisible: boolean
+ isReadOnlyMode: boolean
+ usedPercentage: number
+ isWithinCooldownWindow: boolean
+ currentDiskSizeGb?: number
+ disableDiskSizeInput: boolean
+}
+
+function getDiskNoticeDescription({
+ isAwsK8s,
+ isBranch,
+}: Pick) {
+ if (isAwsK8s) {
+ return 'Configuring your disk for AWS (Revamped) projects is unavailable for now.'
+ }
+ if (isBranch) {
+ return 'Delete and recreate your Preview Branch to configure disk size. It was deployed on an older branching infrastructure.'
+ }
+ return 'The Fly Postgres offering is deprecated - please migrate your instance to the AWS cloud provider to configure your disk.'
+}
+
+export function DiskSection({
+ form,
+ settingsRef,
+ showBillingBadge,
+ beforePrice,
+ afterPrice,
+ isAws,
+ isAwsK8s,
+ isBranch,
+ isNoticeVisible,
+ isReadOnlyMode,
+ usedPercentage,
+ isWithinCooldownWindow,
+ currentDiskSizeGb,
+ disableDiskSizeInput,
+}: DiskSectionProps) {
+ const noticeDescription = getDiskNoticeDescription({ isAwsK8s, isBranch })
+
+ return (
+
+
+
+ Disk
+
+ Configure provisioned storage for your primary database.
+
+
+
+
+
+
+
+
+ {isAws && }
+
+
+
+
+
+ {isAws && (
+ <>
+
+
+ {!isReadOnlyMode && usedPercentage >= 90 && isWithinCooldownWindow && (
+
+
+
+ )}
+ {isReadOnlyMode && (
+
+
+
+ )}
+
+
+
+
+
+
+
+ >
+ )}
+
+
+ )
+}
+
+interface AdvancedSectionProps {
+ form: UseFormReturn
+ autoscaleSettingsRef: RefObject
+ storageSettingsRef: RefObject
+ showBillingBadge: boolean
+ beforePrice: number
+ afterPrice: number
+ disableIopsThroughputConfig?: boolean
+ canUpdateDiskConfiguration: boolean
+ isDiskTooSmallForCustomIops: boolean
+ disableDiskInputs: boolean
+ disableDiskSizeInput: boolean
+ suggestedDiskSizeForCustomIops: number
+}
+
+export function AdvancedSection({
+ form,
+ autoscaleSettingsRef,
+ storageSettingsRef,
+ showBillingBadge,
+ beforePrice,
+ afterPrice,
+ disableIopsThroughputConfig,
+ canUpdateDiskConfiguration,
+ isDiskTooSmallForCustomIops,
+ disableDiskInputs,
+ disableDiskSizeInput,
+ suggestedDiskSizeForCustomIops,
+}: AdvancedSectionProps) {
+ return (
+
+
+
+ Advanced
+
+ Configure autoscaling, storage type, IOPS, and throughput.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ form.setValue('computeSize', 'ci_large', {
+ shouldDirty: true,
+ shouldValidate: true,
+ })
+ form.trigger('provisionedIOPS')
+ form.trigger('throughput')
+ }}
+ >
+ Change to LARGE Compute
+
+ ) : (
+
+ )
+ }
+ />
+ {
+ form.setValue('totalSize', suggestedDiskSizeForCustomIops, {
+ shouldDirty: true,
+ shouldValidate: true,
+ })
+ }}
+ >
+ Increase to {suggestedDiskSizeForCustomIops} GB
+
+ ) : undefined
+ }
+ />
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx
index c78aab5d4e119..f3e54c03a108f 100644
--- a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx
+++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx
@@ -2,53 +2,39 @@ 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, Form } from 'ui'
+import { PageContainer } from 'ui-patterns/PageContainer'
import {
- Button,
- cn,
- Collapsible,
- CollapsibleContent,
- CollapsibleTrigger,
- DialogSectionSeparator,
- Form,
- Separator,
-} from 'ui'
-import { Admonition } from 'ui-patterns/Admonition'
-
-import { FormFooterChangeBadge } from '../DataWarehouse/FormFooterChangeBadge'
+ PageSection,
+ PageSectionContent,
+ PageSectionMeta,
+ PageSectionSummary,
+ PageSectionTitle,
+} from 'ui-patterns/PageSection'
+
import { CreateDiskStorageSchema, DiskStorageSchemaType } from './DiskManagement.schema'
import { DiskManagementMessage } from './DiskManagement.types'
import {
calculateDiskSizeRequiredForIopsWithGp3,
mapComputeSizeNameToAddonVariantId,
} from './DiskManagement.utils'
+import { AdvancedSection, ComputeSection, DiskSection } from './DiskManagementForm.sections'
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 { useDiskManagementReviewChanges } from './DiskManagementReviewAndSubmitDialog/DiskManagementReviewAndSubmitDialog.hooks'
+import { BillingChangeBadge } from './ui/BillingChangeBadge'
import {
DISK_LIMITS,
DiskType,
PLAN_DETAILS,
RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3,
} from './ui/DiskManagement.constants'
-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 { NoticeBar } from './ui/NoticeBar'
+import { PADDING_CLASSES } from '@/components/layouts/Scaffold'
import { UpgradeToPro } from '@/components/ui/UpgradeToPro'
import {
useDiskAttributesQuery,
@@ -73,7 +59,7 @@ import {
useIsAwsNimbusCloudProvider,
useSelectedProjectQuery,
} from '@/hooks/misc/useSelectedProject'
-import { DOCS_URL, GB, PROJECT_STATUS } from '@/lib/constants'
+import { GB, PROJECT_STATUS } from '@/lib/constants'
export function DiskManagementForm() {
const { ref: projectRef } = useParams()
@@ -81,7 +67,10 @@ export function DiskManagementForm() {
const { data: org } = useSelectedOrganizationQuery()
const { setProjectStatus } = useSetProjectStatus()
- const advancedSettingsRef = useRef(null)
+ const autoscaleSettingsRef = useRef(null)
+ const storageSettingsRef = useRef(null)
+ const computeSettingsRef = useRef(null)
+ const diskSizeSettingsRef = useRef(null)
const isSpendCapEnabled =
org?.plan.id !== 'free' && !org?.usage_billing_enabled && project?.cloud_provider !== 'FLY'
@@ -110,7 +99,6 @@ export function DiskManagementForm() {
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(
@@ -183,6 +171,18 @@ export function DiskManagementForm() {
const readReplicas = (databases ?? []).filter((db) => db.identifier !== projectRef)
const isPlanUpgradeRequired = !hasAccess
+ const {
+ computeSizePrice,
+ diskSizePrice,
+ totalBeforePrice,
+ totalAfterPrice,
+ advancedBeforePrice,
+ advancedAfterPrice,
+ showComputeBillingBadge,
+ showDiskBillingBadge,
+ showAdvancedBillingBadge,
+ } = useDiskManagementReviewChanges(form, readReplicas.length)
+
const { formState } = form
const errors = formState.errors
const usedSize = Math.round(((diskUtil?.metrics.fs_used_bytes ?? 0) / GB) * 100) / 100
@@ -346,7 +346,7 @@ export function DiskManagementForm() {
form.setValue('provisionedIOPS', DISK_LIMITS['gp3'].minIops)
}
}
- }, [modifiedComputeSize, isDialogOpen, project])
+ }, [modifiedComputeSize, form, isDialogOpen, project])
useEffect(() => {
// Initialize field values properly when data has been loaded, preserving any user changes
@@ -358,287 +358,166 @@ export function DiskManagementForm() {
useEffect(() => {
const fieldErrors = Object.keys(errors)
- if (fieldErrors.length > 0) {
- if (
- fieldErrors.includes('throughput') ||
- fieldErrors.includes('provisionedIOPS') ||
- fieldErrors.includes('maxSizeGb')
- ) {
- setAdvancedSettingsOpenState(true)
+ if (fieldErrors.length === 0) return
+
+ const scrollTargets = [
+ {
+ hasError:
+ fieldErrors.includes('maxSizeGb') ||
+ fieldErrors.includes('growthPercent') ||
+ fieldErrors.includes('minIncrementGb'),
+ ref: autoscaleSettingsRef,
+ },
+ {
+ hasError: fieldErrors.includes('throughput') || fieldErrors.includes('provisionedIOPS'),
+ ref: storageSettingsRef,
+ },
+ { hasError: fieldErrors.includes('totalSize'), ref: diskSizeSettingsRef },
+ { hasError: fieldErrors.includes('computeSize'), ref: computeSettingsRef },
+ ]
+ const scrollTarget = scrollTargets.find(({ hasError }) => hasError)?.ref ?? null
- // [Joshen] The timeout is to let the collapsible open prior to scrolling
- const timeoutId = setTimeout(() => {
- advancedSettingsRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
- }, 100)
+ if (!scrollTarget) return
- return () => clearTimeout(timeoutId)
- }
- }
+ const timeoutId = setTimeout(() => {
+ scrollTarget.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
+ }, 100)
+
+ return () => clearTimeout(timeoutId)
}, [errors])
return (
- <>
-
- {isEntitlementsLoaded && isPlanUpgradeRequired && (
-
- )}
-
- {(isProjectResizing ||
- isProjectRequestingDiskChanges ||
- (isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions)) && (
-
-
- {isProjectRequestingDiskChanges && (
-
+
-
- >
+ Cancel
+
+
+
+
+ ) : null}
+
+
+
)
}
diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog/DiskManagementReviewAndSubmitDialog.hooks.test.ts b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog/DiskManagementReviewAndSubmitDialog.hooks.test.ts
new file mode 100644
index 0000000000000..9c9edcfbf1fa9
--- /dev/null
+++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog/DiskManagementReviewAndSubmitDialog.hooks.test.ts
@@ -0,0 +1,49 @@
+import { describe, expect, test } from 'vitest'
+
+import { shouldShowComputeBillingBadge } from './DiskManagementReviewAndSubmitDialog.hooks'
+
+describe('shouldShowComputeBillingBadge', () => {
+ test('shows for a dirty, valid compute price change', () => {
+ expect(
+ shouldShowComputeBillingBadge({
+ isDirty: true,
+ hasComputeSizeError: false,
+ oldPrice: '10.00',
+ newPrice: '20.00',
+ })
+ ).toBe(true)
+ })
+
+ test('hides when the selected compute size has the same price', () => {
+ expect(
+ shouldShowComputeBillingBadge({
+ isDirty: true,
+ hasComputeSizeError: false,
+ oldPrice: '10.00',
+ newPrice: '10',
+ })
+ ).toBe(false)
+ })
+
+ test('hides when the form is not dirty', () => {
+ expect(
+ shouldShowComputeBillingBadge({
+ isDirty: false,
+ hasComputeSizeError: false,
+ oldPrice: '10.00',
+ newPrice: '20.00',
+ })
+ ).toBe(false)
+ })
+
+ test('hides when compute size validation fails', () => {
+ expect(
+ shouldShowComputeBillingBadge({
+ isDirty: true,
+ hasComputeSizeError: true,
+ oldPrice: '10.00',
+ newPrice: '20.00',
+ })
+ ).toBe(false)
+ })
+})
diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog/DiskManagementReviewAndSubmitDialog.hooks.ts b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog/DiskManagementReviewAndSubmitDialog.hooks.ts
index a61cd8442096a..56db829918d98 100644
--- a/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog/DiskManagementReviewAndSubmitDialog.hooks.ts
+++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog/DiskManagementReviewAndSubmitDialog.hooks.ts
@@ -1,5 +1,5 @@
import { useMemo } from 'react'
-import { UseFormReturn } from 'react-hook-form'
+import { useFormState, useWatch, type UseFormReturn } from 'react-hook-form'
import { DiskStorageSchemaType } from '../DiskManagement.schema'
import { ComputeInstanceAddonVariantId } from '../DiskManagement.types'
@@ -26,10 +26,48 @@ const COMPUTE_SIZES_BELOW_LARGE: Array = [
'ci_medium',
]
+export function shouldShowComputeBillingBadge({
+ isDirty,
+ hasComputeSizeError,
+ oldPrice,
+ newPrice,
+}: {
+ isDirty: boolean
+ hasComputeSizeError: boolean
+ oldPrice: string | number
+ newPrice: string | number
+}) {
+ return isDirty && !hasComputeSizeError && Number(oldPrice) !== Number(newPrice)
+}
+
export function useDiskManagementReviewChanges(
form: UseFormReturn,
numReplicas: number
) {
+ const [
+ computeSize,
+ totalSize,
+ storageType,
+ provisionedIOPS,
+ throughput,
+ growthPercent,
+ minIncrementGb,
+ maxSizeGb,
+ ] = useWatch({
+ control: form.control,
+ name: [
+ 'computeSize',
+ 'totalSize',
+ 'storageType',
+ 'provisionedIOPS',
+ 'throughput',
+ 'growthPercent',
+ 'minIncrementGb',
+ 'maxSizeGb',
+ ],
+ })
+ const { isDirty, errors, defaultValues } = useFormState({ control: form.control })
+
const { data: project } = useSelectedProjectQuery()
const { data: org } = useSelectedOrganizationQuery()
const isAwsNimbus = useIsAwsNimbusCloudProvider()
@@ -48,29 +86,29 @@ export function useDiskManagementReviewChanges(
const computeSizePrice = calculateComputeSizePrice({
availableOptions,
- oldComputeSize: form.formState.defaultValues?.computeSize || 'ci_micro',
- newComputeSize: form.getValues('computeSize'),
+ oldComputeSize: defaultValues?.computeSize || 'ci_micro',
+ newComputeSize: computeSize,
plan: planId,
})
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,
+ oldSize: defaultValues?.totalSize || 0,
+ oldStorageType: defaultValues?.storageType as DiskType,
+ newSize: totalSize,
+ newStorageType: 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'),
+ oldStorageType: defaultValues?.storageType as DiskType,
+ oldProvisionedIOPS: defaultValues?.provisionedIOPS || 0,
+ newStorageType: storageType as DiskType,
+ newProvisionedIOPS: provisionedIOPS,
numReplicas,
})
const throughputPrice = calculateThroughputPrice({
- storageType: form.getValues('storageType') as DiskType,
- newThroughput: form.getValues('throughput') || 0,
- oldThroughput: form.formState.defaultValues?.throughput || 0,
+ storageType: storageType as DiskType,
+ newThroughput: throughput || 0,
+ oldThroughput: defaultValues?.throughput || 0,
numReplicas,
})
@@ -86,49 +124,55 @@ export function useDiskManagementReviewChanges(
Number(iopsPrice.newPrice) +
Number(throughputPrice.newPrice)
+ const advancedBeforePrice = Number(iopsPrice.oldPrice) + Number(throughputPrice.oldPrice)
+ const advancedAfterPrice = Number(iopsPrice.newPrice) + Number(throughputPrice.newPrice)
+
+ const showComputeBillingBadge = shouldShowComputeBillingBadge({
+ isDirty,
+ hasComputeSizeError: !!errors.computeSize,
+ oldPrice: computeSizePrice.oldPrice,
+ newPrice: computeSizePrice.newPrice,
+ })
+
+ const showDiskBillingBadge =
+ isDirty &&
+ !errors.totalSize &&
+ Number(diskSizePrice.oldPrice) !== Number(diskSizePrice.newPrice)
+
+ const showAdvancedBillingBadge =
+ isDirty &&
+ advancedBeforePrice !== advancedAfterPrice &&
+ !errors.provisionedIOPS &&
+ !errors.throughput
+
// --- Change flags ---
- const hasComputeChanges =
- form.formState.defaultValues?.computeSize !== form.getValues('computeSize')
+ const hasComputeChanges = defaultValues?.computeSize !== computeSize
const hasTotalSizeChanges =
- !isAwsK8sProject &&
- !isAwsNimbus &&
- form.formState.defaultValues?.totalSize !== form.getValues('totalSize')
+ !isAwsK8sProject && !isAwsNimbus && defaultValues?.totalSize !== totalSize
const hasStorageTypeChanges =
- !isAwsK8sProject &&
- !isAwsNimbus &&
- form.formState.defaultValues?.storageType !== form.getValues('storageType')
+ !isAwsK8sProject && !isAwsNimbus && defaultValues?.storageType !== storageType
const hasThroughputChanges =
- !isAwsK8sProject &&
- !isAwsNimbus &&
- form.formState.defaultValues?.throughput !== form.getValues('throughput')
+ !isAwsK8sProject && !isAwsNimbus && defaultValues?.throughput !== throughput
const hasIOPSChanges =
- !isAwsK8sProject &&
- !isAwsNimbus &&
- form.formState.defaultValues?.provisionedIOPS !== form.getValues('provisionedIOPS')
+ !isAwsK8sProject && !isAwsNimbus && defaultValues?.provisionedIOPS !== provisionedIOPS
const hasGrowthPercentChanges =
- !isAwsK8sProject &&
- !isAwsNimbus &&
- form.formState.defaultValues?.growthPercent !== form.getValues('growthPercent')
+ !isAwsK8sProject && !isAwsNimbus && defaultValues?.growthPercent !== growthPercent
const hasMinIncrementChanges =
- !isAwsK8sProject &&
- !isAwsNimbus &&
- form.formState.defaultValues?.minIncrementGb !== form.getValues('minIncrementGb')
+ !isAwsK8sProject && !isAwsNimbus && defaultValues?.minIncrementGb !== minIncrementGb
const hasMaxSizeChanges =
- !isAwsK8sProject &&
- !isAwsNimbus &&
- form.formState.defaultValues?.maxSizeGb !== form.getValues('maxSizeGb')
+ !isAwsK8sProject && !isAwsNimbus && defaultValues?.maxSizeGb !== maxSizeGb
// --- Derived predicates ---
- const storageTypeAfter = form.getValues('storageType') as DiskType
+ const storageTypeAfter = storageType as DiskType
// Show hero whenever any line-item price actually changes, not just compute
const anyBillableDiskChange =
@@ -143,11 +187,9 @@ export function useDiskManagementReviewChanges(
const hasExtendedDowntimeRisk =
hasComputeChanges &&
(COMPUTE_SIZES_BELOW_LARGE.includes(
- (form.formState.defaultValues?.computeSize ?? 'ci_nano') as ComputeInstanceAddonVariantId
+ (defaultValues?.computeSize ?? 'ci_nano') as ComputeInstanceAddonVariantId
) ||
- COMPUTE_SIZES_BELOW_LARGE.includes(
- form.getValues('computeSize') as ComputeInstanceAddonVariantId
- ))
+ COMPUTE_SIZES_BELOW_LARGE.includes(computeSize as ComputeInstanceAddonVariantId))
// Throughput is only a user-configurable, separately-billed attribute for GP3. For IO2 it is
// derived from provisioned IOPS (0.256 MiB/s per IOPS) and isn't surfaced as its own value, so
@@ -171,10 +213,8 @@ export function useDiskManagementReviewChanges(
// --- Labels ---
- const oldComputeLabel = mapAddOnVariantIdToComputeSize(
- form.formState.defaultValues?.computeSize ?? 'ci_nano'
- )
- const newComputeLabel = mapAddOnVariantIdToComputeSize(form.getValues('computeSize'))
+ const oldComputeLabel = mapAddOnVariantIdToComputeSize(defaultValues?.computeSize ?? 'ci_nano')
+ const newComputeLabel = mapAddOnVariantIdToComputeSize(computeSize)
return {
// prices
@@ -184,6 +224,11 @@ export function useDiskManagementReviewChanges(
throughputPrice,
totalBeforePrice,
totalAfterPrice,
+ advancedBeforePrice,
+ advancedAfterPrice,
+ showComputeBillingBadge,
+ showDiskBillingBadge,
+ showAdvancedBillingBadge,
// change flags
hasComputeChanges,
hasTotalSizeChanges,
diff --git a/apps/studio/components/interfaces/DiskManagement/fields/AutoScaleFields.tsx b/apps/studio/components/interfaces/DiskManagement/fields/AutoScaleFields.tsx
index e6a675e6ddace..82bfd945a6d92 100644
--- a/apps/studio/components/interfaces/DiskManagement/fields/AutoScaleFields.tsx
+++ b/apps/studio/components/interfaces/DiskManagement/fields/AutoScaleFields.tsx
@@ -55,7 +55,7 @@ export const AutoScaleFields = ({ form }: AutoScaleFieldProps) => {
render={({ field }) => {
return (
{
render={({ field }) => {
return (
{
render={({ field }) => {
return (
Hardware resources allocated to your Postgres database
+}
+
+type ComputeSectionBillingBadgeProps = {
+ form: UseFormReturn
+ show: boolean
+ beforePrice: number
+ afterPrice: number
+}
+
+export function ComputeSectionBillingBadge({
+ form,
+ show,
+ beforePrice,
+ afterPrice,
+}: ComputeSectionBillingBadgeProps) {
+ const computeSize = form.watch('computeSize')
+ const { showMicroUpgradeBadge } = useShowMicroUpgradeBadge()
+
+ return (
+
+ )
+}
+
export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
const { ref } = useParams()
const { data: org } = useSelectedOrganizationQuery()
- const { data: project, isPending: isProjectLoading } = useSelectedProjectQuery()
-
- const { hasAccess: entitledUpdateCompute, isLoading: isEntitlementLoading } =
- useCheckEntitlements('instances.compute_update_available_sizes')
+ const { project, isProjectLoading, isEntitlementLoading, showMicroUpgradeBadge } =
+ useShowMicroUpgradeBadge()
const showComputePrice = useIsFeatureEnabled('project_addons:show_compute_price')
- const { computeSize } = form.watch()
-
const {
data: addons,
isPending: isAddonsLoading,
@@ -67,7 +86,7 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
const isLoading = isProjectLoading || isAddonsLoading || isEntitlementLoading
- const { control, formState, setValue, trigger } = form
+ const { control, setValue, trigger } = form
const availableAddons = useMemo(() => {
return addons?.available_addons ?? []
@@ -81,319 +100,239 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
return getAvailableComputeOptions(availableAddons, project?.cloud_provider)
}, [availableAddons, project?.cloud_provider])
- // Expand by default if the project's current compute size is beyond the initial visible set
- const [showAllSizes, setShowAllSizes] = useState(() => {
- const idx = availableOptions.findIndex((o) => o.identifier === computeSize)
- return idx >= INITIALLY_VISIBLE_COUNT
- })
-
- // Expand whenever the selected size falls outside the visible set — covers both initial data
- // load (availableOptions starts empty) and computeSize changes after mount (e.g. form reset)
- useEffect(() => {
- const idx = availableOptions.findIndex((o) => o.identifier === computeSize)
- if (idx >= INITIALLY_VISIBLE_COUNT) {
- setShowAllSizes(true)
- }
- }, [computeSize, availableOptions])
-
const subscriptionPitr = addons?.selected_addons.find((addon) => addon.type === 'pitr')
- const computeSizePrice = calculateComputeSizePrice({
- availableOptions: availableOptions,
- oldComputeSize: form.formState.defaultValues?.computeSize || 'ci_micro',
- newComputeSize: form.getValues('computeSize'),
- plan: org?.plan.id ?? 'free',
- })
-
- const projectComputeSize = project?.infra_compute_size ?? 'nano'
- const showUpgradeBadge = entitledUpdateCompute && projectComputeSize === 'nano'
-
- const selectedOptionIndex = availableOptions.findIndex((o) => o.identifier === computeSize)
- const selectedOptionIsHidden = selectedOptionIndex >= INITIALLY_VISIBLE_COUNT
-
- // Always show all options if the selected one would be outside the visible slice,
- // so the active card is never hidden from the user.
- const visibleOptions =
- showAllSizes || selectedOptionIsHidden
- ? availableOptions
- : availableOptions.slice(0, INITIALLY_VISIBLE_COUNT)
- const hasHiddenOptions = availableOptions.length > INITIALLY_VISIBLE_COUNT
+ const showSkeletons = isLoading
+ const showLoadError = !isLoading && !!addonsError
+ const showComputeOptions = !isLoading && !addonsError
return (
(
- {
- setValue('computeSize', value, {
- shouldDirty: true,
- shouldValidate: true,
- })
- trigger('provisionedIOPS')
- trigger('throughput')
- }}
- defaultValue={field.value}
- disabled={disabled}
- >
-
+ {
+ setValue('computeSize', value, {
+ shouldDirty: true,
+ shouldValidate: true,
+ })
+ trigger('provisionedIOPS')
+ trigger('throughput')
+ }}
+ defaultValue={field.value}
+ disabled={disabled}
+ className={cn(
+ !addonsError && 'grid grid-cols-2 gap-4 @[680px]:grid-cols-3 @[900px]:grid-cols-4'
+ )}
+ >
+ {showSkeletons &&
+ Array(SKELETON_PLACEHOLDER_COUNT)
+ .fill(0)
+ .map((_, i) => )}
+ {showLoadError && (
+
+ {addonsError?.message}
+
+ )}
+ {showComputeOptions && (
<>
-
-
- Hardware resources allocated to your Postgres database
-
+ {availableOptions.map((compute) => {
+ const lockedMicroDueToPITR =
+ compute.identifier === 'ci_micro' && !!subscriptionPitr
+ const lockedNanoDueToPlan =
+ org?.plan.id !== 'free' &&
+ project?.infra_compute_size !== 'nano' &&
+ compute.identifier === 'ci_nano'
-
-
-
+ const lockedOption = lockedNanoDueToPlan || lockedMicroDueToPITR
- {showUpgradeBadge && form.watch('computeSize') === 'ci_nano' && (
-
- )}
- >
- }
- >
-
-
- {isLoading ? (
- Array(INITIALLY_VISIBLE_COUNT)
- .fill(0)
- .map((_, i) =>
)
- ) : addonsError ? (
-
- {addonsError?.message}
-
- ) : (
- <>
- {visibleOptions.map((compute) => {
- const lockedMicroDueToPITR =
- compute.identifier === 'ci_micro' && !!subscriptionPitr
- const lockedNanoDueToPlan =
- org?.plan.id !== 'free' &&
- project?.infra_compute_size !== 'nano' &&
- compute.identifier === 'ci_nano'
-
- const lockedOption = lockedNanoDueToPlan || lockedMicroDueToPITR
+ // Nano on a paid plan is billed at the Micro rate
+ const isNanoBilledAsMicro =
+ org?.plan.id !== 'free' &&
+ project?.infra_compute_size === 'nano' &&
+ compute.identifier === 'ci_nano'
- const price =
- org?.plan.id !== 'free' &&
- project?.infra_compute_size === 'nano' &&
- compute.identifier === 'ci_nano'
- ? availableOptions.find(
- (option: ComputeAddonVariant) => option.identifier === 'ci_micro'
- )?.price
- : compute.price
+ const price = isNanoBilledAsMicro
+ ? availableOptions.find(
+ (option: ComputeAddonVariant) => option.identifier === 'ci_micro'
+ )?.price
+ : compute.price
- const cpuLabel = (() => {
- const cpuCores = compute.meta?.cpu_cores
- if (typeof cpuCores === 'number') {
- return `${cpuCores}-core CPU`
- }
- if (cpuCores) {
- return `${cpuCores} CPU`
- }
- return 'CPU'
- })()
+ const cpuLabel = (() => {
+ const cpuCores = compute.meta?.cpu_cores
+ if (typeof cpuCores === 'number') {
+ return `${cpuCores}-core CPU`
+ }
+ if (cpuCores) {
+ return `${cpuCores} CPU`
+ }
+ return 'CPU'
+ })()
- return (
-
-
-
- {showUpgradeBadge && compute.identifier === 'ci_micro' && (
-
-
No additional charge
+ return (
+
+
+
+ {showMicroUpgradeBadge && compute.identifier === 'ci_micro' && (
+
+
+ e.stopPropagation()}
+ onPointerDown={(e) => e.stopPropagation()}
+ >
+ Free Upgrade
- )}
-
-
-
-
- {lockedOption ? (
-
-
-
- ) : (
- showComputePrice && (
- <>
-
- ${price}
-
-
- {' '}
- /{' '}
- {compute.price_interval === 'monthly'
- ? 'month'
- : 'hour'}
-
- >
- )
- )}
-
-
-
-
-
-
-
-
- {compute.identifier === 'ci_nano' && 'Up to '}
- {compute.meta?.memory_gb ?? 0} GB memory
-
-
-
-
- {cpuLabel}
-
+
+
e.stopPropagation()}
+ >
+
+ Upgrade to Micro Compute
+
+
+ This Project is already paying for Micro Compute. You can
+ upgrade to Micro Compute at any time when convenient.
+
+
+
+ )}
+
+
+
+
+ {lockedOption && (
+
+
-
+ )}
+ {!lockedOption && showComputePrice && (
+ <>
+
+ ${price}
+
+
+ {' '}
+ /{' '}
+ {compute.price_interval === 'monthly' ? 'month' : 'hour'}
+
+ >
+ )}
-
- {lockedMicroDueToPITR && (
-
- Project has PITR enabled which requires a minimum of Small
- compute. Please{' '}
-
- disable PITR
- {' '}
- first before selecting Micro
-
- )}
-
- }
- />
- )
- })}
- {showAllSizes && (
-
e.preventDefault()}
- className={cn(
- 'relative text-sm text-left flex flex-col gap-0 px-0 py-3 [&_label]:w-full group w-full h-[110px]'
- )}
- label={
-
-
-
-
-
-
-
- Custom memory
-
-
-
-
Custom CPU
+
+
+
+
+
+ {compute.identifier === 'ci_nano' && 'Up to '}
+ {compute.meta?.memory_gb ?? 0} GB memory
+
+
+
+
+ {cpuLabel}
+
-
- }
- />
- )}
- >
- )}
-
-
+
+ {lockedMicroDueToPITR && (
+
+ Project has PITR enabled which requires a minimum of Small compute.
+ Please{' '}
+
+ disable PITR
+ {' '}
+ first before selecting Micro
+
+ )}
+
+ }
+ />
+ )
+ })}
+
+
+
+
+
- {!isLoading && !addonsError && hasHiddenOptions && (
-
+
+ Contact Us
+
+
+
+
+
+
+ Custom memory
+
+
+
+ Custom CPU
+
+
+
+
+
+
+ >
)}
-
-
+
+
)}
/>
)
diff --git a/apps/studio/components/interfaces/DiskManagement/fields/DiskSizeField.tsx b/apps/studio/components/interfaces/DiskManagement/fields/DiskSizeField.tsx
index df65d74b239bf..8b4342b999f03 100644
--- a/apps/studio/components/interfaces/DiskManagement/fields/DiskSizeField.tsx
+++ b/apps/studio/components/interfaces/DiskManagement/fields/DiskSizeField.tsx
@@ -1,7 +1,7 @@
import { useParams } from 'common'
import dayjs from 'dayjs'
import { RotateCcw } from 'lucide-react'
-import { UseFormReturn } from 'react-hook-form'
+import { useFormState, useWatch, type UseFormReturn } from 'react-hook-form'
import {
Button,
FormControl,
@@ -15,33 +15,24 @@ import {
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { DiskStorageSchemaType } from '../DiskManagement.schema'
-import { calculateDiskSizePrice } from '../DiskManagement.utils'
-import { BillingChangeBadge } from '../ui/BillingChangeBadge'
import { DiskType, PLAN_DETAILS } from '../ui/DiskManagement.constants'
import { DiskManagementDiskSizeReadReplicas } from '../ui/DiskManagementReadReplicas'
-import { DiskSpaceBar } from '../ui/DiskSpaceBar'
import { DiskTypeRecommendationSection } from '../ui/DiskTypeRecommendationSection'
import FormMessage from '../ui/FormMessage'
-import { DocsButton } from '@/components/ui/DocsButton'
import { useDiskAttributesQuery } from '@/data/config/disk-attributes-query'
import { useDiskUtilizationQuery } from '@/data/config/disk-utilization-query'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
-import { DOCS_URL, GB } from '@/lib/constants'
+import { GB } from '@/lib/constants'
type DiskSizeFieldProps = {
form: UseFormReturn
disableInput: boolean
- setAdvancedSettingsOpenState: (state: boolean) => void
}
-export function DiskSizeField({
- form,
- disableInput,
- setAdvancedSettingsOpenState,
-}: DiskSizeFieldProps) {
+export function DiskSizeField({ form, disableInput }: DiskSizeFieldProps) {
const { ref: projectRef } = useParams()
- const { control, formState, setValue, trigger, getValues, resetField, watch } = form
+ const { control, setValue, trigger, resetField } = form
const { data: org } = useSelectedOrganizationQuery()
const { data: project } = useSelectedProjectQuery()
@@ -70,8 +61,8 @@ export function DiskSizeField({
dayjs.utc().diff(dayjs.utc(project?.inserted_at), 'minute') < 10 ||
project?.status === 'COMING_UP'
- const watchedStorageType = watch('storageType')
- const watchedTotalSize = watch('totalSize')
+ const watchedStorageType = useWatch({ control, name: 'storageType' })
+ const watchedTotalSize = useWatch({ control, name: 'totalSize' })
const planId = org?.plan.id ?? 'free'
@@ -79,125 +70,115 @@ export function DiskSizeField({
PLAN_DETAILS?.[planId as keyof typeof PLAN_DETAILS] ?? {}
const includedDiskGB = includedDiskGBMeta[watchedStorageType]
- const { defaultValues, dirtyFields, isDirty, errors } = formState
- const diskSizePrice = calculateDiskSizePrice({
- planId,
- oldSize: defaultValues?.totalSize || 0,
- oldStorageType: defaultValues?.storageType as DiskType,
- newSize: getValues('totalSize'),
- newStorageType: getValues('storageType') as DiskType,
- })
+ const { defaultValues, dirtyFields } = useFormState({ control })
const mainDiskUsed = Math.round(((diskUtil?.metrics.fs_used_bytes ?? 0) / GB) * 100) / 100
return (
-
-
-
(
-
-
-
- e.currentTarget.blur()}
- onChange={(e) => {
- setValue('totalSize', e.target.valueAsNumber, {
- shouldDirty: true,
- shouldValidate: true,
- })
+ (
+
+ {includedDiskGB > 0 && org?.plan.id && (
+
+ Your plan includes up to {includedDiskGB} GB of {watchedStorageType} storage.
+
+ )}
+
+ {
+ setValue('storageType', 'io2', { shouldDirty: true })
trigger('provisionedIOPS')
- trigger('throughput')
+ trigger('totalSize')
}}
- min={includedDiskGB}
- />
-
- GB
- {isDirty ? (
- {
- resetField('totalSize')
- trigger('provisionedIOPS')
- }}
- title="Reset"
- >
-
-
- ) : null}
-
-
-
-
- )}
- />
-
-
-
- {includedDiskGB > 0 &&
- org?.plan.id &&
- `Your plan includes up to ${includedDiskGB} GB of ${watchedStorageType} storage.`}
-
-
-
+ >
+ Change to High Performance SSD
+
+ }
+ />
+
+ {isProjectNew ? (
+
+ ) : (
+ error && (
+
+ {error?.message}
+
+ )
+ )}
+
+
-
-
{
- setValue('storageType', 'io2')
+ }
+ >
+
+
+ e.currentTarget.blur()}
+ onChange={(e) => {
+ // valueAsNumber is NaN while the input is empty, which would otherwise
+ // propagate into the price calculations and the read replica sizing
+ const value = e.target.valueAsNumber
+ setValue('totalSize', Number.isNaN(value) ? 0 : value, {
+ shouldDirty: true,
+ shouldValidate: true,
+ })
trigger('provisionedIOPS')
- trigger('totalSize')
- setAdvancedSettingsOpenState(true)
+ trigger('throughput')
}}
- >
- Change to High Performance SSD
-
- }
- />
-
-
-
-
-
- {isProjectNew ? (
-
- ) : (
- error && (
-
- {error?.message}
-
- )
- )}
-
-
-
-
+ min={includedDiskGB}
+ />
+
+ GB
+ {isDirty ? (
+ {
+ resetField('totalSize')
+ trigger('provisionedIOPS')
+ trigger('throughput')
+ }}
+ title="Reset"
+ >
+
+
+ ) : null}
+
+
+
+
+ )}
+ />
)
}
diff --git a/apps/studio/components/interfaces/DiskManagement/fields/IOPSField.tsx b/apps/studio/components/interfaces/DiskManagement/fields/IOPSField.tsx
index 4545339266057..71f0a40bed55f 100644
--- a/apps/studio/components/interfaces/DiskManagement/fields/IOPSField.tsx
+++ b/apps/studio/components/interfaces/DiskManagement/fields/IOPSField.tsx
@@ -13,10 +13,8 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { DiskStorageSchemaType } from '../DiskManagement.schema'
import {
calculateComputeSizeRequiredForIops,
- calculateIOPSPrice,
mapAddOnVariantIdToComputeSize,
} from '../DiskManagement.utils'
-import { BillingChangeBadge } from '../ui/BillingChangeBadge'
import { ComputeSizeRecommendationSection } from '../ui/ComputeSizeRecommendationSection'
import { DiskType, RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3 } from '../ui/DiskManagement.constants'
import { DiskManagementIOPSReadReplicas } from '../ui/DiskManagementReadReplicas'
@@ -37,13 +35,6 @@ export function IOPSField({ form, disableInput }: IOPSFieldProps) {
const { isError } = useDiskAttributesQuery({ projectRef })
- 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'
@@ -55,7 +46,7 @@ export function IOPSField({ form, disableInput }: IOPSFieldProps) {
const reccomendedComputeSize = calculateComputeSizeRequiredForIops(watchedIOPS)
return (
}
labelOptional={
- <>
-
- Input/output operations per second.
- >
+ Input/output operations per second.
}
>
diff --git a/apps/studio/components/interfaces/DiskManagement/fields/StorageTypeField.tsx b/apps/studio/components/interfaces/DiskManagement/fields/StorageTypeField.tsx
index dbd30360c2bf7..0c235d4f31344 100644
--- a/apps/studio/components/interfaces/DiskManagement/fields/StorageTypeField.tsx
+++ b/apps/studio/components/interfaces/DiskManagement/fields/StorageTypeField.tsx
@@ -45,7 +45,7 @@ export function StorageTypeField({ form, disableInput }: StorageTypeFieldProps)
name="storageType"
control={control}
render={({ field }) => (
-
+