diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagement.schema.ts b/apps/studio/components/interfaces/DiskManagement/DiskManagement.schema.ts index 569fe74dbab..7df4a3e4f98 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagement.schema.ts +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagement.schema.ts @@ -8,15 +8,12 @@ import { calculateMaxIopsAllowedForDiskSizeWithio2, formatNumber, } from './DiskManagement.utils' -import { DISK_LIMITS, DiskType, IOPS_RANGE, THROUGHPUT_RANGE } from './ui/DiskManagement.constants' +import { DISK_LIMITS, DiskType } from './ui/DiskManagement.constants' +import { CloudProvider } from 'shared-data' const baseSchema = z.object({ storageType: z.enum(['io2', 'gp3']).describe('Type of storage: io2 or gp3'), - totalSize: z - .number() - .int('Value must be an integer') - .min(8, { message: 'Allocated disk size must be at least 8 GB.' }) - .describe('Allocated disk size in GB'), + totalSize: z.number().int('Value must be an integer').describe('Allocated disk size in GB'), provisionedIOPS: z.number().describe('Provisioned IOPS for storage type'), throughput: z.number().optional().describe('Throughput in MB/s for gp3'), computeSize: z @@ -44,11 +41,21 @@ const baseSchema = z.object({ .nullable(), }) -export const CreateDiskStorageSchema = (defaultTotalSize: number) => { +export const CreateDiskStorageSchema = (defaultTotalSize: number, cloudProvider: CloudProvider) => { + const isFlyProject = cloudProvider === 'FLY' + const schema = baseSchema.superRefine((data, ctx) => { const { storageType, totalSize, provisionedIOPS, throughput, maxSizeGb } = data - if (totalSize < defaultTotalSize) { + if (!isFlyProject && totalSize < 8) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Allocated disk size must be at least 8 GB.', + path: ['totalSize'], + }) + } + + if (!isFlyProject && totalSize < defaultTotalSize) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Disk size cannot be reduced in size. Reduce your database size and then head to the Infrastructure settings and go through a Postgres version upgrade to right-size your disk.`, @@ -57,7 +64,7 @@ export const CreateDiskStorageSchema = (defaultTotalSize: number) => { } // Validate maxSizeGb cannot be lower than totalSize - if (!!maxSizeGb && maxSizeGb < totalSize) { + if (!isFlyProject && !!maxSizeGb && maxSizeGb < totalSize) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Max disk size cannot be lower than the current disk size. Must be at least ${formatNumber(totalSize)} GB.`, @@ -65,23 +72,23 @@ export const CreateDiskStorageSchema = (defaultTotalSize: number) => { }) } - if (storageType === 'io2') { + if (!isFlyProject && storageType === 'io2') { // Validation rules for io2 - if (provisionedIOPS > IOPS_RANGE[DiskType.IO2].max) { + if (provisionedIOPS > DISK_LIMITS[DiskType.IO2].maxIops) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: `IOPS can not exceed ${formatNumber(IOPS_RANGE[DiskType.IO2].max)} for io2 Disk type. Please reach out to support if you need higher IOPS than this.`, + message: `IOPS can not exceed ${formatNumber(DISK_LIMITS[DiskType.IO2].maxIops)} for io2 Disk type. Please reach out to support if you need higher IOPS than this.`, path: ['provisionedIOPS'], }) } const maxIOPSforDiskSizeWithio2 = calculateMaxIopsAllowedForDiskSizeWithio2(totalSize) - if (provisionedIOPS < IOPS_RANGE[DiskType.IO2].min) { + if (provisionedIOPS < DISK_LIMITS[DiskType.IO2].minIops) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: `Provisioned IOPS must be at least ${formatNumber(IOPS_RANGE[DiskType.IO2].min)}`, + message: `Provisioned IOPS must be at least ${formatNumber(DISK_LIMITS[DiskType.IO2].minIops)}`, path: ['provisionedIOPS'], }) } else if (provisionedIOPS > maxIOPSforDiskSizeWithio2) { @@ -122,21 +129,21 @@ export const CreateDiskStorageSchema = (defaultTotalSize: number) => { } } - if (storageType === 'gp3') { + if (!isFlyProject && storageType === 'gp3') { const maxIopsAllowedForDiskSizeWithGp3 = calculateMaxIopsAllowedForDiskSizeWithGp3(totalSize) - if (provisionedIOPS > IOPS_RANGE[DiskType.GP3].max) { + if (provisionedIOPS > DISK_LIMITS[DiskType.GP3].maxIops) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: `IOPS can not exceed ${formatNumber(IOPS_RANGE[DiskType.GP3].max)} for GP3 Disk. Change the Disk type to io2 for higher IOPS support.`, + message: `IOPS can not exceed ${formatNumber(DISK_LIMITS[DiskType.GP3].maxIops)} for GP3 Disk. Change the Disk type to io2 for higher IOPS support.`, path: ['provisionedIOPS'], }) } - if (provisionedIOPS < IOPS_RANGE[DiskType.GP3].min) { + if (provisionedIOPS < DISK_LIMITS[DiskType.GP3].minIops) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: `IOPS must be at least ${formatNumber(IOPS_RANGE[DiskType.GP3].min)}`, + message: `IOPS must be at least ${formatNumber(DISK_LIMITS[DiskType.GP3].minIops)}`, path: ['provisionedIOPS'], }) } else if (provisionedIOPS > maxIopsAllowedForDiskSizeWithGp3) { @@ -161,7 +168,7 @@ export const CreateDiskStorageSchema = (defaultTotalSize: number) => { if (throughput !== undefined) { const maxThroughput = Math.min(0.25 * provisionedIOPS, 1000) - if (throughput > THROUGHPUT_RANGE['gp3'].max) { + if (throughput > DISK_LIMITS['gp3'].maxThroughput) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Throughput can not exceed ${formatNumber(maxThroughput)} MB/s`, @@ -176,10 +183,10 @@ export const CreateDiskStorageSchema = (defaultTotalSize: number) => { path: ['throughput'], }) } - if (throughput < THROUGHPUT_RANGE[DiskType.GP3].min) { + if (throughput < DISK_LIMITS[DiskType.GP3].minThroughput) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: `Throughput must be at least ${formatNumber(THROUGHPUT_RANGE[DiskType.GP3].min)} MB/s`, + message: `Throughput must be at least ${formatNumber(DISK_LIMITS[DiskType.GP3].minThroughput)} MB/s`, path: ['throughput'], }) } diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx index c76965a4e80..7da06fd5de7 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx @@ -4,7 +4,6 @@ import { useQueryClient } from '@tanstack/react-query' import { AnimatePresence, motion } from 'framer-motion' import { ChevronRight } from 'lucide-react' import Link from 'next/link' -import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' @@ -30,7 +29,6 @@ import { AddonVariantId } from 'data/subscriptions/types' import { useResourceWarningsQuery } from 'data/usage/resource-warnings-query' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' -import { useFlag } from 'hooks/ui/useFlag' import { GB, PROJECT_STATUS } from 'lib/constants' import { Button, @@ -55,13 +53,10 @@ import { IOPSField } from './fields/IOPSField' import { StorageTypeField } from './fields/StorageTypeField' import { ThroughputField } from './fields/ThroughputField' import { DiskCountdownRadial } from './ui/DiskCountdownRadial' -import { - DiskType, - IOPS_RANGE, - RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3, -} from './ui/DiskManagement.constants' +import { DiskType, RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3 } from './ui/DiskManagement.constants' import { NoticeBar } from './ui/NoticeBar' import { SpendCapDisabledSection } from './ui/SpendCapDisabledSection' +import { CloudProvider } from 'shared-data' export function DiskManagementForm() { // isLoading is used to avoid a useCheckPermissions() race condition @@ -69,14 +64,13 @@ export function DiskManagementForm() { const org = useSelectedOrganization() const { ref: projectRef } = useParams() const queryClient = useQueryClient() - const router = useRouter() - const diskAndComputeForm = useFlag('diskAndComputeForm') const { data: resourceWarnings } = useResourceWarningsQuery() const projectResourceWarnings = (resourceWarnings ?? [])?.find( (warning) => warning.project === project?.ref ) const isReadOnlyMode = projectResourceWarnings?.is_readonly_mode_enabled + const isFlyArchitecture = project?.cloud_provider === 'FLY' /** * Permissions @@ -123,27 +117,35 @@ export function DiskManagementForm() { } } }, + enabled: project != null && !isFlyArchitecture, } ) const { isSuccess: isAddonsSuccess } = useProjectAddonsQuery({ projectRef }) const { isWithinCooldownWindow, isSuccess: isCooldownSuccess } = useRemainingDurationForDiskAttributeUpdate({ projectRef, + enabled: project != null && !isFlyArchitecture, }) - const { data: diskUtil, isSuccess: isDiskUtilizationSuccess } = useDiskUtilizationQuery({ - projectRef, - }) + const { data: diskUtil, isSuccess: isDiskUtilizationSuccess } = useDiskUtilizationQuery( + { + projectRef, + }, + { enabled: project != null && !isFlyArchitecture } + ) const { data: subscription, isSuccess: isSubscriptionSuccess } = useOrgSubscriptionQuery({ orgSlug: org?.slug, }) const { data: diskAutoscaleConfig, isSuccess: isDiskAutoscaleConfigSuccess } = - useDiskAutoscaleCustomConfigQuery({ projectRef }) + useDiskAutoscaleCustomConfigQuery( + { projectRef }, + { enabled: project != null && !isFlyArchitecture } + ) /** * Handle default values */ // @ts-ignore - const { type, iops, throughput_mbps, size_gb } = data?.attributes ?? { size_gb: 0 } + const { type, iops, throughput_mbps, size_gb } = data?.attributes ?? { size_gb: 0, iops: 0 } const { growth_percent, max_size_gb, min_increment_gb } = diskAutoscaleConfig ?? {} const defaultValues = { storageType: type ?? DiskType.GP3, @@ -159,7 +161,9 @@ export function DiskManagementForm() { } const form = useForm({ - resolver: zodResolver(CreateDiskStorageSchema(defaultValues.totalSize)), + resolver: zodResolver( + CreateDiskStorageSchema(defaultValues.totalSize, project?.cloud_provider as CloudProvider) + ), defaultValues, mode: 'onBlur', reValidateMode: 'onChange', @@ -187,11 +191,12 @@ export function DiskManagementForm() { const totalSize = formState.defaultValues?.totalSize || 0 const usedPercentage = (usedSize / totalSize) * 100 - const isFlyArchitecture = project?.cloud_provider === 'FLY' const disableIopsThroughputConfig = RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3.includes(form.watch('computeSize')) && subscription?.plan.id !== 'free' + const isBranch = project?.parent_project_ref !== undefined + const disableDiskInputs = isRequestingChanges || isPlanUpgradeRequired || @@ -242,18 +247,11 @@ export function DiskManagementForm() { ) { willUpdateDiskConfiguration = true - if ( - payload.storageType === 'gp3' && - RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3.includes(payload.computeSize) - ) { - payload.provisionedIOPS = IOPS_RANGE[DiskType.GP3].min - } - await updateDiskConfiguration({ ref: projectRef, - provisionedIOPS: payload.provisionedIOPS, + provisionedIOPS: payload.provisionedIOPS!, storageType: payload.storageType, - totalSize: payload.totalSize, + totalSize: payload.totalSize!, throughput: payload.throughput, }) } @@ -301,13 +299,6 @@ export function DiskManagementForm() { } }, [isSuccess, isDiskAttributesSuccess]) - // Redirect logic incase disk and compute feature is not live yet - useEffect(() => { - if (diskAndComputeForm !== undefined && !diskAndComputeForm && projectRef) { - router.push(`/project/${projectRef}/settings/addons?panel=computeInstance`) - } - }, [diskAndComputeForm, projectRef, router]) - return (
@@ -357,7 +348,11 @@ export function DiskManagementForm() { type="default" visible={isFlyArchitecture} title="Disk configuration is not available on Fly Postgres" - description="Please contact Fly support if you need to update your disk configuration" + description={ + isBranch + ? 'Delete and recreate your Preview Branch to configure disk size. It was deployed on an older branching infrastructure.' + : 'The Fly Postgres offering is deprecated - please migrate your instance to Supabase to configure your disk.' + } /> {!isFlyArchitecture && ( <> @@ -427,11 +422,11 @@ export function DiskManagementForm() {
- } /> + (false) - const [remainingTime, setRemainingTime] = useState(0) - const [refetchInterval, setRefetchInterval] = useState(false) - - const canUpdateDiskConfiguration = useCheckPermissions(PermissionAction.UPDATE, 'projects', { - resource: { - project_id: project?.id, - }, - }) - - const { data: databases } = useReadReplicasQuery({ projectRef }) - const readReplicas = (databases ?? []).filter((db) => db.identifier !== projectRef) - - const { data, isSuccess } = useDiskAttributesQuery( - { projectRef }, - { - refetchInterval, - refetchOnWindowFocus: false, - onSuccess: (data) => { - // @ts-ignore - const { type, iops, throughput_mbps, size_gb } = data?.attributes ?? { size_gb: 0 } - const formValues = { - storageType: type, - provisionedIOPS: iops, - throughput: throughput_mbps, - totalSize: size_gb, - } - - if (!('requested_modification' in data)) { - if (refetchInterval !== false) { - form.reset(formValues) - setRefetchInterval(false) - toast.success('Disk configuration changes have been successfully applied!') - } - } - }, - } - ) - // @ts-ignore - const { type, iops, throughput_mbps, size_gb } = data?.attributes ?? { size_gb: 0 } - const isRequestingChanges = data?.requested_modification !== undefined - - const { remainingDuration: initialRemainingTime, isWithinCooldownWindow } = - useRemainingDurationForDiskAttributeUpdate({ - projectRef, - }) - - const { data: addons } = useProjectAddonsQuery({ projectRef }) - const currentCompute = (addons?.selected_addons ?? []).find( - (x: { type: string }) => x.type === 'compute_instance' - )?.variant - const maxIopsBasedOnCompute = - COMPUTE_MAX_IOPS[(currentCompute?.identifier ?? '') as keyof typeof COMPUTE_MAX_IOPS] - const maxThroughputBasedOnCompute = - COMPUTE_MAX_THROUGHPUT[ - (currentCompute?.identifier ?? '') as keyof typeof COMPUTE_MAX_THROUGHPUT - ] - - const { data: subscription } = useOrgSubscriptionQuery({ - orgSlug: org?.slug, - }) - const planId = subscription?.plan.id ?? 'free' - const isPlanUpgradeRequired = - subscription?.plan.id === 'pro' && !subscription.usage_billing_enabled - - const { data: diskUtil } = useDiskUtilizationQuery({ projectRef }) - const mainDiskUsed = Math.round(((diskUtil?.metrics.fs_used_bytes ?? 0) / GB) * 100) / 100 - - const { mutate: updateDiskConfigurationRQ, isLoading: isUpdatingDiskConfiguration } = - useUpdateDiskAttributesMutation({ - onSuccess: (_, vars) => { - toast.success( - 'Successfully requested disk configuration changes! Your changes will be applied shortly' - ) - const { ref, ...formData } = vars - setIsDialogOpen(false) - setRefetchInterval(3000) - form.reset(formData as DiskStorageSchemaType) - }, - }) - - const defaultValues = { - storageType: type || DiskType.GP3, - provisionedIOPS: iops, - throughput: throughput_mbps, - totalSize: size_gb, - computeSize: undefined, - } - const form = useForm({ - resolver: zodResolver(CreateDiskStorageSchema(defaultValues.totalSize)), - defaultValues, - mode: 'onBlur', - reValidateMode: 'onChange', - }) - - const { watch, setValue, trigger, control, formState } = form - const watchedStorageType = watch('storageType') - const watchedTotalSize = watch('totalSize') - const watchedIOPS = watch('provisionedIOPS') - const { dirtyFields } = formState // Destructure dirtyFields from formState - const isAllocatedStorageDirty = !!dirtyFields.totalSize // Check if 'allocatedStorage' is dirty - const disableInput = - isRequestingChanges || - isPlanUpgradeRequired || - isWithinCooldownWindow || - !canUpdateDiskConfiguration - - const { includedDiskGB: includedDiskGBMeta } = PLAN_DETAILS[planId] - const includedDiskGB = includedDiskGBMeta[watchedStorageType] - - const minIOPS = IOPS_RANGE[watchedStorageType]?.min ?? 0 - const maxIOPS = - watchedStorageType === 'gp3' - ? Math.min(500 * watchedTotalSize, IOPS_RANGE[DiskType.GP3].max) - : Math.min(1000 * watchedTotalSize, IOPS_RANGE[DiskType.IO2].max) - const minThroughput = - watchedStorageType === 'gp3' ? THROUGHPUT_RANGE[watchedStorageType]?.min ?? 0 : 0 - const maxThroughput = - watchedStorageType === 'gp3' - ? Math.min(0.25 * watchedIOPS, THROUGHPUT_RANGE[DiskType.GP3].max) - : undefined - - const onSubmit = async (data: DiskStorageSchemaType) => { - if (projectRef === undefined) return console.error('Project ref is required') - updateDiskConfigurationRQ({ ref: projectRef, ...data }) - } - - // i hate typescript enums - // gotta clean this up but it allows me to remove type casts for now - function getEnumFromFormValue(value: string) { - if (value === 'io2') return DiskType.IO2 - if (value === 'gp3') return DiskType.GP3 - return DiskType.IO2 - } - - const newStorageType = form.getValues('storageType') - const diskSizePrice = calculateDiskSizePrice({ - planId, - oldSize: form.formState.defaultValues?.totalSize || 0, - oldStorageType: getEnumFromFormValue(form.formState.defaultValues?.storageType || 'io2'), - newSize: form.getValues('totalSize'), - newStorageType: getEnumFromFormValue(newStorageType), - }) - - 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) - }, [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]) - - useEffect(() => { - if (remainingTime <= 0) return - - const timer = setInterval(() => { - setRemainingTime(Math.max(0, remainingTime - 1)) - }, 1000) - - return () => clearInterval(timer) - }, [remainingTime]) - - if (diskAndComputeFormEnabled) { - return ( -
- - - - Go to Compute and Disk - - - } - /> -
- ) - } - - if (planId === 'free') { - return ( -
- - - - - Disk size configuration is not available for projects on the Free Plan - - -

- If you are intending to use more than 500MB of disk space, then you will need to - upgrade to at least the Pro Plan. -

- -
-
-
- ) - } return (
@@ -336,470 +15,22 @@ export function DiskManagementPanelForm() { title="Disk Management" docsUrl="https://supabase.com/docs/guides/platform/database-size#disk-management" /> - - -
- - - ( - - - { - 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} - > - - - -
- General Purpose SSD{' '} -
- - gp3 - -
-
-

- gp3 provides a balance between price and performance -

-
- } - /> - - - - - -
- Provisioned IOPS SSD{' '} -
- - io2 - -
-
-

- io2 offers high IOPS for mission-critical applications. -

-
- } - /> - - - - - - )} - /> - ( - -
- {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 is suitable for applications requiring high throughput." - > -
-
- - { - setValue('provisionedIOPS', e.target.valueAsNumber, { - shouldDirty: true, - shouldValidate: true, - }) - }} - /> - -
- IOPS -
-
- -
- - )} - /> - - {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 && ( - - )} - - } - > -
-
- - { - setValue('throughput', e.target.valueAsNumber, { - shouldDirty: true, - shouldValidate: true, - }) - }} - className="flex-grow font-mono rounded-r-none max-w-32" - disabled={disableInput || watchedStorageType === 'io2'} - /> - -
- - MB/s - -
-
- -
-
- )} - /> -
- )} -
- - - - ( - - {includedDiskGB > 0 && - `Your plan includes ${includedDiskGB} GB of disk size for ${watchedStorageType}.`} - {field.value < size_gb && ( - - } - > - - - )} - - } - > -
-
- - e.currentTarget.blur()} - onChange={(e) => { - setValue('totalSize', e.target.valueAsNumber, { - shouldDirty: true, - shouldValidate: true, - }) - trigger('provisionedIOPS') - trigger('throughput') - }} - min={includedDiskGB} - /> - -
- GB -
-
- - {isAllocatedStorageDirty && ( - - - - )} - - -
-
- )} - /> -
- {/* You can add additional content in the remaining 4 columns if needed */} -
- {/* Additional content or information can go here */} -
-
- - -
-
-
- - - {isRequestingChanges ? ( - - -
-

- Disk configuration changes have been requested -

-

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

-
-
-
- ) : ( - - )} - - - - - - -
- - -
-
-
- - -
+ + + Go to Compute and Disk + + + } + /> ) } diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx index 3b36909419e..c343c57677c 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx @@ -321,17 +321,18 @@ export const DiskManagementReviewAndSubmitDialog = ({ priceTooltip={numReplicas > 0 ? replicaTooltipText : undefined} /> )} - {form.formState.defaultValues?.totalSize !== form.getValues('totalSize') && ( - 0 ? replicaTooltipText : undefined} - /> - )} + {form.formState.defaultValues?.totalSize !== form.getValues('totalSize') || + (form.formState.defaultValues?.storageType !== form.getValues('storageType') && ( + 0 ? replicaTooltipText : undefined} + /> + ))} {form.formState.defaultValues?.growthPercent !== form.getValues('growthPercent') && ( diff --git a/apps/studio/components/interfaces/DiskManagement/fields/DiskSizeField.tsx b/apps/studio/components/interfaces/DiskManagement/fields/DiskSizeField.tsx index 1fe4709a778..440f540fcb6 100644 --- a/apps/studio/components/interfaces/DiskManagement/fields/DiskSizeField.tsx +++ b/apps/studio/components/interfaces/DiskManagement/fields/DiskSizeField.tsx @@ -43,7 +43,10 @@ export function DiskSizeField({ isLoading: isLoadingDiskAttributes, error: diskAttributesError, isError: isDiskAttributesError, - } = useDiskAttributesQuery({ projectRef }) + } = useDiskAttributesQuery( + { projectRef }, + { enabled: project && project.cloud_provider !== 'FLY' } + ) const { data: subscription, error: subscriptionError, @@ -55,9 +58,12 @@ export function DiskSizeField({ data: diskUtil, error: diskUtilError, isError: isDiskUtilizationError, - } = useDiskUtilizationQuery({ - projectRef: projectRef, - }) + } = useDiskUtilizationQuery( + { + projectRef: projectRef, + }, + { enabled: project && project.cloud_provider !== 'FLY' } + ) const error = subscriptionError || diskUtilError || diskAttributesError const isError = isSubscriptionError || isDiskUtilizationError || isDiskAttributesError diff --git a/apps/studio/components/interfaces/DiskManagement/fields/StorageTypeField.tsx b/apps/studio/components/interfaces/DiskManagement/fields/StorageTypeField.tsx index dc5f70e81fd..db30e3931ce 100644 --- a/apps/studio/components/interfaces/DiskManagement/fields/StorageTypeField.tsx +++ b/apps/studio/components/interfaces/DiskManagement/fields/StorageTypeField.tsx @@ -54,13 +54,23 @@ export function StorageTypeField({ form, disableInput }: StorageTypeFieldProps) /** * Set default IOPS if not dirty * This is to ensure that the IOPS is set to the minimum value if the user has not changed it + * + * Only set it if the current configured IOPS is smaller than the min IOPS, otherwise leave at same IOPS */ if (e === 'gp3') { - if (!form.getFieldState('provisionedIOPS').isDirty) { + if ( + !form.getFieldState('provisionedIOPS').isDirty && + (!form.getValues('provisionedIOPS') || + form.getValues('provisionedIOPS') < DISK_LIMITS[DiskType.GP3].minIops) + ) { form.setValue('provisionedIOPS', DISK_LIMITS[DiskType.GP3].minIops) } } else { - if (!form.getFieldState('provisionedIOPS').isDirty) { + if ( + !form.getFieldState('provisionedIOPS').isDirty && + (!form.getValues('provisionedIOPS') || + form.getValues('provisionedIOPS') < DISK_LIMITS[DiskType.IO2].minIops) + ) { form.setValue('provisionedIOPS', DISK_LIMITS[DiskType.IO2].minIops) } } diff --git a/apps/studio/components/interfaces/DiskManagement/fields/ThroughputField.tsx b/apps/studio/components/interfaces/DiskManagement/fields/ThroughputField.tsx index 87e35699be8..1aa93f0df82 100644 --- a/apps/studio/components/interfaces/DiskManagement/fields/ThroughputField.tsx +++ b/apps/studio/components/interfaces/DiskManagement/fields/ThroughputField.tsx @@ -12,9 +12,9 @@ import { calculateThroughputPrice } from '../DiskManagement.utils' import { BillingChangeBadge } from '../ui/BillingChangeBadge' import { COMPUTE_BASELINE_THROUGHPUT, + DISK_LIMITS, DiskType, RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3, - THROUGHPUT_RANGE, } from '../ui/DiskManagement.constants' import { DiskManagementThroughputReadReplicas } from '../ui/DiskManagementReadReplicas' import FormMessage from '../ui/FormMessage' @@ -53,9 +53,13 @@ export function ThroughputField({ form, disableInput }: ThroughputFieldProps) { } 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 + const { minThroughput, maxThroughput } = DISK_LIMITS[DiskType.GP3] + if ( + !currentThroughput || + currentThroughput < minThroughput || + currentThroughput > maxThroughput + ) { + setValue('throughput', minThroughput) // Reset to default if undefined or out of bounds } } }, [watchedStorageType, watchedTotalSize, setValue, form]) diff --git a/apps/studio/components/interfaces/DiskManagement/ui/DiskManagement.constants.tsx b/apps/studio/components/interfaces/DiskManagement/ui/DiskManagement.constants.tsx index 734dc4f28cc..7409aa2dbad 100644 --- a/apps/studio/components/interfaces/DiskManagement/ui/DiskManagement.constants.tsx +++ b/apps/studio/components/interfaces/DiskManagement/ui/DiskManagement.constants.tsx @@ -15,15 +15,6 @@ export const DISK_AUTOSCALE_CONFIG_DEFAULTS = { maxSizeGb: 60000, } -export const IOPS_RANGE = { - [DiskType.GP3]: { min: 3000, max: 16000 }, - [DiskType.IO2]: { min: 100, max: 256000 }, -} - -export const THROUGHPUT_RANGE = { - [DiskType.GP3]: { min: 125, max: 1000 }, -} - export const DISK_PRICING = { [DiskType.GP3]: { storage: 0.125, // per GB per month @@ -50,7 +41,7 @@ export const DISK_LIMITS = { [DiskType.IO2]: { minStorage: 4, maxStorage: 61440, - minIops: 100, + minIops: 1500, maxIops: 256000, includedIops: 0, includedThroughput: 0, diff --git a/apps/studio/components/interfaces/DiskManagement/ui/DiskManagementReadReplicas.tsx b/apps/studio/components/interfaces/DiskManagement/ui/DiskManagementReadReplicas.tsx index ebb9d10ea07..ed2fd756bd1 100644 --- a/apps/studio/components/interfaces/DiskManagement/ui/DiskManagementReadReplicas.tsx +++ b/apps/studio/components/interfaces/DiskManagement/ui/DiskManagementReadReplicas.tsx @@ -33,8 +33,8 @@ export const DiskManagementDiskSizeReadReplicas = ({ const { data: databases } = useReadReplicasQuery({ projectRef }) const readReplicas = (databases ?? []).filter((db) => db.identifier !== projectRef) - const beforePrice = totalSize * DISK_PRICING[oldStorageType]?.storage ?? 0 - const afterPrice = newTotalSize * DISK_PRICING[newStorageType]?.storage ?? 0 + const beforePrice = totalSize * DISK_PRICING[oldStorageType]?.storage + const afterPrice = newTotalSize * DISK_PRICING[newStorageType]?.storage if (readReplicas.length === 0) return null @@ -146,9 +146,9 @@ export const DiskManagementIOPSReadReplicas = ({ const readReplicas = (databases ?? []).filter((db) => db.identifier !== projectRef) const beforePrice = - (oldIOPS - DISK_LIMITS[oldStorageType]?.includedIops) * DISK_PRICING[oldStorageType]?.iops ?? 0 + (oldIOPS - DISK_LIMITS[oldStorageType]?.includedIops) * DISK_PRICING[oldStorageType]?.iops const afterPrice = - (newIOPS - DISK_LIMITS[newStorageType]?.includedIops) * DISK_PRICING[newStorageType]?.iops ?? 0 + (newIOPS - DISK_LIMITS[newStorageType]?.includedIops) * DISK_PRICING[newStorageType]?.iops if (readReplicas.length === 0) return null @@ -205,12 +205,12 @@ export const DiskManagementThroughputReadReplicas = ({ const beforePrice = oldStorageType === DiskType.GP3 ? (oldThroughput - DISK_LIMITS[oldStorageType].includedThroughput) * - DISK_PRICING[oldStorageType]?.throughput ?? 0 + DISK_PRICING[oldStorageType]?.throughput : 0 const afterPrice = newStorageType === DiskType.GP3 ? (newThroughput - DISK_LIMITS[newStorageType].includedThroughput) * - DISK_PRICING[newStorageType]?.throughput ?? 0 + DISK_PRICING[newStorageType]?.throughput : 0 if (readReplicas.length === 0) return null diff --git a/apps/studio/components/interfaces/Settings/Addons/Addons.tsx b/apps/studio/components/interfaces/Settings/Addons/Addons.tsx index 9951c1138d5..a1d50b6d2cd 100644 --- a/apps/studio/components/interfaces/Settings/Addons/Addons.tsx +++ b/apps/studio/components/interfaces/Settings/Addons/Addons.tsx @@ -40,7 +40,6 @@ import { getDatabaseMajorVersion, getSemanticVersion } from 'lib/helpers' import { useAddonsPagePanel } from 'state/addons-page' import { Alert, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui' import { ComputeBadge } from 'ui-patterns/ComputeBadge' -import ComputeInstanceSidePanel from './ComputeInstanceSidePanel' import CustomDomainSidePanel from './CustomDomainSidePanel' import IPv4SidePanel from './IPv4SidePanel' import PITRSidePanel from './PITRSidePanel' @@ -61,7 +60,6 @@ const Addons = () => { const computeSizeChangesDisabled = useFlag('disableComputeSizeChanges') const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate') - const diskAndComputeFormEnabled = useFlag('diskAndComputeForm') const hasHipaaAddon = subscriptionHasHipaaAddon(subscription) @@ -234,38 +232,19 @@ const Addons = () => { )} - {!diskAndComputeFormEnabled ? ( - - - - ) : ( - - - Go to Compute and Disk - - - } - /> - )} + } + /> {Number(mostRecentRemainingIOBudget?.disk_io_budget) === 0 ? ( {

No. of pooler connections

{meta?.connections_pooler ?? '-'}

- {!diskAndComputeFormEnabled && ( - <> -
- -
-

- Max Disk Throughput -

- -
- -

- {meta?.max_disk_io_mbs?.toLocaleString() ?? '-'} Mbps -

-
-
- -
-

- Baseline Disk Throughput -

- -
- -

- {meta?.baseline_disk_io_mbs?.toLocaleString() ?? '-'} Mbps -

-
- - )} @@ -658,7 +599,6 @@ const Addons = () => { )} - diff --git a/apps/studio/components/interfaces/Settings/Addons/ComputeInstanceSidePanel.tsx b/apps/studio/components/interfaces/Settings/Addons/ComputeInstanceSidePanel.tsx deleted file mode 100644 index 838e438022e..00000000000 --- a/apps/studio/components/interfaces/Settings/Addons/ComputeInstanceSidePanel.tsx +++ /dev/null @@ -1,477 +0,0 @@ -import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useQueryClient } from '@tanstack/react-query' -import { ExternalLink, Info } from 'lucide-react' -import Link from 'next/link' -import { useRouter } from 'next/router' -import { useEffect, useMemo, useState } from 'react' -import { toast } from 'sonner' - -import { useParams } from 'common' -import { - useIsProjectActive, - useProjectContext, -} from 'components/layouts/ProjectLayout/ProjectContext' -import { setProjectStatus } from 'data/projects/projects-query' -import { MAX_REPLICAS_BELOW_XL, useReadReplicasQuery } from 'data/read-replicas/replicas-query' -import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' -import { useProjectAddonRemoveMutation } from 'data/subscriptions/project-addon-remove-mutation' -import { useProjectAddonUpdateMutation } from 'data/subscriptions/project-addon-update-mutation' -import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' -import type { AddonVariantId, ProjectAddonVariantMeta } from 'data/subscriptions/types' -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 { INSTANCE_MICRO_SPECS, PROJECT_STATUS } from 'lib/constants' -import { useAddonsPagePanel } from 'state/addons-page' -import { - Alert, - Alert_Shadcn_, - AlertDescription_Shadcn_, - AlertTitle_Shadcn_, - Badge, - Button, - Modal, - Radio, - SidePanel, - Tooltip, - TooltipContent, - TooltipTrigger, - WarningIcon, -} from 'ui' - -const ComputeInstanceSidePanel = () => { - const queryClient = useQueryClient() - const router = useRouter() - const { ref: projectRef } = useParams() - const { project: selectedProject } = useProjectContext() - const organization = useSelectedOrganization() - const computeSizeChangesDisabled = useFlag('disableComputeSizeChanges') - const diskAndComputeForm = useFlag('diskAndComputeForm') - const [showConfirmationModal, setShowConfirmationModal] = useState(false) - const canUpdateCompute = useCheckPermissions( - PermissionAction.BILLING_WRITE, - 'stripe.subscriptions' - ) - const isProjectActive = useIsProjectActive() - const { panel, setPanel, closePanel } = useAddonsPagePanel() - const visible = panel === 'computeInstance' - - const { data: databases } = useReadReplicasQuery({ projectRef }, { enabled: visible }) - const { data: addons, isLoading } = useProjectAddonsQuery({ projectRef }, { enabled: visible }) - const { data: subscription } = useOrgSubscriptionQuery( - { orgSlug: organization?.slug }, - { enabled: visible } - ) - const { mutate: updateAddon, isLoading: isUpdating } = useProjectAddonUpdateMutation({ - onSuccess: () => { - toast.success( - `Successfully updated compute instance to ${selectedCompute?.name}. Your project is currently being restarted to update its instance`, - { duration: 8000 } - ) - setProjectStatus(queryClient, projectRef!, PROJECT_STATUS.RESIZING) - closePanel() - router.push(`/project/${projectRef}`) - }, - onError: (error) => { - toast.error(`Unable to update compute instance: ${error.message}`) - }, - }) - const { mutate: removeAddon, isLoading: isRemoving } = useProjectAddonRemoveMutation({ - onSuccess: () => { - toast.success( - `Successfully updated compute instance. Your project is currently being restarted to update its instance`, - { duration: 8000 } - ) - setProjectStatus(queryClient, projectRef!, PROJECT_STATUS.RESIZING) - closePanel() - router.push(`/project/${projectRef}`) - }, - onError: (error) => { - toast.error(`Unable to update compute instance: ${error.message}`) - }, - }) - - const [selectedOption, setSelectedOption] = useState('') - - const isSubmitting = isUpdating || isRemoving - const projectId = selectedProject?.id - const cpuArchitecture = getCloudProviderArchitecture(selectedProject?.cloud_provider) - const selectedAddons = addons?.selected_addons ?? [] - const availableAddons = useMemo(() => { - return addons?.available_addons ?? [] - }, [addons]) - - const isFreePlan = subscription?.plan?.id === 'free' - const subscriptionCompute = selectedAddons.find((addon) => addon.type === 'compute_instance') - const pitrAddon = selectedAddons.find((addon) => addon.type === 'pitr') - - const hasMicroOptionFromApi = useMemo(() => { - return ( - availableAddons.find((addon) => addon.type === 'compute_instance')?.variants ?? [] - ).some((variant) => variant.identifier === 'ci_micro') - }, [availableAddons]) - - const availableOptions = useMemo(() => { - const computeOptions = - availableAddons - .find((addon) => addon.type === 'compute_instance') - ?.variants.filter((option) => { - if (!selectedProject?.cloud_provider) { - return true - } - - const meta = option.meta as ProjectAddonVariantMeta - - return ( - !meta.supported_cloud_providers || - meta.supported_cloud_providers.includes(selectedProject.cloud_provider) - ) - }) ?? [] - - // Backwards comp until API is deployed - if (!hasMicroOptionFromApi) { - // Unshift to push to start of array - computeOptions.unshift({ - identifier: 'ci_micro', - name: 'Micro', - price_description: '$0.01344/hour (~$10/month)', - price: 0.01344, - price_interval: 'hourly', - price_type: 'usage', - // @ts-ignore API types it as Record - meta: { - cpu_cores: INSTANCE_MICRO_SPECS.cpu_cores, - cpu_dedicated: INSTANCE_MICRO_SPECS.cpu_dedicated, - memory_gb: INSTANCE_MICRO_SPECS.memory_gb, - baseline_disk_io_mbs: INSTANCE_MICRO_SPECS.baseline_disk_io_mbs, - max_disk_io_mbs: INSTANCE_MICRO_SPECS.max_disk_io_mbs, - connections_direct: INSTANCE_MICRO_SPECS.connections_direct, - connections_pooler: INSTANCE_MICRO_SPECS.connections_pooler, - } as ProjectAddonVariantMeta, - }) - } - - return computeOptions - }, [availableAddons, hasMicroOptionFromApi]) - - const defaultInstanceSize = useMemo(() => { - if (!selectedProject) return '' - - return selectedProject.infra_compute_size === 'nano' ? 'ci_nano' : 'ci_micro' - }, [selectedProject]) - - const selectedCompute = availableOptions.find((option) => option.identifier === selectedOption) - const hasChanges = - selectedOption !== (subscriptionCompute?.variant.identifier ?? defaultInstanceSize) - const numReadReplicas = (databases ?? []).filter((db) => db.identifier !== projectRef).length - const hasReadReplicas = numReadReplicas > 0 - - const isDowngradingToBelowSmall = ['ci_micro', 'ci_nano'].includes(selectedOption) - const blockDowngradeDueToPitr = pitrAddon !== undefined && isDowngradingToBelowSmall && hasChanges - const hasMoreThanTwoReplicasForXLAndAbove = - numReadReplicas > MAX_REPLICAS_BELOW_XL && - ['ci_micro', 'ci_small', 'ci_medium', 'ci_large'].includes(selectedOption) - const blockDowngradeDueToReadReplicas = - hasChanges && - hasReadReplicas && - (isDowngradingToBelowSmall || hasMoreThanTwoReplicasForXLAndAbove) - - const onConfirmUpdateComputeInstance = async () => { - if (!projectRef) return console.error('Project ref is required') - if (!projectId) return console.error('Project ID is required') - if (computeSizeChangesDisabled) - toast.error( - 'Compute size changes are currently disabled - our engineers are working on a fix.' - ) - - // Temporary backwards compatibility fix - if ( - !hasMicroOptionFromApi && - selectedOption === 'ci_micro' && - subscriptionCompute !== undefined - ) { - removeAddon({ projectRef, variant: subscriptionCompute.variant.identifier }) - } else { - updateAddon({ - projectRef, - type: 'compute_instance', - variant: selectedOption as AddonVariantId, - }) - } - } - - useEffect(() => { - if (visible) { - if (subscriptionCompute !== undefined) { - setSelectedOption(subscriptionCompute.variant.identifier) - } else { - setSelectedOption(defaultInstanceSize) - } - } - }, [visible, isLoading]) - - useEffect(() => { - if (visible && diskAndComputeForm) { - router.push(`/project/${projectRef}/settings/compute-and-disk`) - } - }, [visible, diskAndComputeForm, router, projectRef]) - - return ( - <> - setShowConfirmationModal(true)} - loading={isLoading} - disabled={ - isFreePlan || - isLoading || - !hasChanges || - blockDowngradeDueToPitr || - blockDowngradeDueToReadReplicas || - !canUpdateCompute || - !isProjectActive - } - tooltip={ - isFreePlan - ? 'Unable to update compute instance on a Free Plan' - : !isProjectActive - ? 'Unable to update compute as project is currently not active' - : !canUpdateCompute - ? 'You do not have permission to update compute instance' - : undefined - } - header={ -
-

Change project compute size

- -
- } - > - -
- {selectedProject?.infra_compute_size === 'nano' && subscription?.plan.id !== 'free' && ( - - - Free compute upgrade to Micro - - Paid Plans include a free upgrade to Micro compute. Your project is ready to - upgrade for no additional charges. - - - )} - -

- For the database, compute size refers to the amount of CPU and memory allocated to the - database instance. -

- -
- {isFreePlan && ( - - - View available plans - - - } - > - Upgrade your plan to change the compute size of your project - - )} - Choose the compute size you want to use

} - onChange={(event: any) => setSelectedOption(event.target.value)} - > - {availableOptions.map((option) => ( - -
-
-

- {option.name}{' '} - {(subscriptionCompute?.variant.identifier === option.identifier || - (!subscriptionCompute && - option.identifier === - `ci_${selectedProject?.infra_compute_size}`)) && ( - Current - )} - {selectedProject?.infra_compute_size === 'nano' && - option.identifier === 'ci_micro' && Free Upgrade} -

-
-
-

- {option.meta?.memory_gb ?? 0} GB memory -

-

- {option.meta?.cpu_cores ?? 0}-core {cpuArchitecture} CPU ( - {option.meta?.cpu_dedicated ? 'Dedicated' : 'Shared'}) -

-
-
- - {/* Price needs to be exact here */}${option.price} - - - {' '} - / {option.price_interval === 'monthly' ? 'month' : 'hour'} - -
- {option.price_interval === 'hourly' && ( - - - - - - ${Number(option.price * 672).toFixed(0)} - $ - {Number(option.price * 744).toFixed(0)} per month - - - )} -
-
-
-
- ))} -
-
- - {hasChanges && ( -

- There are no immediate charges when changing compute. Compute Hours are a - usage-based item and you're billed at the end of your billing cycle based on your - compute usage. Read more about{' '} - - Compute Billing - - . -

- )} - - {hasChanges && !blockDowngradeDueToPitr && !blockDowngradeDueToReadReplicas && ( - - - - Your project will need to be restarted when changing its compute size - - - Your project will be unavailable for up to 2 minutes while the changes take place. - - {hasReadReplicas && ( - - The compute sizes for all read replicas{' '} - on your project will also be changed to the {selectedCompute?.name} size as - well. - - )} - - )} - - {blockDowngradeDueToReadReplicas ? ( - - - - {hasMoreThanTwoReplicasForXLAndAbove && selectedOption !== 'ci_micro' - ? `Unable to downgrade as project has more than ${MAX_REPLICAS_BELOW_XL} read replicas` - : 'Unable to downgrade as project has active read replicas'} - - - {hasMoreThanTwoReplicasForXLAndAbove && selectedOption !== 'ci_micro' - ? `You can only have up to ${MAX_REPLICAS_BELOW_XL} read replicas for compute sizes below XL, as such you will need to remove at least ${numReadReplicas - MAX_REPLICAS_BELOW_XL} read replica${numReadReplicas - MAX_REPLICAS_BELOW_XL > 1 ? 's' : ''} before downgrading your compute.` - : 'The minimum compute size for using read replicas is the Small Compute. You need to remove all read replicas before downgrading Compute as it requires at least a Small compute instance.'} - - - - - - ) : blockDowngradeDueToPitr ? ( - - - Disable PITR before downgrading - - You currently have PITR enabled. The minimum compute size for using PITR is the - Small Compute. - - - You need to disable PITR before downgrading Compute as it requires at least a - Small compute instance. - - - - - - ) : null} -
-
-
- - setShowConfirmationModal(false)} - onConfirm={() => onConfirmUpdateComputeInstance()} - header="Confirm to upgrade compute instance now" - > - - - Your project will be unavailable for up to 2 minutes while the changes take place. - - - - - ) -} - -export default ComputeInstanceSidePanel diff --git a/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx b/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx index 474d6d8dbef..c14c6e1ae7a 100644 --- a/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx +++ b/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx @@ -1,4 +1,5 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' +import { ExternalLink } from 'lucide-react' import { useTheme } from 'next-themes' import Link from 'next/link' import { useEffect, useState } from 'react' @@ -30,7 +31,6 @@ import { WarningIcon, cn, } from 'ui' -import { ExternalLink, AlertTriangle } from 'lucide-react' const PITR_CATEGORY_OPTIONS: { id: 'off' | 'on' @@ -296,12 +296,10 @@ const PITRSidePanel = () => { className="mb-4" title="Your project is required to minimally be on a Small compute size to enable PITR" actions={[ - , ]} > diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx index 0f89233ffb5..9c5f88dcf82 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx @@ -27,7 +27,6 @@ import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-que import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { useSelectedProject } from 'hooks/misc/useSelectedProject' -import { useFlag } from 'hooks/ui/useFlag' import { AWS_REGIONS_DEFAULT, BASE_PATH } from 'lib/constants' import { formatCurrency } from 'lib/helpers' import type { AWS_REGIONS_KEYS } from 'shared-data' @@ -71,7 +70,6 @@ const DeployNewReplicaPanel = ({ const { ref: projectRef } = useParams() const project = useSelectedProject() const org = useSelectedOrganization() - const diskAndComputeFormEnabled = useFlag('diskAndComputeForm') const { data } = useReadReplicasQuery({ projectRef }) const { data: addons, isSuccess } = useProjectAddonsQuery({ projectRef }) @@ -302,9 +300,7 @@ const DeployNewReplicaPanel = ({ href={ isFreePlan ? `/org/${org?.slug}/billing?panel=subscriptionPlan&source=deployNewReplicaPanelSmallCompute` - : diskAndComputeFormEnabled - ? `/project/${projectRef}/settings/compute-and-disk` - : `/project/${projectRef}/settings/addons?panel=computeInstance` + : `/project/${projectRef}/settings/compute-and-disk` } > {isFreePlan ? 'Upgrade to Pro' : 'Change compute size'} @@ -401,9 +397,7 @@ const DeployNewReplicaPanel = ({ href={ isFreePlan ? `/org/${org?.slug}/billing?panel=subscriptionPlan&source=deployNewReplicaPanelMaxReplicas` - : diskAndComputeFormEnabled - ? `/project/${projectRef}/settings/compute-and-disk` - : `/project/${projectRef}/settings/addons?panel=computeInstance` + : `/project/${projectRef}/settings/compute-and-disk` } > Upgrade compute size diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx index 1fca2965519..42b0d0818ca 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx @@ -17,9 +17,10 @@ import { useReadReplicasStatusesQuery, } from 'data/read-replicas/replicas-status-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useIsOrioleDb } from 'hooks/misc/useSelectedProject' import { timeout } from 'lib/helpers' +import Link from 'next/link' import { type AWS_REGIONS_KEYS } from 'shared-data' -import { useAddonsPagePanel } from 'state/addons-page' import { Button, DropdownMenu, @@ -29,7 +30,6 @@ import { DropdownMenuTrigger, cn, } from 'ui' -import ComputeInstanceSidePanel from '../../Addons/ComputeInstanceSidePanel' import DeployNewReplicaPanel from './DeployNewReplicaPanel' import DropAllReplicasConfirmationModal from './DropAllReplicasConfirmationModal' import DropReplicaConfirmationModal from './DropReplicaConfirmationModal' @@ -39,7 +39,6 @@ import { addRegionNodes, generateNodes, getDagreGraphLayout } from './InstanceCo import { LoadBalancerNode, PrimaryNode, RegionNode, ReplicaNode } from './InstanceNode' import MapView from './MapView' import { RestartReplicaConfirmationModal } from './RestartReplicaConfirmationModal' -import { useIsOrioleDb } from 'hooks/misc/useSelectedProject' const InstanceConfigurationUI = () => { const reactFlow = useReactFlow() @@ -48,7 +47,6 @@ const InstanceConfigurationUI = () => { const { ref: projectRef } = useParams() const numTransition = useRef() const { project, isLoading: isLoadingProject } = useProjectContext() - const { setPanel } = useAddonsPagePanel() const [view, setView] = useState<'flow' | 'map'>('flow') const [showDeleteAllModal, setShowDeleteAllModal] = useState(false) @@ -253,8 +251,10 @@ const InstanceConfigurationUI = () => { /> - setPanel('computeInstance')}> -
Resize databases
+ + + Resize databases + setShowDeleteAllModal(true)}> @@ -346,8 +346,6 @@ const InstanceConfigurationUI = () => { onSuccess={() => setRefetchInterval(5000)} onCancel={() => setSelectedReplicaToRestart(undefined)} /> - - ) } diff --git a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsLayout.tsx b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsLayout.tsx index 50297f9f669..ddbf124d097 100644 --- a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsLayout.tsx +++ b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsLayout.tsx @@ -46,7 +46,6 @@ const SettingsLayout = ({ title, children }: PropsWithChildren { @@ -24,7 +23,6 @@ export const generateSettingsMenu = ( const authEnabled = features?.auth ?? true const edgeFunctionsEnabled = features?.edgeFunctions ?? true const storageEnabled = features?.storage ?? true - const newDiskComputeEnabled = features?.diskAndCompute ?? false return [ { @@ -36,7 +34,7 @@ export const generateSettingsMenu = ( url: `/project/${ref}/settings/general`, items: [], }, - ...(IS_PLATFORM && newDiskComputeEnabled + ...(IS_PLATFORM ? [ { name: 'Compute and Disk', diff --git a/apps/studio/components/ui/ComputeBadgeWrapper.tsx b/apps/studio/components/ui/ComputeBadgeWrapper.tsx index 145a3097a35..ce6965f2e38 100644 --- a/apps/studio/components/ui/ComputeBadgeWrapper.tsx +++ b/apps/studio/components/ui/ComputeBadgeWrapper.tsx @@ -1,5 +1,5 @@ -import { useRouter } from 'next/router' -import { MouseEvent, useState } from 'react' +import Link from 'next/link' +import { useState } from 'react' import { components } from 'api-types' import { getAddons } from 'components/interfaces/Billing/Subscription/Subscription.utils' @@ -11,7 +11,6 @@ import { INSTANCE_MICRO_SPECS } from 'lib/constants' import { Button, HoverCard, HoverCardContent, HoverCardTrigger, Separator } from 'ui' import { ComputeBadge } from 'ui-patterns/ComputeBadge' import ShimmeringLoader from './ShimmeringLoader' -import { useFlag } from 'hooks/ui/useFlag' const Row = ({ label, stat }: { label: string; stat: React.ReactNode | string }) => { return ( @@ -32,13 +31,10 @@ interface ComputeBadgeWrapperProps { } export const ComputeBadgeWrapper = ({ project }: ComputeBadgeWrapperProps) => { - const router = useRouter() // handles the state of the hover card // once open it will fetch the addons const [open, setOpenState] = useState(false) - const diskAndComputeFormEnabled = useFlag('diskAndComputeForm') - // returns hardcoded values for infra const cpuArchitecture = getCloudProviderArchitecture(project.cloud_provider) @@ -64,19 +60,6 @@ export const ComputeBadgeWrapper = ({ project }: ComputeBadgeWrapperProps) => { (addon) => addon.name === 'Compute Instance' )?.variants - const navigateToAddons = (e: MouseEvent) => { - // These are required as the button is inside an a tag - // side note: not the best idea to nest clickables 🥲 - e.preventDefault() - e.stopPropagation() - - if (diskAndComputeFormEnabled) { - router.push(`/project/${project?.ref}/settings/compute-and-disk`) - } else { - router.push(`/project/${project?.ref}/settings/addons?panel=computeInstance`) - } - } - const highestComputeAvailable = availableCompute?.[availableCompute.length - 1].identifier const isHighestCompute = @@ -95,77 +78,82 @@ export const ComputeBadgeWrapper = ({ project }: ComputeBadgeWrapperProps) => { if (!project?.infra_compute_size) return null return ( - <> - setOpenState(!open)} openDelay={280}> - - - - -
Compute size
- -
-
- -
-
- {isLoading ? ( - <> -
- - -
- - ) : ( - <> -
- {meta !== undefined ? ( - <> - - - - ) : ( - <> - {/* meta is only undefined for nano sized compute */} - - - - )} -
- - )} -
+ setOpenState(!open)} openDelay={280}> + e.stopPropagation()}> + + + + + e.stopPropagation()} + > +
Compute size
+ +
+
+
- {(!isHighestCompute || isEligibleForFreeUpgrade) && ( - <> - -
-
-

- {isEligibleForFreeUpgrade - ? 'Free upgrade to Micro available' - : 'Unlock more compute'} -

-

- {isEligibleForFreeUpgrade - ? 'Paid plans include a free upgrade to Micro compute.' - : 'Scale your project up to 64 cores and 256 GB RAM.'} -

+
+ {isLoading ? ( + <> +
+ +
-
- + + ) : ( + <> +
+ {meta !== undefined ? ( + <> + + + + ) : ( + <> + {/* meta is only undefined for nano sized compute */} + + + + )}
+ + )} +
+
+ {(!isHighestCompute || isEligibleForFreeUpgrade) && ( + <> + +
+
+

+ {isEligibleForFreeUpgrade + ? 'Free upgrade to Micro available' + : 'Unlock more compute'} +

+

+ {isEligibleForFreeUpgrade + ? 'Paid plans include a free upgrade to Micro compute.' + : 'Scale your project up to 64 cores and 256 GB RAM.'} +

- - )} - - - +
+ +
+
+ + )} + + ) } diff --git a/apps/studio/components/ui/UpgradeToPro.tsx b/apps/studio/components/ui/UpgradeToPro.tsx index b1979f74866..3854dcfbc15 100644 --- a/apps/studio/components/ui/UpgradeToPro.tsx +++ b/apps/studio/components/ui/UpgradeToPro.tsx @@ -14,7 +14,7 @@ interface UpgradeToProProps { icon?: ReactNode primaryText: string secondaryText: string - addon?: 'pitr' | 'customDomain' | 'computeInstance' + addon?: 'pitr' | 'customDomain' buttonText?: string source?: string disabled?: boolean diff --git a/apps/studio/data/config/disk-attributes-query.ts b/apps/studio/data/config/disk-attributes-query.ts index 1e964bcb374..84c55f721cf 100644 --- a/apps/studio/data/config/disk-attributes-query.ts +++ b/apps/studio/data/config/disk-attributes-query.ts @@ -47,10 +47,15 @@ export const useDiskAttributesQuery = ( export const useRemainingDurationForDiskAttributeUpdate = ({ projectRef, + enabled = true, }: { projectRef?: string + enabled?: boolean }) => { - const { data, isLoading, isError, isSuccess, error } = useDiskAttributesQuery({ projectRef }) + const { data, isLoading, isError, isSuccess, error } = useDiskAttributesQuery( + { projectRef }, + { enabled } + ) const lastModifiedAtString = dayjs(data?.last_modified_at ?? '').utc() const secondsFromNow = Math.max( diff --git a/apps/studio/hooks/ui/useFlag.ts b/apps/studio/hooks/ui/useFlag.ts index 1a8f85b6047..0b2416ef627 100644 --- a/apps/studio/hooks/ui/useFlag.ts +++ b/apps/studio/hooks/ui/useFlag.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/nextjs' import { useFeatureFlags } from 'common' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' -import { useSelectedProject } from 'hooks/misc/useSelectedProject' import { trackFeatureFlag } from 'lib/posthog' const isObjectEmpty = (obj: Object) => { @@ -10,16 +9,10 @@ const isObjectEmpty = (obj: Object) => { } export function useFlag(name: string) { - const project = useSelectedProject() const flagStore = useFeatureFlags() const store = flagStore.configcat - // Temporary override as Fly projects are cant seem to upgrade their compute with the new disk UI - if (name === 'diskAndComputeForm' && project?.cloud_provider === 'FLY') { - return false - } - if (!isObjectEmpty(store) && store[name] === undefined) { console.error(`Flag key "${name}" does not exist in ConfigCat flag store`) return false diff --git a/apps/studio/next.config.js b/apps/studio/next.config.js index c2e729d23fa..6ee5f671c8c 100644 --- a/apps/studio/next.config.js +++ b/apps/studio/next.config.js @@ -280,7 +280,7 @@ const nextConfig = { value: 'computeInstance', }, ], - destination: '/project/:ref/settings/addons?panel=computeInstance', + destination: '/project/:ref/settings/compute-and-disk', permanent: true, }, { diff --git a/apps/studio/state/addons-page.ts b/apps/studio/state/addons-page.ts index 4c15d268fa1..00cc0365a33 100644 --- a/apps/studio/state/addons-page.ts +++ b/apps/studio/state/addons-page.ts @@ -1,6 +1,6 @@ import { useUrlState } from 'hooks/ui/useUrlState' -export const ADDONS_PANEL_KEYS_ARRAY = ['computeInstance', 'pitr', 'customDomain', 'ipv4'] as const +export const ADDONS_PANEL_KEYS_ARRAY = ['pitr', 'customDomain', 'ipv4'] as const export type ADDONS_PANEL_KEYS = undefined | (typeof ADDONS_PANEL_KEYS_ARRAY)[number]