chore(studio): describe compute CPU by size tier (#50401)

## What kind of change does this PR introduce?

Copy/label update in Studio's compute surfaces.

## Description

Compute CPU descriptions now branch on the compute size tier:

- Sizes below Large read **"Shared compute"** (no core count)
- Large and up read **"Dedicated · N vCPUs"** — the unit is always vCPU

Changes:

- New `lib/compute-labels.ts` helper (`isSharedComputeSize`,
`getComputeCpuLabel`) with unit tests
- Compute badge hover card, compute size picker, and project-creation
selector use the new labels
- `new-project.constants.ts` cpu strings updated accordingly
- ">16XL" card: "Custom CPU" → "Custom compute"; upsell copy now says
"64 vCPUs"
- The synthetic Nano/Micro addon `meta` no longer has
`cpu_cores`/`cpu_dedicated`; removed the now-unused cpu fields from the
hardcoded instance specs
- Project-creation sub-text: "Larger, dedicated compute available after
creation"

## Tests

- New unit tests for the label helper
- Infrastructure settings page test now asserts the rendered labels

Fixes PROD-663

Related #49998 #49996


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **User Interface**
* Updated compute-size labels to use “Shared compute” and vCPU
terminology.
  * Clarified dedicated compute options and availability messaging.
* Updated custom instance and upgrade labels, including “Custom compute”
and “64 vCPUs.”
* **Consistency**
* Standardized compute labels across project creation, infrastructure
settings, and compute details.
* **Tests**
* Added coverage verifying shared and dedicated compute classifications
and displayed labels.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Lukas BernertandClaude Fable 5 authored and GitHub committed 2026-09-17 12:18:48 +02:00
1 parent 1e444589c6
commit 77ee1ec127
10 files changed
+89 -46

No files matched your search

@@ -248,8 +248,6 @@ export function getAvailableComputeOptions(
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,
@@ -268,8 +266,6 @@ export function getAvailableComputeOptions(
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,
@@ -34,6 +34,7 @@ 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
@@ -175,16 +176,7 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
)?.price
: compute.price
const cpuLabel = (() => {
const cpuCores = compute.meta?.cpu_cores
if (typeof cpuCores === 'number') {
return `${cpuCores}-core CPU`
}
if (cpuCores) {
return `${cpuCores} CPU`
}
return 'CPU'
})()
const cpuLabel = getComputeCpuLabel(compute.identifier, compute.meta?.cpu_cores)
return (
<RadioGroupCardItem
@@ -343,7 +335,7 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
size={14}
className="text-foreground-lighter"
/>
<span>Custom CPU</span>
<span>Custom compute</span>
</div>
</div>
</div>
@@ -86,8 +86,7 @@ export const ComputeSizeSelector = ({ form }: ComputeSizeSelectorProps) => {
<div className="text-sm">
<span className="text-foreground">
{instanceSizeSpecs[option].ram} RAM / {instanceSizeSpecs[option].cpu}{' '}
CPU
{instanceSizeSpecs[option].ram} RAM / {instanceSizeSpecs[option].cpu}
</span>
<p
translate="no"
@@ -118,7 +117,7 @@ export const ComputeSizeSelector = ({ form }: ComputeSizeSelectorProps) => {
{!highAvailability && (
<SelectItem key={'disabled'} value={'disabled'} disabled>
<div className="flex items-center justify-center w-full">
<span>Larger instance sizes available after creation</span>
<span>Larger, dedicated compute available after creation</span>
</div>
</SelectItem>
)}
@@ -10,6 +10,7 @@ import { ProjectDetail } from '@/data/projects/project-detail-query'
import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
import { ResourceWarning } from '@/data/usage/resource-warnings-query'
import { getComputeCpuLabel } from '@/lib/compute-labels'
import { useTrack } from '@/lib/telemetry/track'
export const ChevronsUpAnimated = () => (
@@ -149,14 +150,14 @@ export const ComputeBadgeWrapper = ({
<div className="flex flex-col gap-1">
{computeSize === 'nano' ? (
<>
<Row label="CPU" stat="Shared" />
<Row label="CPU" stat="Shared compute" />
<Row label="Memory" stat="Up to 0.5 GB" />
</>
) : meta !== undefined ? (
<>
<Row
label="CPU"
stat={`${meta.cpu_cores ?? '?'}-core ${meta.cpu_dedicated ? '(Dedicated)' : '(Shared)'}`}
stat={getComputeCpuLabel(computeSize ?? '', meta.cpu_cores)}
/>
<Row label="Memory" stat={`${meta.memory_gb ?? '-'} GB`} />
</>
@@ -179,7 +180,7 @@ export const ComputeBadgeWrapper = ({
<p className="text-foreground-light">
{isEligibleForFreeUpgrade
? 'Paid plans include a free upgrade to Micro compute.'
: 'Scale your project up to 64 cores and 256 GB RAM.'}
: 'Scale your project up to 64 vCPUs and 256 GB RAM.'}
</p>
</div>
<div>
@@ -29,7 +29,7 @@ export const instanceSizeSpecs: Record<
micro: {
label: 'Micro',
ram: '1 GB',
cpu: '2-core',
cpu: 'Shared compute',
priceHourly: 0.01344,
priceMonthly: 10,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
@@ -37,7 +37,7 @@ export const instanceSizeSpecs: Record<
small: {
label: 'Small',
ram: '2 GB',
cpu: '2-core',
cpu: 'Shared compute',
priceHourly: 0.0206,
priceMonthly: 15,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
@@ -45,7 +45,7 @@ export const instanceSizeSpecs: Record<
medium: {
label: 'Medium',
ram: '4 GB',
cpu: '2-core',
cpu: 'Shared compute',
priceHourly: 0.0822,
priceMonthly: 60,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
@@ -53,7 +53,7 @@ export const instanceSizeSpecs: Record<
large: {
label: 'Large',
ram: '8 GB',
cpu: '2-core',
cpu: '2 vCPUs',
priceHourly: 0.1517,
priceMonthly: 110,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
@@ -61,7 +61,7 @@ export const instanceSizeSpecs: Record<
xlarge: {
label: 'XL',
ram: '16 GB',
cpu: '4-core',
cpu: '4 vCPUs',
priceHourly: 0.2877,
priceMonthly: 210,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
@@ -69,7 +69,7 @@ export const instanceSizeSpecs: Record<
'2xlarge': {
label: '2XL',
ram: '32 GB',
cpu: '8-core',
cpu: '8 vCPUs',
priceHourly: 0.562,
priceMonthly: 410,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
@@ -77,7 +77,7 @@ export const instanceSizeSpecs: Record<
'4xlarge': {
label: '4XL',
ram: '64 GB',
cpu: '16-core',
cpu: '16 vCPUs',
priceHourly: 1.32,
priceMonthly: 960,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
@@ -85,7 +85,7 @@ export const instanceSizeSpecs: Record<
'8xlarge': {
label: '8XL',
ram: '128 GB',
cpu: '32-core',
cpu: '32 vCPUs',
priceHourly: 2.562,
priceMonthly: 1870,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
@@ -93,7 +93,7 @@ export const instanceSizeSpecs: Record<
'12xlarge': {
label: '12XL',
ram: '192 GB',
cpu: '48-core',
cpu: '48 vCPUs',
priceHourly: 3.836,
priceMonthly: 2800,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
@@ -101,7 +101,7 @@ export const instanceSizeSpecs: Record<
'16xlarge': {
label: '16XL',
ram: '256 GB',
cpu: '64-core',
cpu: '64 vCPUs',
priceHourly: 5.12,
priceMonthly: 3730,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
@@ -109,7 +109,7 @@ export const instanceSizeSpecs: Record<
'24xlarge': {
label: '24XL',
ram: '384 GB',
cpu: '96-core',
cpu: '96 vCPUs',
priceHourly: 9.73,
priceMonthly: 7100,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
@@ -117,7 +117,7 @@ export const instanceSizeSpecs: Record<
'24xlarge_optimized_cpu': {
label: '24XL - Optimized CPU',
ram: '192 GB',
cpu: '96-core',
cpu: '96 vCPUs',
priceHourly: 8.9,
priceMonthly: 6500,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
@@ -125,7 +125,7 @@ export const instanceSizeSpecs: Record<
'24xlarge_optimized_memory': {
label: '24XL - Optimized Memory',
ram: '768 GB',
cpu: '96-core',
cpu: '96 vCPUs',
priceHourly: 13.84,
priceMonthly: 10100,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
@@ -133,7 +133,7 @@ export const instanceSizeSpecs: Record<
'24xlarge_high_memory': {
label: '24XL - High Memory',
ram: '1536 GB',
cpu: '96-core',
cpu: '96 vCPUs',
priceHourly: 21.91,
priceMonthly: 16000,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
@@ -141,7 +141,7 @@ export const instanceSizeSpecs: Record<
'48xlarge': {
label: '48XL',
ram: '768 GB',
cpu: '192-core',
cpu: '192 vCPUs',
priceHourly: 19.47,
priceMonthly: 14200,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
@@ -149,7 +149,7 @@ export const instanceSizeSpecs: Record<
'48xlarge_optimized_cpu': {
label: '48XL - Optimized CPU',
ram: '384 GB',
cpu: '192-core',
cpu: '192 vCPUs',
priceHourly: 17.8,
priceMonthly: 13000,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
@@ -157,7 +157,7 @@ export const instanceSizeSpecs: Record<
'48xlarge_optimized_memory': {
label: '48XL - Optimized Memory',
ram: '1536 GB',
cpu: '192-core',
cpu: '192 vCPUs',
priceHourly: 27.68,
priceMonthly: 20200,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
@@ -165,7 +165,7 @@ export const instanceSizeSpecs: Record<
'48xlarge_high_memory': {
label: '48XL - High Memory',
ram: '3072 GB',
cpu: '192-core',
cpu: '192 vCPUs',
priceHourly: 43.84,
priceMonthly: 32000,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
+42
View File
@@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest'
import { getComputeCpuLabel, isSharedComputeSize } from './compute-labels'
describe('isSharedComputeSize', () => {
it('treats sizes below Large as shared', () => {
expect(isSharedComputeSize('pico')).toBe(true)
expect(isSharedComputeSize('nano')).toBe(true)
expect(isSharedComputeSize('micro')).toBe(true)
expect(isSharedComputeSize('small')).toBe(true)
expect(isSharedComputeSize('medium')).toBe(true)
})
it('treats Large and up as dedicated', () => {
expect(isSharedComputeSize('large')).toBe(false)
expect(isSharedComputeSize('xlarge')).toBe(false)
expect(isSharedComputeSize('16xlarge')).toBe(false)
expect(isSharedComputeSize('48xlarge_high_memory')).toBe(false)
})
it('accepts addon variant identifiers and mixed case', () => {
expect(isSharedComputeSize('ci_micro')).toBe(true)
expect(isSharedComputeSize('ci_large')).toBe(false)
expect(isSharedComputeSize('Medium')).toBe(true)
})
})
describe('getComputeCpuLabel', () => {
it('labels shared sizes without a count', () => {
expect(getComputeCpuLabel('micro', 2)).toBe('Shared compute')
expect(getComputeCpuLabel('medium')).toBe('Shared compute')
})
it('labels dedicated sizes with a vCPU count', () => {
expect(getComputeCpuLabel('large', 2)).toBe('Dedicated · 2 vCPUs')
expect(getComputeCpuLabel('16xlarge', 64)).toBe('Dedicated · 64 vCPUs')
})
it('falls back to a generic dedicated label without a count', () => {
expect(getComputeCpuLabel('large')).toBe('Dedicated compute')
})
})
+14
View File
@@ -0,0 +1,14 @@
/** Compute sizes below Large run on shared compute; Large and up are dedicated. */
const SHARED_COMPUTE_SIZES = ['pico', 'nano', 'micro', 'small', 'medium']
export const isSharedComputeSize = (size: string) =>
SHARED_COMPUTE_SIZES.includes(size.toLowerCase().replace(/^ci_/, ''))
/**
* User-facing CPU description for a compute size. Branches on the size itself,
* not on `meta.cpu_cores` — the platform is dropping that field.
*/
export const getComputeCpuLabel = (size: string, vcpuCount?: number) => {
if (isSharedComputeSize(size)) return 'Shared compute'
return typeof vcpuCount === 'number' ? `Dedicated · ${vcpuCount} vCPUs` : 'Dedicated compute'
}
@@ -121,8 +121,6 @@ export type InstanceSpecs = {
baseline_disk_io_mbs: number
connections_direct: number
connections_pooler: number
cpu_cores: number | 'Shared'
cpu_dedicated: boolean
max_disk_io_mbs: number
memory_gb: number
}
@@ -131,8 +129,6 @@ export const INSTANCE_NANO_SPECS: InstanceSpecs = {
baseline_disk_io_mbs: 43,
connections_direct: 30,
connections_pooler: 200,
cpu_cores: 'Shared',
cpu_dedicated: false,
max_disk_io_mbs: 2085,
memory_gb: 0.5,
}
@@ -141,8 +137,6 @@ export const INSTANCE_MICRO_SPECS: InstanceSpecs = {
baseline_disk_io_mbs: 87,
connections_direct: 60,
connections_pooler: 200,
cpu_cores: 2,
cpu_dedicated: false,
max_disk_io_mbs: 2085,
memory_gb: 1,
}
+1 -1
View File
@@ -493,7 +493,7 @@ describe('project creation wizard', () => {
await generateAndWaitForStrongPassword()
await user.click(getSelectTriggerByLabel('Compute size'))
await user.click(await screen.findByText('4 GB RAM / 2-core CPU'))
await user.click(await screen.findByText('4 GB RAM / Shared compute'))
fireEvent.click(screen.getByRole('button', { name: 'Create new project' }))
@@ -329,6 +329,11 @@ describe('/project/[ref]/settings/infrastructure', () => {
expect(screen.getByTestId('metric-Memory')).toHaveTextContent('50%')
expect(screen.getByTestId('metric-Disk')).toHaveTextContent('50%')
// Compute options describe CPU by size: shared below Large, dedicated with vCPUs from Large up
expect(screen.getAllByText('Shared compute')).toHaveLength(3) // Nano, Micro, Small
expect(screen.getByText('Dedicated · 2 vCPUs')).toBeInTheDocument() // Large
expect(screen.queryByText(/-core/)).not.toBeInTheDocument()
await user.click(screen.getByText('Small'))
expect(screen.getByRole('button', { name: 'Review changes' })).toBeEnabled()