| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442 |
- import {
- COMPUTE_BASELINE_IOPS,
- COMPUTE_MAX_IOPS,
- computeInstanceAddonVariantIdSchema,
- } from 'shared-data'
- import {
- ComputeInstanceAddonVariantId,
- ComputeInstanceSize,
- InfraInstanceSize,
- } from './DiskManagement.types'
- import { DISK_LIMITS, DISK_PRICING, DiskType, PLAN_DETAILS } from './ui/DiskManagement.constants'
- import { ProjectDetail } from '@/data/projects/project-detail-query'
- import { PlanId, ProjectAddonVariantMeta } from '@/data/subscriptions/types'
- import { INSTANCE_MICRO_SPECS, INSTANCE_NANO_SPECS } from '@/lib/constants'
- // Included disk size only applies to primary, not replicas
- export const calculateDiskSizePrice = ({
- planId,
- oldSize,
- oldStorageType,
- newSize,
- newStorageType,
- numReplicas = 0,
- }: {
- planId: string
- oldSize: number
- oldStorageType: DiskType
- newSize: number
- newStorageType: DiskType
- numReplicas?: number
- }) => {
- const oldPricePerGB = DISK_PRICING[oldStorageType]?.storage ?? 0
- const newPricePerGB = DISK_PRICING[newStorageType]?.storage ?? 0
- const { includedDiskGB } = PLAN_DETAILS?.[planId as keyof typeof PLAN_DETAILS]
- const oldPrice = Math.max(oldSize - includedDiskGB[oldStorageType], 0) * oldPricePerGB
- const oldPriceReplica = oldSize * 1.25 * oldPricePerGB
- const newPrice = Math.max(newSize - includedDiskGB[newStorageType], 0) * newPricePerGB
- const newPriceReplica = newSize * 1.25 * newPricePerGB
- return {
- oldPrice: (oldPrice + numReplicas * oldPriceReplica).toFixed(2),
- newPrice: (newPrice + numReplicas * newPriceReplica).toFixed(2),
- }
- }
- export const calculateComputeSizePrice = ({
- availableOptions,
- oldComputeSize,
- newComputeSize,
- plan,
- }: {
- availableOptions: {
- identifier: string
- price: number
- }[]
- oldComputeSize: string
- newComputeSize: string
- plan: PlanId
- }) => {
- let _oldComputeSize = oldComputeSize
- if (plan !== 'free' && oldComputeSize === 'ci_nano') {
- /**
- * override the old compute size to micro if the plan is not free
- * this is to handle the case in which nano compute is a paid entity
- */
- _oldComputeSize = 'ci_micro'
- }
- const oldPrice = availableOptions?.find((x) => x.identifier === _oldComputeSize)?.price ?? 0
- const newPrice = availableOptions?.find((x) => x.identifier === newComputeSize)?.price ?? 0
- const oldPriceMonthly = oldPrice * 720
- const newPriceMonthly = newPrice * 720
- return {
- oldPrice: oldPriceMonthly.toFixed(2),
- newPrice: newPriceMonthly.toFixed(2),
- }
- }
- // Included IOPS applies to both primary and replicas
- export const calculateIOPSPrice = ({
- oldStorageType,
- oldProvisionedIOPS,
- newStorageType,
- newProvisionedIOPS,
- numReplicas = 0,
- }: {
- oldStorageType: DiskType
- oldProvisionedIOPS: number
- newStorageType: DiskType
- newProvisionedIOPS: number
- numReplicas?: number
- }) => {
- if (newStorageType === DiskType.GP3) {
- const oldChargeableIOPS = Math.max(
- 0,
- oldProvisionedIOPS - DISK_LIMITS[DiskType.GP3].includedIops
- )
- const newChargeableIOPS = Math.max(
- 0,
- newProvisionedIOPS - DISK_LIMITS[DiskType.GP3].includedIops
- )
- const oldPrice = oldChargeableIOPS * (DISK_PRICING[oldStorageType]?.iops ?? 0)
- const newPrice = newChargeableIOPS * (DISK_PRICING[newStorageType]?.iops ?? 0)
- return {
- oldPrice: (oldPrice * (1 + numReplicas)).toFixed(2),
- newPrice: (newPrice * (1 + numReplicas)).toFixed(2),
- }
- } else {
- const oldPrice =
- oldStorageType === 'gp3'
- ? (oldProvisionedIOPS - DISK_LIMITS[oldStorageType].includedIops) *
- DISK_PRICING[oldStorageType].iops
- : oldProvisionedIOPS * (DISK_PRICING[oldStorageType]?.iops ?? 0)
- const newPrice = newProvisionedIOPS * (DISK_PRICING[newStorageType]?.iops ?? 0)
- return {
- oldPrice: (oldPrice * (1 + numReplicas)).toFixed(2),
- newPrice: (newPrice * (1 + numReplicas)).toFixed(2),
- }
- }
- }
- // This is only applicable for GP3 storage type, no need to consider IO2 at all
- // Also assumes that disk size is > 400 GB (separate requirement to update throughput)
- // Also, included throughput applies to both primary and replicas
- export const calculateThroughputPrice = ({
- storageType,
- newThroughput,
- oldThroughput,
- numReplicas = 0,
- }: {
- storageType: DiskType
- newThroughput: number
- oldThroughput: number
- numReplicas?: number
- }) => {
- if (storageType === DiskType.GP3 && newThroughput) {
- const oldChargeableThroughput = Math.max(
- 0,
- oldThroughput - DISK_LIMITS[DiskType.GP3].includedThroughput
- )
- const newChargeableThroughput = Math.max(
- 0,
- newThroughput - DISK_LIMITS[DiskType.GP3].includedThroughput
- )
- const oldPrice = oldChargeableThroughput * DISK_PRICING[DiskType.GP3].throughput
- const newPrice = newChargeableThroughput * DISK_PRICING[DiskType.GP3].throughput
- return {
- oldPrice: (oldPrice * (1 + numReplicas)).toFixed(2),
- newPrice: (newPrice * (1 + numReplicas)).toFixed(2),
- }
- }
- return { oldPrice: '0.00', newPrice: '0.00' }
- }
- export type ComputeAddonVariant = {
- identifier: ComputeInstanceAddonVariantId
- name: string
- price_description: string
- price: number
- price_interval: 'hourly' | 'monthly'
- price_type: string
- meta?: ProjectAddonVariantMeta
- }
- type AvailableAddon = {
- type: string
- variants: Array<{
- identifier: string
- name: string
- price_description: string
- price: number
- price_interval: 'hourly' | 'monthly'
- price_type: string
- meta?: unknown
- }>
- }
- const isProjectAddonVariantMeta = (meta: unknown): meta is ProjectAddonVariantMeta => {
- if (typeof meta !== 'object' || meta === null) return false
- const obj = meta as Record<string, unknown>
- // Validate supported_cloud_providers is an array if present (used at line 200)
- if ('supported_cloud_providers' in obj && !Array.isArray(obj.supported_cloud_providers)) {
- return false
- }
- // Check for at least one expected property to ensure it's likely a real ProjectAddonVariantMeta
- const hasExpectedProperty =
- 'cpu_cores' in obj ||
- 'memory_gb' in obj ||
- 'cpu_dedicated' in obj ||
- 'baseline_disk_io_mbs' in obj ||
- 'max_disk_io_mbs' in obj ||
- 'connections_direct' in obj ||
- 'connections_pooler' in obj ||
- 'backup_duration_days' in obj ||
- 'supported_cloud_providers' in obj
- return hasExpectedProperty
- }
- export function getAvailableComputeOptions(
- availableAddons: AvailableAddon[],
- projectCloudProvider?: string
- ) {
- const computeAddon = availableAddons.find((addon) => addon.type === 'compute_instance')
- const computeOptions: ComputeAddonVariant[] =
- computeAddon?.variants.flatMap((option) => {
- const parsedId = computeInstanceAddonVariantIdSchema.safeParse(option.identifier)
- if (!parsedId.success) return []
- if (projectCloudProvider && isProjectAddonVariantMeta(option.meta)) {
- const isSupported =
- !option.meta.supported_cloud_providers ||
- option.meta.supported_cloud_providers.includes(projectCloudProvider)
- if (!isSupported) return []
- }
- return [
- {
- ...option,
- identifier: parsedId.data,
- meta: isProjectAddonVariantMeta(option.meta) ? option.meta : undefined,
- },
- ]
- }) ?? []
- function hasMicroOptionFromApi() {
- return (computeAddon?.variants ?? []).some((variant) => 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,
- })
- }
- computeOptions.unshift({
- identifier: 'ci_nano',
- name: 'Nano',
- price_description: '$0/hour (~$0/month)',
- price: 0,
- price_interval: 'hourly',
- price_type: 'usage',
- // @ts-ignore API types it as Record<string, never>
- meta: {
- cpu_cores: INSTANCE_NANO_SPECS.cpu_cores,
- cpu_dedicated: INSTANCE_NANO_SPECS.cpu_dedicated,
- memory_gb: INSTANCE_NANO_SPECS.memory_gb,
- baseline_disk_io_mbs: INSTANCE_NANO_SPECS.baseline_disk_io_mbs,
- max_disk_io_mbs: INSTANCE_NANO_SPECS.max_disk_io_mbs,
- connections_direct: INSTANCE_NANO_SPECS.connections_direct,
- connections_pooler: INSTANCE_NANO_SPECS.connections_pooler,
- } as ProjectAddonVariantMeta,
- })
- return computeOptions
- }
- export const calculateMaxIopsAllowedForDiskSizeWithGp3 = (totalSize: number) => {
- return Math.max(3000, Math.min(500 * totalSize, 16000))
- }
- export const calculateDiskSizeRequiredForIopsWithGp3 = (iops: number) => {
- return Math.max(1, Math.ceil(iops / 500))
- }
- export const calculateMaxIopsAllowedForDiskSizeWithio2 = (totalSize: number) => {
- return Math.min(500 * totalSize, 256000)
- }
- export const calculateDiskSizeRequiredForIopsWithIo2 = (iops: number) => {
- return Math.max(4, Math.ceil(iops / 1000))
- }
- export const calculateMaxThroughput = (iops: number) => {
- return Math.min(0.256 * iops, 1000)
- }
- export const calculateIopsRequiredForThroughput = (throughput: number) => {
- return Math.max(125, Math.ceil(throughput / 0.256))
- }
- export const calculateBaselineIopsForComputeSize = (computeSize: string): number => {
- const parsed = computeInstanceAddonVariantIdSchema.safeParse(computeSize)
- if (!parsed.success) return 0
- return COMPUTE_BASELINE_IOPS[parsed.data] ?? 0
- }
- export const calculateMaxIopsForComputeSize = (computeSize: string): number => {
- const parsed = computeInstanceAddonVariantIdSchema.safeParse(computeSize)
- if (!parsed.success) return 0
- return COMPUTE_MAX_IOPS[parsed.data] ?? 0
- }
- export const calculateComputeSizeRequiredForIops = (
- iops: number
- ): ComputeInstanceAddonVariantId | undefined => {
- type ComputeSizeMax = { size: ComputeInstanceAddonVariantId; maxIops: number }
- const computeSizes: ComputeSizeMax[] = Object.entries(COMPUTE_MAX_IOPS)
- .map((entry) => {
- const [size, maxIops] = entry
- const parsedSize = computeInstanceAddonVariantIdSchema.safeParse(size)
- if (!parsedSize.success) return undefined
- return { size: parsedSize.data, maxIops: Number(maxIops) }
- })
- .filter((value): value is ComputeSizeMax => value !== undefined)
- .sort((a, b) => a.maxIops - b.maxIops)
- for (const { size, maxIops } of computeSizes) {
- if (iops <= maxIops) {
- return size
- }
- }
- const fallbackSize = computeSizes[computeSizes.length - 1]?.size
- if (!fallbackSize) return undefined
- return fallbackSize
- }
- export const calculateDiskSizeRequiredForIops = (provisionedIOPS: number): number | undefined => {
- if (!provisionedIOPS) {
- console.error('IOPS is required')
- return undefined
- }
- if (isNaN(provisionedIOPS) || provisionedIOPS < 0) {
- console.error('IOPS must be a non-negative number')
- return undefined
- }
- if (provisionedIOPS > 256000) {
- console.error('Maximum allowed IOPS is 256000')
- return undefined
- }
- return Math.max(1, Math.ceil(provisionedIOPS / 1000))
- }
- export const formatComputeName = (compute: string) => {
- return compute.toUpperCase().replace('CI_', '')
- }
- const infraToAddonVariant: Record<InfraInstanceSize, ComputeInstanceAddonVariantId> = {
- pico: 'ci_nano',
- nano: 'ci_nano',
- micro: 'ci_micro',
- small: 'ci_small',
- medium: 'ci_medium',
- large: 'ci_large',
- xlarge: 'ci_xlarge',
- '2xlarge': 'ci_2xlarge',
- '4xlarge': 'ci_4xlarge',
- '8xlarge': 'ci_8xlarge',
- '12xlarge': 'ci_12xlarge',
- '16xlarge': 'ci_16xlarge',
- '24xlarge': 'ci_24xlarge',
- '24xlarge_optimized_memory': 'ci_24xlarge_optimized_memory',
- '24xlarge_optimized_cpu': 'ci_24xlarge_optimized_cpu',
- '24xlarge_high_memory': 'ci_24xlarge_high_memory',
- '48xlarge': 'ci_48xlarge',
- '48xlarge_optimized_memory': 'ci_48xlarge_optimized_memory',
- '48xlarge_optimized_cpu': 'ci_48xlarge_optimized_cpu',
- '48xlarge_high_memory': 'ci_48xlarge_high_memory',
- }
- const isInfraInstanceSize = (value: string): value is InfraInstanceSize =>
- Object.prototype.hasOwnProperty.call(infraToAddonVariant, value)
- export const mapComputeSizeNameToAddonVariantId = (
- computeSize: ProjectDetail['infra_compute_size']
- ): ComputeInstanceAddonVariantId => {
- const fallback: InfraInstanceSize = 'nano'
- const matchedSize = computeSize && isInfraInstanceSize(computeSize) ? computeSize : undefined
- const sizeKey = matchedSize ?? fallback
- return infraToAddonVariant[sizeKey]
- }
- const addonVariantToComputeSize: Record<ComputeInstanceAddonVariantId, ComputeInstanceSize> = {
- ci_nano: 'Nano',
- ci_micro: 'Micro',
- ci_small: 'Small',
- ci_medium: 'Medium',
- ci_large: 'Large',
- ci_xlarge: 'XL',
- ci_2xlarge: '2XL',
- ci_4xlarge: '4XL',
- ci_8xlarge: '8XL',
- ci_12xlarge: '12XL',
- ci_16xlarge: '16XL',
- ci_24xlarge: '24XL',
- ci_24xlarge_optimized_memory: '24XL - Optimized Memory',
- ci_24xlarge_optimized_cpu: '24XL - Optimized CPU',
- ci_24xlarge_high_memory: '24XL - High Memory',
- ci_48xlarge: '48XL',
- ci_48xlarge_optimized_memory: '48XL - Optimized Memory',
- ci_48xlarge_optimized_cpu: '48XL - Optimized CPU',
- ci_48xlarge_high_memory: '48XL - High Memory',
- }
- export const mapAddOnVariantIdToComputeSize = (
- addonVariantId: ComputeInstanceAddonVariantId = 'ci_nano'
- ): ComputeInstanceSize => {
- const parsed = computeInstanceAddonVariantIdSchema.safeParse(addonVariantId)
- if (!parsed.success) return addonVariantToComputeSize.ci_nano
- return addonVariantToComputeSize[parsed.data]
- }
- export const formatNumber = (num: number): string => {
- return num.toLocaleString('en-US')
- }
- export const showMicroUpgrade = (plan: PlanId, infraComputeSize: InfraInstanceSize): boolean => {
- return plan !== 'free' && infraComputeSize === 'nano'
- }
- export function hasBurstableIO(): boolean { return false; }
|