Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions static/gsApp/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export const BILLED_DATA_CATEGORY_INFO = {
'Transactions are sent when your service receives a request and sends a response.'
),
hasPerCategory: true,
shortenedUnitName: 'unit',
},
[DataCategoryExact.ATTACHMENT]: {
...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.ATTACHMENT],
Expand Down
7 changes: 7 additions & 0 deletions static/gsApp/utils/billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -375,13 +375,20 @@ export function hasJustStartedPlanTrial(subscription: Subscription) {
export const displayBudgetName = (
plan?: Plan | null,
options: {
abbreviated?: boolean;
pluralOndemand?: boolean;
title?: boolean;
withBudget?: boolean;
} = {}
) => {
const budgetTerm = plan?.budgetTerm ?? 'pay-as-you-go';
const text = `${budgetTerm}${options.withBudget ? ' budget' : ''}`;
if (options.abbreviated) {
if (budgetTerm === 'pay-as-you-go') {
return 'PAYG';
}
return 'OD';
}
if (options.title) {
if (budgetTerm === 'on-demand') {
if (options.withBudget) {
Expand Down
15 changes: 15 additions & 0 deletions static/gsApp/views/onDemandBudgets/editOnDemandButton.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
import {css} from '@emotion/react';

import {openModal} from 'sentry/actionCreators/modal';
import {Button} from 'sentry/components/core/button';
import {IconEdit} from 'sentry/icons';
import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
import type {Theme} from 'sentry/utils/theme';

import type {Subscription} from 'getsentry/types';
import {hasNewBillingUI} from 'getsentry/utils/billing';
import OnDemandBudgetEditModal from 'getsentry/views/onDemandBudgets/onDemandBudgetEditModal';

interface EditOnDemandButtonProps {
organization: Organization;
subscription: Subscription;
theme?: Theme;
}

export function openOnDemandBudgetEditModal({
organization,
subscription,
theme,
}: EditOnDemandButtonProps) {
const isNewBillingUI = hasNewBillingUI(organization);

openModal(
modalProps => (
<OnDemandBudgetEditModal
Expand All @@ -26,6 +34,7 @@ export function openOnDemandBudgetEditModal({
),
{
closeEvents: 'escape-key',
modalCss: theme && isNewBillingUI ? modalCss(theme) : undefined,
}
);
}
Expand All @@ -44,3 +53,9 @@ export function EditOnDemandButton(props: EditOnDemandButtonProps) {
</Button>
);
}

const modalCss = (theme: Theme) => css`
@media (min-width: ${theme.breakpoints.md}) {
width: 1000px;
}
`;
221 changes: 133 additions & 88 deletions static/gsApp/views/onDemandBudgets/onDemandBudgetEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ import styled from '@emotion/styled';
import {Alert} from 'sentry/components/core/alert';
import {Tag} from 'sentry/components/core/badge/tag';
import {Input} from 'sentry/components/core/input';
import {Container} from 'sentry/components/core/layout';
import {Radio} from 'sentry/components/core/radio';
import {Heading} from 'sentry/components/core/text';
import {Tooltip} from 'sentry/components/core/tooltip';
import PanelBody from 'sentry/components/panels/panelBody';
import PanelItem from 'sentry/components/panels/panelItem';
import {DATA_CATEGORY_INFO} from 'sentry/constants';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import {DataCategoryExact} from 'sentry/types/core';
import type {DataCategory} from 'sentry/types/core';
import type {Organization} from 'sentry/types/organization';
import oxfordizeArray from 'sentry/utils/oxfordizeArray';
import {toTitleCase} from 'sentry/utils/string/toTitleCase';
Expand All @@ -21,8 +24,14 @@ import TextBlock from 'sentry/views/settings/components/text/textBlock';
import {CronsOnDemandStepWarning} from 'getsentry/components/cronsOnDemandStepWarning';
import type {OnDemandBudgets, Plan, Subscription} from 'getsentry/types';
import {OnDemandBudgetMode, PlanTier} from 'getsentry/types';
import {displayBudgetName, getOnDemandCategories} from 'getsentry/utils/billing';
import {
displayBudgetName,
getOnDemandCategories,
hasNewBillingUI,
} from 'getsentry/utils/billing';
import {getPlanCategoryName, listDisplayNames} from 'getsentry/utils/dataCategory';
import {parseOnDemandBudgetsFromSubscription} from 'getsentry/views/onDemandBudgets/utils';
import EmbeddedSpendLimitSettings from 'getsentry/views/spendLimits/embeddedSettings';

function coerceValue(value: number): number {
return value / 100;
Expand Down Expand Up @@ -224,8 +233,11 @@ class OnDemandBudgetEdit extends Component<Props> {
setBudgetMode,
activePlan,
subscription,
organization,
} = this.props;

const isNewBillingUI = hasNewBillingUI(organization);

const selectedBudgetMode = onDemandBudget.budgetMode;
const perCategoryCategories = listDisplayNames({
plan: activePlan,
Expand All @@ -235,98 +247,131 @@ class OnDemandBudgetEdit extends Component<Props> {
}),
});

if (subscription.planDetails.budgetTerm === 'pay-as-you-go') {
if (!isNewBillingUI) {
if (subscription.planDetails.budgetTerm === 'pay-as-you-go') {
return (
<PaygBody>
<BudgetDetails>
<Description>
{t(
"This budget ensures continued monitoring after you've used up your reserved event volume. We'll only charge you for actual usage, so this is your maximum charge for overage.%s",
subscription.isSelfServePartner
? ` This will be part of your ${subscription.partner?.partnership.displayName} bill.`
: ''
)}
</Description>
{this.renderInputFields(OnDemandBudgetMode.SHARED)}
</BudgetDetails>
</PaygBody>
);
}

return (
<PaygBody>
<BudgetDetails>
<Description>
{t(
"This budget ensures continued monitoring after you've used up your reserved event volume. We'll only charge you for actual usage, so this is your maximum charge for overage.%s",
subscription.isSelfServePartner
? ` This will be part of your ${subscription.partner?.partnership.displayName} bill.`
: ''
)}
</Description>
{this.renderInputFields(OnDemandBudgetMode.SHARED)}
</BudgetDetails>
</PaygBody>
<PanelBody>
<BudgetModeOption isSelected={selectedBudgetMode === OnDemandBudgetMode.SHARED}>
<Label aria-label={t('Shared')}>
<div>
<BudgetContainer>
<StyledRadio
readOnly
id="shared"
value="shared"
data-test-id="shared-budget-radio"
checked={selectedBudgetMode === OnDemandBudgetMode.SHARED}
disabled={!onDemandSupported}
onClick={() => {
setBudgetMode(OnDemandBudgetMode.SHARED);
}}
/>
<BudgetDetails>
<Title>
<OnDemandType>{t('Shared')}</OnDemandType>
{onDemandEnabled &&
currentBudgetMode === OnDemandBudgetMode.SHARED && (
<Tag>{t('Current Budget')}</Tag>
)}
</Title>
<Description>
{t(
'The on-demand budget is shared among all categories on a first come, first serve basis. There are no restrictions for any single category consuming the entire budget.'
)}
</Description>
{this.renderInputFields(OnDemandBudgetMode.SHARED)}
</BudgetDetails>
</BudgetContainer>
</div>
</Label>
</BudgetModeOption>
<BudgetModeOption
isSelected={selectedBudgetMode === OnDemandBudgetMode.PER_CATEGORY}
>
<Label aria-label={t('Per-Category')}>
<div>
<BudgetContainer>
<StyledRadio
readOnly
id="per_category"
value="per_category"
data-test-id="per-category-budget-radio"
checked={selectedBudgetMode === OnDemandBudgetMode.PER_CATEGORY}
disabled={!onDemandSupported}
onClick={() => {
setBudgetMode(OnDemandBudgetMode.PER_CATEGORY);
}}
/>
<BudgetDetails>
<Title>
<OnDemandType>{t('Per-Category')}</OnDemandType>
{onDemandEnabled &&
currentBudgetMode === OnDemandBudgetMode.PER_CATEGORY && (
<Tag>{t('Current Budget')}</Tag>
)}
</Title>
<Description>
{t(
'Dedicated on-demand budget for %s. Any overages in one category will not consume the budget of another category.',
perCategoryCategories
)}
</Description>
{this.renderInputFields(OnDemandBudgetMode.PER_CATEGORY)}
</BudgetDetails>
</BudgetContainer>
</div>
</Label>
</BudgetModeOption>
</PanelBody>
);
}

const addOnDataCategories = Object.values(
subscription.planDetails.addOnCategories
).flatMap(addOn => addOn.dataCategories);
const currentReserved = Object.fromEntries(
Object.entries(subscription.categories)
.filter(([category]) => !addOnDataCategories.includes(category as DataCategory))
.map(([category, categoryInfo]) => [category, categoryInfo.reserved ?? 0])
);

return (
<PanelBody>
<BudgetModeOption isSelected={selectedBudgetMode === OnDemandBudgetMode.SHARED}>
<Label aria-label={t('Shared')}>
<div>
<BudgetContainer>
<StyledRadio
readOnly
id="shared"
value="shared"
data-test-id="shared-budget-radio"
checked={selectedBudgetMode === OnDemandBudgetMode.SHARED}
disabled={!onDemandSupported}
onClick={() => {
setBudgetMode(OnDemandBudgetMode.SHARED);
}}
/>
<BudgetDetails>
<Title>
<OnDemandType>{t('Shared')}</OnDemandType>
{onDemandEnabled &&
currentBudgetMode === OnDemandBudgetMode.SHARED && (
<Tag>{t('Current Budget')}</Tag>
)}
</Title>
<Description>
{t(
'The on-demand budget is shared among all categories on a first come, first serve basis. There are no restrictions for any single category consuming the entire budget.'
)}
</Description>
{this.renderInputFields(OnDemandBudgetMode.SHARED)}
</BudgetDetails>
</BudgetContainer>
</div>
</Label>
</BudgetModeOption>
<BudgetModeOption
isSelected={selectedBudgetMode === OnDemandBudgetMode.PER_CATEGORY}
>
<Label aria-label={t('Per-Category')}>
<div>
<BudgetContainer>
<StyledRadio
readOnly
id="per_category"
value="per_category"
data-test-id="per-category-budget-radio"
checked={selectedBudgetMode === OnDemandBudgetMode.PER_CATEGORY}
disabled={!onDemandSupported}
onClick={() => {
setBudgetMode(OnDemandBudgetMode.PER_CATEGORY);
}}
/>
<BudgetDetails>
<Title>
<OnDemandType>{t('Per-Category')}</OnDemandType>
{onDemandEnabled &&
currentBudgetMode === OnDemandBudgetMode.PER_CATEGORY && (
<Tag>{t('Current Budget')}</Tag>
)}
</Title>
<Description>
{t(
'Dedicated on-demand budget for %s. Any overages in one category will not consume the budget of another category.',
perCategoryCategories
)}
</Description>
{this.renderInputFields(OnDemandBudgetMode.PER_CATEGORY)}
</BudgetDetails>
</BudgetContainer>
</div>
</Label>
</BudgetModeOption>
</PanelBody>
<Container padding="2xl">
<EmbeddedSpendLimitSettings
organization={organization}
header={
<Heading as="h2" size="xl">
{tct('Set your [budgetTerm] limit', {
budgetTerm: displayBudgetName(subscription.planDetails),
})}
</Heading>
}
activePlan={subscription.planDetails}
initialOnDemandBudgets={parseOnDemandBudgetsFromSubscription(subscription)}
currentReserved={currentReserved}
addOns={subscription.addOns ?? {}}
onUpdate={({onDemandBudgets}) => {
this.props.setOnDemandBudget(onDemandBudgets);
}}
/>
</Container>
);
}
}
Expand Down
29 changes: 16 additions & 13 deletions static/gsApp/views/onDemandBudgets/onDemandBudgetEditModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import withApi from 'sentry/utils/withApi';

import SubscriptionStore from 'getsentry/stores/subscriptionStore';
import type {OnDemandBudgetMode, OnDemandBudgets, Subscription} from 'getsentry/types';
import {displayBudgetName} from 'getsentry/utils/billing';
import {displayBudgetName, hasNewBillingUI} from 'getsentry/utils/billing';

import OnDemandBudgetEdit from './onDemandBudgetEdit';
import {
Expand Down Expand Up @@ -184,22 +184,25 @@ class OnDemandBudgetEditModal extends Component<Props, State> {

render() {
const {Header, Footer, subscription, organization} = this.props;
const isNewBillingUI = hasNewBillingUI(organization);
const onDemandBudgets = subscription.onDemandBudgets!;

return (
<Fragment>
<Header closeButton>
<h4>
{tct('[action] [budgetType]', {
action: onDemandBudgets.enabled ? t('Edit') : t('Set Up'),
budgetType: displayBudgetName(subscription.planDetails, {
title: true,
withBudget: true,
pluralOndemand: true,
}),
})}
</h4>
</Header>
{!isNewBillingUI && (
<Header closeButton>
<h4>
{tct('[action] [budgetType]', {
action: onDemandBudgets.enabled ? t('Edit') : t('Set Up'),
budgetType: displayBudgetName(subscription.planDetails, {
title: true,
withBudget: true,
pluralOndemand: true,
}),
})}
</h4>
</Header>
)}
<OffsetBody>
{this.renderError(this.state.updateError)}
<OnDemandBudgetEdit
Expand Down
Loading
Loading