ComputeBadgeWrapper.tsx215 lines · main
1import Link from 'next/link'
2import { useState } from 'react'
3import { Button, cn, HoverCard, HoverCardContent, HoverCardTrigger, Separator } from 'ui'
4import { ComputeBadge } from 'ui-patterns/ComputeBadge'
5import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
6
7import { getAvailableComputeOptions } from '@/components/interfaces/DiskManagement/DiskManagement.utils'
8import { ProjectDetail } from '@/data/projects/project-detail-query'
9import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
10import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
11import { ResourceWarning } from '@/data/usage/resource-warnings-query'
12import { getCloudProviderArchitecture } from '@/lib/cloudprovider-utils'
13import { useTrack } from '@/lib/telemetry/track'
14
15export const ChevronsUpAnimated = () => (
16 <svg
17 width={10}
18 height={10}
19 viewBox="0 0 24 24"
20 fill="none"
21 stroke="currentColor"
22 strokeWidth="2.5"
23 strokeLinecap="round"
24 strokeLinejoin="round"
25 >
26 <polyline
27 points="17 18 12 13 7 18"
28 className="animate-chevron-up"
29 style={{ animationDelay: '0s' }}
30 />
31 <polyline
32 points="17 11 12 6 7 11"
33 className="animate-chevron-up"
34 style={{ animationDelay: '0.3s' }}
35 />
36 </svg>
37)
38
39const Row = ({ label, stat }: { label: string; stat: React.ReactNode | string }) => {
40 return (
41 <div className="flex flex-row gap-2">
42 <span className="text-sm text-foreground-light w-16">{label}</span>
43 <span className="text-sm">{stat}</span>
44 </div>
45 )
46}
47
48interface ComputeBadgeWrapperProps {
49 slug?: string
50 projectRef?: string
51 cloudProvider?: string
52 computeSize?: ProjectDetail['infra_compute_size']
53 resourceWarnings?: ResourceWarning
54 badgeClassName?: string
55}
56
57export const ComputeBadgeWrapper = ({
58 slug,
59 projectRef,
60 cloudProvider,
61 computeSize,
62 resourceWarnings,
63 badgeClassName,
64}: ComputeBadgeWrapperProps) => {
65 // handles the state of the hover card
66 // once open it will fetch the addons
67 const [open, setOpenState] = useState(false)
68
69 // returns hardcoded values for infra
70 const cpuArchitecture = getCloudProviderArchitecture(cloudProvider)
71
72 // fetches addons
73 const { data: addons, isPending: isLoadingAddons } = useProjectAddonsQuery(
74 { projectRef },
75 { enabled: open }
76 )
77
78 // Derive cores/memory from the same source as the badge (infra_compute_size) by looking up
79 // the matching variant in available_addons. Sourcing from selected_addons can drift out of
80 // sync with infra_compute_size and produce a card that contradicts its own badge.
81 const computeOptions = getAvailableComputeOptions(addons?.available_addons ?? [], cloudProvider)
82 const meta = computeOptions.find((variant) => variant.identifier === `ci_${computeSize}`)?.meta
83
84 const highestComputeAvailable = computeOptions[computeOptions.length - 1]?.identifier
85 const isHighestCompute = computeSize === highestComputeAvailable?.replace('ci_', '')
86
87 const { data, isPending: isLoadingSubscriptions } = useOrgSubscriptionQuery(
88 { orgSlug: slug },
89 { enabled: open }
90 )
91
92 const isEligibleForFreeUpgrade = data?.plan.id !== 'free' && computeSize === 'nano'
93 const isComputeNearExhaustion =
94 !!resourceWarnings?.cpu_exhaustion ||
95 !!resourceWarnings?.memory_and_swap_exhaustion ||
96 !!resourceWarnings?.disk_space_exhaustion ||
97 !!resourceWarnings?.disk_io_exhaustion
98 const showUpgradeGlow = isEligibleForFreeUpgrade && isComputeNearExhaustion
99
100 const track = useTrack()
101
102 const isLoading = isLoadingAddons || isLoadingSubscriptions
103
104 if (!computeSize) return null
105
106 return (
107 <HoverCard onOpenChange={() => setOpenState(!open)} openDelay={280}>
108 <HoverCardTrigger asChild className="group" onClick={(e) => e.stopPropagation()}>
109 <div className={cn('flex items-center', showUpgradeGlow && 'animate-badge-pulse')}>
110 <div
111 className={cn(
112 'flex',
113 showUpgradeGlow && 'relative inline-flex overflow-hidden rounded-sm'
114 )}
115 >
116 <ComputeBadge
117 infraComputeSize={computeSize}
118 icon={showUpgradeGlow && <ChevronsUpAnimated />}
119 className={cn(
120 showUpgradeGlow && 'text-brand-600 border-brand-500 bg-brand/10 gap-1',
121 badgeClassName
122 )}
123 />
124 {showUpgradeGlow && (
125 <span className="animate-badge-shimmer pointer-events-none absolute inset-0 bg-linear-to-br from-transparent via-brand/20 to-transparent blur-md" />
126 )}
127 </div>
128 </div>
129 </HoverCardTrigger>
130 <HoverCardContent
131 side="bottom"
132 align="start"
133 className="p-0 overflow-hidden w-96"
134 onClick={(e) => e.stopPropagation()}
135 >
136 <div className="p-2 px-5 text-xs text-foreground-lighter">Compute size</div>
137 <Separator />
138 <div className="p-3 px-5 flex flex-row gap-4">
139 <div>
140 <ComputeBadge infraComputeSize={computeSize} />
141 </div>
142 <div className="flex flex-col gap-4">
143 {isLoading ? (
144 <>
145 <div className="flex flex-col gap-1">
146 <ShimmeringLoader className="h-[20px] py-0 w-32" />
147 <ShimmeringLoader className="h-[20px] py-0 w-32" />
148 </div>
149 </>
150 ) : (
151 <>
152 <div className="flex flex-col gap-1">
153 {computeSize === 'nano' ? (
154 <>
155 <Row label="CPU" stat="Shared" />
156 <Row label="Memory" stat="Up to 0.5 GB" />
157 </>
158 ) : meta !== undefined ? (
159 <>
160 <Row
161 label="CPU"
162 stat={`${meta.cpu_cores ?? '?'}-core ${cpuArchitecture} ${meta.cpu_dedicated ? '(Dedicated)' : '(Shared)'}`}
163 />
164 <Row label="Memory" stat={`${meta.memory_gb ?? '-'} GB`} />
165 </>
166 ) : null}
167 </div>
168 </>
169 )}
170 </div>
171 </div>
172 {(!isHighestCompute || isEligibleForFreeUpgrade) && (
173 <>
174 <Separator />
175 <div className="p-3 px-5 text-sm flex flex-col gap-2 bg-studio">
176 <div className="flex flex-col gap-0">
177 <p className="text-foreground">
178 {isEligibleForFreeUpgrade
179 ? 'Free upgrade to Micro available'
180 : 'Unlock more compute'}
181 </p>
182 <p className="text-foreground-light">
183 {isEligibleForFreeUpgrade
184 ? 'Paid plans include a free upgrade to Micro compute.'
185 : 'Scale your project up to 64 cores and 256 GB RAM.'}
186 </p>
187 </div>
188 <div>
189 <Button
190 asChild
191 type="default"
192 htmlType="button"
193 role="button"
194 onClick={() => {
195 track('compute_badge_upgrade_clicked', {
196 computeSize: computeSize ?? 'unknown',
197 planId: data?.plan.id ?? 'unknown',
198 upgradeType: isEligibleForFreeUpgrade
199 ? 'free_micro_upgrade'
200 : 'compute_upgrade',
201 })
202 }}
203 >
204 <Link href={`/project/${projectRef}/settings/compute-and-disk`}>
205 Upgrade compute
206 </Link>
207 </Button>
208 </div>
209 </div>
210 </>
211 )}
212 </HoverCardContent>
213 </HoverCard>
214 )
215}