Merge pull request #14761 from supabase/improvements-billing-v2

This commit is contained in:
Kevin Grüneberg authored and GitHub committed 2023-06-05 07:50:04 -07:00
commit e6e3b0345e
13 files changed
+431 -634

No files matched your search

@@ -164,7 +164,7 @@ export const USAGE_BASED_PRODUCTS = [
costPerUnit: 0.000002,
tooltip: (
<span>
Every single serverless function invocation independent of response status is counted.
Every serverless function invocation independent of response status is counted.
Billing is based on the sum of all invocations throughout your billing period.
</span>
),
@@ -198,6 +198,7 @@ const BillingBreakdown = ({}: BillingBreakdownProps) => {
? 'bg-amber-900'
: 'bg-scale-1100'
}
bgClass="bg-gray-300 dark:bg-gray-600"
labelBottom={usageLabel}
labelBottomClass="!text-scale-1000"
labelTop={hasLimit ? percentageLabel : undefined}
@@ -4,5 +4,6 @@ export const getAddons = (selectedAddons: ProjectSelectedAddon[]) => {
const computeInstance = selectedAddons.find((addon) => addon.type === 'compute_instance')
const pitr = selectedAddons.find((addon) => addon.type === 'pitr')
const customDomain = selectedAddons.find((addon) => addon.type === 'custom_domain')
return { computeInstance, pitr, customDomain }
}
@@ -102,6 +102,7 @@ const SubscriptionTier = ({}: SubscriptionTierProps) => {
labelBottom={`Current billing cycle (${billingCycleStart.format(
'MMM DD'
)} - ${billingCycleEnd.format('MMM DD')})`}
bgClass="bg-gray-300 dark:bg-gray-600"
labelBottomClass="!text-scale-1000 pb-1"
labelTop={`${daysToCycleEnd} Days left`}
/>
@@ -1,65 +1,16 @@
import clsx from 'clsx'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import SparkBar from 'components/ui/SparkBar'
import { useDailyStatsQuery } from 'data/analytics/daily-stats-query'
import { useProjectSubscriptionQuery } from 'data/subscriptions/project-subscription-query'
import {
ProjectUsageResponse,
UsageMetric,
useProjectUsageQuery,
} from 'data/usage/project-usage-query'
import dayjs from 'dayjs'
import Link from 'next/link'
import { Button, IconAlertTriangle } from 'ui'
import { USAGE_APPROACHING_THRESHOLD } from '../Billing.constants'
import UsageBarChart from './UsageBarChart'
import SectionContent from './SectionContent'
import SectionHeader from './SectionHeader'
import { USAGE_CATEGORIES } from './Usage.constants'
import { ChartYFormatterCompactNumber, getUpgradeUrl } from './Usage.utils'
import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
import { DataPoint } from 'data/analytics/constants'
import Panel from 'components/ui/Panel'
import { ProjectSubscriptionResponse } from 'data/subscriptions/project-subscription-v2-query'
import UsageSection from './UsageSection'
export interface ActivityProps {
projectRef: string
startDate: string | undefined
endDate: string | undefined
subscription: ProjectSubscriptionResponse | undefined
}
const Activity = ({ projectRef }: ActivityProps) => {
const { data: usage } = useProjectUsageQuery({ projectRef })
const { data: subscription } = useProjectSubscriptionQuery({ projectRef })
const { current_period_start, current_period_end } = subscription?.billing ?? {}
const startDate =
current_period_start !== undefined
? new Date(current_period_start * 1000).toISOString()
: undefined
let endDate =
current_period_end !== undefined ? new Date(current_period_end * 1000).toISOString() : undefined
// If end date is in future, set end date to yesterday/now
if (endDate && dayjs(endDate).isAfter(dayjs())) {
const yesterday = dayjs(new Date()).subtract(1, 'day')
/**
* Currently, daily-stats data is only available a day later, so we'll use yesterday as end date, as otherwise the current day would just show up with "0" values
*
* We are actively working on removing this restriction on the data-eng/LF side and can remove this workaround once that's done
*/
let newEndDate = yesterday.isAfter(dayjs(startDate)) ? yesterday : new Date()
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
endDate = newEndDate.toISOString().slice(0, -5) + 'Z'
}
const categoryMeta = USAGE_CATEGORIES.find((category) => category.key === 'activity')
const upgradeUrl = getUpgradeUrl(projectRef, subscription)
const isFreeTier = subscription?.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.FREE
const isProTier = subscription?.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.PRO
const usageBasedBilling = !isFreeTier && !isProTier
const exceededLimitStyle = !usageBasedBilling ? 'text-red-900' : 'text-amber-900'
const Activity = ({ projectRef, subscription, startDate, endDate }: ActivityProps) => {
const { data: mauData, isLoading: isLoadingMauData } = useDailyStatsQuery({
projectRef,
attribute: 'total_auth_billing_period_mau',
@@ -154,158 +105,13 @@ const Activity = ({ projectRef }: ActivityProps) => {
},
}
if (categoryMeta === undefined) return null
return (
<>
<SectionHeader title={categoryMeta.name} description={categoryMeta.description} />
{categoryMeta.attributes.map((attribute) => {
const usageMeta = usage?.[attribute.key as keyof ProjectUsageResponse] as UsageMetric
const usageRatio =
typeof usageMeta !== 'number' ? (usageMeta?.usage ?? 0) / (usageMeta?.limit ?? 0) : 0
const usageExcess = (usageMeta?.usage ?? 0) - (usageMeta?.limit ?? 0)
const chartData = chartMeta[attribute.key]?.data ?? []
return (
<div id={attribute.anchor} key={attribute.key}>
<SectionContent section={attribute} includedInPlan={usageMeta?.available_in_plan}>
{usageMeta?.available_in_plan ? (
<>
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<p className="text-sm">{attribute.name} usage</p>
{!usageBasedBilling && usageRatio >= 1 ? (
<div className="flex items-center space-x-2 min-w-[115px]">
<IconAlertTriangle
size={14}
strokeWidth={2}
className={exceededLimitStyle}
/>
<p className={`text-sm ${exceededLimitStyle}`}>Exceeded limit</p>
</div>
) : !usageBasedBilling && usageRatio >= USAGE_APPROACHING_THRESHOLD ? (
<div className="flex items-center space-x-2 min-w-[115px]">
<IconAlertTriangle
size={14}
strokeWidth={2}
className="text-amber-900"
/>
<p className="text-sm text-amber-900">Approaching limit</p>
</div>
) : null}
</div>
{!usageBasedBilling && usageRatio >= USAGE_APPROACHING_THRESHOLD && (
<Link href={upgradeUrl}>
<a>
<Button type="default" size="tiny">
Upgrade project
</Button>
</a>
</Link>
)}
</div>
{usageMeta.limit > 0 && (
<SparkBar
type="horizontal"
barClass={clsx(
usageRatio >= 1
? usageBasedBilling
? 'bg-amber-900'
: 'bg-red-900'
: usageRatio >= USAGE_APPROACHING_THRESHOLD
? 'bg-amber-900'
: 'bg-scale-1100'
)}
value={usageMeta?.usage ?? 0}
max={usageMeta?.limit || 1}
/>
)}
<div>
<div className="flex items-center justify-between border-b py-1">
<p className="text-xs text-scale-1000">
Included in {subscription?.tier.name.toLowerCase()}
</p>
{usageMeta?.limit === -1 ? (
<p className="text-xs">None</p>
) : usageMeta?.limit === 0 ? (
<p className="text-xs">Unlimited</p>
) : (
<p className="text-xs">{(usageMeta?.limit ?? 0).toLocaleString()}</p>
)}
</div>
<div className="flex items-center justify-between py-1">
<p className="text-xs text-scale-1000">
{attribute.chartPrefix || 'Used '}in period
</p>
<p className="text-xs">{(usageMeta?.usage ?? 0).toLocaleString()}</p>
</div>
{usageMeta.limit > 0 && (
<div className="flex items-center justify-between border-t py-1">
<p className="text-xs text-scale-1000">Overage this month</p>
<p className="text-xs">
{((usageMeta?.limit ?? 0) === -1 || usageExcess < 0
? 0
: usageExcess
).toLocaleString()}
</p>
</div>
)}
</div>
</div>
<div className="space-y-1">
<p>{attribute.name} per day</p>
{attribute.chartDescription.split('\n').map((paragraph, idx) => (
<p key={`para-${idx}`} className="text-sm text-scale-1000">
{paragraph}
</p>
))}
</div>
{chartMeta[attribute.key].isLoading ? (
<div className="space-y-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
) : (
<UsageBarChart
name={`${attribute.chartPrefix || ''}${attribute.name}`}
unit={attribute.unit}
attribute={attribute.attribute}
data={chartData}
yLeftMargin={chartMeta[attribute.key].margin}
yFormatter={(value) => ChartYFormatterCompactNumber(value, attribute.unit)}
/>
)}
</>
) : (
<Panel>
<Panel.Content>
<div className="flex w-full items-center flex-col justify-center space-y-2 md:flex-row md:justify-between">
<div className="space-y-1">
<p className="text-sm">Not included in plan</p>
<div>
<p className="text-sm text-scale-1100">
You need to be on a higher plan in order to use this feature.
</p>
</div>
</div>
<Link href={`/project/${projectRef}/settings/billing/subscription`}>
<a>
<Button type="primary">Upgrade plan</Button>
</a>
</Link>
</div>
</Panel.Content>
</Panel>
)}
</SectionContent>
</div>
)
})}
</>
<UsageSection
projectRef={projectRef}
categoryKey='activity'
chartMeta={chartMeta}
subscription={subscription}
/>
)
}
@@ -1,53 +1,16 @@
import clsx from 'clsx'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import SparkBar from 'components/ui/SparkBar'
import { DataPoint } from 'data/analytics/constants'
import { useDailyStatsQuery } from 'data/analytics/daily-stats-query'
import { useProjectSubscriptionQuery } from 'data/subscriptions/project-subscription-query'
import {
ProjectUsageResponse,
UsageMetric,
useProjectUsageQuery,
} from 'data/usage/project-usage-query'
import dayjs from 'dayjs'
import { PRICING_TIER_PRODUCT_IDS, USAGE_APPROACHING_THRESHOLD } from 'lib/constants'
import { formatBytes } from 'lib/helpers'
import Link from 'next/link'
import { Button, IconAlertTriangle } from 'ui'
import SectionContent from './SectionContent'
import SectionHeader from './SectionHeader'
import { USAGE_CATEGORIES } from './Usage.constants'
import { ChartYFormatterCompactNumber, getUpgradeUrl } from './Usage.utils'
import UsageBarChart from './UsageBarChart'
import { ProjectSubscriptionResponse } from 'data/subscriptions/project-subscription-v2-query'
import UsageSection from './UsageSection'
export interface BandwidthProps {
projectRef: string
startDate: string | undefined
endDate: string | undefined
subscription: ProjectSubscriptionResponse | undefined
}
const Bandwidth = ({ projectRef }: BandwidthProps) => {
const { data: usage } = useProjectUsageQuery({ projectRef })
const { data: subscription } = useProjectSubscriptionQuery({ projectRef })
const { current_period_start, current_period_end } = subscription?.billing ?? {}
const startDate =
current_period_start !== undefined
? new Date(current_period_start * 1000).toISOString()
: undefined
let endDate =
current_period_end !== undefined ? new Date(current_period_end * 1000).toISOString() : undefined
// If end date is in future, set end date to now
if (endDate && dayjs(endDate).isAfter(dayjs())) {
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
endDate = new Date().toISOString().slice(0, -5) + 'Z'
}
const categoryMeta = USAGE_CATEGORIES.find((category) => category.key === 'bandwidth')
const upgradeUrl = getUpgradeUrl(projectRef, subscription)
const isFreeTier = subscription?.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.FREE
const isProTier = subscription?.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.PRO
const usageBasedBilling = !isFreeTier && !isProTier
const exceededLimitStyle = !usageBasedBilling ? 'text-red-900' : 'text-amber-900'
const Bandwidth = ({ projectRef, subscription, startDate, endDate }: BandwidthProps) => {
const { data: dbEgressData, isLoading: isLoadingDbEgressData } = useDailyStatsQuery({
projectRef,
attribute: 'total_egress_modified',
@@ -81,123 +44,13 @@ const Bandwidth = ({ projectRef }: BandwidthProps) => {
},
}
if (categoryMeta === undefined) return null
return (
<>
<SectionHeader title={categoryMeta.name} description={categoryMeta.description} />
{categoryMeta.attributes.map((attribute) => {
const usageMeta = usage?.[attribute.key as keyof ProjectUsageResponse] as UsageMetric
const usageRatio =
typeof usageMeta !== 'number' ? (usageMeta?.usage ?? 0) / (usageMeta?.limit ?? 0) : 0
const usageExcess = (usageMeta?.usage ?? 0) - (usageMeta?.limit ?? 0)
const chartData = chartMeta[attribute.key]?.data ?? []
return (
<div id={attribute.anchor} key={attribute.key}>
<SectionContent section={attribute}>
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<p className="text-sm">{attribute.name} usage</p>
{!usageBasedBilling && usageRatio >= 1 ? (
<div className="flex items-center space-x-2 min-w-[115px]">
<IconAlertTriangle
size={14}
strokeWidth={2}
className={exceededLimitStyle}
/>
<p className={`text-sm ${exceededLimitStyle}`}>Exceeded limit</p>
</div>
) : !usageBasedBilling && usageRatio >= USAGE_APPROACHING_THRESHOLD ? (
<div className="flex items-center space-x-2 min-w-[115px]">
<IconAlertTriangle size={14} strokeWidth={2} className="text-amber-900" />
<p className="text-sm text-amber-900">Approaching limit</p>
</div>
) : null}
</div>
{!usageBasedBilling && usageRatio >= USAGE_APPROACHING_THRESHOLD && (
<Link href={upgradeUrl}>
<a>
<Button type="default" size="tiny">
Upgrade project
</Button>
</a>
</Link>
)}
</div>
{usageMeta?.limit > 0 && (
<SparkBar
type="horizontal"
barClass={clsx(
usageRatio >= 1
? usageBasedBilling
? 'bg-amber-900'
: 'bg-red-900'
: usageRatio >= USAGE_APPROACHING_THRESHOLD
? 'bg-amber-900'
: 'bg-scale-1100'
)}
value={usageMeta?.usage ?? 0}
max={usageMeta?.limit || 1}
/>
)}
<div>
<div className="flex items-center justify-between border-b py-1">
<p className="text-xs text-scale-1000">
Included in {subscription?.tier.name.toLowerCase()}
</p>
<p className="text-xs">
{usageMeta?.limit === 0 ? 'Unlimited' : formatBytes(usageMeta?.limit ?? 0)}
</p>
</div>
<div className="flex items-center justify-between py-1">
<p className="text-xs text-scale-1000">
{attribute.chartPrefix || 'Used '}in period
</p>
<p className="text-xs">{formatBytes(usageMeta?.usage ?? 0)}</p>
</div>
{usageMeta?.limit > 0 && (
<div className="flex items-center justify-between border-t py-1">
<p className="text-xs text-scale-1000">Overage this month</p>
<p className="text-xs">
{usageExcess < 0 ? formatBytes(0) : formatBytes(usageExcess)}
</p>
</div>
)}
</div>
</div>
<div className="space-y-1">
<p>{attribute.name} per day</p>
{attribute.chartDescription.split('\n').map((paragraph, idx) => (
<p key={`para-${idx}`} className="text-sm text-scale-1000">
{paragraph}
</p>
))}
</div>
{chartMeta[attribute.key]?.isLoading ? (
<div className="space-y-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
) : (
<UsageBarChart
name={`${attribute.chartPrefix || ''}${attribute.name}`}
unit={attribute.unit}
attribute={attribute.attribute}
data={chartData}
yLeftMargin={chartMeta[attribute.key].margin}
yFormatter={(value) => ChartYFormatterCompactNumber(value, attribute.unit)}
/>
)}
</SectionContent>
</div>
)
})}
</>
<UsageSection
projectRef={projectRef}
categoryKey='bandwidth'
chartMeta={chartMeta}
subscription={subscription}
/>
)
}
@@ -1,17 +1,17 @@
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { DataPoint } from 'data/analytics/constants'
import { useInfraMonitoringQuery } from 'data/analytics/infra-monitoring-query'
import { useProjectSubscriptionQuery } from 'data/subscriptions/project-subscription-query'
import dayjs from 'dayjs'
import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
import Link from 'next/link'
import { Alert, Button } from 'ui'
import SectionContent from './SectionContent'
import SectionHeader from './SectionHeader'
import { COMPUTE_INSTANCE_SPECS, USAGE_CATEGORIES } from './Usage.constants'
import { getUpgradeUrl } from './Usage.utils'
import { getUpgradeUrlFromV2Subscription } from './Usage.utils'
import UsageBarChart from './UsageBarChart'
import Panel from 'components/ui/Panel'
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
import { useMemo } from 'react'
export interface InfrastructureProps {
projectRef: string
@@ -20,25 +20,27 @@ export interface InfrastructureProps {
// [Joshen] Need to update the IO budget chart to show burst mbps and duration next time
const Infrastructure = ({ projectRef }: InfrastructureProps) => {
const { data: subscription } = useProjectSubscriptionQuery({ projectRef })
const { current_period_start, current_period_end } = subscription?.billing ?? {}
const startDate =
current_period_start !== undefined
? new Date(current_period_start * 1000).toISOString()
: undefined
let endDate =
current_period_end !== undefined ? new Date(current_period_end * 1000).toISOString() : undefined
const { data: subscription } = useProjectSubscriptionV2Query({ projectRef })
const { current_period_start, current_period_end } = subscription ?? {}
const startDate = useMemo(() => {
return current_period_start ? new Date(current_period_start * 1000).toISOString() : undefined
}, [current_period_start])
// If end date is in future, set end date to now
if (endDate && dayjs(endDate).isAfter(dayjs())) {
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
endDate = new Date().toISOString().slice(0, -5) + 'Z'
}
const endDate = useMemo(() => {
const periodEndDate = current_period_end ? new Date(current_period_end * 1000) : undefined
// If end date is in future, set end date to now
if (periodEndDate && dayjs(periodEndDate).isAfter(dayjs())) {
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
return new Date().toISOString().slice(0, -5) + 'Z'
} else if (periodEndDate) {
return periodEndDate.toISOString()
}
}, [current_period_end])
const categoryMeta = USAGE_CATEGORIES.find((category) => category.key === 'infra')
const upgradeUrl = getUpgradeUrl(projectRef, subscription)
const isFreeTier = subscription?.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.FREE
const upgradeUrl = getUpgradeUrlFromV2Subscription(projectRef, subscription)
const isFreeTier = subscription?.plan?.id === 'free'
const currentComputeInstance = subscription?.addons.find((addon) =>
addon.supabase_prod_id.includes('_instance_')
)
@@ -150,12 +152,26 @@ const Infrastructure = ({ projectRef }: InfrastructureProps) => {
</Alert>
) : null}
<div className="space-y-1">
<p>What is Disk IO Bandwidth?</p>
<p>Disk IO Bandwidth</p>
<p className="text-sm text-scale-1000">
Smaller compute instances can burst up to the maximum disk IO bandwidth for 30
minutes in a day. Beyond that, the performance reverts to the baseline disk IO
bandwidth.
The disk performance of your workload is determined by the Disk IO bandwidth.
</p>
{currentComputeInstanceSpecs.maxBandwidth ===
currentComputeInstanceSpecs.baseBandwidth ? (
<p className="text-sm text-scale-1000">
Your current compute can has a baseline and maximum disk throughput of
{currentComputeInstanceSpecs.maxBandwidth.toLocaleString()} Mbps.
</p>
) : (
<p className="text-sm text-scale-1000">
Your current compute can burst up to{' '}
{currentComputeInstanceSpecs.maxBandwidth.toLocaleString()} Mbps for 30
minutes a day and reverts to the baseline performance of{' '}
{currentComputeInstanceSpecs.baseBandwidth.toLocaleString()} Mbps.
</p>
)}
</div>
<div>
<p className="text-sm mb-2">Overview</p>
@@ -185,7 +201,7 @@ const Infrastructure = ({ projectRef }: InfrastructureProps) => {
<div className="space-y-1">
<div className="flex flex-row justify-between">
{attribute.key === 'disk_io_consumption' ? (
<p>IO consumed per day</p>
<p>IO consumed per {interval === '1d' ? 'day' : 'hour'}</p>
) : (
<p>
Max{' '}
@@ -197,6 +213,18 @@ const Infrastructure = ({ projectRef }: InfrastructureProps) => {
)}
</div>
{attribute.key === 'ram_usage' && (
<p className="text-sm text-scale-1000">
Your compute instance has {currentComputeInstanceSpecs.memoryGb} GB memory.
</p>
)}
{attribute.key === 'cpu_usage' && (
<p className="text-sm text-scale-1000">
Your compute instance has {currentComputeInstanceSpecs.cpuCores} CPU cores.
</p>
)}
{attribute.chartDescription.split('\n').map((paragraph, idx) => (
<p key={`para-${idx}`} className="text-sm text-scale-1000">
{paragraph}
@@ -223,9 +251,7 @@ const Infrastructure = ({ projectRef }: InfrastructureProps) => {
<Panel.Content>
<div className="flex flex-col items-center justify-center space-y-2">
<p>No data</p>
<p className="text-sm text-scale-1000">
There is no data in the timeframe available
</p>
<p className="text-sm text-scale-1000">There is no data in period</p>
</div>
</Panel.Content>
</Panel>
@@ -2,7 +2,6 @@ import { PropsWithChildren } from 'react'
import { Badge, IconExternalLink } from 'ui'
import { CategoryAttribute } from './Usage.constants'
import Link from 'next/link'
import { ReactMarkdown } from 'react-markdown/lib/react-markdown'
export interface SectionContent {
section: CategoryAttribute
@@ -37,7 +36,7 @@ const SectionContent = ({
</div>
{links && links.length && (
<div className="space-y-2">
<p className="text-sm text-scale-1100 mb-2">More information</p>
<p className="text-sm text-scale-1200 dark:text-scale-1100 mb-2">More information</p>
{links.map((link) => (
<div key={link.url}>
<Link href={link.url}>
@@ -1,54 +1,16 @@
import clsx from 'clsx'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import SparkBar from 'components/ui/SparkBar'
import { useDailyStatsQuery } from 'data/analytics/daily-stats-query'
import { useProjectSubscriptionQuery } from 'data/subscriptions/project-subscription-query'
import {
ProjectUsageResponse,
UsageMetric,
useProjectUsageQuery,
} from 'data/usage/project-usage-query'
import dayjs from 'dayjs'
import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
import { formatBytes } from 'lib/helpers'
import Link from 'next/link'
import { Badge, Button, IconAlertTriangle, IconExternalLink } from 'ui'
import { USAGE_APPROACHING_THRESHOLD } from '../Billing.constants'
import UsageBarChart from './UsageBarChart'
import SectionContent from './SectionContent'
import SectionHeader from './SectionHeader'
import { USAGE_CATEGORIES } from './Usage.constants'
import { ChartYFormatterCompactNumber, getUpgradeUrl } from './Usage.utils'
import { DataPoint } from 'data/analytics/constants'
import { ProjectSubscriptionResponse } from 'data/subscriptions/project-subscription-v2-query'
import UsageSection from './UsageSection'
export interface SizeAndCountsProps {
projectRef: string
startDate: string | undefined
endDate: string | undefined
subscription: ProjectSubscriptionResponse | undefined
}
const SizeAndCounts = ({ projectRef }: SizeAndCountsProps) => {
const { data: usage } = useProjectUsageQuery({ projectRef })
const { data: subscription } = useProjectSubscriptionQuery({ projectRef })
const { current_period_start, current_period_end } = subscription?.billing ?? {}
const startDate =
current_period_start !== undefined
? new Date(current_period_start * 1000).toISOString()
: undefined
let endDate =
current_period_end !== undefined ? new Date(current_period_end * 1000).toISOString() : undefined
// If end date is in future, set end date to now
if (endDate && dayjs(endDate).isAfter(dayjs())) {
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
endDate = new Date().toISOString().slice(0, -5) + 'Z'
}
const categoryMeta = USAGE_CATEGORIES.find((category) => category.key === 'sizeCount')
const upgradeUrl = getUpgradeUrl(projectRef, subscription)
const isFreeTier = subscription?.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.FREE
const isProTier = subscription?.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.PRO
const usageBasedBilling = !isFreeTier && !isProTier
const exceededLimitStyle = !usageBasedBilling ? 'text-red-900' : 'text-amber-900'
const SizeAndCounts = ({ projectRef, startDate, endDate, subscription }: SizeAndCountsProps) => {
const { data: dbSizeData, isLoading: isLoadingDbSizeData } = useDailyStatsQuery({
projectRef,
attribute: 'total_db_size_bytes',
@@ -96,160 +58,13 @@ const SizeAndCounts = ({ projectRef }: SizeAndCountsProps) => {
},
}
if (categoryMeta === undefined) return null
return (
<>
<SectionHeader title={categoryMeta.name} description={categoryMeta.description} />
{categoryMeta.attributes.map((attribute) => {
const usageMeta = usage?.[attribute.key as keyof ProjectUsageResponse] as UsageMetric
const usageRatio =
typeof usageMeta !== 'number' ? (usageMeta?.usage ?? 0) / (usageMeta?.limit ?? 0) : 0
const usageExcess = (usageMeta?.usage ?? 0) - (usageMeta?.limit ?? 0)
const chartData = chartMeta[attribute.key]?.data ?? []
return (
<div id={attribute.anchor} key={attribute.key}>
<SectionContent section={attribute}>
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<p className="text-sm">{attribute.name} usage</p>
{!usageBasedBilling && usageRatio >= 1 ? (
<div className="flex items-center space-x-2 min-w-[115px]">
<IconAlertTriangle
size={14}
strokeWidth={2}
className={exceededLimitStyle}
/>
<p className={`text-sm ${exceededLimitStyle}`}>Exceeded limit</p>
</div>
) : !usageBasedBilling && usageRatio >= USAGE_APPROACHING_THRESHOLD ? (
<div className="flex items-center space-x-2 min-w-[115px]">
<IconAlertTriangle size={14} strokeWidth={2} className="text-amber-900" />
<p className="text-sm text-amber-900">Approaching limit</p>
</div>
) : null}
</div>
{!usageBasedBilling && usageRatio >= USAGE_APPROACHING_THRESHOLD && (
<Link href={upgradeUrl}>
<a>
<Button type="default" size="tiny">
Upgrade project
</Button>
</a>
</Link>
)}
</div>
{usageMeta?.limit > 0 && (
<SparkBar
type="horizontal"
barClass={clsx(
usageRatio >= 1
? usageBasedBilling
? 'bg-amber-900'
: 'bg-red-900'
: usageRatio >= USAGE_APPROACHING_THRESHOLD
? 'bg-amber-900'
: 'bg-scale-1100'
)}
value={usageMeta?.usage ?? 0}
max={usageMeta?.limit || 1}
/>
)}
<div>
<div className="flex items-center justify-between border-b py-1">
<p className="text-xs text-scale-1000">
Included in {subscription?.tier.name.toLowerCase()}
</p>
<p className="text-xs">
{attribute.unit === 'bytes'
? formatBytes(usageMeta?.limit ?? 0)
: (usageMeta?.limit ?? 0).toLocaleString()}
</p>
</div>
<div className="flex items-center justify-between py-1">
<p className="text-xs text-scale-1000">
{attribute.chartPrefix || 'Used '}in period
</p>
<p className="text-xs">
{attribute.unit === 'bytes'
? formatBytes(usageMeta?.usage ?? 0)
: (usageMeta?.usage ?? 0).toLocaleString()}
</p>
</div>
{usageMeta?.limit > 0 && (
<div className="flex items-center justify-between border-t py-1">
<p className="text-xs text-scale-1000">Overage this month</p>
<p className="text-xs">
{usageExcess < 0
? attribute.unit === 'bytes'
? formatBytes(0)
: 0
: attribute.unit === 'bytes'
? formatBytes(usageExcess)
: usageExcess.toLocaleString()}
</p>
</div>
)}
</div>
</div>
{attribute.key === 'db_size' &&
subscription?.tier.supabase_prod_id !== PRICING_TIER_PRODUCT_IDS.FREE && (
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<p className="text-sm">Disk size:</p>
<p className="text-sm">{usage?.disk_volume_size_gb} GB</p>
<Badge color="green" size="small">
Auto-scaling
</Badge>
</div>
<Link href="https://supabase.com/docs/guides/platform/database-usage#disk-management">
<a>
<Button size="tiny" type="default" icon={<IconExternalLink size={14} />}>
What is disk size?
</Button>
</a>
</Link>
</div>
)}
<div className="space-y-1">
<p>
{attribute.chartPrefix || ''}
{attribute.name} per day
</p>
{attribute.chartDescription.split('\n').map((paragraph, idx) => (
<p key={`para-${idx}`} className="text-sm text-scale-1000">
{paragraph}
</p>
))}
</div>
{chartMeta[attribute.key].isLoading ? (
<div className="space-y-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
) : (
<UsageBarChart
name={`${attribute.chartPrefix || ''}${attribute.name}`}
unit={attribute.unit}
attribute={attribute.attribute}
data={chartData}
yLeftMargin={chartMeta[attribute.key].margin}
yFormatter={(value) => ChartYFormatterCompactNumber(value, attribute.unit)}
/>
)}
</SectionContent>
</div>
)
})}
</>
<UsageSection
projectRef={projectRef}
categoryKey='sizeCount'
chartMeta={chartMeta}
subscription={subscription}
/>
)
}
@@ -1,3 +1,8 @@
import { ProjectSubscriptionResponse } from 'data/subscriptions/project-subscription-v2-query'
import { ProjectUsageData } from 'data/usage/project-usage-query'
import Link from 'next/link'
import { Badge, Button, IconExternalLink } from 'ui'
export const Y_DOMAIN_CEILING_MULTIPLIER = 4 / 3
export const USAGE_STATUS = {
@@ -19,14 +24,22 @@ export interface CategoryAttribute {
}[]
description: string
chartDescription: string
additionalInfo?: (
subscription?: ProjectSubscriptionResponse,
usage?: ProjectUsageData
) => JSX.Element | null
}
export const USAGE_CATEGORIES: {
export type CategoryMetaKey = 'infra' | 'bandwidth' | 'sizeCount' | 'activity'
export interface CategoryMeta {
key: 'infra' | 'bandwidth' | 'sizeCount' | 'activity'
name: string
description: string
attributes: CategoryAttribute[]
}[] = [
}
export const USAGE_CATEGORIES: CategoryMeta[] = [
{
key: 'infra',
name: 'Infrastructure',
@@ -76,7 +89,7 @@ export const USAGE_CATEGORIES: {
},
],
description:
'SSD Disks are attached to your servers. The disk performance of your workload is determined by the Disk IO bandwidth of this connection.',
'Smaller compute instances (below 4XL) can burst up to their largest throughput and IOPS for 30 minutes in a day. Beyond that, the performance reverts to the baseline. Your disk budget gets replenished throughout the day.',
chartDescription: '',
},
],
@@ -84,7 +97,7 @@ export const USAGE_CATEGORIES: {
{
key: 'bandwidth',
name: 'Bandwidth',
description: 'Amount of data transmitted over network connections',
description: 'Amount of data transmitted over all network connections',
attributes: [
{
anchor: 'dbEgress',
@@ -94,7 +107,7 @@ export const USAGE_CATEGORIES: {
unit: 'bytes',
description:
'Contains any outgoing traffic (egress) from your database.\nBilling is based on the total sum of egress in GB throughout your billing period.',
chartDescription: 'The data shown here refreshes every 24 hours.',
chartDescription: 'The data refreshes every 24 hours.',
},
{
anchor: 'storageEgress',
@@ -103,8 +116,8 @@ export const USAGE_CATEGORIES: {
name: 'Storage egress',
unit: 'bytes',
description:
'All requests to download/view your storage items go through our CDN. We sum up all outgoing traffic (egress) for storage related requests through our CDN. We do not differentiate between cache and no cache hits.\nBilling is based on the total amount of egress in GB throughout your billing period.',
chartDescription: 'The data shown here refreshes every 24 hours.',
'All requests to view and download your storage items go through our CDN. We sum up all outgoing traffic (egress) for storage related requests through our CDN. We do not differentiate between cache and no cache hits.\nBilling is based on the total amount of egress in GB throughout your billing period.',
chartDescription: 'The data refreshes every 24 hours.',
},
],
},
@@ -128,7 +141,26 @@ export const USAGE_CATEGORIES: {
url: 'https://supabase.com/docs/guides/platform/database-size',
},
],
chartDescription: 'The data shown here refreshes every 24 hours.',
chartDescription: 'The data refreshes every 24 hours.',
additionalInfo: (subscription?: ProjectSubscriptionResponse, usage?: ProjectUsageData) =>
subscription?.plan?.id !== 'free' ? (
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<p className="text-sm">Disk size:</p>
<p className="text-sm">{usage?.disk_volume_size_gb} GB</p>
<Badge color="green" size="small">
Auto-scaling
</Badge>
</div>
<Link href="https://supabase.com/docs/guides/platform/database-usage#disk-management">
<a>
<Button size="tiny" type="default" icon={<IconExternalLink size={14} />}>
What is disk size?
</Button>
</a>
</Link>
</div>
) : null,
},
{
anchor: 'storageSize',
@@ -138,8 +170,8 @@ export const USAGE_CATEGORIES: {
chartPrefix: 'Max ',
unit: 'bytes',
description:
'Sum of all objects in your storage buckets.\nBilling is based on the average size in GB throughout your billing period',
chartDescription: 'The data shown here refreshes every 24 hours.',
'Sum of all objects in your storage buckets.\nBilling is based on the average size in GB throughout your billing period.',
chartDescription: 'The data refreshes every 24 hours.',
},
{
anchor: 'funcCount',
@@ -149,8 +181,8 @@ export const USAGE_CATEGORIES: {
chartPrefix: 'Max ',
unit: 'absolute',
description:
'Number of serverless functions in your project.\nBilling is based on the maximum amount of functions at any point in time throughout your billing period',
chartDescription: 'The data shown here refreshes every 24 hours.',
'Number of serverless functions in your project.\nBilling is based on the maximum amount of functions at any point in time throughout your billing period.',
chartDescription: 'The data refreshes every 24 hours.',
},
],
},
@@ -168,7 +200,7 @@ export const USAGE_CATEGORIES: {
description:
'Users who log in or refresh their token count towards MAU.\nBilling is based on the sum of distinct users requesting your API throughout the billing period. Resets every billing cycle.',
chartDescription:
'The data shown here is refreshed over a period of 24 hours and resets at the beginning of every billing period.\nThe data points are relative to the beginning of your billing period.',
'The data is refreshed over a period of 24 hours and resets at the beginning of every billing period.\nThe data points are relative to the beginning of your billing period.',
},
{
anchor: 'mauSso',
@@ -179,7 +211,7 @@ export const USAGE_CATEGORIES: {
description:
'SSO users who log in or refresh their token count towards SSO MAU.\nBilling is based on the sum of distinct Single Sign-On users requesting your API throughout the billing period. Resets every billing cycle.',
chartDescription:
'The data shown here is refreshed over a period of 24 hours and resets at the beginning of every billing period.\nThe data points are relative to the beginning of your billing period.',
'The data refreshes over a period of 24 hours and resets at the beginning of every billing period.\nThe data points are relative to the beginning of your billing period.',
},
{
anchor: 'storageImageTransformations',
@@ -188,9 +220,9 @@ export const USAGE_CATEGORIES: {
name: 'Storage image transformations',
unit: 'absolute',
description:
'We distinctly count all images that were transformed in the billing period, ignoring any transformations. If you transform one image with different transformations, it only counts as one.\nBilling is based on the unique count of (origin) images that used transformations throughout the billing period. Resets every billing cycle.',
'We count all images that were transformed in the billing period, ignoring any transformations.\nUsage example: You transform one image with four different size transformations and another image with just a single transformations. It counts as two, as only two images were transformed.\nBilling is based on the count of (origin) images that used transformations throughout the billing period. Resets every billing cycle.',
chartDescription:
'The data shown here refreshes every 24 hours.\nThe data points are relative to the beginning of your billing period.',
'The data refreshes every 24 hours.\nThe data points are relative to the beginning of your billing period.',
},
{
anchor: 'functionInvocations',
@@ -199,8 +231,8 @@ export const USAGE_CATEGORIES: {
name: 'Edge function invocations',
unit: 'absolute',
description:
'Every single serverless function invocation independent of response status is counted.\nBilling is based on the sum of all invocations throughout your billing period.',
chartDescription: 'The data shown here refreshes every 24 hours.',
'Every serverless function invocation independent of response status is counted.\nBilling is based on the sum of all invocations throughout your billing period.',
chartDescription: 'The data refreshes every 24 hours.',
},
{
anchor: 'realtimeMessageCount',
@@ -209,8 +241,8 @@ export const USAGE_CATEGORIES: {
name: 'Realtime message count',
unit: 'absolute',
description:
"Count of messages going through Realtime. If you do a database change and 5 clients listen to that change via Realtime, that's 5 messages. If you broadcast a message and 4 clients listen to that, that's 5 messages (1 message sent, 4 received).\nBilling is based on the total amount of messages throughout your billing period.",
chartDescription: 'The data shown here refreshes every 24 hours.',
"Count of messages going through Realtime.\nUsage example: If you do a database change and 5 clients listen to that change via Realtime, that's 5 messages. If you broadcast a message and 4 clients listen to that, that's 5 messages (1 message sent, 4 received).\nBilling is based on the total amount of messages throughout your billing period.",
chartDescription: 'The data refreshes every 24 hours.',
},
{
anchor: 'realtimePeakConnection',
@@ -221,7 +253,7 @@ export const USAGE_CATEGORIES: {
unit: 'absolute',
description:
'Total number of successful connections (not connection attempts).\nBilling is based on the maximum amount of concurrent peak connections throughout your billing period.',
chartDescription: 'The data shown here refreshes every 24 hours.',
chartDescription: 'The data refreshes every 24 hours.',
},
],
},
@@ -229,45 +261,65 @@ export const USAGE_CATEGORIES: {
// [Joshen] Ideally live in common package for www as well
export const COMPUTE_INSTANCE_SPECS: {
[key: string]: { maxBandwidth: number; baseBandwidth: number }
[key: string]: { maxBandwidth: number; baseBandwidth: number; memoryGb: number; cpuCores: number }
} = {
addon_instance_micro: {
memoryGb: 1,
cpuCores: 2,
maxBandwidth: 2085,
baseBandwidth: 87,
},
addon_instance_small: {
memoryGb: 2,
cpuCores: 2,
maxBandwidth: 2085,
baseBandwidth: 174,
},
addon_instance_medium: {
memoryGb: 4,
cpuCores: 2,
maxBandwidth: 2085,
baseBandwidth: 347,
},
addon_instance_large: {
memoryGb: 8,
cpuCores: 2,
maxBandwidth: 4750,
baseBandwidth: 630,
},
addon_instance_xlarge: {
memoryGb: 16,
cpuCores: 4,
maxBandwidth: 4750,
baseBandwidth: 1188,
},
addon_instance_xxlarge: {
memoryGb: 32,
cpuCores: 8,
maxBandwidth: 4750,
baseBandwidth: 2375,
},
addon_instance_4xlarge: {
memoryGb: 64,
cpuCores: 16,
maxBandwidth: 4750,
baseBandwidth: 4750,
},
addon_instance_8xlarge: {
memoryGb: 128,
cpuCores: 32,
maxBandwidth: 9500,
baseBandwidth: 9500,
},
addon_instance_12xlarge: {
memoryGb: 192,
cpuCores: 48,
maxBandwidth: 14250,
baseBandwidth: 14250,
},
addon_instance_16xlarge: {
memoryGb: 256,
cpuCores: 64,
maxBandwidth: 19000,
baseBandwidth: 19000,
},
@@ -1,11 +1,10 @@
import clsx from 'clsx'
import { useParams } from 'common'
import { useProjectSubscriptionQuery } from 'data/subscriptions/project-subscription-query'
import { useProjectUsageQuery } from 'data/usage/project-usage-query'
import dayjs from 'dayjs'
import Link from 'next/link'
import { useRef, useState } from 'react'
import { Button, IconAlertCircle, IconCheckCircle, IconLoader, Listbox } from 'ui'
import { useMemo, useRef, useState } from 'react'
import { Button, IconAlertCircle, IconLoader, Listbox } from 'ui'
import Activity from './Activity'
import Bandwidth from './Bandwidth'
import Infrastructure from './Infrastructure'
@@ -13,7 +12,7 @@ import SizeAndCounts from './SizeAndCounts'
import { USAGE_CATEGORIES, USAGE_STATUS } from './Usage.constants'
import { getUsageStatus } from './Usage.utils'
import { useInfraMonitoringQuery } from 'data/analytics/infra-monitoring-query'
import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
const Usage = () => {
const { ref } = useParams()
@@ -28,17 +27,38 @@ const Usage = () => {
const activityRef = useRef<HTMLDivElement>(null)
const { data: usage } = useProjectUsageQuery({ projectRef: ref })
const { data: subscription, isLoading: isLoadingSubscription } = useProjectSubscriptionQuery({
const { data: subscription, isLoading: isLoadingSubscription } = useProjectSubscriptionV2Query({
projectRef: selectedProjectRef,
})
const { current_period_start, current_period_end } = subscription?.billing ?? {}
const startDate =
current_period_start !== undefined
? new Date(current_period_start * 1000).toISOString()
: undefined
const { current_period_start, current_period_end } = subscription ?? {}
const startDate = useMemo(() => {
return current_period_start ? new Date(current_period_start * 1000).toISOString() : undefined
}, [current_period_start])
const endDate =
current_period_end !== undefined ? new Date(current_period_end * 1000).toISOString() : undefined
const dailyStatsEndDate = useMemo(() => {
// If end date is in future, set end date to now
if (endDate && dayjs(endDate).isAfter(dayjs())) {
const yesterday = dayjs(new Date()).subtract(1, 'day')
/**
* Currently, daily-stats data is only available a day later, so we'll use yesterday as end date, as otherwise the current day would just show up with "0" values
*
* We are actively working on removing this restriction on the data-eng/LF side and can remove this workaround once that's done
*/
const newEndDate = yesterday.isAfter(dayjs(startDate)) ? yesterday : new Date()
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
return newEndDate.toISOString().slice(0, -5) + 'Z'
} else if (endDate) {
return endDate
}
}, [endDate, startDate])
const { data: ioBudgetData } = useInfraMonitoringQuery({
projectRef: selectedProjectRef,
attribute: 'disk_io_budget',
@@ -52,13 +72,10 @@ const Usage = () => {
] ?? 100
)
const billingCycleStart = dayjs.unix(subscription?.billing?.current_period_start ?? 0).utc()
const billingCycleEnd = dayjs.unix(subscription?.billing?.current_period_end ?? 0).utc()
const billingCycleStart = dayjs.unix(subscription?.current_period_start ?? 0).utc()
const billingCycleEnd = dayjs.unix(subscription?.current_period_end ?? 0).utc()
const subscriptionTierId = subscription?.tier?.supabase_prod_id
const usageBillingEnabled =
subscriptionTierId !== PRICING_TIER_PRODUCT_IDS.FREE &&
subscriptionTierId !== PRICING_TIER_PRODUCT_IDS.PRO
const usageBillingEnabled = subscription?.usage_billing_enabled
const scrollTo = (id: 'infra' | 'bandwidth' | 'sizeCount' | 'activity') => {
switch (id) {
@@ -107,7 +124,7 @@ const Usage = () => {
) : subscription !== undefined ? (
<div>
<p className={clsx('text-sm transition', isLoadingSubscription && 'opacity-50')}>
Project is on {subscription.tier.name}
Project is on {subscription.plan.name} plan
</p>
<p className="text-sm text-scale-1000">
{billingCycleStart.format('DD MMM YYYY')} -{' '}
@@ -170,13 +187,28 @@ const Usage = () => {
<Infrastructure projectRef={selectedProjectRef} />
</div>
<div id="bandwidth" ref={bandwidthRef}>
<Bandwidth projectRef={selectedProjectRef} />
<Bandwidth
projectRef={selectedProjectRef}
subscription={subscription}
startDate={startDate}
endDate={dailyStatsEndDate}
/>
</div>
<div id="size_and_counts" ref={sizeAndCountsRef}>
<SizeAndCounts projectRef={selectedProjectRef} />
<SizeAndCounts
projectRef={selectedProjectRef}
subscription={subscription}
startDate={startDate}
endDate={dailyStatsEndDate}
/>
</div>
<div id="activity" ref={activityRef}>
<Activity projectRef={selectedProjectRef} />
<Activity
projectRef={selectedProjectRef}
subscription={subscription}
startDate={startDate}
endDate={dailyStatsEndDate}
/>
</div>
</>
)
@@ -5,6 +5,7 @@ import { CategoryAttribute, USAGE_STATUS } from './Usage.constants'
import { StripeSubscription } from 'components/interfaces/Billing'
import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
import { formatBytes } from 'lib/helpers'
import { ProjectSubscriptionResponse } from 'data/subscriptions/project-subscription-v2-query'
// [Joshen] This is just for development to generate some test data for chart rendering
export const generateUsageData = (attribute: string, days: number): DataPoint[] => {
@@ -48,6 +49,21 @@ export const getUpgradeUrl = (projectRef: string, subscription?: StripeSubscript
: `/project/${projectRef}/settings/billing/update/pro`
}
export const getUpgradeUrlFromV2Subscription = (
projectRef: string,
subscription?: ProjectSubscriptionResponse
) => {
if (!subscription) return `/project/${projectRef}/settings/billing/update`
return subscription?.plan.id === 'enterprise'
? `/project/${projectRef}/settings/billing/update/enterprise`
: subscription?.plan.id === 'team'
? `/project/${projectRef}/settings/billing/update/team`
: subscription?.plan.id === 'free'
? `/project/${projectRef}/settings/billing/update`
: `/project/${projectRef}/settings/billing/update/pro`
}
const compactNumberFormatter = new Intl.NumberFormat('en-US', {
notation: 'compact',
compactDisplay: 'short',
@@ -58,7 +74,7 @@ export const ChartYFormatterCompactNumber = (number: number | string, unit: stri
if (unit === 'bytes') {
const formattedBytes = formatBytes(number, 0).replace(/\s/g, '')
return formattedBytes === '0bytes' ? '0' : formattedBytes
} else {
return compactNumberFormatter.format(number)
@@ -0,0 +1,195 @@
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import SectionHeader from './SectionHeader'
import { CategoryMetaKey, USAGE_CATEGORIES } from './Usage.constants'
import {
ProjectUsageResponse,
UsageMetric,
useProjectUsageQuery,
} from 'data/usage/project-usage-query'
import { DataPoint } from 'data/analytics/constants'
import SectionContent from './SectionContent'
import { Button, IconAlertTriangle } from 'ui'
import { USAGE_APPROACHING_THRESHOLD } from '../Billing.constants'
import Link from 'next/link'
import SparkBar from 'components/ui/SparkBar'
import clsx from 'clsx'
import { ProjectSubscriptionResponse } from 'data/subscriptions/project-subscription-v2-query'
import { ChartYFormatterCompactNumber, getUpgradeUrlFromV2Subscription } from './Usage.utils'
import { formatBytes } from 'lib/helpers'
import UsageBarChart from './UsageBarChart'
import Panel from 'components/ui/Panel'
interface UsageSectionProps {
projectRef: string
categoryKey: CategoryMetaKey
subscription: ProjectSubscriptionResponse | undefined
chartMeta: {
[key: string]: { data: DataPoint[]; margin: number; isLoading: boolean; hasNoData: boolean }
}
}
const UsageSection = ({ projectRef, categoryKey, chartMeta, subscription }: UsageSectionProps) => {
const { data: usage } = useProjectUsageQuery({ projectRef })
const categoryMeta = USAGE_CATEGORIES.find((category) => category.key === categoryKey)
if (!categoryMeta) return null
const usageBasedBilling = subscription?.usage_billing_enabled
const exceededLimitStyle = !usageBasedBilling ? 'text-red-900' : 'text-amber-900'
const upgradeUrl = getUpgradeUrlFromV2Subscription(projectRef, subscription)
return (
<>
<SectionHeader title={categoryMeta.name} description={categoryMeta.description} />
{categoryMeta.attributes.map((attribute) => {
const usageMeta = usage?.[attribute.key as keyof ProjectUsageResponse] as UsageMetric
const usageRatio =
typeof usageMeta !== 'number' ? (usageMeta?.usage ?? 0) / (usageMeta?.limit ?? 0) : 0
const usageExcess = (usageMeta?.usage ?? 0) - (usageMeta?.limit ?? 0)
const chartData = chartMeta[attribute.key]?.data ?? []
const notAllValuesZero = chartData.some(
(dataPoint) => Number(dataPoint[attribute.attribute]) !== 0
)
return (
<div id={attribute.anchor} key={attribute.key}>
<SectionContent section={attribute}>
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<p className="text-sm">{attribute.name} usage</p>
{!usageBasedBilling && usageRatio >= 1 ? (
<div className="flex items-center space-x-2 min-w-[115px]">
<IconAlertTriangle
size={14}
strokeWidth={2}
className={exceededLimitStyle}
/>
<p className={`text-sm ${exceededLimitStyle}`}>Exceeded limit</p>
</div>
) : !usageBasedBilling && usageRatio >= USAGE_APPROACHING_THRESHOLD ? (
<div className="flex items-center space-x-2 min-w-[115px]">
<IconAlertTriangle size={14} strokeWidth={2} className="text-amber-900" />
<p className="text-sm text-amber-900">Approaching limit</p>
</div>
) : null}
</div>
{!usageBasedBilling && usageRatio >= USAGE_APPROACHING_THRESHOLD && (
<Link href={upgradeUrl}>
<a>
<Button type="default" size="tiny">
Upgrade project
</Button>
</a>
</Link>
)}
</div>
{usageMeta?.limit > 0 && (
<SparkBar
type="horizontal"
barClass={clsx(
usageRatio >= 1
? usageBasedBilling
? 'bg-amber-900'
: 'bg-red-900'
: usageRatio >= USAGE_APPROACHING_THRESHOLD
? 'bg-amber-900'
: 'bg-scale-1100'
)}
bgClass="bg-gray-300 dark:bg-gray-600"
value={usageMeta?.usage ?? 0}
max={usageMeta?.limit || 1}
/>
)}
<div>
<div className="flex items-center justify-between border-b py-1">
<p className="text-xs text-scale-1000">
Included in {subscription?.plan?.name.toLowerCase()} plan
</p>
<p className="text-xs">
{attribute.unit === 'bytes'
? formatBytes(usageMeta?.limit ?? 0)
: (usageMeta?.limit ?? 0).toLocaleString()}
</p>
</div>
<div className="flex items-center justify-between py-1">
<p className="text-xs text-scale-1000">
{attribute.chartPrefix || 'Used '}in period
</p>
<p className="text-xs">
{attribute.unit === 'bytes'
? formatBytes(usageMeta?.usage ?? 0)
: (usageMeta?.usage ?? 0).toLocaleString()}
</p>
</div>
{usageMeta?.limit > 0 && (
<div className="flex items-center justify-between border-t py-1">
<p className="text-xs text-scale-1000">Overage in period</p>
<p className="text-xs">
{usageExcess < 0
? attribute.unit === 'bytes'
? formatBytes(0)
: 0
: attribute.unit === 'bytes'
? formatBytes(usageExcess)
: usageExcess.toLocaleString()}
</p>
</div>
)}
</div>
</div>
{attribute.additionalInfo?.(subscription, usage)}
<div className="space-y-1">
<p>
{attribute.chartPrefix || ''}
{attribute.name} per day
</p>
{attribute.chartDescription.split('\n').map((paragraph, idx) => (
<p key={`para-${idx}`} className="text-sm text-scale-1000">
{paragraph}
</p>
))}
</div>
{chartMeta[attribute.key].isLoading ? (
<div className="space-y-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
) : chartData.length > 1 && notAllValuesZero ? (
<UsageBarChart
name={`${attribute.chartPrefix || ''}${attribute.name}`}
unit={attribute.unit}
attribute={attribute.attribute}
data={chartData}
yLeftMargin={chartMeta[attribute.key].margin}
yFormatter={(value) => ChartYFormatterCompactNumber(value, attribute.unit)}
/>
) : (
<Panel>
<Panel.Content>
<div className="flex flex-col items-center justify-center space-y-2">
<p>No data</p>
<p className="text-sm text-scale-1000">
No {notAllValuesZero ? 'data' : 'usage'} in period
</p>
</div>
</Panel.Content>
</Panel>
)}
</SectionContent>
</div>
)
})}
</>
)
}
export default UsageSection