diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx index 044ae31e015a2..6005536fc8442 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx @@ -1,38 +1,28 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useQueryClient } from '@tanstack/react-query' import { isArray } from 'lodash' -import { ChevronRight, ExternalLink } from 'lucide-react' +import { ExternalLink } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { useEffect, useRef, useState } from 'react' -import toast from 'react-hot-toast' -import { billingPartnerLabel } from 'components/interfaces/Billing/Subscription/Subscription.utils' -import Table from 'components/to-be-cleaned/Table' -import AlertError from 'components/ui/AlertError' import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import InformationBox from 'components/ui/InformationBox' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query' -import { organizationKeys } from 'data/organizations/keys' -import { useOrganizationBillingSubscriptionPreview } from 'data/organizations/organization-billing-subscription-preview' import { useProjectsQuery } from 'data/projects/projects-query' import { useOrgPlansQuery } from 'data/subscriptions/org-plans-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' -import { useOrgSubscriptionUpdateMutation } from 'data/subscriptions/org-subscription-update-mutation' -import type { OrgPlan, SubscriptionTier } from 'data/subscriptions/types' +import type { OrgPlan } from 'data/subscriptions/types' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' -import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants' import { formatCurrency } from 'lib/helpers' import { pickFeatures, pickFooter, plans as subscriptionsPlans } from 'shared-data/plans' import { useOrgSettingsPageStateSnapshot } from 'state/organization-settings' -import { Button, IconCheck, IconInfo, Modal, SidePanel, cn } from 'ui' +import { Button, IconCheck, SidePanel, cn } from 'ui' import DowngradeModal from './DowngradeModal' import EnterpriseCard from './EnterpriseCard' import ExitSurveyModal from './ExitSurveyModal' import MembersExceedLimitModal from './MembersExceedLimitModal' -import PaymentMethodSelection from './PaymentMethodSelection' +import { UpgradeDowngradeModal } from './UpgradeDowngradeModal' import UpgradeSurveyModal from './UpgradeModal' const PlanUpdateSidePanel = () => { @@ -40,16 +30,13 @@ const PlanUpdateSidePanel = () => { const selectedOrganization = useSelectedOrganization() const slug = selectedOrganization?.slug - const queryClient = useQueryClient() - const originalPlanRef = useRef() const [showExitSurvey, setShowExitSurvey] = useState(false) const [showUpgradeSurvey, setShowUpgradeSurvey] = useState(false) - const [selectedPaymentMethod, setSelectedPaymentMethod] = useState() + const [showDowngradeError, setShowDowngradeError] = useState(false) const [selectedTier, setSelectedTier] = useState<'tier_free' | 'tier_pro' | 'tier_team'>() - const [usageFeesExpanded, setUsageFeesExpanded] = useState([]) const canUpdateSubscription = useCheckPermissions( PermissionAction.BILLING_WRITE, @@ -75,44 +62,13 @@ const PlanUpdateSidePanel = () => { }) const { data: plans, isLoading: isLoadingPlans } = useOrgPlansQuery({ orgSlug: slug }) const { data: membersExceededLimit } = useFreeProjectLimitCheckQuery({ slug }) - const { mutate: updateOrgSubscription, isLoading: isUpdating } = useOrgSubscriptionUpdateMutation( - { - onSuccess: () => { - toast.success(`Successfully updated subscription to ${subscriptionPlanMeta?.name}!`) - setSelectedTier(undefined) - onClose() - window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }) - setShowUpgradeSurvey(true) - }, - onError: (error) => { - toast.error(`Unable to update subscription: ${error.message}`) - }, - } - ) - const billingViaPartner = subscription?.billing_via_partner === true const billingPartner = subscription?.billing_partner - const paymentViaInvoice = subscription?.payment_method_type === 'invoice' - - const { - data: subscriptionPreview, - error: subscriptionPreviewError, - isLoading: subscriptionPreviewIsLoading, - isSuccess: subscriptionPreviewInitialized, - } = useOrganizationBillingSubscriptionPreview({ tier: selectedTier, organizationSlug: slug }) const availablePlans: OrgPlan[] = plans?.plans ?? [] const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0 const subscriptionPlanMeta = subscriptionsPlans.find((tier) => tier.id === selectedTier) - const expandUsageFee = (fee: string) => { - setUsageFeesExpanded([...usageFeesExpanded, fee]) - } - - const collapseUsageFee = (fee: string) => { - setUsageFeesExpanded(usageFeesExpanded.filter((item) => item !== fee)) - } - useEffect(() => { if (visible) { setSelectedTier(undefined) @@ -135,40 +91,6 @@ const PlanUpdateSidePanel = () => { } } - const onUpdateSubscription = async () => { - if (!slug) return console.error('org slug is required') - if (!selectedTier) return console.error('Selected plan is required') - if (!selectedPaymentMethod && !paymentViaInvoice) { - return toast.error('Please select a payment method') - } - - if (selectedPaymentMethod) { - queryClient.setQueriesData(organizationKeys.paymentMethods(slug), (prev: any) => { - if (!prev) return prev - return { - ...prev, - defaultPaymentMethodId: selectedPaymentMethod, - data: prev.data.map((pm: any) => ({ - ...pm, - is_default: pm.id === selectedPaymentMethod, - })), - } - }) - } - - // If the user is downgrading from team, should have spend cap disabled by default - const tier = - subscription?.plan?.id === 'team' && selectedTier === PRICING_TIER_PRODUCT_IDS.PRO - ? (PRICING_TIER_PRODUCT_IDS.PAYG as SubscriptionTier) - : selectedTier - - updateOrgSubscription({ slug, tier, paymentMethod: selectedPaymentMethod }) - } - - const planMeta = selectedTier - ? availablePlans.find((p) => p.id === selectedTier.split('tier_')[1]) - : null - return ( <> { projects={orgProjects} /> - setSelectedTier(undefined)} - onConfirm={onUpdateSubscription} - dialogOverlayProps={{ className: 'pointer-events-none' }} - header={`Confirm ${planMeta?.change_type === 'downgrade' ? 'downgrade' : 'upgrade'} to ${subscriptionPlanMeta?.name}`} - > - - {subscriptionPreviewError && ( - - )} - {subscriptionPreviewIsLoading && ( -
- Estimating monthly costs... - - - -
- )} - {subscriptionPreviewInitialized && ( -
- Item, - - Usage - , - Unit Price, - - Cost - , - ]} - body={ - <> - {subscriptionPreview.breakdown.map((item) => ( - <> - - - {item.breakdown && item.breakdown.length > 0 && ( -
- - } - defaultVisibility={true} - hideCollapse={true} - description={ -
-

- Each project is a dedicated server and database. Paid plans come with $10 of - Compute Credits to cover one project on the default Micro Compute size or - parts of any compute addon. Additional unpaused projects on paid plans will - incur compute usage costs starting at $10 per month, billed hourly. -

- - {subscription?.plan?.id === 'free' && ( -

- Mixing paid and non-paid projects in a single organization is not possible. - If you want projects to be on the Free Plan, use self-serve project - transfers. -

- )} - -
- - {subscription?.plan?.id === 'free' && ( - - )} -
-
- } - /> -
- )} -
- - - {!billingViaPartner ? ( -
-

- Upon clicking confirm, your monthly invoice will be adjusted and your credit card - will be charged immediately. Changing the plan resets your billing cycle and may - result in a prorated charge for previous usage. -

- -
- -
-
- ) : ( -
-

- This organization is billed through our partner{' '} - {billingPartnerLabel(billingPartner)}.{' '} - {billingPartner === 'aws' ? ( - <>The organization's credit balance will be decreased accordingly. - ) : ( - <>You will be charged by them directly. - )} -

- {billingViaPartner && - billingPartner === 'fly' && - subscriptionPreview?.plan_change_type === 'downgrade' && ( -

- Your organization will be downgraded at the end of your current billing cycle. -

- )} -
- )} -
-
+ { + setSelectedTier(undefined) + onClose() + window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }) + setShowUpgradeSurvey(true) + }} + projects={orgProjects} + /> { + const previousMonthlyPrice = previousPlan.priceMonthly + const nextMonthlyPrice = nextPlan.priceMonthly + // If the price is not a number (enterprise tier), don't render + if (typeof previousMonthlyPrice !== 'number' || typeof nextMonthlyPrice !== 'number') { + return null + } + + return ( + + +
+ + {nextPlan.name} + + Organization plan +
+
+ + + {formatCurrency(nextMonthlyPrice)} per month + + + + + {formatCurrency(previousMonthlyPrice)} + + {formatCurrency(nextMonthlyPrice)} + + +
+ ) +} + +const RunningProjectRow = ({ + project, + previousPlan, + nextPlan, + expanded, + onToggle, +}: { + project: ProjectInfo + previousPlan: PricingInformation + nextPlan: PricingInformation + expanded: boolean + onToggle: () => void +}) => { + // this corrects a situation where the customer upgraded from pro to team but the instance is still nano + const correctedComputeSize = + nextPlan.id !== 'tier_free' && project.infra_compute_size === 'nano' + ? 'micro' + : project.infra_compute_size! + + let instanceSizeBadge = {correctedComputeSize} + let instancePriceBadge = ( + <> + + {formatCurrency(instanceSizeSpecs[correctedComputeSize].priceMonthly)} + + ) + // if the user is upgrading the plan from free + if (previousPlan.id === 'tier_free' && nextPlan.id !== 'tier_free') { + instanceSizeBadge = ( + <> + Nano + Micro + + ) + + instancePriceBadge = ( + <> + + {formatCurrency(instanceSizeSpecs['nano'].priceMonthly)} + + {formatCurrency(instanceSizeSpecs['micro'].priceMonthly)} + + ) + } + // if the user is downgrading to free (shouldn't happen because downgrading to free is handled by another modal) + if (previousPlan.id !== 'tier_free' && nextPlan.id === 'tier_free') { + instanceSizeBadge = ( + <> + {project.infra_compute_size} + Nano + + ) + + instancePriceBadge = ( + <> + + {formatCurrency(instanceSizeSpecs[project.infra_compute_size!].priceMonthly)} + + {formatCurrency(instanceSizeSpecs['nano'].priceMonthly)} + + ) + } + + return ( + + +
+ + Project + {project.name} + {instanceSizeBadge} +
+
+ + + + {instancePriceBadge} + +
+ ) +} + +const SumRow = ({ + projects, + previousPlan, + nextPlan, +}: { + projects: ProjectInfo[] + previousPlan: PricingInformation + nextPlan: PricingInformation +}) => { + const projectsCost = projects + .map((project) => { + // this corrects a situation where the customer upgraded from pro to team but the instance is still nano + const correctedComputeSize = + nextPlan.id !== 'tier_free' && project.infra_compute_size === 'nano' + ? 'micro' + : project.infra_compute_size! + return instanceSizeSpecs[correctedComputeSize].priceMonthly + }) + .reduce((sum, cost) => sum + cost, 0) + + const previousMonthlyPrice = +previousPlan.priceMonthly + projectsCost - AMOUNT_OF_CREDITS + const nextMonthlyPrice = +nextPlan.priceMonthly + projectsCost - AMOUNT_OF_CREDITS + + return ( + <> + + + + + Costs before upgrade + + + {formatCurrency(previousMonthlyPrice)} + + + {/* empty row to separate the dotted lines */} + + + + + +
+ New monthly cost + (Excluding over-usage and credits) +
+
+ + + {formatCurrency(nextMonthlyPrice)} + + +
+ + ) +} + +export const UpgradeDowngradeModal = ({ + subscription, + selectedTier, + setSelectedTier, + availablePlans, + projects, + onSuccess, +}: { + selectedTier: PricingTier + setSelectedTier: Dispatch> + subscription: OrgSubscriptionData | undefined + availablePlans: OrgPlan[] + projects: ProjectInfo[] + onSuccess: () => void +}) => { + const selectedOrganization = useSelectedOrganization() + const slug = selectedOrganization?.slug + + const queryClient = useQueryClient() + + const [selectedPaymentMethod, setSelectedPaymentMethod] = useState() + const [usageFeesExpanded, setUsageFeesExpanded] = useState([]) + + const previousPlanMeta = subscriptionsPlans.find( + (tier) => tier.id === `tier_${subscription?.plan.id}` + ) + const nextPlanMeta = subscriptionsPlans.find((tier) => tier.id === selectedTier) + + const planMeta = selectedTier + ? availablePlans.find((p) => p.id === selectedTier.split('tier_')[1]) + : null + + const { mutate: updateOrgSubscription, isLoading: isUpdating } = useOrgSubscriptionUpdateMutation( + { + onSuccess: () => { + toast.success(`Successfully updated subscription to ${nextPlanMeta?.name}!`) + onSuccess() + }, + onError: (error) => { + toast.error(`Unable to update subscription: ${error.message}`) + }, + } + ) + + const billingViaPartner = subscription?.billing_via_partner === true + const billingPartner = subscription?.billing_partner + const paymentViaInvoice = subscription?.payment_method_type === 'invoice' + + const { + data: subscriptionPreview, + error: subscriptionPreviewError, + isLoading: subscriptionPreviewIsLoading, + isSuccess: subscriptionPreviewInitialized, + } = useOrganizationBillingSubscriptionPreview({ tier: selectedTier, organizationSlug: slug }) + + const onUpdateSubscription = async () => { + if (!slug) return console.error('org slug is required') + if (!selectedTier) return console.error('Selected plan is required') + if (!selectedPaymentMethod && !paymentViaInvoice) { + return toast.error('Please select a payment method') + } + + if (selectedPaymentMethod) { + queryClient.setQueriesData(organizationKeys.paymentMethods(slug), (prev: any) => { + if (!prev) return prev + return { + ...prev, + defaultPaymentMethodId: selectedPaymentMethod, + data: prev.data.map((pm: any) => ({ + ...pm, + is_default: pm.id === selectedPaymentMethod, + })), + } + }) + } + + // If the user is downgrading from team, should have spend cap disabled by default + const tier = + subscription?.plan?.id === 'team' && selectedTier === PRICING_TIER_PRODUCT_IDS.PRO + ? (PRICING_TIER_PRODUCT_IDS.PAYG as SubscriptionTier) + : selectedTier + + updateOrgSubscription({ slug, tier, paymentMethod: selectedPaymentMethod }) + } + + return ( + setSelectedTier(undefined)} + onConfirm={onUpdateSubscription} + dialogOverlayProps={{ className: 'pointer-events-none' }} + header={`Confirm ${planMeta?.change_type === 'downgrade' ? 'downgrade' : 'upgrade'} to ${nextPlanMeta?.name}`} + > +
+ {subscriptionPreviewError && ( + + )} + {subscriptionPreviewIsLoading && ( +
+ Estimating monthly costs... + + + +
+ )} + {subscriptionPreviewInitialized && ( +
+ + Item + , + + Usage + , + + Unit Price + , + + Cost + , + ]} + body={ + <> + + + {projects.map((project) => { + const expanded = usageFeesExpanded.includes(project.name) + return ( + { + if (expanded) { + setUsageFeesExpanded( + usageFeesExpanded.filter((item) => item !== project.name) + ) + } else { + setUsageFeesExpanded([...usageFeesExpanded, project.name]) + } + }} + /> + ) + })} + {/* + + Any un-paused projects will be charged for compute hours + + + + +
+ + Project + Project name + + + Paused + +
+
+ + + {formatCurrency(10)} per month + + + + + {formatCurrency(0)} + + {formatCurrency(10)} + + +
*/} + + + + Credits + Up to $10 Compute credits + + + + + {formatCurrency(-AMOUNT_OF_CREDITS)} + + + + + } + >
+ + {subscription?.plan?.id === 'free' && ( + +
+ +
+

+ Mixing paid and non-paid projects in a single organization is not possible. +

+

+ To keep projects on a free plan, move them to another organization. +

+
+ + +
+
+ )} +
+ )} +
+ + + {!billingViaPartner ? ( +
+ +

+ When upgrading to Pro, the monthly invoice will be adjusted and your credit card will + be charged immediately. Changing the plan resets your billing cycle and may result in + a prorated charge for the previous usage. +

+
+ ) : ( +
+

+ This organization is billed through our partner {billingPartnerLabel(billingPartner)}.{' '} + {billingPartner === 'aws' ? ( + <>The organization's credit balance will be decreased accordingly. + ) : ( + <>You will be charged by them directly. + )} +

+ {billingViaPartner && + billingPartner === 'fly' && + subscriptionPreview?.plan_change_type === 'downgrade' && ( +

+ Your organization will be downgraded at the end of your current billing cycle. +

+ )} +
+ )} +
+
+ ) +} diff --git a/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx b/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx index 245b265dfff7b..a6de09ad73262 100644 --- a/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx @@ -3,8 +3,7 @@ import { ControllerRenderProps, UseFormReturn } from 'react-hook-form' import { useDefaultRegionQuery } from 'data/misc/get-default-region-query' import { useFlag } from 'hooks/ui/useFlag' -import { PROVIDERS } from 'lib/constants' -import type { CloudProvider } from 'shared-data' +import { PROVIDERS, type CloudProvider } from 'shared-data' import { SelectContent_Shadcn_, SelectGroup_Shadcn_, diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx index 563c60a7be558..bfc3b96d2db3c 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx @@ -12,9 +12,9 @@ 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 { AWS_REGIONS_DEFAULT, BASE_PATH } from 'lib/constants' +import { BASE_PATH } from 'lib/constants' import type { AWS_REGIONS_KEYS } from 'shared-data' -import { AWS_REGIONS } from 'shared-data' +import { AWS_REGIONS, AWS_REGIONS_DEFAULT } from 'shared-data' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, @@ -22,8 +22,8 @@ import { Button, Listbox, SidePanel, + WarningIcon, } from 'ui' -import { WarningIcon } from 'ui' import { AVAILABLE_REPLICA_REGIONS } from './InstanceConfiguration.constants' // [Joshen] FYI this is purely for AWS only, need to update to support Fly eventually diff --git a/apps/studio/data/projects/project-create-mutation.ts b/apps/studio/data/projects/project-create-mutation.ts index 8484041d65c8e..53eff4f395aa7 100644 --- a/apps/studio/data/projects/project-create-mutation.ts +++ b/apps/studio/data/projects/project-create-mutation.ts @@ -1,10 +1,10 @@ import * as Sentry from '@sentry/nextjs' import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' import { toast } from 'react-hot-toast' +import { PROVIDERS } from 'shared-data' import type { components } from 'data/api' import { handleError, post } from 'data/fetchers' -import { PROVIDERS } from 'lib/constants' import type { ResponseError } from 'types' import { projectKeys } from './keys' diff --git a/apps/studio/lib/cloudprovider-utils.ts b/apps/studio/lib/cloudprovider-utils.ts index e40898ff32a87..8848442c079c5 100644 --- a/apps/studio/lib/cloudprovider-utils.ts +++ b/apps/studio/lib/cloudprovider-utils.ts @@ -1,4 +1,4 @@ -import { PROVIDERS } from './constants' +import { PROVIDERS } from 'shared-data' export function getCloudProviderArchitecture(cloudProvider: string | undefined) { switch (cloudProvider) { diff --git a/apps/studio/lib/constants/infrastructure.ts b/apps/studio/lib/constants/infrastructure.ts index 691c14d28743f..4684f834f4ff6 100644 --- a/apps/studio/lib/constants/infrastructure.ts +++ b/apps/studio/lib/constants/infrastructure.ts @@ -1,14 +1,7 @@ import type { CloudProvider } from 'shared-data' -import { AWS_REGIONS, FLY_REGIONS } from 'shared-data' import type { components } from 'data/api' -export const AWS_REGIONS_DEFAULT = - process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod' ? AWS_REGIONS.SOUTHEAST_ASIA : AWS_REGIONS.EAST_US - -// TO DO, change default to US region for prod -export const FLY_REGIONS_DEFAULT = FLY_REGIONS.SOUTHEAST_ASIA - export const PRICING_TIER_LABELS_ORG = { FREE: 'Free - $0/month', PRO: 'Pro - $25/month', @@ -25,22 +18,6 @@ export const PRICING_TIER_PRODUCT_IDS = { export const DEFAULT_PROVIDER: CloudProvider = 'AWS' -export const PROVIDERS = { - FLY: { - id: 'FLY', - name: 'Fly.io', - default_region: FLY_REGIONS_DEFAULT, - regions: { ...FLY_REGIONS }, - }, - AWS: { - id: 'AWS', - name: 'AWS', - DEFAULT_SSH_KEY: 'supabase-app-instance', - default_region: AWS_REGIONS_DEFAULT, - regions: { ...AWS_REGIONS }, - }, -} as const - export const PROJECT_STATUS: { [key: string]: components['schemas']['ResourceWithServicesStatusResponse']['status'] } = { diff --git a/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx b/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx index c0cf0bd4ef1b5..e9758403479ad 100644 --- a/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx +++ b/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx @@ -18,10 +18,9 @@ import { useVercelProjectsQuery } from 'data/integrations/integrations-vercel-pr import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { useProjectCreateMutation } from 'data/projects/project-create-mutation' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' -import { PROVIDERS } from 'lib/constants' import { getInitialMigrationSQLFromGitHubRepo } from 'lib/integration-utils' import passwordStrength from 'lib/password-strength' -import { AWS_REGIONS } from 'shared-data' +import { AWS_REGIONS, PROVIDERS } from 'shared-data' import { useIntegrationInstallationSnapshot } from 'state/integration-installation' import type { NextPageWithLayout } from 'types' diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx index c57d8e8fb6773..c494493de2600 100644 --- a/apps/studio/pages/new/[slug].tsx +++ b/apps/studio/pages/new/[slug].tsx @@ -11,7 +11,6 @@ import toast from 'react-hot-toast' import { z } from 'zod' import { PopoverSeparator } from '@ui/components/shadcn/ui/popover' -import { components } from 'api-types' import { useParams } from 'common' import { FreeProjectLimitWarning, @@ -37,16 +36,17 @@ import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { withAuth } from 'hooks/misc/withAuth' import { useFlag } from 'hooks/ui/useFlag' import { getCloudProviderArchitecture } from 'lib/cloudprovider-utils' +import { DEFAULT_MINIMUM_PASSWORD_STRENGTH, DEFAULT_PROVIDER, PROJECT_STATUS } from 'lib/constants' +import passwordStrength from 'lib/password-strength' import { AWS_REGIONS_DEFAULT, - DEFAULT_MINIMUM_PASSWORD_STRENGTH, - DEFAULT_PROVIDER, + DesiredInstanceSize, FLY_REGIONS_DEFAULT, - PROJECT_STATUS, PROVIDERS, -} from 'lib/constants' -import passwordStrength from 'lib/password-strength' -import type { CloudProvider } from 'shared-data' + instanceSizeSpecs, + instanceSizes, + type CloudProvider, +} from 'shared-data' import type { NextPageWithLayout } from 'types' import { Admonition, @@ -80,114 +80,6 @@ import { Input } from 'ui-patterns/DataInputs/Input' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { InfoTooltip } from 'ui-patterns/info-tooltip' -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 - cloud_providers: string[] - } -> = { - micro: { - label: 'Micro', - ram: '1 GB', - cpu: '2-core', - priceHourly: 0.01344, - priceMonthly: 10, - cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], - }, - small: { - label: 'Small', - ram: '2 GB', - cpu: '2-core', - priceHourly: 0.0206, - priceMonthly: 15, - cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], - }, - medium: { - label: 'Medium', - ram: '4 GB', - cpu: '2-core', - priceHourly: 0.0822, - priceMonthly: 60, - cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], - }, - large: { - label: 'Large', - ram: '8 GB', - cpu: '2-core', - priceHourly: 0.1517, - priceMonthly: 110, - cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], - }, - xlarge: { - label: 'XL', - ram: '16 GB', - cpu: '4-core', - priceHourly: 0.2877, - priceMonthly: 210, - cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], - }, - '2xlarge': { - label: '2XL', - ram: '32 GB', - cpu: '8-core', - priceHourly: 0.562, - priceMonthly: 410, - cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], - }, - '4xlarge': { - label: '4XL', - ram: '64 GB', - cpu: '16-core', - priceHourly: 1.32, - priceMonthly: 960, - cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], - }, - '8xlarge': { - label: '8XL', - ram: '128 GB', - cpu: '32-core', - priceHourly: 2.562, - priceMonthly: 1870, - cloud_providers: [PROVIDERS.AWS.id], - }, - '12xlarge': { - label: '12XL', - ram: '192 GB', - cpu: '48-core', - priceHourly: 3.836, - priceMonthly: 2800, - cloud_providers: [PROVIDERS.AWS.id], - }, - '16xlarge': { - label: '16XL', - ram: '256 GB', - cpu: '64-core', - priceHourly: 5.12, - priceMonthly: 3730, - cloud_providers: [PROVIDERS.AWS.id], - }, -} - const Wizard: NextPageWithLayout = () => { const router = useRouter() const { slug } = useParams() @@ -313,7 +205,7 @@ const Wizard: NextPageWithLayout = () => { dbPass: '', dbPassStrength: 0, dbRegion: defaultRegion || undefined, - instanceSize: sizes[0], + instanceSize: instanceSizes[0], dataApi: true, useApiSchema: false, }, @@ -645,7 +537,7 @@ const Wizard: NextPageWithLayout = () => { - {sizes + {instanceSizes .filter((option) => instanceSizeSpecs[option].cloud_providers.includes( form.getValues('cloudProvider') as CloudProvider diff --git a/apps/studio/pages/vercel/setupProject.tsx b/apps/studio/pages/vercel/setupProject.tsx index fa7d07fde2ff2..de5302534617f 100644 --- a/apps/studio/pages/vercel/setupProject.tsx +++ b/apps/studio/pages/vercel/setupProject.tsx @@ -7,7 +7,6 @@ import { ChangeEvent, createContext, useContext, useEffect, useRef, useState } f import { toast } from 'react-hot-toast' import { Button, Input, Listbox } from 'ui' -import type { Dictionary } from 'types' import VercelIntegrationLayout from 'components/layouts/VercelIntegrationLayout' import { createVercelEnv, @@ -17,14 +16,11 @@ import { import { Loading } from 'components/ui/Loading' import PasswordStrengthBar from 'components/ui/PasswordStrengthBar' import { useProjectCreateMutation } from 'data/projects/project-create-mutation' -import { - DEFAULT_MINIMUM_PASSWORD_STRENGTH, - PRICING_TIER_PRODUCT_IDS, - PROVIDERS, -} from 'lib/constants' +import { DEFAULT_MINIMUM_PASSWORD_STRENGTH, PRICING_TIER_PRODUCT_IDS } from 'lib/constants' import passwordStrength from 'lib/password-strength' import { VERCEL_INTEGRATION_CONFIGS } from 'lib/vercelConfigs' -import { AWS_REGIONS } from 'shared-data' +import { AWS_REGIONS, PROVIDERS } from 'shared-data' +import type { Dictionary } from 'types' interface ISetupProjectStore { token: string diff --git a/packages/shared-data/index.ts b/packages/shared-data/index.ts index 20529e456bbc4..a732db14edd1b 100644 --- a/packages/shared-data/index.ts +++ b/packages/shared-data/index.ts @@ -1,24 +1,31 @@ import config from './config' import extensions from './extensions.json' +import { type DesiredInstanceSize, instanceSizes, instanceSizeSpecs } from './instance-sizes' import logConstants from './logConstants' import { plans, PricingInformation } from './plans' import { pricing } from './pricing' import { products } from './products' +import { PROVIDERS } from './providers' import questions from './questions' import type { AWS_REGIONS_KEYS, CloudProvider, Region } from './regions' -import { AWS_REGIONS, FLY_REGIONS } from './regions' +import { AWS_REGIONS, AWS_REGIONS_DEFAULT, FLY_REGIONS, FLY_REGIONS_DEFAULT } from './regions' import tweets from './tweets' -export type { AWS_REGIONS_KEYS, CloudProvider, PricingInformation, Region } export { AWS_REGIONS, - FLY_REGIONS, + AWS_REGIONS_DEFAULT, config, extensions, + FLY_REGIONS, + FLY_REGIONS_DEFAULT, + instanceSizes, + instanceSizeSpecs, logConstants, plans, pricing, products, + PROVIDERS, questions, tweets, } +export type { AWS_REGIONS_KEYS, CloudProvider, DesiredInstanceSize, PricingInformation, Region } diff --git a/packages/shared-data/instance-sizes.ts b/packages/shared-data/instance-sizes.ts new file mode 100644 index 0000000000000..6ec64ef185a40 --- /dev/null +++ b/packages/shared-data/instance-sizes.ts @@ -0,0 +1,119 @@ +import type { components } from 'api-types' +import { PROVIDERS } from './providers' + +export type DesiredInstanceSize = components['schemas']['DesiredInstanceSize'] + +export const instanceSizes: DesiredInstanceSize[] = [ + 'micro', + 'small', + 'medium', + 'large', + 'xlarge', + '2xlarge', + '4xlarge', + '8xlarge', + '12xlarge', + '16xlarge', +] + +export const instanceSizeSpecs: Record< + 'nano' | DesiredInstanceSize, + { + label: string + ram: string + cpu: string + priceHourly: number + priceMonthly: number + cloud_providers: string[] + } +> = { + nano: { + label: 'Micro', + ram: 'Up to 0.5 BG', + cpu: 'Shared', + // Set to 0 because it costs the customer 0$ on their invoice + priceHourly: 0, + priceMonthly: 0, + cloud_providers: [PROVIDERS.AWS.id], + }, + micro: { + label: 'Micro', + ram: '1 GB', + cpu: '2-core', + priceHourly: 0.01344, + priceMonthly: 10, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], + }, + small: { + label: 'Small', + ram: '2 GB', + cpu: '2-core', + priceHourly: 0.0206, + priceMonthly: 15, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], + }, + medium: { + label: 'Medium', + ram: '4 GB', + cpu: '2-core', + priceHourly: 0.0822, + priceMonthly: 60, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], + }, + large: { + label: 'Large', + ram: '8 GB', + cpu: '2-core', + priceHourly: 0.1517, + priceMonthly: 110, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], + }, + xlarge: { + label: 'XL', + ram: '16 GB', + cpu: '4-core', + priceHourly: 0.2877, + priceMonthly: 210, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], + }, + '2xlarge': { + label: '2XL', + ram: '32 GB', + cpu: '8-core', + priceHourly: 0.562, + priceMonthly: 410, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], + }, + '4xlarge': { + label: '4XL', + ram: '64 GB', + cpu: '16-core', + priceHourly: 1.32, + priceMonthly: 960, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], + }, + '8xlarge': { + label: '8XL', + ram: '128 GB', + cpu: '32-core', + priceHourly: 2.562, + priceMonthly: 1870, + cloud_providers: [PROVIDERS.AWS.id], + }, + '12xlarge': { + label: '12XL', + ram: '192 GB', + cpu: '48-core', + priceHourly: 3.836, + priceMonthly: 2800, + cloud_providers: [PROVIDERS.AWS.id], + }, + '16xlarge': { + label: '16XL', + ram: '256 GB', + cpu: '64-core', + priceHourly: 5.12, + priceMonthly: 3730, + cloud_providers: [PROVIDERS.AWS.id], + }, +} diff --git a/packages/shared-data/package.json b/packages/shared-data/package.json index 9dd0d49e5be49..dd77122bffa45 100644 --- a/packages/shared-data/package.json +++ b/packages/shared-data/package.json @@ -5,6 +5,9 @@ "main": "./index.ts", "types": "./index.ts", "scripts": {}, + "dependencies": { + "api-types": "*" + }, "author": "", "license": "MIT" } diff --git a/packages/shared-data/providers.ts b/packages/shared-data/providers.ts new file mode 100644 index 0000000000000..2a2b306e886ed --- /dev/null +++ b/packages/shared-data/providers.ts @@ -0,0 +1,17 @@ +import { AWS_REGIONS, AWS_REGIONS_DEFAULT, FLY_REGIONS, FLY_REGIONS_DEFAULT } from './regions' + +export const PROVIDERS = { + FLY: { + id: 'FLY', + name: 'Fly.io', + default_region: FLY_REGIONS_DEFAULT, + regions: { ...FLY_REGIONS }, + }, + AWS: { + id: 'AWS', + name: 'AWS', + DEFAULT_SSH_KEY: 'supabase-app-instance', + default_region: AWS_REGIONS_DEFAULT, + regions: { ...AWS_REGIONS }, + }, +} as const diff --git a/packages/shared-data/regions.ts b/packages/shared-data/regions.ts index 850e7880f992c..61b524628d0c0 100644 --- a/packages/shared-data/regions.ts +++ b/packages/shared-data/regions.ts @@ -11,7 +11,7 @@ export const AWS_REGIONS = { WEST_EU_3: { code: 'eu-west-3', displayName: 'West EU (Paris)' }, CENTRAL_EU: { code: 'eu-central-1', displayName: 'Central EU (Frankfurt)' }, CENTRAL_EU_2: { code: 'eu-central-2', displayName: 'Central Europe (Zurich)' }, - NORTH_EU: { code: 'eu-north-1', displayName: 'North EU (Stockholm)'}, + NORTH_EU: { code: 'eu-north-1', displayName: 'North EU (Stockholm)' }, SOUTH_ASIA: { code: 'ap-south-1', displayName: 'South Asia (Mumbai)' }, SOUTHEAST_ASIA: { code: 'ap-southeast-1', displayName: 'Southeast Asia (Singapore)' }, NORTHEAST_ASIA: { code: 'ap-northeast-1', displayName: 'Northeast Asia (Tokyo)' }, @@ -25,3 +25,9 @@ export type AWS_REGIONS_KEYS = keyof typeof AWS_REGIONS export const FLY_REGIONS = { SOUTHEAST_ASIA: { code: 'sin', displayName: 'Singapore' }, } as const + +export const AWS_REGIONS_DEFAULT = + process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod' ? AWS_REGIONS.SOUTHEAST_ASIA : AWS_REGIONS.EAST_US + +// TO DO, change default to US region for prod +export const FLY_REGIONS_DEFAULT = FLY_REGIONS.SOUTHEAST_ASIA diff --git a/packages/ui/src/components/Modal/Modal.tsx b/packages/ui/src/components/Modal/Modal.tsx index b32f81a4a17a9..232dbcb9c8e38 100644 --- a/packages/ui/src/components/Modal/Modal.tsx +++ b/packages/ui/src/components/Modal/Modal.tsx @@ -7,14 +7,13 @@ import { Button, ButtonVariantProps } from '../Button/Button' import { Dialog, DialogContent, + DialogDescription, DialogFooter, DialogHeader, DialogSection, DialogSectionSeparator, DialogTitle, DialogTrigger, - DialogDescription, - DialogClose, } from '../shadcn/ui/dialog' export interface ModalProps extends React.ComponentProps {