| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251 |
- import { ChevronRight } from 'lucide-react'
- import Link from 'next/link'
- import { useMemo } from 'react'
- import { cn, HoverCard, HoverCardContent, HoverCardTrigger } from 'ui'
- import { billingMetricUnit, formatUsage } from '../helpers'
- import { Metric, USAGE_APPROACHING_THRESHOLD } from './BillingBreakdown.constants'
- import { UpgradePlanButton } from '@/components/ui/UpgradePlanButton'
- import { PricingMetric } from '@/data/analytics/org-daily-stats-query'
- import type { OrgSubscription } from '@/data/subscriptions/types'
- import type { OrgUsageResponse } from '@/data/usage/org-usage-query'
- import { formatCurrency } from '@/lib/helpers'
- export interface BillingMetricProps {
- idx: number
- slug?: string
- metric: Metric
- usage: OrgUsageResponse
- subscription: OrgSubscription
- relativeToSubscription: boolean
- className?: string
- }
- export const BillingMetric = ({
- slug,
- metric,
- usage,
- subscription,
- relativeToSubscription,
- className,
- }: BillingMetricProps) => {
- const usageMeta = usage.usages.find((x) => x.metric === metric.key)
- const usageLabel = useMemo(() => {
- if (!usageMeta) return ''
- if (relativeToSubscription && usageMeta.available_in_plan === false) {
- return 'Unavailable in plan'
- } else if (
- (usageMeta.cost && usageMeta.cost > 0) ||
- !relativeToSubscription ||
- usageMeta.unlimited ||
- usageMeta.pricing_free_units === 0
- ) {
- return metric.units === 'bytes' || metric.units === 'gigabytes'
- ? `${usageMeta.usage.toLocaleString() ?? 0} GB`
- : usageMeta.usage.toLocaleString() + (metric.unitName ? ` ${metric.unitName}` : '')
- } else {
- return metric.units === 'bytes' || metric.units === 'gigabytes'
- ? `${usageMeta.usage.toLocaleString() ?? 0} / ${usageMeta.pricing_free_units ?? 0} GB`
- : `${usageMeta.usage.toLocaleString()} / ${usageMeta.pricing_free_units?.toLocaleString()}` +
- (metric.unitName ? ` ${metric.unitName}` : '')
- }
- }, [usageMeta, relativeToSubscription, metric])
- const sortedProjectAllocations = useMemo(() => {
- if (!usageMeta || !usageMeta.project_allocations) return []
- return usageMeta.project_allocations.sort((a, b) => b.usage - a.usage)
- }, [usageMeta])
- if (!usageMeta) return null
- const usageRatio =
- usageMeta.usage === 0 ? 0 : usageMeta.usage / (usageMeta.pricing_free_units ?? 0)
- const isUsageBillingEnabled = subscription?.usage_billing_enabled === true
- const hasLimit = !!usageMeta.unlimited === false
- const isApproachingLimit = hasLimit && usageRatio >= USAGE_APPROACHING_THRESHOLD
- const isExceededLimit = relativeToSubscription && hasLimit && usageRatio >= 1
- const unit = billingMetricUnit(usageMeta.metric as PricingMetric)
- const percentageLabel =
- usageMeta.usage === 0 || usageMeta.pricing_free_units === 0
- ? ''
- : usageRatio < 0.01
- ? '(<1%)'
- : `(${(+(usageRatio * 100).toFixed(0)).toLocaleString()}%)`
- return (
- <HoverCard openDelay={50} closeDelay={200}>
- <HoverCardTrigger asChild>
- <div className={cn('flex items-center justify-between', className)}>
- {metric.anchor ? (
- <Link href={`/org/${slug}/usage#${metric.anchor}`} className="block w-full group">
- <div className="group flex items-center gap-1">
- <p className="text-sm text-foreground-light group-hover:text-foreground transition cursor-pointer">
- {metric.name}
- </p>
- {usageMeta.available_in_plan && (
- <span className="text-foreground-muted transition inline-block group-hover:transform group-hover:translate-x-0.5">
- <ChevronRight strokeWidth={1.5} size={16} className="transition" />
- </span>
- )}
- </div>
- <span className="text-sm">{usageLabel}</span>
- {relativeToSubscription && usageMeta.cost && usageMeta.cost > 0 ? (
- <span className="text-sm" translate="no">
- ({formatCurrency(usageMeta.cost)})
- </span>
- ) : usageMeta.available_in_plan &&
- usageMeta.pricing_free_units !== 0 &&
- !usageMeta.unlimited &&
- relativeToSubscription ? (
- <span className="text-sm">{percentageLabel}</span>
- ) : null}
- </Link>
- ) : (
- <div className="block w-full">
- <p className="text-sm text-foreground-light flex space-x-1">{metric.name}</p>
- <span className="text-sm">{usageLabel}</span>
- {relativeToSubscription && usageMeta.cost && usageMeta.cost > 0 ? (
- <span className="text-sm" translate="no">
- ({formatCurrency(usageMeta.cost)})
- </span>
- ) : usageMeta.available_in_plan &&
- usageMeta.pricing_free_units !== 0 &&
- !usageMeta.unlimited &&
- relativeToSubscription ? (
- <span className="text-sm">{percentageLabel}</span>
- ) : null}
- </div>
- )}
- {usageMeta.available_in_plan ? (
- <div>
- {relativeToSubscription &&
- !usageMeta.unlimited &&
- usageMeta.pricing_free_units !== 0 ? (
- <svg className="h-8 w-8 -rotate-90 transform">
- <circle
- cx={15}
- cy={15}
- r={12}
- fill="transparent"
- stroke="currentColor"
- strokeWidth={4}
- className="text-background-surface-300"
- />
- <circle
- cx={15}
- cy={15}
- r={12}
- fill="transparent"
- stroke="currentColor"
- strokeDasharray={75.398}
- strokeDashoffset={`calc(75.39822 - ${
- usageRatio < 1 ? usageRatio * 100 : 100
- } / 100 * 75.39822)`}
- strokeWidth={4}
- className={
- isUsageBillingEnabled
- ? 'text-gray-dark-800'
- : isExceededLimit
- ? 'text-red-900'
- : isApproachingLimit
- ? 'text-yellow-1000'
- : 'text-gray-dark-800'
- }
- />
- </svg>
- ) : null}
- </div>
- ) : (
- <div>
- <UpgradePlanButton
- source={`billingBreakdownUsage${metric.anchor}`}
- featureProposition={`to use ${metric.name}`}
- >
- Upgrade
- </UpgradePlanButton>
- </div>
- )}
- </div>
- </HoverCardTrigger>
- {usageMeta.available_in_plan && (
- <HoverCardContent side="bottom" align="end" className="w-[500px]" animate="slide-in">
- <div className="text-sm">
- <p className="font-medium" translate="no">
- {usageMeta.unit_price_desc}
- </p>
- {metric.tip && (
- <div className="my-2">
- <p className="text-sm">
- {metric.tip}{' '}
- {metric.docLink && (
- <Link
- href={metric.docLink.url}
- target="_blank"
- className="transition text-brand hover:text-brand-600 underline"
- >
- {metric.docLink.title}
- </Link>
- )}
- </p>
- </div>
- )}
- {subscription.usage_billing_enabled === false &&
- relativeToSubscription &&
- (isApproachingLimit || isExceededLimit) && (
- <div className="my-2">
- <p className="text-sm">
- Exceeding your plans included usage will lead to restrictions to your project.
- Upgrade to a usage-based plan or disable the spend cap to avoid restrictions.
- </p>
- </div>
- )}
- {sortedProjectAllocations && sortedProjectAllocations.length > 0 && (
- <table className="list-disc w-full">
- <thead>
- <tr>
- <th className="text-left">Project</th>
- <th className="text-right">Usage</th>
- </tr>
- </thead>
- <tbody>
- {sortedProjectAllocations.map((allocation) => (
- <tr key={`${usageMeta.metric}_${allocation.ref}`}>
- <td>{allocation.name}</td>
- <td className="text-right">
- {formatUsage(usageMeta.metric as PricingMetric, allocation)}
- </td>
- </tr>
- ))}
- <tr></tr>
- </tbody>
- <tfoot>
- <tr>
- <td className="py-2 border-t text-left">
- Total{unit && <span> ({unit})</span>}
- </td>
- <td className="py-2 border-t text-right">
- {formatUsage(usageMeta.metric as PricingMetric, {
- usage: usageMeta.usage_original,
- })}{' '}
- </td>
- </tr>
- </tfoot>
- </table>
- )}
- </div>
- </HoverCardContent>
- )}
- </HoverCard>
- )
- }
|