TotalUsage.tsx254 lines · main
1import { useBreakpoint } from 'common'
2import { useMemo } from 'react'
3import { cn } from 'ui'
4import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
5
6import { BILLING_BREAKDOWN_METRICS } from '../BillingSettings/BillingBreakdown/BillingBreakdown.constants'
7import { BillingMetric } from '../BillingSettings/BillingBreakdown/BillingMetric'
8import { ComputeMetric } from '../BillingSettings/BillingBreakdown/ComputeMetric'
9import { SectionContent } from './SectionContent'
10import AlertError from '@/components/ui/AlertError'
11import {
12 ComputeUsageMetric,
13 computeUsageMetricLabel,
14 PricingMetric,
15} from '@/data/analytics/org-daily-stats-query'
16import type { OrgSubscription } from '@/data/subscriptions/types'
17import { useOrgUsageQuery } from '@/data/usage/org-usage-query'
18import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
19import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
20import { DOCS_URL } from '@/lib/constants'
21
22export interface ComputeProps {
23 orgSlug: string
24 projectRef?: string | null
25 startDate: string | undefined
26 endDate: string | undefined
27 subscription: OrgSubscription | undefined
28 currentBillingCycleSelected: boolean
29}
30
31const METRICS_TO_HIDE_WITH_NO_USAGE: PricingMetric[] = [
32 PricingMetric.DISK_IOPS_IO2,
33 PricingMetric.DISK_IOPS_GP3,
34 PricingMetric.DISK_SIZE_GB_HOURS_GP3,
35 PricingMetric.DISK_SIZE_GB_HOURS_IO2,
36 PricingMetric.DISK_THROUGHPUT_GP3,
37 PricingMetric.LOG_INGESTION,
38 PricingMetric.LOG_STORAGE,
39 PricingMetric.LOG_QUERYING,
40 PricingMetric.ACTIVE_COMPUTE_HOURS,
41]
42
43export const TotalUsage = ({
44 orgSlug,
45 projectRef,
46 subscription,
47 startDate,
48 endDate,
49 currentBillingCycleSelected,
50}: ComputeProps) => {
51 const isMobile = useBreakpoint('md')
52 const isUsageBillingEnabled = subscription?.usage_billing_enabled
53 const { billingAll } = useIsFeatureEnabled(['billing:all'])
54 const { data: org } = useSelectedOrganizationQuery()
55 const hasActiveRestriction = Boolean(org?.restriction_status)
56
57 const {
58 data: usage,
59 error: usageError,
60 isPending: isLoadingUsage,
61 isError: isErrorUsage,
62 isSuccess: isSuccessUsage,
63 } = useOrgUsageQuery({
64 orgSlug,
65 projectRef,
66 start: !currentBillingCycleSelected && startDate ? new Date(startDate) : undefined,
67 end: !currentBillingCycleSelected && endDate ? new Date(endDate) : undefined,
68 })
69
70 // When the user filters by project ref or selects a custom timeframe, we only display usage+project breakdown, but no costs/limits
71 const showRelationToSubscription = currentBillingCycleSelected && !projectRef
72
73 const isOnHigherPlan = ['team', 'enterprise', 'platform'].includes(subscription?.plan.id ?? '')
74
75 const hasExceededAnyLimits =
76 showRelationToSubscription &&
77 Boolean(
78 usage?.usages.find(
79 (usageItem) =>
80 // Filter out compute as compute has no quota and is always being charged for
81 !usageItem.metric.startsWith('COMPUTE_') &&
82 !usageItem.unlimited &&
83 usageItem.usage > (usageItem?.pricing_free_units ?? 0)
84 )
85 )
86
87 const sortedBillingMetrics = useMemo(() => {
88 if (!usage) return []
89
90 const breakdownMetrics = BILLING_BREAKDOWN_METRICS.filter((metric) =>
91 usage.usages.some((usage) => usage.metric === metric.key)
92 ).filter((metric) => {
93 if (!METRICS_TO_HIDE_WITH_NO_USAGE.includes(metric.key as PricingMetric)) return true
94
95 const metricUsage = usage.usages.find((it) => it.metric === metric.key)
96
97 return metricUsage && metricUsage.usage > 0
98 })
99
100 return breakdownMetrics.slice().sort((a, b) => {
101 const usageMetaA = usage.usages.find((x) => x.metric === a.key)
102 const usageRatioA =
103 typeof usageMetaA !== 'number'
104 ? (usageMetaA?.usage ?? 0) / (usageMetaA?.pricing_free_units ?? 0)
105 : 0
106
107 const usageMetaB = usage.usages.find((x) => x.metric === b.key)
108 const usageRatioB =
109 typeof usageMetaB !== 'number'
110 ? (usageMetaB?.usage ?? 0) / (usageMetaB?.pricing_free_units ?? 0)
111 : 0
112
113 return (
114 // Sort unavailable features to bottom
115 Number(usageMetaB?.available_in_plan) - Number(usageMetaA?.available_in_plan) ||
116 // Sort high-usage features to top
117 usageRatioB - usageRatioA
118 )
119 })
120 }, [usage])
121
122 const computeMetrics = (usage?.usages || [])
123 .filter((it) => it.metric.startsWith('COMPUTE'))
124 .map((it) => it.metric) as ComputeUsageMetric[]
125
126 return (
127 <div id="summary">
128 <SectionContent
129 section={{
130 name: 'Usage Summary',
131 description: isUsageBillingEnabled
132 ? `Your plan includes a limited amount of usage. If exceeded, you will be charged for the overages. It may take up to 1 hour to refresh.`
133 : `Your plan includes a limited amount of usage. If exceeded, you may experience restrictions, as you are currently not billed for overages. It may take up to 1 hour to refresh.`,
134 links: billingAll
135 ? [
136 {
137 name: 'How billing works',
138 url: `${DOCS_URL}/guides/platform/billing-on-briven`,
139 },
140 {
141 name: 'Briven Plans',
142 url: 'https://supabase.com/pricing',
143 },
144 ]
145 : [],
146 }}
147 >
148 {isLoadingUsage && (
149 <div className="space-y-2">
150 <ShimmeringLoader />
151 <ShimmeringLoader className="w-3/4" />
152 <ShimmeringLoader className="w-1/2" />
153 </div>
154 )}
155
156 {isErrorUsage && <AlertError subject="Failed to retrieve usage data" error={usageError} />}
157
158 {isSuccessUsage && subscription && (
159 <div>
160 {showRelationToSubscription && !isOnHigherPlan && !hasActiveRestriction && (
161 <p className="text-sm">
162 {!hasExceededAnyLimits ? (
163 <span>
164 You have not exceeded your{' '}
165 <span className="font-medium">{subscription?.plan.name}</span> Plan quota in
166 this billing cycle.
167 </span>
168 ) : hasExceededAnyLimits && subscription?.plan?.id === 'free' ? (
169 <span>
170 You have exceeded your{' '}
171 <span className="font-medium">{subscription?.plan.name}</span> Plan quota in
172 this billing cycle. Upgrade your plan to continue using Briven without
173 restrictions.
174 </span>
175 ) : hasExceededAnyLimits &&
176 subscription?.usage_billing_enabled === false &&
177 subscription?.plan?.id === 'pro' ? (
178 <span>
179 You have exceeded your{' '}
180 <span className="font-medium">{subscription?.plan.name}</span> Plan quota in
181 this billing cycle. Disable your spend cap to continue using Briven without
182 restrictions.
183 </span>
184 ) : hasExceededAnyLimits && subscription?.usage_billing_enabled === true ? (
185 <span>
186 You have exceeded your{' '}
187 <span className="font-medium">{subscription?.plan.name}</span> Plan quota in
188 this billing cycle and will be charged for over-usage.
189 </span>
190 ) : (
191 <span>
192 You have not exceeded your{' '}
193 <span className="font-medium">{subscription?.plan.name}</span> Plan quota in
194 this billing cycle.
195 </span>
196 )}
197 </p>
198 )}
199 <div className="grid grid-cols-2 mt-3 gap-px bg-border">
200 {sortedBillingMetrics.map((metric, i) => {
201 return (
202 <div
203 key={metric.key}
204 className={cn('col-span-2 md:col-span-1 bg-sidebar space-y-4 py-4')}
205 >
206 <BillingMetric
207 idx={i}
208 slug={orgSlug}
209 metric={metric}
210 usage={usage}
211 subscription={subscription!}
212 relativeToSubscription={showRelationToSubscription}
213 className={cn(i % 2 === 0 ? 'md:pr-4' : 'md:pl-4')}
214 />
215 </div>
216 )
217 })}
218
219 {computeMetrics.map((metric, i) => {
220 return (
221 <div
222 key={metric}
223 className={cn('col-span-2 md:col-span-1 bg-sidebar space-y-4 py-4')}
224 >
225 <ComputeMetric
226 slug={orgSlug}
227 metric={{
228 key: metric,
229 name: computeUsageMetricLabel(metric) + ' Compute Hours' || metric,
230 units: 'hours',
231 anchor: 'compute',
232 category: 'Compute',
233 unitName: 'GB',
234 }}
235 relativeToSubscription={showRelationToSubscription}
236 usage={usage}
237 className={cn(
238 (i + sortedBillingMetrics.length) % 2 === 0 ? 'md:pr-4' : 'md:pl-4'
239 )}
240 />
241 </div>
242 )
243 })}
244
245 {!isMobile && (sortedBillingMetrics.length + computeMetrics.length) % 2 === 1 && (
246 <div className="col-span-2 md:col-span-1 bg-sidebar" />
247 )}
248 </div>
249 </div>
250 )}
251 </SectionContent>
252 </div>
253 )
254}