Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions static/gsApp/components/productTrial/productTrialTag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,21 @@ function ProductTrialTag({trial, type, showTrialEnded = false}: ProductTrialTagP

return (
<Tag icon={<IconFlag />} type={type ?? 'default'}>
{t('Trial Ended')}
{t('Trial ended')}
</Tag>
);
}

if (!trial.isStarted) {
return (
<Tag icon={<IconBusiness gradient />} type={type ?? 'info'}>
{t('Trial Available')}
<Tag icon={<IconBusiness gradient />} type={type ?? 'promotion'}>
{t('Trial available')}
</Tag>
);
}

const daysLeft = -1 * getDaysSinceDate(trial.endDate ?? '');
const tagType = type ?? (daysLeft <= 7 ? 'promotion' : 'highlight');
const tagType = type ?? (daysLeft <= 7 ? 'warning' : 'highlight');

return (
<Tag icon={<IconClock />} type={tagType}>
Expand Down
1 change: 0 additions & 1 deletion static/gsApp/components/subscriptionSettingsLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ function SubscriptionSettingsLayout(props: Props) {
<SettingsSearch />
</Flex>
</StyledSettingsHeader>

<Flex minWidth={0} flex="1" direction="column">
{children}
</Flex>
Expand Down
25 changes: 25 additions & 0 deletions static/gsApp/hooks/useCurrentBillingHistory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {useMemo} from 'react';

import {useApiQuery} from 'sentry/utils/queryClient';
import useOrganization from 'sentry/utils/useOrganization';

import type {BillingHistory} from 'getsentry/types';

export function useCurrentBillingHistory() {
const organization = useOrganization();

const {
data: histories,
isPending,
isError,
} = useApiQuery<BillingHistory[]>([`/customers/${organization.slug}/history/`], {
staleTime: 0, // TODO(billing): Create an endpoint that returns the current history
});

const currentHistory: BillingHistory | null = useMemo(() => {
if (!histories) return null;
return histories.find((history: BillingHistory) => history.isCurrent) ?? null;
}, [histories]);

return {currentHistory, isPending, isError};
}
12 changes: 12 additions & 0 deletions static/gsApp/utils/billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,18 @@ export function partnerPlanEndingModalIsDismissed(
}
}

export 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) + '%';
}

/**
* Returns true if some billing details are set.
*/
Expand Down
5 changes: 5 additions & 0 deletions static/gsApp/views/subscriptionPage/overview.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ describe('Subscription > Overview', () => {
method: 'GET',
});

MockApiClient.addMockResponse({
url: `/customers/${organization.slug}/history/`,
method: 'GET',
});

SubscriptionStore.set(organization.slug, {});
});

Expand Down
35 changes: 24 additions & 11 deletions static/gsApp/views/subscriptionPage/overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import withPromotions from 'getsentry/utils/withPromotions';
import ContactBillingMembers from 'getsentry/views/contactBillingMembers';
import {openOnDemandBudgetEditModal} from 'getsentry/views/onDemandBudgets/editOnDemandButton';
import SubscriptionPageContainer from 'getsentry/views/subscriptionPage/components/subscriptionPageContainer';
import UsageOverview from 'getsentry/views/subscriptionPage/usageOverview';

import openPerformanceQuotaCreditsPromoModal from './promotions/performanceQuotaCreditsPromo';
import openPerformanceReservedTransactionsDiscountModal from './promotions/performanceReservedTransactionsPromo';
Expand All @@ -60,8 +61,8 @@ type Props = {
function Overview({location, subscription, promotionData}: Props) {
const api = useApi();
const organization = useOrganization();
const navigate = useNavigate();
const isNewBillingUI = hasNewBillingUI(organization);
const navigate = useNavigate();

const displayMode = ['cost', 'usage'].includes(location.query.displayMode as string)
? (location.query.displayMode as 'cost' | 'usage')
Expand Down Expand Up @@ -335,14 +336,20 @@ function Overview({location, subscription, promotionData}: Props) {
<RecurringCredits displayType="data" planDetails={planDetails} />
<OnDemandDisabled subscription={subscription} />
<UsageAlert subscription={subscription} usage={usageData} />
<DisplayModeToggle
subscription={subscription}
displayMode={displayMode}
organization={organization}
/>
{renderUsageChart(usageData)}
{renderUsageCards(usageData)}
<OnDemandSettings organization={organization} subscription={subscription} />
{isNewBillingUI ? (
<UsageOverview subscription={subscription} organization={organization} />
) : (
<Fragment>
<DisplayModeToggle
subscription={subscription}
displayMode={displayMode}
organization={organization}
/>
{renderUsageChart(usageData)}
{renderUsageCards(usageData)}
<OnDemandSettings organization={organization} subscription={subscription} />
</Fragment>
)}
<TrialEnded subscription={subscription} />
</Fragment>
);
Expand All @@ -353,8 +360,14 @@ function Overview({location, subscription, promotionData}: Props) {
<Fragment>
<OnDemandDisabled subscription={subscription} />
<UsageAlert subscription={subscription} usage={usageData} />
{renderUsageChart(usageData)}
{renderUsageCards(usageData)}
{isNewBillingUI ? (
<UsageOverview subscription={subscription} organization={organization} />
) : (
<Fragment>
{renderUsageChart(usageData)}
{renderUsageCards(usageData)}
</Fragment>
)}
<TrialEnded subscription={subscription} />
</Fragment>
);
Expand Down
119 changes: 119 additions & 0 deletions static/gsApp/views/subscriptionPage/usageOverview.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import moment from 'moment-timezone';
import {OrganizationFixture} from 'sentry-fixture/organization';

import {BillingHistoryFixture} from 'getsentry-test/fixtures/billingHistory';
import {SubscriptionFixture} from 'getsentry-test/fixtures/subscription';
import {render, screen} from 'sentry-test/reactTestingLibrary';
import {resetMockDate, setMockDate} from 'sentry-test/utils';

import {DataCategory} from 'sentry/types/core';

import SubscriptionStore from 'getsentry/stores/subscriptionStore';
import UsageOverview from 'getsentry/views/subscriptionPage/usageOverview';

describe('UsageOverview', () => {
const organization = OrganizationFixture();
const subscription = SubscriptionFixture({organization, plan: 'am3_business'});

beforeEach(() => {
setMockDate(new Date('2021-05-07'));
MockApiClient.clearMockResponses();
organization.access = ['org:billing'];
SubscriptionStore.set(organization.slug, subscription);
MockApiClient.addMockResponse({
url: `/customers/${organization.slug}/history/`,
method: 'GET',
body: BillingHistoryFixture(),
});
});

afterEach(() => {
resetMockDate();
});

it('renders columns and buttons for billing users', () => {
render(<UsageOverview subscription={subscription} organization={organization} />);
expect(screen.getByRole('columnheader', {name: 'Product'})).toBeInTheDocument();
expect(screen.getByRole('columnheader', {name: 'Current usage'})).toBeInTheDocument();
expect(
screen.getByRole('columnheader', {name: 'Reserved usage'})
).toBeInTheDocument();
expect(
screen.getByRole('columnheader', {name: 'Reserved spend'})
).toBeInTheDocument();
expect(
screen.getByRole('columnheader', {name: 'Pay-as-you-go spend'})
).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'View usage history'})).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Download as CSV'})).toBeInTheDocument();
});

it('renders columns for non-billing users', () => {
organization.access = [];
render(<UsageOverview subscription={subscription} organization={organization} />);
expect(screen.getByRole('columnheader', {name: 'Product'})).toBeInTheDocument();
expect(screen.getByRole('columnheader', {name: 'Current usage'})).toBeInTheDocument();
expect(
screen.getByRole('columnheader', {name: 'Reserved usage'})
).toBeInTheDocument();
expect(screen.queryByText('Reserved spend')).not.toBeInTheDocument();
expect(screen.queryByText('Pay-as-you-go spend')).not.toBeInTheDocument();
expect(
screen.queryByRole('button', {name: 'View usage history'})
).not.toBeInTheDocument();
expect(
screen.queryByRole('button', {name: 'Download as CSV'})
).not.toBeInTheDocument();
});

it('renders table based on subscription state', () => {
subscription.onDemandMaxSpend = 100_00;
subscription.productTrials = [
{
category: DataCategory.PROFILE_DURATION,
isStarted: false,
reasonCode: 1001,
startDate: undefined,
endDate: moment().utc().add(20, 'years').format(),
},
{
category: DataCategory.REPLAYS,
isStarted: true,
reasonCode: 1001,
startDate: moment().utc().subtract(10, 'days').format(),
endDate: moment().utc().add(20, 'days').format(),
},
];
subscription.categories.errors = {
...subscription.categories.errors!,
free: 1000,
usage: 6000,
onDemandSpendUsed: 10_00,
};
subscription.categories.spans = {
...subscription.categories.spans!,
reserved: 20_000_000,
};

render(<UsageOverview subscription={subscription} organization={organization} />);

// Continuous profile hours product trial available
expect(
screen.getByRole('button', {
name: 'Start 14 day free Continuous Profile Hours trial',
})
).toBeInTheDocument();
expect(screen.getByText('Trial available')).toBeInTheDocument();

// Replays product trial active
expect(screen.getByText('20 days left')).toBeInTheDocument();

// Errors usage and gifted units
expect(screen.getByText('6,000 / 51,000')).toBeInTheDocument();
expect(screen.getByText('$10.00')).toBeInTheDocument();

// Reserved spans above platform volume
expect(screen.getByText('0 / 20,000,000')).toBeInTheDocument();
expect(screen.getByText('$32.00')).toBeInTheDocument();
});
});
Loading
Loading