import {Component, Fragment} from 'react'; import {css} from '@emotion/react'; import styled from '@emotion/styled'; import type {ModalRenderProps} from 'sentry/actionCreators/modal'; import {openModal} from 'sentry/actionCreators/modal'; import type {Client} from 'sentry/api'; import {Alert} from 'sentry/components/core/alert'; import BooleanField from 'sentry/components/deprecatedforms/booleanField'; import DateTimeField from 'sentry/components/deprecatedforms/dateTimeField'; import Form from 'sentry/components/deprecatedforms/form'; import InputField from 'sentry/components/deprecatedforms/inputField'; import NumberField from 'sentry/components/deprecatedforms/numberField'; import SelectField from 'sentry/components/deprecatedforms/selectField'; import {space} from 'sentry/styles/space'; import withApi from 'sentry/utils/withApi'; import {prettyDate} from 'admin/utils'; import {CPE_MULTIPLIER_TO_CENTS, RESERVED_BUDGET_QUOTA} from 'getsentry/constants'; import type {ReservedBudgetMetricHistory, Subscription} from 'getsentry/types'; import { isAm3DsPlan, isAm3Plan, isAmEnterprisePlan, isAmPlan, } from 'getsentry/utils/billing'; const CPE_DECIMAL_PRECISION = 8; // TODO: replace with modern fields so we don't need these workarounds class DateField extends DateTimeField { getType() { return 'date'; } } type DollarsAndCentsFieldProps = { max?: number; min?: number; step?: any; } & NumberField['props']; class DollarsField extends NumberField { getField() { return (
$ {super.getField()}
); } } class DollarsAndCentsField extends InputField { getField() { return (
$ {super.getField()}
); } coerceValue(value: any): number | '' { const floatValue = parseFloat(value); if (isNaN(floatValue)) { return ''; } return floatValue; } getType() { return 'number'; } getAttributes() { return { min: this.props.min || undefined, max: this.props.max || undefined, step: this.props.step || undefined, }; } } type Props = { api: Client; onSuccess: () => void; orgId: string; subscription: Subscription; canProvisionDsPlan?: boolean; // TODO(DS Spans): remove once we need to provision DS plans }; type ModalProps = ModalRenderProps & Props; type ModalState = { data: any; // TODO(ts), TODO:categories get data.plan categories to dynamically create fields effectiveAtDisabled: boolean; }; function toAnnualDollars( cents: number | null | undefined, billingInterval: string | null | undefined, decimals = 0 ) { if (typeof cents !== 'number') { return cents; } if (billingInterval === 'monthly') { return parseFloat(((cents * 12) / 100).toFixed(decimals)); } return parseFloat((cents / 100).toFixed(decimals)); } /** * Convert dollars to 0.000001 cents * @param dollars - dollars to convert * @returns dollars in units of 0.000001 cents */ function toCpeCents(dollars: number | null | undefined) { if (typeof dollars !== 'number') { return dollars; } return parseInt(((dollars * 100) / CPE_MULTIPLIER_TO_CENTS).toFixed(0), 10); } function toCents(dollars: number | null | undefined, decimals = 0) { if (typeof dollars !== 'number') { return dollars; } return parseFloat((dollars * 100).toFixed(decimals)); } class ProvisionSubscriptionModal extends Component { state: ModalState = { data: {}, effectiveAtDisabled: false, }; componentDidMount() { const {subscription} = this.props; const existingPlanWithoutSuffix = subscription.plan.endsWith('_auf') ? subscription.plan.slice(0, subscription.plan.length - 4) : subscription.plan.endsWith('_ac') ? subscription.plan.slice(0, subscription.plan.length - 3) : subscription.plan; const existingPlanIsEnterprise = this.provisionablePlans.some( plan => plan[0] === existingPlanWithoutSuffix ); const reservedBudgets = subscription.reservedBudgets; const reservedBudgetMetricHistories: Record = {}; reservedBudgets?.forEach(budget => { Object.entries(budget.categories).forEach(([category, info]) => { reservedBudgetMetricHistories[category] = info; }); }); if (existingPlanIsEnterprise) { this.setState(state => ({ ...state, data: { ...state.data, plan: existingPlanWithoutSuffix, billingInterval: subscription.billingInterval, retainOnDemandBudget: false, type: subscription.type, onDemandInvoicedManual: subscription.onDemandInvoicedManual ? subscription.onDemandBudgets?.budgetMode.toString().toUpperCase() : subscription.onDemandInvoicedManual === null ? null : 'DISABLE', managed: subscription.isManaged, reservedErrors: subscription.categories.errors?.reserved, reservedTransactions: subscription.categories.transactions?.reserved, reservedReplays: subscription.categories.replays?.reserved, reservedMonitorSeats: subscription.categories.monitorSeats?.reserved, reservedUptime: subscription.categories.uptime?.reserved, reservedSpans: subscription.categories.spans?.reserved, reservedSpansIndexed: subscription.categories.spansIndexed?.reserved, reservedAttachments: subscription.categories.attachments?.reserved, reservedProfileDuration: subscription.categories.profileDuration?.reserved, softCapTypeErrors: subscription.categories.errors?.softCapType, softCapTypeTransactions: subscription.categories.transactions?.softCapType, softCapTypeReplays: subscription.categories.replays?.softCapType, softCapTypeMonitorSeats: subscription.categories.monitorSeats?.softCapType, softCapTypeUptime: subscription.categories.uptime?.softCapType, softCapTypeSpans: subscription.categories.spans?.softCapType, softCapTypeSpansIndexed: subscription.categories.spansIndexed?.softCapType, softCapTypeAttachments: subscription.categories.attachments?.softCapType, softCapTypeProfileDuration: subscription.categories.profileDuration?.softCapType, customPriceErrors: toAnnualDollars( subscription.categories.errors?.customPrice, subscription.billingInterval ), customPriceTransactions: toAnnualDollars( subscription.categories.transactions?.customPrice, subscription.billingInterval ), customPriceReplays: toAnnualDollars( subscription.categories.replays?.customPrice, subscription.billingInterval ), customPriceMonitorSeats: toAnnualDollars( subscription.categories.monitorSeats?.customPrice, subscription.billingInterval ), customPriceUptime: toAnnualDollars( subscription.categories.uptime?.customPrice, subscription.billingInterval ), customPriceSpans: toAnnualDollars( subscription.categories.spans?.customPrice, subscription.billingInterval ), customPriceSpansIndexed: toAnnualDollars( subscription.categories.spansIndexed?.customPrice, subscription.billingInterval ), customPriceAttachments: toAnnualDollars( subscription.categories.attachments?.customPrice, subscription.billingInterval ), customPriceProfileDuration: toAnnualDollars( subscription.categories.profileDuration?.customPrice, subscription.billingInterval ), customPricePcss: toAnnualDollars( subscription.customPricePcss, subscription.billingInterval ), customPrice: toAnnualDollars( subscription.customPrice, subscription.billingInterval ), onDemandCpeErrors: toAnnualDollars( subscription.categories.errors?.onDemandCpe, null, CPE_DECIMAL_PRECISION ), onDemandCpeTransactions: toAnnualDollars( subscription.categories.transactions?.onDemandCpe, null, CPE_DECIMAL_PRECISION ), onDemandCpeReplays: toAnnualDollars( subscription.categories.replays?.onDemandCpe, null, CPE_DECIMAL_PRECISION ), onDemandCpeMonitorSeats: toAnnualDollars( subscription.categories.monitorSeats?.onDemandCpe, null, CPE_DECIMAL_PRECISION ), onDemandCpeUptime: toAnnualDollars( subscription.categories.uptime?.onDemandCpe, null, CPE_DECIMAL_PRECISION ), onDemandCpeSpans: toAnnualDollars( subscription.categories.spans?.onDemandCpe, null, CPE_DECIMAL_PRECISION ), onDemandCpeSpansIndexed: toAnnualDollars( subscription.categories.spansIndexed?.onDemandCpe, null, CPE_DECIMAL_PRECISION ), onDemandCpeAttachments: toAnnualDollars( subscription.categories.attachments?.onDemandCpe, null, CPE_DECIMAL_PRECISION ), onDemandCpeProfileDuration: toAnnualDollars( subscription.categories.profileDuration?.onDemandCpe, null, CPE_DECIMAL_PRECISION ), // coming from the API, reservedCpe is in cents reservedCpeSpans: toAnnualDollars( reservedBudgetMetricHistories.spans?.reservedCpe, null, CPE_DECIMAL_PRECISION ), reservedCpeSpansIndexed: toAnnualDollars( reservedBudgetMetricHistories.spansIndexed?.reservedCpe, null, CPE_DECIMAL_PRECISION ), }, })); } } get endpoint() { return `/customers/${this.props.orgId}/provision-subscription/`; } isEnablingOnDemandMaxSpend = () => this.state.data.onDemandInvoicedManual === 'SHARED' || this.state.data.onDemandInvoicedManual === 'PER_CATEGORY'; isEnablingSoftCap = () => this.state.data.softCapTypeErrors || this.state.data.softCapTypeTransactions || this.state.data.softCapTypeReplays || this.state.data.softCapTypeMonitorSeats || this.state.data.softCapTypeUptime || this.state.data.softCapTypeSpans || this.state.data.softCapTypeSpansIndexed || this.state.data.softCapTypeAttachments; isSettingSpansBudget = () => isAm3DsPlan(this.state.data.plan) && this.state.data.reservedCpeSpans && this.state.data.reservedCpeSpansIndexed; hasCompleteSpansBudget = () => this.isSettingSpansBudget() && this.state.data.reservedSpans === RESERVED_BUDGET_QUOTA && this.state.data.reservedSpansIndexed === RESERVED_BUDGET_QUOTA && this.state.data.customPriceSpans; disableRetainOnDemand = () => { if (this.state.data.onDemandInvoicedManual === null) { // don't show the toggle if there is no ondemand type return true; } const original = this.props.subscription.onDemandInvoicedManual ? this.props.subscription.onDemandBudgets?.budgetMode.toString().toUpperCase() : this.props.subscription.onDemandInvoicedManual === null ? null : 'DISABLE'; return ( this.state.data.onDemandInvoicedManual !== original || this.state.data.onDemandInvoicedManual === 'DISABLE' ); }; onSubmit: Form['props']['onSubmit'] = (formData, _onSubmitSuccess, onSubmitError) => { const postData = {...this.state.data}; for (const k in formData) { if (formData[k] !== '' && formData[k] !== null) { postData[k] = formData[k]; } } // clear disabled fields if (postData.atPeriodEnd || postData.coterm) { delete postData.effectiveAt; } if (!postData.coterm) { delete postData.coterm; } const hasCustomSkuPrices = isAmEnterprisePlan(postData.plan); if (!hasCustomSkuPrices) { delete postData.customPriceErrors; delete postData.customPriceTransactions; delete postData.customPriceAttachments; delete postData.customPricePcss; delete postData.customPriceReplays; delete postData.customPriceMonitorSeats; delete postData.customPriceUptime; delete postData.customPriceSpans; delete postData.customPriceSpansIndexed; delete postData.customPriceProfileDuration; } // only set reserved & custom price for spans OR transactions if (isAm3Plan(postData.plan)) { delete postData.reservedTransactions; delete postData.customPriceTransactions; } else { delete postData.reservedSpans; delete postData.customPriceSpans; } if (postData.type !== 'invoiced') { delete postData.onDemandInvoicedManual; delete postData.onDemandCpeErrors; delete postData.onDemandCpeTransactions; delete postData.onDemandCpeReplays; delete postData.onDemandCpeAttachments; delete postData.onDemandCpeMonitorSeats; delete postData.onDemandCpeUptime; delete postData.onDemandCpeSpans; delete postData.onDemandCpeProfileDuration; // clear corresponding state this.setState(state => ({ ...state, data: { ...state.data, onDemandInvoicedManual: null, }, })); } if (this.isEnablingOnDemandMaxSpend()) { postData.softCapTypeErrors = null; postData.softCapTypeTransactions = null; postData.softCapTypeReplays = null; postData.softCapTypeAttachments = null; postData.softCapTypeMonitorSeats = null; postData.softCapTypeUptime = null; postData.softCapTypeSpans = null; postData.softCapTypeProfileDuration = null; this.setState(state => ({ ...state, data: { ...state.data, softCapTypeErrors: null, softCapTypeTransactions: null, softCapTypeReplays: null, softCapTypeAttachments: null, softCapTypeMonitorSeats: null, softCapTypeUptime: null, softCapTypeSpans: null, softCapTypeProfileDuration: null, }, })); } else { delete postData.onDemandCpeErrors; delete postData.onDemandCpeTransactions; delete postData.onDemandCpeReplays; delete postData.onDemandCpeAttachments; delete postData.onDemandCpeMonitorSeats; delete postData.onDemandCpeUptime; delete postData.onDemandCpeSpans; delete postData.onDemandCpeProfileDuration; } if (this.isEnablingSoftCap()) { postData.onDemandInvoicedManual = 'DISABLE'; delete postData.onDemandCpeErrors; delete postData.onDemandCpeTransactions; delete postData.onDemandCpeReplays; delete postData.onDemandCpeAttachments; delete postData.onDemandCpeMonitorSeats; delete postData.onDemandCpeUptime; delete postData.onDemandCpeSpans; delete postData.onDemandCpeProfileDuration; } if (!isNaN(postData.onDemandCpeErrors)) { postData.onDemandCpeErrors = toCents( postData.onDemandCpeErrors, CPE_DECIMAL_PRECISION ); } if (!isNaN(postData.onDemandCpeTransactions)) { postData.onDemandCpeTransactions = toCents( postData.onDemandCpeTransactions, CPE_DECIMAL_PRECISION ); } if (!isNaN(postData.onDemandCpeReplays)) { postData.onDemandCpeReplays = toCents( postData.onDemandCpeReplays, CPE_DECIMAL_PRECISION ); } if (!isNaN(postData.onDemandCpeAttachments)) { postData.onDemandCpeAttachments = toCents( postData.onDemandCpeAttachments, CPE_DECIMAL_PRECISION ); } if (!isNaN(postData.onDemandCpeMonitorSeats)) { postData.onDemandCpeMonitorSeats = toCents( postData.onDemandCpeMonitorSeats, CPE_DECIMAL_PRECISION ); } if (!isNaN(postData.onDemandCpeUptime)) { postData.onDemandCpeUptime = toCents( postData.onDemandCpeUptime, CPE_DECIMAL_PRECISION ); } if (!isNaN(postData.onDemandCpeSpans)) { postData.onDemandCpeSpans = toCents( postData.onDemandCpeSpans, CPE_DECIMAL_PRECISION ); } if (!isNaN(postData.onDemandCpeSpansIndexed)) { postData.onDemandCpeSpansIndexed = toCents( postData.onDemandCpeSpansIndexed, CPE_DECIMAL_PRECISION ); } if (!isNaN(postData.onDemandCpeProfileDuration)) { postData.onDemandCpeProfileDuration = toCents( postData.onDemandCpeProfileDuration, CPE_DECIMAL_PRECISION ); } if (!isNaN(postData.reservedCpeSpans)) { postData.reservedCpeSpans = toCpeCents(postData.reservedCpeSpans); } if (!isNaN(postData.reservedCpeSpansIndexed)) { postData.reservedCpeSpansIndexed = toCpeCents(postData.reservedCpeSpansIndexed); } postData.retainOnDemandBudget = postData.retainOnDemandBudget ? !this.disableRetainOnDemand() : false; const hasCustomPrice = hasCustomSkuPrices || postData.managed; if (!hasCustomPrice) { delete postData.hasCustomPrice; } if (!isNaN(postData.customPriceErrors)) { postData.customPriceErrors *= 100; // Price should be in cents } if (!isNaN(postData.customPriceTransactions)) { postData.customPriceTransactions *= 100; // Price should be in cents } if (!isNaN(postData.customPriceReplays)) { postData.customPriceReplays *= 100; // Price should be in cents } if (!isNaN(postData.customPriceSpans)) { postData.customPriceSpans *= 100; // Price should be in cents } if (!isNaN(postData.customPriceSpansIndexed)) { postData.customPriceSpansIndexed *= 100; // Price should be in cents } if (!isNaN(postData.customPriceMonitorSeats)) { postData.customPriceMonitorSeats *= 100; // Price should be in cents } if (!isNaN(postData.customPriceUptime)) { postData.customPriceUptime *= 100; // Price should be in cents } if (!isNaN(postData.customPriceAttachments)) { postData.customPriceAttachments *= 100; // Price should be in cents } if (!isNaN(postData.customPricePcss)) { postData.customPricePcss *= 100; // Price should be in cents } if (!isNaN(postData.customPriceProfileDuration)) { postData.customPriceProfileDuration *= 100; // Price should be in cents } if (!isNaN(postData.customPrice)) { postData.customPrice *= 100; // Price should be in cents // For AM only: If customPrice is set, ensure that it is equal to sum of SKU prices if ( hasCustomSkuPrices && postData.customPrice !== postData.customPriceErrors + (isAm3Plan(postData.plan) ? (postData.customPriceSpans ?? 0) : (postData.customPriceTransactions ?? 0)) + (postData.customPriceReplays ?? 0) + (postData.customPriceMonitorSeats ?? 0) + (postData.customPriceUptime ?? 0) + postData.customPriceAttachments + postData.customPricePcss + (postData.customPriceProfileDuration ?? 0) + (isAm3DsPlan(postData.plan) ? (postData.customPriceSpansIndexed ?? 0) : 0) ) { onSubmitError({ responseJSON: { customPrice: ['Custom Price must be equal to sum of SKU prices'], }, }); return; } } if (isAmPlan(postData.plan)) { // Setting soft cap types to null if not `ON_DEMAND` or `TRUE_FORWARD` ensures soft cap type // is disabled if it was set but is not set with the new provisioning request. if (!postData.softCapTypeErrors) { postData.softCapTypeErrors = null; } if (!postData.softCapTypeReplays) { postData.softCapTypeReplays = null; } if (!postData.softCapTypeAttachments) { postData.softCapTypeAttachments = null; } if (!postData.softCapTypeMonitorSeats) { postData.softCapTypeMonitorSeats = null; } if (!postData.softCapTypeUptime) { postData.softCapTypeUptime = null; } if (!postData.softCapTypeProfileDuration) { postData.softCapTypeProfileDuration = null; } // If a data category has a set soft cap type, trueForwad will also need to be set to true for that category // until the true forward fields are fully deprecated and soft cap types are used in their place. postData.trueForward = { errors: postData.softCapTypeErrors ? true : false, replays: postData.softCapTypeReplays ? true : false, attachments: postData.softCapTypeAttachments ? true : false, monitor_seats: postData.softCapTypeMonitorSeats ? true : false, uptime: postData.softCapTypeUptime ? true : false, profile_duration: postData.softCapTypeProfileDuration ? true : false, }; if (isAm3Plan(postData.plan)) { postData.trueForward = { ...postData.trueForward, spans: postData.softCapTypeSpans ? true : false, }; delete postData.softCapTypeTransactions; if (!postData.softCapTypeSpans) { postData.softCapTypeSpans = null; } } else { postData.trueForward = { ...postData.trueForward, transactions: postData.softCapTypeTransactions ? true : false, }; delete postData.softCapTypeSpans; delete postData.softCapTypeSpansIndexed; if (!postData.softCapTypeTransactions) { postData.softCapTypeTransactions = null; } } } if (isAm3DsPlan(postData.plan)) { postData.trueForward = { ...postData.trueForward, spansIndexed: postData.softCapTypeSpansIndexed ? true : false, }; if (!postData.softCapTypeSpansIndexed) { postData.softCapTypeSpansIndexed = null; } if (this.hasCompleteSpansBudget()) { postData.reservedBudgets = [ { categories: ['spans', 'spansIndexed'], budget: postData.customPriceSpans, }, ]; } else { onSubmitError({ responseJSON: { customPriceSpans: [ 'Dynamic Sampling plans require reserved spans budget with reserved CPEs for both accepted and stored spans', ], }, }); return; } } else { for (const k in postData) { if (k.endsWith('SpansIndexed')) { delete postData[k]; } } delete postData.reservedCpeSpans; } this.props.api.request(this.endpoint, { method: 'POST', data: postData, success: () => { this.props.onSuccess(); this.props.closeModal(); }, error: error => { onSubmitError({ responseJSON: error.responseJSON, }); }, }); }; provisionablePlans = [ ['am3_business_ent_ds', 'Business with Dynamic Sampling (am3)'], ['am3_team_ent_ds', 'Team with Dynamic Sampling (am3)'], ['am3_business_ent', 'Business (am3)'], ['am3_team_ent', 'Team (am3)'], ['am2_business_ent', 'Business (am2)'], ['am2_team_ent', 'Team (am2)'], ['am1_business_ent', 'Business (am1)'], ['am1_team_ent', 'Team (am1)'], ['mm2_a', 'Business (mm2)'], ['mm2_b', 'Team (mm2)'], ['e1', 'Enterprise (mm1)'], ]; render() { const {Header, Body, closeModal, canProvisionDsPlan = false} = this.props; const {data} = this.state; const isAmEnt = isAmEnterprisePlan(data.plan); const isAm3 = isAm3Plan(data.plan); const isAm3Ds = isAm3DsPlan(data.plan); const hasCustomSkuPrices = isAmEnt; const hasCustomPrice = hasCustomSkuPrices || !!data.managed; // Refers to ACV if (!canProvisionDsPlan) { this.provisionablePlans = this.provisionablePlans.filter( plan => !isAm3DsPlan(plan[0]) ); } return (
Provision Subscription Changes
{ // Reset price fields if next plan is not AM Enterprise const isManagedPlan = isAmEnterprisePlan(v as string); const chosenPlanIsAm3Ds = isAm3DsPlan(v as string); const nextPrices = isManagedPlan ? {} : { customPriceErrors: '', customPriceTransactions: '', customPriceReplays: '', customPriceMonitorSeats: '', customPriceUptime: '', customPriceSpans: '', customPriceSpansIndexed: '', customPriceAttachments: '', customPricePcss: '', customPrice: '', }; const nextReservedCpes = chosenPlanIsAm3Ds ? {} : { reservedCpeSpans: '', reservedCpeSpansIndexed: '', }; this.setState(state => ({ ...state, data: { ...state.data, plan: v, ...nextPrices, ...nextReservedCpes, }, })); }} value={this.state.data.plan} /> this.setState(state => ({ ...state, effectiveAtDisabled: !!v, data: {...state.data, atPeriodEnd: v}, })) } /> this.setState(state => ({ ...state, data: {...state.data, coterm: v}, effectiveAtDisabled: !!v, })) } /> this.setState(state => ({ ...state, data: { ...this.state.data, billingInterval: v, }, })) } /> this.setState(state => ({ ...state, data: { ...state.data, managed: v, customPrice: v ? state.data.customPrice : '', }, })) } /> { if (v === 'credit_card') { this.setState(state => ({ ...state, data: {...state.data, onDemandInvoicedManual: ''}, })); } this.setState(state => ({...state, data: {...state.data, type: v}})); }} value={this.state.data.type} /> {this.state.data.type === 'invoiced' && ( this.setState(state => ({ ...state, data: {...state.data, onDemandInvoicedManual: v}, })) } /> )} {!this.disableRetainOnDemand() && ( this.setState(state => ({ ...state, data: { ...state.data, retainOnDemandBudget: v, }, })) } /> )} Plan Quotas Monthly quantities for each SKU this.setState(state => ({ ...state, data: {...state.data, reservedErrors: v}, })) } /> this.setState(state => ({ ...state, data: {...state.data, softCapTypeErrors: v}, })) } /> this.setState(state => ({ ...state, data: {...state.data, reservedTransactions: v}, })) } /> this.setState(state => ({ ...state, data: {...state.data, softCapTypeTransactions: v}, })) } /> this.setState(state => ({ ...state, data: {...state.data, reservedReplays: v}, })) } /> this.setState(state => ({ ...state, data: {...state.data, softCapTypeReplays: v}, })) } /> this.setState(state => ({ ...state, data: {...state.data, reservedSpans: v}, })) } /> this.setState(state => ({ ...state, data: {...state.data, softCapTypeSpans: v}, })) } /> {isAm3Ds && ( this.setState(state => ({ ...state, data: { ...state.data, reservedCpeSpans: v, reservedSpans: RESERVED_BUDGET_QUOTA, }, })) } onBlur={() => { const currentValue = parseFloat(this.state.data.reservedCpeSpans); if (!isNaN(currentValue)) { this.setState(state => ({ ...state, data: { ...state.data, reservedCpeSpans: currentValue.toFixed(CPE_DECIMAL_PRECISION), }, })); } }} /> )} {isAm3Ds && ( this.setState(state => ({ ...state, data: {...state.data, reservedSpansIndexed: v}, })) } /> this.setState(state => ({ ...state, data: {...state.data, softCapTypeSpansIndexed: v}, })) } /> this.setState(state => ({ ...state, data: { ...state.data, reservedCpeSpansIndexed: v, reservedSpansIndexed: RESERVED_BUDGET_QUOTA, }, })) } onBlur={() => { const currentValue = parseFloat( this.state.data.reservedCpeSpansIndexed ); if (!isNaN(currentValue)) { this.setState(state => ({ ...state, data: { ...state.data, reservedCpeSpansIndexed: currentValue.toFixed(CPE_DECIMAL_PRECISION), }, })); } }} /> )} this.setState(state => ({ ...state, data: {...state.data, reservedMonitorSeats: v}, })) } /> this.setState(state => ({ ...state, data: {...state.data, softCapTypeMonitorSeats: v}, })) } /> this.setState(state => ({ ...state, data: {...state.data, reservedUptime: v}, })) } /> this.setState(state => ({ ...state, data: {...state.data, softCapTypeUptime: v}, })) } /> this.setState(state => ({ ...state, data: {...state.data, reservedAttachments: v}, })) } /> this.setState(state => ({ ...state, data: {...state.data, softCapTypeAttachments: v}, })) } /> this.setState(state => ({ ...state, data: {...state.data, reservedProfileDuration: v}, })) } /> this.setState(state => ({ ...state, data: {...state.data, softCapTypeProfileDuration: v}, })) } />
Reserved Volume Prices Annual prices for reserved volumes, in whole dollars. this.setState(state => ({ ...state, data: { ...state.data, customPriceErrors: v, }, })) } /> this.setState(state => ({ ...state, data: { ...state.data, customPriceTransactions: v, }, })) } /> this.setState(state => ({ ...state, data: { ...state.data, customPriceReplays: v, }, })) } /> this.setState(state => ({ ...state, data: { ...state.data, customPriceSpans: v, }, })) } /> {isAm3Ds && ( this.setState(state => ({ ...state, data: { ...state.data, customPriceSpansIndexed: v, }, })) } /> )} this.setState(state => ({ ...state, data: { ...state.data, customPriceMonitorSeats: v, }, })) } /> this.setState(state => ({ ...state, data: { ...state.data, customPriceUptime: v, }, })) } /> this.setState(state => ({ ...state, data: { ...state.data, customPriceAttachments: v, }, })) } /> this.setState(state => ({ ...state, data: { ...state.data, customPriceProfileDuration: v, }, })) } /> this.setState(state => ({ ...state, data: { ...state.data, customPricePcss: v, }, })) } /> this.setState(state => ({ ...state, data: { ...state.data, customPrice: v, }, })) } /> {this.isEnablingOnDemandMaxSpend() && ( On-Demand Cost-Per-Event (CPE) The cost of on-demand units, in dollars, for invoiced customers with on-demand max spend. If not set, the on-demand spend will be calculated with the self-serve on-demand pricing. If the subscription already has on-demand spend in the current period, and the new cost-per-event overrides would cause the spend to exceed the on-demand budget, the request will fail. this.setState(state => ({ ...state, data: { ...state.data, onDemandCpeErrors: v, }, })) } onBlur={() => { const currentValue = parseFloat( this.state.data.onDemandCpeErrors ); if (!isNaN(currentValue)) { this.setState(state => ({ ...state, data: { ...state.data, onDemandCpeErrors: currentValue.toFixed(CPE_DECIMAL_PRECISION), }, })); } }} /> this.setState(state => ({ ...state, data: { ...state.data, onDemandCpeTransactions: v, }, })) } onBlur={() => { const currentValue = parseFloat( this.state.data.onDemandCpeTransactions ); if (!isNaN(currentValue)) { this.setState(state => ({ ...state, data: { ...state.data, onDemandCpeTransactions: currentValue.toFixed(CPE_DECIMAL_PRECISION), }, })); } }} /> this.setState(state => ({ ...state, data: { ...state.data, onDemandCpeReplays: v, }, })) } onBlur={() => { const currentValue = parseFloat( this.state.data.onDemandCpeReplays ); if (!isNaN(currentValue)) { this.setState(state => ({ ...state, data: { ...state.data, onDemandCpeReplays: currentValue.toFixed(CPE_DECIMAL_PRECISION), }, })); } }} /> this.setState(state => ({ ...state, data: { ...state.data, onDemandCpeSpans: v, }, })) } onBlur={() => { const currentValue = parseFloat(this.state.data.onDemandCpeSpans); if (!isNaN(currentValue)) { this.setState(state => ({ ...state, data: { ...state.data, onDemandCpeSpans: currentValue.toFixed(CPE_DECIMAL_PRECISION), }, })); } }} /> this.setState(state => ({ ...state, data: { ...state.data, onDemandCpeAttachments: v, }, })) } onBlur={() => { const currentValue = parseFloat( this.state.data.onDemandCpeAttachments ); if (!isNaN(currentValue)) { this.setState(state => ({ ...state, data: { ...state.data, onDemandCpeAttachments: currentValue.toFixed(CPE_DECIMAL_PRECISION), }, })); } }} /> this.setState(state => ({ ...state, data: { ...state.data, onDemandCpeProfileDuration: v, }, })) } onBlur={() => { const currentValue = parseFloat( this.state.data.onDemandCpeProfileDuration ); if (!isNaN(currentValue)) { this.setState(state => ({ ...state, data: { ...state.data, onDemandCpeProfileDuration: currentValue.toFixed(CPE_DECIMAL_PRECISION), }, })); } }} /> this.setState(state => ({ ...state, data: { ...state.data, onDemandCpeMonitorSeats: v, }, })) } onBlur={() => { const currentValue = parseFloat( this.state.data.onDemandCpeMonitorSeats ); if (!isNaN(currentValue)) { this.setState(state => ({ ...state, data: { ...state.data, onDemandCpeMonitorSeats: currentValue.toFixed(CPE_DECIMAL_PRECISION), }, })); } }} /> this.setState(state => ({ ...state, data: { ...state.data, onDemandCpeUptime: v, }, })) } onBlur={() => { const currentValue = parseFloat( this.state.data.onDemandCpeUptime ); if (!isNaN(currentValue)) { this.setState(state => ({ ...state, data: { ...state.data, onDemandCpeUptime: currentValue.toFixed(CPE_DECIMAL_PRECISION), }, })); } }} /> )}
); } } const Columns = styled('div')` display: grid; grid-template-columns: 1fr 1fr; gap: ${space(3)}; `; const SectionHeader = styled('h5')` margin-bottom: 0; `; const SectionHeaderDescription = styled('small')` display: block; margin-bottom: ${space(3)}; `; const modalCss = css` width: 100%; max-width: 1200px; `; const StyledSelectFieldWithHelpText = styled(SelectField)` margin-bottom: 15px; div[class*='StyledSelectControl'] { margin-bottom: 0; } `; const StyledDollarsField = styled(DollarsField)` div[class='dollars-field-container'] { display: flex; } span[class='dollar-sign'] { padding: 12px; } `; const StyledDollarsAndCentsField = styled(DollarsAndCentsField)` div[class='dollars-cents-field-container'] { display: flex; } span[class='dollar-sign'] { padding: 12px; } `; const Modal = withApi(ProvisionSubscriptionModal); type Options = Pick; const triggerProvisionSubscription = (opts: Options) => openModal(deps => , {modalCss}); export default triggerProvisionSubscription;