diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite new file mode 100644 index 000000000..2891048dc Binary files /dev/null and b/.vs/slnx.sqlite differ diff --git a/src/Explorer/Controls/Settings/SettingsRenderUtils.test.tsx b/src/Explorer/Controls/Settings/SettingsRenderUtils.test.tsx index 057bbdc86..a6d010a1a 100644 --- a/src/Explorer/Controls/Settings/SettingsRenderUtils.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsRenderUtils.test.tsx @@ -1,9 +1,9 @@ import { shallow } from "enzyme"; import React from "react"; +import { IColumn, Text } from "office-ui-fabric-react"; import { getAutoPilotV3SpendElement, - getEstimatedSpendElement, - getEstimatedAutoscaleSpendElement, + getEstimatedSpendingElement, manualToAutoscaleDisclaimerElement, ttlWarning, indexingPolicynUnsavedWarningMessage, @@ -19,11 +19,37 @@ import { mongoIndexingPolicyDisclaimer, mongoIndexingPolicyAADError, mongoIndexTransformationRefreshingMessage, - renderMongoIndexTransformationRefreshMessage + renderMongoIndexTransformationRefreshMessage, + ManualEstimatedSpendingDisplayProps, + PriceBreakdown, + getRuPriceBreakdown } from "./SettingsRenderUtils"; class SettingsRenderUtilsTestComponent extends React.Component { public render(): JSX.Element { + const estimatedSpendingColumns: IColumn[] = [ + { key: "costType", name: "", fieldName: "costType", minWidth: 100, maxWidth: 200, isResizable: true }, + { key: "hourly", name: "Hourly", fieldName: "hourly", minWidth: 100, maxWidth: 200, isResizable: true }, + { key: "daily", name: "Daily", fieldName: "daily", minWidth: 100, maxWidth: 200, isResizable: true }, + { key: "monthly", name: "Monthly", fieldName: "monthly", minWidth: 100, maxWidth: 200, isResizable: true } + ]; + const estimatedSpendingItems: ManualEstimatedSpendingDisplayProps[] = [ + { + costType: Current Cost, + hourly: $ 1.02, + daily: $ 24.48, + monthly: $ 744.6 + } + ]; + const priceBreakdown: PriceBreakdown = { + hourlyPrice: 1.02, + dailyPrice: 24.48, + monthlyPrice: 744.6, + pricePerRu: 0.00051, + currency: "RMB", + currencySign: "¥" + }; + return ( <> {getAutoPilotV3SpendElement(1000, false)} @@ -31,9 +57,7 @@ class SettingsRenderUtilsTestComponent extends React.Component { {getAutoPilotV3SpendElement(1000, true)} {getAutoPilotV3SpendElement(undefined, true)} - {getEstimatedSpendElement(1000, "mooncake", 2, false)} - - {getEstimatedAutoscaleSpendElement(1000, "mooncake", 2, false)} + {getEstimatedSpendingElement(estimatedSpendingColumns, estimatedSpendingItems, 1000, 2, priceBreakdown, false)} {manualToAutoscaleDisclaimerElement} {ttlWarning} @@ -69,4 +93,14 @@ describe("SettingsUtils functions", () => { const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); + + it("should return correct price breakdown for a manual RU setting of 500, 1 region, multimaster disabled", () => { + const prices = getRuPriceBreakdown(500, "", 1, false, false); + expect(prices.hourlyPrice).toBe(0.04); + expect(prices.dailyPrice).toBe(0.96); + expect(prices.monthlyPrice).toBe(29.2); + expect(prices.pricePerRu).toBe(0.00008); + expect(prices.currency).toBe("USD"); + expect(prices.currencySign).toBe("$"); + }); }); diff --git a/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx b/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx index 5aa871d1b..a3f186af7 100644 --- a/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx @@ -3,14 +3,13 @@ import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants"; import { Urls, StyleConstants } from "../../../Common/Constants"; import { - computeAutoscaleUsagePriceHourly, getPriceCurrency, getCurrencySign, getAutoscalePricePerRu, getMultimasterMultiplier, computeRUUsagePriceHourly, getPricePerRu, - calculateEstimateNumber + estimatedCostDisclaimer } from "../../../Utils/PricingUtils"; import { ITextFieldStyles, @@ -32,10 +31,41 @@ import { MessageBarType, Stack, Spinner, - SpinnerSize + SpinnerSize, + DetailsList, + IColumn, + SelectionMode, + DetailsListLayoutMode, + IDetailsRowProps, + DetailsRow, + IDetailsColumnStyles } from "office-ui-fabric-react"; import { isDirtyTypes, isDirty } from "./SettingsUtils"; +export interface EstimatedSpendingDisplayProps { + costType: JSX.Element; +} + +export interface ManualEstimatedSpendingDisplayProps extends EstimatedSpendingDisplayProps { + hourly: JSX.Element; + daily: JSX.Element; + monthly: JSX.Element; +} + +export interface AutoscaleEstimatedSpendingDisplayProps extends EstimatedSpendingDisplayProps { + minPerMonth: JSX.Element; + maxPerMonth: JSX.Element; +} + +export interface PriceBreakdown { + hourlyPrice: number; + dailyPrice: number; + monthlyPrice: number; + pricePerRu: number; + currency: string; + currencySign: string; +} + export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } }; export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = { @@ -104,6 +134,16 @@ export const transparentDetailsRowStyles: Partial = { } }; +export const transparentDetailsHeaderStyle: Partial = { + root: { + selectors: { + ":hover": { + background: "transparent" + } + } + } +}; + export const customDetailsListStyles: Partial = { root: { selectors: { @@ -130,6 +170,10 @@ export const messageBarStyles: Partial = { root: { marginTop: export const throughputUnit = "RU/s"; +export function onRenderRow(props: IDetailsRowProps): JSX.Element { + return ; +} + export const getAutoPilotV3SpendElement = ( maxAutoPilotThroughputSet: number, isDatabaseThroughput: boolean, @@ -165,63 +209,61 @@ export const getAutoPilotV3SpendElement = ( ); }; -export const getEstimatedAutoscaleSpendElement = ( +export const getRuPriceBreakdown = ( throughput: number, serverId: string, - regions: number, - multimaster: boolean -): JSX.Element => { - const hourlyPrice: number = computeAutoscaleUsagePriceHourly(serverId, throughput, regions, multimaster); - const monthlyPrice: number = hourlyPrice * hoursInAMonth; - const currency: string = getPriceCurrency(serverId); - const currencySign: string = getCurrencySign(serverId); - const pricePerRu = - getAutoscalePricePerRu(serverId, getMultimasterMultiplier(regions, multimaster)) * - getMultimasterMultiplier(regions, multimaster); - - return ( - - Estimated monthly cost ({currency}) is{" "} - - {currencySign} - {calculateEstimateNumber(monthlyPrice / 10)} - {` - `} - {currencySign} - {calculateEstimateNumber(monthlyPrice)}{" "} - - ({"regions: "} {regions}, {throughput / 10} - {throughput} RU/s, {currencySign} - {pricePerRu}/RU) - - ); + numberOfRegions: number, + isMultimaster: boolean, + isAutoscale: boolean +): PriceBreakdown => { + const hourlyPrice: number = computeRUUsagePriceHourly({ + serverId: serverId, + requestUnits: throughput, + numberOfRegions: numberOfRegions, + multimasterEnabled: isMultimaster, + isAutoscale: isAutoscale + }); + const basePricePerRu: number = isAutoscale + ? getAutoscalePricePerRu(serverId, getMultimasterMultiplier(numberOfRegions, isMultimaster)) + : getPricePerRu(serverId); + return { + hourlyPrice: hourlyPrice, + dailyPrice: hourlyPrice * 24, + monthlyPrice: hourlyPrice * hoursInAMonth, + pricePerRu: basePricePerRu * getMultimasterMultiplier(numberOfRegions, isMultimaster), + currency: getPriceCurrency(serverId), + currencySign: getCurrencySign(serverId) + }; }; -export const getEstimatedSpendElement = ( +export const getEstimatedSpendingElement = ( + estimatedSpendingColumns: IColumn[], + estimatedSpendingItems: EstimatedSpendingDisplayProps[], throughput: number, - serverId: string, - regions: number, - multimaster: boolean + numberOfRegions: number, + priceBreakdown: PriceBreakdown, + isAutoscale: boolean ): JSX.Element => { - const hourlyPrice: number = computeRUUsagePriceHourly(serverId, throughput, regions, multimaster); - const dailyPrice: number = hourlyPrice * 24; - const monthlyPrice: number = hourlyPrice * hoursInAMonth; - const currency: string = getPriceCurrency(serverId); - const currencySign: string = getCurrencySign(serverId); - const pricePerRu = getPricePerRu(serverId) * getMultimasterMultiplier(regions, multimaster); - + const ruRange: string = isAutoscale ? throughput / 10 + " RU/s - " : ""; return ( - - Estimated cost ({currency}):{" "} - - {currencySign} - {calculateEstimateNumber(hourlyPrice)} hourly {` / `} - {currencySign} - {calculateEstimateNumber(dailyPrice)} daily {` / `} - {currencySign} - {calculateEstimateNumber(monthlyPrice)} monthly{" "} - - ({"regions: "} {regions}, {throughput}RU/s, {currencySign} - {pricePerRu}/RU) - + + + + ({"regions: "} {numberOfRegions}, {ruRange} + {throughput} RU/s, {priceBreakdown.currencySign} + {priceBreakdown.pricePerRu}/RU) + + + {estimatedCostDisclaimer} + + ); }; @@ -265,6 +307,13 @@ export const updateThroughputDelayedApplyWarningMessage: JSX.Element = ( ); +export const saveThroughputWarningMessage: JSX.Element = ( + + Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below + before saving your changes + +); + const getCurrentThroughput = ( isAutoscale: boolean, throughput: number, diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx index a74eaff1e..8c20c88e6 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx @@ -6,8 +6,6 @@ import { IconButton, Text, SelectionMode, - IDetailsRowProps, - DetailsRow, IColumn, MessageBar, MessageBarType, @@ -21,11 +19,11 @@ import { mongoIndexingPolicyDisclaimer, mediumWidthStackStyles, subComponentStackProps, - transparentDetailsRowStyles, createAndAddMongoIndexStackProps, separatorStyles, indexingPolicynUnsavedWarningMessage, - infoAndToolTipTextStyle + infoAndToolTipTextStyle, + onRenderRow } from "../../SettingsRenderUtils"; import { MongoIndex } from "../../../../../Utils/arm/generatedClients/2020-04-01/types"; import { @@ -140,10 +138,6 @@ export class MongoIndexingPolicyComponent extends React.Component { - return ; - }; - private getActionButton = (arrayPosition: number, isCurrentIndex: boolean): JSX.Element => { return isCurrentIndex ? ( {this.renderIndexesToBeAdded()} @@ -279,7 +273,7 @@ export class MongoIndexingPolicyComponent extends React.Component )} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx index 2ccf70079..605ab7a02 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx @@ -54,7 +54,6 @@ describe("ThroughputInputAutoPilotV3Component", () => { expect(wrapper.exists("#throughputInput")).toEqual(true); expect(wrapper.exists("#autopilotInput")).toEqual(false); expect(wrapper.exists("#throughputSpendElement")).toEqual(true); - expect(wrapper.exists("#autoscaleSpendElement")).toEqual(false); }); it("autopilot input visible", () => { @@ -72,8 +71,7 @@ describe("ThroughputInputAutoPilotV3Component", () => { wrapper.setProps({ wasAutopilotOriginallySet: true }); wrapper.update(); - expect(wrapper.exists("#autoscaleSpendElement")).toEqual(true); - expect(wrapper.exists("#throughputSpendElement")).toEqual(false); + expect(wrapper.exists("#throughputSpendElement")).toEqual(true); }); it("spendAck checkbox visible", () => { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx index cc23b0ec9..7cb3f81ce 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx @@ -8,10 +8,15 @@ import { checkBoxAndInputStackProps, getChoiceGroupStyles, messageBarStyles, - getEstimatedSpendElement, - getEstimatedAutoscaleSpendElement, + getEstimatedSpendingElement, getAutoPilotV3SpendElement, - manualToAutoscaleDisclaimerElement + manualToAutoscaleDisclaimerElement, + saveThroughputWarningMessage, + ManualEstimatedSpendingDisplayProps, + AutoscaleEstimatedSpendingDisplayProps, + PriceBreakdown, + getRuPriceBreakdown, + transparentDetailsHeaderStyle } from "../../SettingsRenderUtils"; import { Text, @@ -23,7 +28,9 @@ import { Label, Link, MessageBar, - MessageBarType + MessageBarType, + FontIcon, + IColumn } from "office-ui-fabric-react"; import { ToolTipLabelComponent } from "../ToolTipLabelComponent"; import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils"; @@ -32,7 +39,7 @@ import * as DataModels from "../../../../../Contracts/DataModels"; import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon"; import { userContext } from "../../../../../UserContext"; import { SubscriptionType } from "../../../../../Contracts/SubscriptionType"; -import { usageInGB } from "../../../../../Utils/PricingUtils"; +import { usageInGB, calculateEstimateNumber } from "../../../../../Utils/PricingUtils"; import { Features } from "../../../../../Common/Constants"; export interface ThroughputInputAutoPilotV3Props { @@ -165,33 +172,243 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< return <>; } + const isDirty: boolean = this.IsComponentDirty().isDiscardable; const serverId: string = this.props.serverId; - const offerThroughput: number = this.props.throughput; - const regions = account?.properties?.readLocations?.length || 1; const multimaster = account?.properties?.enableMultipleWriteLocations || false; let estimatedSpend: JSX.Element; if (!this.props.isAutoPilotSelected) { - estimatedSpend = getEstimatedSpendElement( + estimatedSpend = this.getEstimatedManualSpendElement( // if migrating from autoscale to manual, we use the autoscale RUs value as that is what will be set... - this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : offerThroughput, + this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : this.props.throughputBaseline, serverId, regions, - multimaster + multimaster, + isDirty ? this.props.throughput : undefined ); } else { - estimatedSpend = getEstimatedAutoscaleSpendElement( - this.props.maxAutoPilotThroughput, + estimatedSpend = this.getEstimatedAutoscaleSpendElement( + this.props.maxAutoPilotThroughputBaseline, serverId, regions, - multimaster + multimaster, + isDirty ? this.props.maxAutoPilotThroughput : undefined ); } return estimatedSpend; }; + private getEstimatedAutoscaleSpendElement = ( + throughput: number, + serverId: string, + numberOfRegions: number, + isMultimaster: boolean, + newThroughput?: number + ): JSX.Element => { + const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, true); + const estimatedSpendingColumns: IColumn[] = [ + { + key: "costType", + name: "", + fieldName: "costType", + minWidth: 100, + maxWidth: 200, + isResizable: true, + styles: transparentDetailsHeaderStyle + }, + { + key: "minPerMonth", + name: "Min Per Month", + fieldName: "minPerMonth", + minWidth: 100, + maxWidth: 200, + isResizable: true, + styles: transparentDetailsHeaderStyle + }, + { + key: "maxPerMonth", + name: "Max Per Month", + fieldName: "maxPerMonth", + minWidth: 100, + maxWidth: 200, + isResizable: true, + styles: transparentDetailsHeaderStyle + } + ]; + const estimatedSpendingItems: AutoscaleEstimatedSpendingDisplayProps[] = [ + { + costType: Current Cost, + minPerMonth: ( + + {prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)} + + ), + maxPerMonth: ( + + {prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)} + + ) + } + ]; + + if (newThroughput) { + const newPrices: PriceBreakdown = getRuPriceBreakdown( + newThroughput, + serverId, + numberOfRegions, + isMultimaster, + true + ); + estimatedSpendingItems.unshift({ + costType: ( + + Updated Cost + + ), + minPerMonth: ( + + + {newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)} + + + ), + maxPerMonth: ( + + + {newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)} + + + ) + }); + } + + return getEstimatedSpendingElement( + estimatedSpendingColumns, + estimatedSpendingItems, + newThroughput ?? throughput, + numberOfRegions, + prices, + true + ); + }; + + private getEstimatedManualSpendElement = ( + throughput: number, + serverId: string, + numberOfRegions: number, + isMultimaster: boolean, + newThroughput?: number + ): JSX.Element => { + const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, false); + const estimatedSpendingColumns: IColumn[] = [ + { + key: "costType", + name: "", + fieldName: "costType", + minWidth: 100, + maxWidth: 200, + isResizable: true, + styles: transparentDetailsHeaderStyle + }, + { + key: "hourly", + name: "Hourly", + fieldName: "hourly", + minWidth: 100, + maxWidth: 200, + isResizable: true, + styles: transparentDetailsHeaderStyle + }, + { + key: "daily", + name: "Daily", + fieldName: "daily", + minWidth: 100, + maxWidth: 200, + isResizable: true, + styles: transparentDetailsHeaderStyle + }, + { + key: "monthly", + name: "Monthly", + fieldName: "monthly", + minWidth: 100, + maxWidth: 200, + isResizable: true, + styles: transparentDetailsHeaderStyle + } + ]; + const estimatedSpendingItems: ManualEstimatedSpendingDisplayProps[] = [ + { + costType: Current Cost, + hourly: ( + + {prices.currencySign} {calculateEstimateNumber(prices.hourlyPrice)} + + ), + daily: ( + + {prices.currencySign} {calculateEstimateNumber(prices.dailyPrice)} + + ), + monthly: ( + + {prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)} + + ) + } + ]; + + if (newThroughput) { + const newPrices: PriceBreakdown = getRuPriceBreakdown( + newThroughput, + serverId, + numberOfRegions, + isMultimaster, + false + ); + estimatedSpendingItems.unshift({ + costType: ( + + Updated Cost + + ), + hourly: ( + + + {newPrices.currencySign} {calculateEstimateNumber(newPrices.hourlyPrice)} + + + ), + daily: ( + + + {newPrices.currencySign} {calculateEstimateNumber(newPrices.dailyPrice)} + + + ), + monthly: ( + + + {newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)} + + + ) + }); + } + + return getEstimatedSpendingElement( + estimatedSpendingColumns, + estimatedSpendingItems, + newThroughput ?? throughput, + numberOfRegions, + prices, + false + ); + }; + private getAutoPilotUsageCost = (): JSX.Element => { if (!this.props.maxAutoPilotThroughput) { return <>; @@ -318,6 +535,12 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< private renderThroughputInput = (): JSX.Element => ( + + Estimate your required throughput with + + {` capacity calculator`} + + )} +
{this.props.isFixed &&

When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.

}
); + private renderWarningMessage = (): JSX.Element => { + let warningMessage: JSX.Element; + if (this.IsComponentDirty().isDiscardable) { + warningMessage = saveThroughputWarningMessage; + } + + return <>{warningMessage && {warningMessage}}; + }; + public render(): JSX.Element { return ( + {this.renderWarningMessage()} {this.renderThroughputModeChoices()} {this.props.isAutoPilotSelected ? this.renderAutoPilotInput() : this.renderThroughputInput()} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap index 9fe18a4cc..0cd8eb2c2 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap @@ -8,6 +8,21 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = ` } } > + + + Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below before saving your changes + + + + Estimate your required throughput with + + capacity calculator + + + + - - Estimated cost ( - USD - ): - - - $ - 0.0080 - hourly - / - $ - 0.19 - daily - / - $ - 5.84 - monthly + + Current Cost + , + "daily": + $ + + 0.19 + , + "hourly": + $ + + 0.0080 + , + "monthly": + $ + + 5.84 + , + }, + ] + } + layoutMode={1} + onRenderRow={[Function]} + selectionMode={0} + /> + + ( + regions: - - ( - regions: - - 1 - , - 100 - RU/s, - $ - 0.00008 - /RU) - + 1 + , + 100 + RU/s, + $ + 0.00008 + /RU) + + + + *This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account + + + +
`; @@ -369,6 +502,19 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = ` } } > + + Estimate your required throughput with + + capacity calculator + + + + - - Estimated cost ( - USD - ): - - - $ - 0.0080 - hourly - / - $ - 0.19 - daily - / - $ - 5.84 - monthly + + Current Cost + , + "daily": + $ + + 0.19 + , + "hourly": + $ + + 0.0080 + , + "monthly": + $ + + 5.84 + , + }, + ] + } + layoutMode={1} + onRenderRow={[Function]} + selectionMode={0} + /> + + ( + regions: - - ( - regions: - - 1 - , - 100 - RU/s, - $ - 0.00008 - /RU) - + 1 + , + 100 + RU/s, + $ + 0.00008 + /RU) + + + + *This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account + + + +
`; diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap index a13590e36..0efa51f9f 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap @@ -60,66 +60,100 @@ exports[`SettingsUtils functions render 1`] = ` . - - Estimated cost ( - RMB - ): - - - ¥ - 1.02 - hourly - / - ¥ - 24.48 - daily - / - ¥ - 744.60 - monthly + + Current Cost + , + "daily": + $ 24.48 + , + "hourly": + $ 1.02 + , + "monthly": + $ 744.6 + , + }, + ] + } + layoutMode={1} + onRenderRow={[Function]} + selectionMode={0} + /> + + ( + regions: - - ( - regions: - - 2 - , - 1000 - RU/s, - ¥ - 0.00051 - /RU) - - - Estimated monthly cost ( - RMB - ) is - - + 2 + , + 1000 + RU/s, ¥ - 111.69 - - - ¥ - 1116.90 - - - ( - regions: - - 2 - , - 100 - - - 1000 - RU/s, - ¥ - 0.000765 - /RU) - + 0.00051 + /RU) + + + + *This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account + + + { describe("computeRUUsagePriceHourly()", () => { it("should return 0 for NaN regions default cloud", () => { - const value = PricingUtils.computeRUUsagePriceHourly("default", 1, null, false); + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: null, + multimasterEnabled: false, + isAutoscale: false + }); + expect(value).toBe(0); + }); + it("should return 0 for NaN regions default cloud, autoscale", () => { + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: null, + multimasterEnabled: false, + isAutoscale: true + }); expect(value).toBe(0); }); it("should return 0 for -1 regions", () => { - const value = PricingUtils.computeRUUsagePriceHourly("default", 1, -1, false); + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: -1, + multimasterEnabled: false, + isAutoscale: false + }); + expect(value).toBe(0); + }); + it("should return 0 for -1 regions, autoscale", () => { + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: -1, + multimasterEnabled: false, + isAutoscale: true + }); expect(value).toBe(0); }); it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster disabled", () => { - const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 1, false); + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: 1, + multimasterEnabled: false, + isAutoscale: false + }); expect(value).toBe(0.00008); }); + it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster disabled, autoscale", () => { + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: 1, + multimasterEnabled: false, + isAutoscale: true + }); + expect(value).toBe(0.00012); + }); it("should return 0.00051 for Mooncake cloud, 1RU, 1 region, multimaster disabled", () => { - const value = PricingUtils.computeRUUsagePriceHourly("mooncake", 1, 1, false); + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "mooncake", + requestUnits: 1, + numberOfRegions: 1, + multimasterEnabled: false, + isAutoscale: false + }); expect(value).toBe(0.00051); }); + it("should return 0.00051 for Mooncake cloud, 1RU, 1 region, multimaster disabled, autoscale", () => { + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "mooncake", + requestUnits: 1, + numberOfRegions: 1, + multimasterEnabled: false, + isAutoscale: true + }); + expect(value).toBe(0.00076); + }); it("should return 0.00016 for default cloud, 1RU, 2 regions, multimaster disabled", () => { - const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 2, false); + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: 2, + multimasterEnabled: false, + isAutoscale: false + }); expect(value).toBe(0.00016); }); + it("should return 0.00016 for default cloud, 1RU, 2 regions, multimaster disabled, autoscale", () => { + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: 2, + multimasterEnabled: false, + isAutoscale: true + }); + expect(value).toBe(0.00024); + }); it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster enabled", () => { - const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 1, true); + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: 1, + multimasterEnabled: true, + isAutoscale: false + }); expect(value).toBe(0.00008); }); + it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster enabled, autoscale", () => { + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: 1, + multimasterEnabled: true, + isAutoscale: true + }); + expect(value).toBe(0.00012); + }); it("should return 0.00048 for default cloud, 1RU, 2 region, multimaster enabled", () => { - const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 2, true); + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: 2, + multimasterEnabled: true, + isAutoscale: false + }); expect(value).toBe(0.00048); }); + it("should return 0.00048 for default cloud, 1RU, 2 region, multimaster enabled, autoscale", () => { + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: 2, + multimasterEnabled: true, + isAutoscale: true + }); + expect(value).toBe(0.00096); + }); }); describe("getPriceCurrency()", () => { diff --git a/src/Utils/PricingUtils.ts b/src/Utils/PricingUtils.ts index 5b6c4ec0e..af7d1d8d7 100644 --- a/src/Utils/PricingUtils.ts +++ b/src/Utils/PricingUtils.ts @@ -1,6 +1,17 @@ import * as AutoPilotUtils from "../Utils/AutoPilotUtils"; import * as Constants from "../Shared/Constants"; +interface ComputeRUUsagePriceHourlyArgs { + serverId: string; + requestUnits: number; + numberOfRegions: number; + multimasterEnabled: boolean; + isAutoscale: boolean; +} + +export const estimatedCostDisclaimer = + "*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account"; + /** * Anything that is not a number should return 0 * Otherwise, return numberOfRegions @@ -47,15 +58,16 @@ export function getMultimasterMultiplier(numberOfRegions: number, multimasterEna return multimasterMultiplier; } -export function computeRUUsagePriceHourly( - serverId: string, - requestUnits: number, - numberOfRegions: number, - multimasterEnabled: boolean -): number { +export function computeRUUsagePriceHourly({ + serverId, + requestUnits, + numberOfRegions, + multimasterEnabled, + isAutoscale +}: ComputeRUUsagePriceHourlyArgs): number { const regionMultiplier: number = getRegionMultiplier(numberOfRegions, multimasterEnabled); const multimasterMultiplier: number = getMultimasterMultiplier(numberOfRegions, multimasterEnabled); - const pricePerRu = getPricePerRu(serverId); + const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multimasterMultiplier) : getPricePerRu(serverId); const ruCharge = requestUnits * pricePerRu * multimasterMultiplier * regionMultiplier; return Number(ruCharge.toFixed(5)); @@ -159,28 +171,19 @@ export function getAutoPilotV3SpendHtml(maxAutoPilotThroughputSet: number, isDat }' target='_blank' aria-label='Learn more about autoscale throughput'>Learn more.`; } -export function computeAutoscaleUsagePriceHourly( - serverId: string, - requestUnits: number, - numberOfRegions: number, - multimasterEnabled: boolean -): number { - const regionMultiplier: number = getRegionMultiplier(numberOfRegions, multimasterEnabled); - const multimasterMultiplier: number = getMultimasterMultiplier(numberOfRegions, multimasterEnabled); - - const pricePerRu = getAutoscalePricePerRu(serverId, multimasterMultiplier); - const ruCharge = requestUnits * pricePerRu * multimasterMultiplier * regionMultiplier; - - return Number(ruCharge.toFixed(5)); -} - export function getEstimatedAutoscaleSpendHtml( throughput: number, serverId: string, regions: number, multimaster: boolean ): string { - const hourlyPrice: number = computeAutoscaleUsagePriceHourly(serverId, throughput, regions, multimaster); + const hourlyPrice: number = computeRUUsagePriceHourly({ + serverId: serverId, + requestUnits: throughput, + numberOfRegions: regions, + multimasterEnabled: multimaster, + isAutoscale: true + }); const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth; const currency: string = getPriceCurrency(serverId); const currencySign: string = getCurrencySign(serverId); @@ -203,7 +206,13 @@ export function getEstimatedSpendHtml( regions: number, multimaster: boolean ): string { - const hourlyPrice: number = computeRUUsagePriceHourly(serverId, throughput, regions, multimaster); + const hourlyPrice: number = computeRUUsagePriceHourly({ + serverId: serverId, + requestUnits: throughput, + numberOfRegions: regions, + multimasterEnabled: multimaster, + isAutoscale: false + }); const dailyPrice: number = hourlyPrice * 24; const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth; const currency: string = getPriceCurrency(serverId); @@ -217,7 +226,7 @@ export function getEstimatedSpendHtml( `${currencySign}${calculateEstimateNumber(monthlyPrice)} monthly ` + `(${regions} ${regions === 1 ? "region" : "regions"}, ${throughput}RU/s, ${currencySign}${pricePerRu}/RU)` + `

` + - `*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account

` + `${estimatedCostDisclaimer}

` ); } @@ -228,9 +237,13 @@ export function getEstimatedSpendAcknowledgeString( multimaster: boolean, isAutoscale: boolean ): string { - const hourlyPrice: number = isAutoscale - ? computeAutoscaleUsagePriceHourly(serverId, throughput, regions, multimaster) - : computeRUUsagePriceHourly(serverId, throughput, regions, multimaster); + const hourlyPrice: number = computeRUUsagePriceHourly({ + serverId: serverId, + requestUnits: throughput, + numberOfRegions: regions, + multimasterEnabled: multimaster, + isAutoscale: isAutoscale + }); const dailyPrice: number = hourlyPrice * 24; const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth; const currencySign: string = getCurrencySign(serverId);