AttributeUsage.tsx276 lines · main
1import { AlertTriangle, BarChart2 } from 'lucide-react'
2import Link from 'next/link'
3import { useMemo } from 'react'
4import { Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
5import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
6
7import { SectionContent } from '../SectionContent'
8import { CategoryAttribute } from '../Usage.constants'
9import {
10 ChartTooltipValueFormatter,
11 ChartYFormatterCompactNumber,
12 useGetUpgradeUrl,
13} from '../Usage.utils'
14import UsageBarChart from '../UsageBarChart'
15import { ChartMeta } from './UsageSection'
16import AlertError from '@/components/ui/AlertError'
17import Panel from '@/components/ui/Panel'
18import SparkBar from '@/components/ui/SparkBar'
19import type { OrgSubscription } from '@/data/subscriptions/types'
20import type { OrgMetricsUsage, OrgUsageResponse } from '@/data/usage/org-usage-query'
21import { USAGE_APPROACHING_THRESHOLD } from '@/lib/constants'
22import type { ResponseError } from '@/types'
23
24export interface AttributeUsageProps {
25 slug: string
26 projectRef?: string | null
27 attribute: CategoryAttribute
28 usage?: OrgUsageResponse
29 usageMeta?: OrgMetricsUsage
30 chartMeta: ChartMeta
31 subscription?: OrgSubscription
32
33 error: ResponseError | null
34 isLoading: boolean
35 isError: boolean
36 isSuccess: boolean
37
38 currentBillingCycleSelected: boolean
39}
40
41const AttributeUsage = ({
42 slug,
43 projectRef,
44 attribute,
45 usage,
46 usageMeta,
47 chartMeta,
48 subscription,
49
50 error,
51 isLoading,
52 isError,
53 isSuccess,
54 currentBillingCycleSelected,
55}: AttributeUsageProps) => {
56 const upgradeUrl = useGetUpgradeUrl(slug ?? '', subscription, attribute.key)
57 const usageRatio = (usageMeta?.usage ?? 0) / (usageMeta?.pricing_free_units ?? 0)
58 const usageExcess = (usageMeta?.usage ?? 0) - (usageMeta?.pricing_free_units ?? 0)
59 const usageBasedBilling = subscription?.usage_billing_enabled
60 const exceededLimitStyle = !usageBasedBilling ? 'text-red-900' : 'text-amber-900'
61
62 const chartData = useMemo(() => chartMeta[attribute.key]?.data ?? [], [attribute.key, chartMeta])
63
64 const showUsageWarning =
65 projectRef === undefined &&
66 currentBillingCycleSelected &&
67 usageBasedBilling === false &&
68 usageRatio >= USAGE_APPROACHING_THRESHOLD
69
70 const notAllValuesZero = useMemo(() => {
71 return (
72 attribute.attributes
73 ?.map((attr) => {
74 return chartData.some((dataPoint) => Number(dataPoint[attr.key]) !== 0)
75 })
76 .some((x) => !!x) ?? false
77 )
78 }, [attribute.attributes, chartData])
79
80 return (
81 <div id={attribute.anchor} className="scroll-my-12">
82 <SectionContent section={attribute}>
83 {isLoading && (
84 <div className="space-y-2">
85 <ShimmeringLoader />
86 <ShimmeringLoader className="w-3/4" />
87 <ShimmeringLoader className="w-1/2" />
88 </div>
89 )}
90
91 {isError && <AlertError subject="Failed to retrieve usage data" error={error} />}
92
93 {isSuccess && (
94 <>
95 {!usageMeta || usageMeta?.available_in_plan ? (
96 <>
97 {!projectRef && (
98 <div className="space-y-2">
99 <div className="flex items-center justify-between">
100 <div className="flex items-center space-x-4">
101 <p className="text-sm">{attribute.name} usage</p>
102 {showUsageWarning && (
103 <Tooltip>
104 <TooltipTrigger asChild>
105 {usageRatio >= 1 ? (
106 <div className="flex items-center space-x-2 min-w-[115px] cursor-help">
107 <AlertTriangle
108 size={14}
109 strokeWidth={2}
110 className={exceededLimitStyle}
111 />
112 <p className={`text-sm ${exceededLimitStyle}`}>Exceeded limit</p>
113 </div>
114 ) : (
115 usageRatio >= USAGE_APPROACHING_THRESHOLD && (
116 <div className="flex items-center space-x-2 min-w-[115px] cursor-help">
117 <AlertTriangle
118 size={14}
119 strokeWidth={2}
120 className="text-amber-900"
121 />
122 <p className="text-sm text-amber-900">Approaching limit</p>
123 </div>
124 )
125 )}
126 </TooltipTrigger>
127 <TooltipContent side="bottom">
128 <p>
129 Exceeding your plans included usage will lead to restrictions to
130 your project.
131 </p>
132 <p>
133 Upgrade to a usage-based plan or disable the spend cap to avoid
134 restrictions.
135 </p>
136 </TooltipContent>
137 </Tooltip>
138 )}
139 </div>
140 </div>
141
142 {currentBillingCycleSelected && usageMeta && !usageMeta.unlimited && (
143 <SparkBar
144 type="horizontal"
145 barClass={cn(
146 usageRatio >= 1
147 ? usageBasedBilling
148 ? 'bg-foreground-light'
149 : 'bg-red-900'
150 : usageBasedBilling === false &&
151 usageRatio >= USAGE_APPROACHING_THRESHOLD
152 ? 'bg-amber-900'
153 : 'bg-foreground-light'
154 )}
155 bgClass="bg-surface-300"
156 value={usageMeta?.usage ?? 0}
157 max={usageMeta?.pricing_free_units || 1}
158 />
159 )}
160
161 <div>
162 {usageMeta && usageMeta.pricing_free_units !== 0 && (
163 <div className="flex items-center justify-between border-b py-1">
164 <p className="text-xs text-foreground-light">
165 Included in {subscription?.plan?.name} Plan
166 </p>
167 {usageMeta.unlimited ? (
168 <p className="text-xs">Unlimited</p>
169 ) : (
170 <p className="text-xs">
171 {attribute.unit === 'bytes' || attribute.unit === 'gigabytes'
172 ? `${usageMeta.pricing_free_units ?? 0} GB`
173 : (usageMeta.pricing_free_units ?? 0).toLocaleString()}
174 </p>
175 )}
176 </div>
177 )}
178 {currentBillingCycleSelected && usageMeta && (
179 <div className="flex items-center justify-between py-1">
180 <p className="text-xs text-foreground-light">
181 {attribute.chartPrefix || 'Used '} in period
182 </p>
183 <p className="text-xs">
184 {attribute.unit === 'bytes' || attribute.unit === 'gigabytes'
185 ? `${(usageMeta?.usage ?? 0).toFixed(2)} GB`
186 : (usageMeta?.usage ?? 0).toLocaleString()}
187 </p>
188 </div>
189 )}
190 {currentBillingCycleSelected && (usageMeta?.pricing_free_units ?? 0) > 0 && (
191 <div className="flex items-center justify-between border-t py-1">
192 <p className="text-xs text-foreground-light">Overage in period</p>
193 <p className="text-xs">
194 {(usageMeta?.pricing_free_units ?? 0) === -1 || usageExcess < 0
195 ? `0${attribute.unit === 'bytes' || attribute.unit === 'gigabytes' ? ' GB' : ''}`
196 : attribute.unit === 'bytes' || attribute.unit === 'gigabytes'
197 ? `${usageExcess.toFixed(2)} GB`
198 : usageExcess.toLocaleString()}
199 </p>
200 </div>
201 )}
202 </div>
203 </div>
204 )}
205
206 {attribute.additionalInfo?.(usage)}
207
208 <div className="space-y-1">
209 <p className="text-sm">
210 {attribute.chartPrefix || ''} {attribute.name}{' '}
211 {attribute.chartSuffix || 'per day'}
212 </p>
213 {attribute.chartDescription.split('\n').map((paragraph, idx) => (
214 <p key={`para-${idx}`} className="text-sm text-foreground-light">
215 {paragraph}
216 </p>
217 ))}
218 </div>
219 {chartMeta[attribute.key].isLoading ? (
220 <div className="space-y-2">
221 <ShimmeringLoader />
222 <ShimmeringLoader className="w-3/4" />
223 <ShimmeringLoader className="w-1/2" />
224 </div>
225 ) : chartData.length > 0 && notAllValuesZero ? (
226 <UsageBarChart
227 name={`${attribute.chartPrefix || ''} ${attribute.name}`}
228 unit={attribute.unit}
229 attributes={attribute.attributes}
230 data={chartData}
231 yLeftMargin={chartMeta[attribute.key].margin}
232 yFormatter={(value) => ChartYFormatterCompactNumber(value, attribute.unit)}
233 tooltipFormatter={(value) => ChartTooltipValueFormatter(value, attribute.unit)}
234 />
235 ) : (
236 <Panel>
237 <Panel.Content>
238 <div className="flex flex-col items-center justify-center">
239 <BarChart2 className="text-foreground-light mb-2" />
240 <p className="text-sm">No data in period</p>
241 <p className="text-sm text-foreground-light">
242 May take up to 24 hours to show
243 </p>
244 </div>
245 </Panel.Content>
246 </Panel>
247 )}
248 </>
249 ) : (
250 <Panel>
251 <Panel.Content>
252 <div className="flex w-full items-center flex-col justify-center space-y-2 md:flex-row md:justify-between">
253 <div className="space-y-1">
254 <p className="text-sm">Not included in plan</p>
255 <div>
256 <p className="text-sm text-foreground-light">
257 You need to be on a higher plan in order to use this feature.
258 </p>
259 </div>
260 </div>
261
262 <Button type="primary" asChild>
263 <Link href={upgradeUrl}>Upgrade plan</Link>
264 </Button>
265 </div>
266 </Panel.Content>
267 </Panel>
268 )}
269 </>
270 )}
271 </SectionContent>
272 </div>
273 )
274}
275
276export default AttributeUsage