Fix throughput cost estimate in add collection panel (#1070)

This commit is contained in:
victor-meng 2021-09-15 13:05:55 -07:00 committed by GitHub
parent 2d945c8231
commit 665270296f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 117 additions and 134 deletions

View File

@ -5,7 +5,14 @@ import { Collection } from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import { extractFeatures } from "../Platform/Hosted/extractFeatures"; import { extractFeatures } from "../Platform/Hosted/extractFeatures";
import { updateUserContext } from "../UserContext"; import { updateUserContext } from "../UserContext";
import { deleteDocument, getEndpoint, getFeatureEndpointOrDefault, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient"; import {
deleteDocument,
getEndpoint,
getFeatureEndpointOrDefault,
queryDocuments,
readDocument,
updateDocument,
} from "./MongoProxyClient";
const databaseId = "testDB"; const databaseId = "testDB";
@ -260,11 +267,10 @@ describe("MongoProxyClient", () => {
const features = extractFeatures(params); const features = extractFeatures(params);
updateUserContext({ updateUserContext({
authType: AuthType.AAD, authType: AuthType.AAD,
features: features features: features,
}); });
}); });
it("returns a local endpoint", () => { it("returns a local endpoint", () => {
const endpoint = getFeatureEndpointOrDefault("readDocument"); const endpoint = getFeatureEndpointOrDefault("readDocument");
expect(endpoint).toEqual("https://localhost:12901/api/mongo/explorer"); expect(endpoint).toEqual("https://localhost:12901/api/mongo/explorer");

View File

@ -268,7 +268,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
? documentId.partitionKeyProperty ? documentId.partitionKeyProperty
: "", : "",
}; };
const endpoint = getFeatureEndpointOrDefault("deleteDocument");; const endpoint = getFeatureEndpointOrDefault("deleteDocument");
return window return window
.fetch(`${endpoint}?${queryString.stringify(params)}`, { .fetch(`${endpoint}?${queryString.stringify(params)}`, {
@ -336,11 +336,13 @@ export function createMongoCollectionWithProxy(
} }
export function getFeatureEndpointOrDefault(feature: string): string { export function getFeatureEndpointOrDefault(feature: string): string {
return (hasFlag(userContext.features.mongoProxyAPIs, feature)) ? getEndpoint(userContext.features.mongoProxyEndpoint) : getEndpoint(); return hasFlag(userContext.features.mongoProxyAPIs, feature)
? getEndpoint(userContext.features.mongoProxyEndpoint)
: getEndpoint();
} }
export function getEndpoint(customEndpoint?: string): string { export function getEndpoint(customEndpoint?: string): string {
let url = customEndpoint ? customEndpoint : (configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT); let url = customEndpoint ? customEndpoint : configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
url += "/api/mongo/explorer"; url += "/api/mongo/explorer";
if (userContext.authType === AuthType.EncryptedToken) { if (userContext.authType === AuthType.EncryptedToken) {

View File

@ -1,45 +1,45 @@
import * as React from "react";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants";
import { Urls, StyleConstants } from "../../../Common/Constants";
import { import {
getPriceCurrency, DetailsList,
getCurrencySign, DetailsListLayoutMode,
getAutoscalePricePerRu, DetailsRow,
getMultimasterMultiplier,
computeRUUsagePriceHourly,
getPricePerRu,
estimatedCostDisclaimer,
} from "../../../Utils/PricingUtils";
import {
ITextFieldStyles,
ICheckboxStyles, ICheckboxStyles,
IStackProps,
IStackTokens,
IChoiceGroupStyles, IChoiceGroupStyles,
Link, IColumn,
Text, IDetailsColumnStyles,
IMessageBarStyles,
ITextStyles,
IDetailsRowStyles,
IStackStyles,
IDetailsListStyles, IDetailsListStyles,
IDetailsRowProps,
IDetailsRowStyles,
IDropdownStyles, IDropdownStyles,
IMessageBarStyles,
ISeparatorStyles, ISeparatorStyles,
IStackProps,
IStackStyles,
IStackTokens,
ITextFieldStyles,
ITextStyles,
Link,
MessageBar, MessageBar,
MessageBarType, MessageBarType,
Stack, SelectionMode,
Spinner, Spinner,
SpinnerSize, SpinnerSize,
DetailsList, Stack,
IColumn, Text,
SelectionMode,
DetailsListLayoutMode,
IDetailsRowProps,
DetailsRow,
IDetailsColumnStyles,
} from "@fluentui/react"; } from "@fluentui/react";
import { isDirtyTypes, isDirty } from "./SettingsUtils"; import * as React from "react";
import { StyleConstants, Urls } from "../../../Common/Constants";
import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import {
computeRUUsagePriceHourly,
estimatedCostDisclaimer,
getAutoscalePricePerRu,
getCurrencySign,
getMultimasterMultiplier,
getPriceCurrency,
getPricePerRu,
} from "../../../Utils/PricingUtils";
import { isDirty, isDirtyTypes } from "./SettingsUtils";
export interface EstimatedSpendingDisplayProps { export interface EstimatedSpendingDisplayProps {
costType: JSX.Element; costType: JSX.Element;
@ -223,14 +223,15 @@ export const getRuPriceBreakdown = (
multimasterEnabled: isMultimaster, multimasterEnabled: isMultimaster,
isAutoscale: isAutoscale, isAutoscale: isAutoscale,
}); });
const basePricePerRu: number = isAutoscale const multimasterMultiplier = getMultimasterMultiplier(numberOfRegions, isMultimaster);
? getAutoscalePricePerRu(serverId, getMultimasterMultiplier(numberOfRegions, isMultimaster)) const pricePerRu: number = isAutoscale
: getPricePerRu(serverId); ? getAutoscalePricePerRu(serverId, multimasterMultiplier)
: getPricePerRu(serverId, multimasterMultiplier);
return { return {
hourlyPrice: hourlyPrice, hourlyPrice,
dailyPrice: hourlyPrice * 24, dailyPrice: hourlyPrice * 24,
monthlyPrice: hourlyPrice * hoursInAMonth, monthlyPrice: hourlyPrice * hoursInAMonth,
pricePerRu: basePricePerRu * getMultimasterMultiplier(numberOfRegions, isMultimaster), pricePerRu,
currency: getPriceCurrency(serverId), currency: getPriceCurrency(serverId),
currencySign: getCurrencySign(serverId), currencySign: getCurrencySign(serverId),
}; };

View File

@ -6,6 +6,7 @@ import { userContext } from "../../../../UserContext";
import { import {
calculateEstimateNumber, calculateEstimateNumber,
computeRUUsagePriceHourly, computeRUUsagePriceHourly,
estimatedCostDisclaimer,
getAutoscalePricePerRu, getAutoscalePricePerRu,
getCurrencySign, getCurrencySign,
getMultimasterMultiplier, getMultimasterMultiplier,
@ -42,11 +43,9 @@ export const CostEstimateText: FunctionComponent<CostEstimateTextProps> = ({
const currency: string = getPriceCurrency(serverId); const currency: string = getPriceCurrency(serverId);
const currencySign: string = getCurrencySign(serverId); const currencySign: string = getCurrencySign(serverId);
const multiplier = getMultimasterMultiplier(numberOfRegions, multimasterEnabled); const multiplier = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
const pricePerRu = isAutoscale const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multiplier) : getPricePerRu(serverId, multiplier);
? getAutoscalePricePerRu(serverId, multiplier) * multiplier
: getPricePerRu(serverId) * multiplier;
const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>PricingUtils.estimatedCostDisclaimer</InfoTooltip>; const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>{estimatedCostDisclaimer}</InfoTooltip>;
if (isAutoscale) { if (isAutoscale) {
return ( return (

View File

@ -31,8 +31,8 @@ describe("hasFlag", () => {
expect(hasFlag(singleFlagValue, desiredFlag)).toBe(true); expect(hasFlag(singleFlagValue, desiredFlag)).toBe(true);
expect(hasFlag(multipleFlagValues, desiredFlag)).toBe(true); expect(hasFlag(multipleFlagValues, desiredFlag)).toBe(true);
expect(hasFlag(differentFlagValue, desiredFlag)).toBe(false); expect(hasFlag(differentFlagValue, desiredFlag)).toBe(false);
expect(hasFlag(multipleFlagValues, undefined as unknown as string)).toBe(false); expect(hasFlag(multipleFlagValues, (undefined as unknown) as string)).toBe(false);
expect(hasFlag(undefined as unknown as string, desiredFlag)).toBe(false); expect(hasFlag((undefined as unknown) as string, desiredFlag)).toBe(false);
expect(hasFlag(undefined as unknown as string, undefined as unknown as string)).toBe(false); expect(hasFlag((undefined as unknown) as string, (undefined as unknown) as string)).toBe(false);
}); });
}); });

View File

@ -125,7 +125,8 @@ export class OfferPricing {
S3Price: 0.1344, S3Price: 0.1344,
Standard: { Standard: {
StartingPrice: 24 / hoursInAMonth, // per hour StartingPrice: 24 / hoursInAMonth, // per hour
PricePerRU: 0.00008, SingleMasterPricePerRU: 0.00008,
MultiMasterPricePerRU: 0.00016,
PricePerGB: 0.25 / hoursInAMonth, PricePerGB: 0.25 / hoursInAMonth,
}, },
}, },
@ -137,7 +138,8 @@ export class OfferPricing {
S3Price: 0.6, S3Price: 0.6,
Standard: { Standard: {
StartingPrice: OfferPricing.MonthlyPricing.mooncake.Standard.StartingPrice / hoursInAMonth, // per hour StartingPrice: OfferPricing.MonthlyPricing.mooncake.Standard.StartingPrice / hoursInAMonth, // per hour
PricePerRU: 0.00051, SingleMasterPricePerRU: 0.00051,
MultiMasterPricePerRU: 0.00102,
PricePerGB: OfferPricing.MonthlyPricing.mooncake.Standard.PricePerGB / hoursInAMonth, PricePerGB: OfferPricing.MonthlyPricing.mooncake.Standard.PricePerGB / hoursInAMonth,
}, },
}, },

View File

@ -2,11 +2,11 @@ import * as Constants from "./Constants";
export function computeRUUsagePrice(serverId: string, requestUnits: number): string { export function computeRUUsagePrice(serverId: string, requestUnits: number): string {
if (serverId === "mooncake") { if (serverId === "mooncake") {
const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRU; const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.SingleMasterPricePerRU;
return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency; return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency;
} }
const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU; const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.SingleMasterPricePerRU;
return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency; return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency;
} }

View File

@ -150,7 +150,7 @@ describe("PricingUtils Tests", () => {
expect(value).toBe(0.00012); expect(value).toBe(0.00012);
}); });
it("should return 0.00048 for default cloud, 1RU, 2 region, multimaster enabled", () => { it("should return 0.00032 for default cloud, 1RU, 2 region, multimaster enabled", () => {
const value = PricingUtils.computeRUUsagePriceHourly({ const value = PricingUtils.computeRUUsagePriceHourly({
serverId: "default", serverId: "default",
requestUnits: 1, requestUnits: 1,
@ -158,9 +158,9 @@ describe("PricingUtils Tests", () => {
multimasterEnabled: true, multimasterEnabled: true,
isAutoscale: false, isAutoscale: false,
}); });
expect(value).toBe(0.00048); expect(value).toBe(0.00032);
}); });
it("should return 0.00048 for default cloud, 1RU, 2 region, multimaster enabled, autoscale", () => { it("should return 0.00032 for default cloud, 1RU, 2 region, multimaster enabled, autoscale", () => {
const value = PricingUtils.computeRUUsagePriceHourly({ const value = PricingUtils.computeRUUsagePriceHourly({
serverId: "default", serverId: "default",
requestUnits: 1, requestUnits: 1,
@ -168,7 +168,7 @@ describe("PricingUtils Tests", () => {
multimasterEnabled: true, multimasterEnabled: true,
isAutoscale: true, isAutoscale: true,
}); });
expect(value).toBe(0.00096); expect(value).toBe(0.00032);
}); });
}); });
@ -251,70 +251,47 @@ describe("PricingUtils Tests", () => {
}); });
describe("getPricePerRu()", () => { describe("getPricePerRu()", () => {
it("should return 0.00008 for default clouds", () => { it("should return 0.00008 for single master default clouds", () => {
const value = PricingUtils.getPricePerRu("default"); const value = PricingUtils.getPricePerRu("default", 1);
expect(value).toBe(0.00008); expect(value).toBe(0.00008);
}); });
it("should return 0.00051 for mooncake", () => { it("should return 0.00016 for multi master default clouds", () => {
const value = PricingUtils.getPricePerRu("mooncake"); const value = PricingUtils.getPricePerRu("default", 2);
expect(value).toBe(0.00016);
});
it("should return 0.00051 for single master mooncake", () => {
const value = PricingUtils.getPricePerRu("mooncake", 1);
expect(value).toBe(0.00051); expect(value).toBe(0.00051);
}); });
it("should return 0.00102 for multi master mooncake", () => {
const value = PricingUtils.getPricePerRu("mooncake", 2);
expect(value).toBe(0.00102);
});
}); });
describe("getRegionMultiplier()", () => { describe("getRegionMultiplier()", () => {
describe("without multimaster", () => { it("should return 0 for undefined", () => {
it("should return 0 for undefined", () => { const value = PricingUtils.getRegionMultiplier(undefined);
const value = PricingUtils.getRegionMultiplier(undefined, false); expect(value).toBe(0);
expect(value).toBe(0);
});
it("should return 0 for -1", () => {
const value = PricingUtils.getRegionMultiplier(-1, false);
expect(value).toBe(0);
});
it("should return 0 for 0", () => {
const value = PricingUtils.getRegionMultiplier(0, false);
expect(value).toBe(0);
});
it("should return 1 for 1", () => {
const value = PricingUtils.getRegionMultiplier(1, false);
expect(value).toBe(1);
});
it("should return 2 for 2", () => {
const value = PricingUtils.getRegionMultiplier(2, false);
expect(value).toBe(2);
});
}); });
it("should return 0 for -1", () => {
describe("with multimaster", () => { const value = PricingUtils.getRegionMultiplier(-1);
it("should return 0 for undefined", () => { expect(value).toBe(0);
const value = PricingUtils.getRegionMultiplier(undefined, true); });
expect(value).toBe(0); it("should return 0 for 0", () => {
}); const value = PricingUtils.getRegionMultiplier(0);
expect(value).toBe(0);
it("should return 0 for -1", () => { });
const value = PricingUtils.getRegionMultiplier(-1, true); it("should return 1 for 1", () => {
expect(value).toBe(0); const value = PricingUtils.getRegionMultiplier(1);
}); expect(value).toBe(1);
});
it("should return 0 for 0", () => { it("should return 2 for 2", () => {
const value = PricingUtils.getRegionMultiplier(0, true); const value = PricingUtils.getRegionMultiplier(2);
expect(value).toBe(0); expect(value).toBe(2);
});
it("should return 1 for 1", () => {
const value = PricingUtils.getRegionMultiplier(1, true);
expect(value).toBe(1);
});
it("should return 3 for 2", () => {
const value = PricingUtils.getRegionMultiplier(2, true);
expect(value).toBe(3);
});
}); });
}); });
@ -376,7 +353,7 @@ describe("PricingUtils Tests", () => {
true /* multimaster */ true /* multimaster */
); );
expect(value).toBe( expect(value).toBe(
"Cost (USD): <b>$0.19 hourly / $4.61 daily / $140.16 monthly </b> (2 regions, 400RU/s, $0.00016/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>" "Cost (USD): <b>$0.13 hourly / $3.07 daily / $93.44 monthly </b> (2 regions, 400RU/s, $0.00016/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>"
); );
}); });
@ -424,7 +401,7 @@ describe("PricingUtils Tests", () => {
true /* multimaster */, true /* multimaster */,
false false
); );
expect(value).toBe("I acknowledge the estimated $4.61 daily cost for the throughput above."); expect(value).toBe("I acknowledge the estimated $3.07 daily cost for the throughput above.");
}); });
it("should return 'I acknowledge the estimated $1.54 daily cost for the throughput above.' for 400RU/s on default cloud, 2 region, without multimaster", () => { it("should return 'I acknowledge the estimated $1.54 daily cost for the throughput above.' for 400RU/s on default cloud, 2 region, without multimaster", () => {

View File

@ -34,26 +34,18 @@ export function getRuToolTipText(): string {
* Otherwise, return numberOfRegions * Otherwise, return numberOfRegions
* @param numberOfRegions * @param numberOfRegions
*/ */
export function getRegionMultiplier(numberOfRegions: number, multimasterEnabled: boolean): number { export function getRegionMultiplier(numberOfRegions: number): number {
const normalizedNumberOfRegions: number = normalizeNumber(numberOfRegions); const normalizedNumberOfRegions: number = normalizeNumber(numberOfRegions);
if (normalizedNumberOfRegions <= 0) { if (normalizedNumberOfRegions <= 0) {
return 0; return 0;
} }
if (numberOfRegions === 1) {
return numberOfRegions;
}
if (multimasterEnabled) {
return numberOfRegions + 1;
}
return numberOfRegions; return numberOfRegions;
} }
export function getMultimasterMultiplier(numberOfRegions: number, multimasterEnabled: boolean): number { export function getMultimasterMultiplier(numberOfRegions: number, multimasterEnabled: boolean): number {
const regionMultiplier: number = getRegionMultiplier(numberOfRegions, multimasterEnabled); const regionMultiplier: number = getRegionMultiplier(numberOfRegions);
const multimasterMultiplier: number = !multimasterEnabled ? 1 : regionMultiplier > 1 ? 2 : 1; const multimasterMultiplier: number = !multimasterEnabled ? 1 : regionMultiplier > 1 ? 2 : 1;
return multimasterMultiplier; return multimasterMultiplier;
@ -66,10 +58,12 @@ export function computeRUUsagePriceHourly({
multimasterEnabled, multimasterEnabled,
isAutoscale, isAutoscale,
}: ComputeRUUsagePriceHourlyArgs): number { }: ComputeRUUsagePriceHourlyArgs): number {
const regionMultiplier: number = getRegionMultiplier(numberOfRegions, multimasterEnabled); const regionMultiplier: number = getRegionMultiplier(numberOfRegions);
const multimasterMultiplier: number = getMultimasterMultiplier(numberOfRegions, multimasterEnabled); const multimasterMultiplier: number = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multimasterMultiplier) : getPricePerRu(serverId); const pricePerRu = isAutoscale
const ruCharge = requestUnits * pricePerRu * multimasterMultiplier * regionMultiplier; ? getAutoscalePricePerRu(serverId, multimasterMultiplier)
: getPricePerRu(serverId, multimasterMultiplier);
const ruCharge = requestUnits * pricePerRu * regionMultiplier;
return Number(ruCharge.toFixed(5)); return Number(ruCharge.toFixed(5));
} }
@ -149,12 +143,16 @@ export function getAutoscalePricePerRu(serverId: string, mmMultiplier: number):
} }
} }
export function getPricePerRu(serverId: string): number { export function getPricePerRu(serverId: string, mmMultiplier: number): number {
if (serverId === "mooncake") { if (serverId === "mooncake") {
return Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRU; return mmMultiplier > 1
? Constants.OfferPricing.HourlyPricing.mooncake.Standard.MultiMasterPricePerRU
: Constants.OfferPricing.HourlyPricing.mooncake.Standard.SingleMasterPricePerRU;
} }
return Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU; return mmMultiplier > 1
? Constants.OfferPricing.HourlyPricing.default.Standard.MultiMasterPricePerRU
: Constants.OfferPricing.HourlyPricing.default.Standard.SingleMasterPricePerRU;
} }
export function getAutoPilotV3SpendHtml(maxAutoPilotThroughputSet: number, isDatabaseThroughput: boolean): string { export function getAutoPilotV3SpendHtml(maxAutoPilotThroughputSet: number, isDatabaseThroughput: boolean): string {
@ -188,9 +186,7 @@ export function getEstimatedAutoscaleSpendHtml(
const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth; const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth;
const currency: string = getPriceCurrency(serverId); const currency: string = getPriceCurrency(serverId);
const currencySign: string = getCurrencySign(serverId); const currencySign: string = getCurrencySign(serverId);
const pricePerRu = const pricePerRu = getAutoscalePricePerRu(serverId, getMultimasterMultiplier(regions, multimaster));
getAutoscalePricePerRu(serverId, getMultimasterMultiplier(regions, multimaster)) *
getMultimasterMultiplier(regions, multimaster);
return ( return (
`Estimated monthly cost (${currency}): <b>` + `Estimated monthly cost (${currency}): <b>` +
@ -219,7 +215,7 @@ export function getEstimatedSpendHtml(
const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth; const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth;
const currency: string = getPriceCurrency(serverId); const currency: string = getPriceCurrency(serverId);
const currencySign: string = getCurrencySign(serverId); const currencySign: string = getCurrencySign(serverId);
const pricePerRu = getPricePerRu(serverId) * getMultimasterMultiplier(regions, multimaster); const pricePerRu = getPricePerRu(serverId, getMultimasterMultiplier(regions, multimaster));
return ( return (
`Cost (${currency}): <b>` + `Cost (${currency}): <b>` +