diff --git a/apps/studio/components/interfaces/DiskManagement/BillingChangeBadge.tsx b/apps/studio/components/interfaces/DiskManagement/BillingChangeBadge.tsx index 231d94551f8..a9aad07ae87 100644 --- a/apps/studio/components/interfaces/DiskManagement/BillingChangeBadge.tsx +++ b/apps/studio/components/interfaces/DiskManagement/BillingChangeBadge.tsx @@ -8,6 +8,7 @@ interface BillingChangeBadgeProps { afterPrice?: number show: boolean | undefined tooltip?: string + className?: string } const BillingChangeBadge = ({ @@ -15,17 +16,21 @@ const BillingChangeBadge = ({ afterPrice, show, tooltip, + className, }: BillingChangeBadgeProps) => { return ( {beforePrice !== undefined && afterPrice !== undefined && show && ( - +
@@ -33,9 +38,14 @@ const BillingChangeBadge = ({ {formatCurrency(beforePrice)} - + {formatCurrency(afterPrice)} - +
{tooltip !== undefined && ( diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagement.utils.ts b/apps/studio/components/interfaces/DiskManagement/DiskManagement.utils.ts index a219ad188f5..4d406907e17 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagement.utils.ts +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagement.utils.ts @@ -1,4 +1,8 @@ +import { components } from 'api-types' import { DISK_LIMITS, DISK_PRICING, DiskType, PLAN_DETAILS } from './DiskManagement.constants' +import { INSTANCE_MICRO_SPECS } from 'lib/constants' +import { ProjectAddonVariantMeta } from 'data/subscriptions/types' +import { useMemo } from 'react' // Included disk size only applies to primary, not replicas export const calculateDiskSizePrice = ({ @@ -31,6 +35,29 @@ export const calculateDiskSizePrice = ({ } } +export const calculateComputeSizePrice = ({ + availableOptions, + oldComputeSize, + newComputeSize, +}: { + availableOptions: { + identifier: string + price: number + }[] + oldComputeSize: string + newComputeSize: string +}) => { + console.log('newComputeSize', newComputeSize) + + const oldPrice = availableOptions.find((x) => x.identifier === oldComputeSize)?.price * 720 + const newPrice = availableOptions.find((x) => x.identifier === newComputeSize)?.price * 720 + + return { + oldPrice, + newPrice, + } +} + // Included IOPS applies to both primary and replicas export const calculateIOPSPrice = ({ oldStorageType, @@ -109,3 +136,51 @@ export const calculateThroughputPrice = ({ } return { oldPrice: '0.00', newPrice: '0.00' } } + +export function getAvailableComputeOptions(availableAddons: any[], projectCloudProvider?: string) { + const computeOptions = + availableAddons + .find((addon) => addon.type === 'compute_instance') + ?.variants.filter((option) => { + if (!projectCloudProvider) { + return true + } + + const meta = option.meta as ProjectAddonVariantMeta + + return ( + !meta.supported_cloud_providers || + meta.supported_cloud_providers.includes(projectCloudProvider) + ) + }) ?? [] + + function hasMicroOptionFromApi() { + return ( + availableAddons.find((addon) => addon.type === 'compute_instance')?.variants ?? [] + ).some((variant: any) => variant.identifier === 'ci_micro') + } + + // 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', + 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 +} diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx index a87071c7c53..b883ea64fd8 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx @@ -29,7 +29,7 @@ import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-que import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' -import { GB } from 'lib/constants' +import { GB, INSTANCE_MICRO_SPECS } from 'lib/constants' import { Alert_Shadcn_, AlertDescription_Shadcn_, @@ -38,6 +38,7 @@ import { Button, Card, CardContent, + cn, Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, @@ -63,9 +64,11 @@ import { THROUGHPUT_RANGE, } from './DiskManagement.constants' import { + calculateComputeSizePrice, calculateDiskSizePrice, calculateIOPSPrice, calculateThroughputPrice, + getAvailableComputeOptions, } from './DiskManagement.utils' import { DiskStorageSchema, DiskStorageSchemaType } from './DiskManagementPanelSchema' import { DiskManagementPlanUpgradeRequired } from './DiskManagementPlanUpgradeRequired' @@ -80,6 +83,7 @@ import { ProjectAddonVariantMeta } from 'data/subscriptions/types' import { getCloudProviderArchitecture } from 'lib/cloudprovider-utils' import { ComputeBadge } from 'ui-patterns' import { components } from 'api-types' +import { MAX_WIDTH_CLASSES, PADDING_CLASSES, ScaffoldContainer } from 'components/layouts/Scaffold' export function DiskManagementForm() { const { project } = useProjectContext() @@ -170,54 +174,12 @@ export function DiskManagementForm() { return addons?.available_addons ?? [] }, [addons]) - const hasMicroOptionFromApi = useMemo(() => { - return ( - availableAddons.find((addon) => addon.type === 'compute_instance')?.variants ?? [] - ).some((variant) => variant.identifier === 'ci_micro') - }, [availableAddons]) + const selectedAddons = addons?.selected_addons ?? [] + const subscriptionCompute = selectedAddons.find((addon) => addon.type === 'compute_instance') const availableOptions = useMemo(() => { - const computeOptions = - availableAddons - .find((addon) => addon.type === 'compute_instance') - ?.variants.filter((option) => { - if (!project?.cloud_provider) { - return true - } - - const meta = option.meta as ProjectAddonVariantMeta - - return ( - !meta.supported_cloud_providers || - meta.supported_cloud_providers.includes(project.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]) + return getAvailableComputeOptions(availableAddons, project?.cloud_provider) + }, [availableAddons, project?.cloud_provider]) /** * Handle default values @@ -229,10 +191,9 @@ export function DiskManagementForm() { provisionedIOPS: iops, throughput: throughput_mbps, totalSize: size_gb, + computeSize: subscriptionCompute?.variant.identifier ?? 'ci_micro', } - console.log('type', type) - const form = useForm({ resolver: zodResolver(DiskStorageSchema), defaultValues, @@ -311,6 +272,12 @@ export function DiskManagementForm() { /** * Price calculations */ + + const computeSizePrice = calculateComputeSizePrice({ + availableOptions: availableOptions, + oldComputeSize: form.formState.defaultValues?.computeSize || 'ci_micro', + newComputeSize: form.getValues('computeSize'), + }) const diskSizePrice = calculateDiskSizePrice({ planId, oldSize: form.formState.defaultValues?.totalSize || 0, @@ -423,71 +390,105 @@ export function DiskManagementForm() { // return <> + const isDirty = !!Object.keys(form.formState.dirtyFields).length + return ( -
- {/* */} + +
+ + {/* {showNewDiskManagementUI ? : null} */} + - - - - {availableOptions.map((compute) => { - const cpuArchitecture = getCloudProviderArchitecture(project?.cloud_provider) + ( + <> + + +

hardware resources allocated to your postgres database

+ + } + > + { + 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'} + +
+
+ {/* */} +
+
+
+ + {compute.meta?.memory_gb ?? 0} GB memory +
+
+ + + {compute.meta?.cpu_cores ?? 0}-core {cpuArchitecture} CPU + +
+
+ {/*
{compute.meta?.cpu_dedicated ? 'Dedicated' : 'Shared'}
*/} - {/*
+ {/*
${compute.price} @@ -498,13 +499,17 @@ export function DiskManagementForm() {
*/} -
-
- } - >
- ) - })} - +
+
+ } + >
+ ) + })} +
+
+ + )} + /> {/* */} @@ -661,7 +666,24 @@ export function DiskManagementForm() { )}
} - labelOptional="Input/output operations per second. Higher IOPS is suitable for applications requiring high throughput." + labelOptional={ + <> + +

+ Input/output operations per second. Higher IOPS is suitable for applications + requiring high throughput. +

+ + } >
@@ -684,15 +706,6 @@ export function DiskManagementForm() { IOPS
- )} @@ -758,6 +771,25 @@ export function DiskManagementForm() { )} } + labelOptional={ + <> + +

+ Throughput is the amount of data that can be read or written to the disk + per second. Higher throughput is suitable for applications requiring + high throughput. +

+ + } >
@@ -779,15 +811,6 @@ export function DiskManagementForm() { MB/s
- )} @@ -910,28 +933,48 @@ export function DiskManagementForm() { )} {isPlanUpgradeRequired && } + - {/* + {/* */} - -
- - -
- {/*
*/} - {/*
*/} - - - + + + {isDirty ? ( + +
+ + + +
+
+ ) : null} +
+ + {/* */} + {/*
*/} + + ) } diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelSchema.ts b/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelSchema.ts index ca1db6987fc..48737d5ad29 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelSchema.ts +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelSchema.ts @@ -9,10 +9,11 @@ const baseSchema = z.object({ .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.string().describe('Compute size'), }) export const DiskStorageSchema = baseSchema.superRefine((data, ctx) => { - const { storageType, totalSize, provisionedIOPS, throughput } = data + const { storageType, totalSize, provisionedIOPS, throughput, computeSize } = data if (storageType === 'io2') { // Validation rules for io2 diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx index 4ac8bd80fbb..75a50a14216 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx @@ -28,13 +28,17 @@ import { import BillingChangeBadge from './BillingChangeBadge' import { DiskType } from './DiskManagement.constants' import { + calculateComputeSizePrice, calculateDiskSizePrice, calculateIOPSPrice, calculateThroughputPrice, + getAvailableComputeOptions, } from './DiskManagement.utils' import { DiskMangementCoolDownSection } from './DiskManagementCoolDownSection' import { DiskStorageSchemaType } from './DiskManagementPanelSchema' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { useMemo } from 'react' +import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' const TableHeaderRow = () => ( @@ -127,7 +131,17 @@ export const DiskManagementReviewAndSubmitDialog = ({ }: DiskSizeMeterProps) => { const { project } = useProjectContext() const org = useSelectedOrganization() + + /** + * Queries + * */ const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: org?.slug }) + const { + data: addons, + isLoading: isAddonsLoading, + error: addonsError, + isSuccess: isAddonsSuccess, + } = useProjectAddonsQuery({ projectRef: project?.ref }) const canUpdateDiskConfiguration = useCheckPermissions(PermissionAction.UPDATE, 'projects', { resource: { @@ -136,10 +150,26 @@ export const DiskManagementReviewAndSubmitDialog = ({ }) const planId = subscription?.plan.id ?? '' - const isDirty = Object.keys(form.formState.dirtyFields).length > 0 - + const isDirty = !!Object.keys(form.formState.dirtyFields).length const replicaTooltipText = `Price change includes primary database and ${numReplicas} replica${numReplicas > 1 ? 's' : ''}` + /** + * Handle compute instances + */ + const availableAddons = useMemo(() => { + return addons?.available_addons ?? [] + }, [addons]) + + const availableOptions = useMemo(() => { + return getAvailableComputeOptions(availableAddons, project?.cloud_provider) + }, [availableAddons, project?.cloud_provider]) + + const computeSizePrice = calculateComputeSizePrice({ + availableOptions, + oldComputeSize: form.formState.defaultValues?.computeSize || 'ci_micro', + newComputeSize: form.getValues('computeSize'), + }) + const diskSizePrice = calculateDiskSizePrice({ planId, oldSize: form.formState.defaultValues?.totalSize || 0, @@ -190,7 +220,7 @@ export const DiskManagementReviewAndSubmitDialog = ({ Review changes - + Review changes @@ -204,6 +234,14 @@ export const DiskManagementReviewAndSubmitDialog = ({ + {!navLayoutV2 && !hideHeader && IS_PLATFORM && } -
+
{showPausedState ? (
diff --git a/apps/studio/components/layouts/Scaffold.tsx b/apps/studio/components/layouts/Scaffold.tsx index 9497e4a1115..06943794c26 100644 --- a/apps/studio/components/layouts/Scaffold.tsx +++ b/apps/studio/components/layouts/Scaffold.tsx @@ -1,9 +1,9 @@ import React from 'react' import { cn } from 'ui' -const maxWidthClasses = 'mx-auto w-full max-w-[1200px]' -const paddingClasses = 'px-6 lg:px-14 xl:px-24 2xl:px-32' -const maxWidthClassesColumn = 'min-w-[420px]' +export const MAX_WIDTH_CLASSES = 'mx-auto w-full max-w-[1200px]' +export const PADDING_CLASSES = 'px-6 lg:px-14 xl:px-24 2xl:px-32' +export const MAX_WIDTH_CLASSES_COLUMN = 'min-w-[420px]' const ScaffoldHeader = React.forwardRef>( ({ className, ...props }, ref) => { @@ -39,7 +39,7 @@ const ScaffoldContainer = React.forwardRef<
) }) @@ -68,7 +68,7 @@ const ScaffoldColumn = React.forwardRef ) } @@ -131,7 +131,7 @@ const ScaffoldContainerLegacy = React.forwardRef<
) }) diff --git a/apps/studio/pages/project/[ref]/settings/compute-and-disk.tsx b/apps/studio/pages/project/[ref]/settings/compute-and-disk.tsx index 0e4c2cf7319..e3db08dc4c2 100644 --- a/apps/studio/pages/project/[ref]/settings/compute-and-disk.tsx +++ b/apps/studio/pages/project/[ref]/settings/compute-and-disk.tsx @@ -35,11 +35,8 @@ const AuthSettings: NextPageWithLayout = () => { - - {/* {showNewDiskManagementUI ? : null} */} - - - + + ) }