diff --git a/apps/studio/components/interfaces/DiskManagement/BillingMangementForm.loading.tsx b/apps/studio/components/interfaces/DiskManagement/BillingMangementForm.loading.tsx new file mode 100644 index 00000000000..743aef5bd23 --- /dev/null +++ b/apps/studio/components/interfaces/DiskManagement/BillingMangementForm.loading.tsx @@ -0,0 +1,64 @@ +import { ScaffoldContainer } from 'components/layouts/Scaffold' +import { Form_Shadcn_, Separator, Skeleton } from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' + +export function DiskManagementFormLoading() { + return ( + +
+ + + + Hardware resources allocated to your postgres database

} + > +
+ + + + + + + + + + +
+
+ + + + +
+ + +
+
+ + {/* + + + + + + + + */} +
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/DiskManagement/DiskCountdownRadial.tsx b/apps/studio/components/interfaces/DiskManagement/DiskCountdownRadial.tsx index 3905780b1df..f855145deab 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskCountdownRadial.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskCountdownRadial.tsx @@ -5,25 +5,35 @@ import { Card, CardContent } from 'ui' import CountdownTimerRadial from '../../ui/CountdownTimer/CountdownTimerRadial' import CountdownTimerSpan from '../../ui/CountdownTimer/CountdownTimerSpan' -export function DiskCountdownRadial({ remainingTime }: { remainingTime: number }) { +export function DiskCountdownRadial({ + remainingTime, + show = true, +}: { + remainingTime: number + show?: boolean +}) { const progressPercentage = (remainingTime / COOLDOWN_DURATION) * 100 return ( - {remainingTime !== 0 && ( + {remainingTime !== 0 && show && ( - +
-

+

6-hour wait period between disk modifications

+

+ You can't modify your disk configuration until the 6-hour cool down period ends. + You can however change the compute size.. +

diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx index 3f4167f22e4..71094194f6f 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx @@ -1,7 +1,7 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { AnimatePresence, motion } from 'framer-motion' -import { CpuIcon, InfoIcon, Microchip, RotateCcw } from 'lucide-react' +import { ChevronRight, CpuIcon, InfoIcon, Microchip, RotateCcw } from 'lucide-react' import Link from 'next/link' import { useEffect, useMemo, useState } from 'react' import { useForm } from 'react-hook-form' @@ -31,6 +31,10 @@ import { Card, CardContent, cn, + Collapsible, + Collapsible_Shadcn_, + CollapsibleContent_Shadcn_, + CollapsibleTrigger_Shadcn_, Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, @@ -38,6 +42,11 @@ import { Input_Shadcn_ as Input, RadioGroupCard, RadioGroupCardItem, + Select_Shadcn_, + SelectContent_Shadcn_, + SelectItem_Shadcn_, + SelectTrigger_Shadcn_, + SelectValue_Shadcn_, Separator, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' @@ -63,7 +72,7 @@ import { calculateThroughputPrice, getAvailableComputeOptions, } from './DiskManagement.utils' -import { DiskStorageSchema, DiskStorageSchemaType } from './DiskManagementPanelSchema' +import { CreateDiskStorageSchema, DiskStorageSchemaType } from './DiskManagementPanelSchema' import { DiskManagementPlanUpgradeRequired } from './DiskManagementPlanUpgradeRequired' import { DiskManagementDiskSizeReadReplicas, @@ -78,6 +87,7 @@ import { MAX_WIDTH_CLASSES, PADDING_CLASSES, ScaffoldContainer } from 'component import { InputPostTab } from './InputPostTab' import { InfoTooltip } from 'ui-patterns/info-tooltip' import { InputResetButton } from './InputResetButton' +import { DiskManagementFormLoading } from './BillingMangementForm.loading' export function DiskManagementForm() { const { project } = useProjectContext() @@ -88,6 +98,8 @@ export function DiskManagementForm() { const [remainingTime, setRemainingTime] = useState(0) const [refetchInterval, setRefetchInterval] = useState(false) + const [advancedSettingsOpen, setAdvancedSettingsOpenState] = useState(false) + const canUpdateDiskConfiguration = useCheckPermissions(PermissionAction.UPDATE, 'projects', { resource: { project_id: project?.id, @@ -189,7 +201,7 @@ export function DiskManagementForm() { } const form = useForm({ - resolver: zodResolver(DiskStorageSchema), + resolver: zodResolver(CreateDiskStorageSchema(defaultValues.totalSize)), defaultValues, mode: 'onBlur', reValidateMode: 'onChange', @@ -260,8 +272,6 @@ export function DiskManagementForm() { const { includedDiskGB: includedDiskGBMeta } = PLAN_DETAILS?.[planId as keyof typeof PLAN_DETAILS] ?? {} - console.log('includedDiskGBMeta', includedDiskGBMeta) - console.log('watchedStorageType', watchedStorageType) const includedDiskGB = includedDiskGBMeta[watchedStorageType] const minIOPS = IOPS_RANGE[watchedStorageType]?.min ?? 0 @@ -355,8 +365,21 @@ export function DiskManagementForm() { updateDiskConfigurationRQ({ ref: projectRef, ...data }) } + const diskTypeOptions = [ + { + type: 'gp3', + name: 'General Purpose SSD', + description: 'Balance between price and performance', + }, + { + type: 'io2', + name: 'Provisioned IOPS SSD', + description: 'High IOPS for mission-critical applications.', + }, + ] + if (isLoading) { - return
Loading...
+ return } if (error) { @@ -400,259 +423,207 @@ export function DiskManagementForm() { const isDirty = !!Object.keys(form.formState.dirtyFields).length return ( - -
- - {/* {showNewDiskManagementUI ? : null} */} - + <> + {isRequestingChanges ? ( + + +
+

+ Disk configuration changes have been requested +

+

The requested changes will be applied to your disk shortly

+
+
+
+ ) : ( + <> + )} + {/* */} + + + + {/* {showNewDiskManagementUI ? : null} */} - ( - <> - - -

hardware resources allocated to your postgres database

- - } - > - { - setValue('computeSize', value, { - shouldDirty: true, - shouldValidate: true, - }) + {true ? ( + + +
+

+ Disk configuration changes have been requested +

+

+ The requested changes will be applied to your disk shortly +

+
+
+
+ ) : ( + <> + )} - /** - * If IOPS hasn't been set yet, auto add recommeneded IOPS - */ - // if ( - // !form.formState.dirtyFields.provisionedIOPS && - // !RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3.includes(value) - // ) { - // setValue( - // 'provisionedIOPS', - // COMPUTE_BASELINE_IOPS[value as keyof typeof COMPUTE_BASELINE_IOPS], - // { - // // shouldDirty: true, - // // shouldValidate: true, - // } - // ) - // } else if ( - // !form.formState.dirtyFields.provisionedIOPS && - // RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3.includes(value) - // ) { - // setValue('provisionedIOPS', IOPS_RANGE[watchedStorageType].min, { - // // shouldDirty: true, - // // shouldValidate: true, - // }) - // } - }} - defaultValue={field.value} - value={field.value} + + + ( + <> + + +

hardware resources allocated to your postgres database

+ + } > - {availableOptions.map((compute) => { - const cpuArchitecture = getCloudProviderArchitecture(project?.cloud_provider) + { + setValue('computeSize', value, { + shouldDirty: true, + shouldValidate: true, + }) + }} + defaultValue={field.value} + value={field.value} + > + {availableOptions.map((compute) => { + 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 + return ( + +
+ +
+ + {/* Price needs to be exact here */}${compute.price} + + + {' '} + / {compute.price_interval === 'monthly' ? 'month' : 'hour'}
-
-
- } - > - ) - })} - - - - )} - /> - - - - ( - - - { - field.onChange(e) - // only trigger provisionedIOPS due to other input being hidden - await form.trigger('provisionedIOPS') - await form.trigger('totalSize') - }} - defaultValue={field.value} - disabled={disableInput} - > - {[ - { - type: 'gp3', - name: 'General Purpose SSD', - description: 'gp3 provides a balance between price and performance', - }, - { - type: 'io2', - name: 'Provisioned IOPS SSD', - description: 'io2 offers high IOPS for mission-critical applications.', - }, - ].map((item) => ( - - - -
- {item.name}{' '} -
- - {item.type} - + {/* */} +
+
+
+ + {compute.meta?.memory_gb ?? 0} GB memory +
+
+ + + {compute.meta?.cpu_cores ?? 0}-core {cpuArchitecture} CPU + +
-

{item.description}

} - /> - - - ))} - - - - )} - /> -
- ( - 0 && - `Your plan includes ${includedDiskGB} GB of disk size for ${watchedStorageType}.` - } - labelOptional={ - - } - > -
- - - e.currentTarget.blur()} - onChange={(e) => { - setValue('totalSize', e.target.valueAsNumber, { - shouldDirty: true, - shouldValidate: true, - }) - trigger('provisionedIOPS') - trigger('throughput') - }} - min={includedDiskGB} - /> - - - form.resetField('totalSize')} - /> -
-
+ > + ) + })} + + + )} /> -
- {/* You can add additional content in the remaining 4 columns if needed */} -
- {/* Additional content or information can go here */} -
-
+ + + + + + {isPlanUpgradeRequired && } + +
+ ( + 0 && + `Your plan includes ${includedDiskGB} GB of disk size for ${watchedStorageType}.` + } + labelOptional={ + + } + > +
+ + + e.currentTarget.blur()} + onChange={(e) => { + setValue('totalSize', e.target.valueAsNumber, { + shouldDirty: true, + shouldValidate: true, + }) + trigger('provisionedIOPS') + trigger('throughput') + }} + min={includedDiskGB} + /> + + + form.resetField('totalSize')} + /> +
+
+ )} + /> +
+ {/* You can add additional content in the remaining 4 columns if needed */}
-
- ( - -
- {watchedStorageType === 'io2' ? ( - <> - - IOPS must be{' '} - {watchedTotalSize >= 8 - ? `between ${minIOPS} and ${maxIOPS.toLocaleString()} based on your disk size.` - : `at least ${minIOPS}`} - - - For io2 storage type, min IOPS is at {minIOPS}, while max IOPS is at - 1000 * disk size in GB or{' '} - {IOPS_RANGE[DiskType.IO2].max.toLocaleString()}, whichever is lower - - - ) : ( - <> - - IOPS must be{' '} - {watchedTotalSize >= 8 - ? `between ${minIOPS.toLocaleString()} and ${maxIOPS.toLocaleString()} based on your disk size.` - : `at least ${minIOPS.toLocaleString()}`} - - - For gp3 storage type, min IOPS is at {minIOPS} while max IOPS is at 500 - * disk size in GB or {IOPS_RANGE[DiskType.GP3].max.toLocaleString()}, - whichever is lower - - - )} -
- {!form.formState.errors.provisionedIOPS && - field.value > maxIopsBasedOnCompute && ( -

- Note: Final usable IOPS will be at{' '} - - {maxIopsBasedOnCompute.toLocaleString()} - {' '} - based on your current compute size of {currentCompute?.name} -

- )} + - {!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' && ( - + setAdvancedSettingsOpenState((prev) => !prev)} + > + + +
+ Advanced disk settings + + Specify additional settings for your disk, including IOPS, throughput, and disk + type. + +
+
+ + ( + + { + field.onChange(e) + // only trigger provisionedIOPS due to other input being hidden + await form.trigger('provisionedIOPS') + await form.trigger('totalSize') + }} + defaultValue={field.value} + disabled={disableInput} + > + + + + + + + <> + {diskTypeOptions.map((item) => ( + +
+
+ {item.name}{' '} +
+ + {item.type} + +
+
+

{item.description}

+
+
+ ))} + +
+
+
+ )} + /> + + ( -
-
- - Throughput must be between {minThroughput.toLocaleString()} and{' '} - {maxThroughput?.toLocaleString()} MB/s based on your IOPS. - - - Min throughput is at 125MB/s, while max throughput is at 0.25MB/s * - IOPS or 1,000, whichever is lower - -
- {!form.formState.errors.throughput && - field.value !== undefined && - field.value > maxThroughputBasedOnCompute && ( -

- Note: Final usable throughput will be at{' '} - - {maxThroughputBasedOnCompute.toFixed(0)} - {' '} - MB/s based on your current compute size of {currentCompute?.name} -

- )} +
+ {watchedStorageType === 'io2' ? ( + <> + + IOPS must be{' '} + {watchedTotalSize >= 8 + ? `between ${minIOPS} and ${maxIOPS.toLocaleString()} based on your disk size.` + : `at least ${minIOPS}`} + + + For io2 storage type, min IOPS is at {minIOPS}, while max IOPS is + at 1000 * disk size in GB or{' '} + {IOPS_RANGE[DiskType.IO2].max.toLocaleString()}, whichever is + lower + + + ) : ( + <> + + IOPS must be{' '} + {watchedTotalSize >= 8 + ? `between ${minIOPS.toLocaleString()} and ${maxIOPS.toLocaleString()} based on your disk size.` + : `at least ${minIOPS.toLocaleString()}`} + + + For gp3 storage type, min IOPS is at {minIOPS} while max IOPS is + at 500 * disk size in GB or{' '} + {IOPS_RANGE[DiskType.GP3].max.toLocaleString()}, whichever is + lower + + + )}
- {!form.formState.errors.throughput && ( - maxIopsBasedOnCompute && ( +

+ Note: Final usable IOPS will be at{' '} + + {maxIopsBasedOnCompute.toLocaleString()} + {' '} + based on your current compute size of {currentCompute?.name} +

+ )} + + {!form.formState.errors.provisionedIOPS && ( + @@ -834,108 +780,195 @@ export function DiskManagementForm() { <> -

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

-

Higher throughput suits applications with high data transfer needs.

+

Input/output operations per second.

+

Higher IOPS for high-throughput apps.

} > - + { - setValue('throughput', e.target.valueAsNumber, { + setValue('provisionedIOPS', e.target.valueAsNumber, { shouldDirty: true, shouldValidate: true, }) }} - className="flex-grow font-mono rounded-r-none max-w-32" - disabled={ - disableInput || disableIopsInput || watchedStorageType === 'io2' - } /> )} /> + + + {form.getValues('storageType') === 'gp3' && ( + + ( + +
+
+ + Throughput must be between {minThroughput.toLocaleString()}{' '} + and {maxThroughput?.toLocaleString()} MB/s based on your IOPS. + + + Min throughput is at 125MB/s, while max throughput is at + 0.25MB/s * IOPS or 1,000, whichever is lower + +
+ {!form.formState.errors.throughput && + field.value !== undefined && + field.value > maxThroughputBasedOnCompute && ( +

+ Note: Final usable throughput will be at{' '} + + {maxThroughputBasedOnCompute.toFixed(0)} + {' '} + MB/s based on your current compute size of{' '} + {currentCompute?.name} +

+ )} +
+ {!form.formState.errors.throughput && ( + + )} +
+ } + 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}
- {isRequestingChanges ? ( - - -
-

- Disk configuration changes have been requested -

-

- The requested changes will be applied to your disk shortly -

-
-
-
- ) : ( - - )} - - {isPlanUpgradeRequired && } - - - - {isDirty ? ( - -
- - - -
-
- ) : null} -
- - {/* */} - {/* */} - - + {/* */} + {/* */} + + + ) } diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelSchema.ts b/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelSchema.ts index cf0e7e8c632..91d51cf4194 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelSchema.ts +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelSchema.ts @@ -13,94 +13,104 @@ const baseSchema = z.object({ computeSize: z.string().describe('Compute size'), }) -export const DiskStorageSchema = baseSchema.superRefine((data, ctx) => { - const { storageType, totalSize, provisionedIOPS, throughput, computeSize } = data +export const CreateDiskStorageSchema = (defaultTotalSize: number) => { + const schema = baseSchema.superRefine((data, ctx) => { + const { storageType, totalSize, provisionedIOPS, throughput, computeSize } = data - // if(computeSize) - - if (storageType === 'io2') { - // Validation rules for io2 - const maxIOPS = calculateMaxIopsAllowed(totalSize) - - if (provisionedIOPS < 100) { + if (totalSize < defaultTotalSize) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: 'Provisioned IOPS must be at least 100', - path: ['provisionedIOPS'], - }) - } else if (provisionedIOPS > maxIOPS) { - if (totalSize >= 8) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Provisioned IOPS must be at most ${maxIOPS}.`, - path: ['provisionedIOPS'], - }) - } else { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Invalid IOPS value due to small disk size`, - path: ['provisionedIOPS'], - }) - } - } - - if (throughput !== undefined) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'Throughput is not configurable for io2.', - path: ['throughput'], - }) - } - - if (totalSize > DISK_LIMITS[DiskType.IO2].maxStorage) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'Allocated disksize must not exceed 61,440 GB', + message: `Allocated disk size must be at least ${defaultTotalSize} GB.`, path: ['totalSize'], }) } - } else if (storageType === 'gp3') { - // Validation rules for gp3 - const maxIOPS = Math.min(500 * totalSize, 16000) - const maxThroughput = Math.min(0.25 * provisionedIOPS, 1000) - if (provisionedIOPS < 3000) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Provisioned IOPS must be at least 3000`, - path: ['provisionedIOPS'], - }) - } else if (provisionedIOPS > maxIOPS) { - if (totalSize >= 8) { + if (storageType === 'io2') { + // Validation rules for io2 + const maxIOPS = calculateMaxIopsAllowed(totalSize) + + if (provisionedIOPS < 100) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: `Provisioned IOPS must be at most ${maxIOPS}`, + message: 'Provisioned IOPS must be at least 100', path: ['provisionedIOPS'], }) - } else { + } else if (provisionedIOPS > maxIOPS) { + if (totalSize >= 8) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Provisioned IOPS must be at most ${maxIOPS}.`, + path: ['provisionedIOPS'], + }) + } else { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Invalid IOPS value due to small disk size`, + path: ['provisionedIOPS'], + }) + } + } + + if (throughput !== undefined) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: `Invalid IOPS value due to invalid disk size`, + message: 'Throughput is not configurable for io2.', + path: ['throughput'], + }) + } + + if (totalSize > DISK_LIMITS[DiskType.IO2].maxStorage) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Allocated disksize must not exceed 61,440 GB', + path: ['totalSize'], + }) + } + } else if (storageType === 'gp3') { + // Validation rules for gp3 + const maxIOPS = Math.min(500 * totalSize, 16000) + const maxThroughput = Math.min(0.25 * provisionedIOPS, 1000) + + if (provisionedIOPS < 3000) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Provisioned IOPS must be at least 3000`, path: ['provisionedIOPS'], }) + } else if (provisionedIOPS > maxIOPS) { + if (totalSize >= 8) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Provisioned IOPS must be at most ${maxIOPS}`, + path: ['provisionedIOPS'], + }) + } else { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Invalid IOPS value due to invalid disk size`, + path: ['provisionedIOPS'], + }) + } + } + + if (throughput !== undefined && (throughput < 125 || throughput > maxThroughput)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Throughput must be set between 125 and ${maxThroughput?.toLocaleString()} MB/s.`, + path: ['throughput'], + }) + } + if (totalSize > DISK_LIMITS[DiskType.GP3].maxStorage) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Allocated disksize must not exceed 16,384 GB', + path: ['totalSize'], + }) } } + }) - if (throughput !== undefined && (throughput < 125 || throughput > maxThroughput)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Throughput must be set between 125 and ${maxThroughput?.toLocaleString()} MB/s.`, - path: ['throughput'], - }) - } - if (totalSize > DISK_LIMITS[DiskType.GP3].maxStorage) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'Allocated disksize must not exceed 16,384 GB', - path: ['totalSize'], - }) - } - } -}) + return schema +} -export type DiskStorageSchemaType = z.infer +export type DiskStorageSchemaType = z.infer> diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx index 75a50a14216..fe91e0a0ac0 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx @@ -115,7 +115,7 @@ interface DiskSizeMeterProps { form: UseFormReturn numReplicas: number isDialogOpen: boolean - isWithinCooldown: boolean + disabled?: boolean setIsDialogOpen: (isOpen: boolean) => void onSubmit: (values: DiskStorageSchemaType) => Promise } @@ -123,7 +123,7 @@ interface DiskSizeMeterProps { export const DiskManagementReviewAndSubmitDialog = ({ isDialogOpen, setIsDialogOpen, - isWithinCooldown, + disabled, form, numReplicas, loading, @@ -205,14 +205,14 @@ export const DiskManagementReviewAndSubmitDialog = ({ const isValid = await form.trigger() if (isValid) setIsDialogOpen(true) }} - disabled={isWithinCooldown || !isDirty} + disabled={disabled || !isDirty} tooltip={{ content: { side: 'bottom', text: !canUpdateDiskConfiguration ? 'You need additional permissions to update disk configuration' - : isWithinCooldown - ? 'Currently within cooldown period from previous disk change' + : disabled + ? 'Current form values are invalid' : undefined, }, }}