TotalUsage.tsx 9.6 KB

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