feat(compute): collapse large instance sizes behind expand toggle DEBUG-50 (#44189)

## Problem

The Compute and Disk settings page shows all 12+ instance sizes (Nano
through >16XL) in a grid, which requires scrolling on smaller screens
like a 13" MacBook. This makes it impossible to see all three sections
(compute size, disk size, advanced settings) in a single view. Free tier
users have no need to see 8XL, 12XL, or 16XL options.

## Fix

Show only the first 6 compute sizes (Nano through XL) by default,
cutting the grid from 4 rows to 2 rows. Larger sizes (2XL and above,
plus the >16XL contact card) are revealed via a "Show all sizes" toggle
button below the grid. The grid auto-expands if the project is already
running on a large instance size so the selected option is always
visible.

## How to test

- Go to Settings > Compute and Disk on a project
- Verify only 6 cards are visible by default (Nano, Micro, Small,
Medium, Large, XL)
- Verify the Disk size and Advanced settings sections are visible
without scrolling on a 13" screen
- Click "Show all sizes" and verify the remaining sizes appear (2XL
through 16XL plus the >16XL contact card)
- Click "Show fewer sizes" and verify the grid collapses back to 6 cards
- On a project running 4XL or larger, verify the grid opens expanded by
default so the selected card is visible

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

## Summary by CodeRabbit

* **New Features**
* Compute size options now display in a compact view with a toggle to
expand and show all available sizes
* Auto-expands to reveal the currently selected compute size when it's
not visible in the initial view
  * Enhanced loading state for improved visual clarity

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jordi EnricandClaude Sonnet 4.6 authored and GitHub committed 2026-04-01 12:29:04 +02:00
1 parent c30d3e62de
commit c3af2858bd
1 file changed
+111 -59
@@ -1,9 +1,20 @@
import { SupportCategories } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { CpuIcon, Lock, Microchip } from 'lucide-react'
import { useMemo } from 'react'
import { SupportLink } from 'components/interfaces/Support/SupportLink'
import { DocsButton } from 'components/ui/DocsButton'
import { InlineLink } from 'components/ui/InlineLink'
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { getCloudProviderArchitecture } from 'lib/cloudprovider-utils'
import { DOCS_URL } from 'lib/constants'
import { ChevronRight, CpuIcon, Lock, Microchip } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
import { UseFormReturn } from 'react-hook-form'
import {
Button,
cn,
FormField_Shadcn_,
RadioGroupCard,
@@ -26,16 +37,8 @@ import {
import { BillingChangeBadge } from '../ui/BillingChangeBadge'
import FormMessage from '../ui/FormMessage'
import { NoticeBar } from '../ui/NoticeBar'
import { SupportLink } from '@/components/interfaces/Support/SupportLink'
import { DocsButton } from '@/components/ui/DocsButton'
import { InlineLink } from '@/components/ui/InlineLink'
import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { getCloudProviderArchitecture } from '@/lib/cloudprovider-utils'
import { DOCS_URL, InstanceSpecs } from '@/lib/constants'
const INITIALLY_VISIBLE_COUNT = 6
/**
* to do: this could be a type from api-types
@@ -79,6 +82,21 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
return getAvailableComputeOptions(availableAddons, project?.cloud_provider)
}, [availableAddons, project?.cloud_provider])
// Expand by default if the project's current compute size is beyond the initial visible set
const [showAllSizes, setShowAllSizes] = useState(() => {
const idx = availableOptions.findIndex((o) => o.identifier === computeSize)
return idx >= INITIALLY_VISIBLE_COUNT
})
// Expand whenever the selected size falls outside the visible set — covers both initial data
// load (availableOptions starts empty) and computeSize changes after mount (e.g. form reset)
useEffect(() => {
const idx = availableOptions.findIndex((o) => o.identifier === computeSize)
if (idx >= INITIALLY_VISIBLE_COUNT) {
setShowAllSizes(true)
}
}, [computeSize, availableOptions])
const subscriptionPitr = addons?.selected_addons.find((addon) => addon.type === 'pitr')
const computeSizePrice = calculateComputeSizePrice({
@@ -91,6 +109,17 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
const projectComputeSize = project?.infra_compute_size ?? 'nano'
const showUpgradeBadge = entitledUpdateCompute && projectComputeSize === 'nano'
const selectedOptionIndex = availableOptions.findIndex((o) => o.identifier === computeSize)
const selectedOptionIsHidden = selectedOptionIndex >= INITIALLY_VISIBLE_COUNT
// Always show all options if the selected one would be outside the visible slice,
// so the active card is never hidden from the user.
const visibleOptions =
showAllSizes || selectedOptionIsHidden
? availableOptions
: availableOptions.slice(0, INITIALLY_VISIBLE_COUNT)
const hasHiddenOptions = availableOptions.length > INITIALLY_VISIBLE_COUNT
return (
<FormField_Shadcn_
name="computeSize"
@@ -157,7 +186,7 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
}
>
{isLoading ? (
Array(10)
Array(INITIALLY_VISIBLE_COUNT)
.fill(0)
.map((_, i) => <Skeleton key={i} className="w-full h-[110px] rounded-md" />)
) : addonsError ? (
@@ -166,7 +195,7 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
</FormMessage>
) : (
<>
{availableOptions.map((compute) => {
{visibleOptions.map((compute) => {
const cpuArchitecture = getCloudProviderArchitecture(project?.cloud_provider)
const lockedMicroDueToPITR =
@@ -205,7 +234,7 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
key={compute.identifier}
value={compute.identifier}
className={cn(
'relative text-sm text-left flex flex-col gap-0 px-0 py-3 [&_label]:w-full group] w-full h-[110px]',
'relative text-sm text-left flex flex-col gap-0 px-0 py-3 [&_label]:w-full group w-full h-[110px]',
lockedOption && 'opacity-50'
)}
disabled={disabled || lockedOption}
@@ -293,58 +322,81 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
)
})}
<RadioGroupCardItem
id="larger-compute"
key="larger-compute"
showIndicator={false}
value="larger-compute"
onClick={(e) => e.preventDefault()}
className={cn(
'relative text-sm text-left flex flex-col gap-0 px-0 py-3 [&_label]:w-full group] w-full h-[110px]'
)}
label={
<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" />
{showAllSizes && (
<RadioGroupCardItem
id="larger-compute"
key="larger-compute"
showIndicator={false}
value="larger-compute"
onClick={(e) => e.preventDefault()}
className={cn(
'relative text-sm text-left flex flex-col gap-0 px-0 py-3 [&_label]:w-full group w-full h-[110px]'
)}
label={
<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 className="flex items-center space-x-1 opacity-50 ">
<span className="text-foreground-light text-sm">Contact Us</span>
</div>
<div className="text-foreground-light flex gap-2 items-center">
<CpuIcon
strokeWidth={1}
size={14}
className="text-foreground-lighter"
/>
<span>Custom CPU</span>
</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 CPU</span>
</div>
</div>
</div>
</div>
</div>
</SupportLink>
}
/>
</SupportLink>
}
/>
)}
</>
)}
</div>
{!isLoading && !addonsError && hasHiddenOptions && (
<Button
type="default"
size="tiny"
className="mt-4"
aria-expanded={showAllSizes}
// Prevent collapsing when the selected size would become hidden
disabled={showAllSizes && selectedOptionIsHidden}
onClick={() => setShowAllSizes((prev) => !prev)}
icon={
<ChevronRight
size={14}
strokeWidth={1.5}
className={cn('transition-transform', showAllSizes && '-rotate-90')}
/>
}
>
{showAllSizes ? 'Show fewer sizes' : 'Show all sizes'}
</Button>
)}
</FormItemLayout>
</RadioGroupCard>
)}