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 }) const { data: addons, isLoading } = useProjectAddonsQuery({ projectRef }) const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) 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