AttributeUsage.tsx 12 KB

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