| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353 |
- import { PermissionAction } from '@supabase/shared-types/out/constants'
- import { useParams } from 'common'
- import dayjs from 'dayjs'
- import { Check, ChevronDown } from 'lucide-react'
- import Link from 'next/link'
- import { useQueryState } from 'nuqs'
- import { useMemo, useState } from 'react'
- import { Button, cn, CommandGroup, CommandItem } from 'ui'
- import { Admonition } from 'ui-patterns'
- import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
- import { Restriction } from '../BillingSettings/Restriction'
- import ActiveCompute from './ActiveCompute'
- import Activity from './Activity'
- import Compute from './Compute'
- import Egress from './Egress'
- import OrgLogUsage from './OrgLogUsage'
- import SizeAndCounts from './SizeAndCounts'
- import { TotalUsage } from './TotalUsage'
- import {
- ScaffoldContainer,
- ScaffoldHeader,
- ScaffoldSection,
- ScaffoldTitle,
- } from '@/components/layouts/Scaffold'
- import AlertError from '@/components/ui/AlertError'
- import DateRangePicker from '@/components/ui/DateRangePicker'
- import NoPermission from '@/components/ui/NoPermission'
- import { OrganizationProjectSelector } from '@/components/ui/OrganizationProjectSelector'
- import { useOrgDailyStatsQuery } from '@/data/analytics/org-daily-stats-query'
- import { useProjectDetailQuery } from '@/data/projects/project-detail-query'
- import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
- import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
- import { TIME_PERIODS_BILLING, TIME_PERIODS_REPORTS } from '@/lib/constants/metrics'
- export const Usage = () => {
- const { slug } = useParams()
- const [dateRange, setDateRange] = useState<any>()
- const [selectedProjectRef, setSelectedProjectRef] = useQueryState('projectRef')
- const [openProjectSelector, setOpenProjectSelector] = useState(false)
- const { can: canReadSubscriptions, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
- PermissionAction.BILLING_READ,
- 'stripe.subscriptions'
- )
- const {
- data: subscription,
- error: subscriptionError,
- isPending: isLoadingSubscription,
- isError: isErrorSubscription,
- isSuccess: isSuccessSubscription,
- } = useOrgSubscriptionQuery({ orgSlug: slug })
- const { data: selectedProject } = useProjectDetailQuery({
- ref: selectedProjectRef ?? undefined,
- })
- const billingCycleStart = useMemo(() => {
- return dayjs.unix(subscription?.current_period_start ?? 0).utc()
- }, [subscription])
- const billingCycleEnd = useMemo(() => {
- return dayjs.unix(subscription?.current_period_end ?? 0).utc()
- }, [subscription])
- const currentBillingCycleSelected = useMemo(() => {
- // Selected by default
- if (!dateRange?.period_start || !dateRange?.period_end) return true
- return (
- dayjs(dateRange.period_start.date).isSame(billingCycleStart) &&
- dayjs(dateRange.period_end.date).isSame(billingCycleEnd)
- )
- }, [dateRange, billingCycleStart, billingCycleEnd])
- const startDate = useMemo(() => {
- // If end date is in future, set end date to now
- if (!dateRange?.period_start?.date) {
- return undefined
- } else {
- // LF seems to have an issue with the milliseconds, causes infinite loading sometimes
- return new Date(dateRange?.period_start?.date).toISOString().slice(0, -5) + 'Z'
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [dateRange, subscription])
- const endDate = useMemo(() => {
- // If end date is in future, set end date to end of current day
- if (dateRange?.period_end?.date && dayjs(dateRange.period_end.date).isAfter(dayjs())) {
- // LF seems to have an issue with the milliseconds, causes infinite loading sometimes
- // In order to have full days from Prometheus metrics when using 1d interval,
- // the time needs to be greater or equal than the time of the start date
- return dayjs().endOf('day').toISOString().slice(0, -5) + 'Z'
- } else if (dateRange?.period_end?.date) {
- // LF seems to have an issue with the milliseconds, causes infinite loading sometimes
- return new Date(dateRange.period_end.date).toISOString().slice(0, -5) + 'Z'
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [dateRange, subscription])
- const {
- data: orgDailyStats,
- error: orgDailyStatsError,
- isPending: isLoadingOrgDailyStats,
- isError: isErrorOrgDailyStats,
- } = useOrgDailyStatsQuery({
- orgSlug: slug,
- projectRef: selectedProjectRef ?? undefined,
- startDate,
- endDate,
- })
- return (
- <>
- <ScaffoldContainer>
- <ScaffoldHeader className="pt-8">
- <ScaffoldTitle>Usage</ScaffoldTitle>
- </ScaffoldHeader>
- </ScaffoldContainer>
- <div className="sticky top-0 border-b bg-sidebar z-1">
- <ScaffoldContainer>
- <div className="py-4 flex items-center space-x-4">
- {isLoadingSubscription || isLoadingPermissions ? (
- <div className="flex lg:items-center items-start gap-3 flex-col lg:flex-row lg:justify-between w-full">
- <div className="flex items-center gap-2">
- <ShimmeringLoader className="w-48" />
- <ShimmeringLoader className="w-48" />
- </div>
- <ShimmeringLoader className="w-[280px]" />
- </div>
- ) : !canReadSubscriptions ? (
- <NoPermission resourceText="view organization usage" />
- ) : null}
- {isErrorSubscription && (
- <AlertError
- className="w-full"
- subject="Failed to retrieve usage data"
- error={subscriptionError}
- />
- )}
- {isSuccessSubscription && (
- <div className="flex lg:items-center items-start gap-3 flex-col lg:flex-row lg:justify-between w-full">
- <div className="flex items-center gap-2">
- <DateRangePicker
- onChange={setDateRange}
- value={TIME_PERIODS_BILLING[0].key}
- options={[...TIME_PERIODS_BILLING, ...TIME_PERIODS_REPORTS]}
- loading={isLoadingSubscription}
- currentBillingPeriodStart={subscription?.current_period_start}
- currentBillingPeriodEnd={subscription?.current_period_end}
- className="w-48!"
- />
- <OrganizationProjectSelector
- open={openProjectSelector}
- setOpen={setOpenProjectSelector}
- selectedRef={selectedProjectRef}
- onSelect={(project) => {
- setSelectedProjectRef(project.ref)
- }}
- renderTrigger={({ listboxId, open }) => {
- return (
- <Button
- block
- type="default"
- role="combobox"
- size="tiny"
- aria-expanded={open}
- aria-controls={listboxId}
- className="justify-between w-[180px]"
- iconRight={<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />}
- >
- {!selectedProject ? 'All projects' : selectedProject?.name}
- </Button>
- )
- }}
- renderRow={(project) => {
- const isSelected = selectedProjectRef === project.ref
- return (
- <div className="w-full flex items-center justify-between">
- <span className={cn('truncate', isSelected ? 'max-w-60' : 'max-w-64')}>
- {project.name}
- </span>
- {isSelected && <Check size={16} />}
- </div>
- )
- }}
- renderActions={() => (
- <CommandGroup>
- <CommandItem
- className="cursor-pointer flex items-center justify-between w-full"
- onSelect={() => {
- setOpenProjectSelector(false)
- setSelectedProjectRef(null)
- }}
- onClick={() => {
- setOpenProjectSelector(false)
- setSelectedProjectRef(null)
- }}
- >
- All projects
- {!selectedProjectRef && <Check size={16} />}
- </CommandItem>
- </CommandGroup>
- )}
- />
- </div>
- <div className="flex items-center gap-2">
- <p className={cn('text-sm transition', isLoadingSubscription && 'opacity-50')}>
- Organization is on the{' '}
- <span className="font-medium text-brand">{subscription.plan.name} Plan</span>
- </p>
- <span className="text-border-stronger">
- <svg
- viewBox="0 0 24 24"
- width="16"
- height="16"
- stroke="currentColor"
- strokeWidth="1"
- strokeLinecap="round"
- strokeLinejoin="round"
- fill="none"
- shapeRendering="geometricPrecision"
- >
- <path d="M16 3.549L7.12 20.600" />
- </svg>
- </span>
- <p className="text-sm text-foreground-light">
- {billingCycleStart.format('DD MMM YYYY')} -{' '}
- {billingCycleEnd.format('DD MMM YYYY')}
- </p>
- </div>
- </div>
- )}
- </div>
- </ScaffoldContainer>
- </div>
- {isErrorOrgDailyStats && (
- <ScaffoldContainer>
- <ScaffoldSection isFullWidth className="pb-0">
- <AlertError
- error={orgDailyStatsError}
- subject="Failed to retrieve usage statistics for organization"
- />
- </ScaffoldSection>
- </ScaffoldContainer>
- )}
- {selectedProject ? (
- <ScaffoldContainer className="mt-5">
- <Admonition
- type="default"
- title="Usage filtered by project"
- description={
- <div>
- You are currently viewing usage for the{' '}
- <span className="font-medium text-foreground">
- {selectedProject?.name || selectedProjectRef}
- </span>{' '}
- project. Briven uses{' '}
- <Link
- href="/docs/guides/platform/billing-on-briven#organization-based-billing"
- target="_blank"
- >
- organization-level billing
- </Link>{' '}
- and quotas. For billing purposes, we sum up usage from all your projects. To view
- your usage quota, set the project filter above back to "All Projects".
- </div>
- }
- />
- </ScaffoldContainer>
- ) : (
- <ScaffoldContainer id="restriction" className="mt-5">
- <Restriction />
- </ScaffoldContainer>
- )}
- <TotalUsage
- orgSlug={slug as string}
- projectRef={selectedProjectRef}
- subscription={subscription}
- startDate={startDate}
- endDate={endDate}
- currentBillingCycleSelected={currentBillingCycleSelected}
- />
- {subscription?.plan.id !== 'free' && (
- <Compute orgDailyStats={orgDailyStats} isLoadingOrgDailyStats={isLoadingOrgDailyStats} />
- )}
- {subscription?.plan.id === 'platform' && (
- <ActiveCompute
- orgDailyStats={orgDailyStats}
- isLoadingOrgDailyStats={isLoadingOrgDailyStats}
- />
- )}
- <Egress
- orgSlug={slug as string}
- projectRef={selectedProjectRef}
- subscription={subscription}
- currentBillingCycleSelected={currentBillingCycleSelected}
- orgDailyStats={orgDailyStats}
- isLoadingOrgDailyStats={isLoadingOrgDailyStats}
- startDate={startDate}
- endDate={endDate}
- />
- <SizeAndCounts
- orgSlug={slug as string}
- projectRef={selectedProjectRef}
- subscription={subscription}
- currentBillingCycleSelected={currentBillingCycleSelected}
- orgDailyStats={orgDailyStats}
- isLoadingOrgDailyStats={isLoadingOrgDailyStats}
- startDate={startDate}
- endDate={endDate}
- />
- <Activity
- orgSlug={slug as string}
- projectRef={selectedProjectRef}
- subscription={subscription}
- startDate={startDate}
- endDate={endDate}
- currentBillingCycleSelected={currentBillingCycleSelected}
- orgDailyStats={orgDailyStats}
- isLoadingOrgDailyStats={isLoadingOrgDailyStats}
- />
- {subscription?.plan.id === 'platform' && (
- <OrgLogUsage
- orgSlug={slug as string}
- projectRef={selectedProjectRef}
- subscription={subscription}
- startDate={startDate}
- endDate={endDate}
- currentBillingCycleSelected={currentBillingCycleSelected}
- orgDailyStats={orgDailyStats}
- isLoadingOrgDailyStats={isLoadingOrgDailyStats}
- />
- )}
- </>
- )
- }
|