diff --git a/studio/components/interfaces/Billing/Billing.constants.tsx b/studio/components/interfaces/Billing/Billing.constants.tsx index 6d22f8dc1a7..7661fa41f61 100644 --- a/studio/components/interfaces/Billing/Billing.constants.tsx +++ b/studio/components/interfaces/Billing/Billing.constants.tsx @@ -164,7 +164,7 @@ export const USAGE_BASED_PRODUCTS = [ costPerUnit: 0.000002, tooltip: ( - 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. ), diff --git a/studio/components/interfaces/BillingV2/Subscription/BillingBreakdown.tsx b/studio/components/interfaces/BillingV2/Subscription/BillingBreakdown.tsx index 5d5f97b9457..1ae114d3079 100644 --- a/studio/components/interfaces/BillingV2/Subscription/BillingBreakdown.tsx +++ b/studio/components/interfaces/BillingV2/Subscription/BillingBreakdown.tsx @@ -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} diff --git a/studio/components/interfaces/BillingV2/Subscription/Subscription.utils.ts b/studio/components/interfaces/BillingV2/Subscription/Subscription.utils.ts index e809b7db543..44da0c1b0ae 100644 --- a/studio/components/interfaces/BillingV2/Subscription/Subscription.utils.ts +++ b/studio/components/interfaces/BillingV2/Subscription/Subscription.utils.ts @@ -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 } } diff --git a/studio/components/interfaces/BillingV2/Subscription/Tier/SubscriptionTier.tsx b/studio/components/interfaces/BillingV2/Subscription/Tier/SubscriptionTier.tsx index e61b6193a97..6955be892c6 100644 --- a/studio/components/interfaces/BillingV2/Subscription/Tier/SubscriptionTier.tsx +++ b/studio/components/interfaces/BillingV2/Subscription/Tier/SubscriptionTier.tsx @@ -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`} /> diff --git a/studio/components/interfaces/BillingV2/Usage/Activity.tsx b/studio/components/interfaces/BillingV2/Usage/Activity.tsx index 987f30d3fff..b6bb88ef7c1 100644 --- a/studio/components/interfaces/BillingV2/Usage/Activity.tsx +++ b/studio/components/interfaces/BillingV2/Usage/Activity.tsx @@ -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 ( - <> - - - {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 ( -
- - {usageMeta?.available_in_plan ? ( - <> -
-
-
-

{attribute.name} usage

- {!usageBasedBilling && usageRatio >= 1 ? ( -
- -

Exceeded limit

-
- ) : !usageBasedBilling && usageRatio >= USAGE_APPROACHING_THRESHOLD ? ( -
- -

Approaching limit

-
- ) : null} -
- {!usageBasedBilling && usageRatio >= USAGE_APPROACHING_THRESHOLD && ( - - - - - - )} -
- {usageMeta.limit > 0 && ( - = 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} - /> - )} -
-
-

- Included in {subscription?.tier.name.toLowerCase()} -

- {usageMeta?.limit === -1 ? ( -

None

- ) : usageMeta?.limit === 0 ? ( -

Unlimited

- ) : ( -

{(usageMeta?.limit ?? 0).toLocaleString()}

- )} -
-
-

- {attribute.chartPrefix || 'Used '}in period -

-

{(usageMeta?.usage ?? 0).toLocaleString()}

-
- {usageMeta.limit > 0 && ( -
-

Overage this month

-

- {((usageMeta?.limit ?? 0) === -1 || usageExcess < 0 - ? 0 - : usageExcess - ).toLocaleString()} -

-
- )} -
-
-
-

{attribute.name} per day

- {attribute.chartDescription.split('\n').map((paragraph, idx) => ( -

- {paragraph} -

- ))} -
- {chartMeta[attribute.key].isLoading ? ( -
- - - -
- ) : ( - ChartYFormatterCompactNumber(value, attribute.unit)} - /> - )} - - ) : ( - - -
-
-

Not included in plan

-
-

- You need to be on a higher plan in order to use this feature. -

-
-
- - - - - -
-
-
- )} -
-
- ) - })} - + ) } diff --git a/studio/components/interfaces/BillingV2/Usage/Bandwidth.tsx b/studio/components/interfaces/BillingV2/Usage/Bandwidth.tsx index 4e1c17070e7..c013dc314b0 100644 --- a/studio/components/interfaces/BillingV2/Usage/Bandwidth.tsx +++ b/studio/components/interfaces/BillingV2/Usage/Bandwidth.tsx @@ -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 ( - <> - - - {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 ( -
- -
-
-
-

{attribute.name} usage

- {!usageBasedBilling && usageRatio >= 1 ? ( -
- -

Exceeded limit

-
- ) : !usageBasedBilling && usageRatio >= USAGE_APPROACHING_THRESHOLD ? ( -
- -

Approaching limit

-
- ) : null} -
- {!usageBasedBilling && usageRatio >= USAGE_APPROACHING_THRESHOLD && ( - - - - - - )} -
- {usageMeta?.limit > 0 && ( - = 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} - /> - )} -
-
-

- Included in {subscription?.tier.name.toLowerCase()} -

-

- {usageMeta?.limit === 0 ? 'Unlimited' : formatBytes(usageMeta?.limit ?? 0)} -

-
-
-

- {attribute.chartPrefix || 'Used '}in period -

-

{formatBytes(usageMeta?.usage ?? 0)}

-
- {usageMeta?.limit > 0 && ( -
-

Overage this month

-

- {usageExcess < 0 ? formatBytes(0) : formatBytes(usageExcess)} -

-
- )} -
-
-
-

{attribute.name} per day

- {attribute.chartDescription.split('\n').map((paragraph, idx) => ( -

- {paragraph} -

- ))} -
- {chartMeta[attribute.key]?.isLoading ? ( -
- - - -
- ) : ( - ChartYFormatterCompactNumber(value, attribute.unit)} - /> - )} -
-
- ) - })} - + ) } diff --git a/studio/components/interfaces/BillingV2/Usage/Infrastructure.tsx b/studio/components/interfaces/BillingV2/Usage/Infrastructure.tsx index 70477e64f4b..9be2b2ffb20 100644 --- a/studio/components/interfaces/BillingV2/Usage/Infrastructure.tsx +++ b/studio/components/interfaces/BillingV2/Usage/Infrastructure.tsx @@ -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) => { ) : null}
-

What is Disk IO Bandwidth?

+

Disk IO Bandwidth

+

- 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.

+ + {currentComputeInstanceSpecs.maxBandwidth === + currentComputeInstanceSpecs.baseBandwidth ? ( +

+ Your current compute can has a baseline and maximum disk throughput of + {currentComputeInstanceSpecs.maxBandwidth.toLocaleString()} Mbps. +

+ ) : ( +

+ 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. +

+ )}

Overview

@@ -185,7 +201,7 @@ const Infrastructure = ({ projectRef }: InfrastructureProps) => {
{attribute.key === 'disk_io_consumption' ? ( -

IO consumed per day

+

IO consumed per {interval === '1d' ? 'day' : 'hour'}

) : (

Max{' '} @@ -197,6 +213,18 @@ const Infrastructure = ({ projectRef }: InfrastructureProps) => { )}

+ {attribute.key === 'ram_usage' && ( +

+ Your compute instance has {currentComputeInstanceSpecs.memoryGb} GB memory. +

+ )} + + {attribute.key === 'cpu_usage' && ( +

+ Your compute instance has {currentComputeInstanceSpecs.cpuCores} CPU cores. +

+ )} + {attribute.chartDescription.split('\n').map((paragraph, idx) => (

{paragraph} @@ -223,9 +251,7 @@ const Infrastructure = ({ projectRef }: InfrastructureProps) => {

No data

-

- There is no data in the timeframe available -

+

There is no data in period

diff --git a/studio/components/interfaces/BillingV2/Usage/SectionContent.tsx b/studio/components/interfaces/BillingV2/Usage/SectionContent.tsx index 752aae4e2dc..b52b0983efd 100644 --- a/studio/components/interfaces/BillingV2/Usage/SectionContent.tsx +++ b/studio/components/interfaces/BillingV2/Usage/SectionContent.tsx @@ -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 = ({
{links && links.length && (
-

More information

+

More information

{links.map((link) => (
diff --git a/studio/components/interfaces/BillingV2/Usage/SizeAndCounts.tsx b/studio/components/interfaces/BillingV2/Usage/SizeAndCounts.tsx index d2a0abda6c9..2f66c88e095 100644 --- a/studio/components/interfaces/BillingV2/Usage/SizeAndCounts.tsx +++ b/studio/components/interfaces/BillingV2/Usage/SizeAndCounts.tsx @@ -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 ( - <> - - - {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 ( -
- -
-
-
-

{attribute.name} usage

- {!usageBasedBilling && usageRatio >= 1 ? ( -
- -

Exceeded limit

-
- ) : !usageBasedBilling && usageRatio >= USAGE_APPROACHING_THRESHOLD ? ( -
- -

Approaching limit

-
- ) : null} -
- - {!usageBasedBilling && usageRatio >= USAGE_APPROACHING_THRESHOLD && ( - - - - - - )} -
- {usageMeta?.limit > 0 && ( - = 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} - /> - )} -
-
-

- Included in {subscription?.tier.name.toLowerCase()} -

-

- {attribute.unit === 'bytes' - ? formatBytes(usageMeta?.limit ?? 0) - : (usageMeta?.limit ?? 0).toLocaleString()} -

-
-
-

- {attribute.chartPrefix || 'Used '}in period -

-

- {attribute.unit === 'bytes' - ? formatBytes(usageMeta?.usage ?? 0) - : (usageMeta?.usage ?? 0).toLocaleString()} -

-
- {usageMeta?.limit > 0 && ( -
-

Overage this month

-

- {usageExcess < 0 - ? attribute.unit === 'bytes' - ? formatBytes(0) - : 0 - : attribute.unit === 'bytes' - ? formatBytes(usageExcess) - : usageExcess.toLocaleString()} -

-
- )} -
-
- - {attribute.key === 'db_size' && - subscription?.tier.supabase_prod_id !== PRICING_TIER_PRODUCT_IDS.FREE && ( -
-
-

Disk size:

-

{usage?.disk_volume_size_gb} GB

- - Auto-scaling - -
- - - - - -
- )} - -
-

- {attribute.chartPrefix || ''} - {attribute.name} per day -

- {attribute.chartDescription.split('\n').map((paragraph, idx) => ( -

- {paragraph} -

- ))} -
- {chartMeta[attribute.key].isLoading ? ( -
- - - -
- ) : ( - ChartYFormatterCompactNumber(value, attribute.unit)} - /> - )} -
-
- ) - })} - + ) } diff --git a/studio/components/interfaces/BillingV2/Usage/Usage.constants.ts b/studio/components/interfaces/BillingV2/Usage/Usage.constants.tsx similarity index 61% rename from studio/components/interfaces/BillingV2/Usage/Usage.constants.ts rename to studio/components/interfaces/BillingV2/Usage/Usage.constants.tsx index c8b99e2b1d4..c5433c5abbe 100644 --- a/studio/components/interfaces/BillingV2/Usage/Usage.constants.ts +++ b/studio/components/interfaces/BillingV2/Usage/Usage.constants.tsx @@ -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' ? ( +
+
+

Disk size:

+

{usage?.disk_volume_size_gb} GB

+ + Auto-scaling + +
+ + + + + +
+ ) : 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, }, diff --git a/studio/components/interfaces/BillingV2/Usage/Usage.tsx b/studio/components/interfaces/BillingV2/Usage/Usage.tsx index d96a78e9292..01d1fc8f79a 100644 --- a/studio/components/interfaces/BillingV2/Usage/Usage.tsx +++ b/studio/components/interfaces/BillingV2/Usage/Usage.tsx @@ -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(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 ? (

- Project is on {subscription.tier.name} + Project is on {subscription.plan.name} plan

{billingCycleStart.format('DD MMM YYYY')} -{' '} @@ -170,13 +187,28 @@ const Usage = () => {

- +
- +
- +
) diff --git a/studio/components/interfaces/BillingV2/Usage/Usage.utils.ts b/studio/components/interfaces/BillingV2/Usage/Usage.utils.ts index 99f6092748f..11e35b9331d 100644 --- a/studio/components/interfaces/BillingV2/Usage/Usage.utils.ts +++ b/studio/components/interfaces/BillingV2/Usage/Usage.utils.ts @@ -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) diff --git a/studio/components/interfaces/BillingV2/Usage/UsageSection.tsx b/studio/components/interfaces/BillingV2/Usage/UsageSection.tsx new file mode 100644 index 00000000000..db512cd7ad2 --- /dev/null +++ b/studio/components/interfaces/BillingV2/Usage/UsageSection.tsx @@ -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 ( + <> + + + {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 ( +
+ +
+
+
+

{attribute.name} usage

+ {!usageBasedBilling && usageRatio >= 1 ? ( +
+ +

Exceeded limit

+
+ ) : !usageBasedBilling && usageRatio >= USAGE_APPROACHING_THRESHOLD ? ( +
+ +

Approaching limit

+
+ ) : null} +
+ + {!usageBasedBilling && usageRatio >= USAGE_APPROACHING_THRESHOLD && ( + + + + + + )} +
+ {usageMeta?.limit > 0 && ( + = 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} + /> + )} +
+
+

+ Included in {subscription?.plan?.name.toLowerCase()} plan +

+

+ {attribute.unit === 'bytes' + ? formatBytes(usageMeta?.limit ?? 0) + : (usageMeta?.limit ?? 0).toLocaleString()} +

+
+
+

+ {attribute.chartPrefix || 'Used '}in period +

+

+ {attribute.unit === 'bytes' + ? formatBytes(usageMeta?.usage ?? 0) + : (usageMeta?.usage ?? 0).toLocaleString()} +

+
+ {usageMeta?.limit > 0 && ( +
+

Overage in period

+

+ {usageExcess < 0 + ? attribute.unit === 'bytes' + ? formatBytes(0) + : 0 + : attribute.unit === 'bytes' + ? formatBytes(usageExcess) + : usageExcess.toLocaleString()} +

+
+ )} +
+
+ + {attribute.additionalInfo?.(subscription, usage)} + +
+

+ {attribute.chartPrefix || ''} + {attribute.name} per day +

+ {attribute.chartDescription.split('\n').map((paragraph, idx) => ( +

+ {paragraph} +

+ ))} +
+ {chartMeta[attribute.key].isLoading ? ( +
+ + + +
+ ) : chartData.length > 1 && notAllValuesZero ? ( + ChartYFormatterCompactNumber(value, attribute.unit)} + /> + ) : ( + + +
+

No data

+

+ No {notAllValuesZero ? 'data' : 'usage'} in period +

+
+
+
+ )} +
+
+ ) + })} + + ) +} + +export default UsageSection