diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx index 0b8ab1f307f..7a9a56becf7 100644 --- a/apps/studio/pages/new/[slug].tsx +++ b/apps/studio/pages/new/[slug].tsx @@ -15,6 +15,7 @@ import { useDefaultRegionQuery } from 'data/misc/get-default-region-query' import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query' import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { + DbInstanceSize, ProjectCreateVariables, useProjectCreateMutation, } from 'data/projects/project-create-mutation' @@ -49,13 +50,111 @@ import { SelectTrigger_Shadcn_, SelectValue_Shadcn_, Select_Shadcn_, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, } from 'ui' import { Input } from 'ui-patterns/DataInputs/Input' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { z } from 'zod' +import { InfoTooltip } from 'ui-patterns/info-tooltip' +import { useProjectsQuery } from 'data/projects/projects-query' +import { PopoverSeparator } from '@ui/components/shadcn/ui/popover' type DesiredInstanceSize = components['schemas']['DesiredInstanceSize'] +const sizes: DesiredInstanceSize[] = [ + 'micro', + 'small', + 'medium', + 'large', + 'xlarge', + '2xlarge', + '4xlarge', + '8xlarge', + '12xlarge', + '16xlarge', +] + +const instanceSizeSpecs: Record< + DesiredInstanceSize, + { label: string; ram: string; cpu: string; priceHourly: number; priceMonthly: number } +> = { + micro: { + label: 'Micro', + ram: '1 GB', + cpu: '2-core ARM', + priceHourly: 0.01344, + priceMonthly: 10, + }, + small: { + label: 'Small', + ram: '2 GB', + cpu: '2-core ARM', + priceHourly: 0.0206, + priceMonthly: 15, + }, + medium: { + label: 'Medium', + ram: '4 GB', + cpu: '2-core ARM', + priceHourly: 0.0822, + priceMonthly: 60, + }, + large: { + label: 'Large', + ram: '8 GB', + cpu: '2-core ARM', + priceHourly: 0.1517, + priceMonthly: 110, + }, + xlarge: { + label: 'XL', + ram: '16 GB', + cpu: '4-core ARM', + priceHourly: 0.2877, + priceMonthly: 210, + }, + '2xlarge': { + label: '2XL', + ram: '32 GB', + cpu: '8-core ARM', + priceHourly: 0.562, + priceMonthly: 410, + }, + '4xlarge': { + label: '4XL', + ram: '64 GB', + cpu: '16-core ARM', + priceHourly: 1.32, + priceMonthly: 960, + }, + '8xlarge': { + label: '8XL', + ram: '128 GB', + cpu: '32-core ARM', + priceHourly: 2.562, + priceMonthly: 1870, + }, + '12xlarge': { + label: '12XL', + ram: '192 GB', + cpu: '48-core ARM', + priceHourly: 3.836, + priceMonthly: 2800, + }, + '16xlarge': { + label: '16XL', + ram: '256 GB', + cpu: '64-core ARM', + priceHourly: 5.12, + priceMonthly: 3730, + }, +} + const Wizard: NextPageWithLayout = () => { const router = useRouter() const { slug } = useParams() @@ -70,6 +169,13 @@ const Wizard: NextPageWithLayout = () => { const { data: organizations, isSuccess: isOrganizationsSuccess } = useOrganizationsQuery() const currentOrg = organizations?.find((o: any) => o.slug === slug) + + const { data: allProjects } = useProjectsQuery({}) + const organizationProjects = + allProjects?.filter( + (project) => project.organization_id === currentOrg?.id && project.status !== 'paused' + ) ?? [] + const { data: orgSubscription } = useOrgSubscriptionQuery({ orgSlug: slug, }) @@ -121,85 +227,6 @@ const Wizard: NextPageWithLayout = () => { setPasswordStrengthMessage(message) } - const sizes: DesiredInstanceSize[] = [ - 'micro', - 'small', - 'medium', - 'large', - 'xlarge', - '2xlarge', - '4xlarge', - '8xlarge', - '12xlarge', - '16xlarge', - ] - - const instanceSizeSpecs: Record< - DesiredInstanceSize, - { label: string; ram: string; cpu: string; price: string } - > = { - micro: { - label: 'Micro', - ram: '1 GB', - cpu: '2-core ARM', - price: '$0.01344/hour (~$10/month)', - }, - small: { - label: 'Small', - ram: '2 GB', - cpu: '2-core ARM', - price: '$0.0206/hour (~$15/month)', - }, - medium: { - label: 'Medium', - ram: '4 GB', - cpu: '2-core ARM', - price: '$0.0822/hour (~$60/month)', - }, - large: { - label: 'Large', - ram: '8 GB', - cpu: '2-core ARM', - price: '$0.1517/hour (~$110/month)', - }, - xlarge: { - label: 'XL', - ram: '16 GB', - cpu: '4-core ARM', - price: '$0.2877/hour (~$210/month)', - }, - '2xlarge': { - label: '2XL', - ram: '32 GB', - cpu: '8-core ARM', - price: '$0.562/hour (~$410/month)', - }, - '4xlarge': { - label: '4XL', - ram: '64 GB', - cpu: '16-core ARM', - price: '$1.32/hour (~$960/month)', - }, - '8xlarge': { - label: '8XL', - ram: '128 GB', - cpu: '32-core ARM', - price: '$2.562/hour (~$1,870/month)', - }, - '12xlarge': { - label: '12XL', - ram: '192 GB', - cpu: '48-core ARM', - price: '$3.836/hour (~$2,800/month)', - }, - '16xlarge': { - label: '16XL', - ram: '256 GB', - cpu: '64-core ARM', - price: '$5.12/hour (~$3,730/month)', - }, - } - const FormSchema = z .object({ organization: z.string({ @@ -252,6 +279,20 @@ const Wizard: NextPageWithLayout = () => { }, }) + const { instanceSize } = form.watch() + + // [kevin] This will eventually all be provided by a new API endpoint to preview and validate project creation, this is just for kaizen now + const monthlyComputeCosts = + // current project costs + organizationProjects.reduce( + (prev, acc) => prev + monthlyInstancePrice(acc.infra_compute_size), + 0 + ) + + // selected instance size + monthlyInstancePrice(instanceSize) - + // compute credits + 10 + // [Joshen] Refactor: DB Password could be a common component // used in multiple pages with repeated logic function generateStrongPassword() { @@ -330,6 +371,11 @@ const Wizard: NextPageWithLayout = () => { } }, [defaultRegionError]) + const availableComputeCredits = organizationProjects.length === 0 ? 10 : 0 + + const additionalMonthlySpend = + instanceSizeSpecs[instanceSize as DbInstanceSize]!.priceMonthly - availableComputeCredits + return (
@@ -508,7 +554,7 @@ const Wizard: NextPageWithLayout = () => { layout="horizontal" label={
- Instance Size + Compute Size
{
- - -
-

Compute Billing

- -
-
} description={ <>

- Select the size for your dedicated database. You can always - change this later. -

-

- Your organization has $10/month in Compute Credits to cover one - instance on Micro Compute or parts of any other instance size. + The size for your dedicated database. You can change this later.

} @@ -573,7 +604,8 @@ const Wizard: NextPageWithLayout = () => { {instanceSizeSpecs[option].cpu} CPU

- {instanceSizeSpecs[option].price} + ${instanceSizeSpecs[option].priceHourly}/hour (~$ + {instanceSizeSpecs[option].priceMonthly}/month)

@@ -646,6 +678,163 @@ const Wizard: NextPageWithLayout = () => { )} /> + {orgSubscription && orgSubscription.plan.id !== 'free' && ( + + + Compute Billing +
+ +
+

Docs

+ +
+ +
+ + } + layout="horizontal" + > +
+ Additional Monthly Compute Costs +
+ {organizationProjects.length > 0 ? ( + <> + ${additionalMonthlySpend} + + ) : ( + <> + + $ + { + instanceSizeSpecs[instanceSize as DbInstanceSize]! + .priceMonthly + } + + ${additionalMonthlySpend} + + )} + + + + + Project + Compute Size + Monthly Costs + + + + {organizationProjects.map((project) => ( + + {project.name} + + {instanceLabel(project.infra_compute_size)} + + + ${monthlyInstancePrice(project.infra_compute_size)} + + + ))} + + + + + {form.getValues('projectName') + ? form.getValues('projectName') + : 'New project'} + + + NEW + + + + {instanceLabel(instanceSize)} + + + ${monthlyInstancePrice(instanceSize)} + + + +
+ + + + + Compute Credits + + -$10 + + + + + + + Total Monthly Compute Costs + {/** + * API currently doesnt output replica information on the projects list endpoint. Until then, we cannot correctly calculate the costs including RRs. + * + * Will be adjusted in the future [kevin] + */} + {organizationProjects.length > 0 && ( +

+ Excluding Read replicas +

+ )} +
+ + ${monthlyComputeCosts} + +
+
+
+ +
+

+ Compute is charged usage-based whenever your billing cycle + resets. Given compute charges are hourly, your invoice will + contain "Compute Hours" for each hour a project ran on a + specific instance size. +

+ {monthlyComputeCosts > 0 && ( +

+ Compute costs are applied on top of your subscription plan + costs. +

+ )} +
+
+
+
+ +
+ {additionalMonthlySpend > 0 && availableComputeCredits === 0 ? ( +

+ Your monthly spend will increase, and can be more than above if you + exceed your plan's usage quota. Your organization includes $10/month + of compute credits, which you already exceed with your existing + projects. +

+ ) : additionalMonthlySpend > 0 && availableComputeCredits > 0 ? ( +

+ Your monthly spend will increase, and can be more than above if you + exceed your plan's usage quota. Your organization includes $10/month + of compute credits, which you exceed with the selected compute size. +

+ ) : ( +

+ Your monthly spend won't increase, unless you exceed your plan's + usage quota. Your organization includes $10/month of compute + credits, which cover this project. +

+ )} +
+
+
+ )} )} @@ -666,6 +855,20 @@ const Wizard: NextPageWithLayout = () => { ) } +/** + * When launching new projects, they only get assigned a compute size once successfully launched, + * this might assume wrong instance size, but only for projects being rapidly launched after one another on non-default compute sizes. + * + * Needs to be in the API in the future [kevin] + */ +const monthlyInstancePrice = (instance: string | undefined): number => { + return instanceSizeSpecs[instance as DbInstanceSize]?.priceMonthly || 10 +} + +const instanceLabel = (instance: string | undefined): string => { + return instanceSizeSpecs[instance as DbInstanceSize]?.label || 'Micro' +} + const PageLayout = withAuth(({ children }: PropsWithChildren) => { const { slug } = useParams()