diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx index 6a4ec30f974..e3053cf0218 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx @@ -1,5 +1,6 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' +import { useQueryClient } from '@tanstack/react-query' import { useParams } from 'common' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { MAX_WIDTH_CLASSES, PADDING_CLASSES, ScaffoldContainer } from 'components/layouts/Scaffold' @@ -9,14 +10,15 @@ import { } from 'data/config/disk-attributes-query' import { useUpdateDiskAttributesMutation } from 'data/config/disk-attributes-update-mutation' import { useDiskUtilizationQuery } from 'data/config/disk-utilization-query' +import { setProjectStatus } from 'data/projects/projects-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 { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' -import { useFlag } from 'hooks/ui/useFlag' +import { PROJECT_STATUS } from 'lib/constants' import { ChevronRight } from 'lucide-react' import { useEffect, useState } from 'react' import { useForm } from 'react-hook-form' @@ -32,7 +34,7 @@ import { } from 'ui' import { FormFooterChangeBadge } from '../DataWarehouse/FormFooterChangeBadge' import { CreateDiskStorageSchema, DiskStorageSchemaType } from './DiskManagement.schema' -import { formatComputeName } from './DiskManagement.utils' +import { DiskMangementRestartRequiredSection } from './DiskManagementRestartRequiredSection' import { DiskManagementReviewAndSubmitDialog } from './DiskManagementReviewAndSubmitDialog' import { ComputeSizeField } from './fields/ComputeSizeField' import { DiskSizeField } from './fields/DiskSizeField' @@ -47,10 +49,6 @@ import { } from './ui/DiskManagement.constants' import { DiskManagementPlanUpgradeRequired } from './ui/DiskManagementPlanUpgradeRequired' import { NoticeBar } from './ui/NoticeBar' -import { DiskMangementRestartRequiredSection } from './DiskManagementRestartRequiredSection' -import { useQueryClient } from '@tanstack/react-query' -import { PROJECT_STATUS } from 'lib/constants' -import { setProjectStatus } from 'data/projects/projects-query' export function DiskManagementForm() { const { project } = useProjectContext() @@ -63,8 +61,12 @@ export function DiskManagementForm() { message: string type: 'error' | 'success' } | null>(null) - const [showRestartPending, setShowRestartPendingState] = useState(false) const [advancedSettingsOpen, setAdvancedSettingsOpenState] = useState(false) + + /** + * Permissions + */ + const isPermissionsLoaded = usePermissionsLoaded() const canUpdateDiskConfiguration = useCheckPermissions(PermissionAction.UPDATE, 'projects', { resource: { project_id: project?.id, @@ -208,6 +210,11 @@ export function DiskManagementForm() { } }, [isSuccess]) + const isDirty = !!Object.keys(form.formState.dirtyFields).length + const isProjectResizing = project?.status === PROJECT_STATUS.RESIZING + const isProjectRequestingDiskChanges = isRequestingChanges && !isProjectResizing + const noPermissions = isPermissionsLoaded && !canUpdateDiskConfiguration + const { mutateAsync: updateDiskConfiguration, isLoading: isUpdatingDisk } = useUpdateDiskAttributesMutation({}) const { mutateAsync: updateSubscriptionAddon, isLoading: isUpdatingCompute } = @@ -298,13 +305,9 @@ export function DiskManagementForm() { // return <> - const isDirty = !!Object.keys(form.formState.dirtyFields).length - const isProjectResizing = project?.status === PROJECT_STATUS.RESIZING - const isProjectRequestingDiskChanges = isRequestingChanges && !isProjectResizing - return ( <> - {isProjectResizing || isProjectRequestingDiskChanges ? ( + {isProjectResizing || isProjectRequestingDiskChanges || noPermissions ? ( + ) : null} @@ -331,7 +339,8 @@ export function DiskManagementForm() { setAdvancedSettingsOpenState((prev) => !prev)} > @@ -382,6 +391,7 @@ export function DiskManagementForm() {
- + { @@ -81,7 +81,7 @@ export function ComputeSizeField({ form }: ComputeSizeFieldProps) { } > { setValue('computeSize', value, { shouldDirty: true, @@ -99,7 +99,7 @@ export function ComputeSizeField({ form }: ComputeSizeFieldProps) { .fill(0) .map((_, i) => ) ) : addonsError ? ( -

Error loading compute options

+ ) : ( availableOptions.map((compute: ComputeOption) => { // @ts-expect-error diff --git a/apps/studio/components/interfaces/DiskManagement/fields/DiskSizeField.tsx b/apps/studio/components/interfaces/DiskManagement/fields/DiskSizeField.tsx index aa064aa869a..917da8c9633 100644 --- a/apps/studio/components/interfaces/DiskManagement/fields/DiskSizeField.tsx +++ b/apps/studio/components/interfaces/DiskManagement/fields/DiskSizeField.tsx @@ -16,6 +16,7 @@ import DiskSpaceBar from '../ui/DiskSpaceBar' import { InputPostTab } from '../ui/InputPostTab' import { InputResetButton } from '../ui/InputResetButton' import FormMessage from '../ui/FormMessage' +import { useDiskAttributesQuery } from 'data/config/disk-attributes-query' type DiskSizeFieldProps = { form: UseFormReturn @@ -27,10 +28,16 @@ export function DiskSizeField({ form, disableInput }: DiskSizeFieldProps) { const { control, formState, setValue, trigger, getValues, resetField, watch } = form const org = useSelectedOrganization() + const { + isLoading: isLoadingDiskAttributes, + error: diskAttributesError, + isError: isDiskAttributesError, + } = useDiskAttributesQuery({ projectRef }) const { data: subscription, isLoading: isSubscriptionLoading, error: subscriptionError, + isError: isSubscriptionError, } = useOrgSubscriptionQuery({ orgSlug: org?.slug, }) @@ -38,12 +45,14 @@ export function DiskSizeField({ form, disableInput }: DiskSizeFieldProps) { data: diskUtil, isLoading: isDiskUtilizationLoading, error: diskUtilError, + isError: isDiskUtilizationError, } = useDiskUtilizationQuery({ projectRef: projectRef, }) - const isLoading = isSubscriptionLoading || isDiskUtilizationLoading - const error = subscriptionError || diskUtilError + const isLoading = isSubscriptionLoading || isDiskUtilizationLoading || isLoadingDiskAttributes + const error = subscriptionError || diskUtilError || diskAttributesError + const isError = isSubscriptionError || isDiskUtilizationError || isDiskAttributesError const watchedStorageType = watch('storageType') const watchedTotalSize = watch('totalSize') @@ -73,7 +82,6 @@ export function DiskSizeField({ form, disableInput }: DiskSizeFieldProps) { control={control} render={({ field }) => ( - {error && }
{isLoading ? ( @@ -90,7 +98,7 @@ export function DiskSizeField({ form, disableInput }: DiskSizeFieldProps) { e.currentTarget.blur()} onChange={(e) => { @@ -136,13 +144,8 @@ export function DiskSizeField({ form, disableInput }: DiskSizeFieldProps) {
- + + {error && } @@ -21,12 +25,15 @@ type IOPSFieldProps = { } export function IOPSField({ form, disableInput }: IOPSFieldProps) { + const { ref: projectRef } = useParams() const { control, formState, setValue, trigger, getValues, watch } = form const watchedStorageType = watch('storageType') const watchedComputeSize = watch('computeSize') const watchedIOPS = watch('provisionedIOPS') ?? 0 + const { isLoading, error, isError } = useDiskAttributesQuery({ projectRef }) + const iopsPrice = calculateIOPSPrice({ oldStorageType: formState.defaultValues?.storageType as DiskType, oldProvisionedIOPS: formState.defaultValues?.provisionedIOPS || 0, @@ -47,8 +54,9 @@ export function IOPSField({ form, disableInput }: IOPSFieldProps) { + )} -
+ } labelOptional={ <> @@ -98,29 +106,37 @@ export function IOPSField({ form, disableInput }: IOPSFieldProps) { } > - - { - setValue('provisionedIOPS', e.target.valueAsNumber, { - shouldDirty: true, - shouldValidate: true, - }) - }} - /> - + {isLoading ? ( +
+ +
+ ) : ( + + { + setValue('provisionedIOPS', e.target.valueAsNumber, { + shouldDirty: true, + shouldValidate: true, + }) + }} + /> + + )}
+ {error && } ) }} diff --git a/apps/studio/components/interfaces/DiskManagement/fields/StorageTypeField.tsx b/apps/studio/components/interfaces/DiskManagement/fields/StorageTypeField.tsx index 96ce156c4a6..dca7310624b 100644 --- a/apps/studio/components/interfaces/DiskManagement/fields/StorageTypeField.tsx +++ b/apps/studio/components/interfaces/DiskManagement/fields/StorageTypeField.tsx @@ -1,16 +1,23 @@ -import { FormField_Shadcn_, FormControl_Shadcn_ } from 'ui' -import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { useParams } from 'common' +import { useDiskAttributesQuery } from 'data/config/disk-attributes-query' +import { UseFormReturn } from 'react-hook-form' import { + Badge, + FormControl_Shadcn_, + FormField_Shadcn_, Select_Shadcn_, - SelectTrigger_Shadcn_, - SelectValue_Shadcn_, SelectContent_Shadcn_, SelectItem_Shadcn_, + SelectTrigger_Shadcn_, + SelectValue_Shadcn_, + Skeleton, + buttonVariants, + cn, } from 'ui' -import { Badge } from 'ui' -import { UseFormReturn } from 'react-hook-form' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { DiskStorageSchemaType } from '../DiskManagement.schema' import { DISK_TYPE_OPTIONS } from '../ui/DiskManagement.constants' +import FormMessage from '../ui/FormMessage' type StorageTypeFieldProps = { form: UseFormReturn @@ -18,8 +25,11 @@ type StorageTypeFieldProps = { } export function StorageTypeField({ form, disableInput }: StorageTypeFieldProps) { + const { ref: projectRef } = useParams() const { control, trigger } = form + const { isLoading, error, isError } = useDiskAttributesQuery({ projectRef }) + return ( - - - - - + {isLoading ? ( + + ) : ( + + + + + + )} <> {DISK_TYPE_OPTIONS.map((item) => ( @@ -66,6 +85,7 @@ export function StorageTypeField({ form, disableInput }: StorageTypeFieldProps) + {error && } )} /> diff --git a/apps/studio/components/interfaces/DiskManagement/fields/ThroughputField.tsx b/apps/studio/components/interfaces/DiskManagement/fields/ThroughputField.tsx index a8c1a3a0ffd..0e335735529 100644 --- a/apps/studio/components/interfaces/DiskManagement/fields/ThroughputField.tsx +++ b/apps/studio/components/interfaces/DiskManagement/fields/ThroughputField.tsx @@ -1,6 +1,6 @@ import { AnimatePresence, motion } from 'framer-motion' import { UseFormReturn } from 'react-hook-form' -import { FormControl_Shadcn_, FormField_Shadcn_, Input_Shadcn_ } from 'ui' +import { cn, FormControl_Shadcn_, FormField_Shadcn_, Input_Shadcn_, Skeleton } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { BillingChangeBadge } from '../ui/BillingChangeBadge' import { @@ -14,6 +14,10 @@ import { DiskStorageSchemaType } from '../DiskManagement.schema' import { DiskManagementThroughputReadReplicas } from '../ui/DiskManagementReadReplicas' import { InputPostTab } from '../ui/InputPostTab' import { useEffect } from 'react' +import FormMessage from '../ui/FormMessage' +import { useDiskAttributesQuery } from 'data/config/disk-attributes-query' +import { useParams } from 'common' +import { InputVariants } from '@ui/components/shadcn/ui/input' type ThroughputFieldProps = { form: UseFormReturn @@ -21,6 +25,8 @@ type ThroughputFieldProps = { } export function ThroughputField({ form, disableInput }: ThroughputFieldProps) { + const { ref: projectRef } = useParams() + const { control, formState, setValue, getValues, watch } = form const watchedStorageType = watch('storageType') @@ -28,6 +34,8 @@ export function ThroughputField({ form, disableInput }: ThroughputFieldProps) { const watchedComputeSize = watch('computeSize') const throughput_mbps = formState.defaultValues?.throughput + const { isLoading, error } = useDiskAttributesQuery({ projectRef }) + const throughputPrice = calculateThroughputPrice({ storageType: form.getValues('storageType') as DiskType, newThroughput: form.getValues('throughput') || 0, @@ -98,28 +106,40 @@ export function ThroughputField({ form, disableInput }: ThroughputFieldProps) { } > - - { - 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'} - /> - + {isLoading ? ( +
+ +
+ ) : ( + + { + 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'} + /> + + )}
+ {error && } )} /> diff --git a/apps/studio/components/interfaces/DiskManagement/ui/ComputeSizeReccomendationSection.tsx b/apps/studio/components/interfaces/DiskManagement/ui/ComputeSizeReccomendationSection.tsx index a3e86e82cd5..224ece65b32 100644 --- a/apps/studio/components/interfaces/DiskManagement/ui/ComputeSizeReccomendationSection.tsx +++ b/apps/studio/components/interfaces/DiskManagement/ui/ComputeSizeReccomendationSection.tsx @@ -24,7 +24,7 @@ export function ComputeSizeReccomendationSection({ iops > maxIOPSforComputeSize && !RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3.includes(computeSize) return ( - + {isVisible && ( + {isDirty && ( + {isDirty && ( - {type === 'error' ? : } - {message} - + ) }