diff --git a/.eslintignore b/.eslintignore index b37c00904..49fefb80d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -14,7 +14,6 @@ src/Common/DataAccessUtilityBase.ts src/Common/DeleteFeedback.ts src/Common/DocumentClientUtilityBase.ts src/Common/EditableUtility.ts -src/Common/EnvironmentUtility.ts src/Common/HashMap.test.ts src/Common/HashMap.ts src/Common/HeadersUtility.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d48c70a62..440c65185 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,6 +101,7 @@ jobs: PLATFORM: "Emulator" NODE_TLS_REJECT_UNAUTHORIZED: 0 - uses: actions/upload-artifact@v2 + if: failure() with: name: screenshots path: failed-* @@ -159,13 +160,14 @@ jobs: TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }} DATA_EXPLORER_ENDPOINT: "https://localhost:1234/hostedExplorer.html" - uses: actions/upload-artifact@v2 + if: failure() with: name: screenshots path: failed-* nuget: name: Publish Nuget if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') - needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted] + needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted, accessibility] runs-on: ubuntu-latest env: NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} @@ -189,7 +191,7 @@ jobs: nugetmpac: name: Publish Nuget MPAC if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') - needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted] + needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted, accessibility] runs-on: ubuntu-latest env: NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} @@ -211,3 +213,28 @@ jobs: name: packages with: path: "*.nupkg" + nugetie: + name: Publish Nuget IE + if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') + needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted, accessibility] + runs-on: ubuntu-latest + env: + NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} + AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }} + steps: + - uses: nuget/setup-nuget@v1 + with: + nuget-api-key: ${{ secrets.NUGET_API_KEY }} + - name: Download Dist Folder + uses: actions/download-artifact@v2 + with: + name: dist + - run: cp ./configs/prod.json config.json + - run: sed -i 's/Azure.Cosmos.DB.Data.Explorer/Azure.Cosmos.DB.Data.Explorer.IE/g' DataExplorer.nuspec + - run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "GitHub" -Password "$AZURE_DEVOPS_PAT" + - run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}" + - run: nuget push -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg + - uses: actions/upload-artifact@v2 + name: packages + with: + path: "*.nupkg" 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/README.md b/README.md index 316975238..9057742d5 100644 --- a/README.md +++ b/README.md @@ -13,29 +13,18 @@ UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), ht ### Watch mode -Run `npm run watch` to start the development server and automatically rebuild on changes +Run `npm start` to start the development server and automatically rebuild on changes -### Specifying Development Platform +### Hosted Development (https://cosmos.azure.com) -Setting the environment variable `PLATFORM` during the build process will force the explorer to load the specified platform. By default in development it will run in `Hosted` mode. Valid options: - -- Hosted -- Emulator -- Portal - -`PLATFORM=Emulator npm run watch` - -### Hosted Development - -The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine. - -To run pure hosted mode, in `webpack.config.js` change index HtmlWebpackPlugin to use hostedExplorer.html and change entry for index to use HostedExplorer.ts. +- Visit: `https://localhost:1234/hostedExplorer.html` +- Local sign in via AAD will NOT work. Connection string only in dev mode. Use the Portal if you need AAD auth. +- The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine. ### Emulator Development -In a window environment, running `npm run build` will automatically copy the built files from `/dist` over to the default emulator install paths. In a non-windows environment you can specify an alternate endpoint using `EMULATOR_ENDPOINT` and webpack dev server will proxy requests for you. - -`PLATFORM=Emulator EMULATOR_ENDPOINT=https://my-vm.azure.com:8081 npm run watch` +- Start the Cosmos Emulator +- Visit: https://localhost:1234/index.html #### Setting up a Remote Emulator @@ -55,16 +44,8 @@ The Cosmos emulator currently only runs in Windows environments. You can still d ### Portal Development -The Cosmos Portal that consumes this repo is not currently open source. If you have access to this project, `npm run build` will copy the built files over to the portal where they will be loaded by the portal development environment - -You can however load a local running instance of data explorer in the production portal. - -1. Turn off browser SSL validation for localhost: chrome://flags/#allow-insecure-localhost OR Install valid SSL certs for localhost (on IE, follow these [instructions](https://www.technipages.com/ie-bypass-problem-with-this-websites-security-certificate) to install the localhost certificate in the right place) -2. Allowlist `https://localhost:1234` domain for CORS in the Azure Cosmos DB portal -3. Start the project in portal mode: `PLATFORM=Portal npm run watch` -4. Load the portal using the following link: https://ms.portal.azure.com/?dataExplorerSource=https%3A%2F%2Flocalhost%3A1234%2Fexplorer.html - -Live reload will occur, but data explorer will not properly integrate again with the parent iframe. You will have to manually reload the page. +- Visit: https://ms.portal.azure.com/?dataExplorerSource=https%3A%2F%2Flocalhost%3A1234%2Fexplorer.html +- You may have to manually visit https://localhost:1234/explorer.html first and click through any SSL certificate warnings ### Testing diff --git a/src/Common/EnvironmentUtility.ts b/src/Common/EnvironmentUtility.ts index acb371359..0a4516514 100644 --- a/src/Common/EnvironmentUtility.ts +++ b/src/Common/EnvironmentUtility.ts @@ -1,8 +1,6 @@ -export default class EnvironmentUtility { - public static normalizeArmEndpointUri(uri: string): string { - if (uri && uri.slice(-1) !== "/") { - return `${uri}/`; - } - return uri; +export function normalizeArmEndpoint(uri: string): string { + if (uri && uri.slice(-1) !== "/") { + return `${uri}/`; } + return uri; } diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index a023f2fc4..c98243586 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -362,7 +362,7 @@ export enum CollectionTabKind { Gallery = 17, NotebookViewer = 18, Schema = 19, - SettingsV2 = 19 + SettingsV2 = 20 } export enum TerminalKind { 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__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index bc0e13735..aef4316e1 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -731,7 +731,6 @@ exports[`SettingsComponent renders 1`] = ` "visible": [Function], }, "arcadiaToken": [Function], - "armEndpoint": [Function], "browseQueriesPane": BrowseQueriesPane { "canSaveQueries": [Function], "container": [Circular], @@ -1023,7 +1022,6 @@ exports[`SettingsComponent renders 1`] = ` "onRefreshResourcesClick": [Function], "onSwitchToConnectionString": [Function], "onToggleKeyDown": [Function], - "parentFrameDataExplorerVersion": [Function], "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], @@ -1049,7 +1047,6 @@ exports[`SettingsComponent renders 1`] = ` "titleLabel": "Select Columns", "visible": [Function], }, - "quotaId": [Function], "refreshDatabaseAccount": [Function], "refreshNotebookList": [Function], "refreshTreeTitle": [Function], @@ -2005,7 +2002,6 @@ exports[`SettingsComponent renders 1`] = ` "visible": [Function], }, "arcadiaToken": [Function], - "armEndpoint": [Function], "browseQueriesPane": BrowseQueriesPane { "canSaveQueries": [Function], "container": [Circular], @@ -2297,7 +2293,6 @@ exports[`SettingsComponent renders 1`] = ` "onRefreshResourcesClick": [Function], "onSwitchToConnectionString": [Function], "onToggleKeyDown": [Function], - "parentFrameDataExplorerVersion": [Function], "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], @@ -2323,7 +2318,6 @@ exports[`SettingsComponent renders 1`] = ` "titleLabel": "Select Columns", "visible": [Function], }, - "quotaId": [Function], "refreshDatabaseAccount": [Function], "refreshNotebookList": [Function], "refreshTreeTitle": [Function], @@ -3292,7 +3286,6 @@ exports[`SettingsComponent renders 1`] = ` "visible": [Function], }, "arcadiaToken": [Function], - "armEndpoint": [Function], "browseQueriesPane": BrowseQueriesPane { "canSaveQueries": [Function], "container": [Circular], @@ -3584,7 +3577,6 @@ exports[`SettingsComponent renders 1`] = ` "onRefreshResourcesClick": [Function], "onSwitchToConnectionString": [Function], "onToggleKeyDown": [Function], - "parentFrameDataExplorerVersion": [Function], "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], @@ -3610,7 +3602,6 @@ exports[`SettingsComponent renders 1`] = ` "titleLabel": "Select Columns", "visible": [Function], }, - "quotaId": [Function], "refreshDatabaseAccount": [Function], "refreshNotebookList": [Function], "refreshTreeTitle": [Function], @@ -4566,7 +4557,6 @@ exports[`SettingsComponent renders 1`] = ` "visible": [Function], }, "arcadiaToken": [Function], - "armEndpoint": [Function], "browseQueriesPane": BrowseQueriesPane { "canSaveQueries": [Function], "container": [Circular], @@ -4858,7 +4848,6 @@ exports[`SettingsComponent renders 1`] = ` "onRefreshResourcesClick": [Function], "onSwitchToConnectionString": [Function], "onToggleKeyDown": [Function], - "parentFrameDataExplorerVersion": [Function], "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], @@ -4884,7 +4873,6 @@ exports[`SettingsComponent renders 1`] = ` "titleLabel": "Select Columns", "visible": [Function], }, - "quotaId": [Function], "refreshDatabaseAccount": [Function], "refreshNotebookList": [Function], "refreshTreeTitle": [Function], 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 + + + ; public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults; public subscriptionType: ko.Observable; - public quotaId: ko.Observable; public defaultExperience: ko.Observable; public isPreferredApiDocumentDB: ko.Computed; public isPreferredApiCassandra: ko.Computed; @@ -135,12 +134,10 @@ export default class Explorer { public canSaveQueries: ko.Computed; public features: ko.Observable; public serverId: ko.Observable; - public armEndpoint: ko.Observable; public isTryCosmosDBSubscription: ko.Observable; public queriesClient: QueriesClient; public tableDataClient: TableDataClient; public splitter: Splitter; - public parentFrameDataExplorerVersion: ko.Observable = ko.observable(""); public mostRecentActivity: MostRecentActivity.MostRecentActivity; // Notification Console @@ -278,7 +275,6 @@ export default class Explorer { this.databaseAccount = ko.observable(); this.subscriptionType = ko.observable(SharedConstants.CollectionCreation.DefaultSubscriptionType); - this.quotaId = ko.observable(""); let firstInitialization = true; this.isRefreshingExplorer = ko.observable(true); this.isRefreshingExplorer.subscribe((isRefreshing: boolean) => { @@ -318,9 +314,9 @@ export default class Explorer { if (isAccountReady) { this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(true); RouteHandler.getInstance().initHandler(); - this.notebookWorkspaceManager = new NotebookWorkspaceManager(this.armEndpoint()); + this.notebookWorkspaceManager = new NotebookWorkspaceManager(); this.arcadiaWorkspaces = ko.observableArray(); - this._arcadiaManager = new ArcadiaResourceManager(this.armEndpoint()); + this._arcadiaManager = new ArcadiaResourceManager(); this._isAfecFeatureRegistered(Constants.AfecFeatures.StorageAnalytics).then(isRegistered => this.hasStorageAnalyticsAfecFeature(isRegistered) ); @@ -370,7 +366,6 @@ export default class Explorer { this.features = ko.observable(); this.serverId = ko.observable(); - this.armEndpoint = ko.observable(undefined); this.queriesClient = new QueriesClient(this); this.isTryCosmosDBSubscription = ko.observable(false); @@ -1012,9 +1007,7 @@ export default class Explorer { this.isSynapseLinkUpdating(true); this._closeSynapseLinkModalDialog(); - const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate( - this.databaseAccount().id - ); + const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(this.databaseAccount().id); try { const databaseAccount: DataModels.DatabaseAccount = await resourceProviderClient.patchAsync( @@ -1754,61 +1747,59 @@ export default class Explorer { inputs.extensionEndpoint = configContext.PROXY_PATH; } - const initPromise: Q.Promise = inputs ? this.initDataExplorerWithFrameInputs(inputs) : Q(); + this.initDataExplorerWithFrameInputs(inputs); - initPromise.then(() => { - const openAction: ActionContracts.DataExplorerAction = message.openAction; - if (!!openAction) { - if (this.isRefreshingExplorer()) { - const subscription = this.databases.subscribe((databases: ViewModels.Database[]) => { - handleOpenAction(openAction, this.nonSystemDatabases(), this); - subscription.dispose(); - }); - } else { + const openAction: ActionContracts.DataExplorerAction = message.openAction; + if (!!openAction) { + if (this.isRefreshingExplorer()) { + const subscription = this.databases.subscribe((databases: ViewModels.Database[]) => { handleOpenAction(openAction, this.nonSystemDatabases(), this); - } + subscription.dispose(); + }); + } else { + handleOpenAction(openAction, this.nonSystemDatabases(), this); } - if (message.actionType === ActionContracts.ActionType.TransmitCachedData) { - handleCachedDataMessage(message); - return; - } - if (message.type) { - switch (message.type) { - case MessageTypes.UpdateLocationHash: - if (!message.locationHash) { - break; - } - hasher.replaceHash(message.locationHash); - RouteHandler.getInstance().parseHash(message.locationHash); - break; - case MessageTypes.SendNotification: - if (!message.message) { - break; - } - NotificationConsoleUtils.logConsoleMessage( - message.consoleDataType || ConsoleDataType.Info, - message.message, - message.id - ); - break; - case MessageTypes.ClearNotification: - if (!message.id) { - break; - } - NotificationConsoleUtils.clearInProgressMessageWithId(message.id); - break; - case MessageTypes.LoadingStatus: - if (!message.text) { - break; - } - this._setLoadingStatusText(message.text, message.title); - break; - } - return; + } + if (message.actionType === ActionContracts.ActionType.TransmitCachedData) { + handleCachedDataMessage(message); + return; + } + if (message.type) { + switch (message.type) { + case MessageTypes.UpdateLocationHash: + if (!message.locationHash) { + break; + } + hasher.replaceHash(message.locationHash); + RouteHandler.getInstance().parseHash(message.locationHash); + break; + case MessageTypes.SendNotification: + if (!message.message) { + break; + } + NotificationConsoleUtils.logConsoleMessage( + message.consoleDataType || ConsoleDataType.Info, + message.message, + message.id + ); + break; + case MessageTypes.ClearNotification: + if (!message.id) { + break; + } + NotificationConsoleUtils.clearInProgressMessageWithId(message.id); + break; + case MessageTypes.LoadingStatus: + if (!message.text) { + break; + } + this._setLoadingStatusText(message.text, message.title); + break; } + return; + } - this.splashScreenAdapter.forceRender(); - }); + this.splashScreenAdapter.forceRender(); } public findSelectedDatabase(): ViewModels.Database { @@ -1848,8 +1839,14 @@ export default class Explorer { return false; } - public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): Q.Promise { + public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): void { if (inputs != null) { + // In development mode, save the iframe message from the portal in session storage. + // This allows webpack hot reload to funciton properly + if (process.env.NODE_ENV === "development") { + sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs)); + } + const authorizationToken = inputs.authorizationToken || ""; const masterKey = inputs.masterKey || ""; const databaseAccount = inputs.databaseAccount || null; @@ -1858,25 +1855,18 @@ export default class Explorer { } this.features(inputs.features); this.serverId(inputs.serverId); - this.armEndpoint(EnvironmentUtility.normalizeArmEndpointUri(inputs.csmEndpoint || configContext.ARM_ENDPOINT)); this.databaseAccount(databaseAccount); this.subscriptionType(inputs.subscriptionType); - this.quotaId(inputs.quotaId); this.hasWriteAccess(inputs.hasWriteAccess); this.flight(inputs.addCollectionDefaultFlight); this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription); this.isAuthWithResourceToken(inputs.isAuthWithresourceToken); this.setFeatureFlagsFromFlights(inputs.flights); - - if (!!inputs.dataExplorerVersion) { - this.parentFrameDataExplorerVersion(inputs.dataExplorerVersion); - } - this._importExplorerConfigComplete = true; updateConfigContext({ BACKEND_ENDPOINT: inputs.extensionEndpoint || "", - ARM_ENDPOINT: this.armEndpoint() + ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT) }); updateUserContext({ @@ -1885,7 +1875,8 @@ export default class Explorer { databaseAccount, resourceGroup: inputs.resourceGroup, subscriptionId: inputs.subscriptionId, - subscriptionType: inputs.subscriptionType + subscriptionType: inputs.subscriptionType, + quotaId: inputs.quotaId }); TelemetryProcessor.traceSuccess( Action.LoadDatabaseAccount, @@ -1899,7 +1890,6 @@ export default class Explorer { this.isAccountReady(true); } - return Q(); } public setFeatureFlagsFromFlights(flights: readonly string[]): void { @@ -2562,7 +2552,7 @@ export default class Explorer { public _refreshSparkEnabledStateForAccount = async (): Promise => { const subscriptionId = userContext.subscriptionId; - const armEndpoint = this.armEndpoint(); + const armEndpoint = configContext.ARM_ENDPOINT; const authType = window.authType as AuthType; if (!subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) { // explorer is not aware of the database account yet @@ -2571,7 +2561,7 @@ export default class Explorer { } const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${Constants.AfecFeatures.Spark}`; - const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate(featureUri); + const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri); try { const sparkNotebooksFeature: DataModels.AfecFeature = await resourceProviderClient.getAsync( featureUri, @@ -2591,7 +2581,7 @@ export default class Explorer { public _isAfecFeatureRegistered = async (featureName: string): Promise => { const subscriptionId = userContext.subscriptionId; - const armEndpoint = this.armEndpoint(); + const armEndpoint = configContext.ARM_ENDPOINT; const authType = window.authType as AuthType; if (!featureName || !subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) { // explorer is not aware of the database account yet @@ -2599,7 +2589,7 @@ export default class Explorer { } const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${featureName}`; - const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate(featureUri); + const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri); try { const featureStatus: DataModels.AfecFeature = await resourceProviderClient.getAsync( featureUri, diff --git a/src/Explorer/Panes/AddCollectionPane.ts b/src/Explorer/Panes/AddCollectionPane.ts index cf37da3ca..08bd372f1 100644 --- a/src/Explorer/Panes/AddCollectionPane.ts +++ b/src/Explorer/Panes/AddCollectionPane.ts @@ -16,6 +16,7 @@ import { ContextualPaneBase } from "./ContextualPaneBase"; import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent"; import { createCollection } from "../../Common/dataAccess/createCollection"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; +import { userContext } from "../../UserContext"; export interface AddCollectionPaneOptions extends ViewModels.PaneOptions { isPreferredApiTable: ko.Computed; @@ -668,7 +669,7 @@ export default class AddCollectionPane extends ContextualPaneBase { databaseId: this.databaseId() }), subscriptionType: SubscriptionType[this.container.subscriptionType()], - subscriptionQuotaId: this.container.quotaId(), + subscriptionQuotaId: userContext.quotaId, defaultsCheck: { storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u", throughput: this._getThroughput(), @@ -770,7 +771,7 @@ export default class AddCollectionPane extends ContextualPaneBase { collectionWithThroughputInShared: this.collectionWithThroughputInShared() }), subscriptionType: SubscriptionType[this.container.subscriptionType()], - subscriptionQuotaId: this.container.quotaId(), + subscriptionQuotaId: userContext.quotaId, defaultsCheck: { storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u", throughput: offerThroughput, @@ -844,7 +845,7 @@ export default class AddCollectionPane extends ContextualPaneBase { collectionWithThroughputInShared: this.collectionWithThroughputInShared() }), subscriptionType: SubscriptionType[this.container.subscriptionType()], - subscriptionQuotaId: this.container.quotaId(), + subscriptionQuotaId: userContext.quotaId, defaultsCheck: { storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u", throughput: offerThroughput, @@ -878,7 +879,7 @@ export default class AddCollectionPane extends ContextualPaneBase { collectionWithThroughputInShared: this.collectionWithThroughputInShared() }, subscriptionType: SubscriptionType[this.container.subscriptionType()], - subscriptionQuotaId: this.container.quotaId(), + subscriptionQuotaId: userContext.quotaId, defaultsCheck: { storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u", throughput: offerThroughput, diff --git a/src/Explorer/Panes/AddDatabasePane.ts b/src/Explorer/Panes/AddDatabasePane.ts index 5f198d9a2..16f800923 100644 --- a/src/Explorer/Panes/AddDatabasePane.ts +++ b/src/Explorer/Panes/AddDatabasePane.ts @@ -13,6 +13,7 @@ import { createDatabase } from "../../Common/dataAccess/createDatabase"; import { configContext, Platform } from "../../ConfigContext"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { SubscriptionType } from "../../Contracts/SubscriptionType"; +import { userContext } from "../../UserContext"; export default class AddDatabasePane extends ContextualPaneBase { public defaultExperience: ko.Computed; @@ -250,7 +251,7 @@ export default class AddDatabasePane extends ContextualPaneBase { databaseAccountName: this.container.databaseAccount().name, defaultExperience: this.container.defaultExperience(), subscriptionType: SubscriptionType[this.container.subscriptionType()], - subscriptionQuotaId: this.container.quotaId(), + subscriptionQuotaId: userContext.quotaId, defaultsCheck: { throughput: this.throughput(), flight: this.container.flight() @@ -278,7 +279,7 @@ export default class AddDatabasePane extends ContextualPaneBase { }), offerThroughput, subscriptionType: SubscriptionType[this.container.subscriptionType()], - subscriptionQuotaId: this.container.quotaId(), + subscriptionQuotaId: userContext.quotaId, defaultsCheck: { flight: this.container.flight() }, @@ -342,7 +343,7 @@ export default class AddDatabasePane extends ContextualPaneBase { }), offerThroughput: offerThroughput, subscriptionType: SubscriptionType[this.container.subscriptionType()], - subscriptionQuotaId: this.container.quotaId(), + subscriptionQuotaId: userContext.quotaId, defaultsCheck: { flight: this.container.flight() }, @@ -366,7 +367,7 @@ export default class AddDatabasePane extends ContextualPaneBase { }), offerThroughput: offerThroughput, subscriptionType: SubscriptionType[this.container.subscriptionType()], - subscriptionQuotaId: this.container.quotaId(), + subscriptionQuotaId: userContext.quotaId, defaultsCheck: { flight: this.container.flight() }, diff --git a/src/Explorer/Panes/CassandraAddCollectionPane.ts b/src/Explorer/Panes/CassandraAddCollectionPane.ts index b72dec1ea..092f032a7 100644 --- a/src/Explorer/Panes/CassandraAddCollectionPane.ts +++ b/src/Explorer/Panes/CassandraAddCollectionPane.ts @@ -15,6 +15,7 @@ import { HashMap } from "../../Common/HashMap"; import { configContext, Platform } from "../../ConfigContext"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { SubscriptionType } from "../../Contracts/SubscriptionType"; +import { userContext } from "../../UserContext"; export default class CassandraAddCollectionPane extends ContextualPaneBase { public createTableQuery: ko.Observable; @@ -299,7 +300,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { databaseId: this.keyspaceId() }), subscriptionType: SubscriptionType[this.container.subscriptionType()], - subscriptionQuotaId: this.container.quotaId(), + subscriptionQuotaId: userContext.quotaId, defaultsCheck: { storage: "u", throughput: this.throughput(), @@ -353,7 +354,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { }), keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), subscriptionType: SubscriptionType[this.container.subscriptionType()], - subscriptionQuotaId: this.container.quotaId(), + subscriptionQuotaId: userContext.quotaId, defaultsCheck: { storage: "u", throughput: this.throughput(), @@ -399,7 +400,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { }), keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), subscriptionType: SubscriptionType[this.container.subscriptionType()], - subscriptionQuotaId: this.container.quotaId(), + subscriptionQuotaId: userContext.quotaId, defaultsCheck: { storage: "u", throughput: this.throughput(), @@ -429,7 +430,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { }, keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), subscriptionType: SubscriptionType[this.container.subscriptionType()], - subscriptionQuotaId: this.container.quotaId(), + subscriptionQuotaId: userContext.quotaId, defaultsCheck: { storage: "u", throughput: this.throughput(), diff --git a/src/Explorer/Tabs/QueryTab.html b/src/Explorer/Tabs/QueryTab.html index 0d16152da..e27a4bb7c 100644 --- a/src/Explorer/Tabs/QueryTab.html +++ b/src/Explorer/Tabs/QueryTab.html @@ -103,7 +103,7 @@
- +
diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 57a6d97ec..0cc27d48d 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -551,7 +551,7 @@ export default class Collection implements ViewModels.Collection { const tabTitle = !this.offer() ? "Settings" : "Scale & Settings"; const pendingNotificationsPromise: Q.Promise = this._getPendingThroughputSplitNotification(); - const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Settings, tab => { + const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.SettingsV2, tab => { return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id(); }); diff --git a/src/NotebookWorkspaceManager/NotebookWorkspaceManager.ts b/src/NotebookWorkspaceManager/NotebookWorkspaceManager.ts index 563645f25..6b3d36f3f 100644 --- a/src/NotebookWorkspaceManager/NotebookWorkspaceManager.ts +++ b/src/NotebookWorkspaceManager/NotebookWorkspaceManager.ts @@ -12,8 +12,8 @@ import { getErrorMessage } from "../Common/ErrorHandlingUtils"; export class NotebookWorkspaceManager { private resourceProviderClientFactory: IResourceProviderClientFactory; - constructor(private _armEndpoint: string) { - this.resourceProviderClientFactory = new ResourceProviderClientFactory(this._armEndpoint); + constructor() { + this.resourceProviderClientFactory = new ResourceProviderClientFactory(); } public async getNotebookWorkspacesAsync(cosmosdbResourceId: string): Promise { diff --git a/src/Platform/Emulator/Main.ts b/src/Platform/Emulator/Main.ts index 7cc62b094..d3f5783a1 100644 --- a/src/Platform/Emulator/Main.ts +++ b/src/Platform/Emulator/Main.ts @@ -19,6 +19,7 @@ export function initializeExplorer(): Explorer { cassandraEndpoint: "" } }); + explorer.isAccountReady(true); return explorer; } diff --git a/src/Platform/Hosted/Main.ts b/src/Platform/Hosted/Main.ts index e218a136e..672efba95 100644 --- a/src/Platform/Hosted/Main.ts +++ b/src/Platform/Hosted/Main.ts @@ -268,7 +268,7 @@ export default class Main { masterKey?: string /* master key extracted from connection string if available */, account?: DatabaseAccount, authorizationToken?: string /* access key */ - ): Q.Promise { + ): void { const serverId: string = AuthHeadersUtil.serverId; const authType: string = (window).authType; const accountResourceId = @@ -373,7 +373,7 @@ export default class Main { }); } - return Q.reject(`Unsupported AuthType ${authType}`); + throw new Error(`Unsupported AuthType ${authType}`); } private static _instantiateExplorer(): Explorer { diff --git a/src/Platform/Portal/Main.ts b/src/Platform/Portal/Main.ts index 1e9af9e29..5d0b652ab 100644 --- a/src/Platform/Portal/Main.ts +++ b/src/Platform/Portal/Main.ts @@ -1,9 +1,23 @@ import "../../Explorer/Tables/DataTable/DataTableBindingManager"; import Explorer from "../../Explorer/Explorer"; +import { handleMessage } from "../../Controls/Heatmap/Heatmap"; export function initializeExplorer(): Explorer { const explorer = new Explorer(); + // In development mode, try to load the iframe message from session storage. + // This allows webpack hot reload to funciton properly + if (process.env.NODE_ENV === "development") { + const initMessage = sessionStorage.getItem("portalDataExplorerInitMessage"); + if (initMessage) { + const message = JSON.parse(initMessage); + console.warn("Loaded cached portal iframe message from session storage"); + console.dir(message); + explorer.initDataExplorerWithFrameInputs(message); + } + } + window.addEventListener("message", explorer.handleMessage.bind(explorer), false); + return explorer; } diff --git a/src/ResourceProvider/ResourceProviderClientFactory.ts b/src/ResourceProvider/ResourceProviderClientFactory.ts index 89c31e0f2..07b71a34e 100644 --- a/src/ResourceProvider/ResourceProviderClientFactory.ts +++ b/src/ResourceProvider/ResourceProviderClientFactory.ts @@ -1,10 +1,14 @@ +import { configContext } from "../ConfigContext"; import { IResourceProviderClientFactory, IResourceProviderClient } from "./IResourceProviderClient"; import { ResourceProviderClient } from "./ResourceProviderClient"; export class ResourceProviderClientFactory implements IResourceProviderClientFactory { + private armEndpoint: string; private cachedClients: { [url: string]: IResourceProviderClient } = {}; - constructor(private armEndpoint: string) {} + constructor() { + this.armEndpoint = configContext.ARM_ENDPOINT; + } public getOrCreate(url: string): IResourceProviderClient { if (!url) { diff --git a/src/RouteHandlers/TabRouteHandler.ts b/src/RouteHandlers/TabRouteHandler.ts index 251bc7c75..b79e1f69a 100644 --- a/src/RouteHandlers/TabRouteHandler.ts +++ b/src/RouteHandlers/TabRouteHandler.ts @@ -17,9 +17,7 @@ export class TabRouteHandler { ): void { this._initRouter(); const parseHash = (newHash: string, oldHash: string) => this._tabRouter.parse(newHash); - const defaultRoutedCallback = (request: string, data: { route: any; params: string[]; isFirst: boolean }) => { - console.log(request); - }; + const defaultRoutedCallback = (request: string, data: { route: any; params: string[]; isFirst: boolean }) => {}; this._tabRouter.routed.add(onMatch || defaultRoutedCallback); hasher.initialized.add(parseHash); hasher.changed.add(parseHash); diff --git a/src/Shared/Constants.ts b/src/Shared/Constants.ts index 212ff41ca..7aae90063 100644 --- a/src/Shared/Constants.ts +++ b/src/Shared/Constants.ts @@ -144,10 +144,6 @@ export class OfferPricing { }; } -export class GeneralResources { - public static loadingText: string = "Loading..."; -} - export class CollectionCreation { // TODO generate these values based on Product\Services\Documents\ImageStore\GatewayApplication\Settings.xml public static readonly MinRUPerPartitionBelow7Partitions: number = 400; @@ -228,32 +224,6 @@ export class IndexingPolicies { } export class SubscriptionUtilMappings { - // TODO: Expose this through a web API from the portal - public static SubscriptionTypeMap: { [key: string]: SubscriptionType } = { - "AAD_2015-09-01": SubscriptionType.Free, - "AzureDynamics_2014-09-01": SubscriptionType.Free, - "AzureInOpen_2014-09-01": SubscriptionType.EA, - "AzurePass_2014-09-01": SubscriptionType.Free, - "BackupStorage_2014-09-01": SubscriptionType.PAYG, - "BizSpark_2014-09-01": SubscriptionType.Benefits, - "BizSparkPlus_2014-09-01": SubscriptionType.Benefits, - "CSP_2015-05-01": SubscriptionType.EA, - "Default_2014-09-01": SubscriptionType.PAYG, - "DevEssentials_2016-01-01": SubscriptionType.Benefits, - "DreamSpark_2015-02-01": SubscriptionType.Benefits, - "EnterpriseAgreement_2014-09-01": SubscriptionType.EA, - "FreeTrial_2014-09-01": SubscriptionType.Free, - "Internal_2014-09-01": SubscriptionType.Internal, - "LegacyMonetaryCommitment_2014-09-01": SubscriptionType.EA, - "LightweightTrial_2016-09-01": SubscriptionType.Free, - "MonetaryCommitment_2015-05-01": SubscriptionType.EA, - "MPN_2014-09-01": SubscriptionType.Benefits, - "MSDN_2014-09-01": SubscriptionType.Benefits, - "MSDNDevTest_2014-09-01": SubscriptionType.Benefits, - "PayAsYouGo_2014-09-01": SubscriptionType.PAYG, - "Sponsored_2016-01-01": SubscriptionType.Benefits - }; - public static FreeTierSubscriptionIds: string[] = [ "b8f2ff04-0a81-4cf9-95ef-5828d16981d2", "39b1fdff-e5b2-4f83-adb4-33cb3aabf5ea", @@ -264,57 +234,6 @@ export class SubscriptionUtilMappings { ]; } -export class Offers { - public static offerTypeS1: string = "S1"; - public static offerTypeS2: string = "S2"; - public static offerTypeS3: string = "S3"; - public static offerTypeStandard: string = "Standard"; -} - -export class OfferThoughput { - public static offerS1Throughput: number = 250; - public static offerS2Throughput: number = 1000; - public static offerS3Throughput: number = 2500; -} - -export class OfferVersions { - public static offerV1: string = "V1"; - public static offerV2: string = "V2"; -} - -export class InvalidOffers { - public static offerTypeInvalid: string = "Invalid"; - public static offerTypeError: string = "Loading Error"; -} - -export class SpecTypes { - public static collection: string = "DocumentDbCollection"; -} - -export class CurrencyCodes { - public static usd: string = "USD"; - public static rmb: string = "RMB"; -} - -export class ColorSchemes { - public static standard: string = "mediumBlue"; - public static legacy: string = "yellowGreen"; -} - -export class FeatureIds { - public static storage: string = "storage"; - public static sla: string = "sla"; - public static partitioned: string = "partitioned"; - public static singlePartitioned: string = "singlePartition"; - public static legacySinglePartitioned: string = "legacySinglePartition"; -} - -export class FeatureIconNames { - public static storage: string = "SSD"; - public static sla: string = "Monitoring"; - public static productionReady: string = "ProductionReadyDb"; -} - export class AutopilotDocumentation { public static Url: string = "https://aka.ms/cosmos-autoscale-info"; } diff --git a/src/SparkClusterManager/ArcadiaResourceManager.ts b/src/SparkClusterManager/ArcadiaResourceManager.ts index 68fa2318b..b16121ec0 100644 --- a/src/SparkClusterManager/ArcadiaResourceManager.ts +++ b/src/SparkClusterManager/ArcadiaResourceManager.ts @@ -8,14 +8,13 @@ import { ArmApiVersions, ArmResourceTypes } from "../Common/Constants"; import { IResourceProviderClient, IResourceProviderClientFactory } from "../ResourceProvider/IResourceProviderClient"; import * as Logger from "../Common/Logger"; import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory"; -import { configContext } from "../ConfigContext"; import { getErrorMessage } from "../Common/ErrorHandlingUtils"; export class ArcadiaResourceManager { private resourceProviderClientFactory: IResourceProviderClientFactory; - constructor(private armEndpoint = configContext.ARM_ENDPOINT) { - this.resourceProviderClientFactory = new ResourceProviderClientFactory(this.armEndpoint); + constructor() { + this.resourceProviderClientFactory = new ResourceProviderClientFactory(); } public async getWorkspacesAsync(arcadiaResourceId: string): Promise { diff --git a/src/UserContext.ts b/src/UserContext.ts index d7ef737f2..ea76afe86 100644 --- a/src/UserContext.ts +++ b/src/UserContext.ts @@ -14,6 +14,7 @@ interface UserContext { defaultExperience?: DefaultAccountExperienceType; useSDKOperations?: boolean; subscriptionType?: SubscriptionType; + quotaId?: string; } const userContext: Readonly = {} as const; diff --git a/src/Utils/PricingUtils.test.ts b/src/Utils/PricingUtils.test.ts index 92acadffe..09c0ef5e5 100644 --- a/src/Utils/PricingUtils.test.ts +++ b/src/Utils/PricingUtils.test.ts @@ -25,39 +25,151 @@ describe("PricingUtils Tests", () => { 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); diff --git a/test/sql/container.spec.ts b/test/sql/container.spec.ts index ad5d7f251..853eff9e3 100644 --- a/test/sql/container.spec.ts +++ b/test/sql/container.spec.ts @@ -54,7 +54,7 @@ describe("Collection Add and Delete SQL spec", () => { // validate created // open database menu await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); - await frame.waitFor(LOADING_STATE_DELAY); + await frame.waitFor(CREATE_DELAY); await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); const databases = await frame.$$(`div[class="databaseHeader main1 nodeItem "] > div[class="treeNodeHeader "]`); const selectedDbId = await frame.evaluate(element => {