Skip to content
Draft
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
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