123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830 |
- import {Fragment, useState} from 'react';
- import styled from '@emotion/styled';
- import colorFn from 'color';
- import {Button} from 'sentry/components/button';
- import Card from 'sentry/components/card';
- import {CHART_PALETTE} from 'sentry/constants/chartPalette';
- import {IconChevron} from 'sentry/icons';
- import {t, tct} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import {DataCategory} from 'sentry/types/core';
- import type {Organization} from 'sentry/types/organization';
- import getDaysSinceDate from 'sentry/utils/getDaysSinceDate';
- import {formatPercentage} from 'sentry/utils/number/formatPercentage';
- import ProductTrialTag from 'getsentry/components/productTrial/productTrialTag';
- import StartTrialButton from 'getsentry/components/startTrialButton';
- import {GIGABYTE, RESERVED_BUDGET_QUOTA, UNLIMITED} from 'getsentry/constants';
- import {
- type BillingMetricHistory,
- type BillingStatTotal,
- type EventBucket,
- PlanTier,
- type ProductTrial,
- type Subscription,
- } from 'getsentry/types';
- import {
- formatReservedWithUnits,
- formatUsageWithUnits,
- getActiveProductTrial,
- getPotentialProductTrial,
- isUnlimitedReserved,
- MILLISECONDS_IN_HOUR,
- } from 'getsentry/utils/billing';
- import {getPlanCategoryName} from 'getsentry/utils/dataCategory';
- import formatCurrency from 'getsentry/utils/formatCurrency';
- import {roundUpToNearestDollar} from 'getsentry/utils/roundUpToNearestDollar';
- import titleCase from 'getsentry/utils/titleCase';
- import {getBucket} from 'getsentry/views/amCheckout/utils';
- import {
- getOnDemandBudget,
- hasOnDemandBudgetsFeature,
- parseOnDemandBudgetsFromSubscription,
- } from 'getsentry/views/onDemandBudgets/utils';
- import UsageTotalsTable from 'getsentry/views/subscriptionPage/usageTotalsTable';
- import {
- calculateCategorySpend,
- calculateTotalSpend,
- } from 'getsentry/views/subscriptionPage/utils';
- const EMPTY_STAT_TOTAL = {
- accepted: 0,
- dropped: 0,
- droppedOther: 0,
- droppedOverQuota: 0,
- droppedSpikeProtection: 0,
- filtered: 0,
- projected: 0,
- };
- const COLORS = {
- reserved: CHART_PALETTE[5]![0]!,
- ondemand: CHART_PALETTE[5]![1]!,
- } as const;
- function getPercentage(quantity: number, total: number | null) {
- if (typeof total === 'number' && total > 0) {
- return (Math.min(quantity, total) / total) * 100;
- }
- return 0;
- }
- export function displayPercentage(quantity: number, total: number | null) {
- const percentage = getPercentage(quantity, total);
- return percentage.toFixed(0) + '%';
- }
- type UsageProps = {
- /**
- * The data category to display
- */
- category: string;
- displayMode: 'usage' | 'cost';
- organization: Organization;
- subscription: Subscription;
- /**
- * Do not allow the table to be expansded
- */
- disableTable?: boolean;
- /**
- * Event breakdown totals
- */
- eventTotals?: {[key: string]: BillingStatTotal};
- /**
- * Gifted budget for the current billing period.
- */
- freeBudget?: number | null;
- /**
- * Gifted events for the current billing period.
- */
- freeUnits?: number;
- /**
- * The prepaid budget (reserved + gifted) if any
- */
- prepaidBudget?: number | null;
- /**
- * Total events allowed for the current usage period including gifted
- */
- prepaidUnits?: number;
- /**
- * The reserved budget if any
- */
- reservedBudget?: number | null;
- /**
- * The reserved spend if any
- */
- reservedSpend?: number | null;
- /**
- * The reserved amount or null if the account doesn't have this category.
- */
- reservedUnits?: number | null;
- /**
- * Show event breakdown
- */
- showEventBreakdown?: boolean;
- /**
- * If soft cap is enabled, the type of soft cap in use: true forward or on-demand
- */
- softCapType?: 'ON_DEMAND' | 'TRUE_FORWARD' | null;
- /**
- * Usage totals.
- */
- totals?: BillingStatTotal;
- /**
- * Whether this category has True Forward
- */
- trueForward?: boolean;
- };
- type State = {expanded: boolean; trialButtonBusy: boolean};
- /**
- * Calculates usage metrics for a subscription category's prepaid (reserved) events.
- *
- * @param category - The data category to calculate usage for (e.g. 'errors', 'transactions')
- * @param subscription - The subscription object containing plan and usage details
- * @param totals - Object containing the accepted event count for this category
- * @param prepaid - The prepaid/reserved event limit (volume-based reserved) or commited spend (budget-based reserved) for this category
- * @param reservedCpe - The reserved cost-per-event for this category (for reserved budget categories), in cents
- * @param reservedSpend - The reserved spend for this category (for reserved budget categories). If provided, calculations with `totals` and `reservedCpe` are overriden to use the number provided for `prepaidSpend`
- *
- * @returns Object containing:
- * - onDemandUsage: Number of events that exceeded the prepaid limit and went to on-demand
- * - prepaidPercentUsed: Percentage of prepaid limit used (0-100)
- * - prepaidPrice: Monthly cost of the prepaid events (reserved budget if it is a reserved budget category)
- * - prepaidSpend: Cost of prepaid events used so far this period
- * - prepaidUsage: Number of events used within prepaid limit
- */
- export function calculateCategoryPrepaidUsage(
- category: string,
- subscription: Subscription,
- totals: Pick<BillingStatTotal, 'accepted'>,
- prepaid: number,
- reservedCpe?: number | null,
- reservedSpend?: number | null
- ): {
- onDemandUsage: number;
- prepaidPercentUsed: number;
- prepaidPrice: number;
- /**
- * Total category spend this period
- */
- prepaidSpend: number;
- prepaidUsage: number;
- } {
- // Calculate the prepaid total
- let prepaidTotal: any;
- if (isUnlimitedReserved(prepaid)) {
- prepaidTotal = prepaid;
- } else {
- // Convert prepaid limits to the appropriate unit based on category
- switch (category) {
- case DataCategory.ATTACHMENTS:
- prepaidTotal = prepaid * GIGABYTE;
- break;
- case DataCategory.PROFILE_DURATION:
- prepaidTotal = prepaid * MILLISECONDS_IN_HOUR;
- break;
- default:
- prepaidTotal = prepaid;
- }
- }
- const hasReservedBudget = reservedCpe || typeof reservedSpend === 'number'; // reservedSpend can be 0
- const prepaidUsed = hasReservedBudget
- ? (reservedSpend ?? totals.accepted * (reservedCpe ?? 0))
- : totals.accepted;
- const prepaidPercentUsed = getPercentage(prepaidUsed, prepaidTotal);
- // Calculate the prepaid price
- // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
- const categoryInfo: BillingMetricHistory = subscription.categories[category];
- // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
- const slots: EventBucket[] = subscription.planDetails.planCategories[category];
- // If the category billing info is not in the subscription, return 0 for all values
- // This seems to happen sometimes on partner accounts
- if (!categoryInfo || !slots) {
- return {
- prepaidPrice: 0,
- prepaidSpend: 0,
- prepaidPercentUsed: 0,
- onDemandUsage: 0,
- prepaidUsage: 0,
- };
- }
- // Get the price bucket for the reserved event amount
- const prepaidPriceBucket = getBucket({events: categoryInfo.reserved!, buckets: slots});
- // Convert annual prices to monthly if needed
- const isMonthly = subscription.planDetails.billingInterval === 'monthly';
- // This will be 0 when they are using the included amount
- const prepaidPrice = hasReservedBudget
- ? prepaid
- : (prepaidPriceBucket.price ?? 0) / (isMonthly ? 1 : 12);
- // Calculate spend based on percentage used
- const prepaidSpend = (prepaidPercentUsed / 100) * prepaidPrice;
- // Round the usage width to avoid half pixel artifacts
- const prepaidPercentUsedRounded = Math.round(prepaidPercentUsed);
- // Calculate on-demand usage if we've exceeded prepaid limit
- // No on-demand usage for unlimited reserved
- const onDemandUsage =
- (prepaidUsed > prepaidTotal && !isUnlimitedReserved(prepaidTotal)) ||
- (hasReservedBudget && prepaidUsed >= prepaidTotal)
- ? categoryInfo.onDemandQuantity
- : 0;
- const prepaidUsage = totals.accepted - onDemandUsage;
- return {
- prepaidPrice,
- prepaidSpend,
- prepaidPercentUsed: prepaidPercentUsedRounded,
- onDemandUsage,
- prepaidUsage,
- };
- }
- export function calculateCategoryOnDemandUsage(
- category: string,
- subscription: Subscription
- ): {
- /**
- * The maximum amount of on demand spend allowed for this category
- * This can be shared across all categories or specific to this category.
- * Other categories may have spent some of this budget making less avilable for this category.
- */
- onDemandCategoryMax: number;
- onDemandCategorySpend: number;
- /**
- * Will be the total on demand spend available for all categories if shared
- * or the total available for this category if not shared.
- */
- onDemandTotalAvailable: number;
- ondemandPercentUsed: number;
- } {
- const onDemandBudgets = parseOnDemandBudgetsFromSubscription(subscription);
- const isSharedOnDemand = 'sharedMaxBudget' in onDemandBudgets;
- const onDemandTotalAvailable = isSharedOnDemand
- ? onDemandBudgets.sharedMaxBudget
- : getOnDemandBudget(onDemandBudgets, category as DataCategory);
- const {onDemandTotalSpent} = calculateTotalSpend(subscription);
- const {onDemandSpent: onDemandCategorySpend} = calculateCategorySpend(
- subscription,
- category
- );
- const onDemandCategoryMax = isSharedOnDemand
- ? // Subtract other category spend from shared on demand budget
- onDemandTotalAvailable - onDemandTotalSpent + onDemandCategorySpend
- : onDemandTotalAvailable;
- // Round the usage width to avoid half pixel artifacts
- const ondemandPercentUsed = Math.round(
- getPercentage(onDemandCategorySpend, onDemandCategoryMax)
- );
- return {
- onDemandTotalAvailable,
- onDemandCategorySpend,
- onDemandCategoryMax,
- ondemandPercentUsed,
- };
- }
- function ReservedUsage({
- prepaidUsage,
- reserved,
- category,
- productTrial,
- }: {
- category: string;
- prepaidUsage: number;
- productTrial: ProductTrial | null;
- reserved: number | null;
- }) {
- const reservedOptions = {
- isAbbreviated: category !== DataCategory.ATTACHMENTS,
- };
- return (
- <Fragment>
- {formatUsageWithUnits(prepaidUsage, category, {
- isAbbreviated: true,
- })}{' '}
- of{' '}
- {productTrial?.isStarted && getDaysSinceDate(productTrial.endDate ?? '') <= 0
- ? UNLIMITED
- : formatReservedWithUnits(reserved, category, reservedOptions)}
- </Fragment>
- );
- }
- function UsageTotals({
- category,
- subscription,
- organization,
- freeUnits = 0,
- prepaidUnits = 0,
- reservedUnits = null,
- freeBudget = null,
- prepaidBudget = null,
- reservedBudget = null,
- reservedSpend = null,
- softCapType = null,
- totals = EMPTY_STAT_TOTAL,
- eventTotals = {},
- trueForward = false,
- showEventBreakdown = false,
- disableTable,
- displayMode,
- }: UsageProps) {
- const [state, setState] = useState<State>({expanded: false, trialButtonBusy: false});
- const usageOptions = {useUnitScaling: true};
- const reservedOptions = {
- isAbbreviated: category !== DataCategory.ATTACHMENTS,
- };
- const hasReservedBudget = reservedUnits === RESERVED_BUDGET_QUOTA;
- const free = hasReservedBudget ? freeBudget : freeUnits;
- const reserved = hasReservedBudget ? reservedBudget : reservedUnits;
- const prepaid = hasReservedBudget ? (prepaidBudget ?? 0) : prepaidUnits;
- const displayGifts = (free || freeBudget) && !isUnlimitedReserved(reservedUnits);
- const reservedTestId = displayGifts ? `gifted-${category}` : `reserved-${category}`;
- const hasOnDemand =
- hasOnDemandBudgetsFeature(organization, subscription) ||
- subscription.planTier === PlanTier.AM3;
- const onDemandBudgets = parseOnDemandBudgetsFromSubscription(subscription);
- const totalMaxOndemandBudget =
- 'sharedMaxBudget' in onDemandBudgets
- ? onDemandBudgets.sharedMaxBudget
- : getOnDemandBudget(onDemandBudgets, category as DataCategory);
- const {onDemandSpent: categoryOnDemandSpent, onDemandUnitPrice} =
- calculateCategorySpend(subscription, category);
- function getReservedInfo() {
- let reservedInfo = tct('[reserved] Reserved', {
- reserved: formatReservedWithUnits(
- reserved,
- category,
- reservedOptions,
- hasReservedBudget
- ),
- });
- if (softCapType) {
- const softCapName = titleCase(softCapType.replace(/_/g, ' '));
- reservedInfo = tct('[reservedInfo] ([softCapName])', {reservedInfo, softCapName});
- }
- // Fallback if softCapType was not set but True Forward is
- else if (trueForward) {
- reservedInfo = tct('[reservedInfo] (True Forward)', {reservedInfo});
- }
- if (displayGifts) {
- reservedInfo = tct('[reservedInfo] + [giftedAmount] Gifted', {
- reservedInfo,
- giftedAmount: formatReservedWithUnits(
- free,
- category,
- reservedOptions,
- hasReservedBudget
- ),
- });
- }
- return reservedInfo;
- }
- const productTrial =
- getActiveProductTrial(subscription.productTrials ?? null, category as DataCategory) ??
- getPotentialProductTrial(
- subscription.productTrials ?? null,
- category as DataCategory
- );
- const {
- ondemandPercentUsed,
- onDemandTotalAvailable,
- onDemandCategorySpend,
- onDemandCategoryMax,
- } = calculateCategoryOnDemandUsage(category, subscription);
- const unusedOnDemandWidth = 100 - ondemandPercentUsed;
- const {prepaidPrice, prepaidPercentUsed, prepaidUsage, onDemandUsage} =
- calculateCategoryPrepaidUsage(
- category,
- subscription,
- totals,
- prepaid,
- undefined,
- reservedSpend
- );
- const unusedPrepaidWidth =
- reserved !== 0 || subscription.isTrial ? 100 - prepaidPercentUsed : 0;
- const totalCategorySpend =
- (hasReservedBudget ? (reservedSpend ?? 0) : prepaidPrice) + categoryOnDemandSpent;
- // Shared on demand spend is gone, another category has spent all of it
- // It is confusing to show on demand spend when the category did not spend any and the budget is gone
- const onDemandIsGoneAndCategorySpentNone =
- 'sharedMaxBudget' in onDemandBudgets &&
- categoryOnDemandSpent === 0 &&
- onDemandCategoryMax === 0;
- // Don't show on demand when:
- // - There is none left to spend and this category spent 0
- // - There is no on demand budget for this category
- // - There is no on demand budget at all
- const showOnDemand =
- !onDemandIsGoneAndCategorySpentNone && hasOnDemand && totalMaxOndemandBudget !== 0;
- const isDisplayingSpend = displayMode === 'cost' || hasReservedBudget; // always display as spend for reserved budgets
- // Calculate the width of the reserved bar relative to on demand
- let reservedMaxWidth = showOnDemand ? (reserved !== 0 ? 50 : 0) : 100;
- if (showOnDemand && reserved && onDemandUnitPrice) {
- const onDemandTotalUnitsAvailable = onDemandCategoryMax / onDemandUnitPrice;
- reservedMaxWidth =
- showOnDemand && reserved
- ? (reserved / (reserved + onDemandTotalUnitsAvailable)) * 100
- : 100;
- }
- function getTitle(): React.ReactNode {
- if (productTrial?.isStarted) {
- return t('trial usage this period');
- }
- if (isDisplayingSpend) {
- return t('spend this period');
- }
- return t('usage this period');
- }
- const formattedUnitsUsed = formatUsageWithUnits(
- totals.accepted,
- category,
- usageOptions
- );
- return (
- <SubscriptionCard>
- <CardBody>
- <UsageProgress>
- <BaseRow>
- <div>
- <UsageSummaryTitle>
- {getPlanCategoryName({
- plan: subscription.planDetails,
- category,
- hadCustomDynamicSampling: subscription.hadCustomDynamicSampling,
- })}{' '}
- {getTitle()}
- {productTrial && (
- <MarginSpan>
- <ProductTrialTag trial={productTrial} />
- </MarginSpan>
- )}
- </UsageSummaryTitle>
- <SubText data-test-id={reservedTestId}>
- {productTrial?.isStarted &&
- getDaysSinceDate(productTrial.endDate ?? '') <= 0
- ? UNLIMITED
- : getReservedInfo()}
- </SubText>
- </div>
- <AcceptedSummary>
- {productTrial && !productTrial.isStarted && (
- <MarginSpan>
- <StartTrialButton
- organization={organization}
- source="usage-product-trials"
- requestData={{
- productTrial: {
- category,
- reasonCode: productTrial.reasonCode,
- },
- }}
- aria-label={t('Start trial')}
- priority="primary"
- handleClick={() => {
- setState({...state, trialButtonBusy: true});
- }}
- onTrialStarted={() => {
- setState({...state, trialButtonBusy: true});
- }}
- onTrialFailed={() => {
- setState({...state, trialButtonBusy: false});
- }}
- busy={state.trialButtonBusy}
- disabled={state.trialButtonBusy}
- />
- </MarginSpan>
- )}
- {!disableTable && (
- <Button
- data-test-id="expand-usage-totals"
- size="sm"
- onClick={() => setState({...state, expanded: !state.expanded})}
- icon={<IconChevron direction={state.expanded ? 'up' : 'down'} />}
- aria-label={t('Expand usage totals')}
- />
- )}
- </AcceptedSummary>
- </BaseRow>
- <PlanUseBarContainer>
- <PlanUseBarGroup style={{width: `${reservedMaxWidth}%`}}>
- {prepaidPercentUsed >= 1 && (
- <PlanUseBar
- style={{
- width: `${prepaidPercentUsed}%`,
- backgroundColor: COLORS.reserved,
- }}
- />
- )}
- {unusedPrepaidWidth >= 1 && (
- <PlanUseBar
- style={{
- width: `${unusedPrepaidWidth}%`,
- backgroundColor: colorFn(COLORS.reserved).fade(0.5).string(),
- }}
- />
- )}
- </PlanUseBarGroup>
- {showOnDemand && (
- <PlanUseBarGroup style={{width: `${100 - reservedMaxWidth}%`}}>
- {ondemandPercentUsed >= 1 && (
- <PlanUseBar
- style={{
- width: `${ondemandPercentUsed}%`,
- backgroundColor: COLORS.ondemand,
- }}
- />
- )}
- {unusedOnDemandWidth >= 1 && (
- <PlanUseBar
- style={{
- width: `${unusedOnDemandWidth}%`,
- backgroundColor: colorFn(COLORS.ondemand).fade(0.5).string(),
- }}
- />
- )}
- </PlanUseBarGroup>
- )}
- </PlanUseBarContainer>
- <LegendFooterWrapper>
- <LegendPriceWrapper>
- <LegendContainer>
- <LegendDot style={{backgroundColor: COLORS.reserved}} />
- {isDisplayingSpend ? (
- prepaidPrice === 0 ? (
- // No reserved price, included in plan
- <div>
- <LegendTitle>{t('Included in Subscription')}</LegendTitle>
- <LegendPriceSubText>
- <ReservedUsage
- prepaidUsage={prepaidUsage}
- reserved={reserved}
- category={category}
- productTrial={productTrial}
- />
- </LegendPriceSubText>
- </div>
- ) : (
- <div>
- <LegendTitle>{t('Included in Subscription')}</LegendTitle>
- <LegendPrice>
- {formatPercentage(prepaidPercentUsed / 100)} of{' '}
- {prepaidPrice === 0
- ? reserved
- : formatCurrency(roundUpToNearestDollar(prepaidPrice))}
- </LegendPrice>
- </div>
- )
- ) : (
- <div>
- <LegendTitle>{t('Included in Subscription')}</LegendTitle>
- <LegendPrice>
- <ReservedUsage
- prepaidUsage={prepaidUsage}
- reserved={reserved}
- category={category}
- productTrial={productTrial}
- />
- </LegendPrice>
- </div>
- )}
- </LegendContainer>
- {showOnDemand && (
- <LegendContainer>
- <LegendDot style={{backgroundColor: COLORS.ondemand}} />
- {isDisplayingSpend ? (
- <div>
- <LegendTitle>
- {subscription.planTier === PlanTier.AM3
- ? t('Pay-as-you-go')
- : t('On-Demand')}
- </LegendTitle>
- <LegendPrice>
- {formatCurrency(onDemandCategorySpend)} of{' '}
- {formatCurrency(onDemandCategoryMax)}{' '}
- {/* Shared on demand was used in another category, display the max */}
- {onDemandTotalAvailable !== onDemandCategoryMax && (
- <Fragment>
- ({formatCurrency(onDemandTotalAvailable)} max)
- </Fragment>
- )}
- </LegendPrice>
- </div>
- ) : (
- <div>
- <LegendTitle>
- {subscription.planTier === PlanTier.AM3
- ? t('Pay-as-you-go')
- : t('On-Demand')}
- </LegendTitle>
- <LegendPrice>
- {formatUsageWithUnits(onDemandUsage, category, usageOptions)}
- </LegendPrice>
- </div>
- )}
- </LegendContainer>
- )}
- </LegendPriceWrapper>
- {isDisplayingSpend ? (
- <TotalSpendWrapper>
- <UsageSummaryTitle>
- {formatCurrency(totalCategorySpend)}
- </UsageSummaryTitle>
- <TotalSpendLabel>
- {prepaidPrice !== 0 && (
- <Fragment>
- {formatCurrency(prepaidPrice)} {t('Included in Subscription')}
- </Fragment>
- )}
- {prepaidPrice !== 0 && showOnDemand && <Fragment> + </Fragment>}
- {showOnDemand && (
- <Fragment>
- {formatCurrency(onDemandCategorySpend)}{' '}
- {subscription.planTier === PlanTier.AM3
- ? t('Pay-as-you-go')
- : t('On-Demand')}
- </Fragment>
- )}
- </TotalSpendLabel>
- </TotalSpendWrapper>
- ) : (
- <TotalSpendWrapper>
- <UsageSummaryTitle>{formattedUnitsUsed}</UsageSummaryTitle>
- <TotalSpendLabel>{t('Total Usage')}</TotalSpendLabel>
- </TotalSpendWrapper>
- )}
- </LegendFooterWrapper>
- </UsageProgress>
- </CardBody>
- {state.expanded && !disableTable && (
- <Fragment>
- <UsageTotalsTable
- category={category}
- totals={totals}
- subscription={subscription}
- />
- {showEventBreakdown &&
- Object.entries(eventTotals).map(([key, eventTotal]) => {
- return (
- <UsageTotalsTable
- isEventBreakdown
- key={key}
- category={key}
- totals={eventTotal}
- subscription={subscription}
- />
- );
- })}
- </Fragment>
- )}
- </SubscriptionCard>
- );
- }
- export default UsageTotals;
- const SubscriptionCard = styled(Card)`
- padding: ${space(2)};
- `;
- const CardBody = styled('div')`
- display: grid;
- align-items: center;
- gap: ${space(2)};
- `;
- const UsageSummaryTitle = styled('h4')`
- font-size: ${p => p.theme.fontSizeLarge};
- margin-bottom: 0px;
- font-weight: 400;
- @media (min-width: ${p => p.theme.breakpoints.small}) {
- font-size: ${p => p.theme.fontSizeExtraLarge};
- }
- `;
- const UsageProgress = styled('div')`
- display: grid;
- grid-auto-rows: auto;
- gap: ${space(1)};
- `;
- const BaseRow = styled('div')`
- display: grid;
- grid-template-columns: repeat(2, auto);
- justify-content: space-between;
- align-items: center;
- gap: ${space(1)};
- `;
- const SubText = styled('span')`
- color: ${p => p.theme.chartLabel};
- font-size: ${p => p.theme.fontSizeMedium};
- `;
- const AcceptedSummary = styled('div')`
- display: flex;
- align-items: center;
- gap: ${space(1)};
- `;
- const MarginSpan = styled('span')`
- margin-left: ${space(0.5)};
- margin-right: ${space(1)};
- `;
- const TotalSpendWrapper = styled('div')`
- text-align: right;
- `;
- const TotalSpendLabel = styled('div')`
- color: ${p => p.theme.subText};
- `;
- const LegendFooterWrapper = styled('div')`
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: ${space(1)};
- `;
- const LegendPriceWrapper = styled('div')`
- display: flex;
- gap: ${space(1)};
- `;
- const LegendDot = styled('div')`
- border-radius: 100%;
- width: 10px;
- height: 10px;
- `;
- const LegendContainer = styled('div')`
- display: grid;
- grid-template-columns: min-content 1fr;
- gap: ${space(1)};
- align-items: baseline;
- `;
- const LegendTitle = styled('div')`
- font-weight: 700;
- font-size: ${p => p.theme.fontSizeSmall};
- `;
- const LegendPrice = styled('div')`
- font-size: ${p => p.theme.fontSizeSmall};
- `;
- const LegendPriceSubText = styled(LegendPrice)`
- color: ${p => p.theme.subText};
- `;
- const PlanUseBarContainer = styled('div')`
- display: flex;
- height: 16px;
- width: 100%;
- overflow: hidden;
- gap: 2px;
- `;
- const PlanUseBarGroup = styled('div')`
- display: flex;
- gap: 2px;
- `;
- const PlanUseBar = styled('div')`
- height: 100%;
- `;
|