Usage.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. import { PermissionAction } from '@supabase/shared-types/out/constants'
  2. import { useParams } from 'common'
  3. import dayjs from 'dayjs'
  4. import { Check, ChevronDown } from 'lucide-react'
  5. import Link from 'next/link'
  6. import { useQueryState } from 'nuqs'
  7. import { useMemo, useState } from 'react'
  8. import { Button, cn, CommandGroup, CommandItem } from 'ui'
  9. import { Admonition } from 'ui-patterns'
  10. import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
  11. import { Restriction } from '../BillingSettings/Restriction'
  12. import ActiveCompute from './ActiveCompute'
  13. import Activity from './Activity'
  14. import Compute from './Compute'
  15. import Egress from './Egress'
  16. import OrgLogUsage from './OrgLogUsage'
  17. import SizeAndCounts from './SizeAndCounts'
  18. import { TotalUsage } from './TotalUsage'
  19. import {
  20. ScaffoldContainer,
  21. ScaffoldHeader,
  22. ScaffoldSection,
  23. ScaffoldTitle,
  24. } from '@/components/layouts/Scaffold'
  25. import AlertError from '@/components/ui/AlertError'
  26. import DateRangePicker from '@/components/ui/DateRangePicker'
  27. import NoPermission from '@/components/ui/NoPermission'
  28. import { OrganizationProjectSelector } from '@/components/ui/OrganizationProjectSelector'
  29. import { useOrgDailyStatsQuery } from '@/data/analytics/org-daily-stats-query'
  30. import { useProjectDetailQuery } from '@/data/projects/project-detail-query'
  31. import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
  32. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  33. import { TIME_PERIODS_BILLING, TIME_PERIODS_REPORTS } from '@/lib/constants/metrics'
  34. export const Usage = () => {
  35. const { slug } = useParams()
  36. const [dateRange, setDateRange] = useState<any>()
  37. const [selectedProjectRef, setSelectedProjectRef] = useQueryState('projectRef')
  38. const [openProjectSelector, setOpenProjectSelector] = useState(false)
  39. const { can: canReadSubscriptions, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
  40. PermissionAction.BILLING_READ,
  41. 'stripe.subscriptions'
  42. )
  43. const {
  44. data: subscription,
  45. error: subscriptionError,
  46. isPending: isLoadingSubscription,
  47. isError: isErrorSubscription,
  48. isSuccess: isSuccessSubscription,
  49. } = useOrgSubscriptionQuery({ orgSlug: slug })
  50. const { data: selectedProject } = useProjectDetailQuery({
  51. ref: selectedProjectRef ?? undefined,
  52. })
  53. const billingCycleStart = useMemo(() => {
  54. return dayjs.unix(subscription?.current_period_start ?? 0).utc()
  55. }, [subscription])
  56. const billingCycleEnd = useMemo(() => {
  57. return dayjs.unix(subscription?.current_period_end ?? 0).utc()
  58. }, [subscription])
  59. const currentBillingCycleSelected = useMemo(() => {
  60. // Selected by default
  61. if (!dateRange?.period_start || !dateRange?.period_end) return true
  62. return (
  63. dayjs(dateRange.period_start.date).isSame(billingCycleStart) &&
  64. dayjs(dateRange.period_end.date).isSame(billingCycleEnd)
  65. )
  66. }, [dateRange, billingCycleStart, billingCycleEnd])
  67. const startDate = useMemo(() => {
  68. // If end date is in future, set end date to now
  69. if (!dateRange?.period_start?.date) {
  70. return undefined
  71. } else {
  72. // LF seems to have an issue with the milliseconds, causes infinite loading sometimes
  73. return new Date(dateRange?.period_start?.date).toISOString().slice(0, -5) + 'Z'
  74. }
  75. // eslint-disable-next-line react-hooks/exhaustive-deps
  76. }, [dateRange, subscription])
  77. const endDate = useMemo(() => {
  78. // If end date is in future, set end date to end of current day
  79. if (dateRange?.period_end?.date && dayjs(dateRange.period_end.date).isAfter(dayjs())) {
  80. // LF seems to have an issue with the milliseconds, causes infinite loading sometimes
  81. // In order to have full days from Prometheus metrics when using 1d interval,
  82. // the time needs to be greater or equal than the time of the start date
  83. return dayjs().endOf('day').toISOString().slice(0, -5) + 'Z'
  84. } else if (dateRange?.period_end?.date) {
  85. // LF seems to have an issue with the milliseconds, causes infinite loading sometimes
  86. return new Date(dateRange.period_end.date).toISOString().slice(0, -5) + 'Z'
  87. }
  88. // eslint-disable-next-line react-hooks/exhaustive-deps
  89. }, [dateRange, subscription])
  90. const {
  91. data: orgDailyStats,
  92. error: orgDailyStatsError,
  93. isPending: isLoadingOrgDailyStats,
  94. isError: isErrorOrgDailyStats,
  95. } = useOrgDailyStatsQuery({
  96. orgSlug: slug,
  97. projectRef: selectedProjectRef ?? undefined,
  98. startDate,
  99. endDate,
  100. })
  101. return (
  102. <>
  103. <ScaffoldContainer>
  104. <ScaffoldHeader className="pt-8">
  105. <ScaffoldTitle>Usage</ScaffoldTitle>
  106. </ScaffoldHeader>
  107. </ScaffoldContainer>
  108. <div className="sticky top-0 border-b bg-sidebar z-1">
  109. <ScaffoldContainer>
  110. <div className="py-4 flex items-center space-x-4">
  111. {isLoadingSubscription || isLoadingPermissions ? (
  112. <div className="flex lg:items-center items-start gap-3 flex-col lg:flex-row lg:justify-between w-full">
  113. <div className="flex items-center gap-2">
  114. <ShimmeringLoader className="w-48" />
  115. <ShimmeringLoader className="w-48" />
  116. </div>
  117. <ShimmeringLoader className="w-[280px]" />
  118. </div>
  119. ) : !canReadSubscriptions ? (
  120. <NoPermission resourceText="view organization usage" />
  121. ) : null}
  122. {isErrorSubscription && (
  123. <AlertError
  124. className="w-full"
  125. subject="Failed to retrieve usage data"
  126. error={subscriptionError}
  127. />
  128. )}
  129. {isSuccessSubscription && (
  130. <div className="flex lg:items-center items-start gap-3 flex-col lg:flex-row lg:justify-between w-full">
  131. <div className="flex items-center gap-2">
  132. <DateRangePicker
  133. onChange={setDateRange}
  134. value={TIME_PERIODS_BILLING[0].key}
  135. options={[...TIME_PERIODS_BILLING, ...TIME_PERIODS_REPORTS]}
  136. loading={isLoadingSubscription}
  137. currentBillingPeriodStart={subscription?.current_period_start}
  138. currentBillingPeriodEnd={subscription?.current_period_end}
  139. className="w-48!"
  140. />
  141. <OrganizationProjectSelector
  142. open={openProjectSelector}
  143. setOpen={setOpenProjectSelector}
  144. selectedRef={selectedProjectRef}
  145. onSelect={(project) => {
  146. setSelectedProjectRef(project.ref)
  147. }}
  148. renderTrigger={({ listboxId, open }) => {
  149. return (
  150. <Button
  151. block
  152. type="default"
  153. role="combobox"
  154. size="tiny"
  155. aria-expanded={open}
  156. aria-controls={listboxId}
  157. className="justify-between w-[180px]"
  158. iconRight={<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />}
  159. >
  160. {!selectedProject ? 'All projects' : selectedProject?.name}
  161. </Button>
  162. )
  163. }}
  164. renderRow={(project) => {
  165. const isSelected = selectedProjectRef === project.ref
  166. return (
  167. <div className="w-full flex items-center justify-between">
  168. <span className={cn('truncate', isSelected ? 'max-w-60' : 'max-w-64')}>
  169. {project.name}
  170. </span>
  171. {isSelected && <Check size={16} />}
  172. </div>
  173. )
  174. }}
  175. renderActions={() => (
  176. <CommandGroup>
  177. <CommandItem
  178. className="cursor-pointer flex items-center justify-between w-full"
  179. onSelect={() => {
  180. setOpenProjectSelector(false)
  181. setSelectedProjectRef(null)
  182. }}
  183. onClick={() => {
  184. setOpenProjectSelector(false)
  185. setSelectedProjectRef(null)
  186. }}
  187. >
  188. All projects
  189. {!selectedProjectRef && <Check size={16} />}
  190. </CommandItem>
  191. </CommandGroup>
  192. )}
  193. />
  194. </div>
  195. <div className="flex items-center gap-2">
  196. <p className={cn('text-sm transition', isLoadingSubscription && 'opacity-50')}>
  197. Organization is on the{' '}
  198. <span className="font-medium text-brand">{subscription.plan.name} Plan</span>
  199. </p>
  200. <span className="text-border-stronger">
  201. <svg
  202. viewBox="0 0 24 24"
  203. width="16"
  204. height="16"
  205. stroke="currentColor"
  206. strokeWidth="1"
  207. strokeLinecap="round"
  208. strokeLinejoin="round"
  209. fill="none"
  210. shapeRendering="geometricPrecision"
  211. >
  212. <path d="M16 3.549L7.12 20.600" />
  213. </svg>
  214. </span>
  215. <p className="text-sm text-foreground-light">
  216. {billingCycleStart.format('DD MMM YYYY')} -{' '}
  217. {billingCycleEnd.format('DD MMM YYYY')}
  218. </p>
  219. </div>
  220. </div>
  221. )}
  222. </div>
  223. </ScaffoldContainer>
  224. </div>
  225. {isErrorOrgDailyStats && (
  226. <ScaffoldContainer>
  227. <ScaffoldSection isFullWidth className="pb-0">
  228. <AlertError
  229. error={orgDailyStatsError}
  230. subject="Failed to retrieve usage statistics for organization"
  231. />
  232. </ScaffoldSection>
  233. </ScaffoldContainer>
  234. )}
  235. {selectedProject ? (
  236. <ScaffoldContainer className="mt-5">
  237. <Admonition
  238. type="default"
  239. title="Usage filtered by project"
  240. description={
  241. <div>
  242. You are currently viewing usage for the{' '}
  243. <span className="font-medium text-foreground">
  244. {selectedProject?.name || selectedProjectRef}
  245. </span>{' '}
  246. project. Briven uses{' '}
  247. <Link
  248. href="/docs/guides/platform/billing-on-briven#organization-based-billing"
  249. target="_blank"
  250. >
  251. organization-level billing
  252. </Link>{' '}
  253. and quotas. For billing purposes, we sum up usage from all your projects. To view
  254. your usage quota, set the project filter above back to "All Projects".
  255. </div>
  256. }
  257. />
  258. </ScaffoldContainer>
  259. ) : (
  260. <ScaffoldContainer id="restriction" className="mt-5">
  261. <Restriction />
  262. </ScaffoldContainer>
  263. )}
  264. <TotalUsage
  265. orgSlug={slug as string}
  266. projectRef={selectedProjectRef}
  267. subscription={subscription}
  268. startDate={startDate}
  269. endDate={endDate}
  270. currentBillingCycleSelected={currentBillingCycleSelected}
  271. />
  272. {subscription?.plan.id !== 'free' && (
  273. <Compute orgDailyStats={orgDailyStats} isLoadingOrgDailyStats={isLoadingOrgDailyStats} />
  274. )}
  275. {subscription?.plan.id === 'platform' && (
  276. <ActiveCompute
  277. orgDailyStats={orgDailyStats}
  278. isLoadingOrgDailyStats={isLoadingOrgDailyStats}
  279. />
  280. )}
  281. <Egress
  282. orgSlug={slug as string}
  283. projectRef={selectedProjectRef}
  284. subscription={subscription}
  285. currentBillingCycleSelected={currentBillingCycleSelected}
  286. orgDailyStats={orgDailyStats}
  287. isLoadingOrgDailyStats={isLoadingOrgDailyStats}
  288. startDate={startDate}
  289. endDate={endDate}
  290. />
  291. <SizeAndCounts
  292. orgSlug={slug as string}
  293. projectRef={selectedProjectRef}
  294. subscription={subscription}
  295. currentBillingCycleSelected={currentBillingCycleSelected}
  296. orgDailyStats={orgDailyStats}
  297. isLoadingOrgDailyStats={isLoadingOrgDailyStats}
  298. startDate={startDate}
  299. endDate={endDate}
  300. />
  301. <Activity
  302. orgSlug={slug as string}
  303. projectRef={selectedProjectRef}
  304. subscription={subscription}
  305. startDate={startDate}
  306. endDate={endDate}
  307. currentBillingCycleSelected={currentBillingCycleSelected}
  308. orgDailyStats={orgDailyStats}
  309. isLoadingOrgDailyStats={isLoadingOrgDailyStats}
  310. />
  311. {subscription?.plan.id === 'platform' && (
  312. <OrgLogUsage
  313. orgSlug={slug as string}
  314. projectRef={selectedProjectRef}
  315. subscription={subscription}
  316. startDate={startDate}
  317. endDate={endDate}
  318. currentBillingCycleSelected={currentBillingCycleSelected}
  319. orgDailyStats={orgDailyStats}
  320. isLoadingOrgDailyStats={isLoadingOrgDailyStats}
  321. />
  322. )}
  323. </>
  324. )
  325. }