feat(studio): nano exhaustion indicators (#44395)

## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

This PR introduces some visual indicators when a Nano project is
exhausting resources on Pro plus plans. The indicator tells the user a
free upgrade to Micro is available via a flashing Nano badge that guides
them to Upgrade and a global banner that notifies them resources are
near exhaustion, please upgrade for free.

| Project List | Project View |
|--------|--------|
| <img width="412" height="223" alt="Screenshot 2026-03-31 at 16 11 19"
src="https://github.com/user-attachments/assets/0d0e7727-e4eb-4ade-9024-528a2501596c"
/> | <img width="1256" height="618" alt="Screenshot 2026-03-31 at 16 11
10"
src="https://github.com/user-attachments/assets/b664e020-2b18-4842-8e64-4e49fac69eef"
/> |

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

* **New Features**
* Free Micro Upgrade banner appears for nano-tier projects near compute
exhaustion with animated visuals and an “Upgrade for free” CTA.
* Compute badge highlights near-exhaustion with glow/shimmer and an
animated upgrade indicator; optional icon support added.
* Per-project banner dismissal is saved locally; dismissed banners
remain hidden.
* **Tests**
  * Added tests covering banner display and dismissal scenarios.
* **Chores**
* Added local-storage keys and telemetry events for banner and upgrade
interactions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
kemal.earth authored and GitHub committed 2026-04-01 11:48:43 +01:00
1 parent c3af2858bd
commit 908da85225
12 files changed
+398 -23

No files matched your search

@@ -126,6 +126,8 @@ export const ProjectCard = ({
projectRef={project.ref}
cloudProvider={project.cloud_provider}
computeSize={getComputeSize(project)}
resourceWarnings={resourceWarnings}
badgeClassName="text-[10px] leading-none tracking-[0.07em]"
/>
)}
{isVercelIntegrated && (
@@ -137,6 +137,7 @@ export const ProjectTableRow = ({
projectRef={project.ref}
cloudProvider={project.cloud_provider}
computeSize={getComputeSize(project)}
resourceWarnings={resourceWarnings}
/>
) : (
<span className="text-xs text-foreground-muted">–</span>
@@ -7,6 +7,7 @@ import { InlineLink } from 'components/ui/InlineLink'
import { ProjectUpgradeFailedBanner } from 'components/ui/ProjectUpgradeFailedBanner'
import { useBranchesQuery } from 'data/branches/branches-query'
import { useProjectDetailQuery } from 'data/projects/project-detail-query'
import { useResourceWarningsQuery } from 'data/usage/resource-warnings-query'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useIsOrioleDb, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DOCS_URL, PROJECT_STATUS } from 'lib/constants'
@@ -20,6 +21,8 @@ export const TopSection = () => {
const isOrioleDb = useIsOrioleDb()
const { data: project } = useSelectedProjectQuery()
const { data: organization } = useSelectedOrganizationQuery()
const { data: resourceWarnings } = useResourceWarningsQuery({ slug: organization?.slug })
const projectResourceWarnings = resourceWarnings?.find((w) => w.project === project?.ref)
const { data: parentProject } = useProjectDetailQuery({ ref: project?.parent_project_ref })
const { data: branches } = useBranchesQuery({
@@ -79,6 +82,7 @@ export const TopSection = () => {
slug={organization?.slug}
cloudProvider={project?.cloud_provider}
computeSize={project?.infra_compute_size}
resourceWarnings={projectResourceWarnings}
/>
{project?.high_availability && <HighAvailabilityBadge />}
</div>
@@ -17,6 +17,28 @@ const { mockRouter, mockSetSelectedDatabaseId, mockSetMobileMenuOpen } = vi.hois
mockSetMobileMenuOpen: vi.fn(),
}))
const {
mockAddBanner,
mockDismissBanner,
mockProjectState,
mockResourceWarningsState,
mockBannerDismissedState,
} = vi.hoisted(() => ({
mockAddBanner: vi.fn(),
mockDismissBanner: vi.fn(),
mockProjectState: {
current: {
ref: 'default',
name: 'Project 1',
status: 'ACTIVE_HEALTHY',
postgrestStatus: 'ONLINE',
infra_compute_size: undefined as string | undefined,
},
},
mockResourceWarningsState: { current: undefined as any[] | undefined },
mockBannerDismissedState: { current: false },
}))
vi.mock('next/router', () => ({
useRouter: () => mockRouter,
}))
@@ -47,6 +69,12 @@ vi.mock('common', () => ({
mergeRefs:
(..._refs: any[]) =>
(_value: unknown) => {},
IS_PLATFORM: false,
LOCAL_STORAGE_KEYS: {
FREE_MICRO_UPGRADE_BANNER_DISMISSED: (ref: string) =>
`free-micro-upgrade-banner-dismissed-${ref}`,
},
isFeatureEnabled: () => false,
}))
vi.mock('framer-motion', () => ({
@@ -121,6 +149,27 @@ vi.mock('@/hooks/custom-content/useCustomContent', () => ({
useCustomContent: () => ({ appTitle: 'Supabase' }),
}))
vi.mock('@/hooks/misc/useLocalStorage', () => ({
useLocalStorageQuery: () => [mockBannerDismissedState.current, vi.fn()],
}))
vi.mock('@/components/ui/BannerStack/BannerStackProvider', () => ({
BANNER_ID: { FREE_MICRO_UPGRADE: 'free-micro-upgrade-banner' },
useBannerStack: () => ({
addBanner: mockAddBanner,
dismissBanner: mockDismissBanner,
banners: [],
}),
}))
vi.mock('@/components/ui/BannerStack/Banners/BannerFreeMicroUpgrade', () => ({
BannerFreeMicroUpgrade: () => null,
}))
vi.mock('data/usage/resource-warnings-query', () => ({
useResourceWarningsQuery: () => ({ data: mockResourceWarningsState.current }),
}))
vi.mock('@/hooks/misc/useSelectedOrganization', () => ({
useSelectedOrganizationQuery: () => ({
data: { name: 'Organization 1', slug: 'org-1' },
@@ -128,14 +177,7 @@ vi.mock('@/hooks/misc/useSelectedOrganization', () => ({
}))
vi.mock('@/hooks/misc/useSelectedProject', () => ({
useSelectedProjectQuery: () => ({
data: {
ref: 'default',
name: 'Project 1',
status: 'ACTIVE_HEALTHY',
postgrestStatus: 'ONLINE',
},
}),
useSelectedProjectQuery: () => ({ data: mockProjectState.current }),
}))
vi.mock('@/hooks/misc/withAuth', () => ({
@@ -160,6 +202,15 @@ vi.mock('@/state/database-selector', () => ({
}),
}))
const renderLayout = () =>
render(
<MobileSheetProvider>
<ProjectLayout product="Database" isBlocking={false}>
<div />
</ProjectLayout>
</MobileSheetProvider>
)
describe('ProjectLayout title', () => {
beforeEach(() => {
mockRouter.pathname = '/project/[ref]/observability/query-performance'
@@ -210,3 +261,91 @@ describe('ProjectLayout title', () => {
})
})
})
describe('FREE_MICRO_UPGRADE banner', () => {
beforeEach(() => {
mockRouter.pathname = '/project/[ref]'
mockRouter.asPath = '/project/default'
mockProjectState.current = {
ref: 'default',
name: 'Project 1',
status: 'ACTIVE_HEALTHY',
postgrestStatus: 'ONLINE',
infra_compute_size: 'nano',
}
mockResourceWarningsState.current = [
{
project: 'default',
cpu_exhaustion: true,
memory_and_swap_exhaustion: false,
disk_space_exhaustion: false,
},
]
mockBannerDismissedState.current = false
})
afterEach(() => {
vi.clearAllMocks()
mockRouter.pathname = '/project/[ref]/observability/query-performance'
mockRouter.asPath = '/project/default/observability/query-performance'
mockProjectState.current = {
ref: 'default',
name: 'Project 1',
status: 'ACTIVE_HEALTHY',
postgrestStatus: 'ONLINE',
infra_compute_size: undefined,
}
mockResourceWarningsState.current = undefined
mockBannerDismissedState.current = false
})
it('calls addBanner when project is nano and compute is near exhaustion', async () => {
renderLayout()
await waitFor(() => {
expect(mockAddBanner).toHaveBeenCalledWith(
expect.objectContaining({ id: 'free-micro-upgrade-banner' })
)
})
})
it('calls dismissBanner when banner was previously dismissed', async () => {
mockBannerDismissedState.current = true
renderLayout()
await waitFor(() => {
expect(mockDismissBanner).toHaveBeenCalledWith('free-micro-upgrade-banner')
})
expect(mockAddBanner).not.toHaveBeenCalled()
})
it('calls dismissBanner when compute warnings are cleared', async () => {
mockResourceWarningsState.current = [
{
project: 'default',
cpu_exhaustion: false,
memory_and_swap_exhaustion: false,
disk_space_exhaustion: false,
},
]
renderLayout()
await waitFor(() => {
expect(mockDismissBanner).toHaveBeenCalledWith('free-micro-upgrade-banner')
})
expect(mockAddBanner).not.toHaveBeenCalled()
})
it('calls dismissBanner when project is not nano compute', async () => {
mockProjectState.current = { ...mockProjectState.current, infra_compute_size: 'micro' }
renderLayout()
await waitFor(() => {
expect(mockDismissBanner).toHaveBeenCalledWith('free-micro-upgrade-banner')
})
expect(mockAddBanner).not.toHaveBeenCalled()
})
})
@@ -1,4 +1,5 @@
import { mergeRefs, useParams } from 'common'
import { LOCAL_STORAGE_KEYS, mergeRefs, useParams } from 'common'
import { useResourceWarningsQuery } from 'data/usage/resource-warnings-query'
import { AnimatePresence, motion } from 'framer-motion'
import Head from 'next/head'
import { useRouter } from 'next/router'
@@ -38,8 +39,11 @@ import { RestoringState } from './RestoringState'
import { UpgradingState } from './UpgradingState'
import { CreateBranchModal } from '@/components/interfaces/BranchManagement/CreateBranchModal'
import { ProjectAPIDocs } from '@/components/interfaces/ProjectAPIDocs/ProjectAPIDocs'
import { BannerFreeMicroUpgrade } from '@/components/ui/BannerStack/Banners/BannerFreeMicroUpgrade'
import { BANNER_ID, useBannerStack } from '@/components/ui/BannerStack/BannerStackProvider'
import { ResourceExhaustionWarningBanner } from '@/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner'
import { useCustomContent } from '@/hooks/custom-content/useCustomContent'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { withAuth } from '@/hooks/misc/withAuth'
@@ -110,6 +114,24 @@ export const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<Projec
const router = useRouter()
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const { data: selectedProject } = useSelectedProjectQuery()
const { addBanner, dismissBanner } = useBannerStack()
const { data: resourceWarnings } = useResourceWarningsQuery({
slug: selectedOrganization?.slug,
})
const projectResourceWarnings = resourceWarnings?.find(
(w) => w.project === selectedProject?.ref
)
const isComputeNearExhaustion =
!!projectResourceWarnings?.cpu_exhaustion ||
!!projectResourceWarnings?.memory_and_swap_exhaustion ||
!!projectResourceWarnings?.disk_space_exhaustion ||
!!projectResourceWarnings?.disk_io_exhaustion
const isNanoCompute = selectedProject?.infra_compute_size === 'nano'
const showUpgradeBanner = isNanoCompute && isComputeNearExhaustion
const [isFreeMicroUpgradeBannerDismissed] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.FREE_MICRO_UPGRADE_BANNER_DISMISSED(selectedProject?.ref ?? ''),
false
)
const { showSidebar } = useAppStateSnapshot()
const { setContent: setMobileSheetContent, registerOpenMenu } = useMobileSheet()
@@ -153,6 +175,28 @@ export const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<Projec
router.pathname.includes('/project/[ref]/logs')
const showPausedState = isPaused && !ignorePausedState
useEffect(() => {
if (!selectedProject?.ref) return
const isProjectHomepage = router.pathname === '/project/[ref]'
if (isProjectHomepage && showUpgradeBanner && !isFreeMicroUpgradeBannerDismissed) {
addBanner({
id: BANNER_ID.FREE_MICRO_UPGRADE,
isDismissed: false,
content: <BannerFreeMicroUpgrade />,
priority: 2,
})
} else {
dismissBanner(BANNER_ID.FREE_MICRO_UPGRADE)
}
}, [
router.pathname,
selectedProject?.ref,
showUpgradeBanner,
isFreeMicroUpgradeBannerDismissed,
addBanner,
dismissBanner,
])
useLayoutEffect(() => {
const unregister = registerOpenMenu(() => {
setMobileSheetContent(
@@ -1,10 +1,11 @@
import { createContext, useCallback, useContext, useState } from 'react'
const BANNER_ID = {
export const BANNER_ID = {
METRICS_API: 'metrics-api-banner',
INDEX_ADVISOR: 'index-advisor-banner',
TABLE_EDITOR_QUEUE_OPERATIONS: 'table-editor-queue-operations-banner',
RLS_EVENT_TRIGGER: 'rls-event-trigger-banner',
FREE_MICRO_UPGRADE: 'free-micro-upgrade-banner',
} as const
export type BannerId = (typeof BANNER_ID)[keyof typeof BANNER_ID]
@@ -0,0 +1,69 @@
import { LOCAL_STORAGE_KEYS } from 'common'
import { useParams } from 'common/hooks'
import { ChevronsUpAnimated } from 'components/ui/ComputeBadgeWrapper'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useTrack } from 'lib/telemetry/track'
import Link from 'next/link'
import { Button } from 'ui'
import { ComputeBadge } from 'ui-patterns/ComputeBadge'
import { BannerCard } from '../BannerCard'
import { BANNER_ID, useBannerStack } from '../BannerStackProvider'
export const BannerFreeMicroUpgrade = () => {
const { ref } = useParams()
const track = useTrack()
const { dismissBanner } = useBannerStack()
const [, setIsDismissed] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.FREE_MICRO_UPGRADE_BANNER_DISMISSED(ref ?? ''),
false
)
return (
<BannerCard
onDismiss={() => {
setIsDismissed(true)
dismissBanner(BANNER_ID.FREE_MICRO_UPGRADE)
track('free_micro_upgrade_banner_dismissed')
}}
>
<div className="flex flex-col gap-y-4">
<div className="flex flex-col gap-y-2 items-start">
<div className="animate-badge-pulse">
<div className="relative inline-flex overflow-hidden rounded">
<ComputeBadge
infraComputeSize="nano"
icon={<ChevronsUpAnimated />}
className="text-brand-600 border-brand-500 bg-brand/10 gap-1"
/>
<span className="animate-badge-shimmer pointer-events-none absolute inset-0 bg-gradient-to-br from-transparent via-brand/20 to-transparent blur-md" />
</div>
</div>
</div>
<div className="flex flex-col gap-y-1 mb-2">
<p className="text-sm font-medium">Project low on resources</p>
<p className="text-xs text-foreground-lighter text-balance">
Your Nano compute is approaching its limits. Your Pro plan includes a free upgrade to
Micro — double the memory at no extra cost.
</p>
</div>
<div className="flex gap-2">
<Button
type="primary"
size="tiny"
asChild
onClick={() => {
setIsDismissed(true)
dismissBanner(BANNER_ID.FREE_MICRO_UPGRADE)
track('free_micro_upgrade_banner_cta_clicked')
}}
>
<Link href={`/project/${ref}/settings/compute-and-disk?upgrade=micro`}>
Upgrade for free
</Link>
</Button>
</div>
</div>
</BannerCard>
)
}
@@ -1,16 +1,40 @@
import { getAddons } from 'components/interfaces/Billing/Subscription/Subscription.utils'
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 { ProjectAddonVariantMeta } from 'data/subscriptions/types'
import { ResourceWarning } from 'data/usage/resource-warnings-query'
import { getCloudProviderArchitecture } from 'lib/cloudprovider-utils'
import { INSTANCE_MICRO_SPECS } from 'lib/constants'
import Link from 'next/link'
import { useState } from 'react'
import { Button, HoverCard, HoverCardContent, HoverCardTrigger, Separator } from 'ui'
import { Button, cn, HoverCard, HoverCardContent, HoverCardTrigger, Separator } from 'ui'
import { ComputeBadge } from 'ui-patterns/ComputeBadge'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { getAddons } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
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 { ProjectAddonVariantMeta } from '@/data/subscriptions/types'
import { getCloudProviderArchitecture } from '@/lib/cloudprovider-utils'
import { INSTANCE_MICRO_SPECS } from '@/lib/constants'
export const ChevronsUpAnimated = () => (
<svg
width={10}
height={10}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline
points="17 18 12 13 7 18"
className="animate-chevron-up"
style={{ animationDelay: '0s' }}
/>
<polyline
points="17 11 12 6 7 11"
className="animate-chevron-up"
style={{ animationDelay: '0.3s' }}
/>
</svg>
)
const Row = ({ label, stat }: { label: string; stat: React.ReactNode | string }) => {
return (
@@ -26,6 +50,8 @@ interface ComputeBadgeWrapperProps {
projectRef?: string
cloudProvider?: string
computeSize?: ProjectDetail['infra_compute_size']
resourceWarnings?: ResourceWarning
badgeClassName?: string
}
export const ComputeBadgeWrapper = ({
@@ -33,6 +59,8 @@ export const ComputeBadgeWrapper = ({
projectRef,
cloudProvider,
computeSize,
resourceWarnings,
badgeClassName,
}: ComputeBadgeWrapperProps) => {
// handles the state of the hover card
// once open it will fetch the addons
@@ -71,6 +99,12 @@ export const ComputeBadgeWrapper = ({
)
const isEligibleForFreeUpgrade = data?.plan.id !== 'free' && computeSize === 'nano'
const isComputeNearExhaustion =
!!resourceWarnings?.cpu_exhaustion ||
!!resourceWarnings?.memory_and_swap_exhaustion ||
!!resourceWarnings?.disk_space_exhaustion ||
!!resourceWarnings?.disk_io_exhaustion
const showUpgradeGlow = isEligibleForFreeUpgrade && isComputeNearExhaustion
const isLoading = isLoadingAddons || isLoadingSubscriptions
@@ -79,8 +113,25 @@ export const ComputeBadgeWrapper = ({
return (
<HoverCard onOpenChange={() => setOpenState(!open)} openDelay={280}>
<HoverCardTrigger asChild className="group" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center">
<ComputeBadge infraComputeSize={computeSize} />
<div className={cn('flex items-center', showUpgradeGlow && 'animate-badge-pulse')}>
<div
className={cn(
'flex',
showUpgradeGlow && 'relative inline-flex overflow-hidden rounded'
)}
>
<ComputeBadge
infraComputeSize={computeSize}
icon={showUpgradeGlow && <ChevronsUpAnimated />}
className={cn(
showUpgradeGlow && 'text-brand-600 border-brand-500 bg-brand/10 gap-1',
badgeClassName
)}
/>
{showUpgradeGlow && (
<span className="animate-badge-shimmer pointer-events-none absolute inset-0 bg-gradient-to-br from-transparent via-brand/20 to-transparent blur-md" />
)}
</div>
</div>
</HoverCardTrigger>
<HoverCardContent
+18
View File
@@ -80,6 +80,9 @@ module.exports = config({
animation: {
shimmer: 'shimmer 2s infinite linear',
'badge-shimmer': 'badge-shimmer 3s ease-in-out infinite',
'badge-pulse': 'badge-pulse 3s ease-in-out infinite',
'chevron-up': 'chevron-up 2s ease-in-out infinite',
sway: 'sway 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
},
keyframes: {
@@ -91,6 +94,21 @@ module.exports = config({
'background-position': '1000px 0',
},
},
'badge-shimmer': {
'0%': { transform: 'rotate(-45deg) translateX(-100%)', opacity: '0' },
'10%': { opacity: '1' },
'40%': { opacity: '1' },
'50%': { transform: 'rotate(-45deg) translateX(100%)', opacity: '0' },
'100%': { transform: 'rotate(-45deg) translateX(100%)', opacity: '0' },
},
'badge-pulse': {
'0%, 100%': { opacity: '1' },
'50%': { opacity: '0.8' },
},
'chevron-up': {
'0%, 100%': { opacity: '0.2' },
'50%': { opacity: '1' },
},
sway: {
'0%, 100%': {
transform: 'rotate(-10deg) scale(1.5) translateY(4rem)',
@@ -116,6 +116,9 @@ export const LOCAL_STORAGE_KEYS = {
TABLE_EDITOR_QUEUE_OPERATIONS_BANNER_DISMISSED: (ref: string) =>
`table-editor-queue-operations-banner-dismissed-${ref}`,
FREE_MICRO_UPGRADE_BANNER_DISMISSED: (ref: string) =>
`free-micro-upgrade-banner-dismissed-${ref}`,
PRIVACY_NOTICE_ACKNOWLEDGED: 'privacy-notice-acknowledged-2026-03',
/**
* COMMON
+41
View File
@@ -2930,6 +2930,44 @@ export interface LogExplorerQueryRunButtonClickedEvent {
groups: TelemetryGroups
}
/**
* User clicked an upgrade CTA inside the compute badge hover card.
*
* @group Events
* @source studio
*/
export interface ComputeBadgeUpgradeClickedEvent {
action: 'compute_badge_upgrade_clicked'
properties: {
computeSize: string
planId: string
upgradeType: 'pro_upgrade' | 'free_micro_upgrade' | 'compute_upgrade'
}
groups: TelemetryGroups
}
/**
* User dismissed the free Micro upgrade banner.
*
* @group Events
* @source studio
*/
export interface FreeMicroUpgradeBannerDismissedEvent {
action: 'free_micro_upgrade_banner_dismissed'
groups: TelemetryGroups
}
/**
* User clicked the CTA on the free Micro upgrade banner.
*
* @group Events
* @source studio
*/
export interface FreeMicroUpgradeBannerCtaClickedEvent {
action: 'free_micro_upgrade_banner_cta_clicked'
groups: TelemetryGroups
}
/**
* User clicked the Navigate action in the storage explorer header.
*
@@ -3119,3 +3157,6 @@ export type TelemetryEvent =
| OrgSubmenuOpenedEvent
| OrgMenuBackClickedEvent
| OrgMenuItemClickedEvent
| ComputeBadgeUpgradeClickedEvent
| FreeMicroUpgradeBannerDismissedEvent
| FreeMicroUpgradeBannerCtaClickedEvent
@@ -1,5 +1,5 @@
import { components } from 'api-types'
import type { HTMLAttributes } from 'react'
import type { HTMLAttributes, ReactNode } from 'react'
import { cn } from 'ui'
interface ComputeBadgeProps extends HTMLAttributes<HTMLDivElement> {
@@ -7,9 +7,10 @@ interface ComputeBadgeProps extends HTMLAttributes<HTMLDivElement> {
| components['schemas']['ProjectDetailResponse']['infra_compute_size']
| '>16XL'
| undefined
icon?: ReactNode
}
export function ComputeBadge({ infraComputeSize, className, ...props }: ComputeBadgeProps) {
export function ComputeBadge({ infraComputeSize, className, icon, ...props }: ComputeBadgeProps) {
const smallCompute =
infraComputeSize?.toLocaleLowerCase() === 'micro' ||
infraComputeSize?.toLocaleLowerCase() === 'nano'
@@ -20,7 +21,7 @@ export function ComputeBadge({ infraComputeSize, className, ...props }: ComputeB
<div
className={cn(
// Base styles
'inline-flex items-center justify-center rounded-md text-center font-mono uppercase',
'inline-flex items-center justify-center rounded text-center font-mono uppercase',
'whitespace-nowrap font-medium tracking-[0.06em] text-[11px] leading-[1.1] px-[5.5px] py-[3px]',
'transition-all',
// Variant styles
@@ -39,6 +40,7 @@ export function ComputeBadge({ infraComputeSize, className, ...props }: ComputeB
{...props}
>
{infraComputeSize}
{icon && <span className="flex items-center">{icon}</span>}
</div>
)
}