Files
supabase/apps/studio/components/interfaces/DiskManagement/fields/ComputeSizeField.tsx
T
K-Dog (Kevin) cee7461a9a chore: allow PITR without small compute addon (#50699)
We no longer require Small Compute add-on to configure PITR.
2026-09-23 19:31:12 +08:00

338 lines
14 KiB
TypeScript

import { SupportCategories } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { CpuIcon, Lock, Microchip } from 'lucide-react'
import { useMemo } from 'react'
import { UseFormReturn, useWatch } from 'react-hook-form'
import {
cn,
FormField,
HoverCard,
HoverCardContent,
HoverCardTrigger,
RadioGroupCard,
RadioGroupCardItem,
Skeleton,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import { ComputeBadge } from 'ui-patterns/ComputeBadge'
import { DiskStorageSchemaType } from '../DiskManagement.schema'
import { ComputeInstanceAddonVariantId, InfraInstanceSize } from '../DiskManagement.types'
import {
ComputeAddonVariant,
getAvailableComputeOptions,
mapComputeSizeNameToAddonVariantId,
} from '../DiskManagement.utils'
import { BillingChangeBadge } from '../ui/BillingChangeBadge'
import FormMessage from '../ui/FormMessage'
import { useShowMicroUpgradeBadge } from './useShowMicroUpgradeBadge'
import { SupportLink } from '@/components/interfaces/Support/SupportLink'
import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
import { useHighAvailability } from '@/hooks/misc/useHighAvailability'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { getComputeCpuLabel } from '@/lib/compute-labels'
const SKELETON_PLACEHOLDER_COUNT = 6
/**
* to do: this could be a type from api-types
*/
type ComputeSizeFieldProps = {
form: UseFormReturn<DiskStorageSchemaType>
disabled?: boolean
}
export function ComputeSizeFieldMeta() {
return <p>Hardware resources allocated to your Postgres database</p>
}
type ComputeSectionBillingBadgeProps = {
form: UseFormReturn<DiskStorageSchemaType>
show: boolean
beforePrice: number
afterPrice: number
}
export function ComputeSectionBillingBadge({
form,
show,
beforePrice,
afterPrice,
}: ComputeSectionBillingBadgeProps) {
const computeSize = useWatch({ control: form.control, name: 'computeSize' })
const { showMicroUpgradeBadge } = useShowMicroUpgradeBadge()
return (
<BillingChangeBadge
show={show}
beforePrice={beforePrice}
afterPrice={afterPrice}
free={showMicroUpgradeBadge && computeSize === 'ci_micro'}
/>
)
}
export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
const { ref } = useParams()
const { data: org } = useSelectedOrganizationQuery()
const { project, isProjectLoading, isEntitlementLoading, showMicroUpgradeBadge } =
useShowMicroUpgradeBadge()
const { isHighAvailability } = useHighAvailability()
// High Availability projects run on a fixed compute size during Alpha,
// so every option other than the project's current size is locked
const currentComputeVariantId = project?.infra_compute_size
? mapComputeSizeNameToAddonVariantId(project.infra_compute_size)
: undefined
const showComputePrice = useIsFeatureEnabled('project_addons:show_compute_price')
const {
data: addons,
isPending: isAddonsLoading,
error: addonsError,
} = useProjectAddonsQuery({ projectRef: ref })
const isLoading = isProjectLoading || isAddonsLoading || isEntitlementLoading
const { control, setValue, trigger } = form
const availableAddons = useMemo(() => {
return addons?.available_addons ?? []
}, [addons])
const availableOptions = useMemo(() => {
/**
* Returns the available compute options for the project
* Also handles backwards compatibility for older API versions
* Also handles a case in which Nano is not available from the API
*/
return getAvailableComputeOptions(availableAddons, project?.cloud_provider)
}, [availableAddons, project?.cloud_provider])
const showSkeletons = isLoading
const showLoadError = !isLoading && !!addonsError
const showComputeOptions = !isLoading && !addonsError
return (
<FormField
name="computeSize"
control={control}
render={({ field }) => (
<div className="@container">
<RadioGroupCard
value={field.value}
onValueChange={(value: ComputeInstanceAddonVariantId) => {
setValue('computeSize', value, {
shouldDirty: true,
shouldValidate: true,
})
trigger('provisionedIOPS')
trigger('throughput')
}}
disabled={disabled}
className={cn(
!addonsError && 'grid grid-cols-2 gap-4 @[680px]:grid-cols-3 @[900px]:grid-cols-4'
)}
>
{showSkeletons &&
Array(SKELETON_PLACEHOLDER_COUNT)
.fill(0)
.map((_, i) => <Skeleton key={i} className="w-full h-[110px] rounded-md" />)}
{showLoadError && (
<FormMessage message={'Failed to load Compute size options'} type="error">
<p>{addonsError?.message}</p>
</FormMessage>
)}
{showComputeOptions && (
<>
{availableOptions.map((compute) => {
const lockedNanoDueToPlan =
org?.plan.id !== 'free' &&
project?.infra_compute_size !== 'nano' &&
compute.identifier === 'ci_nano'
const lockedDueToHighAvailability =
isHighAvailability && compute.identifier !== currentComputeVariantId
const lockedOption = lockedNanoDueToPlan || lockedDueToHighAvailability
// Nano on a paid plan is billed at the Micro rate
const isNanoBilledAsMicro =
org?.plan.id !== 'free' &&
project?.infra_compute_size === 'nano' &&
compute.identifier === 'ci_nano'
const price = isNanoBilledAsMicro
? availableOptions.find(
(option: ComputeAddonVariant) => option.identifier === 'ci_micro'
)?.price
: compute.price
const cpuLabel = getComputeCpuLabel(compute.identifier, compute.meta?.cpu_cores)
return (
<RadioGroupCardItem
showIndicator={false}
id={compute.identifier}
key={compute.identifier}
value={compute.identifier}
className={cn(
'relative text-sm text-left flex flex-col gap-0 px-0 py-3 group w-full h-[110px]',
lockedOption && 'opacity-50'
)}
disabled={disabled || lockedOption}
label={
<Tooltip>
<TooltipTrigger asChild>
<div>
{showMicroUpgradeBadge && compute.identifier === 'ci_micro' && (
<HoverCard openDelay={200}>
<HoverCardTrigger asChild>
<div
className="absolute -top-4 -right-3 text-violet-1100 flex items-center gap-1 bg-surface-75 py-0.5 px-2 rounded-full border border-violet-900 cursor-default"
onClick={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>
<span>Free Upgrade</span>
</div>
</HoverCardTrigger>
<HoverCardContent
side="top"
align="end"
className="w-72"
onClick={(e) => e.stopPropagation()}
>
<p className="text-sm font-medium text-foreground">
Upgrade to Micro Compute
</p>
<p className="text-sm text-foreground-light">
This Project is already paying for Micro Compute. You can
upgrade to Micro Compute at any time when convenient.
</p>
</HoverCardContent>
</HoverCard>
)}
<div className="w-full flex flex-col gap-3 justify-between">
<div className="relative px-3 opacity-50 group-data-[state=checked]:opacity-100 flex justify-between">
<ComputeBadge
className="inline-flex font-semibold"
infraComputeSize={compute.name as InfraInstanceSize}
/>
<div className="flex items-center space-x-1">
{lockedOption && (
<div className="bg border rounded-lg h-7 w-7 flex items-center justify-center">
<Lock size={14} />
</div>
)}
{!lockedOption && showComputePrice && (
<>
<span
className="text-foreground text-sm font-semibold"
translate="no"
>
${price}
</span>
<span className="text-foreground-light translate-y-px">
{' '}
/{' '}
{compute.price_interval === 'monthly' ? 'month' : 'hour'}
</span>
</>
)}
</div>
</div>
<div className="w-full">
<div className="px-3 text-sm flex flex-col gap-1">
<div className="text-foreground-light flex gap-2 items-center">
<Microchip
strokeWidth={1}
size={14}
className="text-foreground-lighter"
/>
<span>
{compute.identifier === 'ci_nano' && 'Up to '}
{compute.meta?.memory_gb ?? 0} GB memory
</span>
</div>
<div className="text-foreground-light flex gap-2 items-center">
<CpuIcon
strokeWidth={1}
size={14}
className="text-foreground-lighter"
/>
<span>{cpuLabel}</span>
</div>
</div>
</div>
</div>
</div>
</TooltipTrigger>
{lockedDueToHighAvailability && (
<TooltipContent side="bottom" className="w-64 text-center">
Compute size can't be changed on High Availability projects during
Alpha
</TooltipContent>
)}
</Tooltip>
}
/>
)
})}
{!isHighAvailability && (
<div
className={cn(
'relative text-sm text-left flex flex-col gap-0 px-0 py-3 w-full h-[110px]',
'bg-overlay rounded-md border p-2 hover:border-control-hover'
)}
>
<SupportLink
queryParams={{
projectRef: ref,
category: SupportCategories.SALES_ENQUIRY,
subject: 'Enquiry about larger instance sizes',
}}
>
<div className="w-full flex flex-col gap-3 justify-between">
<div className="relative px-3 flex justify-between">
<ComputeBadge infraComputeSize=">16XL" />
<div className="flex items-center space-x-1 opacity-50 ">
<span className="text-foreground-light text-sm">Contact Us</span>
</div>
</div>
<div className="w-full">
<div className="px-3 text-sm flex flex-col gap-1">
<div className="text-foreground-light flex gap-2 items-center">
<Microchip
strokeWidth={1}
size={14}
className="text-foreground-lighter"
/>
<span>Custom memory</span>
</div>
<div className="text-foreground-light flex gap-2 items-center">
<CpuIcon
strokeWidth={1}
size={14}
className="text-foreground-lighter"
/>
<span>Custom compute</span>
</div>
</div>
</div>
</div>
</SupportLink>
</div>
)}
</>
)}
</RadioGroupCard>
</div>
)}
/>
)
}