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/.github/workflows/runners.yml b/.github/workflows/runners.yml deleted file mode 100644 index 46513584b..000000000 --- a/.github/workflows/runners.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Runners -on: - schedule: - - cron: "0 * 1 * *" -jobs: - sqlcreatecollection: - runs-on: ubuntu-latest - name: "SQL | Create Collection" - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 - - run: npm ci - - run: npm run test:e2e - env: - PORTAL_RUNNER_APP_INSIGHTS_KEY: ${{ secrets.PORTAL_RUNNER_APP_INSIGHTS_KEY }} - PORTAL_RUNNER_USERNAME: ${{ secrets.PORTAL_RUNNER_USERNAME }} - PORTAL_RUNNER_PASSWORD: ${{ secrets.PORTAL_RUNNER_PASSWORD }} - PORTAL_RUNNER_SUBSCRIPTION: 69e02f2d-f059-4409-9eac-97e8a276ae2c - PORTAL_RUNNER_RESOURCE_GROUP: runners - PORTAL_RUNNER_DATABASE_ACCOUNT: portal-sql-runner - - uses: actions/upload-artifact@v2 - if: failure() - with: - name: screenshots - path: failure.png 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/canvas/README.md b/canvas/README.md new file mode 100644 index 000000000..60df6d486 --- /dev/null +++ b/canvas/README.md @@ -0,0 +1,7 @@ +# Why? + +This adds a mock module for `canvas`. Nteract has a ignored require and undeclared dependency on this module. `cavnas` is a server side node module and is not used in browser side code for nteract. + +Installing it locally (`npm install canvas`) will resolve the problem, but it is a native module so it is flaky depending on the system, node version, processor arch, etc. This module provides a simpler, more robust solution. + +Remove this workaround if [this bug](https://github.com/nteract/any-vega/issues/2) ever gets resolved \ No newline at end of file diff --git a/canvas/index.js b/canvas/index.js new file mode 100644 index 000000000..7c6d6c73d --- /dev/null +++ b/canvas/index.js @@ -0,0 +1 @@ +module.exports = {} \ No newline at end of file diff --git a/canvas/package.json b/canvas/package.json new file mode 100644 index 000000000..ce5a08e02 --- /dev/null +++ b/canvas/package.json @@ -0,0 +1,11 @@ +{ + "name": "canvas", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" +} diff --git a/less/Common/Constants.less b/less/Common/Constants.less index 46c24b07e..bc8616929 100644 --- a/less/Common/Constants.less +++ b/less/Common/Constants.less @@ -3,8 +3,8 @@ /******************************************************************************/ @font-face { - font-family: wf_segoe-ui_normal; - src: url('../../fonts/segoe-ui/west-european/normal/latest.woff'); + font-family: wf_segoe-ui_normal; + src: url("../../fonts/segoe-ui/west-european/normal/latest.woff"); } @DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; @@ -20,26 +20,26 @@ COLORS /******************************************************************************/ -@AccentMediumHigh: #0058AD; -@AccentMedium: #004E87; -@AccentHigh: #1EBAED; -@AccentExtraHigh: #55B3FF; -@AccentLow: #EDF6FF; -@AccentMediumLow: #DDEEFE; -@AccentLight: #EEF7FF; -@AccentExtra: #DDF0FF; +@AccentMediumHigh: #0058ad; +@AccentMedium: #004e87; +@AccentHigh: #1ebaed; +@AccentExtraHigh: #55b3ff; +@AccentLow: #edf6ff; +@AccentMediumLow: #ddeefe; +@AccentLight: #eef7ff; +@AccentExtra: #ddf0ff; -@SelectionHigh: #B91F26; -@BaseLight: #FFFFFF; +@SelectionHigh: #b91f26; +@BaseLight: #ffffff; @BaseDark: #000000; -@NotificationLow: #FFF4CE; -@NotificationHigh: #F9E9B0; -@Purple1: #8A2DA5; +@NotificationLow: #fff4ce; +@NotificationHigh: #f9e9b0; +@Purple1: #8a2da5; @Dirty: #9b4f96; -@BaseLow: #F2F2F2; -@BaseMediumLow: #E6E6E6; -@BaseMedium: #CCCCCC; +@BaseLow: #f2f2f2; +@BaseMediumLow: #e6e6e6; +@BaseMedium: #cccccc; @BaseMediumHigh: #767676; @BaseHigh: #393939; @@ -53,7 +53,7 @@ @ErrorColor: @SelectionHigh; -@SelectionColor: #3074B0; +@SelectionColor: #3074b0; @FocusColor: #605e5c; @@ -80,7 +80,7 @@ @ImgWidth: 14px; @ImgHeight: 14px; -@toggleFontWeight:700; +@toggleFontWeight: 700; //Resource Tree @TreeLineHeight: 17px; @@ -144,16 +144,16 @@ /**********************************************************************************/ .flex-display(@display: flex) { - display: ~"-webkit-@{display}"; - display: ~"-ms-@{display}box"; // IE10 uses -ms-flexbox - display: ~"-ms-@{display}"; // IE11 - display: @display; + display: ~"-webkit-@{display}"; + display: ~"-ms-@{display}box"; // IE10 uses -ms-flexbox + display: ~"-ms-@{display}"; // IE11 + display: @display; } .flex-direction(@direction: column) { -webkit-flex-direction: @direction; - -ms-flex-direction: @direction; - flex-direction: @direction; + -ms-flex-direction: @direction; + flex-direction: @direction; } /************************************************************************************* @@ -161,32 +161,31 @@ **************************************************************************************/ @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { - .selectedRadio, - .selectedRadio:hover, - .selectedRadio:active, - .selectedRadio.dirty, - .tab [type=radio]:checked ~ label, - .tab [type=radio]:checked ~ label:hover { - -ms-high-contrast-adjust: none; - -webkit-text-fill-color: HighlightText; - color: HighlightText; - border-color: HighlightText; - background-color: Highlight; - } - - .queryMetricsSummaryTuple { - - th, td { - - &:nth-child(2) { - width: @IETableDataWidth; - } - - &:nth-child(3) { - width: 50%; - } - } + .selectedRadio, + .selectedRadio:hover, + .selectedRadio:active, + .selectedRadio.dirty, + .tab [type="radio"]:checked ~ label, + .tab [type="radio"]:checked ~ label:hover { + -ms-high-contrast-adjust: none; + -webkit-text-fill-color: HighlightText; + color: HighlightText; + border-color: HighlightText; + background-color: Highlight; + } + + .queryMetricsSummaryTuple { + th, + td { + &:nth-child(2) { + width: @IETableDataWidth; + } + + &:nth-child(3) { + width: 50%; + } } + } } /******************************************************************************************** @@ -194,15 +193,15 @@ *********************************************************************************************/ .hover() { - background-color: @AccentLight; + background-color: @AccentLight; } .active() { - background-color: @AccentExtra; + background-color: @AccentExtra; } .focus() { - outline: 1px dashed @FocusColor; + outline: 1px dashed @FocusColor; } /************************************************************************************************ @@ -212,63 +211,87 @@ @ToggleWidth: 180px; .toggleSwitch() { - max-width: 100%; - margin-bottom: @SmallSpace; - padding: @SmallSpace; - cursor: pointer; - color: @BaseHigh; - font-weight: 400; - font-size: @mediumFontSize; - font-family: @DataExplorerFont; + max-width: 100%; + margin-bottom: @SmallSpace; + padding: @SmallSpace; + cursor: pointer; + color: @BaseHigh; + font-weight: 400; + font-size: @mediumFontSize; + font-family: @DataExplorerFont; } .selectedToggle() { - border-bottom: 2px solid @BaseHigh; + border-bottom: 2px solid @BaseHigh; } .unselectedToggle() { - color: @AccentMediumHigh; + color: @AccentMediumHigh; } /******************************************************************************************************** Common Data Explorer Icons *********************************************************************************************************/ .dataExplorerIcons() { - cursor: pointer; - width: @ImgWidth; - height: @ImgHeight; + cursor: pointer; + width: @ImgWidth; + height: @ImgHeight; } /********************************************************************************************************* Info Tooltip **********************************************************************************************************/ .infoTooltip() { - position: relative; - display: inline-block; + position: relative; + display: inline-block; } .tooltipText(@textColor: @BaseLight, @backgroundColor: @BaseHigh) { - visibility: hidden; - background-color: @backgroundColor; - color: @textColor; - position: absolute; - z-index: 1; - left: @MediumSpace; - padding: @MediumSpace; + visibility: hidden; + background-color: @backgroundColor; + color: @textColor; + position: absolute; + z-index: 1; + left: @MediumSpace; + padding: @MediumSpace; } .tooltipTextAfter(@color: @BaseDark) { - content: ""; - position: absolute; - right: 100%; - border-style: solid; - border-color: transparent @color transparent transparent; - left: 0px; - width: 0; - height: 0; - border-color: @InfoPointerColor transparent; + content: ""; + position: absolute; + right: 100%; + border-style: solid; + border-color: transparent @color transparent transparent; + left: 0px; + width: 0; + height: 0; + border-color: @InfoPointerColor transparent; } .tooltipVisible() { - visibility: visible; + visibility: visible; +} + +.inputTooltip() { + position: relative; +} + +.inputTooltipText(@textColor: @BaseLight, @backgroundColor: @BaseHigh) { + background-color: @backgroundColor; + color: @textColor; + position: absolute; + z-index: 1; + padding: @MediumSpace; +} + +.inputTooltipTextAfter(@color: @BaseDark) { + content: ""; + position: absolute; + right: 100%; + border-style: solid; + border-color: transparent @color transparent transparent; + left: 10px; + width: 0; + height: 0; + border-color: @InfoPointerColor transparent; } diff --git a/less/documentDB.less b/less/documentDB.less index e944f2a74..587670560 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -1,1681 +1,1696 @@ @import "./Common/Constants"; html { - font-family: @DataExplorerFont; - padding: 0px; - margin: 0px; - overflow: hidden; - position: fixed; - width: 100%; - height: 100%; + font-family: @DataExplorerFont; + padding: 0px; + margin: 0px; + overflow: hidden; + position: fixed; + width: 100%; + height: 100%; } body { - font-family: @DataExplorerFont; - font-size: 12px; - height: 100%; + font-family: @DataExplorerFont; + font-size: 12px; + height: 100%; - :focus { - .focus() - } + :focus { + .focus(); + } } .float-right { - float: right; + float: right; } .fixedleftpane { - background: #2f2d2d; - height: 100vh; - width: 80px; - float: left; + background: #2f2d2d; + height: 100vh; + width: 80px; + float: left; } .ui-dialog.ui-corner-all.ui-widget.ui-front.ui-widget-content.shareUrlDialog.no-close { - box-shadow: 0 0 @DefaultSpace @BoxShadow; - display: inline-block; + box-shadow: 0 0 @DefaultSpace @BoxShadow; + display: inline-block; + position: absolute; + background: #fff; + border: 1px solid @BaseMedium; + margin: 42px 20px; + border-radius: 3px; + + &:before { + content: ""; position: absolute; - background: #fff; - border: 1px solid @BaseMedium; - margin: 42px 20px; - border-radius: 3px; + top: -10px; + right: 13px; + border-width: 0 10px 10px; + border-style: solid; + border-color: @BaseMedium rgba(0, 0, 0, 0); + display: block; + width: 0; + } - &:before { - content:""; - position: absolute; - top: -10px; - right: 13px; - border-width: 0 10px 10px; - border-style: solid; - border-color: @BaseMedium rgba(0, 0, 0, 0); - display: block; - width: 0; + &:after { + content: ""; + position: absolute; + top: -9px; + right: 14px; + border-width: 0 9px 9px; + border-style: solid; + border-color: #fff rgba(0, 0, 0, 0); + display: block; + width: 0; + } + + .ui-dialog-titlebar.ui-corner-all.ui-helper-clearfix.ui-widget-header.shareUrlTitle { + border: none; + background-color: @BaseLight; + color: @BaseHigh; + font-size: @largeFontSize; + font-family: @DataExplorerFont; + padding: @MediumSpace; + margin: 0px @DefaultSpace 0px @MediumSpace; + font-weight: normal; + + .ui-dialog-titlebar-close.shareClose { + display: none; } + } - &:after { - content:""; - position: absolute; - top: -9px; - right: 14px; - border-width: 0 9px 9px; - border-style: solid; - border-color: #FFF rgba(0, 0, 0, 0); - display: block; - width: 0; - } + .shareDataAccessFlyout { + overflow: visible; + margin-bottom: @LargeSpace; + padding: @DefaultSpace @LargeSpace; - .ui-dialog-titlebar.ui-corner-all.ui-helper-clearfix.ui-widget-header.shareUrlTitle { - border: none; - background-color: @BaseLight; - color: @BaseHigh; - font-size: @largeFontSize; - font-family: @DataExplorerFont; - padding: @MediumSpace; - margin: 0px @DefaultSpace 0px @MediumSpace; - font-weight: normal; + .shareDataAccessFlyoutContent { + height: 100%; + width: 100%; - .ui-dialog-titlebar-close.shareClose { - display: none; + .urlContainer { + margin-left: @DefaultSpace; + + .urlContentText { + color: @BaseHigh; + font-size: @mediumFontSize; + font-family: @DataExplorerFont; } - } - .shareDataAccessFlyout { - overflow: visible; - margin-bottom: @LargeSpace; - padding: @DefaultSpace @LargeSpace; + .toggles { + height: @ToggleHeight; + width: @ToggleWidth; + margin: 24px 0px @LargeSpace 0px; - .shareDataAccessFlyoutContent { - height: 100%; - width: 100%; + &:focus { + .focus(); + } + .tab { + margin-right: @MediumSpace; + } - .urlContainer { - margin-left: @DefaultSpace; + .toggleSwitch { + .toggleSwitch(); + } - .urlContentText { - color: @BaseHigh; - font-size: @mediumFontSize; - font-family: @DataExplorerFont; - } + .selectedToggle { + .selectedToggle(); + } - .toggles { - height: @ToggleHeight; - width: @ToggleWidth; - margin: 24px 0px @LargeSpace 0px; + .unselectedToggle { + .unselectedToggle(); + } + } - &:focus { - .focus(); - } + .shareLabels { + color: @BaseHigh; + font-size: @mediumFontSize; + font-family: @DataExplorerFont; + margin-left: @DefaultSpace; + } - .tab { - margin-right: @MediumSpace; - } + .urlSpace { + margin: @LargeSpace 0px @DefaultSpace 0px; - .toggleSwitch { - .toggleSwitch(); - } + img { + cursor: pointer; + } + } - .selectedToggle { - .selectedToggle(); - } + .tokenSpace { + margin-bottom: (2 * @MediumSpace); - .unselectedToggle { - .unselectedToggle(); - } - } + img { + cursor: pointer; + } + } - .shareLabels { - color: @BaseHigh; - font-size: @mediumFontSize; - font-family: @DataExplorerFont; - margin-left: @DefaultSpace; - } + .urlTokenInfoTooltip { + .infoTooltip(); - .urlSpace { - margin: @LargeSpace 0px @DefaultSpace 0px; + &:hover .urlTokenTooltiptext { + .tooltipVisible(); + } - img { - cursor: pointer; - } - } + .urlTokenTooltiptext { + bottom: 28px; + width: 250px; + .tooltipText(); - .tokenSpace { - margin-bottom: (2 * @MediumSpace); - - img { - cursor: pointer; - } - } - - .urlTokenInfoTooltip { - .infoTooltip(); - - &:hover .urlTokenTooltiptext { - .tooltipVisible(); - } - - .urlTokenTooltiptext { - bottom:28px; - width: 250px; - .tooltipText(); - - &:after { - border-width: (2 * @MediumSpace) (2 * @MediumSpace) 0px 0px; - bottom: -15px; - .tooltipTextAfter(); - } - } - } - - .urlTokenCopyInfoTooltip { - .infoTooltip(); - padding: @SmallSpace; - - &:hover { - .hover(); - } - - &:active { - .active(); - } - - &:focus .urlTokenCopyTooltiptext, &:focus .urlTokenCopyTooltiptext { - .tooltipVisible(); - } - - &:focus .urlTokenCopyTooltiptext { - .tooltipVisible(); - } - - .urlTokenCopyTooltiptext { - visibility: hidden; - text-align: center; - background-color: @BaseHigh; - color: @BaseLight; - top: (2 * @LargeSpace); - width: 80px; - left: -26px; - position: absolute; - z-index: 1; - padding: @SmallSpace; - border-radius: 3px; - - &:after { - content: ""; - position: absolute; - border-style: solid; - border-color: transparent @BaseDark transparent transparent; - border-width: 0px @DefaultSpace 6px @DefaultSpace; - top: -5px; - left: 30px; - width: 0; - height: 0; - border-color: @InfoPointerColor transparent; - } - } - } - - .shareLink { - width: 300px; - background-color: #FFFFFF; - border: 1px solid @BaseMedium; - overflow: hidden; - text-overflow: ellipsis; - padding: 2px @SmallSpace 2px @SmallSpace; - margin: @SmallSpace @DefaultSpace 0px 0px; - font-family: @DataExplorerFont; - } + &:after { + border-width: (2 * @MediumSpace) (2 * @MediumSpace) 0px 0px; + bottom: -15px; + .tooltipTextAfter(); } + } } + + .urlTokenCopyInfoTooltip { + .infoTooltip(); + padding: @SmallSpace; + + &:hover { + .hover(); + } + + &:active { + .active(); + } + + &:focus .urlTokenCopyTooltiptext, + &:focus .urlTokenCopyTooltiptext { + .tooltipVisible(); + } + + &:focus .urlTokenCopyTooltiptext { + .tooltipVisible(); + } + + .urlTokenCopyTooltiptext { + visibility: hidden; + text-align: center; + background-color: @BaseHigh; + color: @BaseLight; + top: (2 * @LargeSpace); + width: 80px; + left: -26px; + position: absolute; + z-index: 1; + padding: @SmallSpace; + border-radius: 3px; + + &:after { + content: ""; + position: absolute; + border-style: solid; + border-color: transparent @BaseDark transparent transparent; + border-width: 0px @DefaultSpace 6px @DefaultSpace; + top: -5px; + left: 30px; + width: 0; + height: 0; + border-color: @InfoPointerColor transparent; + } + } + } + + .shareLink { + width: 300px; + background-color: #ffffff; + border: 1px solid @BaseMedium; + overflow: hidden; + text-overflow: ellipsis; + padding: 2px @SmallSpace 2px @SmallSpace; + margin: @SmallSpace @DefaultSpace 0px 0px; + font-family: @DataExplorerFont; + } + } + } + } + + .openFullScreenBtn { + background-color: @AccentMediumHigh; + color: @BaseLight; + padding: @SmallSpace 28px; + } + + .shareCancelButton { + background-color: @BaseLight; + color: @AccentMediumHigh; + padding: @SmallSpace 24px; + } + + .openFullScreenCancelBtn { + margin: @SmallSpace @DefaultSpace @SmallSpace @SmallSpace; + border: 1px solid @AccentMediumHigh; + cursor: pointer; + font-size: @mediumFontSize; + border-radius: 0px; + font-family: @DataExplorerFont; + } +} + +.connectExplorerContainer { + height: 100%; + width: 100%; + + .connectExplorerFormContainer { + .flex-display(); + .flex-direction(); + height: 100%; + width: 100%; + } + + .connectExplorer { + text-align: center; + .flex-display(); + .flex-direction(); + justify-content: center; + height: 100%; + margin-bottom: 60px; // this is to align the water mark in center between the top command bar and the notification console + + .welcomeText { + font-size: @DefaultFontSize; + color: @BaseHigh; + margin: @DefaultSpace @DefaultSpace @LargeSpace @DefaultSpace; } - .openFullScreenBtn { - background-color: @AccentMediumHigh; - color: @BaseLight; - padding: @SmallSpace 28px; + .switchConnectTypeText { + margin: @DefaultSpace; + font-size: @mediumFontSize; + color: @AccentMediumHigh; + cursor: pointer; } - .shareCancelButton { - background-color: @BaseLight; - color: @AccentMediumHigh; - padding: @SmallSpace 24px; + .connectStringText { + font-size: @mediumFontSize; + color: @BaseHigh; } - .openFullScreenCancelBtn { - margin: @SmallSpace @DefaultSpace @SmallSpace @SmallSpace; - border: 1px solid @AccentMediumHigh; + .connectExplorerContent { + margin: @DefaultSpace; + color: @BaseHigh; + + .inputToken { + width: 300px; + padding: 0px @SmallSpace; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + + &::placeholder { + font-style: italic; + } + } + + .errorDetailsInfoTooltip { + .infoTooltip(); + padding-left: @SmallSpace; + vertical-align: top; + + &:hover .errorDetails { + .tooltipVisible(); + } + + .errorDetails { + bottom: (2 * @MediumSpace); + width: @ErrorDetailsInfowidth; + visibility: hidden; + background-color: @BaseHigh; + color: @BaseLight; + position: absolute; + z-index: 1; + left: -10px; + padding: 6px; + + &:after { + border-width: 10px 10px 0px 10px; + bottom: -8px; + content: ""; + position: absolute; + right: 100%; + border-style: solid; + left: @MediumSpace; + width: 0; + height: 0; + border-color: @InfoPointerColor transparent; + } + } + + .errorImg { + height: @ImgWidth; + width: @ImgHeight; + } + } + } + } +} + +.dataExplorerLoaderContainer { + /* this should have more z-index value than the splitter to disable it */ + z-index: 5; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: @BaseLight; + opacity: @GreyOutOpacity; + + .dataExplorerLoader { + height: 8px; + margin-top: 50vh; + margin-left: 50%; + } +} + +.splashLoaderContainer { + z-index: 5; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: @BaseLight; + opacity: @GreyOutOpacity; + + .splashLoaderContentContainer { + .flex-display(); + .flex-direction(); + height: 100%; + text-align: center; + justify-content: center; + + .splashLoaderText { + margin-top: @LargeSpace; + } + + .splashLoaderTitle { + font-size: @DefaultFontSize; + color: @BaseHigh; + margin: @DefaultSpace @DefaultSpace @LargeSpace @DefaultSpace; + } + + .splashLoader { + display: block; + margin: @DefaultSpace auto; + width: @LoaderWidth; + height: @LoaderHeight; + } + } +} + +.dataExplorerPaneLoaderContainer { + right: 0; + width: 440px; + min-height: 565px; +} + +.dataExplorerTabLoaderContainer { + left: initial; + top: initial; + z-index: 0; +} + +.dataExplorerErrorConsoleContainer { + /* z-index should be greater than that of the splitter so it always overlaps the splitter */ + z-index: 2; + align-self: flex-end; + width: 100%; + .flex-display(); +} + +.ui-dialog.ui-corner-all.ui-widget.ui-widget-content.ui-front.no-close.ui-dialog-buttons { + border: 1px solid @BaseMedium; + box-shadow: 0 0 @DefaultSpace @BoxShadow; + padding: 0px; + + .ui-widget-header.ui-helper-clearfix.ui-dialog-titlebar.connectTitlebar { + background-color: @BaseLight; + font-size: @largeFontSize; + color: @BaseHigh; + font-family: @DataExplorerFont; + border: none; + font-weight: normal; + padding: @SmallSpace; + margin: @DefaultSpace @LargeSpace @MediumSpace (2 * @MediumSpace); + + .ui-button.ui-corner-all.ui-widget.ui-button-icon-only.ui-dialog-titlebar-close { + visibility: hidden; + } + } + + .dataAccessTokenModal { + margin: @LargeSpace 24px 24px; + padding: @SmallSpace; + overflow: visible; + .dataAccessTokenModalContent .dataAccessTokenExpireText { + margin-bottom: @LargeSpace; + color: @BaseHigh; + font-size: @DefaultFontSize; + font-family: @DataExplorerFont; + } + } + + .ui-dialog-buttonpane.ui-widget-content.ui-helper-clearfix { + border-top: @ButtonBorderWidth solid @BaseMediumLow; + padding: @LargeSpace 20px; + + .ui-dialog-buttonset { + float: none; + + .connectDialogButtons { + margin: @SmallSpace 0px @SmallSpace @MediumSpace; + border: @ButtonBorderWidth solid @AccentMediumHigh; cursor: pointer; font-size: @mediumFontSize; border-radius: 0px; font-family: @DataExplorerFont; - } -} + } -.connectExplorerContainer { - height: 100%; - width: 100%; + .connectButton { + padding: @SmallSpace @LargeSpace; + } - .connectExplorerFormContainer { - .flex-display(); - .flex-direction(); - height: 100%; - width: 100%; - } + .okBtn { + padding: @SmallSpace 30px; + } - .connectExplorer { - text-align: center; - .flex-display(); - .flex-direction(); - justify-content: center; - height: 100%; - margin-bottom: 60px; // this is to align the water mark in center between the top command bar and the notification console + .connectOkBtns { + background-color: @AccentMediumHigh; + color: @BaseLight; + } - .welcomeText { - font-size: @DefaultFontSize; - color: @BaseHigh; - margin: @DefaultSpace @DefaultSpace @LargeSpace @DefaultSpace; - } - - .switchConnectTypeText { - margin: @DefaultSpace; - font-size: @mediumFontSize; - color: @AccentMediumHigh; - cursor: pointer; - } - - .connectStringText { - font-size: @mediumFontSize; - color: @BaseHigh; - } - - .connectExplorerContent { - margin: @DefaultSpace; - color: @BaseHigh; - - .inputToken { - width: 300px; - padding: 0px @SmallSpace; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - - &::placeholder { - font-style: italic; - } - } - - .errorDetailsInfoTooltip { - .infoTooltip(); - padding-left: @SmallSpace; - vertical-align: top; - - &:hover .errorDetails { - .tooltipVisible(); - } - - .errorDetails { - bottom: (2 * @MediumSpace); - width: @ErrorDetailsInfowidth; - visibility: hidden; - background-color: @BaseHigh; - color: @BaseLight; - position: absolute; - z-index: 1; - left: -10px; - padding: 6px; - - &:after { - border-width: 10px 10px 0px 10px; - bottom: -8px; - content: ""; - position: absolute; - right: 100%; - border-style: solid; - left: @MediumSpace; - width: 0; - height: 0; - border-color: @InfoPointerColor transparent; - } - } - - .errorImg { - height: @ImgWidth; - width: @ImgHeight; - } - } - } - } -} - -.dataExplorerLoaderContainer { - /* this should have more z-index value than the splitter to disable it */ - z-index: 5; - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - background-color: @BaseLight; - opacity: @GreyOutOpacity; - - .dataExplorerLoader { - height: 8px; - margin-top: 50vh; - margin-left: 50%; - } -} - -.splashLoaderContainer { - z-index: 5; - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - background-color: @BaseLight; - opacity: @GreyOutOpacity; - - .splashLoaderContentContainer { - .flex-display(); - .flex-direction(); - height: 100%; - text-align: center; - justify-content: center; - - .splashLoaderText { - margin-top: @LargeSpace; - } - - .splashLoaderTitle { - font-size: @DefaultFontSize; - color: @BaseHigh; - margin: @DefaultSpace @DefaultSpace @LargeSpace @DefaultSpace; - } - - .splashLoader { - display: block; - margin: @DefaultSpace auto; - width: @LoaderWidth; - height: @LoaderHeight; - } - } -} - -.dataExplorerPaneLoaderContainer { - right: 0; - width: 440px; - min-height: 565px; -} - -.dataExplorerTabLoaderContainer { - left: initial; - top: initial; - z-index: 0; -} - -.dataExplorerErrorConsoleContainer { - /* z-index should be greater than that of the splitter so it always overlaps the splitter */ - z-index: 2; - align-self: flex-end; - width: 100%; - .flex-display(); -} - -.ui-dialog.ui-corner-all.ui-widget.ui-widget-content.ui-front.no-close.ui-dialog-buttons { - border: 1px solid @BaseMedium; - box-shadow:0 0 @DefaultSpace @BoxShadow; - padding: 0px; - - .ui-widget-header.ui-helper-clearfix.ui-dialog-titlebar.connectTitlebar { + .cancelBtn { background-color: @BaseLight; - font-size: @largeFontSize; - color: @BaseHigh; - font-family: @DataExplorerFont; - border: none; - font-weight: normal; - padding: @SmallSpace; - margin: @DefaultSpace @LargeSpace @MediumSpace (2 * @MediumSpace); - - .ui-button.ui-corner-all.ui-widget.ui-button-icon-only.ui-dialog-titlebar-close { - visibility: hidden; - } - } - - .dataAccessTokenModal { - margin: @LargeSpace 24px 24px; - padding: @SmallSpace; - overflow: visible; - .dataAccessTokenModalContent .dataAccessTokenExpireText { - margin-bottom: @LargeSpace; - color: @BaseHigh; - font-size: @DefaultFontSize; - font-family: @DataExplorerFont; - } - } - - .ui-dialog-buttonpane.ui-widget-content.ui-helper-clearfix { - border-top: @ButtonBorderWidth solid @BaseMediumLow; - padding: @LargeSpace 20px; - - .ui-dialog-buttonset { - float: none; - - .connectDialogButtons { - margin: @SmallSpace 0px @SmallSpace @MediumSpace; - border: @ButtonBorderWidth solid @AccentMediumHigh; - cursor: pointer; - font-size: @mediumFontSize; - border-radius: 0px; - font-family: @DataExplorerFont; - } - - .connectButton { - padding: @SmallSpace @LargeSpace; - } - - .okBtn { - padding: @SmallSpace 30px; - } - - .connectOkBtns { - background-color: @AccentMediumHigh; - color: @BaseLight; - } - - .cancelBtn { - background-color: @BaseLight; - color: @AccentMediumHigh; - margin-left: @DefaultSpace; - padding: @SmallSpace 20px; - } - } + color: @AccentMediumHigh; + margin-left: @DefaultSpace; + padding: @SmallSpace 20px; + } } + } } .ui-widget-overlay.ui-front { - background-color: @BaseLight; - opacity: @GreyOutOpacity; + background-color: @BaseLight; + opacity: @GreyOutOpacity; } .renewAccessInfo { - color: @BaseHigh; - padding-right: (2 * @LargeSpace); - margin-bottom: (2 * @MediumSpace); + color: @BaseHigh; + padding-right: (2 * @LargeSpace); + margin-bottom: (2 * @MediumSpace); } .renewUploadItemsHeader { - margin-bottom: @DefaultSpace; - color: @BaseHigh; + margin-bottom: @DefaultSpace; + color: @BaseHigh; } .accessKeyInput { - width: @AccessKeyInputWidth; - padding: 0px @SmallSpace; + width: @AccessKeyInputWidth; + padding: 0px @SmallSpace; - &::placeholder { - font-style: italic; - } + &::placeholder { + font-style: italic; + } } .renewAccessExpandCollapse { - margin-top: 24px; - cursor: pointer; + margin-top: 24px; + cursor: pointer; - img { - width: @AccountNavigationExpandCollapseSize; - height: @AccountNavigationExpandCollapseSize; - margin-bottom: @SmallSpace; - } + img { + width: @AccountNavigationExpandCollapseSize; + height: @AccountNavigationExpandCollapseSize; + margin-bottom: @SmallSpace; + } } .AccountNavigationText { - font-size: @mediumFontSize; - font-family: @DataExplorerFont; + font-size: @mediumFontSize; + font-family: @DataExplorerFont; } .renewAccessImg { - margin: @DefaultSpace @MediumSpace 0px @LargeSpace; + margin: @DefaultSpace @MediumSpace 0px @LargeSpace; - img { - margin-top: @DefaultSpace; - width: @AccountNavigationImgWidth; - height: @AccountNavigationImgHeight; - } + img { + margin-top: @DefaultSpace; + width: @AccountNavigationImgWidth; + height: @AccountNavigationImgHeight; + } } .importFilesTitle { - overflow: hidden; - text-overflow: ellipsis; - background-color: @BaseLight; - border: 1px solid @BaseMedium; - height: 24px; - width: 300px; - padding: 0px @DefaultSpace 0px @DefaultSpace; + overflow: hidden; + text-overflow: ellipsis; + background-color: @BaseLight; + border: 1px solid @BaseMedium; + height: 24px; + width: 300px; + padding: 0px @DefaultSpace 0px @DefaultSpace; } .sparkWorkerCountInput { - margin-top: 5px; - width: 100%; - padding: 1px; + margin-top: 5px; + width: 100%; + padding: 1px; } .fileImportImg { - padding: @SmallSpace; - vertical-align: top; - border: @ButtonBorderWidth solid transparent; - &:hover { - background-color: @BaseMediumLow; - } - &:active { - background-color: @BaseMediumLow; - } - &:focus { - .focus(); - } + padding: @SmallSpace; + vertical-align: top; + border: @ButtonBorderWidth solid transparent; + &:hover { + background-color: @BaseMediumLow; + } + &:active { + background-color: @BaseMediumLow; + } + &:focus { + .focus(); + } } .fileImportButton { - height: 24px; - border: @ButtonBorderWidth solid transparent; - vertical-align: top; + height: 24px; + border: @ButtonBorderWidth solid transparent; + vertical-align: top; } .fileUploadSummaryContainer { - margin-top: 40px; + margin-top: 40px; - .fileUploadSummary { - margin-top: @DefaultSpace; - width: calc(~"100% - 25px"); - table-layout: fixed; + .fileUploadSummary { + margin-top: @DefaultSpace; + width: calc(~"100% - 25px"); + table-layout: fixed; - .fileUploadSummaryHeader { - font-size: 10px; - } - - .fileUploadSummaryTuple { - text-overflow: ellipsis; - overflow: hidden; - border-bottom: 1px solid @BaseMedium; - height: 28px; - - td { - overflow: hidden; - text-overflow: ellipsis; - } - } + .fileUploadSummaryHeader { + font-size: 10px; } + + .fileUploadSummaryTuple { + text-overflow: ellipsis; + overflow: hidden; + border-bottom: 1px solid @BaseMedium; + height: 28px; + + td { + overflow: hidden; + text-overflow: ellipsis; + } + } + } } execute-sproc-params-pane { - @textInputWidth: 200px; - @textInputHeight: 24px; - @dataTypeSelectorWidth: 100px; - @paramTableTypeWidth: 110px; + @textInputWidth: 200px; + @textInputHeight: 24px; + @dataTypeSelectorWidth: 100px; + @paramTableTypeWidth: 110px; - .partitionKeyContainer, - .paramsTable { - padding-bottom: @DefaultSpace; + .partitionKeyContainer, + .paramsTable { + padding-bottom: @DefaultSpace; - .inputHeader, - .enterInputParams { - margin-bottom: @SmallSpace; - font-size: @DefaultFontSize; - } - - .scrollBox { - width: 100%; - overflow: auto; - overflow-x: hidden; - padding-bottom: @SmallSpace; - - .paramsClauseTable { - border-spacing: 0px; - display: table; - width: 100%; - margin-top: 8px; - - .paramTableTypeHead { - width: @paramTableTypeWidth; - } - - .paramTemplateRow { - padding-top: @MediumSpace; - } - - .dataTypeSelector { - width: @dataTypeSelectorWidth; - height: @textInputHeight; - border: @ButtonBorderWidth solid @BaseMedium; - color: @BaseHigh; - padding-left: @SmallSpace; - } - - .partitionKeyValue, - .valueTextBox { - width: @textInputWidth; - height: @textInputHeight; - border: @ButtonBorderWidth solid @BaseMedium; - padding: @SmallSpace @DefaultSpace; - } - - .partitionKeyValue { - margin-right: 30px; - } - - .spEntityAddCancel { - margin-right: 1px; - cursor: pointer; - - &:hover { - .hover(); - } - - &:active { - .active(); - } - - &:focus { - .focus(); - } - - img { - width: @ImgWidth; - height: @ImgHeight; - margin: 0px 0px @SmallSpace @SmallSpace; - } - } - } - } - - .addNewParam { - padding: @DefaultSpace 0px 5px @SmallSpace; - margin-top: @MediumSpace; - width: 125px; - cursor: pointer; - - &:hover { - .hover(); - } - - &:active { - .active(); - } - - &:focus { - .focus(); - } - - img { - width: @ImgWidth; - height: @ImgHeight; - margin-bottom: @SmallSpace; - } - - .addNewParamLabel { - margin-left: @SmallSpace; - } - } + .inputHeader, + .enterInputParams { + margin-bottom: @SmallSpace; + font-size: @DefaultFontSize; } + + .scrollBox { + width: 100%; + overflow: auto; + overflow-x: hidden; + padding-bottom: @SmallSpace; + + .paramsClauseTable { + border-spacing: 0px; + display: table; + width: 100%; + margin-top: 8px; + + .paramTableTypeHead { + width: @paramTableTypeWidth; + } + + .paramTemplateRow { + padding-top: @MediumSpace; + } + + .dataTypeSelector { + width: @dataTypeSelectorWidth; + height: @textInputHeight; + border: @ButtonBorderWidth solid @BaseMedium; + color: @BaseHigh; + padding-left: @SmallSpace; + } + + .partitionKeyValue, + .valueTextBox { + width: @textInputWidth; + height: @textInputHeight; + border: @ButtonBorderWidth solid @BaseMedium; + padding: @SmallSpace @DefaultSpace; + } + + .partitionKeyValue { + margin-right: 30px; + } + + .spEntityAddCancel { + margin-right: 1px; + cursor: pointer; + + &:hover { + .hover(); + } + + &:active { + .active(); + } + + &:focus { + .focus(); + } + + img { + width: @ImgWidth; + height: @ImgHeight; + margin: 0px 0px @SmallSpace @SmallSpace; + } + } + } + } + + .addNewParam { + padding: @DefaultSpace 0px 5px @SmallSpace; + margin-top: @MediumSpace; + width: 125px; + cursor: pointer; + + &:hover { + .hover(); + } + + &:active { + .active(); + } + + &:focus { + .focus(); + } + + img { + width: @ImgWidth; + height: @ImgHeight; + margin-bottom: @SmallSpace; + } + + .addNewParamLabel { + margin-left: @SmallSpace; + } + } + } } stored-procedure-tab { - @ToggleHeight: 30px; - @ToggleWidth: 180px; + @ToggleHeight: 30px; + @ToggleWidth: 180px; - .results-container, .errors-container { - padding: @MediumSpace 0px 0px @MediumSpace; - height: 100%; - .flex-display(); - .flex-direction(); + .results-container, + .errors-container { + padding: @MediumSpace 0px 0px @MediumSpace; + height: 100%; + .flex-display(); + .flex-direction(); + overflow: hidden; + + .toggles { + height: @ToggleHeight; + width: @ToggleWidth; + margin-left: @MediumSpace; + + &:focus { + .focus(); + } + + .tab { + margin-right: @MediumSpace; + } + + .toggleSwitch { + .toggleSwitch(); + } + + .selectedToggle { + .selectedToggle(); + } + + .unselectedToggle { + .unselectedToggle(); + } + } + + .enterInputParameters { + padding: @LargeSpace @MediumSpace; + } + } + + .errors-container { + padding-left: (2 * @MediumSpace); + .errors-header { + font-weight: 700; + font-size: @DefaultFontSize; + padding-bottom: @DefaultSpace; + } + + .errorContent { + .flex-display(); + width: 60%; + white-space: nowrap; + font-size: @mediumFontSize; + + .errorMessage { + padding: @SmallSpace; overflow: hidden; - - .toggles { - height: @ToggleHeight; - width: @ToggleWidth; - margin-left: @MediumSpace; - - &:focus { - .focus(); - } - - .tab { - margin-right: @MediumSpace; - } - - .toggleSwitch { - .toggleSwitch(); - } - - .selectedToggle { - .selectedToggle(); - } - - .unselectedToggle { - .unselectedToggle(); - } - } - - .enterInputParameters { - padding: @LargeSpace @MediumSpace; - } + text-overflow: ellipsis; + } } - .errors-container { - padding-left: (2 * @MediumSpace); - .errors-header { - font-weight: 700; - font-size: @DefaultFontSize; - padding-bottom: @DefaultSpace; - } - - .errorContent { - .flex-display(); - width: 60%; - white-space: nowrap; - font-size: @mediumFontSize; - - .errorMessage { - padding: @SmallSpace; - overflow: hidden; - text-overflow: ellipsis; - } - } - - .errorDetailsLink { - cursor: pointer; - padding: @SmallSpace; - } + .errorDetailsLink { + cursor: pointer; + padding: @SmallSpace; } + } - editor { - .flex-display(); - .flex-direction(); - overflow: hidden; - height: 100%; - width: 100%; - padding-left: 22px; - } + editor { + .flex-display(); + .flex-direction(); + overflow: hidden; + height: 100%; + width: 100%; + padding-left: 22px; + } - json-editor { - .flex-display(); - .flex-direction(); - overflow: hidden; - height: 100%; - width: 100%; - padding: @SmallSpace 0px @SmallSpace 10px; - } + json-editor { + .flex-display(); + .flex-direction(); + overflow: hidden; + height: 100%; + width: 100%; + padding: @SmallSpace 0px @SmallSpace 10px; + } } notification-console { - width: 100%; + width: 100%; } #divQuickStart { - display: inline-block; - width: 100%; + display: inline-block; + width: 100%; } #imgiconwidth1 { - width: 72%; + width: 72%; } #Quickstart { - text-align: center; - width: 80px; - height: 60px; - margin: 0 auto; - padding-top: 5px; - position: relative; + text-align: center; + width: 80px; + height: 60px; + margin: 0 auto; + padding-top: 5px; + position: relative; } .flexContainer { - height:100%; - width:100%; - .flex-display(); - .flex-direction(); + height: 100%; + width: 100%; + .flex-display(); + .flex-direction(); } .hideOverflows { - overflow: hidden; + overflow: hidden; } .explorerContent { - flex-grow: 1; + flex-grow: 1; } .collectionheading { - text-transform: uppercase; - font-size: 10px; + text-transform: uppercase; + font-size: 10px; } #Quickstart #imgiconwidth1 { - width: 24px; - height: 24px; - position: absolute; - right: 27px; + width: 24px; + height: 24px; + position: absolute; + right: 27px; } .topSelected { - border-left: 4px solid @AccentMediumHigh; - background: #666666; + border-left: 4px solid @AccentMediumHigh; + background: #666666; } .topSelected:hover { - border-left: 4px solid @AccentMediumHigh; - background: #666666!important; - cursor: default!important; + border-left: 4px solid @AccentMediumHigh; + background: #666666 !important; + cursor: default !important; } #Quickstart:hover span.activemenu, #Quickstart:active span.activemenu { - color: #fff; + color: #fff; } #Explorer:hover span.menuExplorer, #Explorer:active span.menuExplorer { - color: #fff; + color: #fff; } menuQuickStart { - margin-left: 0; - padding-left: 0; - display: block; - right: 12px; - top: 30px; - position: absolute; + margin-left: 0; + padding-left: 0; + display: block; + right: 12px; + top: 30px; + position: absolute; } #Explorer { - text-align: center; - display: inline-block; - width: 80px; - height: 60px; - margin: 0 auto; - padding-top: 9px; - position: relative; + text-align: center; + display: inline-block; + width: 80px; + height: 60px; + margin: 0 auto; + padding-top: 9px; + position: relative; } #Explorer #imgiconwidth1, .feedbackstyle #imgiconwidth1, .settingstyle #imgiconwidth1 { - width: 24px; - height: 24px; - position: absolute; - right: 30px; + width: 24px; + height: 24px; + position: absolute; + right: 30px; } #Explorer span.menuExplorer { - margin-left: 0; - padding-left: 0; - display: block; - right: 19px; - top: 33px; - position: absolute; + margin-left: 0; + padding-left: 0; + display: block; + right: 19px; + top: 33px; + position: absolute; } .feedbackstyle span.menuExplorer { - margin-left: 0; - padding-left: 0; - display: block; - right: 19px; - top: 33px; - position: absolute; + margin-left: 0; + padding-left: 0; + display: block; + right: 19px; + top: 33px; + position: absolute; } .settingstyle span.menuExplorer { - margin-left: 0; - padding-left: 0; - display: block; - right: 19px; - top: 33px; - position: absolute; + margin-left: 0; + padding-left: 0; + display: block; + right: 19px; + top: 33px; + position: absolute; } .content { - display: inline-block; - width: 100%; - transition: all .4s ease-in-out; - -ms-transition: all .4s ease-in-out; - -webkit-transition: all .4s ease-in-out; - -moz-transition: all .4s ease-in-out; - height: 100vh; + display: inline-block; + width: 100%; + transition: all 0.4s ease-in-out; + -ms-transition: all 0.4s ease-in-out; + -webkit-transition: all 0.4s ease-in-out; + -moz-transition: all 0.4s ease-in-out; + height: 100vh; } .mini { - width: 0%; - float: left; - transition: all .4s ease-in-out; - -webkit-transition: all .4s ease-in-out; - -moz-transition: all .4s ease-in-out; - height: 100vh; - background-color: white; + width: 0%; + float: left; + transition: all 0.4s ease-in-out; + -webkit-transition: all 0.4s ease-in-out; + -moz-transition: all 0.4s ease-in-out; + height: 100vh; + background-color: white; } #sidebar-wrapper { - z-index: 1000; - position: fixed; - left: 250px; - width: 0; - height: 100%; - margin-left: -250px; - overflow-y: auto; - background: white; - -webkit-transition: all 0.5s ease; - -moz-transition: all 0.5s ease; - -o-transition: all 0.5s ease; - transition: all 0.5s ease; + z-index: 1000; + position: fixed; + left: 250px; + width: 0; + height: 100%; + margin-left: -250px; + overflow-y: auto; + background: white; + -webkit-transition: all 0.5s ease; + -moz-transition: all 0.5s ease; + -o-transition: all 0.5s ease; + transition: all 0.5s ease; } .toggle-left { - width: 0%; - overflow: hidden; + width: 0%; + overflow: hidden; } .toggle-minicontent { - width: 100%; + width: 100%; } .toggle-maincontent { - width: 100%; + width: 100%; } .toggle-mini { - width: 36px; + width: 36px; } .toggle-main { - width: 100%; + width: 100%; } .activepartitionmode { - background-color: @AccentMediumHigh; + background-color: @AccentMediumHigh; } .paddingpartition { - color: white; - padding-left: 15px; - padding-top: 25px; + color: white; + padding-left: 15px; + padding-top: 25px; } .paddingspan2 { - padding-top: 20px; - color: #000; - padding-left: 15px; + padding-top: 20px; + color: #000; + padding-left: 15px; } .paddingspan4 { - padding-top: 20px; - padding-bottom: 20px; - color: white; - padding-left: 15px; + padding-top: 20px; + padding-bottom: 20px; + color: white; + padding-left: 15px; } .whitegroove { - width: 344px; - border: groove; + width: 344px; + border: groove; } .dropdownbtn { - color: white; - width: 340px; - background: #262626; + color: white; + width: 340px; + background: #262626; } .queryclr { - color: white; - background: #262626; + color: white; + background: #262626; } .panelContent { - display: flex; - flex-direction: column; - flex: 1; + display: flex; + flex-direction: column; + flex: 1; } .panelContentWrapper { - display: flex; - flex-direction: column; - height: 100%; + display: flex; + flex-direction: column; + height: 100%; } .contextual-pane { - top: 0px; - right: 0 !important; - left: auto; - -webkit-box-flex: 0; - -webkit-flex: 0 0 auto; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - right: auto; - z-index: 1000 !important; - -webkit-align-self: auto !important; - -ms-flex-item-align: auto !important; - align-self: auto !important; - height: 100%; - position: absolute; - width: 440px; - margin: 0; - padding: 0; - border: 0; - font-size: 100%; - font: inherit; - vertical-align: baseline; - outline: none; - background-color: #fff; - /* border-left: 1px solid #cbcbcb; */ - -webkit-animation: mymove 0.2s; - -webkit-animation-timing-function: cubic-bezier(0.47, 0, 0.75, 0.72); - animation: mymove 0.2s; - animation-timing-function: cubic-bezier(0.22, 0.61, 0.36, 1); - box-shadow: -5px 0px 7px 0px #cbcbcb; + top: 0px; + right: 0 !important; + left: auto; + -webkit-box-flex: 0; + -webkit-flex: 0 0 auto; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + right: auto; + z-index: 1000 !important; + -webkit-align-self: auto !important; + -ms-flex-item-align: auto !important; + align-self: auto !important; + height: 100%; + position: absolute; + width: 440px; + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; + outline: none; + background-color: #fff; + /* border-left: 1px solid #cbcbcb; */ + -webkit-animation: mymove 0.2s; + -webkit-animation-timing-function: cubic-bezier(0.47, 0, 0.75, 0.72); + animation: mymove 0.2s; + animation-timing-function: cubic-bezier(0.22, 0.61, 0.36, 1); + box-shadow: -5px 0px 7px 0px #cbcbcb; } @keyframes mymove { - from { - right: -1000px; - } - to { - right: 0px; - } + from { + right: -1000px; + } + to { + right: 0px; + } } .contextual-pane-out { - width: 100%; - height: 100vh; - z-index: 1; - position: absolute; - top: 0px; + width: 100%; + height: 100vh; + z-index: 1; + position: absolute; + top: 0px; } .contextual-pane-in { - width: 100%; - height: 100%; + width: 100%; + height: 100%; } .pointer { - cursor: pointer; + cursor: pointer; } -#tbodycontent>tr>td { - border-bottom: 1px solid #CCCCCC; - border-top: none; - padding: 6px; +#tbodycontent > tr > td { + border-bottom: 1px solid #cccccc; + border-top: none; + padding: 6px; } -#tbodycontent>tr:last-child>td { - border-bottom: 1px solid #ddd; +#tbodycontent > tr:last-child > td { + border-bottom: 1px solid #ddd; } .documentsTabGridAndEditor { + height: 100%; + overflow: hidden; + .flex-display(); + + &.documentsTabGridAndEditorUpperPadding { + padding-top: 16px; + } + + .documentsContainerWithSplitter { + height: 100% !important; + width: 200px; + padding-left: 8px; + flex-shrink: 0; + } + + .documentWaterMark { + margin: auto; + height: 35vh; // this is to align the water mark in center of the layout + text-align: center; + + p { + margin-bottom: @LargeSpace; + } + + .documentWaterMarkText { + color: @BaseHigh; + font-size: @DefaultFontSize; + font-family: @DataExplorerFont; + } + + .loadErrorIcon { + width: 128px; + height: 128px; + } + + .loadErrorDetailsLink { + cursor: pointer; + } + } + + json-editor { + padding-top: 28px; + padding-left: @MediumSpace; height: 100%; + width: 100%; overflow: hidden; .flex-display(); - &.documentsTabGridAndEditorUpperPadding { - padding-top: 16px; + .jsonEditor { + border: none; } + } - .documentsContainerWithSplitter { - height: 100% !important; - width: 200px; - padding-left: 8px; - flex-shrink: 0; + diff-editor { + padding-top: 28px; + height: 100%; + width: 100%; + overflow: hidden; + .flex-display(); + + .jsonEditor { + border: none; } + } - .documentWaterMark { - margin: auto; - height: 35vh; // this is to align the water mark in center of the layout - text-align: center; + .conflictEditorContainer { + width: 100%; - p { - margin-bottom: @LargeSpace; - } + .conflictEditorHeader { + padding: 6px 12px; + width: 100%; + overflow: hidden; - .documentWaterMarkText { - color: @BaseHigh; - font-size: @DefaultFontSize; - font-family: @DataExplorerFont; - } - - .loadErrorIcon { - width:128px; - height:128px; - } - - .loadErrorDetailsLink { - cursor: pointer; - } + .conflictEditorHeaderLabel { + width: 50%; + float: left; + color: @BaseDark; + font-weight: bold; + } } json-editor { - padding-top: 28px; - padding-left: @MediumSpace; - height: 100%; - width:100%; - overflow: hidden; - .flex-display(); - - .jsonEditor { - border: none; - } + padding-top: 0; } diff-editor { - padding-top: 28px; - height: 100%; - width:100%; - overflow: hidden; - .flex-display(); - - .jsonEditor { - border: none; - } - } - - .conflictEditorContainer { - width: 100%; - - .conflictEditorHeader { - padding: 6px 12px; - width: 100%; - overflow: hidden; - - .conflictEditorHeaderLabel { - width: 50%; - float: left; - color: @BaseDark; - font-weight: bold; - } - } - - json-editor { - padding-top: 0; - } - - diff-editor { - padding-top: 0; - height: calc(~"100% - 30px"); - } + padding-top: 0; + height: calc(~"100% - 30px"); } + } } .gridRowSelected { - .active(); + .active(); } .gridRowSelected:hover { - cursor: default; - .hover() + cursor: default; + .hover(); } .gridRowHighlighted { - border-style: dotted; - border-width: 2px; + border-style: dotted; + border-width: 2px; } -.table-hover>tbody>tr:hover { - .hover(); +.table-hover > tbody > tr:hover { + .hover(); } .collectionNodeSelected { - .active(); + .active(); } .collectionNodeSelected:hover { - cursor: default; - .hover(); + cursor: default; + .hover(); } .databaseNodeSelected { - .active(); + .active(); } .databaseNodeSelected:hover { - cursor: default; - .hover(); + cursor: default; + .hover(); } .leftsidepanle-hr { - margin: 16px 0px; - border-top: 1px solid #eee; - margin-left: -17px; - width: 100%; - color: 1px solid #53575B; + margin: 16px 0px; + border-top: 1px solid #eee; + margin-left: -17px; + width: 100%; + color: 1px solid #53575b; } .partitioning-btn { - padding-bottom: 16px; + padding-bottom: 16px; } .btncreatecoll1 { - border: 1px solid @AccentMediumHigh; - background-color: @AccentMediumHigh; - color: #fff; - padding: 2px 30px; - cursor: pointer; - font-size: 12px; + border: 1px solid @AccentMediumHigh; + background-color: @AccentMediumHigh; + color: #fff; + padding: 2px 30px; + cursor: pointer; + font-size: 12px; - &:active { - border-color: #0072c6; - background-color: #0072c6; - } + &:active { + border-color: #0072c6; + background-color: #0072c6; + } } .leftpanel-okbut .genericPaneSubmitBtn { - border: 1px solid @AccentMediumHigh; - background-color: @AccentMediumHigh; - color: #fff; - cursor: pointer; - font-size: 12px; - height: 24px; + border: 1px solid @AccentMediumHigh; + background-color: @AccentMediumHigh; + color: #fff; + cursor: pointer; + font-size: 12px; + height: 24px; - &:active { - border-color: #0072c6; - background-color: #0072c6; - } + &:active { + border-color: #0072c6; + background-color: #0072c6; + } } .btncreatecoll1-off { - border: 1px solid #969696; - background-color: #000; - color: white; - padding: 2px 30px; - cursor: pointer; - font-size: 12px; - margin-left: -5px; + border: 1px solid #969696; + background-color: #000; + color: white; + padding: 2px 30px; + cursor: pointer; + font-size: 12px; + margin-left: -5px; } .leftpanel-okbut { - padding: 20px 0px 24px 30px; + padding: 20px 0px 24px 30px; } .btnpricepad { - margin-left: 24px; + margin-left: 24px; } .btnSetupQueries { - margin-top: @LargeSpace; + margin-top: @LargeSpace; } .collid { - background: #fff; - width: @newCollectionPaneInputWidth; + background: #fff; + width: @newCollectionPaneInputWidth; } .textfontclr { - color: #000; + color: #000; } .collid-white { - width: 100%; - border: solid 1px #DDD; + width: 100%; + border: solid 1px #ddd; } .plusimg-but { - background-image: url(../images/plus_normal.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/plus_normal.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .plusimg-but:hover { - background-image: url(../images/plus_hover.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/plus_hover.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .plusimg-but:active { - background-image: url(../images/plus_pressed.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/plus_pressed.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .plusimg-but:disabled { - background-image: url(../images/plus_disabled.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/plus_disabled.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .minusimg-but { - background-image: url(../images/minus_normal.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/minus_normal.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .minusimg-but:hover { - background-image: url(../images/minus_hover.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/minus_hover.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .minusimg-but:active { - background-image: url(../images/minus_pressed.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/minus_pressed.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .minusimg-but:disabled { - background-image: url(../images/minus_disabled.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/minus_disabled.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .firstdivbg { - padding: @MediumSpace 0px @DefaultSpace (2 * @LargeSpace); - background-color: @BaseLight; + padding: @MediumSpace 0px @DefaultSpace (2 * @LargeSpace); + background-color: @BaseLight; } p { - margin: 0 0 4px; - color: #000; + margin: 0 0 4px; + color: #000; } .headerline .closePaneBtn { - float: right; - cursor: pointer; - width: 16px; - height: 100%; - margin-right: 4px; - color: #000; + float: right; + cursor: pointer; + width: 16px; + height: 100%; + margin-right: 4px; + color: #000; } .closeImg { - float: right; - cursor: pointer; - width: 16px; - height: 16px; - margin-right: 4px; + float: right; + cursor: pointer; + width: 16px; + height: 16px; + margin-right: 4px; - img { - height: 25px; - width: 25px; - } + img { + height: 25px; + width: 25px; + } } .closeImg[tabindex]:active { - outline: none; + outline: none; } .seconddivpadding { - padding-top: 16px; + padding-top: 16px; } .seconddivbg { - height: 100vh; - padding-left: 32px; - padding-top: 16px; + height: 100vh; + padding-left: 32px; + padding-top: 16px; } .pkPadding { - padding-top: 12px; + padding-top: 12px; } .mandatoryStar { - color: #ff0707; - font-size: 14px; - font-weight: bold; + color: #ff0707; + font-size: 14px; + font-weight: bold; } .createNewDatabaseOrUseExisting { - margin-bottom: @SmallSpace; + margin-bottom: @SmallSpace; - .createNewDatabaseOrUseExistingRadio { - vertical-align: text-bottom; - } + .createNewDatabaseOrUseExistingRadio { + vertical-align: text-bottom; + } - .createNewDatabaseOrUseExistingRadio:nth-child(n+2) { - margin-left: @LargeSpace; - } + .createNewDatabaseOrUseExistingRadio:nth-child(n + 2) { + margin-left: @LargeSpace; + } - .createNewDatabaseOrUseExistingSpace { - padding-left: @SmallSpace; - } + .createNewDatabaseOrUseExistingSpace { + padding-left: @SmallSpace; + } } .throughputModeContainer { - margin-bottom: @SmallSpace; + margin-bottom: @SmallSpace; - .throughputModeRadio { - vertical-align: text-bottom; - } + .throughputModeRadio { + vertical-align: text-bottom; + } - .nonFirstRadio { - margin-left: @LargeSpace; - } + .nonFirstRadio { + margin-left: @LargeSpace; + } - .throughputModeSpace { - padding-left: @SmallSpace; - } + .throughputModeSpace { + padding-left: @SmallSpace; + } } .databaseProvision { - margin-top: @SmallSpace; + margin-top: @SmallSpace; - input { - vertical-align: text-bottom; - } + input { + vertical-align: text-bottom; + } - .databaseProvisionText { - padding-left: @SmallSpace; - } + .databaseProvisionText { + padding-left: @SmallSpace; + } } .infoImg { - margin: 0px 0px 2px 2px; + margin: 0px 0px 2px 2px; } .largePartitionKey { - margin: @SmallSpace 0px; + margin: @SmallSpace 0px; - input { - vertical-align: text-bottom; - } + input { + vertical-align: text-bottom; + } - .largePartitionKeyDescription { - margin: @DefaultSpace 0px 0px; - } + .largePartitionKeyDescription { + margin: @DefaultSpace 0px 0px; + } } .enableAnalyticalStorage { - margin: @SmallSpace 0px; + margin: @SmallSpace 0px; - input { - vertical-align: text-bottom; - } + input { + vertical-align: text-bottom; + } } .infoTooltip { - .infoTooltip(); + .infoTooltip(); } .infoTooltip .tooltiptext { - top: 30px; - .tooltipText(); + top: 30px; + .tooltipText(); } .infoTooltip .tooltiptext::after { - border-width: 0px (2 * @MediumSpace) (2 * @MediumSpace) 0px; - top: -15px; - .tooltipTextAfter(); + border-width: 0px (2 * @MediumSpace) (2 * @MediumSpace) 0px; + top: -15px; + .tooltipTextAfter(); } .infoTooltip:hover .tooltiptext { - .tooltipVisible(); + .tooltipVisible(); } .infoTooltip:focus .tooltiptext { - .tooltipVisible(); + .tooltipVisible(); } .infoTooltip a { - color: @AccentHigh; + color: @AccentHigh; +} + +.inputTooltip { + .inputTooltip(); +} + +.inputTooltip .inputTooltipText { + top: -68px; + .inputTooltipText(); +} + +.inputTooltip .inputTooltipText::after { + border-width: @MediumSpace @MediumSpace 0 @MediumSpace; + top: 55px; + .inputTooltipTextAfter(); } .nowrap { - white-space: nowrap; + white-space: nowrap; } .leftAlignInfoTooltip { - .infoTooltip(); - white-space: normal; + .infoTooltip(); + white-space: normal; - .tooltiptext { - .tooltipText(); - top: 30px; - visibility: hidden; - left: -300px; + .tooltiptext { + .tooltipText(); + top: 30px; + visibility: hidden; + left: -300px; - &::after { - .tooltipTextAfter(); - border-width: 0px 0px (2 * @MediumSpace) (2 * @MediumSpace); - top: -15px; - left: 287px; - } + &::after { + .tooltipTextAfter(); + border-width: 0px 0px (2 * @MediumSpace) (2 * @MediumSpace); + top: -15px; + left: 287px; } + } - &:hover .tooltiptext { - .tooltipVisible(); - } + &:hover .tooltiptext { + .tooltipVisible(); + } } .pageOptionTooltipWidth { - min-width: @optionsInfoWidth; + min-width: @optionsInfoWidth; } .noFixedCollectionsTooltipWidth { - min-width: @noFixedCollectionsTooltipWidth; + min-width: @noFixedCollectionsTooltipWidth; } .infoTooltipWidth { - min-width: @tooltipTextWidth; + min-width: @tooltipTextWidth; } .mongoWildcardIndexTooltipWidth { - min-width: @mongoWildcardIndexTooltipWidth; + min-width: @mongoWildcardIndexTooltipWidth; } .sharedCollectionThroughputTooltipWidth { - min-width: @sharedCollectionThroughputTooltipTextWidth; + min-width: @sharedCollectionThroughputTooltipTextWidth; } .addContainerThroughputInput { - min-width: @addContainerPaneThroughputInfoWidth; + min-width: @addContainerPaneThroughputInfoWidth; } .renewInfoTooltipWidth { - width: @RenewAccessInfoWidth; + width: @RenewAccessInfoWidth; } .throughputInfo { - min-width: @ThroughputInfoWidth; + min-width: @ThroughputInfoWidth; } .throughputRuInfo { - min-width: @ThroughputRuInfoWidth; + min-width: @ThroughputRuInfoWidth; } .provisionDatabaseThroughput { - width: @provisionDatabaseThroughputInfo; + width: @provisionDatabaseThroughputInfo; } .pricingtierimg { - padding-left: 20px; - padding-top: 10px; - padding-bottom: 20px; + padding-left: 20px; + padding-top: 10px; + padding-bottom: 20px; } .headerline { - color: @BaseDark; - font-size: 16px; - border-bottom: 1px solid @BaseMedium; - height: 41px; + color: @BaseDark; + font-size: 16px; + border-bottom: 1px solid @BaseMedium; + height: 41px; } .partitionkeystyle { - font-size: 10px; + font-size: 10px; } .arrowprice { - margin-left: 230px; + margin-left: 230px; } .paddingspan { - padding: 20px; - color: white; - font-size: 14px; + padding: 20px; + color: white; + font-size: 14px; } .contextual-pane .paddingspan3 { - padding-left: 0px; - position: absolute; - width: 100%; - height: 100px; - bottom: -40px; - margin: 0 -15px; - border-top: solid 1px #bbbbbb; - margin-left: -32px; + padding-left: 0px; + position: absolute; + width: 100%; + height: 100px; + bottom: -40px; + margin: 0 -15px; + border-top: solid 1px #bbbbbb; + margin-left: -32px; } - /* Variant of paddingspan3 without the margins */ .contextual-pane .paddingspan3b { - padding-left: 0px; - position: absolute; - width: 100%; - height: 100px; - bottom: -40px; - border-top: solid 1px #bbbbbb; + padding-left: 0px; + position: absolute; + width: 100%; + height: 100px; + bottom: -40px; + border-top: solid 1px #bbbbbb; } .contextual-pane hr { - border: 1px solid #53575B; - margin-right: 20px; + border: 1px solid #53575b; + margin-right: 20px; } .contextual-pane .tabs .tab label { - padding: 5px 20px; - margin-bottom: 0px; + padding: 5px 20px; + margin-bottom: 0px; } .contextual-pane .collid { - border: 1px solid #605e5c; - font-size: 10px; - padding: 5px 10px; - color: #000; + border: 1px solid #605e5c; + font-size: 10px; + padding: 5px 10px; + color: #000; } .contextual-pane .select-font-size { - font-size: 12px; + font-size: 12px; } input::-webkit-calendar-picker-indicator { - opacity: 100; + opacity: 100; } .contextual-pane input.collid[type="text"] { - font-size: 12px; - /* color: #000; */ - padding: 4px 10px; + font-size: 12px; + /* color: #000; */ + padding: 4px 10px; } .contextual-pane textarea.collid { - font-size: 12px; + font-size: 12px; } /* Start -- Contextual pane components @@ -1691,650 +1706,650 @@ input::-webkit-calendar-picker-indicator { */ .contextual-pane .paneContentContainer { - display: flex; - flex-direction: column; - height: 100%; + display: flex; + flex-direction: column; + height: 100%; } .contextual-pane .paneErrorDetailsContainer { - display: flex; - flex-direction: column; - height: 100vh; + display: flex; + flex-direction: column; + height: 100vh; } .contextual-pane .paneErrorDetails { - padding: 16px 32px; - color: #000; - overflow-x: hidden; - overflow-y: auto; - flex: 1; + padding: 16px 32px; + color: #000; + overflow-x: hidden; + overflow-y: auto; + flex: 1; } .contextual-pane .paneErrorDetailsHeader { - display: flex; - padding-top: 7px; - padding-bottom: 12px; - height: 46px; - background-color: #000; + display: flex; + padding-top: 7px; + padding-bottom: 12px; + height: 46px; + background-color: #000; } .contextual-pane .backBtn { - cursor: pointer; + cursor: pointer; } .contextual-pane .backBtn img { - width: 18px; - height: 18px; - margin-bottom: @SmallSpace; + width: 18px; + height: 18px; + margin-bottom: @SmallSpace; } .contextual-pane .moreDetails { - padding-left: @DefaultSpace; + padding-left: @DefaultSpace; } .contextual-pane .paneErrorDetailsHeader .errorDetailsTitle { - flex: 1; - padding-left: @DefaultSpace; + flex: 1; + padding-left: @DefaultSpace; } .contextual-pane .paneErrors a { - cursor: pointer; + cursor: pointer; } .contextual-pane .paneMainContent { - flex: 1; - padding-left: 34px; - padding-right: 34px; - color: @BaseDark; - overflow-y: auto; - overflow-x: auto; - margin: (2 * @MediumSpace) 0px; + flex: 1; + padding-left: 34px; + padding-right: 34px; + color: @BaseDark; + overflow-y: auto; + overflow-x: auto; + margin: (2 * @MediumSpace) 0px; } .contextual-pane .panelMainContent { - padding-left: 34px; - padding-right: 34px; - color: @BaseDark; - margin: (2 * @MediumSpace) 0px; + padding-left: 34px; + padding-right: 34px; + color: @BaseDark; + margin: (2 * @MediumSpace) 0px; } .contextual-pane .paneFooter { - width: 100%; - height: 60px; - border-top: solid 1px #bbbbbb; + width: 100%; + height: 60px; + border-top: solid 1px #bbbbbb; } /* End -- Contextual pane components */ .paddingspan3 { - color: white; - font-size: 14px; - position: absolute; - width: 100%; - height: 100px; - bottom: 150px; + color: white; + font-size: 14px; + position: absolute; + width: 100%; + height: 100px; + bottom: 150px; } .paddingspan4 { - padding-top: 20px; - padding-left: 20px; - color: white; - font-size: 14px; + padding-top: 20px; + padding-left: 20px; + color: white; + font-size: 14px; } .closebtnn { - float: right; - padding: 0 10px; - cursor: pointer; + float: right; + padding: 0 10px; + cursor: pointer; } label { - white-space: nowrap; - font: 12px "Segoe UI"; - padding: 5px 25px; + white-space: nowrap; + font: 12px "Segoe UI"; + padding: 5px 25px; } .Introlines { - padding-top: 27px; - padding-left: 40px; + padding-top: 27px; + padding-left: 40px; } .Introline1 { - font-size: 16px; + font-size: 16px; } .Introline2 { - font-size: 14px; - padding-top: 10px; + font-size: 14px; + padding-top: 10px; } .datalist-arrow { - position: relative; + position: relative; } .datalist-arrow:hover:after { - background: #969696; + background: #969696; } .datalist-arrow:focus:after, .datalist-arrow:active:after { - background: #1EBBEE; + background: #1ebbee; } input::-webkit-calendar-picker-indicator::after { - content: '\276F'; - right: 0; - top: -8%; - display: block; - width: 27px; - height: 25px; - line-height: 25px; - color: #fff; - text-align: center; - pointer-events: none; - transform: rotate(90deg); + content: "\276F"; + right: 0; + top: -8%; + display: block; + width: 27px; + height: 25px; + line-height: 25px; + color: #fff; + text-align: center; + pointer-events: none; + transform: rotate(90deg); } .datalist-arrow:after:hover { - content: '\276F'; - position: absolute; - right: 1px; - top: 6%; - transform: rotate(90deg); - display: block; - width: 27px; - height: 25px; - line-height: 25px; - color: #fff; - text-align: center; - pointer-events: none; - background-color: #1EBBEE; + content: "\276F"; + position: absolute; + right: 1px; + top: 6%; + transform: rotate(90deg); + display: block; + width: 27px; + height: 25px; + line-height: 25px; + color: #fff; + text-align: center; + pointer-events: none; + background-color: #1ebbee; } .Introline3 { - padding-top: 10px; - font-size: 14px; - font-weight: 1000; + padding-top: 10px; + font-size: 14px; + font-weight: 1000; } .collectionsTreeWithSplitter { - height: 100%; + height: 100%; } .collectionCollapsed { - color: black; - font-weight: 400; - font-size: 14px; - position: relative; - display: block; - padding: 0px 8px 4px 4px; - cursor: pointer; - float: right; + color: black; + font-weight: 400; + font-size: 14px; + position: relative; + display: block; + padding: 0px 8px 4px 4px; + cursor: pointer; + float: right; } .resourceTreeCollapse { - margin-right: 2px; - padding: 2px 5px 0px 5px; - border: 1px solid #fff; + margin-right: 2px; + padding: 2px 5px 0px 5px; + border: 1px solid #fff; } .resourceTreeCollapse:hover { - background-color: @BaseLow; + background-color: @BaseLow; } .resourceTreeCollapse:active { - background-color: @AccentMediumLow; - outline: none; + background-color: @AccentMediumLow; + outline: none; } .arrowCollapsed { - cursor: pointer; - width: 16px; - height: 16px; - transform: rotate(-90deg) translateX(-50%); - -webkit-transform: rotate(-90deg) translateX(-50%); - -ms-transform: rotate(-90deg) translateX(-50%); - float: right; + cursor: pointer; + width: 16px; + height: 16px; + transform: rotate(-90deg) translateX(-50%); + -webkit-transform: rotate(-90deg) translateX(-50%); + -ms-transform: rotate(-90deg) translateX(-50%); + float: right; } .qslevel { - padding-top: 10px; - border: none; + padding-top: 10px; + border: none; } -.qslevel>li>a { - border: none !important; +.qslevel > li > a { + border: none !important; } -.qslevel>li.active { - border-bottom: 4px solid #767676; +.qslevel > li.active { + border-bottom: 4px solid #767676; } .nav-tabs-margin { - padding-top: 8px; - background-color: #F2F2F2; + padding-top: 8px; + background-color: #f2f2f2; } .navTabHeight { - height: 31px; + height: 31px; } -.qslevel>li.active>a, -.qslevel>li>a:focus, -.nav.nav-tabs.qslevel>li>a:hover { - border: none; - border-radius: 0; - background-color: transparent !important; - border-color: transparent; +.qslevel > li.active > a, +.qslevel > li > a:focus, +.nav.nav-tabs.qslevel > li > a:hover { + border: none; + border-radius: 0; + background-color: transparent !important; + border-color: transparent; } .numbersize { - font-size: 60px; - display: inline; + font-size: 60px; + display: inline; } .numberheading { - display: inline; - position: absolute; - padding-top: 20px; - font-size: 16px; - padding-left: 20px; + display: inline; + position: absolute; + padding-top: 20px; + font-size: 16px; + padding-left: 20px; } -.numberheading>p { - padding-top: 10px; - font-size: 12px !important; +.numberheading > p { + padding-top: 10px; + font-size: 12px !important; } -.numberheading>ul { - padding-top: 10px; +.numberheading > ul { + padding-top: 10px; } -.numberheading>ul>li>a { - font-size: 12px !important; +.numberheading > ul > li > a { + font-size: 12px !important; } .step1 { - padding-bottom: 60px; + padding-bottom: 60px; } -.step1>input { - font-size: 12px; +.step1 > input { + font-size: 12px; } .btncreatecoll { - background: @AccentMediumHigh; - color: #fff; - padding: 0 20px; - cursor: pointer; - font-size: 12px; - border: 1px solid @AccentMediumHigh; + background: @AccentMediumHigh; + color: #fff; + padding: 0 20px; + cursor: pointer; + font-size: 12px; + border: 1px solid @AccentMediumHigh; } .atags:focus { - color: @AccentMediumHigh; + color: @AccentMediumHigh; } .atags { - color: @AccentMediumHigh; - font-weight: 400; - cursor: pointer + color: @AccentMediumHigh; + font-weight: 400; + cursor: pointer; } .qsmenuicons { - width: 25px; - height: 25px; - margin-right: 5px; + width: 25px; + height: 25px; + margin-right: 5px; } .HeaderBg { - background-color: #202428; - height: 60px; + background-color: #202428; + height: 60px; } .title { - color: @AccentMediumHigh; - font-size: 20px; - padding-left: 10px; + color: @AccentMediumHigh; + font-size: 20px; + padding-left: 10px; } .items { - padding-left: 24px; - padding-top: 15px; + padding-left: 24px; + padding-top: 15px; } .divmenuquickstartpadding { - padding-left: 24px; - padding-bottom: 8px; + padding-left: 24px; + padding-bottom: 8px; } .menuQuickStart { - font-size: 12px; - color: white; - padding-left: 10px; + font-size: 12px; + color: white; + padding-left: 10px; } .menuExplorer { - font-size: 12px; - color: white; - padding-left: 20px; + font-size: 12px; + color: white; + padding-left: 20px; } .activemenu { - color: #fff; + color: #fff; } .rightarrowimg { - padding-left: 5px; - padding-bottom: 2px; + padding-left: 5px; + padding-bottom: 2px; } .underlinedLink { - text-decoration: underline !important; - color: @SelectionColor; + text-decoration: underline !important; + color: @SelectionColor; } a:hover, a:visited, a:active, a:link { - text-decoration: none; + text-decoration: none; } -.nav>li>a:focus { - background-color: white; - outline: none; +.nav > li > a:focus { + background-color: white; + outline: none; } .iconpadclick { - background-color: #e6e6e6; - cursor: pointer; - border: 1px solid #1ebbee; - padding: 5px; + background-color: #e6e6e6; + cursor: pointer; + border: 1px solid #1ebbee; + padding: 5px; } .divimgleftarrow { - display: inline-block; - margin-top: 16px; - margin-right: 10px; + display: inline-block; + margin-top: 16px; + margin-right: 10px; } .divimgleftarrow:hover { - background-color: #e6e6e6; - cursor: pointer; - border: 1px solid #1ebbee; + background-color: #e6e6e6; + cursor: pointer; + border: 1px solid #1ebbee; } .adddeliconspan { - display: none; - float: right; - padding: 5px; + display: none; + float: right; + padding: 5px; } .spanparent:hover .adddeliconspan { - display: inline; + display: inline; } .spanchild:hover .adddeliconspan { - display: inline; + display: inline; } .resourceTreeAndTabs { - display: flex; - flex: 1 1 auto; - overflow-x: auto; - overflow-y: hidden; - height: 100%; + display: flex; + flex: 1 1 auto; + overflow-x: auto; + overflow-y: hidden; + height: 100%; } .collectiontitle { - font-size: 14px; - text-transform: uppercase; + font-size: 14px; + text-transform: uppercase; } .coltitle { - background: white; - text-align: justify; - padding: @SmallSpace 0px @DefaultSpace 0px; + background: white; + text-align: justify; + padding: @SmallSpace 0px @DefaultSpace 0px; } .titlepadcol { - padding-left: 20px; - font-weight: 500; - height: 28px; - display: inline-block; - padding-top: 5px; + padding-left: 20px; + font-weight: 500; + height: 28px; + display: inline-block; + padding-top: 5px; } .padimgcolrefresh { - padding: 0px 0px 4px 4px; + padding: 0px 0px 4px 4px; } .padimgcolrefresh:hover { - background: @BaseLow; + background: @BaseLow; } .padimgcolrefresh:active { - background: @AccentMediumLow; - outline: none; + background: @AccentMediumLow; + outline: none; } .refreshcol { - cursor: pointer; - width: 14px; - height: 14px; + cursor: pointer; + width: 14px; + height: 14px; } .refreshcol:focus { - border: Solid 1px @AccentMediumHigh; + border: Solid 1px @AccentMediumHigh; } .refreshcol1 { - cursor: pointer; - width: 16px; - height: 16px; + cursor: pointer; + width: 16px; + height: 16px; } .padimgcolrefresh1 { - padding: 0px 4px 4px 4px; + padding: 0px 4px 4px 4px; } .padimgcolrefresh1:hover { - background-color: @BaseLow; + background-color: @BaseLow; } .padimgcolrefresh1:active { - background-color: @AccentMediumLow; - outline: none; + background-color: @AccentMediumLow; + outline: none; } .btnmainslide { - height: 14px; - margin-top: 14px; - cursor: pointer; + height: 14px; + margin-top: 14px; + cursor: pointer; } .well { - padding: 19px 0px; - padding-top: 0px; - margin-bottom: 20px; - border: 0px; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0); - background: white; + padding: 19px 0px; + padding-top: 0px; + margin-bottom: 20px; + border: 0px; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0); + background: white; } .splitter, .ui-resizable-handle.ui-resizable-e { - z-index: 1; - width: 5px; - border-right: 1px solid @BaseMedium; - border-bottom: none; - padding: 0px; - background-color: @BaseLight; + z-index: 1; + width: 5px; + border-right: 1px solid @BaseMedium; + border-bottom: none; + padding: 0px; + background-color: @BaseLight; } .splitter, .ui-resizable-handle.ui-resizable-s { - z-index: 1; - height: 5px; - border-bottom: 1px solid @BaseMedium; - border-right: none; - padding: 0px; - background-color: transparent; + z-index: 1; + height: 5px; + border-bottom: 1px solid @BaseMedium; + border-right: none; + padding: 0px; + background-color: transparent; } .ui-resizable-helper { - border: 1px dotted; + border: 1px dotted; } .testClass { - padding-left: 30px; + padding-left: 30px; } .level { - padding-left: 16px; - padding-top: 0px; + padding-left: 16px; + padding-top: 0px; } .tabCommandButton { - border-bottom: 1px solid #ddd; - box-sizing: border-box; - padding-left: @DefaultSpace; - .flex-display(); - flex: 0 0 auto; + border-bottom: 1px solid #ddd; + box-sizing: border-box; + padding-left: @DefaultSpace; + .flex-display(); + flex: 0 0 auto; } .imgiconwidth { - margin-right: 5px; + margin-right: 5px; } .id { - padding-left: 8px; - color: #000; - font-weight: bold; - margin-left: 6px; + padding-left: 8px; + color: #000; + font-weight: bold; + margin-left: 6px; } .documentsGridHeaderContainer { - padding-left: 5px; - width: 100%; - border-bottom: 1px solid #CCCCCC; + padding-left: 5px; + width: 100%; + border-bottom: 1px solid #cccccc; } -.documentsGridHeaderContainer>table { - width: 100%; - table-layout: fixed; - border-collapse: unset; +.documentsGridHeaderContainer > table { + width: 100%; + table-layout: fixed; + border-collapse: unset; } .documentsGridHeaderContainer table thead tr { + position: sticky; + top: 0; + th { position: sticky; top: 0; - th { - position: sticky; - top: 0; - background-color: #fff !important; - border-bottom: 1px solid #CCCCCC !important; - } + background-color: #fff !important; + border-bottom: 1px solid #cccccc !important; + } } .documentsGridHeader { - padding-left: 8px; - color: #000; - font-weight: bold; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - cursor: default; - width: 45%; + padding-left: 8px; + color: #000; + font-weight: bold; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: default; + width: 45%; } .refreshColHeader { - padding: 3px 6px 6px 6px; + padding: 3px 6px 6px 6px; } .refreshColHeader:hover { - background-color: @BaseLow; + background-color: @BaseLow; } .refreshColHeader:active { - background-color: @AccentMediumLow; + background-color: @AccentMediumLow; } .refreshColHeader .refreshcol { - float: right; + float: right; } .fixedWidthHeader { - width: 82px; + width: 82px; } .tabdocuments .scrollable { - height: 100%; - overflow-y: auto; - overflow-x: hidden; - padding-left: 5px; - padding-right: 5px; - width:100%; + height: 100%; + overflow-y: auto; + overflow-x: hidden; + padding-left: 5px; + padding-right: 5px; + width: 100%; } -.tabdocuments>.tabdocumentsGridElement { - width: 50%; +.tabdocuments > .tabdocumentsGridElement { + width: 50%; } -.tabdocuments>.evenlySpacedHeader { - width: 30%; +.tabdocuments > .evenlySpacedHeader { + width: 30%; } .tabdocuments.scrollable:focus, .tabdocuments.scrollable:active { - outline: 1px dotted; + outline: 1px dotted; } .tabdocuments .scrollable table td { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .mongoDocumentEditor .monaco-editor.vs .redsquiggly { - display: none !important; + display: none !important; } td a { - color: #393939; + color: #393939; } td a:hover { - color: #393939; + color: #393939; } .loadMore { - width: 100%; - padding-left: 30%; - padding-top: 2px; - cursor: pointer; + width: 100%; + padding-left: 30%; + padding-top: 2px; + cursor: pointer; } -.loadMore>a:focus { - outline: 1px dotted; +.loadMore > a:focus { + outline: 1px dotted; } #content.active .tabdocuments .scrollable { - height: 100%; - overflow-y: auto; + height: 100%; + overflow-y: auto; } .table-fixed thead { - width: 97%; - padding-left: 18px; + width: 97%; + padding-left: 18px; } .table-fixed tbody { - height: 510px; - overflow-y: auto; - width: 100%; - overflow-x: hidden; + height: 510px; + overflow-y: auto; + width: 100%; + overflow-x: hidden; } .table-fixed thead, @@ -2342,560 +2357,560 @@ td a:hover { .table-fixed tr, .table-fixed td, .table-fixed th { - display: block; + display: block; } .table-fixed tbody td, -.table-fixed thead>tr>th { - float: left; - border-bottom-width: 0; +.table-fixed thead > tr > th { + float: left; + border-bottom-width: 0; } a:hover, a:visited, a:active, a:link { - text-decoration: none; + text-decoration: none; } .tabsManagerContainer { - height: 100%; - flex-grow: 1; - overflow: hidden; + height: 100%; + flex-grow: 1; + overflow: hidden; } .tabs { - position: relative; - margin: 15px 0 25px 0; - display: table; - width: 100%; + position: relative; + margin: 15px 0 25px 0; + display: table; + width: 100%; } .tab { - float: left; + float: left; } .tab label { - border: 1px solid #bbbbbb; - margin-left: -1px; - position: inherit; - left: 1px; - color: #393939; + border: 1px solid #bbbbbb; + margin-left: -1px; + position: inherit; + left: 1px; + color: #393939; } -.tab [type=radio] { - display: none; +.tab [type="radio"] { + display: none; } .tabcontent { - clear:both; - left: 0; - right: 0; - bottom: 0; - padding: @MediumSpace 0px; + clear: both; + left: 0; + right: 0; + bottom: 0; + padding: @MediumSpace 0px; } -.tab [type=radio]:checked~label { - border: 1px solid #0072c6; - background-color: @AccentMediumHigh; - color: white; - z-index: 2; +.tab [type="radio"]:checked ~ label { + border: 1px solid #0072c6; + background-color: @AccentMediumHigh; + color: white; + z-index: 2; } -.tab [type=radio]:checked~label:hover { - border: 1px solid @AccentMediumHigh; - background-color: @AccentMediumHigh; - color: white; - z-index: 2; +.tab [type="radio"]:checked ~ label:hover { + border: 1px solid @AccentMediumHigh; + background-color: @AccentMediumHigh; + color: white; + z-index: 2; } -.tab [type=radio]:checked~label:active { - border: 1px solid #0072c6; - background-color: #0072c6; - color: white; - z-index: 2; +.tab [type="radio"]:checked ~ label:active { + border: 1px solid #0072c6; + background-color: #0072c6; + color: white; + z-index: 2; } -.tab [type=radio]:checked~label~.tabcontent { - z-index: 1; - display: initial; +.tab [type="radio"]:checked ~ label ~ .tabcontent { + z-index: 1; + display: initial; } -.tab [type=radio]:not(:checked)~label:hover { - border: 1px solid #969696; - background-color: #969696; - color: white; - cursor: pointer; +.tab [type="radio"]:not(:checked) ~ label:hover { + border: 1px solid #969696; + background-color: #969696; + color: white; + cursor: pointer; } -.tab [type=radio]:not(:checked)~label~.tabcontent { - display: none; +.tab [type="radio"]:not(:checked) ~ label ~ .tabcontent { + display: none; } ::-ms-expand { - color: #969696; + color: #969696; } .atagdetails { - padding-left: 55px!important; + padding-left: 55px !important; } -.contextual-pane-in .form-errors+img { - display: block; - position: absolute; - top: 92px; - left: 12px; +.contextual-pane-in .form-errors + img { + display: block; + position: absolute; + top: 92px; + left: 12px; } .path { - color: lightgray; - font-style: italic; - padding-top: 12px; - padding-left: 20px; + color: lightgray; + font-style: italic; + padding-top: 12px; + padding-left: 20px; } .queryPath { - line-height: 16px; - padding-left: 33px; - padding-bottom: 12px; + line-height: 16px; + padding-left: 33px; + padding-bottom: 12px; } .filterDocCollapsed { - .flex-display(); - padding: 0px 36px 0px 20px; + .flex-display(); + padding: 0px 36px 0px 20px; } .filterDocCollapsed.active { - display: block; + display: block; } .selectQuery { - padding: @SmallSpace @SmallSpace 0px 0px; + padding: @SmallSpace @SmallSpace 0px 0px; } .noFilterApplied { - padding-top: @SmallSpace; + padding-top: @SmallSpace; } .appliedQuery { - overflow: hidden; - text-overflow: ellipsis; - padding-top: @SmallSpace; + overflow: hidden; + text-overflow: ellipsis; + padding-top: @SmallSpace; } .filterDocExpanded { - padding-left: 20px; + padding-left: 20px; } .filterDocExpanded.active { - display: block; + display: block; } .filterbtnstyle { - background: @AccentMediumHigh; - width: 90px; - height: 25px; - color: white; - border: solid 1px; + background: @AccentMediumHigh; + width: 90px; + height: 25px; + color: white; + border: solid 1px; } .filterbtnstyle:hover { - background: @AccentMediumHigh; - width: 90px; - height: 25px; - color: white; - border: none; + background: @AccentMediumHigh; + width: 90px; + height: 25px; + color: white; + border: none; } .filterbtnstyle:active { - background: #0072c6; - width: 90px; - height: 25px; - color: white; - border: none; + background: #0072c6; + width: 90px; + height: 25px; + color: white; + border: none; } .filterbtnstyle:focus { - background: #0072c6; - width: 90px; - height: 25px; - color: white; - border: none; - border: 1px solid #0072c6; + background: #0072c6; + width: 90px; + height: 25px; + color: white; + border: none; + border: 1px solid #0072c6; } .filterbtnstyle:not(:enabled) { - background: lightgray; - width: 90px; - height: 25px; - color: white; - border: none; + background: lightgray; + width: 90px; + height: 25px; + color: white; + border: none; } -.queryButton{ - margin-left:@LargeSpace; +.queryButton { + margin-left: @LargeSpace; } .hrline1 { - color: #d6d7d8; - margin-left: -20px; + color: #d6d7d8; + margin-left: -20px; } .filtdocheader { - font-size: 18px; + font-size: 18px; } .editFilter { - margin-left: 20px; + margin-left: 20px; } .filterdivs { - padding-top: 15px; - height: 45px; - margin-bottom: 8px; - white-space: nowrap; + padding-top: 15px; + height: 45px; + margin-bottom: 8px; + white-space: nowrap; } .editFilterContainer { - display: flex; + display: flex; } .filterspan { - margin-top: @SmallSpace; - padding: 0px @LargeSpace 0px 0px; + margin-top: @SmallSpace; + padding: 0px @LargeSpace 0px 0px; } .filterclose { - padding: @SmallSpace 10px; - cursor: pointer; + padding: @SmallSpace 10px; + cursor: pointer; } .querydropdown { - border: 1px solid @BaseMedium; - font-style: normal; - width: 100%; + border: 1px solid @BaseMedium; + font-style: normal; + width: 100%; } .querydropdown.placeholderVisible { - font-style: italic; + font-style: italic; } .querydropdown:hover { - background-color: @AccentLow; + background-color: @AccentLow; } .querydropdown::-webkit-input-placeholder { - color: lightgray; - padding-left: 3px; + color: lightgray; + padding-left: 3px; } .querydropdown:-moz-placeholder { - /* Firefox 18- */ - color: lightgray; + /* Firefox 18- */ + color: lightgray; } .querydropdown::-moz-placeholder { - /* Firefox 19+ */ - color: lightgray; + /* Firefox 19+ */ + color: lightgray; } .querydropdown:-ms-input-placeholder { - color: lightgray; - padding-left: 7px; + color: lightgray; + padding-left: 7px; } .rowoverride { - margin-left: 7px; - margin-top: 20px; + margin-left: 7px; + margin-top: 20px; } .tabPanesContainer { - display: flex; - height: 100%; - overflow: hidden; + display: flex; + height: 100%; + overflow: hidden; } .tabs-container { - height: 100%; - width: 100%; + height: 100%; + width: 100%; } .paddingspan4 { - padding-top: 20px; - color: white; - padding-left: 25px; - padding-right: 25px; + padding-top: 20px; + color: white; + padding-left: 25px; + padding-right: 25px; } .colResizePointer { - cursor: col-resize; + cursor: col-resize; } -.nav-tabs>li.active>.tabNavContentContainer, -.nav-tabs>li.active>.tabNavContentContainer:focus, -.nav-tabs>li.active>.tabNavContentContainer:hover { - color: #555; - cursor: default; - background-color: @BaseLight; - border-color: @BaseMedium; - border-bottom-color: @BaseLight; - border-style: solid; - border-width: 1px; - height: @ActiveTabHeight; - width: @ActiveTabWidth; +.nav-tabs > li.active > .tabNavContentContainer, +.nav-tabs > li.active > .tabNavContentContainer:focus, +.nav-tabs > li.active > .tabNavContentContainer:hover { + color: #555; + cursor: default; + background-color: @BaseLight; + border-color: @BaseMedium; + border-bottom-color: @BaseLight; + border-style: solid; + border-width: 1px; + height: @ActiveTabHeight; + width: @ActiveTabWidth; } -.nav-tabs>li.active:focus>.tabNavContentContainer { - .focus(); +.nav-tabs > li.active:focus > .tabNavContentContainer { + .focus(); } .tabNavContentContainer { + .flex-display(); + height: @TabsHeight; + justify-content: space-between; + border-radius: 2px 2px 0 0; + padding: @DefaultSpace 0px @SmallSpace 0px; + color: @BaseHigh; + width: @TabsWidth; + text-align: center; + position: relative; + border: @ButtonBorderWidth solid transparent; + + &:hover { + text-decoration: none; + background-color: @BaseMediumLow; + border-color: @BaseMediumLow; + } + + &:active { + background-color: @BaseMediumLow; + } + + .tab_Content { .flex-display(); - height: @TabsHeight; - justify-content: space-between; - border-radius: 2px 2px 0 0; - padding: @DefaultSpace 0px @SmallSpace 0px; - color: @BaseHigh; width: @TabsWidth; - text-align: center; - position: relative; - border: @ButtonBorderWidth solid transparent; + border-right: @ButtonBorderWidth solid @BaseMedium; + white-space: nowrap; - &:hover { - text-decoration: none; - background-color: @BaseMediumLow; - border-color: @BaseMediumLow; - } + .statusIconContainer { + width: @StatusIconContainerSize; + height: @StatusIconContainerSize; + margin-left: @SmallSpace; + display: inline-flex; - &:active { - background-color: @BaseMediumLow; - } + .errorIconContainer { + width: @ErrorIconContainer; + height: @ErrorIconContainer; + margin-top: 1px; - .tab_Content { - .flex-display(); - width: @TabsWidth; - border-right: @ButtonBorderWidth solid @BaseMedium; - white-space: nowrap; + .errorIcon { + width: @ErrorIconWidth; + height: @LoadingErrorIconSize; + background-image: url(../images/error_no_outline.svg); + background-repeat: no-repeat; + background-position: center; + background-size: 3px; + display: block; + margin: 1px 0px 0px 6px; + } + } - .statusIconContainer { - width: @StatusIconContainerSize; - height: @StatusIconContainerSize; - margin-left: @SmallSpace; - display: inline-flex; - - .errorIconContainer { - width: @ErrorIconContainer; - height: @ErrorIconContainer; - margin-top: 1px; - - .errorIcon { - width: @ErrorIconWidth; - height: @LoadingErrorIconSize; - background-image: url(../images/error_no_outline.svg); - background-repeat: no-repeat; - background-position: center; - background-size: 3px; - display: block; - margin: 1px 0px 0px 6px; - } - } - - .errorIconContainer.actionsEnabled { - &:hover { - .hover(); - } - - &:focus { - .focus(); - } - - &:active { - .active(); - } - } - - .errorIconContainer[tabindex]:active { - outline: none; - } - - .loadingIcon { - width: @LoadingErrorIconSize; - height: @LoadingErrorIconSize; - margin: 0px 0px @SmallSpace @SmallSpace; - } + .errorIconContainer.actionsEnabled { + &:hover { + .hover(); } - .tabNavText { - margin-left: @SmallSpace; - margin-right: 2px; - color: @BaseDark; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - flex-grow: 1 + &:focus { + .focus(); } - .tabIconSection { - width: 30px; - position: relative; - padding-top: 2px; - - .cancelButton { - padding: 0px @SmallSpace 0px @SmallSpace; - - &:hover { - .hover(); - } - - &:focus { - .focus(); - } - - &:active { - .active(); - } - } + &:active { + .active(); } + } + + .errorIconContainer[tabindex]:active { + outline: none; + } + + .loadingIcon { + width: @LoadingErrorIconSize; + height: @LoadingErrorIconSize; + margin: 0px 0px @SmallSpace @SmallSpace; + } } + + .tabNavText { + margin-left: @SmallSpace; + margin-right: 2px; + color: @BaseDark; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + flex-grow: 1; + } + + .tabIconSection { + width: 30px; + position: relative; + padding-top: 2px; + + .cancelButton { + padding: 0px @SmallSpace 0px @SmallSpace; + + &:hover { + .hover(); + } + + &:focus { + .focus(); + } + + &:active { + .active(); + } + } + } + } } .cancelButton[tabindex]:active { - outline: none; + outline: none; } .clickableLink { - color: @AccentMediumHigh; - font-family: @DataExplorerFont; - font-size: 12px; - cursor: pointer; + color: @AccentMediumHigh; + font-family: @DataExplorerFont; + font-size: 12px; + cursor: pointer; } .clickableLink:hover { - background-color: #e7f6fc; + background-color: #e7f6fc; } .clickableLink:active { - background-color: #e6f8fe; + background-color: #e6f8fe; } .clickableLink:focus { - outline: 1px dashed #000000; - outline-offset: 0px; + outline: 1px dashed #000000; + outline-offset: 0px; } .paneselect { - height: 23px; + height: 23px; } @media @screen { - .commandBar-ie { - padding-top: 7px; - } - .filterspan { - margin: 0px; - padding-top: 2px; - } - .tabdocuments .scrollable { - height: 100%; - } + .commandBar-ie { + padding-top: 7px; + } + .filterspan { + margin: 0px; + padding-top: 2px; + } + .tabdocuments .scrollable { + height: 100%; + } - .nav-tabs>li>a:active { - background-color: #e0e0e0; - width: 100%; - border: 1px solid @AccentMediumHigh; - cursor: pointer; - } + .nav-tabs > li > a:active { + background-color: #e0e0e0; + width: 100%; + border: 1px solid @AccentMediumHigh; + cursor: pointer; + } } .headerWithoutPartitionKey { - width: 172px; + width: 172px; } .headerWithPartitionKey { - width: 86px; + width: 86px; } .nodeIconSet { - color: black; - margin-left: 7px; - padding-left: 5px; + color: black; + margin-left: 7px; + padding-left: 5px; } .tabCommandDisabled { - color: #CCCCCC; - cursor: default; - background-color: #FFFFFF; + color: #cccccc; + cursor: default; + background-color: #ffffff; } .tabCommandDisabled:active { - border: 1px solid #FFFFFF; + border: 1px solid #ffffff; } .tabCommandDisabled:hover { - background-color: #FFFFFF; + background-color: #ffffff; } #explorerNotificationConsole { - z-index: 1000; + z-index: 1000; } .uniqueIndexesContainer { - width: 100%; + width: 100%; - .uniqueKeys { - padding-bottom: @SmallSpace; + .uniqueKeys { + padding-bottom: @SmallSpace; - .uniqueInfoTooltip { - .infoTooltip(); + .uniqueInfoTooltip { + .infoTooltip(); - &:hover .uniqueTooltiptext { - .tooltipVisible(); - } + &:hover .uniqueTooltiptext { + .tooltipVisible(); + } - &:focus .uniqueTooltiptext { - .tooltipVisible(); - } + &:focus .uniqueTooltiptext { + .tooltipVisible(); + } - .uniqueTooltiptext { - bottom:28px; - .tooltipText(); - } + .uniqueTooltiptext { + bottom: 28px; + .tooltipText(); + } - .uniqueTooltiptext::after { - border-width: (2 * @MediumSpace) (2 * @MediumSpace) 0px 0px; - bottom: -15px; - .tooltipTextAfter(); - } - } + .uniqueTooltiptext::after { + border-width: (2 * @MediumSpace) (2 * @MediumSpace) 0px 0px; + bottom: -15px; + .tooltipTextAfter(); + } } + } } settings-pane { - .settingsSection { - border-bottom: 1px solid @BaseMedium; - margin-right: 24px; - padding: @MediumSpace 0px; + .settingsSection { + border-bottom: 1px solid @BaseMedium; + margin-right: 24px; + padding: @MediumSpace 0px; - &:first-child { - padding-top: 0px; - } - - &:last-child { - border-bottom: none; - } - - .settingsSectionPart { - padding-left: 8px; - } - - .settingsSectionLabel { - margin-bottom: @DefaultSpace; - } - - .pageOptionsPart { - padding-bottom: @MediumSpace; - } + &:first-child { + padding-top: 0px; } + + &:last-child { + border-bottom: none; + } + + .settingsSectionPart { + padding-left: 8px; + } + + .settingsSectionLabel { + margin-bottom: @DefaultSpace; + } + + .pageOptionsPart { + padding-bottom: @MediumSpace; + } + } } // TODO: Remove these styles once we refactor all buttons to use the command button component @@ -2903,127 +2918,169 @@ settings-pane { @ButtonBorderWidth: 1px; .commandButton { - padding: 6px @DefaultSpace @DefaultSpace; - border: @ButtonBorderWidth solid transparent; - color: @BaseHigh; - background-color: transparent; + padding: 6px @DefaultSpace @DefaultSpace; + border: @ButtonBorderWidth solid transparent; + color: @BaseHigh; + background-color: transparent; - &:hover:not(.commandDisabled) { - background-color: @AccentLight; - cursor: pointer; - } + &:hover:not(.commandDisabled) { + background-color: @AccentLight; + cursor: pointer; + } - &:active:not(.commandDisabled) { - background-color: @AccentExtra; - border: @ButtonBorderWidth dashed @AccentMedium; - } + &:active:not(.commandDisabled) { + background-color: @AccentExtra; + border: @ButtonBorderWidth dashed @AccentMedium; + } - &:focus:not(.commandDisabled) { - border: @ButtonBorderWidth dashed @AccentMedium; - } + &:focus:not(.commandDisabled) { + border: @ButtonBorderWidth dashed @AccentMedium; + } - .commandIcon { - margin: 0 @SmallSpace 0 0; - vertical-align: text-top; - width: @ButtonIconSize; - height: @ButtonIconSize; - } + .commandIcon { + margin: 0 @SmallSpace 0 0; + vertical-align: text-top; + width: @ButtonIconSize; + height: @ButtonIconSize; + } } .commandButton.commandDisabled { - color: @BaseMediumHigh; - opacity: 0.5; + color: @BaseMediumHigh; + opacity: 0.5; } .commandButton[tabindex]:focus { - outline: none; + outline: none; } .linkDarkBackground { - color: @AccentExtraHigh + color: @AccentExtraHigh; } .linkDarkBackground:hover, .linkDarkBackground:active, .linkDarkBackground:focus { - color: @AccentHigh + color: @AccentHigh; } .library-add-button { - margin-top: @LargeSpace; + margin-top: @LargeSpace; } .library-grid-container { - margin-top: 24px; + margin-top: 24px; } .library-delete { - cursor: pointer; - margin-left: 1em; + cursor: pointer; + margin-left: 1em; } -#deletecollectionconfirmationpane .paneMainContent > div:not(:first-child){ - margin-top: 12px; +#deletecollectionconfirmationpane .paneMainContent > div:not(:first-child) { + margin-top: 12px; } -#deletedatabaseconfirmationpane .paneMainContent > div:not(:first-child){ - margin-top: 12px; +#deletedatabaseconfirmationpane .paneMainContent > div:not(:first-child) { + margin-top: 12px; } .enableAnalyticalStorage { - margin-bottom: @SmallSpace; + margin-bottom: @SmallSpace; - .enableAnalyticalStorageRadio { - vertical-align: text-bottom; - margin-top: @SmallSpace; - } + .enableAnalyticalStorageRadio { + vertical-align: text-bottom; + margin-top: @SmallSpace; + } - .enableAnalyticalStorageRadio:nth-child(n+2) { - margin-left: @LargeSpace; - } + .enableAnalyticalStorageRadio:nth-child(n + 2) { + margin-left: @LargeSpace; + } - .enableAnalyticalStorageRadioLabel { - padding: 0px - } + .enableAnalyticalStorageRadioLabel { + padding: 0px; + } } .addCollectionLabel { - color: #393939; - font-weight: 600; + color: #393939; + font-weight: 600; } -.button.enabled{ - background: #FFF; - border-radius: 2px; - color: #323130; - padding: 3px 20px; - border: 1px solid #8A8886; +.button.enabled { + background: #fff; + border-radius: 2px; + color: #323130; + padding: 3px 20px; + border: 1px solid #8a8886; } -.button.disabled{ - background: #F3F2F1; - border: 0px solid #8A8886; - border-radius: 2px; - color: #A19F9D; - padding: 3px 20px; +.button.disabled { + background: #f3f2f1; + border: 0px solid #8a8886; + border-radius: 2px; + color: #a19f9d; + padding: 3px 20px; } .paragraph { - margin-top: 8px; + margin-top: 8px; } .italic { - font-style: italic; + font-style: italic; } .warningErrorContent a { - color: @AccentMediumHigh + color: @AccentMediumHigh; } .infoBoxContent a { - color: @AccentMediumHigh + color: @AccentMediumHigh; } -.collapsibleSection :hover{ - cursor: pointer; -} \ No newline at end of file +.collapsibleSection :hover { + cursor: pointer; +} + +.messageBarInfoIcon { + color: #0072c6; +} + +.messageBarWarningIcon { + color: #db7500; +} + +.freeTierInfoBanner { + background-color: @BaseLow; + display: inline-flex; + padding: @DefaultSpace; + width: 100%; + + .freeTierInfoIcon img { + height: 28px; + width: 28px; + margin-left: 4px; + } + + .freeTierInfoMessage { + margin: auto 0; + padding-left: @MediumSpace; + } +} + +.freeTierInlineWarning { + display: inline-flex; + padding: 8px 8px 8px 0; + width: 100%; + + .freeTierWarningIcon img { + height: 20px; + width: 20px; + } + + .freeTierWarningMessage { + margin: auto 0; + padding-left: @SmallSpace; + } +} diff --git a/package-lock.json b/package-lock.json index 8c38c5921..b29b45635 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5393,11 +5393,6 @@ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==" }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, "abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -5641,6 +5636,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "optional": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -6883,14 +6879,7 @@ "dev": true }, "canvas": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.6.1.tgz", - "integrity": "sha512-S98rKsPcuhfTcYbtF53UIJhcbgIAK533d1kJKMwsMwAIFgfd58MOyxRud3kktlzWiEkFliaJtvyZCBtud/XVEA==", - "requires": { - "nan": "^2.14.0", - "node-pre-gyp": "^0.11.0", - "simple-get": "^3.0.3" - } + "version": "file:canvas" }, "capture-exit": { "version": "2.0.0", @@ -7454,7 +7443,8 @@ "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "optional": true }, "constants-browserify": { "version": "1.0.0", @@ -8435,6 +8425,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "optional": true, "requires": { "mimic-response": "^2.0.0" } @@ -8460,7 +8451,8 @@ "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "optional": true }, "deep-is": { "version": "0.1.3", @@ -8652,7 +8644,8 @@ "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "optional": true }, "depd": { "version": "1.1.2", @@ -8688,7 +8681,8 @@ "detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "optional": true }, "detect-newline": { "version": "2.1.0", @@ -10674,14 +10668,6 @@ "universalify": "^0.1.0" } }, - "fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "requires": { - "minipass": "^2.6.0" - } - }, "fs-observable": { "version": "4.1.14", "resolved": "https://registry.npmjs.org/fs-observable/-/fs-observable-4.1.14.tgz", @@ -10823,6 +10809,7 @@ "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "optional": true, "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -10837,12 +10824,14 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -10851,6 +10840,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -10861,6 +10851,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -11350,7 +11341,8 @@ "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "optional": true }, "has-value": { "version": "1.0.0", @@ -11832,14 +11824,6 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==" }, - "ignore-walk": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", - "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", - "requires": { - "minimatch": "^3.0.4" - } - }, "image-size": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", @@ -15544,7 +15528,8 @@ "mimic-response": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "optional": true }, "min-document": { "version": "2.19.0", @@ -15601,15 +15586,6 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, - "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, "minipass-collect": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", @@ -15679,14 +15655,6 @@ } } }, - "minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "requires": { - "minipass": "^2.9.0" - } - }, "mississippi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", @@ -15861,7 +15829,8 @@ "nan": { "version": "2.14.2", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", - "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "optional": true }, "nanomatch": { "version": "1.2.13", @@ -15911,26 +15880,6 @@ "semver": "^5.4.1" } }, - "needle": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.5.2.tgz", - "integrity": "sha512-LbRIwS9BfkPvNwNHlsA41Q29kL2L/6VaOJ0qisM5lLWsTV3nP15abO5ITL6L81zqFhzjRKDAYjpcBcwM0AVvLQ==", - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - } - } - }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -16099,41 +16048,6 @@ "which": "^1.3.0" } }, - "node-pre-gyp": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", - "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - }, - "dependencies": { - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "requires": { - "minimist": "^1.2.5" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "requires": { - "glob": "^7.1.3" - } - } - } - }, "node-releases": { "version": "1.1.66", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.66.tgz", @@ -16146,15 +16060,6 @@ "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=", "optional": true }, - "nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -16179,29 +16084,6 @@ "resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz", "integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==" }, - "npm-bundled": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", - "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" - }, - "npm-packlist": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", - "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -16214,6 +16096,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "optional": true, "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -16605,7 +16488,8 @@ "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true }, "os-locale": { "version": "1.4.0", @@ -16624,20 +16508,6 @@ "windows-release": "^3.1.0" } }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, "p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", @@ -17690,6 +17560,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "optional": true, "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -19121,12 +18992,14 @@ "simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "optional": true }, "simple-get": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", + "optional": true, "requires": { "decompress-response": "^4.2.0", "once": "^1.3.1", @@ -20118,30 +19991,6 @@ "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", "dev": true }, - "tar": { - "version": "4.4.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", - "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - }, - "dependencies": { - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "requires": { - "minimist": "^1.2.5" - } - } - } - }, "tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", @@ -22197,6 +22046,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "optional": true, "requires": { "string-width": "^1.0.2 || 2" }, @@ -22204,12 +22054,14 @@ "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "optional": true }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "optional": true, "requires": { "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^4.0.0" @@ -22219,6 +22071,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "optional": true, "requires": { "ansi-regex": "^3.0.0" } @@ -22388,7 +22241,8 @@ "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true }, "yargs": { "version": "13.3.2", diff --git a/package.json b/package.json index 6e248f749..fda4efe8a 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "applicationinsights": "1.8.0", "babel-polyfill": "6.26.0", "bootstrap": "3.4.1", - "canvas": "2.6.1", + "canvas": "file:./canvas", "clean-webpack-plugin": "0.1.19", "copy-webpack-plugin": "6.0.2", "crossroads": "0.12.2", diff --git a/sampleData/gremlinSampleData.json b/sampleData/gremlinSampleData.json index 485db5d1a..82fed5817 100644 --- a/sampleData/gremlinSampleData.json +++ b/sampleData/gremlinSampleData.json @@ -3,7 +3,6 @@ "offerThroughput": 400, "databaseLevelThroughput": false, "collectionId": "Persons", - "rupmEnabled": false, "partitionKey": { "kind": "Hash", "paths": ["/name"] }, "data": [ "g.addV('person').property(id, '1').property('name', 'Eva').property('age', 44)", @@ -13,4 +12,4 @@ "g.V('1').addE('knows').to(g.V('2')).outV().addE('knows').to(g.V('3'))", "g.V('3').addE('knows').to(g.V('4'))" ] -} \ No newline at end of file +} diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 0aeb6bb60..0a72c55b8 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -108,13 +108,11 @@ export class CapabilityNames { export class Features { public static readonly cosmosdb = "cosmosdb"; public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy"; - public static readonly enableRupm = "enablerupm"; public static readonly executeSproc = "dataexplorerexecutesproc"; public static readonly hostedDataExplorer = "hosteddataexplorerenabled"; public static readonly enableTtl = "enablettl"; public static readonly enableNotebooks = "enablenotebooks"; public static readonly enableGalleryPublish = "enablegallerypublish"; - public static readonly enableCodeOfConduct = "enablecodeofconduct"; public static readonly enableLinkInjection = "enablelinkinjection"; public static readonly enableSpark = "enablespark"; public static readonly livyEndpoint = "livyendpoint"; @@ -182,11 +180,6 @@ export class CassandraBackend { public static readonly guestSchemaApi: string = "api/guest/cassandra/schema"; } -export class RUPMStates { - public static on: string = "on"; - public static off: string = "off"; -} - export class Queries { public static CustomPageOption: string = "custom"; public static UnlimitedPageOption: string = "unlimited"; diff --git a/src/Common/DataAccessUtilityBase.ts b/src/Common/DataAccessUtilityBase.ts deleted file mode 100644 index e835829d4..000000000 --- a/src/Common/DataAccessUtilityBase.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { ConflictDefinition, FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; -import Q from "q"; -import * as DataModels from "../Contracts/DataModels"; -import * as ViewModels from "../Contracts/ViewModels"; -import ConflictId from "../Explorer/Tree/ConflictId"; -import DocumentId from "../Explorer/Tree/DocumentId"; -import StoredProcedure from "../Explorer/Tree/StoredProcedure"; -import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility"; -import * as Constants from "./Constants"; -import { client } from "./CosmosClient"; - -export function getCommonQueryOptions(options: FeedOptions): any { - const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage); - options = options || {}; - options.populateQueryMetrics = true; - options.enableScanInQuery = options.enableScanInQuery || true; - if (!options.partitionKey) { - options.forceQueryPlan = true; - } - options.maxItemCount = - options.maxItemCount || - (storedItemPerPageSetting !== undefined && storedItemPerPageSetting) || - Constants.Queries.itemsPerPage; - options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism); - - return options; -} - -export function queryDocuments( - databaseId: string, - containerId: string, - query: string, - options: any -): Q.Promise> { - options = getCommonQueryOptions(options); - const documentsIterator = client() - .database(databaseId) - .container(containerId) - .items.query(query, options); - return Q(documentsIterator); -} - -export function getPartitionKeyHeaderForConflict(conflictId: ConflictId): Object { - const partitionKeyDefinition: DataModels.PartitionKey = conflictId.partitionKey; - const partitionKeyValue: any = conflictId.partitionKeyValue; - - return getPartitionKeyHeader(partitionKeyDefinition, partitionKeyValue); -} - -export function getPartitionKeyHeader(partitionKeyDefinition: DataModels.PartitionKey, partitionKeyValue: any): Object { - if (!partitionKeyDefinition) { - return undefined; - } - - if (partitionKeyValue === undefined) { - return [{}]; - } - - return [partitionKeyValue]; -} - -export function updateDocument( - collection: ViewModels.CollectionBase, - documentId: DocumentId, - newDocument: any -): Q.Promise { - const partitionKey = documentId.partitionKeyValue; - - return Q( - client() - .database(collection.databaseId) - .container(collection.id()) - .item(documentId.id(), partitionKey) - .replace(newDocument) - .then(response => response.resource) - ); -} - -export function executeStoredProcedure( - collection: ViewModels.Collection, - storedProcedure: StoredProcedure, - partitionKeyValue: any, - params: any[] -): Q.Promise { - // TODO remove this deferred. Kept it because of timeout code at bottom of function - const deferred = Q.defer(); - - client() - .database(collection.databaseId) - .container(collection.id()) - .scripts.storedProcedure(storedProcedure.id()) - .execute(partitionKeyValue, params, { enableScriptLogging: true }) - .then(response => - deferred.resolve({ - result: response.resource, - scriptLogs: response.headers[Constants.HttpHeaders.scriptLogResults] - }) - ) - .catch(error => deferred.reject(error)); - - return deferred.promise.timeout( - Constants.ClientDefaults.requestTimeoutMs, - `Request timed out while executing stored procedure ${storedProcedure.id()}` - ); -} - -export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise { - return Q( - client() - .database(collection.databaseId) - .container(collection.id()) - .items.create(newDocument) - .then(response => response.resource) - ); -} - -export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise { - const partitionKey = documentId.partitionKeyValue; - - return Q( - client() - .database(collection.databaseId) - .container(collection.id()) - .item(documentId.id(), partitionKey) - .read() - .then(response => response.resource) - ); -} - -export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise { - const partitionKey = documentId.partitionKeyValue; - - return Q( - client() - .database(collection.databaseId) - .container(collection.id()) - .item(documentId.id(), partitionKey) - .delete() - ); -} - -export function deleteConflict( - collection: ViewModels.CollectionBase, - conflictId: ConflictId, - options: any = {} -): Q.Promise { - options.partitionKey = options.partitionKey || getPartitionKeyHeaderForConflict(conflictId); - - return Q( - client() - .database(collection.databaseId) - .container(collection.id()) - .conflict(conflictId.id()) - .delete(options) - ); -} - -export function queryConflicts( - databaseId: string, - containerId: string, - query: string, - options: any -): Q.Promise> { - const documentsIterator = client() - .database(databaseId) - .container(containerId) - .conflicts.query(query, options); - return Q(documentsIterator); -} diff --git a/src/Common/DocumentClientUtilityBase.ts b/src/Common/DocumentClientUtilityBase.ts deleted file mode 100644 index ded90cf05..000000000 --- a/src/Common/DocumentClientUtilityBase.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { ConflictDefinition, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; -import Q from "q"; -import * as ViewModels from "../Contracts/ViewModels"; -import ConflictId from "../Explorer/Tree/ConflictId"; -import DocumentId from "../Explorer/Tree/DocumentId"; -import StoredProcedure from "../Explorer/Tree/StoredProcedure"; -import { logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; -import * as Constants from "./Constants"; -import * as DataAccessUtilityBase from "./DataAccessUtilityBase"; -import { MinimalQueryIterator, nextPage } from "./IteratorUtilities"; -import { handleError } from "./ErrorHandlingUtils"; - -// TODO: Log all promise resolutions and errors with verbosity levels -export function queryDocuments( - databaseId: string, - containerId: string, - query: string, - options: any -): Q.Promise> { - return DataAccessUtilityBase.queryDocuments(databaseId, containerId, query, options); -} - -export function queryConflicts( - databaseId: string, - containerId: string, - query: string, - options: any -): Q.Promise> { - return DataAccessUtilityBase.queryConflicts(databaseId, containerId, query, options); -} - -export function getEntityName() { - const defaultExperience = - window.dataExplorer && window.dataExplorer.defaultExperience && window.dataExplorer.defaultExperience(); - if (defaultExperience === Constants.DefaultAccountExperience.MongoDB) { - return "document"; - } - return "item"; -} - -export function executeStoredProcedure( - collection: ViewModels.Collection, - storedProcedure: StoredProcedure, - partitionKeyValue: any, - params: any[] -): Q.Promise { - var deferred = Q.defer(); - - const clearMessage = logConsoleProgress(`Executing stored procedure ${storedProcedure.id()}`); - DataAccessUtilityBase.executeStoredProcedure(collection, storedProcedure, partitionKeyValue, params) - .then( - (response: any) => { - deferred.resolve(response); - logConsoleInfo( - `Finished executing stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}` - ); - }, - (error: any) => { - handleError( - error, - "ExecuteStoredProcedure", - `Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}` - ); - deferred.reject(error); - } - ) - .finally(() => { - clearMessage(); - }); - - return deferred.promise; -} - -export function queryDocumentsPage( - resourceName: string, - documentsIterator: MinimalQueryIterator, - firstItemIndex: number, - options: any -): Q.Promise { - var deferred = Q.defer(); - const entityName = getEntityName(); - const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`); - Q(nextPage(documentsIterator, firstItemIndex)) - .then( - (result: ViewModels.QueryResults) => { - const itemCount = (result.documents && result.documents.length) || 0; - logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`); - deferred.resolve(result); - }, - (error: any) => { - handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`); - deferred.reject(error); - } - ) - .finally(() => { - clearMessage(); - }); - - return deferred.promise; -} - -export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise { - var deferred = Q.defer(); - const entityName = getEntityName(); - const clearMessage = logConsoleProgress(`Reading ${entityName} ${documentId.id()}`); - DataAccessUtilityBase.readDocument(collection, documentId) - .then( - (document: any) => { - deferred.resolve(document); - }, - (error: any) => { - handleError(error, "ReadDocument", `Failed to read ${entityName} ${documentId.id()}`); - deferred.reject(error); - } - ) - .finally(() => { - clearMessage(); - }); - - return deferred.promise; -} - -export function updateDocument( - collection: ViewModels.CollectionBase, - documentId: DocumentId, - newDocument: any -): Q.Promise { - var deferred = Q.defer(); - const entityName = getEntityName(); - const clearMessage = logConsoleProgress(`Updating ${entityName} ${documentId.id()}`); - DataAccessUtilityBase.updateDocument(collection, documentId, newDocument) - .then( - (updatedDocument: any) => { - logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`); - deferred.resolve(updatedDocument); - }, - (error: any) => { - handleError(error, "UpdateDocument", `Failed to update ${entityName} ${documentId.id()}`); - deferred.reject(error); - } - ) - .finally(() => { - clearMessage(); - }); - - return deferred.promise; -} - -export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise { - var deferred = Q.defer(); - const entityName = getEntityName(); - const clearMessage = logConsoleProgress(`Creating new ${entityName} for container ${collection.id()}`); - DataAccessUtilityBase.createDocument(collection, newDocument) - .then( - (savedDocument: any) => { - logConsoleInfo(`Successfully created new ${entityName} for container ${collection.id()}`); - deferred.resolve(savedDocument); - }, - (error: any) => { - handleError(error, "CreateDocument", `Error while creating new ${entityName} for container ${collection.id()}`); - deferred.reject(error); - } - ) - .finally(() => { - clearMessage(); - }); - - return deferred.promise; -} - -export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise { - var deferred = Q.defer(); - const entityName = getEntityName(); - const clearMessage = logConsoleProgress(`Deleting ${entityName} ${documentId.id()}`); - DataAccessUtilityBase.deleteDocument(collection, documentId) - .then( - (response: any) => { - logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`); - deferred.resolve(response); - }, - (error: any) => { - handleError(error, "DeleteDocument", `Error while deleting ${entityName} ${documentId.id()}`); - deferred.reject(error); - } - ) - .finally(() => { - clearMessage(); - }); - - return deferred.promise; -} - -export function deleteConflict( - collection: ViewModels.CollectionBase, - conflictId: ConflictId, - options?: any -): Q.Promise { - var deferred = Q.defer(); - - const clearMessage = logConsoleProgress(`Deleting conflict ${conflictId.id()}`); - DataAccessUtilityBase.deleteConflict(collection, conflictId, options) - .then( - (response: any) => { - logConsoleInfo(`Successfully deleted conflict ${conflictId.id()}`); - deferred.resolve(response); - }, - (error: any) => { - handleError(error, "DeleteConflict", `Error while deleting conflict ${conflictId.id()}`); - deferred.reject(error); - } - ) - .finally(() => { - clearMessage(); - }); - - return deferred.promise; -} diff --git a/src/Common/DocumentUtility.ts b/src/Common/DocumentUtility.ts new file mode 100644 index 000000000..b552ba495 --- /dev/null +++ b/src/Common/DocumentUtility.ts @@ -0,0 +1,10 @@ +import { DefaultAccountExperienceType } from "../DefaultAccountExperienceType"; +import { userContext } from "../UserContext"; + +export const getEntityName = (): string => { + if (userContext.defaultExperience === DefaultAccountExperienceType.MongoDB) { + return "document"; + } + + return "item"; +}; 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/Common/OfferUtility.test.ts b/src/Common/OfferUtility.test.ts index 5b8a39a69..d24310758 100644 --- a/src/Common/OfferUtility.test.ts +++ b/src/Common/OfferUtility.test.ts @@ -24,7 +24,8 @@ describe("parseSDKOfferResponse", () => { autoscaleMaxThroughput: undefined, minimumThroughput: 400, id: "test", - offerDefinition: mockOfferDefinition + offerDefinition: mockOfferDefinition, + offerReplacePending: false }; expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult); @@ -54,7 +55,8 @@ describe("parseSDKOfferResponse", () => { autoscaleMaxThroughput: 5000, minimumThroughput: 400, id: "test", - offerDefinition: mockOfferDefinition + offerDefinition: mockOfferDefinition, + offerReplacePending: false }; expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult); diff --git a/src/Common/OfferUtility.ts b/src/Common/OfferUtility.ts index a71dc7c84..a6e1377fe 100644 --- a/src/Common/OfferUtility.ts +++ b/src/Common/OfferUtility.ts @@ -1,5 +1,6 @@ import { Offer, SDKOfferDefinition } from "../Contracts/DataModels"; import { OfferResponse } from "@azure/cosmos"; +import { HttpHeaders } from "./Constants"; export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => { const offerDefinition: SDKOfferDefinition = offerResponse?.resource; @@ -18,7 +19,7 @@ export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => { manualThroughput: undefined, minimumThroughput, offerDefinition, - headers: offerResponse.headers + offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true" }; } @@ -28,6 +29,6 @@ export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => { manualThroughput: offerContent.offerThroughput, minimumThroughput, offerDefinition, - headers: offerResponse.headers + offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true" }; }; diff --git a/src/Common/QueriesClient.ts b/src/Common/QueriesClient.ts index a7f28f7b7..d11d36e81 100644 --- a/src/Common/QueriesClient.ts +++ b/src/Common/QueriesClient.ts @@ -3,16 +3,18 @@ import * as _ from "underscore"; import * as DataModels from "../Contracts/DataModels"; import * as ViewModels from "../Contracts/ViewModels"; import Explorer from "../Explorer/Explorer"; -import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import DocumentsTab from "../Explorer/Tabs/DocumentsTab"; import DocumentId from "../Explorer/Tree/DocumentId"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import { QueryUtils } from "../Utils/QueryUtils"; import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants"; import { userContext } from "../UserContext"; -import { createDocument, deleteDocument, queryDocuments, queryDocumentsPage } from "./DocumentClientUtilityBase"; +import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage"; import { createCollection } from "./dataAccess/createCollection"; import { handleError } from "./ErrorHandlingUtils"; +import { createDocument } from "./dataAccess/createDocument"; +import { deleteDocument } from "./dataAccess/deleteDocument"; +import { queryDocuments } from "./dataAccess/queryDocuments"; export class QueriesClient { private static readonly PartitionKey: DataModels.PartitionKey = { @@ -31,10 +33,7 @@ export class QueriesClient { return Promise.resolve(queriesCollection.rawDataModel); } - const id = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - "Setting up account for saving queries" - ); + const clearMessage = NotificationConsoleUtils.logConsoleProgress("Setting up account for saving queries"); return createCollection({ collectionId: SavedQueries.CollectionName, createNewDatabase: true, @@ -45,10 +44,7 @@ export class QueriesClient { }) .then( (collection: DataModels.Collection) => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Info, - "Successfully set up account for saving queries" - ); + NotificationConsoleUtils.logConsoleInfo("Successfully set up account for saving queries"); return Promise.resolve(collection); }, (error: any) => { @@ -56,17 +52,14 @@ export class QueriesClient { return Promise.reject(error); } ) - .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); + .finally(() => clearMessage()); } public async saveQuery(query: DataModels.Query): Promise { const queriesCollection = this.findQueriesCollection(); if (!queriesCollection) { const errorMessage: string = "Account not set up to perform saved query operations"; - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to save query ${query.queryName}: ${errorMessage}` - ); + NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`); return Promise.reject(errorMessage); } @@ -74,25 +67,16 @@ export class QueriesClient { this.validateQuery(query); } catch (error) { const errorMessage: string = "Invalid query specified"; - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to save query ${query.queryName}: ${errorMessage}` - ); + NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`); return Promise.reject(errorMessage); } - const id = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - `Saving query ${query.queryName}` - ); + const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Saving query ${query.queryName}`); query.id = query.queryName; return createDocument(queriesCollection, query) .then( (savedQuery: DataModels.Query) => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Info, - `Successfully saved query ${query.queryName}` - ); + NotificationConsoleUtils.logConsoleInfo(`Successfully saved query ${query.queryName}`); return Promise.resolve(); }, (error: any) => { @@ -103,74 +87,65 @@ export class QueriesClient { return Promise.reject(error); } ) - .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); + .finally(() => clearMessage()); } public async getQueries(): Promise { const queriesCollection = this.findQueriesCollection(); if (!queriesCollection) { const errorMessage: string = "Account not set up to perform saved query operations"; - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to fetch saved queries: ${errorMessage}` - ); + NotificationConsoleUtils.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`); return Promise.reject(errorMessage); } const options: any = { enableCrossPartitionQuery: true }; - const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Fetching saved queries"); - return queryDocuments(SavedQueries.DatabaseName, SavedQueries.CollectionName, this.fetchQueriesQuery(), options) + const clearMessage = NotificationConsoleUtils.logConsoleProgress("Fetching saved queries"); + const queryIterator: QueryIterator = queryDocuments( + SavedQueries.DatabaseName, + SavedQueries.CollectionName, + this.fetchQueriesQuery(), + options + ); + const fetchQueries = async (firstItemIndex: number): Promise => + await queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex); + return QueryUtils.queryAllPages(fetchQueries) .then( - (queryIterator: QueryIterator) => { - const fetchQueries = (firstItemIndex: number): Q.Promise => - queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex, options); - return QueryUtils.queryAllPages(fetchQueries).then( - (results: ViewModels.QueryResults) => { - let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => { - if (!document) { - return undefined; - } - const { id, resourceId, query, queryName } = document; - const parsedQuery: DataModels.Query = { - resourceId: resourceId, - queryName: queryName, - query: query, - id: id - }; - try { - this.validateQuery(parsedQuery); - return parsedQuery; - } catch (error) { - return undefined; - } - }); - queries = _.reject(queries, (parsedQuery: DataModels.Query) => !parsedQuery); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully fetched saved queries"); - return Promise.resolve(queries); - }, - (error: any) => { - handleError(error, "getSavedQueries", "Failed to fetch saved queries"); - return Promise.reject(error); + (results: ViewModels.QueryResults) => { + let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => { + if (!document) { + return undefined; } - ); + const { id, resourceId, query, queryName } = document; + const parsedQuery: DataModels.Query = { + resourceId: resourceId, + queryName: queryName, + query: query, + id: id + }; + try { + this.validateQuery(parsedQuery); + return parsedQuery; + } catch (error) { + return undefined; + } + }); + queries = _.reject(queries, (parsedQuery: DataModels.Query) => !parsedQuery); + NotificationConsoleUtils.logConsoleInfo("Successfully fetched saved queries"); + return Promise.resolve(queries); }, (error: any) => { - // should never get into this state but we handle this regardless handleError(error, "getSavedQueries", "Failed to fetch saved queries"); return Promise.reject(error); } ) - .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); + .finally(() => clearMessage()); } public async deleteQuery(query: DataModels.Query): Promise { const queriesCollection = this.findQueriesCollection(); if (!queriesCollection) { const errorMessage: string = "Account not set up to perform saved query operations"; - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to fetch saved queries: ${errorMessage}` - ); + NotificationConsoleUtils.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`); return Promise.reject(errorMessage); } @@ -178,16 +153,10 @@ export class QueriesClient { this.validateQuery(query); } catch (error) { const errorMessage: string = "Invalid query specified"; - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to delete query ${query.queryName}: ${errorMessage}` - ); + NotificationConsoleUtils.logConsoleError(`Failed to delete query ${query.queryName}: ${errorMessage}`); } - const id = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - `Deleting query ${query.queryName}` - ); + const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting query ${query.queryName}`); query.id = query.queryName; const documentId = new DocumentId( { @@ -201,10 +170,7 @@ export class QueriesClient { return deleteDocument(queriesCollection, documentId) .then( () => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Info, - `Successfully deleted query ${query.queryName}` - ); + NotificationConsoleUtils.logConsoleInfo(`Successfully deleted query ${query.queryName}`); return Promise.resolve(); }, (error: any) => { @@ -212,7 +178,7 @@ export class QueriesClient { return Promise.reject(error); } ) - .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); + .finally(() => clearMessage()); } public getResourceId(): string { diff --git a/src/Common/__snapshots__/DataAccessUtilityBase.test.ts.snap b/src/Common/dataAccess/__snapshots__/queryDocuments.test.ts.snap similarity index 100% rename from src/Common/__snapshots__/DataAccessUtilityBase.test.ts.snap rename to src/Common/dataAccess/__snapshots__/queryDocuments.test.ts.snap diff --git a/src/Common/dataAccess/createCollection.test.ts b/src/Common/dataAccess/createCollection.test.ts index 79902e72f..7d4076f35 100644 --- a/src/Common/dataAccess/createCollection.test.ts +++ b/src/Common/dataAccess/createCollection.test.ts @@ -1,6 +1,5 @@ jest.mock("../../Utils/arm/request"); jest.mock("../CosmosClient"); -jest.mock("../DataAccessUtilityBase"); import { AuthType } from "../../AuthType"; import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; diff --git a/src/Common/dataAccess/createDocument.ts b/src/Common/dataAccess/createDocument.ts new file mode 100644 index 000000000..b64f70ff9 --- /dev/null +++ b/src/Common/dataAccess/createDocument.ts @@ -0,0 +1,25 @@ +import { CollectionBase } from "../../Contracts/ViewModels"; +import { client } from "../CosmosClient"; +import { getEntityName } from "../DocumentUtility"; +import { handleError } from "../ErrorHandlingUtils"; +import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; + +export const createDocument = async (collection: CollectionBase, newDocument: unknown): Promise => { + const entityName = getEntityName(); + const clearMessage = logConsoleProgress(`Creating new ${entityName} for container ${collection.id()}`); + + try { + const response = await client() + .database(collection.databaseId) + .container(collection.id()) + .items.create(newDocument); + + logConsoleInfo(`Successfully created new ${entityName} for container ${collection.id()}`); + return response?.resource; + } catch (error) { + handleError(error, "CreateDocument", `Error while creating new ${entityName} for container ${collection.id()}`); + throw error; + } finally { + clearMessage(); + } +}; diff --git a/src/Common/dataAccess/deleteConflict.ts b/src/Common/dataAccess/deleteConflict.ts new file mode 100644 index 000000000..746d3577b --- /dev/null +++ b/src/Common/dataAccess/deleteConflict.ts @@ -0,0 +1,36 @@ +import ConflictId from "../../Explorer/Tree/ConflictId"; +import { CollectionBase } from "../../Contracts/ViewModels"; +import { RequestOptions } from "@azure/cosmos"; +import { client } from "../CosmosClient"; +import { handleError } from "../ErrorHandlingUtils"; +import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; + +export const deleteConflict = async (collection: CollectionBase, conflictId: ConflictId): Promise => { + const clearMessage = logConsoleProgress(`Deleting conflict ${conflictId.id()}`); + + try { + const options = { + partitionKey: getPartitionKeyHeaderForConflict(conflictId) + }; + + await client() + .database(collection.databaseId) + .container(collection.id()) + .conflict(conflictId.id()) + .delete(options as RequestOptions); + logConsoleInfo(`Successfully deleted conflict ${conflictId.id()}`); + } catch (error) { + handleError(error, "DeleteConflict", `Error while deleting conflict ${conflictId.id()}`); + throw error; + } finally { + clearMessage(); + } +}; + +const getPartitionKeyHeaderForConflict = (conflictId: ConflictId): unknown => { + if (!conflictId.partitionKey) { + return undefined; + } + + return conflictId.partitionKeyValue === undefined ? [{}] : [conflictId.partitionKeyValue]; +}; diff --git a/src/Common/dataAccess/deleteDocument.ts b/src/Common/dataAccess/deleteDocument.ts new file mode 100644 index 000000000..0ab2e6999 --- /dev/null +++ b/src/Common/dataAccess/deleteDocument.ts @@ -0,0 +1,25 @@ +import { CollectionBase } from "../../Contracts/ViewModels"; +import { client } from "../CosmosClient"; +import { getEntityName } from "../DocumentUtility"; +import { handleError } from "../ErrorHandlingUtils"; +import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; +import DocumentId from "../../Explorer/Tree/DocumentId"; + +export const deleteDocument = async (collection: CollectionBase, documentId: DocumentId): Promise => { + const entityName: string = getEntityName(); + const clearMessage = logConsoleProgress(`Deleting ${entityName} ${documentId.id()}`); + + try { + await client() + .database(collection.databaseId) + .container(collection.id()) + .item(documentId.id(), documentId.partitionKeyValue) + .delete(); + logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`); + } catch (error) { + handleError(error, "DeleteDocument", `Error while deleting ${entityName} ${documentId.id()}`); + throw error; + } finally { + clearMessage(); + } +}; diff --git a/src/Common/dataAccess/executeStoredProcedure.ts b/src/Common/dataAccess/executeStoredProcedure.ts new file mode 100644 index 000000000..7459c3a02 --- /dev/null +++ b/src/Common/dataAccess/executeStoredProcedure.ts @@ -0,0 +1,48 @@ +import { Collection } from "../../Contracts/ViewModels"; +import { ClientDefaults, HttpHeaders } from "../Constants"; +import { client } from "../CosmosClient"; +import { handleError } from "../ErrorHandlingUtils"; +import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; +import StoredProcedure from "../../Explorer/Tree/StoredProcedure"; + +export interface ExecuteSprocResult { + result: StoredProcedure; + scriptLogs: string; +} + +export const executeStoredProcedure = async ( + collection: Collection, + storedProcedure: StoredProcedure, + partitionKeyValue: string, + params: string[] +): Promise => { + const clearMessage = logConsoleProgress(`Executing stored procedure ${storedProcedure.id()}`); + const timeout = setTimeout(() => { + throw Error(`Request timed out while executing stored procedure ${storedProcedure.id()}`); + }, ClientDefaults.requestTimeoutMs); + + try { + const response = await client() + .database(collection.databaseId) + .container(collection.id()) + .scripts.storedProcedure(storedProcedure.id()) + .execute(partitionKeyValue, params, { enableScriptLogging: true }); + clearTimeout(timeout); + logConsoleInfo( + `Finished executing stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}` + ); + return { + result: response.resource, + scriptLogs: response.headers[HttpHeaders.scriptLogResults] as string + }; + } catch (error) { + handleError( + error, + "ExecuteStoredProcedure", + `Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}` + ); + throw error; + } finally { + clearMessage(); + } +}; diff --git a/src/Common/dataAccess/getCollectionDataUsageSize.ts b/src/Common/dataAccess/getCollectionDataUsageSize.ts index a59eab21a..bdbca9e04 100644 --- a/src/Common/dataAccess/getCollectionDataUsageSize.ts +++ b/src/Common/dataAccess/getCollectionDataUsageSize.ts @@ -1,3 +1,4 @@ +import { AuthType } from "../../AuthType"; import { armRequest } from "../../Utils/arm/request"; import { configContext } from "../../ConfigContext"; import { handleError } from "../ErrorHandlingUtils"; @@ -40,6 +41,10 @@ interface MetricsResponse { } export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise => { + if (window.authType !== AuthType.AAD) { + return undefined; + } + const subscriptionId = userContext.subscriptionId; const resourceGroup = userContext.resourceGroup; const accountName = userContext.databaseAccount.name; diff --git a/src/Common/dataAccess/getIndexTransformationProgress.ts b/src/Common/dataAccess/getIndexTransformationProgress.ts index 94dcf9fde..fa0298fbc 100644 --- a/src/Common/dataAccess/getIndexTransformationProgress.ts +++ b/src/Common/dataAccess/getIndexTransformationProgress.ts @@ -14,7 +14,7 @@ export async function getIndexTransformationProgress(databaseId: string, collect const response = await client() .database(databaseId) .container(collectionId) - .read(); + .read({ populateQuotaInfo: true }); indexTransformationPercentage = parseInt( response.headers[Constants.HttpHeaders.collectionIndexTransformationProgress] as string diff --git a/src/Common/dataAccess/queryConflicts.ts b/src/Common/dataAccess/queryConflicts.ts new file mode 100644 index 000000000..ed3ecaa26 --- /dev/null +++ b/src/Common/dataAccess/queryConflicts.ts @@ -0,0 +1,14 @@ +import { ConflictDefinition, FeedOptions, QueryIterator, Resource } from "@azure/cosmos"; +import { client } from "../CosmosClient"; + +export const queryConflicts = ( + databaseId: string, + containerId: string, + query: string, + options: FeedOptions +): QueryIterator => { + return client() + .database(databaseId) + .container(containerId) + .conflicts.query(query, options); +}; diff --git a/src/Common/DataAccessUtilityBase.test.ts b/src/Common/dataAccess/queryDocuments.test.ts similarity index 73% rename from src/Common/DataAccessUtilityBase.test.ts rename to src/Common/dataAccess/queryDocuments.test.ts index 339d82aca..38565a3df 100644 --- a/src/Common/DataAccessUtilityBase.test.ts +++ b/src/Common/dataAccess/queryDocuments.test.ts @@ -1,13 +1,13 @@ -import { getCommonQueryOptions } from "./DataAccessUtilityBase"; -import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility"; - -describe("getCommonQueryOptions", () => { - it("builds the correct default options objects", () => { - expect(getCommonQueryOptions({})).toMatchSnapshot(); - }); - it("reads from localStorage", () => { - LocalStorageUtility.setEntryNumber(StorageKey.ActualItemPerPage, 37); - LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, 17); - expect(getCommonQueryOptions({})).toMatchSnapshot(); - }); -}); +import { getCommonQueryOptions } from "./queryDocuments"; +import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; + +describe("getCommonQueryOptions", () => { + it("builds the correct default options objects", () => { + expect(getCommonQueryOptions({})).toMatchSnapshot(); + }); + it("reads from localStorage", () => { + LocalStorageUtility.setEntryNumber(StorageKey.ActualItemPerPage, 37); + LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, 17); + expect(getCommonQueryOptions({})).toMatchSnapshot(); + }); +}); diff --git a/src/Common/dataAccess/queryDocuments.ts b/src/Common/dataAccess/queryDocuments.ts new file mode 100644 index 000000000..0436b756c --- /dev/null +++ b/src/Common/dataAccess/queryDocuments.ts @@ -0,0 +1,34 @@ +import { Queries } from "../Constants"; +import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; +import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; +import { client } from "../CosmosClient"; + +export const queryDocuments = ( + databaseId: string, + containerId: string, + query: string, + options: FeedOptions +): QueryIterator => { + options = getCommonQueryOptions(options); + return client() + .database(databaseId) + .container(containerId) + .items.query(query, options); +}; + +export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => { + const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage); + options = options || {}; + options.populateQueryMetrics = true; + options.enableScanInQuery = options.enableScanInQuery || true; + if (!options.partitionKey) { + options.forceQueryPlan = true; + } + options.maxItemCount = + options.maxItemCount || + (storedItemPerPageSetting !== undefined && storedItemPerPageSetting) || + Queries.itemsPerPage; + options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism); + + return options; +}; diff --git a/src/Common/dataAccess/queryDocumentsPage.ts b/src/Common/dataAccess/queryDocumentsPage.ts new file mode 100644 index 000000000..064e4126f --- /dev/null +++ b/src/Common/dataAccess/queryDocumentsPage.ts @@ -0,0 +1,26 @@ +import { QueryResults } from "../../Contracts/ViewModels"; +import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; +import { MinimalQueryIterator, nextPage } from "../IteratorUtilities"; +import { handleError } from "../ErrorHandlingUtils"; +import { getEntityName } from "../DocumentUtility"; + +export const queryDocumentsPage = async ( + resourceName: string, + documentsIterator: MinimalQueryIterator, + firstItemIndex: number +): Promise => { + const entityName = getEntityName(); + const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`); + + try { + const result: QueryResults = await nextPage(documentsIterator, firstItemIndex); + const itemCount = (result.documents && result.documents.length) || 0; + logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`); + return result; + } catch (error) { + handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`); + throw error; + } finally { + clearMessage(); + } +}; diff --git a/src/Common/dataAccess/readCollectionOffer.ts b/src/Common/dataAccess/readCollectionOffer.ts index f7d97a4b0..9d0ed5fc3 100644 --- a/src/Common/dataAccess/readCollectionOffer.ts +++ b/src/Common/dataAccess/readCollectionOffer.ts @@ -105,7 +105,8 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri id: offerId, autoscaleMaxThroughput: autoscaleSettings.maxThroughput, manualThroughput: undefined, - minimumThroughput + minimumThroughput, + offerReplacePending: resource.offerReplacePending === "true" }; } @@ -113,7 +114,8 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri id: offerId, autoscaleMaxThroughput: undefined, manualThroughput: resource.throughput, - minimumThroughput + minimumThroughput, + offerReplacePending: resource.offerReplacePending === "true" }; } diff --git a/src/Common/dataAccess/readDatabaseOffer.ts b/src/Common/dataAccess/readDatabaseOffer.ts index 1f4a67acd..9e99745c7 100644 --- a/src/Common/dataAccess/readDatabaseOffer.ts +++ b/src/Common/dataAccess/readDatabaseOffer.ts @@ -77,7 +77,8 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise => { id: offerId, autoscaleMaxThroughput: autoscaleSettings.maxThroughput, manualThroughput: undefined, - minimumThroughput + minimumThroughput, + offerReplacePending: resource.offerReplacePending === "true" }; } @@ -85,7 +86,8 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise => { id: offerId, autoscaleMaxThroughput: undefined, manualThroughput: resource.throughput, - minimumThroughput + minimumThroughput, + offerReplacePending: resource.offerReplacePending === "true" }; } diff --git a/src/Common/dataAccess/readDocument.ts b/src/Common/dataAccess/readDocument.ts new file mode 100644 index 000000000..d399f25f0 --- /dev/null +++ b/src/Common/dataAccess/readDocument.ts @@ -0,0 +1,27 @@ +import { Item } from "@azure/cosmos"; +import { CollectionBase } from "../../Contracts/ViewModels"; +import { client } from "../CosmosClient"; +import { getEntityName } from "../DocumentUtility"; +import { handleError } from "../ErrorHandlingUtils"; +import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; +import DocumentId from "../../Explorer/Tree/DocumentId"; + +export const readDocument = async (collection: CollectionBase, documentId: DocumentId): Promise => { + const entityName = getEntityName(); + const clearMessage = logConsoleProgress(`Reading ${entityName} ${documentId.id()}`); + + try { + const response = await client() + .database(collection.databaseId) + .container(collection.id()) + .item(documentId.id(), documentId.partitionKeyValue) + .read(); + + return response?.resource; + } catch (error) { + handleError(error, "ReadDocument", `Failed to read ${entityName} ${documentId.id()}`); + throw error; + } finally { + clearMessage(); + } +}; diff --git a/src/Common/dataAccess/updateDocument.ts b/src/Common/dataAccess/updateDocument.ts new file mode 100644 index 000000000..9e1b50fd9 --- /dev/null +++ b/src/Common/dataAccess/updateDocument.ts @@ -0,0 +1,32 @@ +import { CollectionBase } from "../../Contracts/ViewModels"; +import { Item } from "@azure/cosmos"; +import { client } from "../CosmosClient"; +import { getEntityName } from "../DocumentUtility"; +import { handleError } from "../ErrorHandlingUtils"; +import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; +import DocumentId from "../../Explorer/Tree/DocumentId"; + +export const updateDocument = async ( + collection: CollectionBase, + documentId: DocumentId, + newDocument: Item +): Promise => { + const entityName = getEntityName(); + const clearMessage = logConsoleProgress(`Updating ${entityName} ${documentId.id()}`); + + try { + const response = await client() + .database(collection.databaseId) + .container(collection.id()) + .item(documentId.id(), documentId.partitionKeyValue) + .replace(newDocument); + + logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`); + return response?.resource; + } catch (error) { + handleError(error, "UpdateDocument", `Failed to update ${entityName} ${documentId.id()}`); + throw error; + } finally { + clearMessage(); + } +}; diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index 5ec15c572..505090c18 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -214,7 +214,7 @@ export interface Offer { manualThroughput: number; minimumThroughput: number; offerDefinition?: SDKOfferDefinition; - headers?: any; + offerReplacePending: boolean; } export interface SDKOfferDefinition extends Resource { @@ -248,7 +248,6 @@ export interface CreateDatabaseAndCollectionRequest { collectionId: string; offerThroughput: number; databaseLevelThroughput: boolean; - rupmEnabled?: boolean; partitionKey?: PartitionKey; indexingPolicy?: IndexingPolicy; uniqueKeyPolicy?: UniqueKeyPolicy; diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index 8c85a3a5a..704ae0906 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -363,7 +363,7 @@ export enum CollectionTabKind { Gallery = 17, NotebookViewer = 18, Schema = 19, - SettingsV2 = 19 + SettingsV2 = 20 } export enum TerminalKind { diff --git a/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx b/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx index 8b4866de1..daa0ab64e 100644 --- a/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx +++ b/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx @@ -44,12 +44,10 @@ export const FeaturePanelComponent: React.FunctionComponent = () => { onChange?: (_?: React.FormEvent, checked?: boolean) => void; }[] = [ { key: "feature.enablechangefeedpolicy", label: "Enable change feed policy", value: "true" }, - { key: "feature.enablerupm", label: "Enable RUPM", value: "true" }, { key: "feature.dataexplorerexecutesproc", label: "Execute stored procedure", value: "true" }, { key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" }, { key: "feature.enablettl", label: "Enable TTL", value: "true" }, { key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" }, - { key: "feature.enablecodeofconduct", label: "Enable Code Of Conduct Acknowledgement", value: "true" }, { key: "feature.selfServeTypeForTest", label: "self serve type passed on for testing", value: "sample" }, { key: "feature.enableLinkInjection", diff --git a/src/Explorer/Controls/FeaturePanel/__snapshots__/FeaturePanelComponent.test.tsx.snap b/src/Explorer/Controls/FeaturePanel/__snapshots__/FeaturePanelComponent.test.tsx.snap index 7f4a39014..3a47b12eb 100644 --- a/src/Explorer/Controls/FeaturePanel/__snapshots__/FeaturePanelComponent.test.tsx.snap +++ b/src/Explorer/Controls/FeaturePanel/__snapshots__/FeaturePanelComponent.test.tsx.snap @@ -131,12 +131,6 @@ exports[`Feature panel renders all flags 1`] = ` label="Enable change feed policy" onChange={[Function]} /> - @@ -178,12 +172,6 @@ exports[`Feature panel renders all flags 1`] = ` className="checkboxRow" horizontalAlign="space-between" > - { + return !data || data.length === 0; + }; + + private createEmptyTabContent = (iconName: string, line1: string, line2: string): JSX.Element => { + return ( + + + {line1} + {line2} + + ); + }; + + private createSamplesTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => { + return { + tab, + content: this.createSearchBarHeader(this.createCardsTabContent(data)) + }; + }; + private createPublicGalleryTab( tab: GalleryTab, data: IGalleryItem[], @@ -194,17 +216,29 @@ export class GalleryViewerComponent extends React.Component { return { tab, - content: this.createPublishedNotebooksTabContent(data) + content: this.isEmptyData(data) + ? this.createEmptyTabContent( + "Contact", + "You have not published anything", + "Publish your sample notebooks to share your published work with others" + ) + : this.createPublishedNotebooksTabContent(data) }; }; @@ -364,9 +398,9 @@ export class GalleryViewerComponent extends React.Component { if (!offline) { try { - let response: IJunoResponse | IJunoResponse; - if (this.props.container.isCodeOfConductEnabled()) { - response = await this.props.junoClient.fetchPublicNotebooks(); + let response: IJunoResponse | IJunoResponse; + if (this.props.container) { + response = await this.props.junoClient.getPublicGalleryData(); this.isCodeOfConductAccepted = response.data?.metadata.acceptedCodeOfConduct; this.publicNotebooks = response.data?.notebooksData; } else { @@ -568,7 +602,7 @@ export class GalleryViewerComponent extends React.Component => { GalleryUtils.deleteItem(this.props.container, this.props.junoClient, data, item => { - this.publishedNotebooks = this.publishedNotebooks.filter(notebook => item.id !== notebook.id); + this.publishedNotebooks = this.publishedNotebooks?.filter(notebook => item.id !== notebook.id); this.refreshSelectedTab(item); }); }; diff --git a/src/Explorer/Controls/Settings/SettingsComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsComponent.test.tsx index c230753e4..c8ea4f390 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.test.tsx @@ -92,7 +92,8 @@ describe("SettingsComponent", () => { autoscaleMaxThroughput: 10000, manualThroughput: undefined, minimumThroughput: 400, - id: "test" + id: "test", + offerReplacePending: false }); const props = { ...baseProps }; diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index 365666be9..585fc00c3 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -295,7 +295,7 @@ export class SettingsComponent extends React.Component { - return !!this.collection?.offer()?.headers?.[Constants.HttpHeaders.offerReplacePending]; + return this.collection?.offer()?.offerReplacePending; }; public onSaveClick = async (): Promise => { diff --git a/src/Explorer/Controls/Settings/SettingsRenderUtils.test.tsx b/src/Explorer/Controls/Settings/SettingsRenderUtils.test.tsx index f5f7e5f1e..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, true)} - - {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 04305cd96..1891f5875 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,11 +31,42 @@ import { MessageBarType, Stack, Spinner, - SpinnerSize + SpinnerSize, + DetailsList, + IColumn, + SelectionMode, + DetailsListLayoutMode, + IDetailsRowProps, + DetailsRow, + IDetailsColumnStyles } from "office-ui-fabric-react"; import { isDirtyTypes, isDirty } from "./SettingsUtils"; -export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } }; +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: 14 } }; export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = { label: { @@ -104,6 +134,16 @@ export const transparentDetailsRowStyles: Partial = { } }; +export const transparentDetailsHeaderStyle: Partial = { + root: { + selectors: { + ":hover": { + background: "transparent" + } + } + } +}; + export const customDetailsListStyles: Partial = { root: { selectors: { @@ -126,10 +166,17 @@ export const separatorStyles: Partial = { ] }; -export const messageBarStyles: Partial = { root: { marginTop: "5px" } }; +export const messageBarStyles: Partial = { + root: { marginTop: "5px", backgroundColor: "white" }, + text: { fontSize: 14 } +}; export const throughputUnit = "RU/s"; +export function onRenderRow(props: IDetailsRowProps): JSX.Element { + return ; +} + export const getAutoPilotV3SpendElement = ( maxAutoPilotThroughputSet: number, isDatabaseThroughput: boolean, @@ -165,64 +212,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, - rupmEnabled: boolean + numberOfRegions: number, + priceBreakdown: PriceBreakdown, + isAutoscale: boolean ): JSX.Element => { - const hourlyPrice: number = computeRUUsagePriceHourly(serverId, rupmEnabled, 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} + + ); }; @@ -266,6 +310,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/IndexingPolicyRefresh/__snapshots__/IndexingPolicyRefreshComponent.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/__snapshots__/IndexingPolicyRefreshComponent.test.tsx.snap index 1c44dd435..f067bed43 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/__snapshots__/IndexingPolicyRefreshComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/__snapshots__/IndexingPolicyRefreshComponent.test.tsx.snap @@ -8,7 +8,7 @@ exports[`IndexingPolicyRefreshComponent renders 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } 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/ScaleComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx index 628ac46ea..cab9803b7 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx @@ -59,7 +59,7 @@ describe("ScaleComponent", () => { autoscaleMaxThroughput: maxThroughput, minimumThroughput: 400, id: "offer", - headers: { "x-ms-offer-replace-pending": true } + offerReplacePending: true }); const newProps = { ...baseProps, diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx index c8f193b44..373e03c4c 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx @@ -16,7 +16,7 @@ import { } from "../SettingsRenderUtils"; import { hasDatabaseSharedThroughput } from "../SettingsUtils"; import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils"; -import { Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react"; +import { Link, Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react"; import { configContext, Platform } from "../../../../ConfigContext"; export interface ScaleComponentProps { @@ -116,7 +116,7 @@ export class ScaleComponent extends React.Component { } const offer = this.props.collection?.offer(); - if (offer?.headers?.[Constants.HttpHeaders.offerReplacePending]) { + if (offer?.offerReplacePending) { const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput; return getThroughputApplyShortDelayMessage( this.props.isAutoPilotSelected, @@ -176,6 +176,7 @@ export class ScaleComponent extends React.Component { label={this.getThroughputTitle()} isEmulator={this.isEmulator} isFixed={this.props.isFixedContainer} + isFreeTierAccount={this.isFreeTierAccount()} isAutoPilotSelected={this.props.isAutoPilotSelected} onAutoPilotSelected={this.props.onAutoPilotSelected} wasAutopilotOriginallySet={this.props.wasAutopilotOriginallySet} @@ -190,9 +191,37 @@ export class ScaleComponent extends React.Component { /> ); + private isFreeTierAccount(): boolean { + const databaseAccount = this.props.container?.databaseAccount(); + return databaseAccount?.properties?.enableFreeTier; + } + + private getFreeTierInfoMessage(): JSX.Element { + return ( + + With free tier, you will get the first 400 RU/s and 5 GB of storage in this account for free. To keep your + account free, keep the total RU/s across all resources in the account to 400 RU/s. + + Learn more. + + + ); + } + public render(): JSX.Element { return ( + {this.isFreeTierAccount() && ( + + {this.getFreeTierInfoMessage()} + + )} {this.getInitialNotificationElement() && ( {this.getInitialNotificationElement()} )} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx index 4322bd6e8..04944804a 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx @@ -13,16 +13,7 @@ import { } from "../SettingsUtils"; import Explorer from "../../../Explorer"; import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon"; -import { - Label, - Text, - TextField, - Stack, - IChoiceGroupOption, - ChoiceGroup, - MessageBar, - MessageBarType -} from "office-ui-fabric-react"; +import { Label, Text, TextField, Stack, IChoiceGroupOption, ChoiceGroup, MessageBar } from "office-ui-fabric-react"; import { getTextFieldStyles, changeFeedPolicyToolTip, @@ -190,7 +181,10 @@ export class SubSettingsComponent extends React.Component {isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && ( - + {ttlWarning} )} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx index 2ccf70079..fc030193f 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx @@ -26,6 +26,7 @@ describe("ThroughputInputAutoPilotV3Component", () => { spendAckVisible: false, showAsMandatory: true, isFixed: false, + isFreeTierAccount: false, label: "label", infoBubbleText: "infoBubbleText", canExceedMaximumValue: true, @@ -54,7 +55,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 +72,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 f3efba887..b524750a7 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,8 @@ import { Label, Link, MessageBar, - MessageBarType + FontIcon, + IColumn } from "office-ui-fabric-react"; import { ToolTipLabelComponent } from "../ToolTipLabelComponent"; import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils"; @@ -32,7 +38,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 { @@ -51,6 +57,7 @@ export interface ThroughputInputAutoPilotV3Props { spendAckVisible?: boolean; showAsMandatory?: boolean; isFixed: boolean; + isFreeTierAccount: boolean; isEmulator: boolean; label: string; infoBubbleText?: string; @@ -69,6 +76,7 @@ export interface ThroughputInputAutoPilotV3Props { interface ThroughputInputAutoPilotV3State { spendAckChecked: boolean; + exceedFreeTierThroughput: boolean; } export class ThroughputInputAutoPilotV3Component extends React.Component< @@ -142,7 +150,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< public constructor(props: ThroughputInputAutoPilotV3Props) { super(props); this.state = { - spendAckChecked: this.props.spendAckChecked + spendAckChecked: this.props.spendAckChecked, + exceedFreeTierThroughput: + this.props.isFreeTierAccount && !this.props.isAutoPilotSelected && this.props.throughput > 400 }; this.step = this.props.step ?? ThroughputInputAutoPilotV3Component.defaultStep; @@ -165,34 +175,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, - false + 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 <>; @@ -220,6 +439,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< if (this.overrideWithAutoPilotSettings()) { this.props.onMaxAutoPilotThroughputChange(newThroughput); } else { + this.setState({ exceedFreeTierThroughput: this.props.isFreeTierAccount && newThroughput > 400 }); this.props.onThroughputChange(newThroughput); } }; @@ -263,7 +483,10 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< /> {this.overrideWithProvisionedThroughputSettings() && ( - + {manualToAutoscaleDisclaimerElement} )} @@ -319,6 +542,12 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< private renderThroughputInput = (): JSX.Element => ( + + Estimate your required throughput with + + {` capacity calculator`} + + + {this.state.exceedFreeTierThroughput && ( + + { + "Billing will apply if you provision more than 400 RU/s of manual throughput, or if the resource scales beyond 400 RU/s with autoscale." + } + + )} {this.props.getThroughputWarningMessage() && ( - + {this.props.getThroughputWarningMessage()} )} @@ -350,13 +592,32 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< onChange={this.onSpendAckChecked} /> )} +
{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..74161bd49 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,26 @@ 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 + + @@ -44,7 +73,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -156,7 +185,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -214,6 +243,19 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox 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 + + + +
`; @@ -311,7 +458,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -369,6 +516,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/SettingsSubComponents/__snapshots__/ScaleComponent.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ScaleComponent.test.tsx.snap index d498de8e7..abba424b9 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ScaleComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ScaleComponent.test.tsx.snap @@ -16,7 +16,7 @@ exports[`ScaleComponent renders with correct initial notification 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/SubSettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/SubSettingsComponent.test.tsx.snap index b0144bf7a..ff376c3c5 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/SubSettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/SubSettingsComponent.test.tsx.snap @@ -136,7 +136,7 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -412,7 +412,7 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -952,7 +952,7 @@ exports[`SubSettingsComponent renders 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -1228,7 +1228,7 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } diff --git a/src/Explorer/Controls/Settings/TestUtils.tsx b/src/Explorer/Controls/Settings/TestUtils.tsx index 020d1e2e5..6a318bf80 100644 --- a/src/Explorer/Controls/Settings/TestUtils.tsx +++ b/src/Explorer/Controls/Settings/TestUtils.tsx @@ -23,7 +23,8 @@ export const collection = ({ autoscaleMaxThroughput: undefined, manualThroughput: 10000, minimumThroughput: 6000, - id: "offer" + id: "offer", + offerReplacePending: false }), conflictResolutionPolicy: ko.observable( {} as DataModels.ConflictResolutionPolicy diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index c4923e5d0..d8cf13e79 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -55,6 +55,7 @@ exports[`SettingsComponent renders 1`] = ` "firstFieldHasFocus": [Function], "formErrors": [Function], "formErrorsDetails": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "adddatabasepane", "isAutoPilotSelected": [Function], "isExecuting": [Function], @@ -104,6 +105,7 @@ exports[`SettingsComponent renders 1`] = ` "formErrors": [Function], "formErrorsDetails": [Function], "formWarnings": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "addcollectionpane", "isAnalyticalStorageOn": [Function], "isAutoPilotSelected": [Function], @@ -133,8 +135,6 @@ exports[`SettingsComponent renders 1`] = ` "partitionKeyVisible": [Function], "requestUnitsUsageCost": [Function], "ruToolTipText": [Function], - "rupm": [Function], - "rupmVisible": [Function], "sharedAutoPilotThroughput": [Function], "sharedThroughputRangeText": [Function], "shouldCreateMongoWildcardIndex": [Function], @@ -593,6 +593,7 @@ exports[`SettingsComponent renders 1`] = ` "formErrors": [Function], "formErrorsDetails": [Function], "formWarnings": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "addcollectionpane", "isAnalyticalStorageOn": [Function], "isAutoPilotSelected": [Function], @@ -622,8 +623,6 @@ exports[`SettingsComponent renders 1`] = ` "partitionKeyVisible": [Function], "requestUnitsUsageCost": [Function], "ruToolTipText": [Function], - "rupm": [Function], - "rupmVisible": [Function], "sharedAutoPilotThroughput": [Function], "sharedThroughputRangeText": [Function], "shouldCreateMongoWildcardIndex": [Function], @@ -669,6 +668,7 @@ exports[`SettingsComponent renders 1`] = ` "firstFieldHasFocus": [Function], "formErrors": [Function], "formErrorsDetails": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "adddatabasepane", "isAutoPilotSelected": [Function], "isExecuting": [Function], @@ -735,7 +735,6 @@ exports[`SettingsComponent renders 1`] = ` "visible": [Function], }, "arcadiaToken": [Function], - "armEndpoint": [Function], "browseQueriesPane": BrowseQueriesPane { "canSaveQueries": [Function], "container": [Circular], @@ -947,7 +946,6 @@ exports[`SettingsComponent renders 1`] = ` "hasWriteAccess": [Function], "isAccountReady": [Function], "isAuthWithResourceToken": [Function], - "isCodeOfConductEnabled": [Function], "isCopyNotebookPaneEnabled": [Function], "isEnableMongoCapabilityPresent": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], @@ -1028,7 +1026,6 @@ exports[`SettingsComponent renders 1`] = ` "onRefreshResourcesClick": [Function], "onSwitchToConnectionString": [Function], "onToggleKeyDown": [Function], - "parentFrameDataExplorerVersion": [Function], "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], @@ -1054,7 +1051,6 @@ exports[`SettingsComponent renders 1`] = ` "titleLabel": "Select Columns", "visible": [Function], }, - "quotaId": [Function], "refreshDatabaseAccount": [Function], "refreshNotebookList": [Function], "refreshTreeTitle": [Function], @@ -1334,6 +1330,7 @@ exports[`SettingsComponent renders 1`] = ` "firstFieldHasFocus": [Function], "formErrors": [Function], "formErrorsDetails": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "adddatabasepane", "isAutoPilotSelected": [Function], "isExecuting": [Function], @@ -1383,6 +1380,7 @@ exports[`SettingsComponent renders 1`] = ` "formErrors": [Function], "formErrorsDetails": [Function], "formWarnings": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "addcollectionpane", "isAnalyticalStorageOn": [Function], "isAutoPilotSelected": [Function], @@ -1412,8 +1410,6 @@ exports[`SettingsComponent renders 1`] = ` "partitionKeyVisible": [Function], "requestUnitsUsageCost": [Function], "ruToolTipText": [Function], - "rupm": [Function], - "rupmVisible": [Function], "sharedAutoPilotThroughput": [Function], "sharedThroughputRangeText": [Function], "shouldCreateMongoWildcardIndex": [Function], @@ -1872,6 +1868,7 @@ exports[`SettingsComponent renders 1`] = ` "formErrors": [Function], "formErrorsDetails": [Function], "formWarnings": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "addcollectionpane", "isAnalyticalStorageOn": [Function], "isAutoPilotSelected": [Function], @@ -1901,8 +1898,6 @@ exports[`SettingsComponent renders 1`] = ` "partitionKeyVisible": [Function], "requestUnitsUsageCost": [Function], "ruToolTipText": [Function], - "rupm": [Function], - "rupmVisible": [Function], "sharedAutoPilotThroughput": [Function], "sharedThroughputRangeText": [Function], "shouldCreateMongoWildcardIndex": [Function], @@ -1948,6 +1943,7 @@ exports[`SettingsComponent renders 1`] = ` "firstFieldHasFocus": [Function], "formErrors": [Function], "formErrorsDetails": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "adddatabasepane", "isAutoPilotSelected": [Function], "isExecuting": [Function], @@ -2014,7 +2010,6 @@ exports[`SettingsComponent renders 1`] = ` "visible": [Function], }, "arcadiaToken": [Function], - "armEndpoint": [Function], "browseQueriesPane": BrowseQueriesPane { "canSaveQueries": [Function], "container": [Circular], @@ -2226,7 +2221,6 @@ exports[`SettingsComponent renders 1`] = ` "hasWriteAccess": [Function], "isAccountReady": [Function], "isAuthWithResourceToken": [Function], - "isCodeOfConductEnabled": [Function], "isCopyNotebookPaneEnabled": [Function], "isEnableMongoCapabilityPresent": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], @@ -2307,7 +2301,6 @@ exports[`SettingsComponent renders 1`] = ` "onRefreshResourcesClick": [Function], "onSwitchToConnectionString": [Function], "onToggleKeyDown": [Function], - "parentFrameDataExplorerVersion": [Function], "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], @@ -2333,7 +2326,6 @@ exports[`SettingsComponent renders 1`] = ` "titleLabel": "Select Columns", "visible": [Function], }, - "quotaId": [Function], "refreshDatabaseAccount": [Function], "refreshNotebookList": [Function], "refreshTreeTitle": [Function], @@ -2626,6 +2618,7 @@ exports[`SettingsComponent renders 1`] = ` "firstFieldHasFocus": [Function], "formErrors": [Function], "formErrorsDetails": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "adddatabasepane", "isAutoPilotSelected": [Function], "isExecuting": [Function], @@ -2675,6 +2668,7 @@ exports[`SettingsComponent renders 1`] = ` "formErrors": [Function], "formErrorsDetails": [Function], "formWarnings": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "addcollectionpane", "isAnalyticalStorageOn": [Function], "isAutoPilotSelected": [Function], @@ -2704,8 +2698,6 @@ exports[`SettingsComponent renders 1`] = ` "partitionKeyVisible": [Function], "requestUnitsUsageCost": [Function], "ruToolTipText": [Function], - "rupm": [Function], - "rupmVisible": [Function], "sharedAutoPilotThroughput": [Function], "sharedThroughputRangeText": [Function], "shouldCreateMongoWildcardIndex": [Function], @@ -3164,6 +3156,7 @@ exports[`SettingsComponent renders 1`] = ` "formErrors": [Function], "formErrorsDetails": [Function], "formWarnings": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "addcollectionpane", "isAnalyticalStorageOn": [Function], "isAutoPilotSelected": [Function], @@ -3193,8 +3186,6 @@ exports[`SettingsComponent renders 1`] = ` "partitionKeyVisible": [Function], "requestUnitsUsageCost": [Function], "ruToolTipText": [Function], - "rupm": [Function], - "rupmVisible": [Function], "sharedAutoPilotThroughput": [Function], "sharedThroughputRangeText": [Function], "shouldCreateMongoWildcardIndex": [Function], @@ -3240,6 +3231,7 @@ exports[`SettingsComponent renders 1`] = ` "firstFieldHasFocus": [Function], "formErrors": [Function], "formErrorsDetails": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "adddatabasepane", "isAutoPilotSelected": [Function], "isExecuting": [Function], @@ -3306,7 +3298,6 @@ exports[`SettingsComponent renders 1`] = ` "visible": [Function], }, "arcadiaToken": [Function], - "armEndpoint": [Function], "browseQueriesPane": BrowseQueriesPane { "canSaveQueries": [Function], "container": [Circular], @@ -3518,7 +3509,6 @@ exports[`SettingsComponent renders 1`] = ` "hasWriteAccess": [Function], "isAccountReady": [Function], "isAuthWithResourceToken": [Function], - "isCodeOfConductEnabled": [Function], "isCopyNotebookPaneEnabled": [Function], "isEnableMongoCapabilityPresent": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], @@ -3599,7 +3589,6 @@ exports[`SettingsComponent renders 1`] = ` "onRefreshResourcesClick": [Function], "onSwitchToConnectionString": [Function], "onToggleKeyDown": [Function], - "parentFrameDataExplorerVersion": [Function], "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], @@ -3625,7 +3614,6 @@ exports[`SettingsComponent renders 1`] = ` "titleLabel": "Select Columns", "visible": [Function], }, - "quotaId": [Function], "refreshDatabaseAccount": [Function], "refreshNotebookList": [Function], "refreshTreeTitle": [Function], @@ -3905,6 +3893,7 @@ exports[`SettingsComponent renders 1`] = ` "firstFieldHasFocus": [Function], "formErrors": [Function], "formErrorsDetails": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "adddatabasepane", "isAutoPilotSelected": [Function], "isExecuting": [Function], @@ -3954,6 +3943,7 @@ exports[`SettingsComponent renders 1`] = ` "formErrors": [Function], "formErrorsDetails": [Function], "formWarnings": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "addcollectionpane", "isAnalyticalStorageOn": [Function], "isAutoPilotSelected": [Function], @@ -3983,8 +3973,6 @@ exports[`SettingsComponent renders 1`] = ` "partitionKeyVisible": [Function], "requestUnitsUsageCost": [Function], "ruToolTipText": [Function], - "rupm": [Function], - "rupmVisible": [Function], "sharedAutoPilotThroughput": [Function], "sharedThroughputRangeText": [Function], "shouldCreateMongoWildcardIndex": [Function], @@ -4443,6 +4431,7 @@ exports[`SettingsComponent renders 1`] = ` "formErrors": [Function], "formErrorsDetails": [Function], "formWarnings": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "addcollectionpane", "isAnalyticalStorageOn": [Function], "isAutoPilotSelected": [Function], @@ -4472,8 +4461,6 @@ exports[`SettingsComponent renders 1`] = ` "partitionKeyVisible": [Function], "requestUnitsUsageCost": [Function], "ruToolTipText": [Function], - "rupm": [Function], - "rupmVisible": [Function], "sharedAutoPilotThroughput": [Function], "sharedThroughputRangeText": [Function], "shouldCreateMongoWildcardIndex": [Function], @@ -4519,6 +4506,7 @@ exports[`SettingsComponent renders 1`] = ` "firstFieldHasFocus": [Function], "formErrors": [Function], "formErrorsDetails": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "adddatabasepane", "isAutoPilotSelected": [Function], "isExecuting": [Function], @@ -4585,7 +4573,6 @@ exports[`SettingsComponent renders 1`] = ` "visible": [Function], }, "arcadiaToken": [Function], - "armEndpoint": [Function], "browseQueriesPane": BrowseQueriesPane { "canSaveQueries": [Function], "container": [Circular], @@ -4797,7 +4784,6 @@ exports[`SettingsComponent renders 1`] = ` "hasWriteAccess": [Function], "isAccountReady": [Function], "isAuthWithResourceToken": [Function], - "isCodeOfConductEnabled": [Function], "isCopyNotebookPaneEnabled": [Function], "isEnableMongoCapabilityPresent": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], @@ -4878,7 +4864,6 @@ exports[`SettingsComponent renders 1`] = ` "onRefreshResourcesClick": [Function], "onSwitchToConnectionString": [Function], "onToggleKeyDown": [Function], - "parentFrameDataExplorerVersion": [Function], "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], @@ -4904,7 +4889,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 5d0a46560..25cb13989 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap @@ -60,72 +60,106 @@ exports[`SettingsUtils functions render 1`] = ` . - - Estimated cost ( - RMB - ): - - - ¥ - 1.29 - hourly - / - ¥ - 31.06 - daily - / - ¥ - 944.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 + + + ; overrideWithAutoPilotSettings: ko.Observable; overrideWithProvisionedThroughputSettings: ko.Observable; + freeTierExceedThroughputTooltip?: ko.Observable; + freeTierExceedThroughputWarning?: ko.Observable; } export class ThroughputInputViewModel extends WaitsForTemplateViewModel { @@ -165,6 +167,10 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel { public overrideWithProvisionedThroughputSettings: ko.Observable; public isManualThroughputInputFieldRequired: ko.Computed; public isAutoscaleThroughputInputFieldRequired: ko.Computed; + public freeTierExceedThroughputTooltip: ko.Observable; + public freeTierExceedThroughputWarning: ko.Observable; + public showFreeTierExceedThroughputTooltip: ko.Computed; + public showFreeTierExceedThroughputWarning: ko.Computed; public constructor(options: ThroughputInputParams) { super(); @@ -219,6 +225,16 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel { this.isAutoscaleThroughputInputFieldRequired = ko.pureComputed( () => this.isEnabled() && this.isAutoPilotSelected() ); + + this.freeTierExceedThroughputTooltip = options.freeTierExceedThroughputTooltip || ko.observable(); + this.freeTierExceedThroughputWarning = options.freeTierExceedThroughputWarning || ko.observable(); + this.showFreeTierExceedThroughputTooltip = ko.pureComputed( + () => !!this.freeTierExceedThroughputTooltip() && this.value() > 400 + ); + + this.showFreeTierExceedThroughputWarning = ko.pureComputed( + () => !!this.freeTierExceedThroughputWarning() && this.value() > 400 + ); } public decreaseThroughput() { diff --git a/src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoscaleV3.html b/src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoscaleV3.html index bda2ec9b7..8ec328aba 100644 --- a/src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoscaleV3.html +++ b/src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoscaleV3.html @@ -126,6 +126,20 @@
+

+ Estimate your required throughput with + capacity calculator +

+ +
+ +
+
+ Warning + +
+

diff --git a/src/Explorer/DataSamples/ContainerSampleGenerator.test.ts b/src/Explorer/DataSamples/ContainerSampleGenerator.test.ts index d64d84152..838e829a4 100644 --- a/src/Explorer/DataSamples/ContainerSampleGenerator.test.ts +++ b/src/Explorer/DataSamples/ContainerSampleGenerator.test.ts @@ -1,11 +1,11 @@ -jest.mock("../../Common/DocumentClientUtilityBase"); jest.mock("../Graph/GraphExplorerComponent/GremlinClient"); jest.mock("../../Common/dataAccess/createCollection"); +jest.mock("../../Common/dataAccess/createDocument"); import * as ko from "knockout"; import * as ViewModels from "../../Contracts/ViewModels"; import Q from "q"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; -import { createDocument } from "../../Common/DocumentClientUtilityBase"; +import { createDocument } from "../../Common/dataAccess/createDocument"; import Explorer from "../Explorer"; import { updateUserContext } from "../../UserContext"; diff --git a/src/Explorer/DataSamples/ContainerSampleGenerator.ts b/src/Explorer/DataSamples/ContainerSampleGenerator.ts index 3c3b28732..b115a98b1 100644 --- a/src/Explorer/DataSamples/ContainerSampleGenerator.ts +++ b/src/Explorer/DataSamples/ContainerSampleGenerator.ts @@ -4,8 +4,8 @@ import GraphTab from ".././Tabs/GraphTab"; import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import Explorer from "../Explorer"; -import { createDocument } from "../../Common/DocumentClientUtilityBase"; import { createCollection } from "../../Common/dataAccess/createCollection"; +import { createDocument } from "../../Common/dataAccess/createDocument"; import { userContext } from "../../UserContext"; interface SampleDataFile extends DataModels.CreateCollectionParams { @@ -95,12 +95,15 @@ export class ContainerSampleGenerator { .reduce((previous, current) => previous.then(current), Promise.resolve()); } else { // For SQL all queries are executed at the same time - this.sampleDataFile.data.map(doc => { - const subPromise = createDocument(collection, doc); - subPromise.catch(reason => NotificationConsoleUtils.logConsoleError(reason)); - promises.push(subPromise); - }); - await Promise.all(promises); + await Promise.all( + this.sampleDataFile.data.map(async doc => { + try { + await createDocument(collection, doc); + } catch (error) { + NotificationConsoleUtils.logConsoleError(error); + } + }) + ); } } diff --git a/src/Explorer/Explorer.ts b/src/Explorer/Explorer.ts index e0832bfab..c601cb1d8 100644 --- a/src/Explorer/Explorer.ts +++ b/src/Explorer/Explorer.ts @@ -18,7 +18,7 @@ import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPa import { readCollection } from "../Common/dataAccess/readCollection"; import { readDatabases } from "../Common/dataAccess/readDatabases"; import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane"; -import EnvironmentUtility from "../Common/EnvironmentUtility"; +import { normalizeArmEndpoint } from "../Common/EnvironmentUtility"; import GraphStylingPane from "./Panes/GraphStylingPane"; import hasher from "hasher"; import NewVertexPane from "./Panes/NewVertexPane"; @@ -124,7 +124,6 @@ export default class Explorer { public databaseAccount: ko.Observable; 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; @@ -139,12 +138,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 @@ -209,7 +206,6 @@ export default class Explorer { // features public isGalleryPublishEnabled: ko.Computed; - public isCodeOfConductEnabled: ko.Computed; public isLinkInjectionEnabled: ko.Computed; public isGitHubPaneEnabled: ko.Observable; public isPublishNotebookPaneEnabled: ko.Observable; @@ -285,7 +281,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) => { @@ -326,9 +321,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) ); @@ -378,7 +373,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); @@ -411,9 +405,6 @@ export default class Explorer { this.isGalleryPublishEnabled = ko.computed(() => this.isFeatureEnabled(Constants.Features.enableGalleryPublish) ); - this.isCodeOfConductEnabled = ko.computed(() => - this.isFeatureEnabled(Constants.Features.enableCodeOfConduct) - ); this.isLinkInjectionEnabled = ko.computed(() => this.isFeatureEnabled(Constants.Features.enableLinkInjection) ); @@ -1025,9 +1016,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( @@ -1767,61 +1756,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 { @@ -1875,8 +1862,14 @@ export default class Explorer { } } - 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; @@ -1885,26 +1878,19 @@ 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); this.setSelfServeType(inputs); - - 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({ @@ -1913,7 +1899,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, @@ -1927,7 +1914,6 @@ export default class Explorer { this.isAccountReady(true); } - return Q(); } public setFeatureFlagsFromFlights(flights: readonly string[]): void { @@ -2300,7 +2286,6 @@ export default class Explorer { name, content, parentDomElement, - this.isCodeOfConductEnabled(), this.isLinkInjectionEnabled() ); this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter; @@ -2591,7 +2576,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 @@ -2600,7 +2585,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, @@ -2620,7 +2605,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 @@ -2628,7 +2613,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, @@ -3058,4 +3043,25 @@ export default class Explorer { }) ); } + + public isFirstResourceCreated(): boolean { + const databases: ViewModels.Database[] = this.databases(); + + if (!databases || databases.length === 0) { + return false; + } + + return databases.some(database => { + // user has created at least one collection + if (database.collections()?.length > 0) { + return true; + } + // user has created a database with shared throughput + if (database.offer()) { + return true; + } + // use has created an empty database without shared throughput + return false; + }); + } } diff --git a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx index 17c5d963c..c381dc8c8 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx @@ -1,4 +1,5 @@ -jest.mock("../../../Common/DocumentClientUtilityBase"); +jest.mock("../../../Common/dataAccess/queryDocuments"); +jest.mock("../../../Common/dataAccess/queryDocumentsPage"); import React from "react"; import * as sinon from "sinon"; import { mount, ReactWrapper } from "enzyme"; @@ -12,7 +13,8 @@ import * as DataModels from "../../../Contracts/DataModels"; import * as StorageUtility from "../../../Shared/StorageUtility"; import GraphTab from "../../Tabs/GraphTab"; import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; -import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase"; +import { queryDocuments } from "../../../Common/dataAccess/queryDocuments"; +import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage"; describe("Check whether query result is vertex array", () => { it("should reject null as vertex array", () => { @@ -299,12 +301,12 @@ describe("GraphExplorer", () => { ignoreD3Update: boolean ): GraphExplorer => { (queryDocuments as jest.Mock).mockImplementation((container: any, query: string, options: any) => { - return Q.resolve({ + return { _query: query, nextItem: (callback: (error: any, document: DataModels.DocumentId) => void): void => {}, hasMoreResults: () => false, executeNext: (callback: (error: any, documents: DataModels.DocumentId[], headers: any) => void): void => {} - }); + }; }); (queryDocumentsPage as jest.Mock).mockImplementation( (rid: string, iterator: any, firstItemIndex: number, options: any) => { diff --git a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx index 974ec3a8f..af2d90059 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx @@ -28,8 +28,10 @@ import * as Constants from "../../../Common/Constants"; import { InputProperty } from "../../../Contracts/ViewModels"; import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos"; import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif"; -import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase"; +import { queryDocuments } from "../../../Common/dataAccess/queryDocuments"; +import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage"; import { getErrorMessage } from "../../../Common/ErrorHandlingUtils"; +import { FeedOptions } from "@azure/cosmos"; export interface GraphAccessor { applyFilter: () => void; @@ -725,26 +727,32 @@ export class GraphExplorer extends React.Component { - // TODO maxItemCount: this reduces throttling, but won't cap the # of results - return queryDocuments(this.props.databaseId, this.props.collectionId, query, { - maxItemCount: GraphExplorer.PAGE_ALL, - enableCrossPartitionQuery: - StorageUtility.LocalStorageUtility.getEntryString(StorageUtility.StorageKey.IsCrossPartitionQueryEnabled) === - "true" - }).then( - (iterator: QueryIterator) => { - return iterator.fetchNext().then(response => response.resources); - }, - (reason: any) => { - GraphExplorer.reportToConsole( - ConsoleDataType.Error, - `Failed to execute non-paged query ${query}. Reason:${reason}`, - reason - ); - return null; - } - ); + public async executeNonPagedDocDbQuery(query: string): Promise { + try { + // TODO maxItemCount: this reduces throttling, but won't cap the # of results + const iterator: QueryIterator = queryDocuments( + this.props.databaseId, + this.props.collectionId, + query, + { + maxItemCount: GraphExplorer.PAGE_ALL, + enableCrossPartitionQuery: + StorageUtility.LocalStorageUtility.getEntryString( + StorageUtility.StorageKey.IsCrossPartitionQueryEnabled + ) === "true" + } as FeedOptions + ); + const response = await iterator.fetchNext(); + + return response?.resources; + } catch (error) { + GraphExplorer.reportToConsole( + ConsoleDataType.Error, + `Failed to execute non-paged query ${query}. Reason:${error}`, + error + ); + return null; + } } /** @@ -864,7 +872,7 @@ export class GraphExplorer extends React.Component { // Clear any progress indicator this.executeCounter = 0; this.setState({ @@ -882,24 +890,22 @@ export class GraphExplorer extends React.Component (this.queryTotalRequestCharge = result.requestCharge), - (error: any) => { - const errorMsg = `Failure in submitting query: ${query}: ${getErrorMessage(error)}`; - GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg); - this.setState({ - filterQueryError: errorMsg - }); + try { + let result: UserQueryResult; + if (query.toLocaleLowerCase() === "g.V()".toLocaleLowerCase()) { + result = await this.executeDocDbGVQuery(); + } else { + result = await this.executeGremlinQuery(query); } - ); + + this.queryTotalRequestCharge = result.requestCharge; + } catch (error) { + const errorMsg = `Failure in submitting query: ${query}: ${getErrorMessage(error)}`; + GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg); + this.setState({ + filterQueryError: errorMsg + }); + } } /** @@ -1390,7 +1396,7 @@ export class GraphExplorer extends React.Component { + private updatePossibleVertices(): Promise { const highlightedNodeId = this.state.highlightedNode ? this.state.highlightedNode.id : null; const q = `SELECT c.id, c["${this.props.graphConfigUiData.nodeCaptionChoice() || @@ -1721,85 +1727,81 @@ export class GraphExplorer extends React.Component { + private async executeDocDbGVQuery(): Promise { let query = "select root.id from root where IS_DEFINED(root._isEdge) = false order by root._ts desc"; if (this.props.collectionPartitionKeyProperty) { query = `select root.id, root.${this.props.collectionPartitionKeyProperty} from root where IS_DEFINED(root._isEdge) = false order by root._ts asc`; } - return queryDocuments(this.props.databaseId, this.props.collectionId, query, { - maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE, - enableCrossPartitionQuery: LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true" - }) - .then( - (iterator: QueryIterator) => { - this.currentDocDBQueryInfo = { - iterator: iterator, - index: 0, - query: query - }; - }, - (reason: any) => { - GraphExplorer.reportToConsole( - ConsoleDataType.Error, - `Failed to execute CosmosDB query: ${query} reason:${reason}` - ); - } - ) - .then(() => this.loadMoreRootNodes()); + try { + const iterator: QueryIterator = queryDocuments( + this.props.databaseId, + this.props.collectionId, + query, + { + maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE, + enableCrossPartitionQuery: + LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true" + } as FeedOptions + ); + this.currentDocDBQueryInfo = { + iterator: iterator, + index: 0, + query: query + }; + return await this.loadMoreRootNodes(); + } catch (error) { + GraphExplorer.reportToConsole( + ConsoleDataType.Error, + `Failed to execute CosmosDB query: ${query} reason:${error}` + ); + throw error; + } } - private loadMoreRootNodes(): Q.Promise { + private async loadMoreRootNodes(): Promise { if (!this.currentDocDBQueryInfo) { - return Q.resolve(null); + return undefined; } - let RU: string = GraphExplorer.REQUEST_CHARGE_UNKNOWN_MSG; + let RU: string = GraphExplorer.REQUEST_CHARGE_UNKNOWN_MSG; const queryInfoStr = `${this.currentDocDBQueryInfo.query} (${this.currentDocDBQueryInfo.index + 1}-${this .currentDocDBQueryInfo.index + GraphExplorer.ROOT_LIST_PAGE_SIZE})`; const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${queryInfoStr}`); - return queryDocumentsPage( - this.props.collectionId, - this.currentDocDBQueryInfo.iterator, - this.currentDocDBQueryInfo.index, - { - enableCrossPartitionQuery: - LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true" - } - ) - .then((results: ViewModels.QueryResults) => { - GraphExplorer.clearConsoleProgress(id); - this.currentDocDBQueryInfo.index = results.lastItemIndex + 1; - this.setState({ hasMoreRoots: results.hasMoreResults }); - RU = results.requestCharge.toString(); - GraphExplorer.reportToConsole( - ConsoleDataType.Info, - `Executed: ${queryInfoStr} ${GremlinClient.GremlinClient.getRequestChargeString(RU)}` - ); - const documents = results.documents || []; - return documents.map( - (item: DataModels.DocumentId) => { - return GraphExplorer.getPkIdFromDocumentId(item, this.props.collectionPartitionKeyProperty); - }, - (reason: any) => { - // Failure - GraphExplorer.clearConsoleProgress(id); - const errorMsg = `Failed to query: ${this.currentDocDBQueryInfo.query}. Reason:${reason}`; - GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg); - this.setState({ - filterQueryError: errorMsg - }); - this.setFilterQueryStatus(FilterQueryStatus.ErrorResult); - throw reason; - } - ); - }) - .then((pkIds: string[]) => { - const arg = pkIds.join(","); - return this.executeGremlinQuery(`g.V(${arg})`); - }) - .then(() => ({ requestCharge: RU })); + try { + const results: ViewModels.QueryResults = await queryDocumentsPage( + this.props.collectionId, + this.currentDocDBQueryInfo.iterator, + this.currentDocDBQueryInfo.index + ); + + GraphExplorer.clearConsoleProgress(id); + this.currentDocDBQueryInfo.index = results.lastItemIndex + 1; + this.setState({ hasMoreRoots: results.hasMoreResults }); + RU = results.requestCharge.toString(); + GraphExplorer.reportToConsole( + ConsoleDataType.Info, + `Executed: ${queryInfoStr} ${GremlinClient.GremlinClient.getRequestChargeString(RU)}` + ); + const pkIds: string[] = (results.documents || []).map((item: DataModels.DocumentId) => + GraphExplorer.getPkIdFromDocumentId(item, this.props.collectionPartitionKeyProperty) + ); + + const arg = pkIds.join(","); + await this.executeGremlinQuery(`g.V(${arg})`); + + return { requestCharge: RU }; + } catch (error) { + GraphExplorer.clearConsoleProgress(id); + const errorMsg = `Failed to query: ${this.currentDocDBQueryInfo.query}. Reason:${getErrorMessage(error)}`; + GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg); + this.setState({ + filterQueryError: errorMsg + }); + this.setFilterQueryStatus(FilterQueryStatus.ErrorResult); + throw error; + } } private executeGremlinQuery(query: string): Q.Promise { diff --git a/src/Explorer/Menus/NotificationConsole/NotificationConsole.less b/src/Explorer/Menus/NotificationConsole/NotificationConsole.less index 570243fc4..2e9413020 100644 --- a/src/Explorer/Menus/NotificationConsole/NotificationConsole.less +++ b/src/Explorer/Menus/NotificationConsole/NotificationConsole.less @@ -99,7 +99,22 @@ .notificationConsoleControls { padding: @MediumSpace; margin-left:@DefaultSpace; + display: flex; + align-items: center; + .ms-Dropdown-container { + display: flex; + .ms-Dropdown-title { + height: 25px; + line-height: 25px; + } + .ms-Dropdown { + min-width: 110px; + margin-left: 10px; + height: 25px; + line-height: 25px; + } + } #consoleFilterLabel { padding: 4px; } @@ -107,6 +122,7 @@ .consoleSplitter { border-left: 1px solid @BaseMedium; margin: @MediumSpace; + height: 20px; } .clearNotificationsButton { diff --git a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx index de411f2c4..6f5916e89 100644 --- a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx +++ b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx @@ -5,7 +5,7 @@ import * as React from "react"; import { ClientDefaults, KeyCodes } from "../../../Common/Constants"; import AnimateHeight from "react-animate-height"; - +import { Dropdown, IDropdownOption } from "office-ui-fabric-react"; import LoadingIcon from "../../../../images/loading.svg"; import ErrorBlackIcon from "../../../../images/error_black.svg"; import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg"; @@ -53,7 +53,12 @@ export class NotificationConsoleComponent extends React.Component< NotificationConsoleComponentState > { private static readonly transitionDurationMs = 200; - private static readonly FilterOptions = ["All", "In Progress", "Info", "Error"]; + private static readonly FilterOptions = [ + { key: "All", text: "All" }, + { key: "In Progress", text: "In progress" }, + { key: "Info", text: "Info" }, + { key: "Error", text: "Error" } + ]; private headerTimeoutId: number; private prevHeaderStatus: string; private consoleHeaderElement: HTMLElement; @@ -62,7 +67,7 @@ export class NotificationConsoleComponent extends React.Component< super(props); this.state = { headerStatus: "", - selectedFilter: NotificationConsoleComponent.FilterOptions[0], + selectedFilter: NotificationConsoleComponent.FilterOptions[0].key || "", isExpanded: props.isConsoleExpanded }; this.prevHeaderStatus = null; @@ -150,20 +155,15 @@ export class NotificationConsoleComponent extends React.Component< >
- - + aria-labelledby="consoleFilterLabel" + aria-label={this.state.selectedFilter} + /> ): void { - this.setState({ selectedFilter: event.target.value }); + private onFilterSelected(event: React.ChangeEvent, option: IDropdownOption): void { + this.setState({ selectedFilter: String(option.key) }); } private getFilteredConsoleData(): ConsoleData[] { diff --git a/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap b/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap index 51d6908eb..08d3750a3 100644 --- a/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap +++ b/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap @@ -110,43 +110,34 @@ exports[`NotificationConsoleComponent renders the console (expanded) 1`] = `
- - + selectedKey="All" + /> diff --git a/src/Explorer/Notebook/NotebookManager.ts b/src/Explorer/Notebook/NotebookManager.ts index 932d80e13..6442394a0 100644 --- a/src/Explorer/Notebook/NotebookManager.ts +++ b/src/Explorer/Notebook/NotebookManager.ts @@ -128,17 +128,9 @@ export default class NotebookManager { name: string, content: string | ImmutableNotebook, parentDomElement: HTMLElement, - isCodeOfConductEnabled: boolean, isLinkInjectionEnabled: boolean ): Promise { - await this.publishNotebookPaneAdapter.open( - name, - getFullName(), - content, - parentDomElement, - isCodeOfConductEnabled, - isLinkInjectionEnabled - ); + await this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement, isLinkInjectionEnabled); } public openCopyNotebookPane(name: string, content: string): void { diff --git a/src/Explorer/Panes/AddCollectionPane.html b/src/Explorer/Panes/AddCollectionPane.html index 9167bcf93..89096032c 100644 --- a/src/Explorer/Panes/AddCollectionPane.html +++ b/src/Explorer/Panes/AddCollectionPane.html @@ -152,7 +152,8 @@ maxAutoPilotThroughputSet: sharedAutoPilotThroughput, autoPilotUsageCost: autoPilotUsageCost, canExceedMaximumValue: canExceedMaximumValue, - showAutoPilot: !isFreeTierAccount() + showAutoPilot: !isFreeTierAccount(), + freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip }">
@@ -243,38 +244,6 @@
-
-
-

- * - RU/m - - More information - - For each 100 Request Units per second (RU/s) provisioned, 1,000 Request Units - per - minute - (RU/m) can be provisioned. E.g.: for a container with 5,000 RU/s provisioned - with - RU/m - enabled, the RU/m budget will be 50,000 RU/m. - - -

-
-
- - -
-
- - -
-
-
-

* @@ -365,7 +334,8 @@ maxAutoPilotThroughputSet: autoPilotThroughput, autoPilotUsageCost: autoPilotUsageCost, canExceedMaximumValue: canExceedMaximumValue, - showAutoPilot: !isFixedStorageSelected() + showAutoPilot: !isFixedStorageSelected(), + freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip }">

diff --git a/src/Explorer/Panes/AddCollectionPane.test.ts b/src/Explorer/Panes/AddCollectionPane.test.ts index ab57ef4f3..09d46614c 100644 --- a/src/Explorer/Panes/AddCollectionPane.test.ts +++ b/src/Explorer/Panes/AddCollectionPane.test.ts @@ -74,7 +74,7 @@ describe("Add Collection Pane", () => { explorer.databaseAccount(mockFreeTierDatabaseAccount); const addCollectionPane = explorer.addCollectionPane as AddCollectionPane; expect(addCollectionPane.isFreeTierAccount()).toBe(true); - expect(addCollectionPane.upsellMessage()).toContain("With free tier discount"); + expect(addCollectionPane.upsellMessage()).toContain("With free tier"); expect(addCollectionPane.upsellAnchorUrl()).toBe(Constants.Urls.freeTierInformation); expect(addCollectionPane.upsellAnchorText()).toBe("Learn more"); }); diff --git a/src/Explorer/Panes/AddCollectionPane.ts b/src/Explorer/Panes/AddCollectionPane.ts index 62a700d54..4818a3e47 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; @@ -42,8 +43,6 @@ export default class AddCollectionPane extends ContextualPaneBase { public partitionKeyVisible: ko.Computed; public partitionKeyPattern: ko.Computed; public partitionKeyTitle: ko.Computed; - public rupm: ko.Observable; - public rupmVisible: ko.Observable; public storage: ko.Observable; public throughputSinglePartition: ViewModels.Editable; public throughputMultiPartition: ViewModels.Editable; @@ -90,6 +89,7 @@ export default class AddCollectionPane extends ContextualPaneBase { public isSynapseLinkUpdating: ko.Computed; public canExceedMaximumValue: ko.PureComputed; public ruToolTipText: ko.Computed; + public freeTierExceedThroughputTooltip: ko.Computed; public canConfigureThroughput: ko.PureComputed; public showUpsellMessage: ko.PureComputed; public shouldCreateMongoWildcardIndex: ko.Observable; @@ -100,7 +100,6 @@ export default class AddCollectionPane extends ContextualPaneBase { super(options); this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText()); this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled()); - this.showUpsellMessage = ko.pureComputed(() => !this.container.isServerlessEnabled()); this.formWarnings = ko.observable(); this.collectionId = ko.observable(); this.databaseId = ko.observable(); @@ -143,12 +142,6 @@ export default class AddCollectionPane extends ContextualPaneBase { } return ""; }); - this.rupm = ko.observable(Constants.RUPMStates.off); - this.rupmVisible = ko.observable(false); - const featureSubcription = this.container.features.subscribe(() => { - this.rupmVisible(this.container.isFeatureEnabled(Constants.Features.enableRupm)); - featureSubcription.dispose(); - }); this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue()); @@ -201,7 +194,6 @@ export default class AddCollectionPane extends ContextualPaneBase { account.properties.readLocations.length) || 1; const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false; - const rupmEnabled: boolean = this.rupm() === Constants.RUPMStates.on; let throughputSpendAckText: string; let estimatedSpend: string; @@ -211,23 +203,15 @@ export default class AddCollectionPane extends ContextualPaneBase { serverId, regions, multimaster, - rupmEnabled, this.isSharedAutoPilotSelected() ); - estimatedSpend = PricingUtils.getEstimatedSpendHtml( - offerThroughput, - serverId, - regions, - multimaster, - rupmEnabled - ); + estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster); } else { throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString( this.sharedAutoPilotThroughput(), serverId, regions, multimaster, - rupmEnabled, this.isSharedAutoPilotSelected() ); estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( @@ -264,7 +248,6 @@ export default class AddCollectionPane extends ContextualPaneBase { account.properties.readLocations.length) || 1; const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false; - const rupmEnabled: boolean = this.rupm() === Constants.RUPMStates.on; let throughputSpendAckText: string; let estimatedSpend: string; @@ -274,15 +257,13 @@ export default class AddCollectionPane extends ContextualPaneBase { serverId, regions, multimaster, - rupmEnabled, this.isAutoPilotSelected() ); estimatedSpend = PricingUtils.getEstimatedSpendHtml( this.throughputMultiPartition(), serverId, regions, - multimaster, - rupmEnabled + multimaster ); } else { throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString( @@ -290,7 +271,6 @@ export default class AddCollectionPane extends ContextualPaneBase { serverId, regions, multimaster, - rupmEnabled, this.isAutoPilotSelected() ); estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( @@ -501,8 +481,20 @@ export default class AddCollectionPane extends ContextualPaneBase { this.resetData(); }); + this.freeTierExceedThroughputTooltip = ko.pureComputed(() => + this.isFreeTierAccount() && !this.container.isFirstResourceCreated() + ? "The first 400 RU/s in this account are free. Billing will apply to any throughput beyond 400 RU/s." + : "" + ); + this.upsellMessage = ko.pureComputed(() => { - return PricingUtils.getUpsellMessage(this.container.serverId(), this.isFreeTierAccount()); + return PricingUtils.getUpsellMessage( + this.container.serverId(), + this.isFreeTierAccount(), + this.container.isFirstResourceCreated(), + this.container.defaultExperience(), + true + ); }); this.upsellMessageAriaLabel = ko.pureComputed(() => { @@ -554,6 +546,23 @@ export default class AddCollectionPane extends ContextualPaneBase { return isFreeTierAccount; }); + this.showUpsellMessage = ko.pureComputed(() => { + if (this.container.isServerlessEnabled()) { + return false; + } + + if ( + this.isFreeTierAccount() && + !this.databaseCreateNew() && + this.databaseHasSharedOffer() && + !this.collectionWithThroughputInShared() + ) { + return false; + } + + return true; + }); + this.showIndexingOptionsForSharedThroughput = ko.computed(() => { const newDatabaseWithSharedOffer = this.databaseCreateNew() && this.databaseCreateNewShared(); const existingDatabaseWithSharedOffer = !this.databaseCreateNew() && this.databaseHasSharedOffer(); @@ -686,11 +695,10 @@ export default class AddCollectionPane extends ContextualPaneBase { storage: this.storage(), offerThroughput: this._getThroughput(), partitionKey: this.partitionKey(), - databaseId: this.databaseId(), - rupm: this.rupm() + 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(), @@ -788,12 +796,11 @@ export default class AddCollectionPane extends ContextualPaneBase { id: this.collectionId(), storage: this.storage(), partitionKey, - rupm: this.rupm(), uniqueKeyPolicy, 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, @@ -863,12 +870,11 @@ export default class AddCollectionPane extends ContextualPaneBase { id: this.collectionId(), storage: this.storage(), partitionKey, - rupm: this.rupm(), uniqueKeyPolicy, 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, @@ -898,12 +904,11 @@ export default class AddCollectionPane extends ContextualPaneBase { id: this.collectionId(), storage: this.storage(), partitionKey, - rupm: this.rupm(), uniqueKeyPolicy, 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, @@ -981,20 +986,6 @@ export default class AddCollectionPane extends ContextualPaneBase { return true; } - public onRupmOptionsKeyDown(source: any, event: KeyboardEvent): boolean { - if (event.key === "ArrowRight") { - this.rupm("off"); - return false; - } - - if (event.key === "ArrowLeft") { - this.rupm("on"); - return false; - } - - return true; - } - public onEnableSynapseLinkButtonClicked() { this.container.openEnableSynapseLinkDialog(); } @@ -1018,16 +1009,6 @@ export default class AddCollectionPane extends ContextualPaneBase { } const throughput = this._getThroughput(); - const maxThroughputWithRUPM = - SharedConstants.CollectionCreation.MaxRUPMPerPartition * this._calculateNumberOfPartitions(); - - if (this.rupm() === Constants.RUPMStates.on && throughput > maxThroughputWithRUPM) { - this.formErrors( - `The maximum supported provisioned throughput with RU/m enabled is ${maxThroughputWithRUPM} RU/s. Please turn off RU/m to incease thoughput above ${maxThroughputWithRUPM} RU/s.` - ); - return false; - } - if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !this.throughputSpendAck()) { this.formErrors(`Please acknowledge the estimated daily spend.`); return false; diff --git a/src/Explorer/Panes/AddDatabasePane.html b/src/Explorer/Panes/AddDatabasePane.html index e93da1ec0..b96f665fd 100644 --- a/src/Explorer/Panes/AddDatabasePane.html +++ b/src/Explorer/Panes/AddDatabasePane.html @@ -114,7 +114,8 @@ maxAutoPilotThroughputSet: maxAutoPilotThroughputSet, autoPilotUsageCost: autoPilotUsageCost, canExceedMaximumValue: canExceedMaximumValue, - showAutoPilot: !isFreeTierAccount() + showAutoPilot: !isFreeTierAccount(), + freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip }">

diff --git a/src/Explorer/Panes/AddDatabasePane.test.ts b/src/Explorer/Panes/AddDatabasePane.test.ts index d5844eedf..68cb461a9 100644 --- a/src/Explorer/Panes/AddDatabasePane.test.ts +++ b/src/Explorer/Panes/AddDatabasePane.test.ts @@ -77,7 +77,7 @@ describe("Add Database Pane", () => { explorer.databaseAccount(mockFreeTierDatabaseAccount); const addDatabasePane = explorer.addDatabasePane as AddDatabasePane; expect(addDatabasePane.isFreeTierAccount()).toBe(true); - expect(addDatabasePane.upsellMessage()).toContain("With free tier discount"); + expect(addDatabasePane.upsellMessage()).toContain("With free tier"); expect(addDatabasePane.upsellAnchorUrl()).toBe(Constants.Urls.freeTierInformation); expect(addDatabasePane.upsellAnchorText()).toBe("Learn more"); }); diff --git a/src/Explorer/Panes/AddDatabasePane.ts b/src/Explorer/Panes/AddDatabasePane.ts index 8081c7cc8..5119923b0 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; @@ -43,6 +44,7 @@ export default class AddDatabasePane extends ContextualPaneBase { public autoPilotUsageCost: ko.Computed; public canExceedMaximumValue: ko.PureComputed; public ruToolTipText: ko.Computed; + public freeTierExceedThroughputTooltip: ko.Computed; public isFreeTierAccount: ko.Computed; public canConfigureThroughput: ko.PureComputed; public showUpsellMessage: ko.PureComputed; @@ -53,7 +55,6 @@ export default class AddDatabasePane extends ContextualPaneBase { this.databaseId = ko.observable(); this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText()); this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled()); - this.showUpsellMessage = ko.pureComputed(() => !this.container.isServerlessEnabled()); this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue()); @@ -133,19 +134,12 @@ export default class AddDatabasePane extends ContextualPaneBase { let estimatedSpendAcknowledge: string; let estimatedSpend: string; if (!this.isAutoPilotSelected()) { - estimatedSpend = PricingUtils.getEstimatedSpendHtml( - offerThroughput, - serverId, - regions, - multimaster, - false /*rupmEnabled*/ - ); + estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster); estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( offerThroughput, serverId, regions, multimaster, - false /*rupmEnabled*/, this.isAutoPilotSelected() ); } else { @@ -160,7 +154,6 @@ export default class AddDatabasePane extends ContextualPaneBase { serverId, regions, multimaster, - false /*rupmEnabled*/, this.isAutoPilotSelected() ); } @@ -189,6 +182,18 @@ export default class AddDatabasePane extends ContextualPaneBase { return isFreeTierAccount; }); + this.showUpsellMessage = ko.pureComputed(() => { + if (this.container.isServerlessEnabled()) { + return false; + } + + if (this.isFreeTierAccount()) { + return this.databaseCreateNewShared(); + } + + return true; + }); + this.maxThroughputRUText = ko.pureComputed(() => { return this.maxThroughputRU().toLocaleString(); }); @@ -226,8 +231,20 @@ export default class AddDatabasePane extends ContextualPaneBase { this.resetData(); }); + this.freeTierExceedThroughputTooltip = ko.pureComputed(() => + this.isFreeTierAccount() && !this.container.isFirstResourceCreated() + ? "The first 400 RU/s in this account are free. Billing will apply to any throughput beyond 400 RU/s." + : "" + ); + this.upsellMessage = ko.pureComputed(() => { - return PricingUtils.getUpsellMessage(this.container.serverId(), this.isFreeTierAccount()); + return PricingUtils.getUpsellMessage( + this.container.serverId(), + this.isFreeTierAccount(), + this.container.isFirstResourceCreated(), + this.container.defaultExperience(), + false + ); }); this.upsellMessageAriaLabel = ko.pureComputed(() => { @@ -258,7 +275,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() @@ -286,7 +303,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() }, @@ -350,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() }, @@ -374,7 +391,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 864f4c386..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; @@ -138,19 +139,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { let estimatedSpend: string; let estimatedDedicatedSpendAcknowledge: string; if (!this.isAutoPilotSelected()) { - estimatedSpend = PricingUtils.getEstimatedSpendHtml( - offerThroughput, - serverId, - regions, - multimaster, - false /*rupmEnabled*/ - ); + estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster); estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( offerThroughput, serverId, regions, multimaster, - false /*rupmEnabled*/, this.isAutoPilotSelected() ); } else { @@ -165,7 +159,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { serverId, regions, multimaster, - false /*rupmEnabled*/, this.isAutoPilotSelected() ); } @@ -190,19 +183,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { let estimatedSpend: string; let estimatedSharedSpendAcknowledge: string; if (!this.isSharedAutoPilotSelected()) { - estimatedSpend = PricingUtils.getEstimatedSpendHtml( - this.keyspaceThroughput(), - serverId, - regions, - multimaster, - false /*rupmEnabled*/ - ); + estimatedSpend = PricingUtils.getEstimatedSpendHtml(this.keyspaceThroughput(), serverId, regions, multimaster); estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( this.keyspaceThroughput(), serverId, regions, multimaster, - false /*rupmEnabled*/, this.isSharedAutoPilotSelected() ); } else { @@ -217,7 +203,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { serverId, regions, multimaster, - false /*rupmEnabled*/, this.isSharedAutoPilotSelected() ); } @@ -312,11 +297,10 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { storage: Constants.BackendDefaults.multiPartitionStorageInGb, offerThroughput: this.throughput(), partitionKey: "", - databaseId: this.keyspaceId(), - rupm: false + databaseId: this.keyspaceId() }), subscriptionType: SubscriptionType[this.container.subscriptionType()], - subscriptionQuotaId: this.container.quotaId(), + subscriptionQuotaId: userContext.quotaId, defaultsCheck: { storage: "u", throughput: this.throughput(), @@ -366,12 +350,11 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { offerThroughput: this.throughput(), partitionKey: "", databaseId: this.keyspaceId(), - rupm: false, hasDedicatedThroughput: this.dedicateTableThroughput() }), keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), subscriptionType: SubscriptionType[this.container.subscriptionType()], - subscriptionQuotaId: this.container.quotaId(), + subscriptionQuotaId: userContext.quotaId, defaultsCheck: { storage: "u", throughput: this.throughput(), @@ -413,12 +396,11 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { offerThroughput: this.throughput(), partitionKey: "", databaseId: this.keyspaceId(), - rupm: false, hasDedicatedThroughput: this.dedicateTableThroughput() }), keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), subscriptionType: SubscriptionType[this.container.subscriptionType()], - subscriptionQuotaId: this.container.quotaId(), + subscriptionQuotaId: userContext.quotaId, defaultsCheck: { storage: "u", throughput: this.throughput(), @@ -444,12 +426,11 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { offerThroughput: this.throughput(), partitionKey: "", databaseId: this.keyspaceId(), - rupm: false, hasDedicatedThroughput: this.dedicateTableThroughput() }, 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/Panes/PublishNotebookPaneAdapter.tsx b/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx index 967355251..f849b7e21 100644 --- a/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx +++ b/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx @@ -98,26 +98,21 @@ export class PublishNotebookPaneAdapter implements ReactAdapter { author: string, notebookContent: string | ImmutableNotebook, parentDomElement: HTMLElement, - isCodeOfConductEnabled: boolean, isLinkInjectionEnabled: boolean ): Promise { - if (isCodeOfConductEnabled) { - try { - const response = await this.junoClient.isCodeOfConductAccepted(); - if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { - throw new Error(`Received HTTP ${response.status} when accepting code of conduct`); - } - - this.isCodeOfConductAccepted = response.data; - } catch (error) { - handleError( - error, - "PublishNotebookPaneAdapter/isCodeOfConductAccepted", - "Failed to check if code of conduct was accepted" - ); + try { + const response = await this.junoClient.isCodeOfConductAccepted(); + if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { + throw new Error(`Received HTTP ${response.status} when accepting code of conduct`); } - } else { - this.isCodeOfConductAccepted = true; + + this.isCodeOfConductAccepted = response.data; + } catch (error) { + handleError( + error, + "PublishNotebookPaneAdapter/isCodeOfConductAccepted", + "Failed to check if code of conduct was accepted" + ); } this.name = name; diff --git a/src/Explorer/SplashScreen/SplashScreenComponent.less b/src/Explorer/SplashScreen/SplashScreenComponent.less index 38ee7fd0b..d30a52be3 100644 --- a/src/Explorer/SplashScreen/SplashScreenComponent.less +++ b/src/Explorer/SplashScreen/SplashScreenComponent.less @@ -47,7 +47,7 @@ padding: 32px 16px; display: flex; background-color: @BaseLight; - border: 1px solid #E5E5E5; + border: 1px solid #949494; box-sizing: border-box; box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); border-radius: 4px; diff --git a/src/Explorer/Tables/DataTable/TableEntityListViewModel.ts b/src/Explorer/Tables/DataTable/TableEntityListViewModel.ts index 1991d7012..668ec71db 100644 --- a/src/Explorer/Tables/DataTable/TableEntityListViewModel.ts +++ b/src/Explorer/Tables/DataTable/TableEntityListViewModel.ts @@ -421,53 +421,47 @@ export default class TableEntityListViewModel extends DataTableViewModel { * Note that this also means that we can get less entities than the requested download size in a successful call. * See Microsoft Azure API Documentation at: https://msdn.microsoft.com/en-us/library/azure/dd135718.aspx */ - private prefetchData( + private async prefetchData( tableQuery: Entities.ITableQuery, downloadSize: number, currentRetry: number = 0 - ): Q.Promise { + ): Promise { if (!this.cache.serverCallInProgress) { this.cache.serverCallInProgress = true; this.allDownloaded = false; this.lastPrefetchTime = new Date().getTime(); - var time = this.lastPrefetchTime; + const time = this.lastPrefetchTime; - var promise: Q.Promise; if (this._documentIterator && this.continuationToken) { // TODO handle Cassandra case + const response = await this._documentIterator.fetchNext(); + const entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(response?.resources); - promise = Q(this._documentIterator.fetchNext().then(response => response.resources)).then( - (documents: any[]) => { - let entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(documents); - let finalEntities: IListTableEntitiesSegmentedResult = { - Results: entities, - ContinuationToken: this._documentIterator.hasMoreResults() - }; - return Q.resolve(finalEntities); - } - ); - } else if (this.continuationToken && this.queryTablesTab.container.isPreferredApiCassandra()) { - promise = this.queryTablesTab.container.tableDataClient.queryDocuments( - this.queryTablesTab.collection, - this.cqlQuery(), - true, - this.continuationToken - ); - } else { - let query = this.sqlQuery(); - if (this.queryTablesTab.container.isPreferredApiCassandra()) { - query = this.cqlQuery(); - } - promise = this.queryTablesTab.container.tableDataClient.queryDocuments( - this.queryTablesTab.collection, - query, - true - ); + return { + Results: entities, + ContinuationToken: this._documentIterator.hasMoreResults() + }; } - return promise - .then((result: IListTableEntitiesSegmentedResult) => { + + try { + let documents: IListTableEntitiesSegmentedResult; + if (this.continuationToken && this.queryTablesTab.container.isPreferredApiCassandra()) { + documents = await this.queryTablesTab.container.tableDataClient.queryDocuments( + this.queryTablesTab.collection, + this.cqlQuery(), + true, + this.continuationToken + ); + } else { + const query = this.queryTablesTab.container.isPreferredApiCassandra() ? this.cqlQuery() : this.sqlQuery(); + documents = await this.queryTablesTab.container.tableDataClient.queryDocuments( + this.queryTablesTab.collection, + query, + true + ); + if (!this._documentIterator) { - this._documentIterator = result.iterator; + this._documentIterator = documents.iterator; } var actualDownloadSize: number = 0; @@ -478,11 +472,11 @@ export default class TableEntityListViewModel extends DataTableViewModel { return Q.resolve(null); } - var entities = result.Results; + var entities = documents.Results; actualDownloadSize = entities.length; // Queries can fetch no results and still return a continuation header. See prefetchAndRender() method. - this.continuationToken = this.isCancelled ? null : result.ContinuationToken; + this.continuationToken = this.isCancelled ? null : documents.ContinuationToken; if (!this.continuationToken) { this.allDownloaded = true; @@ -514,20 +508,22 @@ export default class TableEntityListViewModel extends DataTableViewModel { // For #2.1, set prefetch exceeds maximum retry number and end prefetch. // For #2.2, go to next round prefetch. if (this.allDownloaded || nextDownloadSize === 0) { - return Q.resolve(result); + return documents; } if (currentRetry >= TableEntityListViewModel._maximumNumberOfPrefetchRetries) { - result.ExceedMaximumRetries = true; - return Q.resolve(result); + documents.ExceedMaximumRetries = true; + return documents; } - return this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1); - }) - .catch((error: Error) => { - this.cache.serverCallInProgress = false; - return Q.reject(error); - }); + + return await this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1); + } + } catch (error) { + this.cache.serverCallInProgress = false; + throw error; + } } - return null; + + return undefined; } } diff --git a/src/Explorer/Tables/TableDataClient.ts b/src/Explorer/Tables/TableDataClient.ts index ba6ae246d..b90073ef9 100644 --- a/src/Explorer/Tables/TableDataClient.ts +++ b/src/Explorer/Tables/TableDataClient.ts @@ -4,6 +4,7 @@ import Q from "q"; import { displayTokenRenewalPromptForStatus, getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; import { AuthType } from "../../AuthType"; import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; +import { FeedOptions } from "@azure/cosmos"; import * as Constants from "../../Common/Constants"; import * as Entities from "./Entities"; import * as HeadersUtility from "../../Common/HeadersUtility"; @@ -12,9 +13,12 @@ import * as TableConstants from "./Constants"; import * as TableEntityProcessor from "./TableEntityProcessor"; import * as ViewModels from "../../Contracts/ViewModels"; import Explorer from "../Explorer"; -import { queryDocuments, deleteDocument, updateDocument, createDocument } from "../../Common/DocumentClientUtilityBase"; import { configContext } from "../../ConfigContext"; import { handleError } from "../../Common/ErrorHandlingUtils"; +import { createDocument } from "../../Common/dataAccess/createDocument"; +import { deleteDocument } from "../../Common/dataAccess/deleteDocument"; +import { queryDocuments } from "../../Common/dataAccess/queryDocuments"; +import { updateDocument } from "../../Common/dataAccess/updateDocument"; export interface CassandraTableKeys { partitionKeys: CassandraTableKey[]; @@ -38,19 +42,19 @@ export abstract class TableDataClient { collection: ViewModels.Collection, originalDocument: any, newEntity: Entities.ITableEntity - ): Q.Promise; + ): Promise; public abstract queryDocuments( collection: ViewModels.Collection, query: string, shouldNotify?: boolean, paginationToken?: string - ): Q.Promise; + ): Promise; public abstract deleteDocuments( collection: ViewModels.Collection, entitiesToDelete: Entities.ITableEntity[] - ): Q.Promise; + ): Promise; } export class TablesAPIDataClient extends TableDataClient { @@ -74,77 +78,63 @@ export class TablesAPIDataClient extends TableDataClient { return deferred.promise; } - public updateDocument( + public async updateDocument( collection: ViewModels.Collection, originalDocument: any, entity: Entities.ITableEntity - ): Q.Promise { - const deferred = Q.defer(); - - updateDocument( - collection, - originalDocument, - TableEntityProcessor.convertEntityToNewDocument(entity) - ).then( - (newDocument: any) => { - const newEntity = TableEntityProcessor.convertDocumentsToEntities([newDocument])[0]; - deferred.resolve(newEntity); - }, - reason => { - deferred.reject(reason); - } - ); - return deferred.promise; + ): Promise { + try { + const newDocument = await updateDocument( + collection, + originalDocument, + TableEntityProcessor.convertEntityToNewDocument(entity) + ); + return TableEntityProcessor.convertDocumentsToEntities([newDocument])[0]; + } catch (error) { + handleError(error, "TablesAPIDataClient/updateDocument"); + throw error; + } } - public queryDocuments( + public async queryDocuments( collection: ViewModels.Collection, query: string - ): Q.Promise { - const deferred = Q.defer(); + ): Promise { + try { + const options = { + enableCrossPartitionQuery: HeadersUtility.shouldEnableCrossPartitionKey() + } as FeedOptions; + const iterator = queryDocuments(collection.databaseId, collection.id(), query, options); + const response = await iterator.fetchNext(); + const documents = response?.resources; + const entities = TableEntityProcessor.convertDocumentsToEntities(documents); - let options: any = {}; - options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); - queryDocuments(collection.databaseId, collection.id(), query, options).then( - iterator => { - iterator - .fetchNext() - .then(response => response.resources) - .then( - (documents: any[] = []) => { - let entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(documents); - let finalEntities: Entities.IListTableEntitiesResult = { - Results: entities, - ContinuationToken: iterator.hasMoreResults(), - iterator: iterator - }; - deferred.resolve(finalEntities); - }, - reason => { - deferred.reject(reason); - } - ); - }, - reason => { - deferred.reject(reason); - } - ); - return deferred.promise; + return { + Results: entities, + ContinuationToken: iterator.hasMoreResults(), + iterator: iterator + }; + } catch (error) { + handleError(error, "TablesAPIDataClient/queryDocuments", "Query documents failed"); + throw error; + } } - public deleteDocuments(collection: ViewModels.Collection, entitiesToDelete: Entities.ITableEntity[]): Q.Promise { - let documentsToDelete: any[] = TableEntityProcessor.convertEntitiesToDocuments( + public async deleteDocuments( + collection: ViewModels.Collection, + entitiesToDelete: Entities.ITableEntity[] + ): Promise { + const documentsToDelete: any[] = TableEntityProcessor.convertEntitiesToDocuments( entitiesToDelete, collection ); - let promiseArray: Q.Promise[] = []; - documentsToDelete && - documentsToDelete.forEach(document => { + + await Promise.all( + documentsToDelete?.map(async document => { document.id = ko.observable(document.id); - let promise: Q.Promise = deleteDocument(collection, document); - promiseArray.push(promise); - }); - return Q.all(promiseArray); + await deleteDocument(collection, document); + }) + ); } } @@ -180,10 +170,7 @@ export class CassandraAPIDataClient extends TableDataClient { (data: any) => { entity[TableConstants.EntityKeyNames.RowKey] = entity[this.getCassandraPartitionKeyProperty(collection)]; entity[TableConstants.EntityKeyNames.RowKey]._ = entity[TableConstants.EntityKeyNames.RowKey]._.toString(); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Info, - `Successfully added new row to table ${collection.id()}` - ); + NotificationConsoleUtils.logConsoleInfo(`Successfully added new row to table ${collection.id()}`); deferred.resolve(entity); }, error => { @@ -197,181 +184,149 @@ export class CassandraAPIDataClient extends TableDataClient { return deferred.promise; } - public updateDocument( + public async updateDocument( collection: ViewModels.Collection, originalDocument: any, newEntity: Entities.ITableEntity - ): Q.Promise { - const notificationId = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - `Updating row ${originalDocument.RowKey._}` - ); - const deferred = Q.defer(); - let promiseArray: Q.Promise[] = []; - let query = `UPDATE ${collection.databaseId}.${collection.id()}`; - let isChange: boolean = false; - for (let property in newEntity) { - if (!originalDocument[property] || newEntity[property]._.toString() !== originalDocument[property]._.toString()) { - if (this.isStringType(newEntity[property].$)) { - query = `${query} SET ${property} = '${newEntity[property]._}',`; - } else { - query = `${query} SET ${property} = ${newEntity[property]._},`; + ): Promise { + const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Updating row ${originalDocument.RowKey._}`); + + try { + let whereSegment = " WHERE"; + let keys: CassandraTableKey[] = collection.cassandraKeys.partitionKeys.concat( + collection.cassandraKeys.clusteringKeys + ); + for (let keyIndex in keys) { + const key = keys[keyIndex].property; + const keyType = keys[keyIndex].type; + whereSegment += this.isStringType(keyType) + ? ` ${key} = '${newEntity[key]._}' AND` + : ` ${key} = ${newEntity[key]._} AND`; + } + whereSegment = whereSegment.slice(0, whereSegment.length - 4); + + let updateQuery = `UPDATE ${collection.databaseId}.${collection.id()}`; + let isPropertyUpdated = false; + for (let property in newEntity) { + if ( + !originalDocument[property] || + newEntity[property]._.toString() !== originalDocument[property]._.toString() + ) { + updateQuery += this.isStringType(newEntity[property].$) + ? ` SET ${property} = '${newEntity[property]._}',` + : ` SET ${property} = ${newEntity[property]._},`; + isPropertyUpdated = true; } - isChange = true; } - } - query = query.slice(0, query.length - 1); - let whereSegment = " WHERE"; - let keys: CassandraTableKey[] = collection.cassandraKeys.partitionKeys.concat( - collection.cassandraKeys.clusteringKeys - ); - for (let keyIndex in keys) { - const key = keys[keyIndex].property; - const keyType = keys[keyIndex].type; - if (this.isStringType(keyType)) { - whereSegment = `${whereSegment} ${key} = '${newEntity[key]._}' AND`; - } else { - whereSegment = `${whereSegment} ${key} = ${newEntity[key]._} AND`; + + if (isPropertyUpdated) { + updateQuery = updateQuery.slice(0, updateQuery.length - 1); + updateQuery += whereSegment; + await this.queryDocuments(collection, updateQuery); } - } - whereSegment = whereSegment.slice(0, whereSegment.length - 4); - query = query + whereSegment; - if (isChange) { - promiseArray.push(this.queryDocuments(collection, query)); - } - query = `DELETE `; - for (let property in originalDocument) { - if (property !== TableConstants.EntityKeyNames.RowKey && !newEntity[property] && !!originalDocument[property]) { - query = `${query} ${property},`; - } - } - if (query.length > 7) { - query = query.slice(0, query.length - 1); - query = `${query} FROM ${collection.databaseId}.${collection.id()}${whereSegment}`; - promiseArray.push(this.queryDocuments(collection, query)); - } - Q.all(promiseArray) - .then( - (data: any) => { - newEntity[TableConstants.EntityKeyNames.RowKey] = originalDocument[TableConstants.EntityKeyNames.RowKey]; - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Info, - `Successfully updated row ${newEntity.RowKey._}` - ); - deferred.resolve(newEntity); - }, - error => { - handleError(error, "UpdateRowCassandra", `Failed to update row ${newEntity.RowKey._}`); - deferred.reject(error); + + let deleteQuery = `DELETE `; + let isPropertyDeleted = false; + for (let property in originalDocument) { + if (property !== TableConstants.EntityKeyNames.RowKey && !newEntity[property] && !!originalDocument[property]) { + deleteQuery += ` ${property},`; + isPropertyDeleted = true; } - ) - .finally(() => { - NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); - }); - return deferred.promise; + } + + if (isPropertyDeleted) { + deleteQuery = deleteQuery.slice(0, deleteQuery.length - 1); + deleteQuery += ` FROM ${collection.databaseId}.${collection.id()}${whereSegment}`; + await this.queryDocuments(collection, deleteQuery); + } + + newEntity[TableConstants.EntityKeyNames.RowKey] = originalDocument[TableConstants.EntityKeyNames.RowKey]; + NotificationConsoleUtils.logConsoleInfo(`Successfully updated row ${newEntity.RowKey._}`); + return newEntity; + } catch (error) { + handleError(error, "UpdateRowCassandra", "Failed to update row ${newEntity.RowKey._}"); + throw error; + } finally { + clearMessage(); + } } - public queryDocuments( + public async queryDocuments( collection: ViewModels.Collection, query: string, shouldNotify?: boolean, paginationToken?: string - ): Q.Promise { - let notificationId: string; - if (shouldNotify) { - notificationId = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - `Querying rows for table ${collection.id()}` - ); - } - const deferred = Q.defer(); - const authType = window.authType; - const apiEndpoint: string = - authType === AuthType.EncryptedToken - ? Constants.CassandraBackend.guestQueryApi - : Constants.CassandraBackend.queryApi; - $.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, { - type: "POST", - data: { - accountName: collection && collection.container.databaseAccount && collection.container.databaseAccount().name, - cassandraEndpoint: this.trimCassandraEndpoint( - collection.container.databaseAccount().properties.cassandraEndpoint - ), - resourceId: collection.container.databaseAccount().id, - keyspaceId: collection.databaseId, - tableId: collection.id(), - query: query, - paginationToken: paginationToken - }, - beforeSend: this.setAuthorizationHeader, - error: this.handleAjaxError, - cache: false - }) - .then( - (data: any) => { - if (shouldNotify) { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Info, - `Successfully fetched ${data.result.length} rows for table ${collection.id()}` - ); - } - deferred.resolve({ - Results: data.result, - ContinuationToken: data.paginationToken - }); + ): Promise { + const clearMessage = + shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`); + try { + const authType = window.authType; + const apiEndpoint: string = + authType === AuthType.EncryptedToken + ? Constants.CassandraBackend.guestQueryApi + : Constants.CassandraBackend.queryApi; + const data: any = await $.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, { + type: "POST", + data: { + accountName: + collection && collection.container.databaseAccount && collection.container.databaseAccount().name, + cassandraEndpoint: this.trimCassandraEndpoint( + collection.container.databaseAccount().properties.cassandraEndpoint + ), + resourceId: collection.container.databaseAccount().id, + keyspaceId: collection.databaseId, + tableId: collection.id(), + query, + paginationToken }, - (error: any) => { - if (shouldNotify) { - handleError(error, "QueryDocumentsCassandra", `Failed to query rows for table ${collection.id()}`); - } - deferred.reject(error); - } - ) - .done(() => { - if (shouldNotify) { - NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); - } + beforeSend: this.setAuthorizationHeader, + error: this.handleAjaxError, + cache: false }); - return deferred.promise; + shouldNotify && + NotificationConsoleUtils.logConsoleInfo( + `Successfully fetched ${data.result.length} rows for table ${collection.id()}` + ); + return { + Results: data.result, + ContinuationToken: data.paginationToken + }; + } catch (error) { + shouldNotify && + handleError(error, "QueryDocumentsCassandra", `Failed to query rows for table ${collection.id()}`); + throw error; + } finally { + clearMessage?.(); + } } - public deleteDocuments(collection: ViewModels.Collection, entitiesToDelete: Entities.ITableEntity[]): Q.Promise { + public async deleteDocuments( + collection: ViewModels.Collection, + entitiesToDelete: Entities.ITableEntity[] + ): Promise { const query = `DELETE FROM ${collection.databaseId}.${collection.id()} WHERE `; - let promiseArray: Q.Promise[] = []; - let partitionKeyProperty = this.getCassandraPartitionKeyProperty(collection); - for (let i = 0, len = entitiesToDelete.length; i < len; i++) { - let currEntityToDelete: Entities.ITableEntity = entitiesToDelete[i]; - let currQuery = query; - let partitionKeyValue = currEntityToDelete[partitionKeyProperty]; - if (partitionKeyValue._ != null && this.isStringType(partitionKeyValue.$)) { - currQuery = `${currQuery}${partitionKeyProperty} = '${partitionKeyValue._}' AND `; - } else { - currQuery = `${currQuery}${partitionKeyProperty} = ${partitionKeyValue._} AND `; - } - currQuery = currQuery.slice(0, currQuery.length - 5); - const notificationId = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - `Deleting row ${currEntityToDelete.RowKey._}` - ); - promiseArray.push( - this.queryDocuments(collection, currQuery) - .then( - () => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Info, - `Successfully deleted row ${currEntityToDelete.RowKey._}` - ); - }, - error => { - handleError(error, "DeleteRowCassandra", `Error while deleting row ${currEntityToDelete.RowKey._}`); - } - ) - .finally(() => { - NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); - }) - ); - } - return Q.all(promiseArray); + const partitionKeyProperty = this.getCassandraPartitionKeyProperty(collection); + + await Promise.all( + entitiesToDelete.map(async (currEntityToDelete: Entities.ITableEntity) => { + const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting row ${currEntityToDelete.RowKey._}`); + const partitionKeyValue = currEntityToDelete[partitionKeyProperty]; + const currQuery = + query + this.isStringType(partitionKeyValue.$) + ? `${partitionKeyProperty} = '${partitionKeyValue._}'` + : `${partitionKeyProperty} = ${partitionKeyValue._}`; + + try { + await this.queryDocuments(collection, currQuery); + NotificationConsoleUtils.logConsoleInfo(`Successfully deleted row ${currEntityToDelete.RowKey._}`); + } catch (error) { + handleError(error, "DeleteRowCassandra", `Error while deleting row ${currEntityToDelete.RowKey._}`); + throw error; + } finally { + clearMessage(); + } + }) + ); } public createKeyspace( diff --git a/src/Explorer/Tabs/ConflictsTab.ts b/src/Explorer/Tabs/ConflictsTab.ts index bbe5370c0..abad6ea4e 100644 --- a/src/Explorer/Tabs/ConflictsTab.ts +++ b/src/Explorer/Tabs/ConflictsTab.ts @@ -16,18 +16,16 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import SaveIcon from "../../../images/save-cosmos.svg"; import DiscardIcon from "../../../images/discard.svg"; import DeleteIcon from "../../../images/delete.svg"; -import { QueryIterator, ItemDefinition, Resource, ConflictDefinition } from "@azure/cosmos"; +import { QueryIterator, Resource, ConflictDefinition, FeedOptions } from "@azure/cosmos"; import { MinimalQueryIterator } from "../../Common/IteratorUtilities"; import Explorer from "../Explorer"; -import { - queryConflicts, - deleteConflict, - deleteDocument, - createDocument, - updateDocument -} from "../../Common/DocumentClientUtilityBase"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; +import { createDocument } from "../../Common/dataAccess/createDocument"; +import { deleteDocument } from "../../Common/dataAccess/deleteDocument"; +import { updateDocument } from "../../Common/dataAccess/updateDocument"; +import { deleteConflict } from "../../Common/dataAccess/deleteConflict"; +import { queryConflicts } from "../../Common/dataAccess/queryConflicts"; export default class ConflictsTab extends TabsBase { public selectedConflictId: ko.Observable; @@ -225,25 +223,15 @@ export default class ConflictsTab extends TabsBase { }); } - public refreshDocumentsGrid(): Q.Promise { - // clear documents grid - this.conflictIds([]); - return this.createIterator() - .then( - // reset iterator - iterator => { - this._documentsIterator = iterator; - } - ) - .then( - // load documents - () => { - return this.loadNextPage(); - } - ) - .catch(error => { - window.alert(getErrorMessage(error)); - }); + public async refreshDocumentsGrid(): Promise { + try { + // clear documents grid + this.conflictIds([]); + this._documentsIterator = this.createIterator(); + await this.loadNextPage(); + } catch (error) { + window.alert(getErrorMessage(error)); + } } public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => { @@ -265,9 +253,9 @@ export default class ConflictsTab extends TabsBase { return Q(); } - public onAcceptChangesClick = (): Q.Promise => { + public onAcceptChangesClick = async (): Promise => { if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) { - return Q(); + return; } this.isExecutionError(false); @@ -285,81 +273,79 @@ export default class ConflictsTab extends TabsBase { conflictResourceId: selectedConflict.resourceId }); - let operationPromise: Q.Promise = Q(); - if (selectedConflict.operationType === Constants.ConflictOperationType.Replace) { - const documentContent = JSON.parse(this.selectedConflictContent()); + try { + if (selectedConflict.operationType === Constants.ConflictOperationType.Replace) { + const documentContent = JSON.parse(this.selectedConflictContent()); - operationPromise = updateDocument( - this.collection, - selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty]), - documentContent - ); - } + await updateDocument( + this.collection, + selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty]), + documentContent + ); + } - if (selectedConflict.operationType === Constants.ConflictOperationType.Create) { - const documentContent = JSON.parse(this.selectedConflictContent()); + if (selectedConflict.operationType === Constants.ConflictOperationType.Create) { + const documentContent = JSON.parse(this.selectedConflictContent()); - operationPromise = createDocument(this.collection, documentContent); - } + await createDocument(this.collection, documentContent); + } - if (selectedConflict.operationType === Constants.ConflictOperationType.Delete && !!this.selectedConflictContent()) { - const documentContent = JSON.parse(this.selectedConflictContent()); + if ( + selectedConflict.operationType === Constants.ConflictOperationType.Delete && + !!this.selectedConflictContent() + ) { + const documentContent = JSON.parse(this.selectedConflictContent()); - operationPromise = deleteDocument( - this.collection, - selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty]) - ); - } + await deleteDocument( + this.collection, + selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty]) + ); + } - return operationPromise - .then( - () => { - return deleteConflict(this.collection, selectedConflict).then(() => { - this.conflictIds.remove((conflictId: ConflictId) => conflictId.rid === selectedConflict.rid); - this.selectedConflictContent(""); - this.selectedConflictCurrent(""); - this.selectedConflictId(null); - this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); - TelemetryProcessor.traceSuccess( - Action.ResolveConflict, - { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - conflictResourceType: selectedConflict.resourceType, - conflictOperationType: selectedConflict.operationType, - conflictResourceId: selectedConflict.resourceId - }, - startKey - ); - }); + await deleteConflict(this.collection, selectedConflict); + this.conflictIds.remove((conflictId: ConflictId) => conflictId.rid === selectedConflict.rid); + this.selectedConflictContent(""); + this.selectedConflictCurrent(""); + this.selectedConflictId(null); + this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); + TelemetryProcessor.traceSuccess( + Action.ResolveConflict, + { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + conflictResourceType: selectedConflict.resourceType, + conflictOperationType: selectedConflict.operationType, + conflictResourceId: selectedConflict.resourceId }, - error => { - this.isExecutionError(true); - const errorMessage = getErrorMessage(error); - window.alert(errorMessage); - TelemetryProcessor.traceFailure( - Action.ResolveConflict, - { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - conflictResourceType: selectedConflict.resourceType, - conflictOperationType: selectedConflict.operationType, - conflictResourceId: selectedConflict.resourceId, - error: errorMessage, - errorStack: getErrorStack(error) - }, - startKey - ); - } - ) - .finally(() => this.isExecuting(false)); + startKey + ); + } catch (error) { + this.isExecutionError(true); + const errorMessage = getErrorMessage(error); + window.alert(errorMessage); + TelemetryProcessor.traceFailure( + Action.ResolveConflict, + { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + conflictResourceType: selectedConflict.resourceType, + conflictOperationType: selectedConflict.operationType, + conflictResourceId: selectedConflict.resourceId, + error: errorMessage, + errorStack: getErrorStack(error) + }, + startKey + ); + } finally { + this.isExecuting(false); + } }; - public onDeleteClick = (): Q.Promise => { + public onDeleteClick = async (): Promise => { this.isExecutionError(false); this.isExecuting(true); @@ -375,50 +361,48 @@ export default class ConflictsTab extends TabsBase { conflictResourceId: selectedConflict.resourceId }); - return deleteConflict(this.collection, selectedConflict) - .then( - () => { - this.conflictIds.remove((conflictId: ConflictId) => conflictId.rid === selectedConflict.rid); - this.selectedConflictContent(""); - this.selectedConflictCurrent(""); - this.selectedConflictId(null); - this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); - TelemetryProcessor.traceSuccess( - Action.DeleteConflict, - { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - conflictResourceType: selectedConflict.resourceType, - conflictOperationType: selectedConflict.operationType, - conflictResourceId: selectedConflict.resourceId - }, - startKey - ); + try { + await deleteConflict(this.collection, selectedConflict); + this.conflictIds.remove((conflictId: ConflictId) => conflictId.rid === selectedConflict.rid); + this.selectedConflictContent(""); + this.selectedConflictCurrent(""); + this.selectedConflictId(null); + this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); + TelemetryProcessor.traceSuccess( + Action.DeleteConflict, + { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + conflictResourceType: selectedConflict.resourceType, + conflictOperationType: selectedConflict.operationType, + conflictResourceId: selectedConflict.resourceId }, - error => { - this.isExecutionError(true); - const errorMessage = getErrorMessage(error); - window.alert(errorMessage); - TelemetryProcessor.traceFailure( - Action.DeleteConflict, - { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - conflictResourceType: selectedConflict.resourceType, - conflictOperationType: selectedConflict.operationType, - conflictResourceId: selectedConflict.resourceId, - error: errorMessage, - errorStack: getErrorStack(error) - }, - startKey - ); - } - ) - .finally(() => this.isExecuting(false)); + startKey + ); + } catch (error) { + this.isExecutionError(true); + const errorMessage = getErrorMessage(error); + window.alert(errorMessage); + TelemetryProcessor.traceFailure( + Action.DeleteConflict, + { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + conflictResourceType: selectedConflict.resourceType, + conflictOperationType: selectedConflict.operationType, + conflictResourceId: selectedConflict.resourceId, + error: errorMessage, + errorStack: getErrorStack(error) + }, + startKey + ); + } finally { + this.isExecuting(false); + } }; public onDiscardClick = (): Q.Promise => { @@ -445,60 +429,47 @@ export default class ConflictsTab extends TabsBase { return Q(); } - public onTabClick(): Q.Promise { - return super.onTabClick().then(() => { - this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Conflicts); - }); + public onTabClick(): void { + super.onTabClick(); + this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Conflicts); } - public onActivate(): Q.Promise { - return super.onActivate().then(() => { - if (this._documentsIterator) { - return Q.resolve(this._documentsIterator); - } + public async onActivate(): Promise { + super.onActivate(); - return this.createIterator().then( - (iterator: QueryIterator) => { - this._documentsIterator = iterator; - return this.loadNextPage(); - }, - error => { - if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { - TelemetryProcessor.traceFailure( - Action.Tab, - { - databaseAccountName: this.collection.container.databaseAccount().name, - databaseName: this.collection.databaseId, - collectionName: this.collection.id(), - defaultExperience: this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: getErrorMessage(error), - errorStack: getErrorStack(error) - }, - this.onLoadStartKey - ); - this.onLoadStartKey = null; - } + if (!this._documentsIterator) { + try { + this._documentsIterator = await this.createIterator(); + await this.loadNextPage(); + } catch (error) { + if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { + TelemetryProcessor.traceFailure( + Action.Tab, + { + databaseAccountName: this.collection.container.databaseAccount().name, + databaseName: this.collection.databaseId, + collectionName: this.collection.id(), + defaultExperience: this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + error: getErrorMessage(error), + errorStack: getErrorStack(error) + }, + this.onLoadStartKey + ); + this.onLoadStartKey = null; } - ); - }); + } + } } - public onRefreshClick(): Q.Promise { - return this.refreshDocumentsGrid().then(() => { - this.selectedConflictContent(""); - this.selectedConflictId(null); - this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); - }); - } - - public createIterator(): Q.Promise> { + public createIterator(): QueryIterator { // TODO: Conflict Feed does not allow filtering atm const query: string = undefined; - let options: any = {}; - options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); - return queryConflicts(this.collection.databaseId, this.collection.id(), query, options); + const options = { + enableCrossPartitionQuery: HeadersUtility.shouldEnableCrossPartitionKey() + }; + return queryConflicts(this.collection.databaseId, this.collection.id(), query, options as FeedOptions); } public loadNextPage(): Q.Promise { diff --git a/src/Explorer/Tabs/DatabaseSettingsTab.html b/src/Explorer/Tabs/DatabaseSettingsTab.html index d3f01198a..e18080bbb 100644 --- a/src/Explorer/Tabs/DatabaseSettingsTab.html +++ b/src/Explorer/Tabs/DatabaseSettingsTab.html @@ -23,6 +23,19 @@

Scale
+
+ Info + With free tier, you'll get the first 400 RU/s and 5 GB of storage in this account for free. To keep your + account free, keep the total RU/s across all resources in the account to 400 RU/s. + + Learn more. + +
diff --git a/src/Explorer/Tabs/DatabaseSettingsTab.ts b/src/Explorer/Tabs/DatabaseSettingsTab.ts index 26c125124..492eba648 100644 --- a/src/Explorer/Tabs/DatabaseSettingsTab.ts +++ b/src/Explorer/Tabs/DatabaseSettingsTab.ts @@ -57,6 +57,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. public canThroughputExceedMaximumValue: ko.Computed; public costsVisible: ko.Computed; public displayedError: ko.Observable; + public isFreeTierAccount: ko.Computed; public isTemplateReady: ko.Observable; public minRUAnotationVisible: ko.Computed; public minRUs: ko.Observable; @@ -82,6 +83,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. public throughputAutoPilotRadioId: string; public throughputProvisionedRadioId: string; public throughputModeRadioName: string; + public freeTierExceedThroughputWarning: ko.Computed; private _hasProvisioningTypeChanged: ko.Computed; private _wasAutopilotOriginallySet: ko.Observable; @@ -155,8 +157,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. this.overrideWithAutoPilotSettings() ? this.autoPilotThroughput() : this.throughput(), serverId, regions, - multimaster, - false /*rupmEnabled*/ + multimaster ); } else { estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( @@ -231,9 +232,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. return this.throughputTitle() + this.requestUnitsUsageCost(); }); this.pendingNotification = ko.observable(); - this._offerReplacePending = ko.observable( - !!this.database.offer()?.headers?.[Constants.HttpHeaders.offerReplacePending] - ); + this._offerReplacePending = ko.observable(!!this.database.offer()?.offerReplacePending); this.notificationStatusInfo = ko.observable(""); this.shouldShowNotificationStatusPrompt = ko.computed(() => this.notificationStatusInfo().length > 0); this.warningMessage = ko.computed(() => { @@ -242,7 +241,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. } const offer = this.database.offer(); - if (offer?.headers?.[Constants.HttpHeaders.offerReplacePending]) { + if (offer?.offerReplacePending) { const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput; return throughputApplyShortDelayMessage(this.isAutoPilotSelected(), throughput, this.database.id()); } @@ -362,6 +361,17 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. this.isTemplateReady = ko.observable(false); + this.isFreeTierAccount = ko.computed(() => { + const databaseAccount = this.container?.databaseAccount(); + return databaseAccount?.properties?.enableFreeTier; + }); + + this.freeTierExceedThroughputWarning = ko.computed(() => + this.isFreeTierAccount() + ? "Billing will apply if you provision more than 400 RU/s of manual throughput, or if the resource scales beyond 400 RU/s with autoscale." + : "" + ); + this._buildCommandBarOptions(); } @@ -432,11 +442,10 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. return Q(); }; - public onActivate(): Q.Promise { - return super.onActivate().then(async () => { - this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings); - await this.database.loadOffer(); - }); + public async onActivate(): Promise { + super.onActivate(); + this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings); + await this.database.loadOffer(); } private _setBaseline() { diff --git a/src/Explorer/Tabs/DocumentsTab.html b/src/Explorer/Tabs/DocumentsTab.html index 5a55c47ee..ffd2e05db 100644 --- a/src/Explorer/Tabs/DocumentsTab.html +++ b/src/Explorer/Tabs/DocumentsTab.html @@ -103,7 +103,7 @@
{ - return super.onTabClick().then(() => { - this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Query); - }); + public onTabClick(): void { + super.onTabClick(); + this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Query); } - public onExecuteQueryClick = (): Q.Promise => { + public onExecuteQueryClick = async (): Promise => { const sqlStatement: string = this.selectedContent() || this.sqlQueryEditorContent(); this.sqlStatementToExecute(sqlStatement); this.allResultsMetadata([]); this.queryResults(""); - this._iterator = null; + this._iterator = undefined; - return this._executeQueryDocumentsPage(0); + await this._executeQueryDocumentsPage(0); }; public onLoadQueryClick = (): void => { @@ -191,13 +191,13 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem this.collection && this.collection.container && this.collection.container.browseQueriesPane.open(); }; - public onFetchNextPageClick(): Q.Promise { + public async onFetchNextPageClick(): Promise { const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || []; const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1]; const firstResultIndex: number = (metadata && Number(metadata.firstItemIndex)) || 1; const itemCount: number = (metadata && Number(metadata.itemCount)) || 0; - return this._executeQueryDocumentsPage(firstResultIndex + itemCount - 1); + await this._executeQueryDocumentsPage(firstResultIndex + itemCount - 1); } public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => { @@ -265,19 +265,18 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem return true; }; - private _executeQueryDocumentsPage(firstItemIndex: number): Q.Promise { + private async _executeQueryDocumentsPage(firstItemIndex: number): Promise { this.error(""); this.roundTrips(undefined); - if (this._iterator == null) { - const queryIteratorPromise = this._initIterator(); - return queryIteratorPromise.finally(() => this._queryDocumentsPage(firstItemIndex)); + if (this._iterator === undefined) { + this._initIterator(); } - return this._queryDocumentsPage(firstItemIndex); + await this._queryDocumentsPage(firstItemIndex); } // TODO: Position and enable spinner when request is in progress - private _queryDocumentsPage(firstItemIndex: number): Q.Promise { + private async _queryDocumentsPage(firstItemIndex: number): Promise { this.isExecutionError(false); this._resetAggregateQueryMetrics(); const startKey: number = TelemetryProcessor.traceStart(Action.ExecuteQuery, { @@ -289,90 +288,75 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem let options: any = {}; options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); - const queryDocuments = (firstItemIndex: number) => - queryDocumentsPage(this.collection && this.collection.id(), this._iterator, firstItemIndex, options); + const queryDocuments = async (firstItemIndex: number) => + await queryDocumentsPage(this.collection && this.collection.id(), this._iterator, firstItemIndex); this.isExecuting(true); - return QueryUtils.queryPagesUntilContentPresent(firstItemIndex, queryDocuments) - .then( - (queryResults: ViewModels.QueryResults) => { - const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || []; - const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1]; - const resultsMetadata: ViewModels.QueryResultsMetadata = { - hasMoreResults: queryResults.hasMoreResults, - itemCount: queryResults.itemCount, - firstItemIndex: queryResults.firstItemIndex, - lastItemIndex: queryResults.lastItemIndex - }; - this.allResultsMetadata.push(resultsMetadata); - this.activityId(queryResults.activityId); - this.roundTrips(queryResults.roundTrips); - this._updateQueryMetricsMap(queryResults.headers[Constants.HttpHeaders.queryMetrics]); + try { + const queryResults: ViewModels.QueryResults = await QueryUtils.queryPagesUntilContentPresent( + firstItemIndex, + queryDocuments + ); + const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || []; + const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1]; + const resultsMetadata: ViewModels.QueryResultsMetadata = { + hasMoreResults: queryResults.hasMoreResults, + itemCount: queryResults.itemCount, + firstItemIndex: queryResults.firstItemIndex, + lastItemIndex: queryResults.lastItemIndex + }; + this.allResultsMetadata.push(resultsMetadata); + this.activityId(queryResults.activityId); + this.roundTrips(queryResults.roundTrips); - if (queryResults.itemCount == 0 && metadata != null && metadata.itemCount >= 0) { - // we let users query for the next page because the SDK sometimes specifies there are more elements - // even though there aren't any so we should not update the prior query results. - return; - } + this._updateQueryMetricsMap(queryResults.headers[Constants.HttpHeaders.queryMetrics]); - const documents: any[] = queryResults.documents; - const results = this.renderObjectForEditor(documents, null, 4); + if (queryResults.itemCount == 0 && metadata != null && metadata.itemCount >= 0) { + // we let users query for the next page because the SDK sometimes specifies there are more elements + // even though there aren't any so we should not update the prior query results. + return; + } - const resultsDisplay: string = - queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`; - this.showingDocumentsDisplayText(resultsDisplay); - this.requestChargeDisplayText(`${queryResults.requestCharge} RUs`); + const documents: any[] = queryResults.documents; + const results = this.renderObjectForEditor(documents, null, 4); - if (!this.queryResults() && !results) { - const errorMessage: string = JSON.stringify({ - error: `Returned no results after query execution`, - accountName: this.collection && this.collection.container.databaseAccount(), - databaseName: this.collection && this.collection.databaseId, - collectionName: this.collection && this.collection.id(), - sqlQuery: this.sqlStatementToExecute(), - hasMoreResults: resultsMetadata.hasMoreResults, - itemCount: resultsMetadata.itemCount, - responseHeaders: queryResults && queryResults.headers - }); - Logger.logError(errorMessage, "QueryTab"); - } + const resultsDisplay: string = + queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`; + this.showingDocumentsDisplayText(resultsDisplay); + this.requestChargeDisplayText(`${queryResults.requestCharge} RUs`); + this.queryResults(results); - this.queryResults(results); - - TelemetryProcessor.traceSuccess( - Action.ExecuteQuery, - { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() - }, - startKey - ); + TelemetryProcessor.traceSuccess( + Action.ExecuteQuery, + { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle() }, - (error: any) => { - this.isExecutionError(true); - const errorMessage = getErrorMessage(error); - this.error(errorMessage); - TelemetryProcessor.traceFailure( - Action.ExecuteQuery, - { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: errorMessage, - errorStack: getErrorStack(error) - }, - startKey - ); - document.getElementById("error-display").focus(); - } - ) - .finally(() => { - this.isExecuting(false); - this.togglesOnFocus(); - }); + startKey + ); + } catch (error) { + this.isExecutionError(true); + const errorMessage = getErrorMessage(error); + this.error(errorMessage); + TelemetryProcessor.traceFailure( + Action.ExecuteQuery, + { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + error: errorMessage, + errorStack: getErrorStack(error) + }, + startKey + ); + document.getElementById("error-display").focus(); + } finally { + this.isExecuting(false); + this.togglesOnFocus(); + } } private _updateQueryMetricsMap(metricsMap: { [partitionKeyRange: string]: DataModels.QueryMetrics }): void { @@ -477,16 +461,17 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem } } - protected _initIterator(): Q.Promise { + protected _initIterator(): void { const options: any = QueryTab.getIteratorOptions(this.collection); if (this._resourceTokenPartitionKey) { options.partitionKey = this._resourceTokenPartitionKey; } - return Q( - queryDocuments(this.collection.databaseId, this.collection.id(), this.sqlStatementToExecute(), options).then( - iterator => (this._iterator = iterator) - ) + this._iterator = queryDocuments( + this.collection.databaseId, + this.collection.id(), + this.sqlStatementToExecute(), + options ); } diff --git a/src/Explorer/Tabs/QueryTablesTab.ts b/src/Explorer/Tabs/QueryTablesTab.ts index 6d4b50040..c68091ab6 100644 --- a/src/Explorer/Tabs/QueryTablesTab.ts +++ b/src/Explorer/Tabs/QueryTablesTab.ts @@ -161,17 +161,16 @@ export default class QueryTablesTab extends TabsBase { return null; }; - public onActivate(): Q.Promise { - return super.onActivate().then(() => { - const columns = - !!this.tableEntityListViewModel() && - !!this.tableEntityListViewModel().table && - this.tableEntityListViewModel().table.columns; - if (!!columns) { - columns.adjust(); - $(window).resize(); - } - }); + public onActivate(): void { + super.onActivate(); + const columns = + !!this.tableEntityListViewModel() && + !!this.tableEntityListViewModel().table && + this.tableEntityListViewModel().table.columns; + if (!!columns) { + columns.adjust(); + $(window).resize(); + } } protected getTabsButtons(): CommandButtonComponentProps[] { diff --git a/src/Explorer/Tabs/ScriptTabBase.ts b/src/Explorer/Tabs/ScriptTabBase.ts index 2bcdab272..e916f1ebf 100644 --- a/src/Explorer/Tabs/ScriptTabBase.ts +++ b/src/Explorer/Tabs/ScriptTabBase.ts @@ -186,12 +186,11 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode this._setBaselines(); } - public onTabClick(): Q.Promise { - return super.onTabClick().then(() => { - if (this.isNew()) { - this.collection.selectedSubnodeKind(this.tabKind); - } - }); + public onTabClick(): void { + super.onTabClick(); + if (this.isNew()) { + this.collection.selectedSubnodeKind(this.tabKind); + } } public abstract onSaveClick: () => Promise; diff --git a/src/Explorer/Tabs/SettingsTabV2.tsx b/src/Explorer/Tabs/SettingsTabV2.tsx index 256792ed4..c5cafdbf6 100644 --- a/src/Explorer/Tabs/SettingsTabV2.tsx +++ b/src/Explorer/Tabs/SettingsTabV2.tsx @@ -42,54 +42,49 @@ export default class SettingsTabV2 extends TabsBase { }); } - public onActivate(): Q.Promise { - this.isExecuting(true); - this.currentCollection.loadOffer().then( - () => { - // passed in options and set by parent as "Settings" by default - this.tabTitle(this.currentCollection.offer() ? "Settings" : "Scale & Settings"); - this.offerRead(true); - this.options.getPendingNotification.then( - (data: DataModels.Notification) => { - this.notification = data; - this.notificationRead(true); - this.isExecuting(false); - }, - error => { - const errorMessage = getErrorMessage(error); - this.notification = undefined; - this.notificationRead(true); - this.isExecuting(false); - traceFailure( - Action.Tab, - { - databaseAccountName: this.options.collection.container.databaseAccount().name, - databaseName: this.options.collection.databaseId, - collectionName: this.options.collection.id(), - defaultExperience: this.options.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle, - error: errorMessage, - errorStack: getErrorStack(error) - }, - this.options.onLoadStartKey - ); - logConsoleError( - `Error while fetching container settings for container ${this.options.collection.id()}: ${errorMessage}` - ); - throw error; - } - ); - }, - () => { - this.offerRead(true); - this.isExecuting(false); - } - ); + public async onActivate(): Promise { + try { + this.isExecuting(true); + await this.currentCollection.loadOffer(); + // passed in options and set by parent as "Settings" by default + this.tabTitle(this.currentCollection.offer() ? "Settings" : "Scale & Settings"); - return super.onActivate().then(() => { - this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.SettingsV2); - }); + this.options.getPendingNotification.then( + (data: DataModels.Notification) => { + this.notification = data; + this.notificationRead(true); + }, + error => { + const errorMessage = getErrorMessage(error); + this.notification = undefined; + this.notificationRead(true); + traceFailure( + Action.Tab, + { + databaseAccountName: this.options.collection.container.databaseAccount().name, + databaseName: this.options.collection.databaseId, + collectionName: this.options.collection.id(), + defaultExperience: this.options.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle, + error: errorMessage, + errorStack: getErrorStack(error) + }, + this.options.onLoadStartKey + ); + logConsoleError( + `Error while fetching container settings for container ${this.options.collection.id()}: ${errorMessage}` + ); + throw error; + } + ); + } finally { + this.offerRead(true); + this.isExecuting(false); + } + + super.onActivate(); + this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.SettingsV2); } public getSettingsTabContainer(): Explorer { diff --git a/src/Explorer/Tabs/TabsBase.ts b/src/Explorer/Tabs/TabsBase.ts index a53816ba6..10072976d 100644 --- a/src/Explorer/Tabs/TabsBase.ts +++ b/src/Explorer/Tabs/TabsBase.ts @@ -94,9 +94,8 @@ export default class TabsBase extends WaitsForTemplateViewModel { }); } - public onTabClick(): Q.Promise { + public onTabClick(): void { this.getContainer().tabsManager.activateTab(this); - return Q(); } protected updateSelectedNode(): void { @@ -128,7 +127,7 @@ export default class TabsBase extends WaitsForTemplateViewModel { return this.onSpaceOrEnterKeyPress(event, () => this.onCloseTabButtonClick()); }; - public onActivate(): Q.Promise { + public onActivate(): void { this.updateSelectedNode(); if (!!this.collection) { this.collection.selectedSubnodeKind(this.tabKind); @@ -151,7 +150,6 @@ export default class TabsBase extends WaitsForTemplateViewModel { tabTitle: this.tabTitle(), tabId: this.tabId }); - return Q(); } public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => { diff --git a/src/Explorer/Tabs/TabsManager.html b/src/Explorer/Tabs/TabsManager.html index 12ff5fcf0..7372fd61e 100644 --- a/src/Explorer/Tabs/TabsManager.html +++ b/src/Explorer/Tabs/TabsManager.html @@ -142,7 +142,7 @@ - +
diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 14d0f68bb..0cc27d48d 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -8,7 +8,6 @@ import * as Constants from "../../Common/Constants"; import { readStoredProcedures } from "../../Common/dataAccess/readStoredProcedures"; import { readTriggers } from "../../Common/dataAccess/readTriggers"; import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions"; -import { createDocument } from "../../Common/DocumentClientUtilityBase"; import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer"; import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize"; import * as Logger from "../../Common/Logger"; @@ -39,6 +38,7 @@ import Explorer from "../Explorer"; import { userContext } from "../../UserContext"; import { fetchPortalNotifications } from "../../Common/PortalNotifications"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; +import { createDocument } from "../../Common/dataAccess/createDocument"; export default class Collection implements ViewModels.Collection { public nodeKind: string; @@ -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(); }); @@ -1091,8 +1091,7 @@ export default class Collection implements ViewModels.Collection { return deferred.promise; } - private _createDocumentsFromFile(fileName: string, documentContent: string): Q.Promise { - const deferred: Q.Deferred = Q.defer(); + private async _createDocumentsFromFile(fileName: string, documentContent: string): Promise { const record: UploadDetailsRecord = { fileName: fileName, numSucceeded: 0, @@ -1102,39 +1101,25 @@ export default class Collection implements ViewModels.Collection { try { const content = JSON.parse(documentContent); - const promises: Array> = []; - - const triggerCreateDocument: (documentContent: any) => Q.Promise = (documentContent: any) => { - return createDocument(this, documentContent).then( - doc => { - record.numSucceeded++; - return Q.resolve(); - }, - error => { - record.numFailed++; - record.errors = [...record.errors, getErrorMessage(error)]; - return Q.resolve(); - } - ); - }; if (Array.isArray(content)) { - for (let i = 0; i < content.length; i++) { - promises.push(triggerCreateDocument(content[i])); - } + await Promise.all( + content.map(async documentContent => { + await createDocument(this, documentContent); + record.numSucceeded++; + }) + ); } else { - promises.push(triggerCreateDocument(content)); + await createDocument(this, documentContent); + record.numSucceeded++; } - Q.all(promises).then(() => { - deferred.resolve(record); - }); - } catch (e) { + return record; + } catch (error) { record.numFailed++; - record.errors = [...record.errors, e.message]; - deferred.resolve(record); + record.errors = [...record.errors, error.message]; + return record; } - return deferred.promise; } private _getPendingThroughputSplitNotification(): Q.Promise { diff --git a/src/Explorer/Tree/ConflictId.ts b/src/Explorer/Tree/ConflictId.ts index 885f113fe..9b43019e9 100644 --- a/src/Explorer/Tree/ConflictId.ts +++ b/src/Explorer/Tree/ConflictId.ts @@ -6,7 +6,7 @@ import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; import { extractPartitionKey } from "@azure/cosmos"; import ConflictsTab from "../Tabs/ConflictsTab"; -import { readDocument } from "../../Common/DocumentClientUtilityBase"; +import { readDocument } from "../../Common/dataAccess/readDocument"; export default class ConflictId { public container: ConflictsTab; @@ -59,41 +59,42 @@ export default class ConflictId { return; } - public loadConflict(): Q.Promise { - const conflictsTab = this.container; + public async loadConflict(): Promise { this.container.selectedConflictId(this); if (this.operationType === Constants.ConflictOperationType.Create) { this.container.initDocumentEditorForCreate(this, this.content); - return Q(); + return; } this.container.loadingConflictData(true); - return readDocument(this.container.collection, this.buildDocumentIdFromConflict(this.partitionKeyValue)).then( - (currentDocumentContent: any) => { - this.container.loadingConflictData(false); - if (this.operationType === Constants.ConflictOperationType.Replace) { - this.container.initDocumentEditorForReplace(this, this.content, currentDocumentContent); - } else { - this.container.initDocumentEditorForDelete(this, currentDocumentContent); - } - }, - (reason: any) => { - this.container.loadingConflictData(false); - // Document could be deleted - if ( - reason && - reason.code === Constants.HttpStatusCodes.NotFound && - this.operationType === Constants.ConflictOperationType.Delete - ) { - this.container.initDocumentEditorForNoOp(this); - return Q(); - } + try { + const currentDocumentContent = await readDocument( + this.container.collection, + this.buildDocumentIdFromConflict(this.partitionKeyValue) + ); - return Q.reject(reason); + if (this.operationType === Constants.ConflictOperationType.Replace) { + this.container.initDocumentEditorForReplace(this, this.content, currentDocumentContent); + } else { + this.container.initDocumentEditorForDelete(this, currentDocumentContent); } - ); + } catch (error) { + // Document could be deleted + if ( + error && + error.code === Constants.HttpStatusCodes.NotFound && + this.operationType === Constants.ConflictOperationType.Delete + ) { + this.container.initDocumentEditorForNoOp(this); + return; + } + + throw error; + } finally { + this.container.loadingConflictData(false); + } } public getPartitionKeyValueAsString(): string { diff --git a/src/Explorer/Tree/DocumentId.ts b/src/Explorer/Tree/DocumentId.ts index 2e9a53bd3..2c043430c 100644 --- a/src/Explorer/Tree/DocumentId.ts +++ b/src/Explorer/Tree/DocumentId.ts @@ -65,7 +65,7 @@ export default class DocumentId { return JSON.stringify(partitionKeyValue); } - public loadDocument(): Q.Promise { - return this.container.selectDocument(this); + public async loadDocument(): Promise { + await this.container.selectDocument(this); } } diff --git a/src/Explorer/Tree/StoredProcedure.ts b/src/Explorer/Tree/StoredProcedure.ts index 1c35dfa8f..8135b6f0d 100644 --- a/src/Explorer/Tree/StoredProcedure.ts +++ b/src/Explorer/Tree/StoredProcedure.ts @@ -2,7 +2,7 @@ import { Resource, StoredProcedureDefinition } from "@azure/cosmos"; import * as ko from "knockout"; import * as Constants from "../../Common/Constants"; import { deleteStoredProcedure } from "../../Common/dataAccess/deleteStoredProcedure"; -import { executeStoredProcedure } from "../../Common/DocumentClientUtilityBase"; +import { executeStoredProcedure } from "../../Common/dataAccess/executeStoredProcedure"; import * as ViewModels from "../../Contracts/ViewModels"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; diff --git a/src/Juno/JunoClient.ts b/src/Juno/JunoClient.ts index b4724002d..bbc1954cc 100644 --- a/src/Juno/JunoClient.ts +++ b/src/Juno/JunoClient.ts @@ -178,8 +178,7 @@ export class JunoClient { return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/public`); } - // will be renamed once feature.enableCodeOfConduct flag is removed - public async fetchPublicNotebooks(): Promise> { + public async getPublicGalleryData(): Promise> { const url = `${this.getNotebooksAccountUrl()}/gallery/public`; const response = await window.fetch(url, { method: "PATCH", @@ -405,7 +404,7 @@ export class JunoClient { } public async reportAbuse(notebookId: string, abuseCategory: string, notes: string): Promise> { - const response = await window.fetch(`${this.getNotebooksUrl()}/avert/reportAbuse`, { + const response = await window.fetch(`${this.getNotebooksUrl()}/gallery/reportAbuse`, { method: "POST", body: JSON.stringify({ notebookId, 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 303217a59..7aae90063 100644 --- a/src/Shared/Constants.ts +++ b/src/Shared/Constants.ts @@ -126,7 +126,6 @@ export class OfferPricing { Standard: { StartingPrice: 24 / hoursInAMonth, // per hour PricePerRU: 0.00008, - PricePerRUPM: (10 * 2) / 1000 / hoursInAMonth, // preview price: $2 per 1000 RU/m per month -> 100 RU/s PricePerGB: 0.25 / hoursInAMonth } }, @@ -139,24 +138,18 @@ export class OfferPricing { Standard: { StartingPrice: OfferPricing.MonthlyPricing.mooncake.Standard.StartingPrice / hoursInAMonth, // per hour PricePerRU: 0.00051, - PricePerRUPM: (10 * 20) / 1000 / hoursInAMonth, // preview price: 20rmb per 1000 RU/m per month -> 100 RU/s PricePerGB: OfferPricing.MonthlyPricing.mooncake.Standard.PricePerGB / hoursInAMonth } } }; } -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; public static readonly MinRU7PartitionsTo25Partitions: number = 2500; public static readonly MinRUPerPartitionAbove25Partitions: number = 100; public static readonly MaxRUPerPartition: number = 10000; - public static readonly MaxRUPMPerPartition: number = 5000; public static readonly MinPartitionedCollectionRUs: number = 2500; public static readonly NumberOfPartitionsInFixedCollection: number = 1; @@ -231,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", @@ -267,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/Shared/PriceEstimateCalculator.ts b/src/Shared/PriceEstimateCalculator.ts index c9a134b76..d7c30d695 100644 --- a/src/Shared/PriceEstimateCalculator.ts +++ b/src/Shared/PriceEstimateCalculator.ts @@ -1,17 +1,13 @@ import * as Constants from "./Constants"; -export function computeRUUsagePrice(serverId: string, rupmEnabled: boolean, requestUnits: number): string { +export function computeRUUsagePrice(serverId: string, requestUnits: number): string { if (serverId === "mooncake") { - let ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRU, - rupmCharge = rupmEnabled ? requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRUPM : 0; - return ( - calculateEstimateNumber(ruCharge + rupmCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency - ); + let ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRU; + return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency; } - let ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU, - rupmCharge = rupmEnabled ? requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRUPM : 0; - return calculateEstimateNumber(ruCharge + rupmCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency; + let ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU; + return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency; } export function computeStorageUsagePrice(serverId: string, storageUsedRoundUpToGB: number): string { 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/Terminal/index.ts b/src/Terminal/index.ts index 79c8a8809..7b49dde8b 100644 --- a/src/Terminal/index.ts +++ b/src/Terminal/index.ts @@ -59,9 +59,8 @@ const main = async (): Promise => { const serverSettings = createServerSettings(urlVars); - const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, { - baseUrl: serverSettings.baseUrl - }); + const data = { baseUrl: serverSettings.baseUrl }; + const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data); try { if (urlVars.hasOwnProperty(TerminalQueryParams.Terminal)) { @@ -70,9 +69,9 @@ const main = async (): Promise => { throw new Error("Only terminal is supported"); } - TelemetryProcessor.traceSuccess(Action.OpenTerminal, startTime); + TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime); } catch (error) { - TelemetryProcessor.traceFailure(Action.OpenTerminal, startTime); + TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime); } }; 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/GalleryUtils.ts b/src/Utils/GalleryUtils.ts index 99dee0f07..426e235b0 100644 --- a/src/Utils/GalleryUtils.ts +++ b/src/Utils/GalleryUtils.ts @@ -10,6 +10,7 @@ import Explorer from "../Explorer/Explorer"; import { IChoiceGroupOption, IChoiceGroupProps } from "office-ui-fabric-react"; import { TextFieldProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent"; import { handleError } from "../Common/ErrorHandlingUtils"; +import { HttpStatusCodes } from "../Common/Constants"; const defaultSelectedAbuseCategory = "Other"; const abuseCategories: IChoiceGroupOption[] = [ @@ -113,7 +114,7 @@ export function reportAbuse( try { const response = await junoClient.reportAbuse(notebookId, abuseCategory, additionalDetails); - if (!response.data) { + if (response.status !== HttpStatusCodes.Accepted) { throw new Error(`Received HTTP ${response.status} when submitting report for ${data.name}`); } diff --git a/src/Utils/PricingUtils.test.ts b/src/Utils/PricingUtils.test.ts index af1cedf66..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", false, 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", false, 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, rupm disabled, 1RU, 1 region, multimaster disabled", () => { - const value = PricingUtils.computeRUUsagePriceHourly("default", false, 1, 1, false); + it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster disabled", () => { + 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, rupm disabled, 1RU, 1 region, multimaster disabled", () => { - const value = PricingUtils.computeRUUsagePriceHourly("mooncake", false, 1, 1, false); + it("should return 0.00051 for Mooncake cloud, 1RU, 1 region, multimaster disabled", () => { + 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, rupm disabled, 1RU, 2 regions, multimaster disabled", () => { - const value = PricingUtils.computeRUUsagePriceHourly("default", false, 1, 2, false); + it("should return 0.00016 for default cloud, 1RU, 2 regions, multimaster disabled", () => { + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: 2, + multimasterEnabled: false, + isAutoscale: false + }); expect(value).toBe(0.00016); }); - - it("should return 0.00008 for default cloud, rupm disabled, 1RU, 1 region, multimaster enabled", () => { - const value = PricingUtils.computeRUUsagePriceHourly("default", false, 1, 1, true); - expect(value).toBe(0.00008); + 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.00048 for default cloud, rupm disabled, 1RU, 2 region, multimaster enabled", () => { - const value = PricingUtils.computeRUUsagePriceHourly("default", false, 1, 2, true); + it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster enabled", () => { + 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({ + 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()", () => { @@ -150,18 +262,6 @@ describe("PricingUtils Tests", () => { }); }); - describe("getPricePerRuPm()", () => { - it("should return 0.000027397260273972603 for default clouds", () => { - const value = PricingUtils.getPricePerRuPm("default"); - expect(value).toBe(0.000027397260273972603); - }); - - it("should return 0.00027397260273972606 for mooncake", () => { - const value = PricingUtils.getPricePerRuPm("mooncake"); - expect(value).toBe(0.00027397260273972606); - }); - }); - describe("getRegionMultiplier()", () => { describe("without multimaster", () => { it("should return 0 for null", () => { @@ -254,103 +354,95 @@ describe("PricingUtils Tests", () => { }); describe("getEstimatedSpendHtml()", () => { - it("should return 'Estimated cost (USD): $0.000080 hourly / $0.0019 daily / $0.058 monthly (1 region, 1RU/s, $0.00008/RU)' for 1RU/s on default cloud, 1 region, with multimaster, and no rupm", () => { + it("should return 'Cost (USD): $0.000080 hourly / $0.0019 daily / $0.058 monthly (1 region, 1RU/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

' for 1RU/s on default cloud, 1 region, with multimaster", () => { const value = PricingUtils.getEstimatedSpendHtml( 1 /*RU/s*/, "default" /* cloud */, 1 /* region */, - true /* multimaster */, - false /* rupm */ + true /* multimaster */ ); expect(value).toBe( - "Estimated cost (USD): $0.000080 hourly / $0.0019 daily / $0.058 monthly (1 region, 1RU/s, $0.00008/RU)" + "Cost (USD): $0.000080 hourly / $0.0019 daily / $0.058 monthly (1 region, 1RU/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

" ); }); - it("should return 'Estimated cost (RMB): ¥0.00051 hourly / ¥0.012 daily / ¥0.37 monthly (1 region, 1RU/s, ¥0.00051/RU)' for 1RU/s on mooncake, 1 region, with multimaster, and no rupm", () => { + it("should return 'Cost (RMB): ¥0.00051 hourly / ¥0.012 daily / ¥0.37 monthly (1 region, 1RU/s, ¥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

' for 1RU/s on mooncake, 1 region, with multimaster", () => { const value = PricingUtils.getEstimatedSpendHtml( 1 /*RU/s*/, "mooncake" /* cloud */, 1 /* region */, - true /* multimaster */, - false /* rupm */ + true /* multimaster */ ); expect(value).toBe( - "Estimated cost (RMB): ¥0.00051 hourly / ¥0.012 daily / ¥0.37 monthly (1 region, 1RU/s, ¥0.00051/RU)" + "Cost (RMB): ¥0.00051 hourly / ¥0.012 daily / ¥0.37 monthly (1 region, 1RU/s, ¥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

" ); }); - it("should return 'Estimated cost (USD): $0.13 hourly / $3.07 daily / $140.16 monthly (2 regions, 400RU/s, $0.00016/RU)' for 400RU/s on default cloud, 2 region, with multimaster, and no rupm", () => { + it("should return 'Cost (USD): $0.13 hourly / $3.07 daily / $140.16 monthly (2 regions, 400RU/s, $0.00016/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

' for 400RU/s on default cloud, 2 region, with multimaster", () => { const value = PricingUtils.getEstimatedSpendHtml( 400 /*RU/s*/, "default" /* cloud */, 2 /* region */, - true /* multimaster */, - false /* rupm */ + true /* multimaster */ ); expect(value).toBe( - "Estimated cost (USD): $0.19 hourly / $4.61 daily / $140.16 monthly (2 regions, 400RU/s, $0.00016/RU)" + "Cost (USD): $0.19 hourly / $4.61 daily / $140.16 monthly (2 regions, 400RU/s, $0.00016/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

" ); }); - it("should return 'Estimated cost (USD): $0.064 hourly / $1.54 daily / $46.72 monthly (2 regions, 400RU/s, $0.00008/RU)' for 400RU/s on default cloud, 2 region, without multimaster, and no rupm", () => { + it("should return 'Cost (USD): $0.064 hourly / $1.54 daily / $46.72 monthly (2 regions, 400RU/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

' for 400RU/s on default cloud, 2 region, without multimaster", () => { const value = PricingUtils.getEstimatedSpendHtml( 400 /*RU/s*/, "default" /* cloud */, 2 /* region */, - false /* multimaster */, - false /* rupm */ + false /* multimaster */ ); expect(value).toBe( - "Estimated cost (USD): $0.064 hourly / $1.54 daily / $46.72 monthly (2 regions, 400RU/s, $0.00008/RU)" + "Cost (USD): $0.064 hourly / $1.54 daily / $46.72 monthly (2 regions, 400RU/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

" ); }); }); describe("getEstimatedSpendAcknowledgeString()", () => { - it("should return 'I acknowledge the estimated $0.0019 daily cost for the throughput above.' for 1RU/s on default cloud, 1 region, with multimaster, and no rupm", () => { + it("should return 'I acknowledge the estimated $0.0019 daily cost for the throughput above.' for 1RU/s on default cloud, 1 region, with multimaster", () => { const value = PricingUtils.getEstimatedSpendAcknowledgeString( 1 /*RU/s*/, "default" /* cloud */, 1 /* region */, true /* multimaster */, - false /* rupm */, false ); expect(value).toBe("I acknowledge the estimated $0.0019 daily cost for the throughput above."); }); - it("should return 'I acknowledge the estimated ¥0.012 daily cost for the throughput above.' for 1RU/s on mooncake, 1 region, with multimaster, and no rupm", () => { + it("should return 'I acknowledge the estimated ¥0.012 daily cost for the throughput above.' for 1RU/s on mooncake, 1 region, with multimaster", () => { const value = PricingUtils.getEstimatedSpendAcknowledgeString( 1 /*RU/s*/, "mooncake" /* cloud */, 1 /* region */, true /* multimaster */, - false /* rupm */, false ); expect(value).toBe("I acknowledge the estimated ¥0.012 daily cost for the throughput above."); }); - it("should return 'I acknowledge the estimated $3.07 daily cost for the throughput above.' for 400RU/s on default cloud, 2 region, with multimaster, and no rupm", () => { + it("should return 'I acknowledge the estimated $3.07 daily cost for the throughput above.' for 400RU/s on default cloud, 2 region, with multimaster", () => { const value = PricingUtils.getEstimatedSpendAcknowledgeString( 400 /*RU/s*/, "default" /* cloud */, 2 /* region */, true /* multimaster */, - false /* rupm */, false ); expect(value).toBe("I acknowledge the estimated $4.61 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, and no rupm", () => { + 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", () => { const value = PricingUtils.getEstimatedSpendAcknowledgeString( 400 /*RU/s*/, "default" /* cloud */, 2 /* region */, false /* multimaster */, - false /* rupm */, false ); expect(value).toBe("I acknowledge the estimated $1.54 daily cost for the throughput above."); diff --git a/src/Utils/PricingUtils.ts b/src/Utils/PricingUtils.ts index bb063d63e..686eedebc 100644 --- a/src/Utils/PricingUtils.ts +++ b/src/Utils/PricingUtils.ts @@ -1,5 +1,17 @@ import * as AutoPilotUtils from "../Utils/AutoPilotUtils"; import * as Constants from "../Shared/Constants"; +import { DefaultAccountExperienceType } from "../DefaultAccountExperienceType"; + +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 @@ -47,23 +59,19 @@ export function getMultimasterMultiplier(numberOfRegions: number, multimasterEna return multimasterMultiplier; } -export function computeRUUsagePriceHourly( - serverId: string, - rupmEnabled: boolean, - 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 pricePerRuPm = getPricePerRuPm(serverId); - + const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multimasterMultiplier) : getPricePerRu(serverId); const ruCharge = requestUnits * pricePerRu * multimasterMultiplier * regionMultiplier; - const rupmCharge = rupmEnabled ? requestUnits * pricePerRuPm : 0; - return Number((ruCharge + rupmCharge).toFixed(5)); + return Number(ruCharge.toFixed(5)); } export function getPriceCurrency(serverId: string): string { @@ -149,14 +157,6 @@ export function getPricePerRu(serverId: string): number { return Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU; } -export function getPricePerRuPm(serverId: string): number { - if (serverId === "mooncake") { - return Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRUPM; - } - - return Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRUPM; -} - export function getAutoPilotV3SpendHtml(maxAutoPilotThroughputSet: number, isDatabaseThroughput: boolean): string { if (!maxAutoPilotThroughputSet) { return ""; @@ -172,28 +172,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); @@ -214,10 +205,15 @@ export function getEstimatedSpendHtml( throughput: number, serverId: string, regions: number, - multimaster: boolean, - rupmEnabled: boolean + multimaster: boolean ): string { - const hourlyPrice: number = computeRUUsagePriceHourly(serverId, rupmEnabled, 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); @@ -225,11 +221,13 @@ export function getEstimatedSpendHtml( const pricePerRu = getPricePerRu(serverId) * getMultimasterMultiplier(regions, multimaster); return ( - `Estimated cost (${currency}): ` + + `Cost (${currency}): ` + `${currencySign}${calculateEstimateNumber(hourlyPrice)} hourly / ` + `${currencySign}${calculateEstimateNumber(dailyPrice)} daily / ` + `${currencySign}${calculateEstimateNumber(monthlyPrice)} monthly ` + - `(${regions} ${regions === 1 ? "region" : "regions"}, ${throughput}RU/s, ${currencySign}${pricePerRu}/RU)` + `(${regions} ${regions === 1 ? "region" : "regions"}, ${throughput}RU/s, ${currencySign}${pricePerRu}/RU)` + + `

` + + `${estimatedCostDisclaimer}

` ); } @@ -238,12 +236,15 @@ export function getEstimatedSpendAcknowledgeString( serverId: string, regions: number, multimaster: boolean, - rupmEnabled: boolean, isAutoscale: boolean ): string { - const hourlyPrice: number = isAutoscale - ? computeAutoscaleUsagePriceHourly(serverId, throughput, regions, multimaster) - : computeRUUsagePriceHourly(serverId, rupmEnabled, 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); @@ -256,9 +257,19 @@ export function getEstimatedSpendAcknowledgeString( )} - ${currencySign}${calculateEstimateNumber(monthlyPrice)} monthly cost for the throughput above.`; } -export function getUpsellMessage(serverId = "default", isFreeTier = false): string { +export function getUpsellMessage( + serverId = "default", + isFreeTier = false, + isFirstResourceCreated = false, + defaultExperience: string, + isCollection: boolean +): string { if (isFreeTier) { - return "With free tier discount, you'll get the first 400 RU/s and 5 GB of storage in this account for free. Charges will apply if your resource throughput exceeds 400 RU/s."; + const collectionName = getCollectionName(defaultExperience); + const resourceType = isCollection ? collectionName : "database"; + return isFirstResourceCreated + ? `The free tier discount of 400 RU/s has already been applied to a database or ${collectionName} in this account. Billing will apply to this ${resourceType} after it is created.` + : `With free tier, you'll get the first 400 RU/s and 5 GB of storage in this account for free. Billing will apply if you provision more than 400 RU/s of manual throughput, or if the ${resourceType} scales beyond 400 RU/s with autoscale.`; } else { let price: number = Constants.OfferPricing.MonthlyPricing.default.Standard.StartingPrice; @@ -269,3 +280,19 @@ export function getUpsellMessage(serverId = "default", isFreeTier = false): stri return `Start at ${getCurrencySign(serverId)}${price}/mo per database, multiple containers included`; } } + +function getCollectionName(defaultExperience: string): string { + switch (defaultExperience) { + case DefaultAccountExperienceType.DocumentDB: + return "container"; + case DefaultAccountExperienceType.MongoDB: + return "collection"; + case DefaultAccountExperienceType.Table: + case DefaultAccountExperienceType.Cassandra: + return "table"; + case DefaultAccountExperienceType.Graph: + return "graph"; + default: + throw Error("unknown API type"); + } +} diff --git a/src/Utils/QueryUtils.ts b/src/Utils/QueryUtils.ts index 1f33b7094..db374b9e7 100644 --- a/src/Utils/QueryUtils.ts +++ b/src/Utils/QueryUtils.ts @@ -58,41 +58,36 @@ export class QueryUtils { return projections.join(","); } - public static queryPagesUntilContentPresent( + public static async queryPagesUntilContentPresent( firstItemIndex: number, - queryItems: (itemIndex: number) => Q.Promise - ): Q.Promise { + queryItems: (itemIndex: number) => Promise + ): Promise { let roundTrips: number = 0; let netRequestCharge: number = 0; - const doRequest = (itemIndex: number): Q.Promise => - queryItems(itemIndex).then( - (results: ViewModels.QueryResults) => { - roundTrips = roundTrips + 1; - results.roundTrips = roundTrips; - results.requestCharge = Number(results.requestCharge) + netRequestCharge; - netRequestCharge = Number(results.requestCharge); - const resultsMetadata: ViewModels.QueryResultsMetadata = { - hasMoreResults: results.hasMoreResults, - itemCount: results.itemCount, - firstItemIndex: results.firstItemIndex, - lastItemIndex: results.lastItemIndex - }; - if (resultsMetadata.itemCount === 0 && resultsMetadata.hasMoreResults) { - return doRequest(resultsMetadata.lastItemIndex); - } - return Q.resolve(results); - }, - (error: any) => { - return Q.reject(error); - } - ); + const doRequest = async (itemIndex: number): Promise => { + const results: ViewModels.QueryResults = await queryItems(itemIndex); + roundTrips = roundTrips + 1; + results.roundTrips = roundTrips; + results.requestCharge = Number(results.requestCharge) + netRequestCharge; + netRequestCharge = Number(results.requestCharge); + const resultsMetadata: ViewModels.QueryResultsMetadata = { + hasMoreResults: results.hasMoreResults, + itemCount: results.itemCount, + firstItemIndex: results.firstItemIndex, + lastItemIndex: results.lastItemIndex + }; + if (resultsMetadata.itemCount === 0 && resultsMetadata.hasMoreResults) { + return await doRequest(resultsMetadata.lastItemIndex); + } + return results; + }; - return doRequest(firstItemIndex); + return await doRequest(firstItemIndex); } - public static queryAllPages( - queryItems: (itemIndex: number) => Q.Promise - ): Q.Promise { + public static async queryAllPages( + queryItems: (itemIndex: number) => Promise + ): Promise { const queryResults: ViewModels.QueryResults = { documents: [], activityId: undefined, @@ -103,25 +98,20 @@ export class QueryUtils { requestCharge: 0, roundTrips: 0 }; - const doRequest = (itemIndex: number): Q.Promise => - queryItems(itemIndex).then( - (results: ViewModels.QueryResults) => { - const { requestCharge, hasMoreResults, itemCount, lastItemIndex, documents } = results; - queryResults.roundTrips = queryResults.roundTrips + 1; - queryResults.requestCharge = Number(queryResults.requestCharge) + Number(requestCharge); - queryResults.hasMoreResults = hasMoreResults; - queryResults.itemCount = queryResults.itemCount + itemCount; - queryResults.lastItemIndex = lastItemIndex; - queryResults.documents = queryResults.documents.concat(documents); - if (queryResults.hasMoreResults) { - return doRequest(queryResults.lastItemIndex + 1); - } - return Q.resolve(queryResults); - }, - (error: any) => { - return Q.reject(error); - } - ); + const doRequest = async (itemIndex: number): Promise => { + const results: ViewModels.QueryResults = await queryItems(itemIndex); + const { requestCharge, hasMoreResults, itemCount, lastItemIndex, documents } = results; + queryResults.roundTrips = queryResults.roundTrips + 1; + queryResults.requestCharge = Number(queryResults.requestCharge) + Number(requestCharge); + queryResults.hasMoreResults = hasMoreResults; + queryResults.itemCount = queryResults.itemCount + itemCount; + queryResults.lastItemIndex = lastItemIndex; + queryResults.documents = queryResults.documents.concat(documents); + if (queryResults.hasMoreResults) { + return doRequest(queryResults.lastItemIndex + 1); + } + return queryResults; + }; return doRequest(0); } 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 => {