Compare commits

..

1 Commits

Author SHA1 Message Date
Sweatha Viswanathan
35b15a1e20 Container pool changes to plumb auth 2021-01-05 17:23:11 -08:00
172 changed files with 5736 additions and 8431 deletions

View File

@@ -14,6 +14,7 @@ src/Common/DataAccessUtilityBase.ts
src/Common/DeleteFeedback.ts src/Common/DeleteFeedback.ts
src/Common/DocumentClientUtilityBase.ts src/Common/DocumentClientUtilityBase.ts
src/Common/EditableUtility.ts src/Common/EditableUtility.ts
src/Common/EnvironmentUtility.ts
src/Common/HashMap.test.ts src/Common/HashMap.test.ts
src/Common/HashMap.ts src/Common/HashMap.ts
src/Common/HeadersUtility.test.ts src/Common/HeadersUtility.test.ts

View File

@@ -101,7 +101,6 @@ jobs:
PLATFORM: "Emulator" PLATFORM: "Emulator"
NODE_TLS_REJECT_UNAUTHORIZED: 0 NODE_TLS_REJECT_UNAUTHORIZED: 0
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
if: failure()
with: with:
name: screenshots name: screenshots
path: failed-* path: failed-*
@@ -160,14 +159,13 @@ jobs:
TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }} TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }}
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/hostedExplorer.html" DATA_EXPLORER_ENDPOINT: "https://localhost:1234/hostedExplorer.html"
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
if: failure()
with: with:
name: screenshots name: screenshots
path: failed-* path: failed-*
nuget: nuget:
name: Publish Nuget name: Publish Nuget
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted, accessibility] needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted]
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
@@ -191,7 +189,7 @@ jobs:
nugetmpac: nugetmpac:
name: Publish Nuget MPAC name: Publish Nuget MPAC
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted, accessibility] needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted]
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
@@ -213,28 +211,3 @@ jobs:
name: packages name: packages
with: with:
path: "*.nupkg" 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"

25
.github/workflows/runners.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
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

Binary file not shown.

View File

@@ -13,18 +13,29 @@ UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), ht
### Watch mode ### Watch mode
Run `npm start` to start the development server and automatically rebuild on changes Run `npm run watch` to start the development server and automatically rebuild on changes
### Hosted Development (https://cosmos.azure.com) ### Specifying Development Platform
- Visit: `https://localhost:1234/hostedExplorer.html` 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:
- 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. - 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.
### Emulator Development ### Emulator Development
- Start the Cosmos Emulator 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.
- Visit: https://localhost:1234/index.html
`PLATFORM=Emulator EMULATOR_ENDPOINT=https://my-vm.azure.com:8081 npm run watch`
#### Setting up a Remote Emulator #### Setting up a Remote Emulator
@@ -44,8 +55,16 @@ The Cosmos emulator currently only runs in Windows environments. You can still d
### Portal Development ### Portal Development
- Visit: https://ms.portal.azure.com/?dataExplorerSource=https%3A%2F%2Flocalhost%3A1234%2Fexplorer.html 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 may have to manually visit https://localhost:1234/explorer.html first and click through any SSL certificate warnings
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.
### Testing ### Testing
@@ -69,10 +88,6 @@ Jest and Puppeteer are used for end to end browser based tests and are contained
We generally adhere to the release strategy [documented by the Azure SDK Guidelines](https://azure.github.io/azure-sdk/policies_repobranching.html#release-branches). Most releases should happen from the master branch. If master contains commits that cannot be released, you may create a release from a `release/` or `hotfix/` branch. See linked documentation for more details. We generally adhere to the release strategy [documented by the Azure SDK Guidelines](https://azure.github.io/azure-sdk/policies_repobranching.html#release-branches). Most releases should happen from the master branch. If master contains commits that cannot be released, you may create a release from a `release/` or `hotfix/` branch. See linked documentation for more details.
### Architechture
[![](https://mermaid.ink/img/eyJjb2RlIjoiZ3JhcGggTFJcbiAgaG9zdGVkKGh0dHBzOi8vY29zbW9zLmF6dXJlLmNvbSlcbiAgcG9ydGFsKFBvcnRhbClcbiAgZW11bGF0b3IoRW11bGF0b3IpXG4gIGFhZFtBQURdXG4gIHJlc291cmNlVG9rZW5bUmVzb3VyY2UgVG9rZW5dXG4gIGNvbm5lY3Rpb25TdHJpbmdbQ29ubmVjdGlvbiBTdHJpbmddXG4gIHBvcnRhbFRva2VuW0VuY3J5cHRlZCBQb3J0YWwgVG9rZW5dXG4gIG1hc3RlcktleVtNYXN0ZXIgS2V5XVxuICBhcm1bQVJNIFJlc291cmNlIFByb3ZpZGVyXVxuICBkYXRhcGxhbmVbRGF0YSBQbGFuZV1cbiAgcHJveHlbUG9ydGFsIEFQSSBQcm94eV1cbiAgc3FsW1NRTF1cbiAgbW9uZ29bTW9uZ29dXG4gIHRhYmxlc1tUYWJsZXNdXG4gIGNhc3NhbmRyYVtDYXNzYW5kcmFdXG4gIGdyYWZbR3JhcGhdXG5cblxuICBlbXVsYXRvciAtLT4gbWFzdGVyS2V5IC0tLS0-IGRhdGFwbGFuZVxuICBwb3J0YWwgLS0-IGFhZFxuICBob3N0ZWQgLS0-IHBvcnRhbFRva2VuICYgcmVzb3VyY2VUb2tlbiAmIGNvbm5lY3Rpb25TdHJpbmcgJiBhYWRcbiAgYWFkIC0tLT4gYXJtXG4gIGFhZCAtLS0-IGRhdGFwbGFuZVxuICBhYWQgLS0tPiBwcm94eVxuICByZXNvdXJjZVRva2VuIC0tLT4gc3FsIC0tPiBkYXRhcGxhbmVcbiAgcG9ydGFsVG9rZW4gLS0tPiBwcm94eVxuICBwcm94eSAtLT4gZGF0YXBsYW5lXG4gIGNvbm5lY3Rpb25TdHJpbmcgLS0-IHNxbCAmIG1vbmdvICYgY2Fzc2FuZHJhICYgZ3JhZiAmIHRhYmxlc1xuICBzcWwgLS0-IGRhdGFwbGFuZVxuICB0YWJsZXMgLS0-IGRhdGFwbGFuZVxuICBtb25nbyAtLT4gcHJveHlcbiAgY2Fzc2FuZHJhIC0tPiBwcm94eVxuICBncmFmIC0tPiBwcm94eVxuXG5cdFx0IiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQifSwidXBkYXRlRWRpdG9yIjpmYWxzZX0)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ3JhcGggTFJcbiAgaG9zdGVkKGh0dHBzOi8vY29zbW9zLmF6dXJlLmNvbSlcbiAgcG9ydGFsKFBvcnRhbClcbiAgZW11bGF0b3IoRW11bGF0b3IpXG4gIGFhZFtBQURdXG4gIHJlc291cmNlVG9rZW5bUmVzb3VyY2UgVG9rZW5dXG4gIGNvbm5lY3Rpb25TdHJpbmdbQ29ubmVjdGlvbiBTdHJpbmddXG4gIHBvcnRhbFRva2VuW0VuY3J5cHRlZCBQb3J0YWwgVG9rZW5dXG4gIG1hc3RlcktleVtNYXN0ZXIgS2V5XVxuICBhcm1bQVJNIFJlc291cmNlIFByb3ZpZGVyXVxuICBkYXRhcGxhbmVbRGF0YSBQbGFuZV1cbiAgcHJveHlbUG9ydGFsIEFQSSBQcm94eV1cbiAgc3FsW1NRTF1cbiAgbW9uZ29bTW9uZ29dXG4gIHRhYmxlc1tUYWJsZXNdXG4gIGNhc3NhbmRyYVtDYXNzYW5kcmFdXG4gIGdyYWZbR3JhcGhdXG5cblxuICBlbXVsYXRvciAtLT4gbWFzdGVyS2V5IC0tLS0-IGRhdGFwbGFuZVxuICBwb3J0YWwgLS0-IGFhZFxuICBob3N0ZWQgLS0-IHBvcnRhbFRva2VuICYgcmVzb3VyY2VUb2tlbiAmIGNvbm5lY3Rpb25TdHJpbmcgJiBhYWRcbiAgYWFkIC0tLT4gYXJtXG4gIGFhZCAtLS0-IGRhdGFwbGFuZVxuICBhYWQgLS0tPiBwcm94eVxuICByZXNvdXJjZVRva2VuIC0tLT4gc3FsIC0tPiBkYXRhcGxhbmVcbiAgcG9ydGFsVG9rZW4gLS0tPiBwcm94eVxuICBwcm94eSAtLT4gZGF0YXBsYW5lXG4gIGNvbm5lY3Rpb25TdHJpbmcgLS0-IHNxbCAmIG1vbmdvICYgY2Fzc2FuZHJhICYgZ3JhZiAmIHRhYmxlc1xuICBzcWwgLS0-IGRhdGFwbGFuZVxuICB0YWJsZXMgLS0-IGRhdGFwbGFuZVxuICBtb25nbyAtLT4gcHJveHlcbiAgY2Fzc2FuZHJhIC0tPiBwcm94eVxuICBncmFmIC0tPiBwcm94eVxuXG5cdFx0IiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQifSwidXBkYXRlRWRpdG9yIjpmYWxzZX0)
# Contributing # Contributing
Please read the [contribution guidelines](./CONTRIBUTING.md). Please read the [contribution guidelines](./CONTRIBUTING.md).

View File

@@ -1,4 +1,3 @@
module.exports = { module.exports = {
presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-react", "@babel/preset-typescript"], presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-react", "@babel/preset-typescript"]
plugins: [["@babel/plugin-proposal-decorators", { legacy: true }]]
}; };

View File

@@ -1,7 +0,0 @@
# 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

View File

@@ -1 +0,0 @@
module.exports = {}

View File

@@ -1,11 +0,0 @@
{
"name": "canvas",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}

View File

@@ -3,8 +3,8 @@
/******************************************************************************/ /******************************************************************************/
@font-face { @font-face {
font-family: wf_segoe-ui_normal; font-family: wf_segoe-ui_normal;
src: url("../../fonts/segoe-ui/west-european/normal/latest.woff"); src: url('../../fonts/segoe-ui/west-european/normal/latest.woff');
} }
@DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; @DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
@@ -20,26 +20,26 @@
COLORS COLORS
/******************************************************************************/ /******************************************************************************/
@AccentMediumHigh: #0058ad; @AccentMediumHigh: #0058AD;
@AccentMedium: #004e87; @AccentMedium: #004E87;
@AccentHigh: #1ebaed; @AccentHigh: #1EBAED;
@AccentExtraHigh: #55b3ff; @AccentExtraHigh: #55B3FF;
@AccentLow: #edf6ff; @AccentLow: #EDF6FF;
@AccentMediumLow: #ddeefe; @AccentMediumLow: #DDEEFE;
@AccentLight: #eef7ff; @AccentLight: #EEF7FF;
@AccentExtra: #ddf0ff; @AccentExtra: #DDF0FF;
@SelectionHigh: #b91f26; @SelectionHigh: #B91F26;
@BaseLight: #ffffff; @BaseLight: #FFFFFF;
@BaseDark: #000000; @BaseDark: #000000;
@NotificationLow: #fff4ce; @NotificationLow: #FFF4CE;
@NotificationHigh: #f9e9b0; @NotificationHigh: #F9E9B0;
@Purple1: #8a2da5; @Purple1: #8A2DA5;
@Dirty: #9b4f96; @Dirty: #9b4f96;
@BaseLow: #f2f2f2; @BaseLow: #F2F2F2;
@BaseMediumLow: #e6e6e6; @BaseMediumLow: #E6E6E6;
@BaseMedium: #cccccc; @BaseMedium: #CCCCCC;
@BaseMediumHigh: #767676; @BaseMediumHigh: #767676;
@BaseHigh: #393939; @BaseHigh: #393939;
@@ -53,7 +53,7 @@
@ErrorColor: @SelectionHigh; @ErrorColor: @SelectionHigh;
@SelectionColor: #3074b0; @SelectionColor: #3074B0;
@FocusColor: #605e5c; @FocusColor: #605e5c;
@@ -80,7 +80,7 @@
@ImgWidth: 14px; @ImgWidth: 14px;
@ImgHeight: 14px; @ImgHeight: 14px;
@toggleFontWeight: 700; @toggleFontWeight:700;
//Resource Tree //Resource Tree
@TreeLineHeight: 17px; @TreeLineHeight: 17px;
@@ -144,16 +144,16 @@
/**********************************************************************************/ /**********************************************************************************/
.flex-display(@display: flex) { .flex-display(@display: flex) {
display: ~"-webkit-@{display}"; display: ~"-webkit-@{display}";
display: ~"-ms-@{display}box"; // IE10 uses -ms-flexbox display: ~"-ms-@{display}box"; // IE10 uses -ms-flexbox
display: ~"-ms-@{display}"; // IE11 display: ~"-ms-@{display}"; // IE11
display: @display; display: @display;
} }
.flex-direction(@direction: column) { .flex-direction(@direction: column) {
-webkit-flex-direction: @direction; -webkit-flex-direction: @direction;
-ms-flex-direction: @direction; -ms-flex-direction: @direction;
flex-direction: @direction; flex-direction: @direction;
} }
/************************************************************************************* /*************************************************************************************
@@ -161,31 +161,32 @@
**************************************************************************************/ **************************************************************************************/
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
.selectedRadio, .selectedRadio,
.selectedRadio:hover, .selectedRadio:hover,
.selectedRadio:active, .selectedRadio:active,
.selectedRadio.dirty, .selectedRadio.dirty,
.tab [type="radio"]:checked ~ label, .tab [type=radio]:checked ~ label,
.tab [type="radio"]:checked ~ label:hover { .tab [type=radio]:checked ~ label:hover {
-ms-high-contrast-adjust: none; -ms-high-contrast-adjust: none;
-webkit-text-fill-color: HighlightText; -webkit-text-fill-color: HighlightText;
color: HighlightText; color: HighlightText;
border-color: HighlightText; border-color: HighlightText;
background-color: Highlight; background-color: Highlight;
} }
.queryMetricsSummaryTuple { .queryMetricsSummaryTuple {
th,
td { th, td {
&:nth-child(2) {
width: @IETableDataWidth; &:nth-child(2) {
} width: @IETableDataWidth;
}
&:nth-child(3) {
width: 50%; &:nth-child(3) {
} width: 50%;
}
}
} }
}
} }
/******************************************************************************************** /********************************************************************************************
@@ -193,15 +194,15 @@
*********************************************************************************************/ *********************************************************************************************/
.hover() { .hover() {
background-color: @AccentLight; background-color: @AccentLight;
} }
.active() { .active() {
background-color: @AccentExtra; background-color: @AccentExtra;
} }
.focus() { .focus() {
outline: 1px dashed @FocusColor; outline: 1px dashed @FocusColor;
} }
/************************************************************************************************ /************************************************************************************************
@@ -211,87 +212,63 @@
@ToggleWidth: 180px; @ToggleWidth: 180px;
.toggleSwitch() { .toggleSwitch() {
max-width: 100%; max-width: 100%;
margin-bottom: @SmallSpace; margin-bottom: @SmallSpace;
padding: @SmallSpace; padding: @SmallSpace;
cursor: pointer; cursor: pointer;
color: @BaseHigh; color: @BaseHigh;
font-weight: 400; font-weight: 400;
font-size: @mediumFontSize; font-size: @mediumFontSize;
font-family: @DataExplorerFont; font-family: @DataExplorerFont;
} }
.selectedToggle() { .selectedToggle() {
border-bottom: 2px solid @BaseHigh; border-bottom: 2px solid @BaseHigh;
} }
.unselectedToggle() { .unselectedToggle() {
color: @AccentMediumHigh; color: @AccentMediumHigh;
} }
/******************************************************************************************************** /********************************************************************************************************
Common Data Explorer Icons Common Data Explorer Icons
*********************************************************************************************************/ *********************************************************************************************************/
.dataExplorerIcons() { .dataExplorerIcons() {
cursor: pointer; cursor: pointer;
width: @ImgWidth; width: @ImgWidth;
height: @ImgHeight; height: @ImgHeight;
} }
/********************************************************************************************************* /*********************************************************************************************************
Info Tooltip Info Tooltip
**********************************************************************************************************/ **********************************************************************************************************/
.infoTooltip() { .infoTooltip() {
position: relative; position: relative;
display: inline-block; display: inline-block;
} }
.tooltipText(@textColor: @BaseLight, @backgroundColor: @BaseHigh) { .tooltipText(@textColor: @BaseLight, @backgroundColor: @BaseHigh) {
visibility: hidden; visibility: hidden;
background-color: @backgroundColor; background-color: @backgroundColor;
color: @textColor; color: @textColor;
position: absolute; position: absolute;
z-index: 1; z-index: 1;
left: @MediumSpace; left: @MediumSpace;
padding: @MediumSpace; padding: @MediumSpace;
} }
.tooltipTextAfter(@color: @BaseDark) { .tooltipTextAfter(@color: @BaseDark) {
content: ""; content: "";
position: absolute; position: absolute;
right: 100%; right: 100%;
border-style: solid; border-style: solid;
border-color: transparent @color transparent transparent; border-color: transparent @color transparent transparent;
left: 0px; left: 0px;
width: 0; width: 0;
height: 0; height: 0;
border-color: @InfoPointerColor transparent; border-color: @InfoPointerColor transparent;
} }
.tooltipVisible() { .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;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -13,11 +13,6 @@
@NavMediumSpace: 10px; @NavMediumSpace: 10px;
@NavLargeSpace: 15px; @NavLargeSpace: 15px;
.skip-link {
position: fixed;
top: -200px;
}
html { html {
font-family: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; font-family: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
padding: 0px; padding: 0px;

View File

@@ -1,12 +1,20 @@
@import "./Common/Constants"; @import "./Common/Constants";
.main {
width: 100%;
float: left;
transition: all .0s ease-in-out;
-ms-transition: all 0s ease-in-out;
-webkit-transition: all 0s ease-in-out;
-moz-transition: all .0s ease-in-out;
height: 100%;
background-color: white;
border-left: 0px solid white;
}
.resourceTree { .resourceTree {
height: 100%; height: 100%;
flex: 0 0 auto; flex: 0 0 auto;
.main {
height: 100%;
}
} }
.resourceTreeScroll { .resourceTreeScroll {

1329
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,15 +8,13 @@
"@azure/cosmos": "3.9.0", "@azure/cosmos": "3.9.0",
"@azure/cosmos-language-service": "0.0.5", "@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "1.1.0", "@azure/identity": "1.1.0",
"@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/plugin-proposal-decorators": "7.12.12",
"@jupyterlab/services": "6.0.0-rc.2", "@jupyterlab/services": "6.0.0-rc.2",
"@jupyterlab/terminal": "3.0.0-rc.2", "@jupyterlab/terminal": "3.0.0-rc.2",
"@microsoft/applicationinsights-web": "2.5.9", "@microsoft/applicationinsights-web": "2.5.9",
"@nteract/commutable": "7.3.2", "@nteract/commutable": "7.3.2",
"@nteract/connected-components": "6.8.2", "@nteract/connected-components": "6.8.2",
"@nteract/core": "15.1.0", "@nteract/core": "15.1.0",
"@nteract/data-explorer": "8.0.3", "@nteract/data-explorer": "8.2.9",
"@nteract/directory-listing": "2.0.6", "@nteract/directory-listing": "2.0.6",
"@nteract/dropdown-menu": "1.0.1", "@nteract/dropdown-menu": "1.0.1",
"@nteract/editor": "10.1.2", "@nteract/editor": "10.1.2",
@@ -46,7 +44,7 @@
"applicationinsights": "1.8.0", "applicationinsights": "1.8.0",
"babel-polyfill": "6.26.0", "babel-polyfill": "6.26.0",
"bootstrap": "3.4.1", "bootstrap": "3.4.1",
"canvas": "file:./canvas", "canvas": "2.6.1",
"clean-webpack-plugin": "0.1.19", "clean-webpack-plugin": "0.1.19",
"copy-webpack-plugin": "6.0.2", "copy-webpack-plugin": "6.0.2",
"crossroads": "0.12.2", "crossroads": "0.12.2",
@@ -87,7 +85,6 @@
"react-notification-system": "0.2.17", "react-notification-system": "0.2.17",
"react-redux": "7.1.3", "react-redux": "7.1.3",
"redux": "4.0.4", "redux": "4.0.4",
"reflect-metadata": "0.1.13",
"rx-jupyter": "5.5.12", "rx-jupyter": "5.5.12",
"rxjs": "6.6.3", "rxjs": "6.6.3",
"styled-components": "4.3.2", "styled-components": "4.3.2",

View File

@@ -3,6 +3,7 @@
"offerThroughput": 400, "offerThroughput": 400,
"databaseLevelThroughput": false, "databaseLevelThroughput": false,
"collectionId": "Persons", "collectionId": "Persons",
"rupmEnabled": false,
"partitionKey": { "kind": "Hash", "paths": ["/name"] }, "partitionKey": { "kind": "Hash", "paths": ["/name"] },
"data": [ "data": [
"g.addV('person').property(id, '1').property('name', 'Eva').property('age', 44)", "g.addV('person').property(id, '1').property('name', 'Eva').property('age', 44)",

View File

@@ -108,11 +108,13 @@ export class CapabilityNames {
export class Features { export class Features {
public static readonly cosmosdb = "cosmosdb"; public static readonly cosmosdb = "cosmosdb";
public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy"; public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy";
public static readonly enableRupm = "enablerupm";
public static readonly executeSproc = "dataexplorerexecutesproc"; public static readonly executeSproc = "dataexplorerexecutesproc";
public static readonly hostedDataExplorer = "hosteddataexplorerenabled"; public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
public static readonly enableTtl = "enablettl"; public static readonly enableTtl = "enablettl";
public static readonly enableNotebooks = "enablenotebooks"; public static readonly enableNotebooks = "enablenotebooks";
public static readonly enableGalleryPublish = "enablegallerypublish"; public static readonly enableGalleryPublish = "enablegallerypublish";
public static readonly enableCodeOfConduct = "enablecodeofconduct";
public static readonly enableLinkInjection = "enablelinkinjection"; public static readonly enableLinkInjection = "enablelinkinjection";
public static readonly enableSpark = "enablespark"; public static readonly enableSpark = "enablespark";
public static readonly livyEndpoint = "livyendpoint"; public static readonly livyEndpoint = "livyendpoint";
@@ -126,15 +128,12 @@ export class Features {
public static readonly enableSchema = "enableschema"; public static readonly enableSchema = "enableschema";
public static readonly enableSDKoperations = "enablesdkoperations"; public static readonly enableSDKoperations = "enablesdkoperations";
public static readonly showMinRUSurvey = "showminrusurvey"; public static readonly showMinRUSurvey = "showminrusurvey";
public static readonly selfServeType = "selfservetype";
} }
// flight names returned from the portal are always lowercase // flight names returned from the portal are always lowercase
export class Flights { export class Flights {
public static readonly SettingsV2 = "settingsv2"; public static readonly SettingsV2 = "settingsv2";
public static readonly MongoIndexEditor = "mongoindexeditor"; public static readonly MongoIndexEditor = "mongoindexeditor";
public static readonly AutoscaleTest = "autoscaletest";
public static readonly MongoIndexing = "mongoindexing";
} }
export class AfecFeatures { export class AfecFeatures {
@@ -182,6 +181,11 @@ export class CassandraBackend {
public static readonly guestSchemaApi: string = "api/guest/cassandra/schema"; 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 { export class Queries {
public static CustomPageOption: string = "custom"; public static CustomPageOption: string = "custom";
public static UnlimitedPageOption: string = "unlimited"; public static UnlimitedPageOption: string = "unlimited";

View File

@@ -1,5 +1,5 @@
import { getCommonQueryOptions } from "./queryDocuments"; import { getCommonQueryOptions } from "./DataAccessUtilityBase";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
describe("getCommonQueryOptions", () => { describe("getCommonQueryOptions", () => {
it("builds the correct default options objects", () => { it("builds the correct default options objects", () => {

View File

@@ -0,0 +1,169 @@
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<QueryIterator<ItemDefinition & Resource>> {
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<any> {
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<any> {
// TODO remove this deferred. Kept it because of timeout code at bottom of function
const deferred = Q.defer<any>();
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<any> {
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<any> {
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<any> {
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<any> {
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<QueryIterator<ConflictDefinition & Resource>> {
const documentsIterator = client()
.database(databaseId)
.container(containerId)
.conflicts.query(query, options);
return Q(documentsIterator);
}

View File

@@ -0,0 +1,217 @@
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<QueryIterator<ItemDefinition & Resource>> {
return DataAccessUtilityBase.queryDocuments(databaseId, containerId, query, options);
}
export function queryConflicts(
databaseId: string,
containerId: string,
query: string,
options: any
): Q.Promise<QueryIterator<ConflictDefinition & Resource>> {
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<any> {
var deferred = Q.defer<any>();
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<ViewModels.QueryResults> {
var deferred = Q.defer<ViewModels.QueryResults>();
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<any> {
var deferred = Q.defer<any>();
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<any> {
var deferred = Q.defer<any>();
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<any> {
var deferred = Q.defer<any>();
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<any> {
var deferred = Q.defer<any>();
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<any> {
var deferred = Q.defer<any>();
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;
}

View File

@@ -1,10 +0,0 @@
import { DefaultAccountExperienceType } from "../DefaultAccountExperienceType";
import { userContext } from "../UserContext";
export const getEntityName = (): string => {
if (userContext.defaultExperience === DefaultAccountExperienceType.MongoDB) {
return "document";
}
return "item";
};

View File

@@ -1,6 +1,8 @@
export function normalizeArmEndpoint(uri: string): string { export default class EnvironmentUtility {
if (uri && uri.slice(-1) !== "/") { public static normalizeArmEndpointUri(uri: string): string {
return `${uri}/`; if (uri && uri.slice(-1) !== "/") {
return `${uri}/`;
}
return uri;
} }
return uri;
} }

View File

@@ -21,7 +21,7 @@ export const handleError = (error: string | ARMError | Error, area: string, cons
sendNotificationForError(errorMessage, errorCode); sendNotificationForError(errorMessage, errorCode);
}; };
export const getErrorMessage = (error: string | Error = ""): string => { export const getErrorMessage = (error: string | Error): string => {
const errorMessage = typeof error === "string" ? error : error.message; const errorMessage = typeof error === "string" ? error : error.message;
return replaceKnownError(errorMessage); return replaceKnownError(errorMessage);
}; };
@@ -45,10 +45,10 @@ const sendNotificationForError = (errorMessage: string, errorCode: number | stri
const replaceKnownError = (errorMessage: string): string => { const replaceKnownError = (errorMessage: string): string => {
if ( if (
window.dataExplorer?.subscriptionType() === SubscriptionType.Internal && window.dataExplorer?.subscriptionType() === SubscriptionType.Internal &&
errorMessage?.indexOf("SharedOffer is Disabled for your account") >= 0 errorMessage.indexOf("SharedOffer is Disabled for your account") >= 0
) { ) {
return "Database throughput is not supported for internal subscriptions."; return "Database throughput is not supported for internal subscriptions.";
} else if (errorMessage?.indexOf("Partition key paths must contain only valid") >= 0) { } else if (errorMessage.indexOf("Partition key paths must contain only valid") >= 0) {
return "Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character."; return "Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character.";
} }

View File

@@ -24,8 +24,7 @@ describe("parseSDKOfferResponse", () => {
autoscaleMaxThroughput: undefined, autoscaleMaxThroughput: undefined,
minimumThroughput: 400, minimumThroughput: 400,
id: "test", id: "test",
offerDefinition: mockOfferDefinition, offerDefinition: mockOfferDefinition
offerReplacePending: false
}; };
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult); expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
@@ -55,8 +54,7 @@ describe("parseSDKOfferResponse", () => {
autoscaleMaxThroughput: 5000, autoscaleMaxThroughput: 5000,
minimumThroughput: 400, minimumThroughput: 400,
id: "test", id: "test",
offerDefinition: mockOfferDefinition, offerDefinition: mockOfferDefinition
offerReplacePending: false
}; };
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult); expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);

View File

@@ -1,12 +1,8 @@
import { Offer, SDKOfferDefinition } from "../Contracts/DataModels"; import { Offer, SDKOfferDefinition } from "../Contracts/DataModels";
import { OfferResponse } from "@azure/cosmos"; import { OfferResponse } from "@azure/cosmos";
import { HttpHeaders } from "./Constants";
export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer | undefined => { export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => {
const offerDefinition: SDKOfferDefinition | undefined = offerResponse?.resource; const offerDefinition: SDKOfferDefinition = offerResponse?.resource;
if (!offerDefinition) {
return undefined;
}
const offerContent = offerDefinition.content; const offerContent = offerDefinition.content;
if (!offerContent) { if (!offerContent) {
return undefined; return undefined;
@@ -15,14 +11,14 @@ export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer | und
const minimumThroughput = offerContent.collectionThroughputInfo?.minimumRUForCollection; const minimumThroughput = offerContent.collectionThroughputInfo?.minimumRUForCollection;
const autopilotSettings = offerContent.offerAutopilotSettings; const autopilotSettings = offerContent.offerAutopilotSettings;
if (autopilotSettings && autopilotSettings.maxThroughput && minimumThroughput) { if (autopilotSettings) {
return { return {
id: offerDefinition.id, id: offerDefinition.id,
autoscaleMaxThroughput: autopilotSettings.maxThroughput, autoscaleMaxThroughput: autopilotSettings.maxThroughput,
manualThroughput: undefined, manualThroughput: undefined,
minimumThroughput, minimumThroughput,
offerDefinition, offerDefinition,
offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true" headers: offerResponse.headers
}; };
} }
@@ -32,6 +28,6 @@ export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer | und
manualThroughput: offerContent.offerThroughput, manualThroughput: offerContent.offerThroughput,
minimumThroughput, minimumThroughput,
offerDefinition, offerDefinition,
offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true" headers: offerResponse.headers
}; };
}; };

View File

@@ -3,18 +3,16 @@ import * as _ from "underscore";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import Explorer from "../Explorer/Explorer"; import Explorer from "../Explorer/Explorer";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import DocumentsTab from "../Explorer/Tabs/DocumentsTab"; import DocumentsTab from "../Explorer/Tabs/DocumentsTab";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { QueryUtils } from "../Utils/QueryUtils"; import { QueryUtils } from "../Utils/QueryUtils";
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants"; import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage"; import { createDocument, deleteDocument, queryDocuments, queryDocumentsPage } from "./DocumentClientUtilityBase";
import { createCollection } from "./dataAccess/createCollection"; import { createCollection } from "./dataAccess/createCollection";
import { handleError } from "./ErrorHandlingUtils"; import { handleError } from "./ErrorHandlingUtils";
import { createDocument } from "./dataAccess/createDocument";
import { deleteDocument } from "./dataAccess/deleteDocument";
import { queryDocuments } from "./dataAccess/queryDocuments";
export class QueriesClient { export class QueriesClient {
private static readonly PartitionKey: DataModels.PartitionKey = { private static readonly PartitionKey: DataModels.PartitionKey = {
@@ -33,7 +31,10 @@ export class QueriesClient {
return Promise.resolve(queriesCollection.rawDataModel); return Promise.resolve(queriesCollection.rawDataModel);
} }
const clearMessage = NotificationConsoleUtils.logConsoleProgress("Setting up account for saving queries"); const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
"Setting up account for saving queries"
);
return createCollection({ return createCollection({
collectionId: SavedQueries.CollectionName, collectionId: SavedQueries.CollectionName,
createNewDatabase: true, createNewDatabase: true,
@@ -44,7 +45,10 @@ export class QueriesClient {
}) })
.then( .then(
(collection: DataModels.Collection) => { (collection: DataModels.Collection) => {
NotificationConsoleUtils.logConsoleInfo("Successfully set up account for saving queries"); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
"Successfully set up account for saving queries"
);
return Promise.resolve(collection); return Promise.resolve(collection);
}, },
(error: any) => { (error: any) => {
@@ -52,14 +56,17 @@ export class QueriesClient {
return Promise.reject(error); return Promise.reject(error);
} }
) )
.finally(() => clearMessage()); .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
} }
public async saveQuery(query: DataModels.Query): Promise<void> { public async saveQuery(query: DataModels.Query): Promise<void> {
const queriesCollection = this.findQueriesCollection(); const queriesCollection = this.findQueriesCollection();
if (!queriesCollection) { if (!queriesCollection) {
const errorMessage: string = "Account not set up to perform saved query operations"; const errorMessage: string = "Account not set up to perform saved query operations";
NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to save query ${query.queryName}: ${errorMessage}`
);
return Promise.reject(errorMessage); return Promise.reject(errorMessage);
} }
@@ -67,16 +74,25 @@ export class QueriesClient {
this.validateQuery(query); this.validateQuery(query);
} catch (error) { } catch (error) {
const errorMessage: string = "Invalid query specified"; const errorMessage: string = "Invalid query specified";
NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to save query ${query.queryName}: ${errorMessage}`
);
return Promise.reject(errorMessage); return Promise.reject(errorMessage);
} }
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Saving query ${query.queryName}`); const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Saving query ${query.queryName}`
);
query.id = query.queryName; query.id = query.queryName;
return createDocument(queriesCollection, query) return createDocument(queriesCollection, query)
.then( .then(
(savedQuery: DataModels.Query) => { (savedQuery: DataModels.Query) => {
NotificationConsoleUtils.logConsoleInfo(`Successfully saved query ${query.queryName}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully saved query ${query.queryName}`
);
return Promise.resolve(); return Promise.resolve();
}, },
(error: any) => { (error: any) => {
@@ -87,65 +103,74 @@ export class QueriesClient {
return Promise.reject(error); return Promise.reject(error);
} }
) )
.finally(() => clearMessage()); .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
} }
public async getQueries(): Promise<DataModels.Query[]> { public async getQueries(): Promise<DataModels.Query[]> {
const queriesCollection = this.findQueriesCollection(); const queriesCollection = this.findQueriesCollection();
if (!queriesCollection) { if (!queriesCollection) {
const errorMessage: string = "Account not set up to perform saved query operations"; const errorMessage: string = "Account not set up to perform saved query operations";
NotificationConsoleUtils.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to fetch saved queries: ${errorMessage}`
);
return Promise.reject(errorMessage); return Promise.reject(errorMessage);
} }
const options: any = { enableCrossPartitionQuery: true }; const options: any = { enableCrossPartitionQuery: true };
const clearMessage = NotificationConsoleUtils.logConsoleProgress("Fetching saved queries"); const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Fetching saved queries");
const queryIterator: QueryIterator<ItemDefinition & Resource> = queryDocuments( return queryDocuments(SavedQueries.DatabaseName, SavedQueries.CollectionName, this.fetchQueriesQuery(), options)
SavedQueries.DatabaseName,
SavedQueries.CollectionName,
this.fetchQueriesQuery(),
options
);
const fetchQueries = async (firstItemIndex: number): Promise<ViewModels.QueryResults> =>
await queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex);
return QueryUtils.queryAllPages(fetchQueries)
.then( .then(
(results: ViewModels.QueryResults) => { (queryIterator: QueryIterator<ItemDefinition & Resource>) => {
let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => { const fetchQueries = (firstItemIndex: number): Q.Promise<ViewModels.QueryResults> =>
if (!document) { queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex, options);
return undefined; 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);
} }
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) => { (error: any) => {
// should never get into this state but we handle this regardless
handleError(error, "getSavedQueries", "Failed to fetch saved queries"); handleError(error, "getSavedQueries", "Failed to fetch saved queries");
return Promise.reject(error); return Promise.reject(error);
} }
) )
.finally(() => clearMessage()); .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
} }
public async deleteQuery(query: DataModels.Query): Promise<void> { public async deleteQuery(query: DataModels.Query): Promise<void> {
const queriesCollection = this.findQueriesCollection(); const queriesCollection = this.findQueriesCollection();
if (!queriesCollection) { if (!queriesCollection) {
const errorMessage: string = "Account not set up to perform saved query operations"; const errorMessage: string = "Account not set up to perform saved query operations";
NotificationConsoleUtils.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to fetch saved queries: ${errorMessage}`
);
return Promise.reject(errorMessage); return Promise.reject(errorMessage);
} }
@@ -153,10 +178,16 @@ export class QueriesClient {
this.validateQuery(query); this.validateQuery(query);
} catch (error) { } catch (error) {
const errorMessage: string = "Invalid query specified"; const errorMessage: string = "Invalid query specified";
NotificationConsoleUtils.logConsoleError(`Failed to delete query ${query.queryName}: ${errorMessage}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to delete query ${query.queryName}: ${errorMessage}`
);
} }
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting query ${query.queryName}`); const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Deleting query ${query.queryName}`
);
query.id = query.queryName; query.id = query.queryName;
const documentId = new DocumentId( const documentId = new DocumentId(
{ {
@@ -170,7 +201,10 @@ export class QueriesClient {
return deleteDocument(queriesCollection, documentId) return deleteDocument(queriesCollection, documentId)
.then( .then(
() => { () => {
NotificationConsoleUtils.logConsoleInfo(`Successfully deleted query ${query.queryName}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully deleted query ${query.queryName}`
);
return Promise.resolve(); return Promise.resolve();
}, },
(error: any) => { (error: any) => {
@@ -178,7 +212,7 @@ export class QueriesClient {
return Promise.reject(error); return Promise.reject(error);
} }
) )
.finally(() => clearMessage()); .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
} }
public getResourceId(): string { public getResourceId(): string {

View File

@@ -23,10 +23,10 @@ export class Splitter {
public splitterId: string; public splitterId: string;
public leftSideId: string; public leftSideId: string;
public splitter!: HTMLElement; public splitter: HTMLElement;
public leftSide!: HTMLElement; public leftSide: HTMLElement;
public lastX!: number; public lastX: number;
public lastWidth!: number; public lastWidth: number;
private isCollapsed: ko.Observable<boolean>; private isCollapsed: ko.Observable<boolean>;
private bounds: SplitterBounds; private bounds: SplitterBounds;
@@ -42,10 +42,9 @@ export class Splitter {
} }
public initialize() { public initialize() {
if (document.getElementById(this.splitterId) !== null && document.getElementById(this.leftSideId) != null) { this.splitter = document.getElementById(this.splitterId);
this.splitter = <HTMLElement>document.getElementById(this.splitterId); this.leftSide = document.getElementById(this.leftSideId);
this.leftSide = <HTMLElement>document.getElementById(this.leftSideId);
}
const isVerticalSplitter: boolean = this.direction === SplitterDirection.Vertical; const isVerticalSplitter: boolean = this.direction === SplitterDirection.Vertical;
const splitterOptions: JQueryUI.ResizableOptions = { const splitterOptions: JQueryUI.ResizableOptions = {
animate: true, animate: true,

View File

@@ -1,5 +1,6 @@
jest.mock("../../Utils/arm/request"); jest.mock("../../Utils/arm/request");
jest.mock("../CosmosClient"); jest.mock("../CosmosClient");
jest.mock("../DataAccessUtilityBase");
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels"; import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";

View File

@@ -1,25 +0,0 @@
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<unknown> => {
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();
}
};

View File

@@ -1,36 +0,0 @@
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<void> => {
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];
};

View File

@@ -1,25 +0,0 @@
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<void> => {
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();
}
};

View File

@@ -1,48 +0,0 @@
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<ExecuteSprocResult> => {
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();
}
};

View File

@@ -1,92 +0,0 @@
import { AuthType } from "../../AuthType";
import { armRequest } from "../../Utils/arm/request";
import { configContext } from "../../ConfigContext";
import { handleError } from "../ErrorHandlingUtils";
import { userContext } from "../../UserContext";
interface TimeSeriesData {
data: {
timeStamp: string;
total: number;
}[];
metadatavalues: {
name: {
localizedValue: string;
value: string;
};
value: string;
};
}
interface MetricsData {
displayDescription: string;
errorCode: string;
id: string;
name: {
value: string;
localizedValue: string;
};
timeseries: TimeSeriesData[];
type: string;
unit: string;
}
interface MetricsResponse {
cost: number;
interval: string;
namespace: string;
resourceregion: string;
timespan: string;
value: MetricsData[];
}
export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => {
if (window.authType !== AuthType.AAD) {
return undefined;
}
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const filter = `DatabaseName eq '${databaseName}' and CollectionName eq '${containerName}'`;
const metricNames = "DataUsage,IndexUsage";
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/providers/microsoft.insights/metrics`;
try {
const metricsResponse: MetricsResponse = await armRequest({
host: configContext.ARM_ENDPOINT,
path,
method: "GET",
apiVersion: "2018-01-01",
queryParams: {
filter,
metricNames
}
});
if (metricsResponse?.value?.length !== 2) {
return undefined;
}
const dataUsageData: MetricsData = metricsResponse.value[0];
const indexUsagedata: MetricsData = metricsResponse.value[1];
const dataUsageSizeInKb: number = getUsageSizeInKb(dataUsageData);
const indexUsageSizeInKb: number = getUsageSizeInKb(indexUsagedata);
return dataUsageSizeInKb + indexUsageSizeInKb;
} catch (error) {
handleError(error, "getCollectionUsageSize");
throw error;
}
};
const getUsageSizeInKb = (metricsData: MetricsData): number => {
if (metricsData?.errorCode !== "Success") {
throw Error(`Get collection usage size failed: ${metricsData.errorCode}`);
}
const timeSeriesData: TimeSeriesData = metricsData?.timeseries?.[0];
const usageSizeInBytes: number = timeSeriesData?.data?.[0]?.total;
return usageSizeInBytes ? usageSizeInBytes / 1024 : 0;
};

View File

@@ -1,14 +0,0 @@
import { ConflictDefinition, FeedOptions, QueryIterator, Resource } from "@azure/cosmos";
import { client } from "../CosmosClient";
export const queryConflicts = (
databaseId: string,
containerId: string,
query: string,
options: FeedOptions
): QueryIterator<ConflictDefinition & Resource> => {
return client()
.database(databaseId)
.container(containerId)
.conflicts.query(query, options);
};

View File

@@ -1,34 +0,0 @@
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<ItemDefinition & Resource> => {
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;
};

View File

@@ -1,26 +0,0 @@
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<QueryResults> => {
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();
}
};

View File

@@ -105,8 +105,7 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
id: offerId, id: offerId,
autoscaleMaxThroughput: autoscaleSettings.maxThroughput, autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
manualThroughput: undefined, manualThroughput: undefined,
minimumThroughput, minimumThroughput
offerReplacePending: resource.offerReplacePending === "true"
}; };
} }
@@ -114,8 +113,7 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
id: offerId, id: offerId,
autoscaleMaxThroughput: undefined, autoscaleMaxThroughput: undefined,
manualThroughput: resource.throughput, manualThroughput: resource.throughput,
minimumThroughput, minimumThroughput
offerReplacePending: resource.offerReplacePending === "true"
}; };
} }

View File

@@ -0,0 +1,45 @@
import * as DataModels from "../../Contracts/DataModels";
import * as HeadersUtility from "../HeadersUtility";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContainerDefinition, Resource } from "@azure/cosmos";
import { HttpHeaders } from "../Constants";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
interface ResourceWithStatistics {
statistics: DataModels.Statistic[];
}
export const readCollectionQuotaInfo = async (
collection: ViewModels.Collection
): Promise<DataModels.CollectionQuotaInfo> => {
const clearMessage = logConsoleProgress(`Querying containers for database ${collection.id}`);
const options: RequestOptions = {};
options.populateQuotaInfo = true;
options.initialHeaders = options.initialHeaders || {};
options.initialHeaders[HttpHeaders.populatePartitionStatistics] = true;
try {
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.read(options);
const quota: DataModels.CollectionQuotaInfo = HeadersUtility.getQuota(response.headers);
const resource = response.resource as ContainerDefinition & Resource & ResourceWithStatistics;
quota["usageSizeInKB"] = resource.statistics.reduce(
(previousValue: number, currentValue: DataModels.Statistic) => previousValue + currentValue.sizeInKB,
0
);
quota["numPartitions"] = resource.statistics.length;
quota["uniqueKeyPolicy"] = collection.uniqueKeyPolicy; // TODO: Remove after refactoring (#119617)
return quota;
} catch (error) {
handleError(error, "ReadCollectionQuotaInfo", `Error while querying quota info for container ${collection.id}`);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -77,8 +77,7 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
id: offerId, id: offerId,
autoscaleMaxThroughput: autoscaleSettings.maxThroughput, autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
manualThroughput: undefined, manualThroughput: undefined,
minimumThroughput, minimumThroughput
offerReplacePending: resource.offerReplacePending === "true"
}; };
} }
@@ -86,8 +85,7 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
id: offerId, id: offerId,
autoscaleMaxThroughput: undefined, autoscaleMaxThroughput: undefined,
manualThroughput: resource.throughput, manualThroughput: resource.throughput,
minimumThroughput, minimumThroughput
offerReplacePending: resource.offerReplacePending === "true"
}; };
} }

View File

@@ -1,27 +0,0 @@
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<Item> => {
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();
}
};

View File

@@ -1,32 +0,0 @@
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<Item> => {
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();
}
};

View File

@@ -210,11 +210,11 @@ export interface QueryMetrics {
export interface Offer { export interface Offer {
id: string; id: string;
autoscaleMaxThroughput: number | undefined; autoscaleMaxThroughput: number;
manualThroughput: number | undefined; manualThroughput: number;
minimumThroughput: number | undefined; minimumThroughput: number;
offerDefinition?: SDKOfferDefinition; offerDefinition?: SDKOfferDefinition;
offerReplacePending: boolean; headers?: any;
} }
export interface SDKOfferDefinition extends Resource { export interface SDKOfferDefinition extends Resource {
@@ -230,6 +230,18 @@ export interface SDKOfferDefinition extends Resource {
offerResourceId?: string; offerResourceId?: string;
} }
export interface CollectionQuotaInfo {
storedProcedures: number;
triggers: number;
functions: number;
documentsSize: number;
collectionSize: number;
documentsCount: number;
usageSizeInKB: number;
numPartitions: number;
uniqueKeyPolicy?: UniqueKeyPolicy; // TODO: This should ideally not be a part of the collection quota. Remove after refactoring. (#119617)
}
export interface OfferThroughputInfo { export interface OfferThroughputInfo {
minimumRUForCollection: number; minimumRUForCollection: number;
numPhysicalPartitions: number; numPhysicalPartitions: number;
@@ -248,6 +260,7 @@ export interface CreateDatabaseAndCollectionRequest {
collectionId: string; collectionId: string;
offerThroughput: number; offerThroughput: number;
databaseLevelThroughput: boolean; databaseLevelThroughput: boolean;
rupmEnabled?: boolean;
partitionKey?: PartitionKey; partitionKey?: PartitionKey;
indexingPolicy?: IndexingPolicy; indexingPolicy?: IndexingPolicy;
uniqueKeyPolicy?: UniqueKeyPolicy; uniqueKeyPolicy?: UniqueKeyPolicy;

View File

@@ -15,7 +15,6 @@ import DocumentId from "../Explorer/Tree/DocumentId";
import StoredProcedure from "../Explorer/Tree/StoredProcedure"; import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import Trigger from "../Explorer/Tree/Trigger"; import Trigger from "../Explorer/Tree/Trigger";
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction"; import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
import { SelfServeType } from "../SelfServe/SelfServeUtils";
import { UploadDetails } from "../workers/upload/definitions"; import { UploadDetails } from "../workers/upload/definitions";
import * as DataModels from "./DataModels"; import * as DataModels from "./DataModels";
import { SubscriptionType } from "./SubscriptionType"; import { SubscriptionType } from "./SubscriptionType";
@@ -121,7 +120,7 @@ export interface Collection extends CollectionBase {
requestSchema?: () => void; requestSchema?: () => void;
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>; indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
uniqueKeyPolicy: DataModels.UniqueKeyPolicy; uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
usageSizeInKB: ko.Observable<number>; quotaInfo: ko.Observable<DataModels.CollectionQuotaInfo>;
offer: ko.Observable<DataModels.Offer>; offer: ko.Observable<DataModels.Offer>;
conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>; conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>; changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
@@ -363,7 +362,7 @@ export enum CollectionTabKind {
Gallery = 17, Gallery = 17,
NotebookViewer = 18, NotebookViewer = 18,
Schema = 19, Schema = 19,
SettingsV2 = 20 SettingsV2 = 19
} }
export enum TerminalKind { export enum TerminalKind {
@@ -396,7 +395,6 @@ export interface DataExplorerInputsFrame {
isAuthWithresourceToken?: boolean; isAuthWithresourceToken?: boolean;
defaultCollectionThroughput?: CollectionCreationDefaults; defaultCollectionThroughput?: CollectionCreationDefaults;
flights?: readonly string[]; flights?: readonly string[];
selfServeType?: SelfServeType;
} }
export interface CollectionCreationDefaults { export interface CollectionCreationDefaults {

View File

@@ -42,7 +42,7 @@ interface CollapsiblePanelParams {
* Use the optional "collapseToLeft" parameter to collapse to the left. * Use the optional "collapseToLeft" parameter to collapse to the left.
*/ */
class CollapsiblePanelViewModel { class CollapsiblePanelViewModel {
public params: CollapsiblePanelParams; private params: CollapsiblePanelParams;
private isCollapsed: ko.Observable<boolean>; private isCollapsed: ko.Observable<boolean>;
public constructor(params: CollapsiblePanelParams) { public constructor(params: CollapsiblePanelParams) {
@@ -50,7 +50,7 @@ class CollapsiblePanelViewModel {
this.isCollapsed = params.isCollapsed || ko.observable(false); this.isCollapsed = params.isCollapsed || ko.observable(false);
} }
public toggleCollapse(): void { private toggleCollapse(): void {
this.isCollapsed(!this.isCollapsed()); this.isCollapsed(!this.isCollapsed());
} }
} }

View File

@@ -44,11 +44,12 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
onChange?: (_?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => void; onChange?: (_?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => void;
}[] = [ }[] = [
{ key: "feature.enablechangefeedpolicy", label: "Enable change feed policy", value: "true" }, { 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.dataexplorerexecutesproc", label: "Execute stored procedure", value: "true" },
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" }, { key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
{ key: "feature.enablettl", label: "Enable TTL", value: "true" }, { key: "feature.enablettl", label: "Enable TTL", value: "true" },
{ key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" }, { key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" },
{ key: "feature.selfServeType", label: "Self serve feature", value: "sample" }, { key: "feature.enablecodeofconduct", label: "Enable Code Of Conduct Acknowledgement", value: "true" },
{ {
key: "feature.enableLinkInjection", key: "feature.enableLinkInjection",
label: "Enable Injecting Notebook Viewer Link into the first cell", label: "Enable Injecting Notebook Viewer Link into the first cell",

View File

@@ -131,6 +131,12 @@ exports[`Feature panel renders all flags 1`] = `
label="Enable change feed policy" label="Enable change feed policy"
onChange={[Function]} onChange={[Function]}
/> />
<StyledCheckboxBase
checked={false}
key="feature.enablerupm"
label="Enable RUPM"
onChange={[Function]}
/>
<StyledCheckboxBase <StyledCheckboxBase
checked={false} checked={false}
key="feature.dataexplorerexecutesproc" key="feature.dataexplorerexecutesproc"
@@ -157,8 +163,8 @@ exports[`Feature panel renders all flags 1`] = `
/> />
<StyledCheckboxBase <StyledCheckboxBase
checked={false} checked={false}
key="feature.selfServeType" key="feature.enablecodeofconduct"
label="Self serve feature" label="Enable Code Of Conduct Acknowledgement"
onChange={[Function]} onChange={[Function]}
/> />
<StyledCheckboxBase <StyledCheckboxBase

View File

@@ -71,7 +71,7 @@ interface InputTypeaheadParams {
/** /**
* This function gets called when pressing ENTER on the input box * This function gets called when pressing ENTER on the input box
*/ */
submitFct?: (inputValue: string | null, selection: Item | null) => void; submitFct?: (inputValue: string, selection: Item) => void;
/** /**
* Typehead comes with a Search button that we normally remove. * Typehead comes with a Search button that we normally remove.
@@ -88,8 +88,8 @@ interface OnClickItem {
} }
interface Cache { interface Cache {
inputValue: string | null; inputValue: string;
selection: Item | null; selection: Item;
} }
class InputTypeaheadViewModel { class InputTypeaheadViewModel {
@@ -98,12 +98,15 @@ class InputTypeaheadViewModel {
private params: InputTypeaheadParams; private params: InputTypeaheadParams;
private cache: Cache; private cache: Cache;
private inputValue: string;
private selection: Item;
public constructor(params: InputTypeaheadParams) { public constructor(params: InputTypeaheadParams) {
this.instanceNumber = InputTypeaheadViewModel.instanceCount++; this.instanceNumber = InputTypeaheadViewModel.instanceCount++;
this.params = params; this.params = params;
this.params.choices.subscribe(this.initializeTypeahead.bind(this)); this.params.choices.subscribe(this.initializeTypeahead.bind(this));
this.cache = { this.cache = {
inputValue: null, inputValue: null,
selection: null selection: null
@@ -158,7 +161,7 @@ class InputTypeaheadViewModel {
} }
} }
($ as any).typeahead(options); $.typeahead(options);
} }
/** /**
@@ -174,11 +177,11 @@ class InputTypeaheadViewModel {
* Use ko's "template: afterRender" callback to do that without actually using any template. * Use ko's "template: afterRender" callback to do that without actually using any template.
* Another way is to call it within setTimeout() in constructor. * Another way is to call it within setTimeout() in constructor.
*/ */
public afterRender(): void { private afterRender(): void {
this.initializeTypeahead(); this.initializeTypeahead();
} }
public submit(): void { private submit(): void {
if (this.params.submitFct) { if (this.params.submitFct) {
this.params.submitFct(this.cache.inputValue, this.cache.selection); this.params.submitFct(this.cache.inputValue, this.cache.selection);
} }

View File

@@ -59,12 +59,10 @@ export class JsonEditorViewModel extends WaitsForTemplateViewModel {
this.params = params; this.params = params;
this.params.content.subscribe((newValue: string) => { this.params.content.subscribe((newValue: string) => {
if (newValue) { if (!!this.editor) {
if (!!this.editor) { this.editor.getModel().setValue(newValue);
this.editor.getModel().setValue(newValue); } else {
} else { this.createEditor(newValue, this.configureEditor.bind(this));
this.createEditor(newValue, this.configureEditor.bind(this));
}
} }
}); });

View File

@@ -39,6 +39,10 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
params.set(TerminalQueryParams.TerminalEndpoint, terminalEndpoint); params.set(TerminalQueryParams.TerminalEndpoint, terminalEndpoint);
} }
params.set("account","contoso-retail-mongodb");
params.set("port","10255");
//tofill
params.set("token","");
return params; return params;
} }

View File

@@ -1,7 +1,6 @@
import { import {
Dropdown, Dropdown,
FocusZone, FocusZone,
FontIcon,
FontWeights, FontWeights,
IDropdownOption, IDropdownOption,
IPageSpecification, IPageSpecification,
@@ -17,7 +16,7 @@ import {
Text Text
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import { IGalleryItem, IJunoResponse, IPublicGalleryData, JunoClient } from "../../../Juno/JunoClient"; import { IGalleryItem, JunoClient, IJunoResponse, IPublicGalleryData } from "../../../Juno/JunoClient";
import * as GalleryUtils from "../../../Utils/GalleryUtils"; import * as GalleryUtils from "../../../Utils/GalleryUtils";
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent"; import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent"; import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
@@ -137,7 +136,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
} }
public render(): JSX.Element { public render(): JSX.Element {
const tabs: GalleryTabInfo[] = [this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)]; const tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
if (this.props.container?.isGalleryPublishEnabled()) { if (this.props.container?.isGalleryPublishEnabled()) {
tabs.push( tabs.push(
@@ -147,7 +146,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
this.state.isCodeOfConductAccepted this.state.isCodeOfConductAccepted
) )
); );
tabs.push(this.createFavoritesTab(GalleryTab.Favorites, this.state.favoriteNotebooks)); tabs.push(this.createTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
// explicitly checking if isCodeOfConductAccepted is not false, as it is initially undefined. // explicitly checking if isCodeOfConductAccepted is not false, as it is initially undefined.
// Displaying code of conduct component on gallery load should not be the default behavior. // Displaying code of conduct component on gallery load should not be the default behavior.
@@ -184,27 +183,6 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
); );
} }
private isEmptyData = (data: IGalleryItem[]): boolean => {
return !data || data.length === 0;
};
private createEmptyTabContent = (iconName: string, line1: string, line2: string): JSX.Element => {
return (
<Stack horizontalAlign="center" tokens={{ childrenGap: 10 }}>
<FontIcon iconName={iconName} style={{ fontSize: 100, color: "lightgray", marginTop: 20 }} />
<Text styles={{ root: { fontWeight: FontWeights.semibold } }}>{line1}</Text>
<Text>{line2}</Text>
</Stack>
);
};
private createSamplesTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
return {
tab,
content: this.createSearchBarHeader(this.createCardsTabContent(data))
};
};
private createPublicGalleryTab( private createPublicGalleryTab(
tab: GalleryTab, tab: GalleryTab,
data: IGalleryItem[], data: IGalleryItem[],
@@ -216,29 +194,17 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
}; };
} }
private createFavoritesTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo { private createTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
return { return {
tab, tab,
content: this.isEmptyData(data) content: this.createSearchBarHeader(this.createCardsTabContent(data))
? this.createEmptyTabContent(
"ContactHeart",
"You have not liked anything",
"Like any notebook from Official Samples or Public gallery"
)
: this.createSearchBarHeader(this.createCardsTabContent(data))
}; };
} }
private createPublishedNotebooksTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => { private createPublishedNotebooksTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
return { return {
tab, tab,
content: this.isEmptyData(data) content: this.createPublishedNotebooksTabContent(data)
? this.createEmptyTabContent(
"Contact",
"You have not published anything",
"Publish your sample notebooks to share your published work with others"
)
: this.createPublishedNotebooksTabContent(data)
}; };
}; };
@@ -398,9 +364,9 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> { private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
if (!offline) { if (!offline) {
try { try {
let response: IJunoResponse<IGalleryItem[]> | IJunoResponse<IPublicGalleryData>; let response: IJunoResponse<IPublicGalleryData> | IJunoResponse<IGalleryItem[]>;
if (this.props.container) { if (this.props.container.isCodeOfConductEnabled()) {
response = await this.props.junoClient.getPublicGalleryData(); response = await this.props.junoClient.fetchPublicNotebooks();
this.isCodeOfConductAccepted = response.data?.metadata.acceptedCodeOfConduct; this.isCodeOfConductAccepted = response.data?.metadata.acceptedCodeOfConduct;
this.publicNotebooks = response.data?.notebooksData; this.publicNotebooks = response.data?.notebooksData;
} else { } else {
@@ -602,7 +568,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private deleteItem = async (data: IGalleryItem): Promise<void> => { private deleteItem = async (data: IGalleryItem): Promise<void> => {
GalleryUtils.deleteItem(this.props.container, this.props.junoClient, data, item => { 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); this.refreshSelectedTab(item);
}); });
}; };

View File

@@ -92,8 +92,7 @@ describe("SettingsComponent", () => {
autoscaleMaxThroughput: 10000, autoscaleMaxThroughput: 10000,
manualThroughput: undefined, manualThroughput: undefined,
minimumThroughput: 400, minimumThroughput: 400,
id: "test", id: "test"
offerReplacePending: false
}); });
const props = { ...baseProps }; const props = { ...baseProps };
@@ -231,7 +230,7 @@ describe("SettingsComponent", () => {
it("getUpdatedConflictResolutionPolicy", () => { it("getUpdatedConflictResolutionPolicy", () => {
const wrapper = shallow(<SettingsComponent {...baseProps} />); const wrapper = shallow(<SettingsComponent {...baseProps} />);
const conflictResolutionPolicyPath = "/_ts"; const conflictResolutionPolicyPath = "_ts";
const conflictResolutionPolicyProcedure = "sample_sproc"; const conflictResolutionPolicyProcedure = "sample_sproc";
const expectSprocPath = const expectSprocPath =
"/dbs/" + collection.databaseId + "/colls/" + collection.id() + "/sprocs/" + conflictResolutionPolicyProcedure; "/dbs/" + collection.databaseId + "/colls/" + collection.id() + "/sprocs/" + conflictResolutionPolicyProcedure;

View File

@@ -138,8 +138,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
// Mongo container with system partition key still treat as "Fixed" // Mongo container with system partition key still treat as "Fixed"
this.isFixedContainer = this.isFixedContainer =
this.container.isPreferredApiMongoDB() && !this.collection.partitionKey ||
(!this.collection.partitionKey || this.collection.partitionKey.systemKey); (this.container.isPreferredApiMongoDB() && this.collection.partitionKey.systemKey);
this.state = { this.state = {
throughput: undefined, throughput: undefined,
@@ -295,7 +295,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
!!this.collection.conflictResolutionPolicy(); !!this.collection.conflictResolutionPolicy();
public isOfferReplacePending = (): boolean => { public isOfferReplacePending = (): boolean => {
return this.collection?.offer()?.offerReplacePending; return !!this.collection?.offer()?.headers?.[Constants.HttpHeaders.offerReplacePending];
}; };
public onSaveClick = async (): Promise<void> => { public onSaveClick = async (): Promise<void> => {
@@ -684,7 +684,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
if (policy.mode === DataModels.ConflictResolutionMode.LastWriterWins) { if (policy.mode === DataModels.ConflictResolutionMode.LastWriterWins) {
policy.conflictResolutionPath = this.state.conflictResolutionPolicyPath; policy.conflictResolutionPath = this.state.conflictResolutionPolicyPath;
if (!policy.conflictResolutionPath?.startsWith("/")) { if (policy.conflictResolutionPath?.startsWith("/")) {
policy.conflictResolutionPath = "/" + policy.conflictResolutionPath; policy.conflictResolutionPath = "/" + policy.conflictResolutionPath;
} }
} }

View File

@@ -1,9 +1,9 @@
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import React from "react"; import React from "react";
import { IColumn, Text } from "office-ui-fabric-react";
import { import {
getAutoPilotV3SpendElement, getAutoPilotV3SpendElement,
getEstimatedSpendingElement, getEstimatedSpendElement,
getEstimatedAutoscaleSpendElement,
manualToAutoscaleDisclaimerElement, manualToAutoscaleDisclaimerElement,
ttlWarning, ttlWarning,
indexingPolicynUnsavedWarningMessage, indexingPolicynUnsavedWarningMessage,
@@ -19,37 +19,11 @@ import {
mongoIndexingPolicyDisclaimer, mongoIndexingPolicyDisclaimer,
mongoIndexingPolicyAADError, mongoIndexingPolicyAADError,
mongoIndexTransformationRefreshingMessage, mongoIndexTransformationRefreshingMessage,
renderMongoIndexTransformationRefreshMessage, renderMongoIndexTransformationRefreshMessage
ManualEstimatedSpendingDisplayProps,
PriceBreakdown,
getRuPriceBreakdown
} from "./SettingsRenderUtils"; } from "./SettingsRenderUtils";
class SettingsRenderUtilsTestComponent extends React.Component { class SettingsRenderUtilsTestComponent extends React.Component {
public render(): JSX.Element { 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: <Text>Current Cost</Text>,
hourly: <Text>$ 1.02</Text>,
daily: <Text>$ 24.48</Text>,
monthly: <Text>$ 744.6</Text>
}
];
const priceBreakdown: PriceBreakdown = {
hourlyPrice: 1.02,
dailyPrice: 24.48,
monthlyPrice: 744.6,
pricePerRu: 0.00051,
currency: "RMB",
currencySign: "¥"
};
return ( return (
<> <>
{getAutoPilotV3SpendElement(1000, false)} {getAutoPilotV3SpendElement(1000, false)}
@@ -57,7 +31,9 @@ class SettingsRenderUtilsTestComponent extends React.Component {
{getAutoPilotV3SpendElement(1000, true)} {getAutoPilotV3SpendElement(1000, true)}
{getAutoPilotV3SpendElement(undefined, true)} {getAutoPilotV3SpendElement(undefined, true)}
{getEstimatedSpendingElement(estimatedSpendingColumns, estimatedSpendingItems, 1000, 2, priceBreakdown, false)} {getEstimatedSpendElement(1000, "mooncake", 2, false, true)}
{getEstimatedAutoscaleSpendElement(1000, "mooncake", 2, false)}
{manualToAutoscaleDisclaimerElement} {manualToAutoscaleDisclaimerElement}
{ttlWarning} {ttlWarning}
@@ -93,14 +69,4 @@ describe("SettingsUtils functions", () => {
const wrapper = shallow(<SettingsRenderUtilsTestComponent />); const wrapper = shallow(<SettingsRenderUtilsTestComponent />);
expect(wrapper).toMatchSnapshot(); 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("$");
});
}); });

View File

@@ -3,13 +3,14 @@ import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants"; import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants";
import { Urls, StyleConstants } from "../../../Common/Constants"; import { Urls, StyleConstants } from "../../../Common/Constants";
import { import {
computeAutoscaleUsagePriceHourly,
getPriceCurrency, getPriceCurrency,
getCurrencySign, getCurrencySign,
getAutoscalePricePerRu, getAutoscalePricePerRu,
getMultimasterMultiplier, getMultimasterMultiplier,
computeRUUsagePriceHourly, computeRUUsagePriceHourly,
getPricePerRu, getPricePerRu,
estimatedCostDisclaimer calculateEstimateNumber
} from "../../../Utils/PricingUtils"; } from "../../../Utils/PricingUtils";
import { import {
ITextFieldStyles, ITextFieldStyles,
@@ -31,42 +32,11 @@ import {
MessageBarType, MessageBarType,
Stack, Stack,
Spinner, Spinner,
SpinnerSize, SpinnerSize
DetailsList,
IColumn,
SelectionMode,
DetailsListLayoutMode,
IDetailsRowProps,
DetailsRow,
IDetailsColumnStyles
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import { isDirtyTypes, isDirty } from "./SettingsUtils"; import { isDirtyTypes, isDirty } from "./SettingsUtils";
export interface EstimatedSpendingDisplayProps { export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } };
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 = { export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
label: { label: {
@@ -134,16 +104,6 @@ export const transparentDetailsRowStyles: Partial<IDetailsRowStyles> = {
} }
}; };
export const transparentDetailsHeaderStyle: Partial<IDetailsColumnStyles> = {
root: {
selectors: {
":hover": {
background: "transparent"
}
}
}
};
export const customDetailsListStyles: Partial<IDetailsListStyles> = { export const customDetailsListStyles: Partial<IDetailsListStyles> = {
root: { root: {
selectors: { selectors: {
@@ -166,17 +126,10 @@ export const separatorStyles: Partial<ISeparatorStyles> = {
] ]
}; };
export const messageBarStyles: Partial<IMessageBarStyles> = { export const messageBarStyles: Partial<IMessageBarStyles> = { root: { marginTop: "5px" } };
root: { marginTop: "5px", backgroundColor: "white" },
text: { fontSize: 14 }
};
export const throughputUnit = "RU/s"; export const throughputUnit = "RU/s";
export function onRenderRow(props: IDetailsRowProps): JSX.Element {
return <DetailsRow {...props} styles={transparentDetailsRowStyles} />;
}
export const getAutoPilotV3SpendElement = ( export const getAutoPilotV3SpendElement = (
maxAutoPilotThroughputSet: number, maxAutoPilotThroughputSet: number,
isDatabaseThroughput: boolean, isDatabaseThroughput: boolean,
@@ -212,61 +165,64 @@ export const getAutoPilotV3SpendElement = (
); );
}; };
export const getRuPriceBreakdown = ( export const getEstimatedAutoscaleSpendElement = (
throughput: number, throughput: number,
serverId: string, serverId: string,
numberOfRegions: number, regions: number,
isMultimaster: boolean, multimaster: boolean
isAutoscale: boolean ): JSX.Element => {
): PriceBreakdown => { const hourlyPrice: number = computeAutoscaleUsagePriceHourly(serverId, throughput, regions, multimaster);
const hourlyPrice: number = computeRUUsagePriceHourly({ const monthlyPrice: number = hourlyPrice * hoursInAMonth;
serverId: serverId, const currency: string = getPriceCurrency(serverId);
requestUnits: throughput, const currencySign: string = getCurrencySign(serverId);
numberOfRegions: numberOfRegions, const pricePerRu =
multimasterEnabled: isMultimaster, getAutoscalePricePerRu(serverId, getMultimasterMultiplier(regions, multimaster)) *
isAutoscale: isAutoscale getMultimasterMultiplier(regions, multimaster);
});
const basePricePerRu: number = isAutoscale return (
? getAutoscalePricePerRu(serverId, getMultimasterMultiplier(numberOfRegions, isMultimaster)) <Text id="autoscaleSpendElement">
: getPricePerRu(serverId); Estimated monthly cost ({currency}) is{" "}
return { <b>
hourlyPrice: hourlyPrice, {currencySign}
dailyPrice: hourlyPrice * 24, {calculateEstimateNumber(monthlyPrice / 10)}
monthlyPrice: hourlyPrice * hoursInAMonth, {` - `}
pricePerRu: basePricePerRu * getMultimasterMultiplier(numberOfRegions, isMultimaster), {currencySign}
currency: getPriceCurrency(serverId), {calculateEstimateNumber(monthlyPrice)}{" "}
currencySign: getCurrencySign(serverId) </b>
}; ({"regions: "} {regions}, {throughput / 10} - {throughput} RU/s, {currencySign}
{pricePerRu}/RU)
</Text>
);
}; };
export const getEstimatedSpendingElement = ( export const getEstimatedSpendElement = (
estimatedSpendingColumns: IColumn[],
estimatedSpendingItems: EstimatedSpendingDisplayProps[],
throughput: number, throughput: number,
numberOfRegions: number, serverId: string,
priceBreakdown: PriceBreakdown, regions: number,
isAutoscale: boolean multimaster: boolean,
rupmEnabled: boolean
): JSX.Element => { ): JSX.Element => {
const ruRange: string = isAutoscale ? throughput / 10 + " RU/s - " : ""; 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);
return ( return (
<Stack {...addMongoIndexStackProps} styles={mediumWidthStackStyles}> <Text id="throughputSpendElement">
<DetailsList Estimated cost ({currency}):{" "}
disableSelectionZone <b>
items={estimatedSpendingItems} {currencySign}
columns={estimatedSpendingColumns} {calculateEstimateNumber(hourlyPrice)} hourly {` / `}
selectionMode={SelectionMode.none} {currencySign}
layoutMode={DetailsListLayoutMode.justified} {calculateEstimateNumber(dailyPrice)} daily {` / `}
onRenderRow={onRenderRow} {currencySign}
/> {calculateEstimateNumber(monthlyPrice)} monthly{" "}
<Text id="throughputSpendElement"> </b>
({"regions: "} {numberOfRegions}, {ruRange} ({"regions: "} {regions}, {throughput}RU/s, {currencySign}
{throughput} RU/s, {priceBreakdown.currencySign} {pricePerRu}/RU)
{priceBreakdown.pricePerRu}/RU) </Text>
</Text>
<Text>
<em>{estimatedCostDisclaimer}</em>
</Text>
</Stack>
); );
}; };
@@ -310,13 +266,6 @@ export const updateThroughputDelayedApplyWarningMessage: JSX.Element = (
</Text> </Text>
); );
export const saveThroughputWarningMessage: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}>
Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below
before saving your changes
</Text>
);
const getCurrentThroughput = ( const getCurrentThroughput = (
isAutoscale: boolean, isAutoscale: boolean,
throughput: number, throughput: number,

View File

@@ -8,7 +8,7 @@ exports[`IndexingPolicyRefreshComponent renders 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }

View File

@@ -6,6 +6,8 @@ import {
IconButton, IconButton,
Text, Text,
SelectionMode, SelectionMode,
IDetailsRowProps,
DetailsRow,
IColumn, IColumn,
MessageBar, MessageBar,
MessageBarType, MessageBarType,
@@ -19,11 +21,11 @@ import {
mongoIndexingPolicyDisclaimer, mongoIndexingPolicyDisclaimer,
mediumWidthStackStyles, mediumWidthStackStyles,
subComponentStackProps, subComponentStackProps,
transparentDetailsRowStyles,
createAndAddMongoIndexStackProps, createAndAddMongoIndexStackProps,
separatorStyles, separatorStyles,
indexingPolicynUnsavedWarningMessage, indexingPolicynUnsavedWarningMessage,
infoAndToolTipTextStyle, infoAndToolTipTextStyle
onRenderRow
} from "../../SettingsRenderUtils"; } from "../../SettingsRenderUtils";
import { MongoIndex } from "../../../../../Utils/arm/generatedClients/2020-04-01/types"; import { MongoIndex } from "../../../../../Utils/arm/generatedClients/2020-04-01/types";
import { import {
@@ -138,6 +140,10 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
return undefined; return undefined;
}; };
private onRenderRow = (props: IDetailsRowProps): JSX.Element => {
return <DetailsRow {...props} styles={transparentDetailsRowStyles} />;
};
private getActionButton = (arrayPosition: number, isCurrentIndex: boolean): JSX.Element => { private getActionButton = (arrayPosition: number, isCurrentIndex: boolean): JSX.Element => {
return isCurrentIndex ? ( return isCurrentIndex ? (
<IconButton <IconButton
@@ -247,7 +253,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
items={initialIndexes} items={initialIndexes}
columns={this.initialIndexesColumns} columns={this.initialIndexesColumns}
selectionMode={SelectionMode.none} selectionMode={SelectionMode.none}
onRenderRow={onRenderRow} onRenderRow={this.onRenderRow}
layoutMode={DetailsListLayoutMode.justified} layoutMode={DetailsListLayoutMode.justified}
/> />
{this.renderIndexesToBeAdded()} {this.renderIndexesToBeAdded()}
@@ -273,7 +279,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
items={indexesToBeDropped} items={indexesToBeDropped}
columns={this.indexesToBeDroppedColumns} columns={this.indexesToBeDroppedColumns}
selectionMode={SelectionMode.none} selectionMode={SelectionMode.none}
onRenderRow={onRenderRow} onRenderRow={this.onRenderRow}
layoutMode={DetailsListLayoutMode.justified} layoutMode={DetailsListLayoutMode.justified}
/> />
)} )}

View File

@@ -59,7 +59,7 @@ describe("ScaleComponent", () => {
autoscaleMaxThroughput: maxThroughput, autoscaleMaxThroughput: maxThroughput,
minimumThroughput: 400, minimumThroughput: 400,
id: "offer", id: "offer",
offerReplacePending: true headers: { "x-ms-offer-replace-pending": true }
}); });
const newProps = { const newProps = {
...baseProps, ...baseProps,

View File

@@ -16,7 +16,7 @@ import {
} from "../SettingsRenderUtils"; } from "../SettingsRenderUtils";
import { hasDatabaseSharedThroughput } from "../SettingsUtils"; import { hasDatabaseSharedThroughput } from "../SettingsUtils";
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils"; import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
import { Link, Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react"; import { Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
import { configContext, Platform } from "../../../../ConfigContext"; import { configContext, Platform } from "../../../../ConfigContext";
export interface ScaleComponentProps { export interface ScaleComponentProps {
@@ -116,7 +116,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
} }
const offer = this.props.collection?.offer(); const offer = this.props.collection?.offer();
if (offer?.offerReplacePending) { if (offer?.headers?.[Constants.HttpHeaders.offerReplacePending]) {
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput; const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
return getThroughputApplyShortDelayMessage( return getThroughputApplyShortDelayMessage(
this.props.isAutoPilotSelected, this.props.isAutoPilotSelected,
@@ -165,8 +165,6 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
private getThroughputInputComponent = (): JSX.Element => ( private getThroughputInputComponent = (): JSX.Element => (
<ThroughputInputAutoPilotV3Component <ThroughputInputAutoPilotV3Component
databaseAccount={this.props.container.databaseAccount()} databaseAccount={this.props.container.databaseAccount()}
databaseName={this.props.collection.databaseId}
collectionName={this.props.collection.id()}
serverId={this.props.container.serverId()} serverId={this.props.container.serverId()}
throughput={this.props.throughput} throughput={this.props.throughput}
throughputBaseline={this.props.throughputBaseline} throughputBaseline={this.props.throughputBaseline}
@@ -178,7 +176,6 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
label={this.getThroughputTitle()} label={this.getThroughputTitle()}
isEmulator={this.isEmulator} isEmulator={this.isEmulator}
isFixed={this.props.isFixedContainer} isFixed={this.props.isFixedContainer}
isFreeTierAccount={this.isFreeTierAccount()}
isAutoPilotSelected={this.props.isAutoPilotSelected} isAutoPilotSelected={this.props.isAutoPilotSelected}
onAutoPilotSelected={this.props.onAutoPilotSelected} onAutoPilotSelected={this.props.onAutoPilotSelected}
wasAutopilotOriginallySet={this.props.wasAutopilotOriginallySet} wasAutopilotOriginallySet={this.props.wasAutopilotOriginallySet}
@@ -189,41 +186,13 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
onScaleSaveableChange={this.props.onScaleSaveableChange} onScaleSaveableChange={this.props.onScaleSaveableChange}
onScaleDiscardableChange={this.props.onScaleDiscardableChange} onScaleDiscardableChange={this.props.onScaleDiscardableChange}
getThroughputWarningMessage={this.getThroughputWarningMessage} getThroughputWarningMessage={this.getThroughputWarningMessage}
usageSizeInKB={this.props.collection.usageSizeInKB()} usageSizeInKB={this.props.collection.quotaInfo().usageSizeInKB}
/> />
); );
private isFreeTierAccount(): boolean {
const databaseAccount = this.props.container?.databaseAccount();
return databaseAccount?.properties?.enableFreeTier;
}
private getFreeTierInfoMessage(): JSX.Element {
return (
<Text>
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.
<Link
href="https://docs.microsoft.com/en-us/azure/cosmos-db/understand-your-bill#billing-examples-with-free-tier-accounts"
target="_blank"
>
Learn more.
</Link>
</Text>
);
}
public render(): JSX.Element { public render(): JSX.Element {
return ( return (
<Stack {...subComponentStackProps}> <Stack {...subComponentStackProps}>
{this.isFreeTierAccount() && (
<MessageBar
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
styles={{ text: { fontSize: 14 } }}
>
{this.getFreeTierInfoMessage()}
</MessageBar>
)}
{this.getInitialNotificationElement() && ( {this.getInitialNotificationElement() && (
<MessageBar messageBarType={MessageBarType.warning}>{this.getInitialNotificationElement()}</MessageBar> <MessageBar messageBarType={MessageBarType.warning}>{this.getInitialNotificationElement()}</MessageBar>
)} )}

View File

@@ -13,7 +13,16 @@ import {
} from "../SettingsUtils"; } from "../SettingsUtils";
import Explorer from "../../../Explorer"; import Explorer from "../../../Explorer";
import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon"; import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
import { Label, Text, TextField, Stack, IChoiceGroupOption, ChoiceGroup, MessageBar } from "office-ui-fabric-react"; import {
Label,
Text,
TextField,
Stack,
IChoiceGroupOption,
ChoiceGroup,
MessageBar,
MessageBarType
} from "office-ui-fabric-react";
import { import {
getTextFieldStyles, getTextFieldStyles,
changeFeedPolicyToolTip, changeFeedPolicyToolTip,
@@ -181,10 +190,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
styles={getChoiceGroupStyles(this.props.timeToLive, this.props.timeToLiveBaseline)} styles={getChoiceGroupStyles(this.props.timeToLive, this.props.timeToLiveBaseline)}
/> />
{isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && ( {isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && (
<MessageBar <MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
styles={messageBarStyles}
>
{ttlWarning} {ttlWarning}
</MessageBar> </MessageBar>
)} )}

View File

@@ -9,8 +9,6 @@ import * as DataModels from "../../../../../Contracts/DataModels";
describe("ThroughputInputAutoPilotV3Component", () => { describe("ThroughputInputAutoPilotV3Component", () => {
const baseProps: ThroughputInputAutoPilotV3Props = { const baseProps: ThroughputInputAutoPilotV3Props = {
databaseAccount: {} as DataModels.DatabaseAccount, databaseAccount: {} as DataModels.DatabaseAccount,
databaseName: "test",
collectionName: "test",
serverId: undefined, serverId: undefined,
wasAutopilotOriginallySet: false, wasAutopilotOriginallySet: false,
throughput: 100, throughput: 100,
@@ -28,7 +26,6 @@ describe("ThroughputInputAutoPilotV3Component", () => {
spendAckVisible: false, spendAckVisible: false,
showAsMandatory: true, showAsMandatory: true,
isFixed: false, isFixed: false,
isFreeTierAccount: false,
label: "label", label: "label",
infoBubbleText: "infoBubbleText", infoBubbleText: "infoBubbleText",
canExceedMaximumValue: true, canExceedMaximumValue: true,
@@ -57,6 +54,7 @@ describe("ThroughputInputAutoPilotV3Component", () => {
expect(wrapper.exists("#throughputInput")).toEqual(true); expect(wrapper.exists("#throughputInput")).toEqual(true);
expect(wrapper.exists("#autopilotInput")).toEqual(false); expect(wrapper.exists("#autopilotInput")).toEqual(false);
expect(wrapper.exists("#throughputSpendElement")).toEqual(true); expect(wrapper.exists("#throughputSpendElement")).toEqual(true);
expect(wrapper.exists("#autoscaleSpendElement")).toEqual(false);
}); });
it("autopilot input visible", () => { it("autopilot input visible", () => {
@@ -74,7 +72,8 @@ describe("ThroughputInputAutoPilotV3Component", () => {
wrapper.setProps({ wasAutopilotOriginallySet: true }); wrapper.setProps({ wasAutopilotOriginallySet: true });
wrapper.update(); wrapper.update();
expect(wrapper.exists("#throughputSpendElement")).toEqual(true); expect(wrapper.exists("#autoscaleSpendElement")).toEqual(true);
expect(wrapper.exists("#throughputSpendElement")).toEqual(false);
}); });
it("spendAck checkbox visible", () => { it("spendAck checkbox visible", () => {

View File

@@ -8,15 +8,10 @@ import {
checkBoxAndInputStackProps, checkBoxAndInputStackProps,
getChoiceGroupStyles, getChoiceGroupStyles,
messageBarStyles, messageBarStyles,
getEstimatedSpendingElement, getEstimatedSpendElement,
getEstimatedAutoscaleSpendElement,
getAutoPilotV3SpendElement, getAutoPilotV3SpendElement,
manualToAutoscaleDisclaimerElement, manualToAutoscaleDisclaimerElement
saveThroughputWarningMessage,
ManualEstimatedSpendingDisplayProps,
AutoscaleEstimatedSpendingDisplayProps,
PriceBreakdown,
getRuPriceBreakdown,
transparentDetailsHeaderStyle
} from "../../SettingsRenderUtils"; } from "../../SettingsRenderUtils";
import { import {
Text, Text,
@@ -28,8 +23,7 @@ import {
Label, Label,
Link, Link,
MessageBar, MessageBar,
FontIcon, MessageBarType
IColumn
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import { ToolTipLabelComponent } from "../ToolTipLabelComponent"; import { ToolTipLabelComponent } from "../ToolTipLabelComponent";
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils"; import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
@@ -38,16 +32,11 @@ import * as DataModels from "../../../../../Contracts/DataModels";
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon"; import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
import { userContext } from "../../../../../UserContext"; import { userContext } from "../../../../../UserContext";
import { SubscriptionType } from "../../../../../Contracts/SubscriptionType"; import { SubscriptionType } from "../../../../../Contracts/SubscriptionType";
import { usageInGB, calculateEstimateNumber } from "../../../../../Utils/PricingUtils"; import { usageInGB } from "../../../../../Utils/PricingUtils";
import { Features } from "../../../../../Common/Constants"; import { Features } from "../../../../../Common/Constants";
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants";
export interface ThroughputInputAutoPilotV3Props { export interface ThroughputInputAutoPilotV3Props {
databaseAccount: DataModels.DatabaseAccount; databaseAccount: DataModels.DatabaseAccount;
databaseName: string;
collectionName: string;
serverId: string; serverId: string;
throughput: number; throughput: number;
throughputBaseline: number; throughputBaseline: number;
@@ -62,7 +51,6 @@ export interface ThroughputInputAutoPilotV3Props {
spendAckVisible?: boolean; spendAckVisible?: boolean;
showAsMandatory?: boolean; showAsMandatory?: boolean;
isFixed: boolean; isFixed: boolean;
isFreeTierAccount: boolean;
isEmulator: boolean; isEmulator: boolean;
label: string; label: string;
infoBubbleText?: string; infoBubbleText?: string;
@@ -81,7 +69,6 @@ export interface ThroughputInputAutoPilotV3Props {
interface ThroughputInputAutoPilotV3State { interface ThroughputInputAutoPilotV3State {
spendAckChecked: boolean; spendAckChecked: boolean;
exceedFreeTierThroughput: boolean;
} }
export class ThroughputInputAutoPilotV3Component extends React.Component< export class ThroughputInputAutoPilotV3Component extends React.Component<
@@ -155,9 +142,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
public constructor(props: ThroughputInputAutoPilotV3Props) { public constructor(props: ThroughputInputAutoPilotV3Props) {
super(props); super(props);
this.state = { 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; this.step = this.props.step ?? ThroughputInputAutoPilotV3Component.defaultStep;
@@ -180,243 +165,34 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
return <></>; return <></>;
} }
const isDirty: boolean = this.IsComponentDirty().isDiscardable;
const serverId: string = this.props.serverId; const serverId: string = this.props.serverId;
const offerThroughput: number = this.props.throughput;
const regions = account?.properties?.readLocations?.length || 1; const regions = account?.properties?.readLocations?.length || 1;
const multimaster = account?.properties?.enableMultipleWriteLocations || false; const multimaster = account?.properties?.enableMultipleWriteLocations || false;
let estimatedSpend: JSX.Element; let estimatedSpend: JSX.Element;
if (!this.props.isAutoPilotSelected) { if (!this.props.isAutoPilotSelected) {
estimatedSpend = this.getEstimatedManualSpendElement( estimatedSpend = getEstimatedSpendElement(
// if migrating from autoscale to manual, we use the autoscale RUs value as that is what will be set... // if migrating from autoscale to manual, we use the autoscale RUs value as that is what will be set...
this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : this.props.throughputBaseline, this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : offerThroughput,
serverId, serverId,
regions, regions,
multimaster, multimaster,
isDirty ? this.props.throughput : undefined false
); );
} else { } else {
estimatedSpend = this.getEstimatedAutoscaleSpendElement( estimatedSpend = getEstimatedAutoscaleSpendElement(
this.props.maxAutoPilotThroughputBaseline, this.props.maxAutoPilotThroughput,
serverId, serverId,
regions, regions,
multimaster, multimaster
isDirty ? this.props.maxAutoPilotThroughput : undefined
); );
} }
return estimatedSpend; 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: <Text>Current Cost</Text>,
minPerMonth: (
<Text>
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)}
</Text>
),
maxPerMonth: (
<Text>
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}
</Text>
)
}
];
if (newThroughput) {
const newPrices: PriceBreakdown = getRuPriceBreakdown(
newThroughput,
serverId,
numberOfRegions,
isMultimaster,
true
);
estimatedSpendingItems.unshift({
costType: (
<Text>
<b>Updated Cost</b>
</Text>
),
minPerMonth: (
<Text>
<b>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)}
</b>
</Text>
),
maxPerMonth: (
<Text>
<b>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}
</b>
</Text>
)
});
}
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: <Text>Current Cost</Text>,
hourly: (
<Text>
{prices.currencySign} {calculateEstimateNumber(prices.hourlyPrice)}
</Text>
),
daily: (
<Text>
{prices.currencySign} {calculateEstimateNumber(prices.dailyPrice)}
</Text>
),
monthly: (
<Text>
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}
</Text>
)
}
];
if (newThroughput) {
const newPrices: PriceBreakdown = getRuPriceBreakdown(
newThroughput,
serverId,
numberOfRegions,
isMultimaster,
false
);
estimatedSpendingItems.unshift({
costType: (
<Text>
<b>Updated Cost</b>
</Text>
),
hourly: (
<Text>
<b>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.hourlyPrice)}
</b>
</Text>
),
daily: (
<Text>
<b>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.dailyPrice)}
</b>
</Text>
),
monthly: (
<Text>
<b>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}
</b>
</Text>
)
});
}
return getEstimatedSpendingElement(
estimatedSpendingColumns,
estimatedSpendingItems,
newThroughput ?? throughput,
numberOfRegions,
prices,
false
);
};
private getAutoPilotUsageCost = (): JSX.Element => { private getAutoPilotUsageCost = (): JSX.Element => {
if (!this.props.maxAutoPilotThroughput) { if (!this.props.maxAutoPilotThroughput) {
return <></>; return <></>;
@@ -432,7 +208,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string newValue?: string
): void => { ): void => {
const newThroughput = getSanitizedInputValue(newValue); const newThroughput = getSanitizedInputValue(newValue, this.autoPilotInputMaxValue);
this.props.onMaxAutoPilotThroughputChange(newThroughput); this.props.onMaxAutoPilotThroughputChange(newThroughput);
}; };
@@ -440,11 +216,10 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string newValue?: string
): void => { ): void => {
const newThroughput = getSanitizedInputValue(newValue); const newThroughput = getSanitizedInputValue(newValue, this.throughputInputMaxValue);
if (this.overrideWithAutoPilotSettings()) { if (this.overrideWithAutoPilotSettings()) {
this.props.onMaxAutoPilotThroughputChange(newThroughput); this.props.onMaxAutoPilotThroughputChange(newThroughput);
} else { } else {
this.setState({ exceedFreeTierThroughput: this.props.isFreeTierAccount && newThroughput > 400 });
this.props.onThroughputChange(newThroughput); this.props.onThroughputChange(newThroughput);
} }
}; };
@@ -452,19 +227,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
private onChoiceGroupChange = ( private onChoiceGroupChange = (
event?: React.FormEvent<HTMLElement | HTMLInputElement>, event?: React.FormEvent<HTMLElement | HTMLInputElement>,
option?: IChoiceGroupOption option?: IChoiceGroupOption
): void => { ): void => this.props.onAutoPilotSelected(option.key === "true");
this.props.onAutoPilotSelected(option.key === "true");
TelemetryProcessor.trace(Action.ToggleAutoscaleSetting, ActionModifiers.Mark, {
changedSelectedValueTo:
option.key === "true" ? ActionModifiers.ToggleAutoscaleOn : ActionModifiers.ToggleAutoscaleOff,
subscriptionId: userContext.subscriptionId,
databaseAccountName: this.props.databaseAccount?.name,
databaseName: this.props.databaseName,
collectionName: this.props.collectionName,
apiKind: userContext.defaultExperience,
dataExplorerArea: "Scale Tab V2"
});
};
private minRUperGBSurvey = (): JSX.Element => { private minRUperGBSurvey = (): JSX.Element => {
const href = `https://ncv.microsoft.com/vRBTO37jmO?ctx={"AzureSubscriptionId":"${userContext.subscriptionId}","CosmosDBAccountName":"${userContext.databaseAccount?.name}"}`; const href = `https://ncv.microsoft.com/vRBTO37jmO?ctx={"AzureSubscriptionId":"${userContext.subscriptionId}","CosmosDBAccountName":"${userContext.databaseAccount?.name}"}`;
@@ -500,10 +263,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
/> />
</Label> </Label>
{this.overrideWithProvisionedThroughputSettings() && ( {this.overrideWithProvisionedThroughputSettings() && (
<MessageBar <MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
styles={messageBarStyles}
>
{manualToAutoscaleDisclaimerElement} {manualToAutoscaleDisclaimerElement}
</MessageBar> </MessageBar>
)} )}
@@ -559,12 +319,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
private renderThroughputInput = (): JSX.Element => ( private renderThroughputInput = (): JSX.Element => (
<Stack {...titleAndInputStackProps}> <Stack {...titleAndInputStackProps}>
<Text>
Estimate your required throughput with
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
{` capacity calculator`} <FontIcon iconName="NavigateExternalInline" />
</Link>
</Text>
<TextField <TextField
required required
type="number" type="number"
@@ -580,21 +334,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
} }
onChange={this.onThroughputChange} onChange={this.onThroughputChange}
/> />
{this.state.exceedFreeTierThroughput && (
<MessageBar
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
styles={messageBarStyles}
>
{
"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."
}
</MessageBar>
)}
{this.props.getThroughputWarningMessage() && ( {this.props.getThroughputWarningMessage() && (
<MessageBar <MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
styles={messageBarStyles}
>
{this.props.getThroughputWarningMessage()} {this.props.getThroughputWarningMessage()}
</MessageBar> </MessageBar>
)} )}
@@ -609,32 +350,13 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
onChange={this.onSpendAckChecked} onChange={this.onSpendAckChecked}
/> />
)} )}
<br />
{this.props.isFixed && <p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>} {this.props.isFixed && <p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>}
</Stack> </Stack>
); );
private renderWarningMessage = (): JSX.Element => {
let warningMessage: JSX.Element;
if (this.IsComponentDirty().isDiscardable) {
warningMessage = saveThroughputWarningMessage;
}
return (
<>
{warningMessage && (
<MessageBar messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}>
{warningMessage}
</MessageBar>
)}
</>
);
};
public render(): JSX.Element { public render(): JSX.Element {
return ( return (
<Stack {...checkBoxAndInputStackProps}> <Stack {...checkBoxAndInputStackProps}>
{this.renderWarningMessage()}
{this.renderThroughputModeChoices()} {this.renderThroughputModeChoices()}
{this.props.isAutoPilotSelected ? this.renderAutoPilotInput() : this.renderThroughputInput()} {this.props.isAutoPilotSelected ? this.renderAutoPilotInput() : this.renderThroughputInput()}

View File

@@ -8,26 +8,6 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
} }
} }
> >
<StyledMessageBarBase
messageBarIconProps={
Object {
"className": "messageBarWarningIcon",
"iconName": "WarningSolid",
}
}
>
<Text
styles={
Object {
"root": Object {
"fontSize": 14,
},
}
}
>
Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below before saving your changes
</Text>
</StyledMessageBarBase>
<Stack> <Stack>
<StyledLabelBase <StyledLabelBase
id="settingsV2RadioButtonLabelId" id="settingsV2RadioButtonLabelId"
@@ -39,7 +19,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }
@@ -50,21 +30,12 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
/> />
</StyledLabelBase> </StyledLabelBase>
<StyledMessageBarBase <StyledMessageBarBase
messageBarIconProps={ messageBarType={5}
Object {
"className": "messageBarInfoIcon",
"iconName": "InfoSolid",
}
}
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"backgroundColor": "white",
"marginTop": "5px", "marginTop": "5px",
}, },
"text": Object {
"fontSize": 14,
},
} }
} }
> >
@@ -73,7 +44,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }
@@ -185,7 +156,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }
@@ -243,19 +214,6 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
} }
} }
> >
<Text>
Estimate your required throughput with
<StyledLinkBase
href="https://cosmos.azure.com/capacitycalculator/"
target="_blank"
>
capacity calculator
<Component
iconName="NavigateExternalInline"
/>
</StyledLinkBase>
</Text>
<StyledTextFieldBase <StyledTextFieldBase
disabled={false} disabled={false}
id="throughputInput" id="throughputInput"
@@ -281,142 +239,38 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
type="number" type="number"
value="100" value="100"
/> />
<Stack <Text
styles={ id="throughputSpendElement"
Object {
"root": Object {
"width": 600,
},
}
}
tokens={
Object {
"childrenGap": 10,
}
}
> >
<StyledWithViewportComponent Estimated cost (
columns={ USD
Array [ ):
Object {
"fieldName": "costType",
"isResizable": true,
"key": "costType",
"maxWidth": 200,
"minWidth": 100,
"name": "",
"styles": Object {
"root": Object {
"selectors": Object {
":hover": Object {
"background": "transparent",
},
},
},
},
},
Object {
"fieldName": "hourly",
"isResizable": true,
"key": "hourly",
"maxWidth": 200,
"minWidth": 100,
"name": "Hourly",
"styles": Object {
"root": Object {
"selectors": Object {
":hover": Object {
"background": "transparent",
},
},
},
},
},
Object {
"fieldName": "daily",
"isResizable": true,
"key": "daily",
"maxWidth": 200,
"minWidth": 100,
"name": "Daily",
"styles": Object {
"root": Object {
"selectors": Object {
":hover": Object {
"background": "transparent",
},
},
},
},
},
Object {
"fieldName": "monthly",
"isResizable": true,
"key": "monthly",
"maxWidth": 200,
"minWidth": 100,
"name": "Monthly",
"styles": Object {
"root": Object {
"selectors": Object {
":hover": Object {
"background": "transparent",
},
},
},
},
},
]
}
disableSelectionZone={true}
items={
Array [
Object {
"costType": <Text>
Current Cost
</Text>,
"daily": <Text>
$
0.19 <b>
</Text>,
"hourly": <Text>
$
0.0080
</Text>,
"monthly": <Text>
$
5.84
</Text>,
},
]
}
layoutMode={1}
onRenderRow={[Function]}
selectionMode={0}
/>
<Text
id="throughputSpendElement"
>
(
regions:
1
,
100
RU/s,
$ $
0.00008 0.0080
/RU) hourly
</Text> /
<Text> $
<em> 0.19
*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account daily
</em> /
</Text> $
</Stack> 5.84
monthly
</b>
(
regions:
1
,
100
RU/s,
$
0.00008
/RU)
</Text>
<StyledCheckboxBase <StyledCheckboxBase
checked={false} checked={false}
id="spendAckCheckBox" id="spendAckCheckBox"
@@ -434,7 +288,6 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
} }
} }
/> />
<br />
</Stack> </Stack>
</Stack> </Stack>
`; `;
@@ -458,7 +311,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }
@@ -516,19 +369,6 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
} }
} }
> >
<Text>
Estimate your required throughput with
<StyledLinkBase
href="https://cosmos.azure.com/capacitycalculator/"
target="_blank"
>
capacity calculator
<Component
iconName="NavigateExternalInline"
/>
</StyledLinkBase>
</Text>
<StyledTextFieldBase <StyledTextFieldBase
disabled={false} disabled={false}
id="throughputInput" id="throughputInput"
@@ -554,143 +394,38 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
type="number" type="number"
value="100" value="100"
/> />
<Stack <Text
styles={ id="throughputSpendElement"
Object {
"root": Object {
"width": 600,
},
}
}
tokens={
Object {
"childrenGap": 10,
}
}
> >
<StyledWithViewportComponent Estimated cost (
columns={ USD
Array [ ):
Object {
"fieldName": "costType",
"isResizable": true,
"key": "costType",
"maxWidth": 200,
"minWidth": 100,
"name": "",
"styles": Object {
"root": Object {
"selectors": Object {
":hover": Object {
"background": "transparent",
},
},
},
},
},
Object {
"fieldName": "hourly",
"isResizable": true,
"key": "hourly",
"maxWidth": 200,
"minWidth": 100,
"name": "Hourly",
"styles": Object {
"root": Object {
"selectors": Object {
":hover": Object {
"background": "transparent",
},
},
},
},
},
Object {
"fieldName": "daily",
"isResizable": true,
"key": "daily",
"maxWidth": 200,
"minWidth": 100,
"name": "Daily",
"styles": Object {
"root": Object {
"selectors": Object {
":hover": Object {
"background": "transparent",
},
},
},
},
},
Object {
"fieldName": "monthly",
"isResizable": true,
"key": "monthly",
"maxWidth": 200,
"minWidth": 100,
"name": "Monthly",
"styles": Object {
"root": Object {
"selectors": Object {
":hover": Object {
"background": "transparent",
},
},
},
},
},
]
}
disableSelectionZone={true}
items={
Array [
Object {
"costType": <Text>
Current Cost
</Text>,
"daily": <Text>
$
0.19 <b>
</Text>,
"hourly": <Text>
$
0.0080
</Text>,
"monthly": <Text>
$
5.84
</Text>,
},
]
}
layoutMode={1}
onRenderRow={[Function]}
selectionMode={0}
/>
<Text
id="throughputSpendElement"
>
(
regions:
1
,
100
RU/s,
$ $
0.00008 0.0080
/RU) hourly
</Text> /
<Text> $
<em> 0.19
*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account daily
</em> /
</Text> $
</Stack> 5.84
<br /> monthly
</b>
(
regions:
1
,
100
RU/s,
$
0.00008
/RU)
</Text>
</Stack> </Stack>
</Stack> </Stack>
`; `;

View File

@@ -16,7 +16,7 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }
@@ -40,8 +40,6 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
> >
<ThroughputInputAutoPilotV3Component <ThroughputInputAutoPilotV3Component
canExceedMaximumValue={true} canExceedMaximumValue={true}
collectionName="test"
databaseName="test"
getThroughputWarningMessage={[Function]} getThroughputWarningMessage={[Function]}
isAutoPilotSelected={false} isAutoPilotSelected={false}
isEmulator={false} isEmulator={false}
@@ -60,7 +58,6 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
spendAckChecked={false} spendAckChecked={false}
throughput={1000} throughput={1000}
throughputBaseline={1000} throughputBaseline={1000}
usageSizeInKB={100}
wasAutopilotOriginallySet={true} wasAutopilotOriginallySet={true}
/> />
<Stack <Stack

View File

@@ -136,7 +136,7 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }
@@ -412,7 +412,7 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }
@@ -952,7 +952,7 @@ exports[`SubSettingsComponent renders 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }
@@ -1228,7 +1228,7 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }

View File

@@ -101,13 +101,13 @@ export const parseConflictResolutionProcedure = (procedureFromBackEnd: string):
return procedureFromBackEnd; return procedureFromBackEnd;
}; };
export const getSanitizedInputValue = (newValueString: string, max?: number): number => { export const getSanitizedInputValue = (newValueString: string, max: number): number => {
const newValue = parseInt(newValueString); const newValue = parseInt(newValueString);
if (isNaN(newValue)) { if (isNaN(newValue)) {
return zeroValue; return zeroValue;
} }
// make sure new value does not exceed the maximum throughput // make sure new value does not exceed the maximum throughput
return max ? Math.min(newValue, max) : newValue; return Math.min(newValue, max);
}; };
export const isDirty = (current: isDirtyTypes, baseline: isDirtyTypes): boolean => { export const isDirty = (current: isDirtyTypes, baseline: isDirtyTypes): boolean => {

View File

@@ -18,13 +18,12 @@ export const collection = ({
excludedPaths: [] excludedPaths: []
}), }),
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy, uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
usageSizeInKB: ko.observable(100), quotaInfo: ko.observable<DataModels.CollectionQuotaInfo>({} as DataModels.CollectionQuotaInfo),
offer: ko.observable<DataModels.Offer>({ offer: ko.observable<DataModels.Offer>({
autoscaleMaxThroughput: undefined, autoscaleMaxThroughput: undefined,
manualThroughput: 10000, manualThroughput: 10000,
minimumThroughput: 6000, minimumThroughput: 6000,
id: "offer", id: "offer"
offerReplacePending: false
}), }),
conflictResolutionPolicy: ko.observable<DataModels.ConflictResolutionPolicy>( conflictResolutionPolicy: ko.observable<DataModels.ConflictResolutionPolicy>(
{} as DataModels.ConflictResolutionPolicy {} as DataModels.ConflictResolutionPolicy

View File

@@ -55,7 +55,6 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane", "id": "adddatabasepane",
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
@@ -105,7 +104,6 @@ exports[`SettingsComponent renders 1`] = `
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"formWarnings": [Function], "formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane", "id": "addcollectionpane",
"isAnalyticalStorageOn": [Function], "isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
@@ -135,6 +133,8 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyVisible": [Function], "partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function], "requestUnitsUsageCost": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"rupm": [Function],
"rupmVisible": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function], "shouldCreateMongoWildcardIndex": [Function],
@@ -593,7 +593,6 @@ exports[`SettingsComponent renders 1`] = `
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"formWarnings": [Function], "formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane", "id": "addcollectionpane",
"isAnalyticalStorageOn": [Function], "isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
@@ -623,6 +622,8 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyVisible": [Function], "partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function], "requestUnitsUsageCost": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"rupm": [Function],
"rupmVisible": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function], "shouldCreateMongoWildcardIndex": [Function],
@@ -668,7 +669,6 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane", "id": "adddatabasepane",
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
@@ -735,6 +735,7 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"arcadiaToken": [Function], "arcadiaToken": [Function],
"armEndpoint": [Function],
"browseQueriesPane": BrowseQueriesPane { "browseQueriesPane": BrowseQueriesPane {
"canSaveQueries": [Function], "canSaveQueries": [Function],
"container": [Circular], "container": [Circular],
@@ -946,7 +947,7 @@ exports[`SettingsComponent renders 1`] = `
"hasWriteAccess": [Function], "hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAuthWithResourceToken": [Function], "isAuthWithResourceToken": [Function],
"isAutoscaleDefaultEnabled": [Function], "isCodeOfConductEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function], "isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function], "isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
@@ -955,7 +956,6 @@ exports[`SettingsComponent renders 1`] = `
"isHostedDataExplorerEnabled": [Function], "isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function], "isLeftPaneExpanded": [Function],
"isLinkInjectionEnabled": [Function], "isLinkInjectionEnabled": [Function],
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isNotificationConsoleExpanded": [Function], "isNotificationConsoleExpanded": [Function],
@@ -1028,6 +1028,7 @@ exports[`SettingsComponent renders 1`] = `
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function], "onSwitchToConnectionString": [Function],
"onToggleKeyDown": [Function], "onToggleKeyDown": [Function],
"parentFrameDataExplorerVersion": [Function],
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
@@ -1053,6 +1054,7 @@ exports[`SettingsComponent renders 1`] = `
"titleLabel": "Select Columns", "titleLabel": "Select Columns",
"visible": [Function], "visible": [Function],
}, },
"quotaId": [Function],
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],
@@ -1120,14 +1122,6 @@ exports[`SettingsComponent renders 1`] = `
}, },
"selectedDatabaseId": [Function], "selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"selfServeComponentAdapter": SelfServeComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
"parameters": [Function],
},
"selfServeType": [Function],
"serverId": [Function], "serverId": [Function],
"settingsPane": SettingsPane { "settingsPane": SettingsPane {
"container": [Circular], "container": [Circular],
@@ -1189,9 +1183,11 @@ exports[`SettingsComponent renders 1`] = `
}, },
"direction": "vertical", "direction": "vertical",
"isCollapsed": [Function], "isCollapsed": [Function],
"leftSide": null,
"leftSideId": "resourcetree", "leftSideId": "resourcetree",
"onResizeStart": [Function], "onResizeStart": [Function],
"onResizeStop": [Function], "onResizeStop": [Function],
"splitter": null,
"splitterId": "h_splitter1", "splitterId": "h_splitter1",
}, },
"stringInputPane": StringInputPane { "stringInputPane": StringInputPane {
@@ -1304,9 +1300,9 @@ exports[`SettingsComponent renders 1`] = `
"version": 2, "version": 2,
}, },
"partitionKeyProperty": "partitionKey", "partitionKeyProperty": "partitionKey",
"quotaInfo": [Function],
"readSettings": [Function], "readSettings": [Function],
"uniqueKeyPolicy": Object {}, "uniqueKeyPolicy": Object {},
"usageSizeInKB": [Function],
} }
} }
container={ container={
@@ -1338,7 +1334,6 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane", "id": "adddatabasepane",
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
@@ -1388,7 +1383,6 @@ exports[`SettingsComponent renders 1`] = `
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"formWarnings": [Function], "formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane", "id": "addcollectionpane",
"isAnalyticalStorageOn": [Function], "isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
@@ -1418,6 +1412,8 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyVisible": [Function], "partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function], "requestUnitsUsageCost": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"rupm": [Function],
"rupmVisible": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function], "shouldCreateMongoWildcardIndex": [Function],
@@ -1876,7 +1872,6 @@ exports[`SettingsComponent renders 1`] = `
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"formWarnings": [Function], "formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane", "id": "addcollectionpane",
"isAnalyticalStorageOn": [Function], "isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
@@ -1906,6 +1901,8 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyVisible": [Function], "partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function], "requestUnitsUsageCost": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"rupm": [Function],
"rupmVisible": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function], "shouldCreateMongoWildcardIndex": [Function],
@@ -1951,7 +1948,6 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane", "id": "adddatabasepane",
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
@@ -2018,6 +2014,7 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"arcadiaToken": [Function], "arcadiaToken": [Function],
"armEndpoint": [Function],
"browseQueriesPane": BrowseQueriesPane { "browseQueriesPane": BrowseQueriesPane {
"canSaveQueries": [Function], "canSaveQueries": [Function],
"container": [Circular], "container": [Circular],
@@ -2229,7 +2226,7 @@ exports[`SettingsComponent renders 1`] = `
"hasWriteAccess": [Function], "hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAuthWithResourceToken": [Function], "isAuthWithResourceToken": [Function],
"isAutoscaleDefaultEnabled": [Function], "isCodeOfConductEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function], "isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function], "isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
@@ -2238,7 +2235,6 @@ exports[`SettingsComponent renders 1`] = `
"isHostedDataExplorerEnabled": [Function], "isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function], "isLeftPaneExpanded": [Function],
"isLinkInjectionEnabled": [Function], "isLinkInjectionEnabled": [Function],
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isNotificationConsoleExpanded": [Function], "isNotificationConsoleExpanded": [Function],
@@ -2311,6 +2307,7 @@ exports[`SettingsComponent renders 1`] = `
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function], "onSwitchToConnectionString": [Function],
"onToggleKeyDown": [Function], "onToggleKeyDown": [Function],
"parentFrameDataExplorerVersion": [Function],
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
@@ -2336,6 +2333,7 @@ exports[`SettingsComponent renders 1`] = `
"titleLabel": "Select Columns", "titleLabel": "Select Columns",
"visible": [Function], "visible": [Function],
}, },
"quotaId": [Function],
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],
@@ -2403,14 +2401,6 @@ exports[`SettingsComponent renders 1`] = `
}, },
"selectedDatabaseId": [Function], "selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"selfServeComponentAdapter": SelfServeComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
"parameters": [Function],
},
"selfServeType": [Function],
"serverId": [Function], "serverId": [Function],
"settingsPane": SettingsPane { "settingsPane": SettingsPane {
"container": [Circular], "container": [Circular],
@@ -2472,9 +2462,11 @@ exports[`SettingsComponent renders 1`] = `
}, },
"direction": "vertical", "direction": "vertical",
"isCollapsed": [Function], "isCollapsed": [Function],
"leftSide": null,
"leftSideId": "resourcetree", "leftSideId": "resourcetree",
"onResizeStart": [Function], "onResizeStart": [Function],
"onResizeStop": [Function], "onResizeStop": [Function],
"splitter": null,
"splitterId": "h_splitter1", "splitterId": "h_splitter1",
}, },
"stringInputPane": StringInputPane { "stringInputPane": StringInputPane {
@@ -2634,7 +2626,6 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane", "id": "adddatabasepane",
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
@@ -2684,7 +2675,6 @@ exports[`SettingsComponent renders 1`] = `
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"formWarnings": [Function], "formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane", "id": "addcollectionpane",
"isAnalyticalStorageOn": [Function], "isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
@@ -2714,6 +2704,8 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyVisible": [Function], "partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function], "requestUnitsUsageCost": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"rupm": [Function],
"rupmVisible": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function], "shouldCreateMongoWildcardIndex": [Function],
@@ -3172,7 +3164,6 @@ exports[`SettingsComponent renders 1`] = `
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"formWarnings": [Function], "formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane", "id": "addcollectionpane",
"isAnalyticalStorageOn": [Function], "isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
@@ -3202,6 +3193,8 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyVisible": [Function], "partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function], "requestUnitsUsageCost": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"rupm": [Function],
"rupmVisible": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function], "shouldCreateMongoWildcardIndex": [Function],
@@ -3247,7 +3240,6 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane", "id": "adddatabasepane",
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
@@ -3314,6 +3306,7 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"arcadiaToken": [Function], "arcadiaToken": [Function],
"armEndpoint": [Function],
"browseQueriesPane": BrowseQueriesPane { "browseQueriesPane": BrowseQueriesPane {
"canSaveQueries": [Function], "canSaveQueries": [Function],
"container": [Circular], "container": [Circular],
@@ -3525,7 +3518,7 @@ exports[`SettingsComponent renders 1`] = `
"hasWriteAccess": [Function], "hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAuthWithResourceToken": [Function], "isAuthWithResourceToken": [Function],
"isAutoscaleDefaultEnabled": [Function], "isCodeOfConductEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function], "isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function], "isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
@@ -3534,7 +3527,6 @@ exports[`SettingsComponent renders 1`] = `
"isHostedDataExplorerEnabled": [Function], "isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function], "isLeftPaneExpanded": [Function],
"isLinkInjectionEnabled": [Function], "isLinkInjectionEnabled": [Function],
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isNotificationConsoleExpanded": [Function], "isNotificationConsoleExpanded": [Function],
@@ -3607,6 +3599,7 @@ exports[`SettingsComponent renders 1`] = `
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function], "onSwitchToConnectionString": [Function],
"onToggleKeyDown": [Function], "onToggleKeyDown": [Function],
"parentFrameDataExplorerVersion": [Function],
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
@@ -3632,6 +3625,7 @@ exports[`SettingsComponent renders 1`] = `
"titleLabel": "Select Columns", "titleLabel": "Select Columns",
"visible": [Function], "visible": [Function],
}, },
"quotaId": [Function],
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],
@@ -3699,14 +3693,6 @@ exports[`SettingsComponent renders 1`] = `
}, },
"selectedDatabaseId": [Function], "selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"selfServeComponentAdapter": SelfServeComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
"parameters": [Function],
},
"selfServeType": [Function],
"serverId": [Function], "serverId": [Function],
"settingsPane": SettingsPane { "settingsPane": SettingsPane {
"container": [Circular], "container": [Circular],
@@ -3768,9 +3754,11 @@ exports[`SettingsComponent renders 1`] = `
}, },
"direction": "vertical", "direction": "vertical",
"isCollapsed": [Function], "isCollapsed": [Function],
"leftSide": null,
"leftSideId": "resourcetree", "leftSideId": "resourcetree",
"onResizeStart": [Function], "onResizeStart": [Function],
"onResizeStop": [Function], "onResizeStop": [Function],
"splitter": null,
"splitterId": "h_splitter1", "splitterId": "h_splitter1",
}, },
"stringInputPane": StringInputPane { "stringInputPane": StringInputPane {
@@ -3883,9 +3871,9 @@ exports[`SettingsComponent renders 1`] = `
"version": 2, "version": 2,
}, },
"partitionKeyProperty": "partitionKey", "partitionKeyProperty": "partitionKey",
"quotaInfo": [Function],
"readSettings": [Function], "readSettings": [Function],
"uniqueKeyPolicy": Object {}, "uniqueKeyPolicy": Object {},
"usageSizeInKB": [Function],
} }
} }
container={ container={
@@ -3917,7 +3905,6 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane", "id": "adddatabasepane",
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
@@ -3967,7 +3954,6 @@ exports[`SettingsComponent renders 1`] = `
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"formWarnings": [Function], "formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane", "id": "addcollectionpane",
"isAnalyticalStorageOn": [Function], "isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
@@ -3997,6 +3983,8 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyVisible": [Function], "partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function], "requestUnitsUsageCost": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"rupm": [Function],
"rupmVisible": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function], "shouldCreateMongoWildcardIndex": [Function],
@@ -4455,7 +4443,6 @@ exports[`SettingsComponent renders 1`] = `
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"formWarnings": [Function], "formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane", "id": "addcollectionpane",
"isAnalyticalStorageOn": [Function], "isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
@@ -4485,6 +4472,8 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyVisible": [Function], "partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function], "requestUnitsUsageCost": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"rupm": [Function],
"rupmVisible": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function], "shouldCreateMongoWildcardIndex": [Function],
@@ -4530,7 +4519,6 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane", "id": "adddatabasepane",
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
@@ -4597,6 +4585,7 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"arcadiaToken": [Function], "arcadiaToken": [Function],
"armEndpoint": [Function],
"browseQueriesPane": BrowseQueriesPane { "browseQueriesPane": BrowseQueriesPane {
"canSaveQueries": [Function], "canSaveQueries": [Function],
"container": [Circular], "container": [Circular],
@@ -4808,7 +4797,7 @@ exports[`SettingsComponent renders 1`] = `
"hasWriteAccess": [Function], "hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAuthWithResourceToken": [Function], "isAuthWithResourceToken": [Function],
"isAutoscaleDefaultEnabled": [Function], "isCodeOfConductEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function], "isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function], "isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
@@ -4817,7 +4806,6 @@ exports[`SettingsComponent renders 1`] = `
"isHostedDataExplorerEnabled": [Function], "isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function], "isLeftPaneExpanded": [Function],
"isLinkInjectionEnabled": [Function], "isLinkInjectionEnabled": [Function],
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isNotificationConsoleExpanded": [Function], "isNotificationConsoleExpanded": [Function],
@@ -4890,6 +4878,7 @@ exports[`SettingsComponent renders 1`] = `
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function], "onSwitchToConnectionString": [Function],
"onToggleKeyDown": [Function], "onToggleKeyDown": [Function],
"parentFrameDataExplorerVersion": [Function],
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
@@ -4915,6 +4904,7 @@ exports[`SettingsComponent renders 1`] = `
"titleLabel": "Select Columns", "titleLabel": "Select Columns",
"visible": [Function], "visible": [Function],
}, },
"quotaId": [Function],
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],
@@ -4982,14 +4972,6 @@ exports[`SettingsComponent renders 1`] = `
}, },
"selectedDatabaseId": [Function], "selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"selfServeComponentAdapter": SelfServeComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
"parameters": [Function],
},
"selfServeType": [Function],
"serverId": [Function], "serverId": [Function],
"settingsPane": SettingsPane { "settingsPane": SettingsPane {
"container": [Circular], "container": [Circular],
@@ -5051,9 +5033,11 @@ exports[`SettingsComponent renders 1`] = `
}, },
"direction": "vertical", "direction": "vertical",
"isCollapsed": [Function], "isCollapsed": [Function],
"leftSide": null,
"leftSideId": "resourcetree", "leftSideId": "resourcetree",
"onResizeStart": [Function], "onResizeStart": [Function],
"onResizeStop": [Function], "onResizeStop": [Function],
"splitter": null,
"splitterId": "h_splitter1", "splitterId": "h_splitter1",
}, },
"stringInputPane": StringInputPane { "stringInputPane": StringInputPane {

View File

@@ -60,106 +60,72 @@ exports[`SettingsUtils functions render 1`] = `
</StyledLinkBase> </StyledLinkBase>
. .
</Text> </Text>
<Stack <Text
styles={ id="throughputSpendElement"
Object {
"root": Object {
"width": 600,
},
}
}
tokens={
Object {
"childrenGap": 10,
}
}
> >
<StyledWithViewportComponent Estimated cost (
columns={ RMB
Array [ ):
Object {
"fieldName": "costType",
"isResizable": true,
"key": "costType",
"maxWidth": 200,
"minWidth": 100,
"name": "",
},
Object {
"fieldName": "hourly",
"isResizable": true,
"key": "hourly",
"maxWidth": 200,
"minWidth": 100,
"name": "Hourly",
},
Object {
"fieldName": "daily",
"isResizable": true,
"key": "daily",
"maxWidth": 200,
"minWidth": 100,
"name": "Daily",
},
Object {
"fieldName": "monthly",
"isResizable": true,
"key": "monthly",
"maxWidth": 200,
"minWidth": 100,
"name": "Monthly",
},
]
}
disableSelectionZone={true}
items={
Array [
Object {
"costType": <Text>
Current Cost
</Text>,
"daily": <Text>
$ 24.48
</Text>,
"hourly": <Text>
$ 1.02
</Text>,
"monthly": <Text>
$ 744.6
</Text>,
},
]
}
layoutMode={1}
onRenderRow={[Function]}
selectionMode={0}
/>
<Text
id="throughputSpendElement"
>
(
regions:
2 <b>
,
1000
RU/s,
¥ ¥
0.00051 1.29
/RU) hourly
</Text> /
<Text> ¥
<em> 31.06
*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account daily
</em> /
</Text> ¥
</Stack> 944.60
monthly
</b>
(
regions:
2
,
1000
RU/s,
¥
0.00051
/RU)
</Text>
<Text
id="autoscaleSpendElement"
>
Estimated monthly cost (
RMB
) is
<b>
¥
111.69
-
¥
1116.90
</b>
(
regions:
2
,
100
-
1000
RU/s,
¥
0.000765
/RU)
</Text>
<Text <Text
id="manualToAutoscaleDisclaimerElement" id="manualToAutoscaleDisclaimerElement"
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }
@@ -176,7 +142,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }
@@ -195,7 +161,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }
@@ -207,7 +173,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }
@@ -219,7 +185,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }
@@ -230,7 +196,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }
@@ -249,7 +215,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }
@@ -268,7 +234,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }
@@ -286,7 +252,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }
@@ -299,7 +265,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }
@@ -310,7 +276,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }
@@ -329,7 +295,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }
@@ -371,7 +337,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }
@@ -386,7 +352,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }
@@ -402,7 +368,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"fontSize": 14, "fontSize": 12,
}, },
} }
} }

View File

@@ -1,9 +1,9 @@
import React from "react"; import React from "react";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import { SmartUiComponent, SmartUiDescriptor, UiType } from "./SmartUiComponent"; import { SmartUiComponent, Descriptor, InputType } from "./SmartUiComponent";
describe("SmartUiComponent", () => { describe("SmartUiComponent", () => {
const exampleData: SmartUiDescriptor = { const exampleData: Descriptor = {
root: { root: {
id: "root", id: "root",
info: { info: {
@@ -24,7 +24,7 @@ describe("SmartUiComponent", () => {
max: 500, max: 500,
step: 10, step: 10,
defaultValue: 400, defaultValue: 400,
uiType: UiType.Spinner inputType: "spin"
} }
}, },
{ {
@@ -37,21 +37,7 @@ describe("SmartUiComponent", () => {
max: 500, max: 500,
step: 10, step: 10,
defaultValue: 400, defaultValue: 400,
uiType: UiType.Slider inputType: "slider"
}
},
{
id: "throughput3",
input: {
label: "Throughput (invalid)",
dataFieldName: "throughput3",
type: "boolean",
min: 400,
max: 500,
step: 10,
defaultValue: 400,
uiType: UiType.Spinner,
errorMessage: "label, truelabel and falselabel are required for boolean input 'throughput3'"
} }
}, },
{ {
@@ -78,11 +64,11 @@ describe("SmartUiComponent", () => {
input: { input: {
label: "Database", label: "Database",
dataFieldName: "database", dataFieldName: "database",
type: "object", type: "enum",
choices: [ choices: [
{ label: "Database 1", key: "db1" }, { label: "Database 1", key: "db1", value: "database1" },
{ label: "Database 2", key: "db2" }, { label: "Database 2", key: "db2", value: "database2" },
{ label: "Database 3", key: "db3" } { label: "Database 3", key: "db3", value: "database3" }
], ],
defaultKey: "db2" defaultKey: "db2"
} }
@@ -91,11 +77,12 @@ describe("SmartUiComponent", () => {
} }
}; };
it("should render", async () => { const exampleCallbacks = (newValues: Map<string, InputType>): void => {
const wrapper = shallow( console.log("New values:", newValues);
<SmartUiComponent descriptor={exampleData} currentValues={new Map()} onInputChange={undefined} /> };
);
await new Promise(resolve => setTimeout(resolve, 0)); it("should render", () => {
const wrapper = shallow(<SmartUiComponent descriptor={exampleData} onChange={exampleCallbacks} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
}); });

View File

@@ -5,9 +5,11 @@ import { SpinButton } from "office-ui-fabric-react/lib/SpinButton";
import { Dropdown, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown"; import { Dropdown, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
import { TextField } from "office-ui-fabric-react/lib/TextField"; import { TextField } from "office-ui-fabric-react/lib/TextField";
import { Text } from "office-ui-fabric-react/lib/Text"; import { Text } from "office-ui-fabric-react/lib/Text";
import { InputType } from "../../Tables/Constants";
import { RadioSwitchComponent } from "../RadioSwitchComponent/RadioSwitchComponent"; import { RadioSwitchComponent } from "../RadioSwitchComponent/RadioSwitchComponent";
import { Stack, IStackTokens } from "office-ui-fabric-react/lib/Stack"; import { Stack, IStackTokens } from "office-ui-fabric-react/lib/Stack";
import { Link, MessageBar, MessageBarType } from "office-ui-fabric-react"; import { Link, MessageBar, MessageBarType } from "office-ui-fabric-react";
import * as InputUtils from "./InputUtils"; import * as InputUtils from "./InputUtils";
import "./SmartUiComponent.less"; import "./SmartUiComponent.less";
@@ -19,16 +21,45 @@ import "./SmartUiComponent.less";
* - a descriptor of the UX. * - a descriptor of the UX.
*/ */
export type InputTypeValue = "number" | "string" | "boolean" | "object"; export type InputTypeValue = "number" | "string" | "boolean" | "enum";
export enum UiType { /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
Spinner = "Spinner", export type EnumItem = { label: string; key: string; value: any };
Slider = "Slider"
export type InputType = number | string | boolean | EnumItem;
interface BaseInput {
label: string;
dataFieldName: string;
type: InputTypeValue;
placeholder?: string;
} }
export type ChoiceItem = { label: string; key: string }; /**
* For now, this only supports integers
*/
export interface NumberInput extends BaseInput {
min?: number;
max?: number;
step: number;
defaultValue: number;
inputType: "spin" | "slider";
}
export type InputType = number | string | boolean | ChoiceItem; export interface BooleanInput extends BaseInput {
trueLabel: string;
falseLabel: string;
defaultValue: boolean;
}
export interface StringInput extends BaseInput {
defaultValue?: string;
}
export interface EnumInput extends BaseInput {
choices: EnumItem[];
defaultKey: string;
}
export interface Info { export interface Info {
message: string; message: string;
@@ -38,62 +69,28 @@ export interface Info {
}; };
} }
interface BaseInput { export type AnyInput = NumberInput | BooleanInput | StringInput | EnumInput;
label: string;
dataFieldName: string;
type: InputTypeValue;
placeholder?: string;
errorMessage?: string;
}
/** export interface Node {
* For now, this only supports integers
*/
interface NumberInput extends BaseInput {
min: number;
max: number;
step: number;
defaultValue?: number;
uiType: UiType;
}
interface BooleanInput extends BaseInput {
trueLabel: string;
falseLabel: string;
defaultValue?: boolean;
}
interface StringInput extends BaseInput {
defaultValue?: string;
}
interface ChoiceInput extends BaseInput {
choices: ChoiceItem[];
defaultKey?: string;
}
type AnyInput = NumberInput | BooleanInput | StringInput | ChoiceInput;
interface Node {
id: string; id: string;
info?: Info; info?: Info;
input?: AnyInput; input?: AnyInput;
children?: Node[]; children?: Node[];
} }
export interface SmartUiDescriptor { export interface Descriptor {
root: Node; root: Node;
} }
/************************** Component implementation starts here ************************************* */ /************************** Component implementation starts here ************************************* */
export interface SmartUiComponentProps { export interface SmartUiComponentProps {
descriptor: SmartUiDescriptor; descriptor: Descriptor;
currentValues: Map<string, InputType>; onChange: (newValues: Map<string, InputType>) => void;
onInputChange: (input: AnyInput, newValue: InputType) => void;
} }
interface SmartUiComponentState { interface SmartUiComponentState {
currentValues: Map<string, InputType>;
errors: Map<string, string>; errors: Map<string, string>;
} }
@@ -107,6 +104,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
constructor(props: SmartUiComponentProps) { constructor(props: SmartUiComponentProps) {
super(props); super(props);
this.state = { this.state = {
currentValues: new Map(),
errors: new Map() errors: new Map()
}; };
} }
@@ -115,37 +113,42 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return ( return (
<MessageBar> <MessageBar>
{info.message} {info.message}
{info.link && ( <Link href={info.link.href} target="_blank">
<Link href={info.link.href} target="_blank"> {info.link.text}
{info.link.text} </Link>
</Link>
)}
</MessageBar> </MessageBar>
); );
} }
private renderTextInput(input: StringInput): JSX.Element { private onInputChange = (newValue: string | number | boolean, dataFieldName: string) => {
const value = this.props.currentValues.get(input.dataFieldName) as string; const { currentValues } = this.state;
currentValues.set(dataFieldName, newValue);
this.setState({ currentValues }, () => this.props.onChange(this.state.currentValues));
};
private renderStringInput(input: StringInput): JSX.Element {
return ( return (
<div className="stringInputContainer"> <div className="stringInputContainer">
<TextField <div>
id={`${input.dataFieldName}-textBox-input`} <TextField
label={input.label} id={`${input.dataFieldName}-input`}
type="text" label={input.label}
value={value} type="text"
placeholder={input.placeholder} value={input.defaultValue}
onChange={(_, newValue) => this.props.onInputChange(input, newValue)} placeholder={input.placeholder}
styles={{ onChange={(_, newValue) => this.onInputChange(newValue, input.dataFieldName)}
subComponentStyles: { styles={{
label: { subComponentStyles: {
root: { label: {
...SmartUiComponent.labelStyle, root: {
fontWeight: 600 ...SmartUiComponent.labelStyle,
fontWeight: 600
}
} }
} }
} }}
}} />
/> </div>
</div> </div>
); );
} }
@@ -156,11 +159,10 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
this.setState({ errors }); this.setState({ errors });
} }
private onValidate = (input: AnyInput, value: string, min: number, max: number): string => { private onValidate = (value: string, min: number, max: number, dataFieldName: string): string => {
const newValue = InputUtils.onValidateValueChange(value, min, max); const newValue = InputUtils.onValidateValueChange(value, min, max);
const dataFieldName = input.dataFieldName;
if (newValue) { if (newValue) {
this.props.onInputChange(input, newValue); this.onInputChange(newValue, dataFieldName);
this.clearError(dataFieldName); this.clearError(dataFieldName);
return newValue.toString(); return newValue.toString();
} else { } else {
@@ -171,22 +173,20 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return undefined; return undefined;
}; };
private onIncrement = (input: AnyInput, value: string, step: number, max: number): string => { private onIncrement = (value: string, step: number, max: number, dataFieldName: string): string => {
const newValue = InputUtils.onIncrementValue(value, step, max); const newValue = InputUtils.onIncrementValue(value, step, max);
const dataFieldName = input.dataFieldName;
if (newValue) { if (newValue) {
this.props.onInputChange(input, newValue); this.onInputChange(newValue, dataFieldName);
this.clearError(dataFieldName); this.clearError(dataFieldName);
return newValue.toString(); return newValue.toString();
} }
return undefined; return undefined;
}; };
private onDecrement = (input: AnyInput, value: string, step: number, min: number): string => { private onDecrement = (value: string, step: number, min: number, dataFieldName: string): string => {
const newValue = InputUtils.onDecrementValue(value, step, min); const newValue = InputUtils.onDecrementValue(value, step, min);
const dataFieldName = input.dataFieldName;
if (newValue) { if (newValue) {
this.props.onInputChange(input, newValue); this.onInputChange(newValue, dataFieldName);
this.clearError(dataFieldName); this.clearError(dataFieldName);
return newValue.toString(); return newValue.toString();
} }
@@ -194,26 +194,18 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
}; };
private renderNumberInput(input: NumberInput): JSX.Element { private renderNumberInput(input: NumberInput): JSX.Element {
const { label, min, max, dataFieldName, step } = input; const { label, min, max, defaultValue, dataFieldName, step } = input;
const props = { const props = { label, min, max, ariaLabel: label, step };
label: label,
min: min,
max: max,
ariaLabel: label,
step: step
};
const value = this.props.currentValues.get(dataFieldName) as number; if (input.inputType === "spin") {
if (input.uiType === UiType.Spinner) {
return ( return (
<> <div>
<SpinButton <SpinButton
{...props} {...props}
id={`${input.dataFieldName}-spinner-input`} defaultValue={defaultValue.toString()}
value={value?.toString()} onValidate={newValue => this.onValidate(newValue, min, max, dataFieldName)}
onValidate={newValue => this.onValidate(input, newValue, props.min, props.max)} onIncrement={newValue => this.onIncrement(newValue, step, max, dataFieldName)}
onIncrement={newValue => this.onIncrement(input, newValue, props.step, props.max)} onDecrement={newValue => this.onDecrement(newValue, step, min, dataFieldName)}
onDecrement={newValue => this.onDecrement(input, newValue, props.step, props.min)}
labelPosition={Position.top} labelPosition={Position.top}
styles={{ styles={{
label: { label: {
@@ -225,35 +217,34 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
{this.state.errors.has(dataFieldName) && ( {this.state.errors.has(dataFieldName) && (
<MessageBar messageBarType={MessageBarType.error}>Error: {this.state.errors.get(dataFieldName)}</MessageBar> <MessageBar messageBarType={MessageBarType.error}>Error: {this.state.errors.get(dataFieldName)}</MessageBar>
)} )}
</>
);
} else if (input.uiType === UiType.Slider) {
return (
<div id={`${input.dataFieldName}-slider-input`}>
<Slider
{...props}
value={value}
onChange={newValue => this.props.onInputChange(input, newValue)}
styles={{
titleLabel: {
...SmartUiComponent.labelStyle,
fontWeight: 600
},
valueLabel: SmartUiComponent.labelStyle
}}
/>
</div> </div>
); );
} else if (input.inputType === "slider") {
return (
<Slider
// showValue={true}
// valueFormat={}
{...props}
defaultValue={defaultValue}
onChange={newValue => this.onInputChange(newValue, dataFieldName)}
styles={{
titleLabel: {
...SmartUiComponent.labelStyle,
fontWeight: 600
},
valueLabel: SmartUiComponent.labelStyle
}}
/>
);
} else { } else {
return <>Unsupported number UI type {input.uiType}</>; return <>Unsupported number input type {input.inputType}</>;
} }
} }
private renderBooleanInput(input: BooleanInput): JSX.Element { private renderBooleanInput(input: BooleanInput): JSX.Element {
const value = this.props.currentValues.get(input.dataFieldName) as boolean; const { dataFieldName } = input;
const selectedKey = value || input.defaultValue ? "true" : "false";
return ( return (
<div id={`${input.dataFieldName}-radioSwitch-input`}> <div>
<div className="inputLabelContainer"> <div className="inputLabelContainer">
<Text variant="small" nowrap className="inputLabel"> <Text variant="small" nowrap className="inputLabel">
{input.label} {input.label}
@@ -264,33 +255,41 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
{ {
label: input.falseLabel, label: input.falseLabel,
key: "false", key: "false",
onSelect: () => this.props.onInputChange(input, false) onSelect: () => this.onInputChange(false, dataFieldName)
}, },
{ {
label: input.trueLabel, label: input.trueLabel,
key: "true", key: "true",
onSelect: () => this.props.onInputChange(input, true) onSelect: () => this.onInputChange(true, dataFieldName)
} }
]} ]}
selectedKey={selectedKey} selectedKey={
(this.state.currentValues.has(dataFieldName)
? (this.state.currentValues.get(dataFieldName) as boolean)
: input.defaultValue)
? "true"
: "false"
}
/> />
</div> </div>
); );
} }
private renderChoiceInput(input: ChoiceInput): JSX.Element { private renderEnumInput(input: EnumInput): JSX.Element {
const { label, defaultKey: defaultKey, dataFieldName, choices, placeholder } = input; const { label, defaultKey, dataFieldName, choices, placeholder } = input;
const value = this.props.currentValues.get(dataFieldName) as string;
return ( return (
<Dropdown <Dropdown
id={`${input.dataFieldName}-dropown-input`}
label={label} label={label}
selectedKey={value ? value : defaultKey} selectedKey={
onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())} this.state.currentValues.has(dataFieldName)
? (this.state.currentValues.get(dataFieldName) as string)
: defaultKey
}
onChange={(_, item: IDropdownOption) => this.onInputChange(item.key.toString(), dataFieldName)}
placeholder={placeholder} placeholder={placeholder}
options={choices.map(c => ({ options={choices.map(c => ({
key: c.key, key: c.key,
text: c.label text: c.value
}))} }))}
styles={{ styles={{
label: { label: {
@@ -303,48 +302,34 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
); );
} }
private renderError(input: AnyInput): JSX.Element {
return <MessageBar messageBarType={MessageBarType.error}>Error: {input.errorMessage}</MessageBar>;
}
private renderInput(input: AnyInput): JSX.Element { private renderInput(input: AnyInput): JSX.Element {
if (input.errorMessage) {
return this.renderError(input);
}
switch (input.type) { switch (input.type) {
case "string": case "string":
return this.renderTextInput(input as StringInput); return this.renderStringInput(input as StringInput);
case "number": case "number":
return this.renderNumberInput(input as NumberInput); return this.renderNumberInput(input as NumberInput);
case "boolean": case "boolean":
return this.renderBooleanInput(input as BooleanInput); return this.renderBooleanInput(input as BooleanInput);
case "object": case "enum":
return this.renderChoiceInput(input as ChoiceInput); return this.renderEnumInput(input as EnumInput);
default: default:
throw new Error(`Unknown input type: ${input.type}`); throw new Error(`Unknown input type: ${input.type}`);
} }
} }
private renderNode(node: Node): JSX.Element { private renderNode(node: Node): JSX.Element {
const containerStackTokens: IStackTokens = { childrenGap: 15 }; const containerStackTokens: IStackTokens = { childrenGap: 10 };
return ( return (
<Stack tokens={containerStackTokens} className="widgetRendererContainer"> <Stack tokens={containerStackTokens} className="widgetRendererContainer">
<Stack.Item> {node.info && this.renderInfo(node.info)}
{node.info && this.renderInfo(node.info as Info)} {node.input && this.renderInput(node.input)}
{node.input && this.renderInput(node.input)}
</Stack.Item>
{node.children && node.children.map(child => <div key={child.id}>{this.renderNode(child)}</div>)} {node.children && node.children.map(child => <div key={child.id}>{this.renderNode(child)}</div>)}
</Stack> </Stack>
); );
} }
render(): JSX.Element { render(): JSX.Element {
const containerStackTokens: IStackTokens = { childrenGap: 20 }; return <>{this.renderNode(this.props.descriptor.root)}</>;
return (
<Stack tokens={containerStackTokens} styles={{ root: { width: 400, padding: 10 } }}>
{this.renderNode(this.props.descriptor.root)}
</Stack>
);
} }
} }

View File

@@ -1,40 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SmartUiComponent should render 1`] = ` exports[`SmartUiComponent should render 1`] = `
<Stack <Fragment>
styles={
Object {
"root": Object {
"padding": 10,
"width": 400,
},
}
}
tokens={
Object {
"childrenGap": 20,
}
}
>
<Stack <Stack
className="widgetRendererContainer" className="widgetRendererContainer"
tokens={ tokens={
Object { Object {
"childrenGap": 15, "childrenGap": 10,
} }
} }
> >
<StackItem> <StyledMessageBarBase>
<StyledMessageBarBase> Start at $24/mo per database
Start at $24/mo per database <StyledLinkBase
<StyledLinkBase href="https://aka.ms/azure-cosmos-db-pricing"
href="https://aka.ms/azure-cosmos-db-pricing" target="_blank"
target="_blank" >
> More Details
More Details </StyledLinkBase>
</StyledLinkBase> </StyledMessageBarBase>
</StyledMessageBarBase>
</StackItem>
<div <div
key="throughput" key="throughput"
> >
@@ -42,11 +26,11 @@ exports[`SmartUiComponent should render 1`] = `
className="widgetRendererContainer" className="widgetRendererContainer"
tokens={ tokens={
Object { Object {
"childrenGap": 15, "childrenGap": 10,
} }
} }
> >
<StackItem> <div>
<CustomizedSpinButton <CustomizedSpinButton
ariaLabel="Throughput (input)" ariaLabel="Throughput (input)"
decrementButtonIcon={ decrementButtonIcon={
@@ -54,8 +38,8 @@ exports[`SmartUiComponent should render 1`] = `
"iconName": "ChevronDownSmall", "iconName": "ChevronDownSmall",
} }
} }
defaultValue="400"
disabled={false} disabled={false}
id="throughput-spinner-input"
incrementButtonIcon={ incrementButtonIcon={
Object { Object {
"iconName": "ChevronUpSmall", "iconName": "ChevronUpSmall",
@@ -80,7 +64,7 @@ exports[`SmartUiComponent should render 1`] = `
} }
} }
/> />
</StackItem> </div>
</Stack> </Stack>
</div> </div>
<div <div
@@ -90,60 +74,34 @@ exports[`SmartUiComponent should render 1`] = `
className="widgetRendererContainer" className="widgetRendererContainer"
tokens={ tokens={
Object { Object {
"childrenGap": 15, "childrenGap": 10,
} }
} }
> >
<StackItem> <StyledSliderBase
<div ariaLabel="Throughput (Slider)"
id="throughput2-slider-input" defaultValue={400}
> label="Throughput (Slider)"
<StyledSliderBase max={500}
ariaLabel="Throughput (Slider)" min={400}
label="Throughput (Slider)" onChange={[Function]}
max={500} step={10}
min={400} styles={
onChange={[Function]} Object {
step={10} "titleLabel": Object {
styles={ "color": "#393939",
Object { "fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"titleLabel": Object { "fontSize": 12,
"color": "#393939", "fontWeight": 600,
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", },
"fontSize": 12, "valueLabel": Object {
"fontWeight": 600, "color": "#393939",
}, "fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"valueLabel": Object { "fontSize": 12,
"color": "#393939", },
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", }
"fontSize": 12,
},
}
}
/>
</div>
</StackItem>
</Stack>
</div>
<div
key="throughput3"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 15,
} }
} />
>
<StackItem>
<StyledMessageBarBase
messageBarType={1}
>
Error:
label, truelabel and falselabel are required for boolean input 'throughput3'
</StyledMessageBarBase>
</StackItem>
</Stack> </Stack>
</div> </div>
<div <div
@@ -153,16 +111,16 @@ exports[`SmartUiComponent should render 1`] = `
className="widgetRendererContainer" className="widgetRendererContainer"
tokens={ tokens={
Object { Object {
"childrenGap": 15, "childrenGap": 10,
} }
} }
> >
<StackItem> <div
<div className="stringInputContainer"
className="stringInputContainer" >
> <div>
<StyledTextFieldBase <StyledTextFieldBase
id="containerId-textBox-input" id="containerId-input"
label="Container id" label="Container id"
onChange={[Function]} onChange={[Function]}
styles={ styles={
@@ -182,7 +140,7 @@ exports[`SmartUiComponent should render 1`] = `
type="text" type="text"
/> />
</div> </div>
</StackItem> </div>
</Stack> </Stack>
</div> </div>
<div <div
@@ -192,44 +150,40 @@ exports[`SmartUiComponent should render 1`] = `
className="widgetRendererContainer" className="widgetRendererContainer"
tokens={ tokens={
Object { Object {
"childrenGap": 15, "childrenGap": 10,
} }
} }
> >
<StackItem> <div>
<div <div
id="analyticalStore-radioSwitch-input" className="inputLabelContainer"
> >
<div <Text
className="inputLabelContainer" className="inputLabel"
nowrap={true}
variant="small"
> >
<Text Analytical Store
className="inputLabel" </Text>
nowrap={true}
variant="small"
>
Analytical Store
</Text>
</div>
<RadioSwitchComponent
choices={
Array [
Object {
"key": "false",
"label": "Disabled",
"onSelect": [Function],
},
Object {
"key": "true",
"label": "Enabled",
"onSelect": [Function],
},
]
}
selectedKey="true"
/>
</div> </div>
</StackItem> <RadioSwitchComponent
choices={
Array [
Object {
"key": "false",
"label": "Disabled",
"onSelect": [Function],
},
Object {
"key": "true",
"label": "Enabled",
"onSelect": [Function],
},
]
}
selectedKey="true"
/>
</div>
</Stack> </Stack>
</div> </div>
<div <div
@@ -239,51 +193,48 @@ exports[`SmartUiComponent should render 1`] = `
className="widgetRendererContainer" className="widgetRendererContainer"
tokens={ tokens={
Object { Object {
"childrenGap": 15, "childrenGap": 10,
} }
} }
> >
<StackItem> <StyledWithResponsiveMode
<StyledWithResponsiveMode label="Database"
id="database-dropown-input" onChange={[Function]}
label="Database" options={
onChange={[Function]} Array [
options={
Array [
Object {
"key": "db1",
"text": "Database 1",
},
Object {
"key": "db2",
"text": "Database 2",
},
Object {
"key": "db3",
"text": "Database 3",
},
]
}
selectedKey="db2"
styles={
Object { Object {
"dropdown": Object { "key": "db1",
"color": "#393939", "text": "database1",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", },
"fontSize": 12, Object {
}, "key": "db2",
"label": Object { "text": "database2",
"color": "#393939", },
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", Object {
"fontSize": 12, "key": "db3",
"fontWeight": 600, "text": "database3",
}, },
} ]
}
selectedKey="db2"
styles={
Object {
"dropdown": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
},
"label": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
} }
/> }
</StackItem> />
</Stack> </Stack>
</div> </div>
</Stack> </Stack>
</Stack> </Fragment>
`; `;

View File

@@ -5,9 +5,6 @@ import ThroughputInputComponentAutoscaleV3 from "./ThroughputInputComponentAutos
import { KeyCodes } from "../../../Common/Constants"; import { KeyCodes } from "../../../Common/Constants";
import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel"; import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel";
import { userContext } from "../../../UserContext";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
/** /**
* Throughput Input: * Throughput Input:
* *
@@ -132,8 +129,6 @@ export interface ThroughputInputParams {
showAutoPilot?: ko.Observable<boolean>; showAutoPilot?: ko.Observable<boolean>;
overrideWithAutoPilotSettings: ko.Observable<boolean>; overrideWithAutoPilotSettings: ko.Observable<boolean>;
overrideWithProvisionedThroughputSettings: ko.Observable<boolean>; overrideWithProvisionedThroughputSettings: ko.Observable<boolean>;
freeTierExceedThroughputTooltip?: ko.Observable<string>;
freeTierExceedThroughputWarning?: ko.Observable<string>;
} }
export class ThroughputInputViewModel extends WaitsForTemplateViewModel { export class ThroughputInputViewModel extends WaitsForTemplateViewModel {
@@ -170,10 +165,6 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel {
public overrideWithProvisionedThroughputSettings: ko.Observable<boolean>; public overrideWithProvisionedThroughputSettings: ko.Observable<boolean>;
public isManualThroughputInputFieldRequired: ko.Computed<boolean>; public isManualThroughputInputFieldRequired: ko.Computed<boolean>;
public isAutoscaleThroughputInputFieldRequired: ko.Computed<boolean>; public isAutoscaleThroughputInputFieldRequired: ko.Computed<boolean>;
public freeTierExceedThroughputTooltip: ko.Observable<string>;
public freeTierExceedThroughputWarning: ko.Observable<string>;
public showFreeTierExceedThroughputTooltip: ko.Computed<boolean>;
public showFreeTierExceedThroughputWarning: ko.Computed<boolean>;
public constructor(options: ThroughputInputParams) { public constructor(options: ThroughputInputParams) {
super(); super();
@@ -204,16 +195,6 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel {
this.label = options.label || ko.observable<string>(); this.label = options.label || ko.observable<string>();
this.showAutoPilot = options.showAutoPilot !== undefined ? options.showAutoPilot : ko.observable<boolean>(true); this.showAutoPilot = options.showAutoPilot !== undefined ? options.showAutoPilot : ko.observable<boolean>(true);
this.isAutoPilotSelected = options.isAutoPilotSelected || ko.observable<boolean>(false); this.isAutoPilotSelected = options.isAutoPilotSelected || ko.observable<boolean>(false);
this.isAutoPilotSelected.subscribe(value => {
TelemetryProcessor.trace(Action.ToggleAutoscaleSetting, ActionModifiers.Mark, {
changedSelectedValueTo: value ? ActionModifiers.ToggleAutoscaleOn : ActionModifiers.ToggleAutoscaleOff,
databaseAccountName: userContext.databaseAccount?.name,
subscriptionId: userContext.subscriptionId,
apiKind: userContext.defaultExperience,
dataExplorerArea: "Scale Tab V1"
});
});
this.throughputAutoPilotRadioId = options.throughputAutoPilotRadioId; this.throughputAutoPilotRadioId = options.throughputAutoPilotRadioId;
this.throughputProvisionedRadioId = options.throughputProvisionedRadioId; this.throughputProvisionedRadioId = options.throughputProvisionedRadioId;
this.throughputModeRadioName = options.throughputModeRadioName; this.throughputModeRadioName = options.throughputModeRadioName;
@@ -238,16 +219,6 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel {
this.isAutoscaleThroughputInputFieldRequired = ko.pureComputed( this.isAutoscaleThroughputInputFieldRequired = ko.pureComputed(
() => this.isEnabled() && this.isAutoPilotSelected() () => this.isEnabled() && this.isAutoPilotSelected()
); );
this.freeTierExceedThroughputTooltip = options.freeTierExceedThroughputTooltip || ko.observable<string>();
this.freeTierExceedThroughputWarning = options.freeTierExceedThroughputWarning || ko.observable<string>();
this.showFreeTierExceedThroughputTooltip = ko.pureComputed<boolean>(
() => !!this.freeTierExceedThroughputTooltip() && this.value() > 400
);
this.showFreeTierExceedThroughputWarning = ko.pureComputed<boolean>(
() => !!this.freeTierExceedThroughputWarning() && this.value() > 400
);
} }
public decreaseThroughput() { public decreaseThroughput() {

View File

@@ -126,20 +126,6 @@
</div> </div>
<div data-bind="visible: !isAutoPilotSelected()"> <div data-bind="visible: !isAutoPilotSelected()">
<p>
<span
>Estimate your required throughput with
<a target="_blank" href="https://cosmos.azure.com/capacitycalculator/">capacity calculator</a></span
>
</p>
<div class="inputTooltip">
<span
data-bind="text: freeTierExceedThroughputTooltip, visible: showFreeTierExceedThroughputTooltip"
class="inputTooltipText"
></span>
</div>
<div data-bind="setTemplateReady: true"> <div data-bind="setTemplateReady: true">
<input <input
data-bind=" data-bind="
@@ -162,11 +148,6 @@
/> />
</div> </div>
<div class="freeTierInlineWarning" data-bind="visible: showFreeTierExceedThroughputWarning">
<span class="freeTierWarningIcon"><img src="/warning.svg" alt="Warning"/></span>
<span class="freeTierWarningMessage" data-bind="text: freeTierExceedThroughputWarning"></span>
</div>
<p data-bind="visible: costsVisible"> <p data-bind="visible: costsVisible">
<span data-bind="html: requestUnitsUsageCost"></span> <span data-bind="html: requestUnitsUsageCost"></span>
</p> </p>

View File

@@ -1,11 +1,11 @@
jest.mock("../../Common/DocumentClientUtilityBase");
jest.mock("../Graph/GraphExplorerComponent/GremlinClient"); jest.mock("../Graph/GraphExplorerComponent/GremlinClient");
jest.mock("../../Common/dataAccess/createCollection"); jest.mock("../../Common/dataAccess/createCollection");
jest.mock("../../Common/dataAccess/createDocument");
import * as ko from "knockout"; import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import Q from "q"; import Q from "q";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import { createDocument } from "../../Common/dataAccess/createDocument"; import { createDocument } from "../../Common/DocumentClientUtilityBase";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { updateUserContext } from "../../UserContext"; import { updateUserContext } from "../../UserContext";

View File

@@ -4,8 +4,8 @@ import GraphTab from ".././Tabs/GraphTab";
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient"; import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { createDocument } from "../../Common/DocumentClientUtilityBase";
import { createCollection } from "../../Common/dataAccess/createCollection"; import { createCollection } from "../../Common/dataAccess/createCollection";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
interface SampleDataFile extends DataModels.CreateCollectionParams { interface SampleDataFile extends DataModels.CreateCollectionParams {
@@ -95,15 +95,12 @@ export class ContainerSampleGenerator {
.reduce((previous, current) => previous.then(current), Promise.resolve()); .reduce((previous, current) => previous.then(current), Promise.resolve());
} else { } else {
// For SQL all queries are executed at the same time // For SQL all queries are executed at the same time
await Promise.all( this.sampleDataFile.data.map(doc => {
this.sampleDataFile.data.map(async doc => { const subPromise = createDocument(collection, doc);
try { subPromise.catch(reason => NotificationConsoleUtils.logConsoleError(reason));
await createDocument(collection, doc); promises.push(subPromise);
} catch (error) { });
NotificationConsoleUtils.logConsoleError(error); await Promise.all(promises);
}
})
);
} }
} }

View File

@@ -18,7 +18,7 @@ import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPa
import { readCollection } from "../Common/dataAccess/readCollection"; import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases"; import { readDatabases } from "../Common/dataAccess/readDatabases";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane"; import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility"; import EnvironmentUtility from "../Common/EnvironmentUtility";
import GraphStylingPane from "./Panes/GraphStylingPane"; import GraphStylingPane from "./Panes/GraphStylingPane";
import hasher from "hasher"; import hasher from "hasher";
import NewVertexPane from "./Panes/NewVertexPane"; import NewVertexPane from "./Panes/NewVertexPane";
@@ -88,9 +88,6 @@ import { stringToBlob } from "../Utils/BlobUtils";
import { IChoiceGroupProps } from "office-ui-fabric-react"; import { IChoiceGroupProps } from "office-ui-fabric-react";
import { getErrorMessage, handleError, getErrorStack } from "../Common/ErrorHandlingUtils"; import { getErrorMessage, handleError, getErrorStack } from "../Common/ErrorHandlingUtils";
import { SubscriptionType } from "../Contracts/SubscriptionType"; import { SubscriptionType } from "../Contracts/SubscriptionType";
import { SelfServeLoadingComponentAdapter } from "../SelfServe/SelfServeLoadingComponentAdapter";
import { SelfServeType } from "../SelfServe/SelfServeUtils";
import { SelfServeComponentAdapter } from "../SelfServe/SelfServeComponentAdapter";
BindingHandlersRegisterer.registerBindingHandlers(); BindingHandlersRegisterer.registerBindingHandlers();
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import // Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
@@ -124,6 +121,7 @@ export default class Explorer {
public databaseAccount: ko.Observable<DataModels.DatabaseAccount>; public databaseAccount: ko.Observable<DataModels.DatabaseAccount>;
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults; public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults;
public subscriptionType: ko.Observable<SubscriptionType>; public subscriptionType: ko.Observable<SubscriptionType>;
public quotaId: ko.Observable<string>;
public defaultExperience: ko.Observable<string>; public defaultExperience: ko.Observable<string>;
public isPreferredApiDocumentDB: ko.Computed<boolean>; public isPreferredApiDocumentDB: ko.Computed<boolean>;
public isPreferredApiCassandra: ko.Computed<boolean>; public isPreferredApiCassandra: ko.Computed<boolean>;
@@ -134,14 +132,15 @@ export default class Explorer {
public isEnableMongoCapabilityPresent: ko.Computed<boolean>; public isEnableMongoCapabilityPresent: ko.Computed<boolean>;
public isServerlessEnabled: ko.Computed<boolean>; public isServerlessEnabled: ko.Computed<boolean>;
public isAccountReady: ko.Observable<boolean>; public isAccountReady: ko.Observable<boolean>;
public selfServeType: ko.Observable<SelfServeType>;
public canSaveQueries: ko.Computed<boolean>; public canSaveQueries: ko.Computed<boolean>;
public features: ko.Observable<any>; public features: ko.Observable<any>;
public serverId: ko.Observable<string>; public serverId: ko.Observable<string>;
public armEndpoint: ko.Observable<string>;
public isTryCosmosDBSubscription: ko.Observable<boolean>; public isTryCosmosDBSubscription: ko.Observable<boolean>;
public queriesClient: QueriesClient; public queriesClient: QueriesClient;
public tableDataClient: TableDataClient; public tableDataClient: TableDataClient;
public splitter: Splitter; public splitter: Splitter;
public parentFrameDataExplorerVersion: ko.Observable<string> = ko.observable<string>("");
public mostRecentActivity: MostRecentActivity.MostRecentActivity; public mostRecentActivity: MostRecentActivity.MostRecentActivity;
// Notification Console // Notification Console
@@ -160,7 +159,6 @@ export default class Explorer {
public selectedNode: ko.Observable<ViewModels.TreeNode>; public selectedNode: ko.Observable<ViewModels.TreeNode>;
public isRefreshingExplorer: ko.Observable<boolean>; public isRefreshingExplorer: ko.Observable<boolean>;
private resourceTree: ResourceTreeAdapter; private resourceTree: ResourceTreeAdapter;
private selfServeComponentAdapter: SelfServeComponentAdapter;
// Resource Token // Resource Token
public resourceTokenDatabaseId: ko.Observable<string>; public resourceTokenDatabaseId: ko.Observable<string>;
@@ -206,15 +204,14 @@ export default class Explorer {
// features // features
public isGalleryPublishEnabled: ko.Computed<boolean>; public isGalleryPublishEnabled: ko.Computed<boolean>;
public isCodeOfConductEnabled: ko.Computed<boolean>;
public isLinkInjectionEnabled: ko.Computed<boolean>; public isLinkInjectionEnabled: ko.Computed<boolean>;
public isGitHubPaneEnabled: ko.Observable<boolean>; public isGitHubPaneEnabled: ko.Observable<boolean>;
public isPublishNotebookPaneEnabled: ko.Observable<boolean>; public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
public isCopyNotebookPaneEnabled: ko.Observable<boolean>; public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
public isHostedDataExplorerEnabled: ko.Computed<boolean>; public isHostedDataExplorerEnabled: ko.Computed<boolean>;
public isRightPanelV2Enabled: ko.Computed<boolean>; public isRightPanelV2Enabled: ko.Computed<boolean>;
public isMongoIndexingEnabled: ko.Observable<boolean>;
public canExceedMaximumValue: ko.Computed<boolean>; public canExceedMaximumValue: ko.Computed<boolean>;
public isAutoscaleDefaultEnabled: ko.Observable<boolean>;
public shouldShowShareDialogContents: ko.Observable<boolean>; public shouldShowShareDialogContents: ko.Observable<boolean>;
public shareAccessData: ko.Observable<AdHocAccessData>; public shareAccessData: ko.Observable<AdHocAccessData>;
@@ -264,7 +261,6 @@ export default class Explorer {
private _dialogProps: ko.Observable<DialogProps>; private _dialogProps: ko.Observable<DialogProps>;
private addSynapseLinkDialog: DialogComponentAdapter; private addSynapseLinkDialog: DialogComponentAdapter;
private _addSynapseLinkDialogProps: ko.Observable<DialogProps>; private _addSynapseLinkDialogProps: ko.Observable<DialogProps>;
private selfServeLoadingComponentAdapter: SelfServeLoadingComponentAdapter;
private static readonly MaxNbDatabasesToAutoExpand = 5; private static readonly MaxNbDatabasesToAutoExpand = 5;
@@ -283,6 +279,7 @@ export default class Explorer {
this.databaseAccount = ko.observable<DataModels.DatabaseAccount>(); this.databaseAccount = ko.observable<DataModels.DatabaseAccount>();
this.subscriptionType = ko.observable<SubscriptionType>(SharedConstants.CollectionCreation.DefaultSubscriptionType); this.subscriptionType = ko.observable<SubscriptionType>(SharedConstants.CollectionCreation.DefaultSubscriptionType);
this.quotaId = ko.observable<string>("");
let firstInitialization = true; let firstInitialization = true;
this.isRefreshingExplorer = ko.observable<boolean>(true); this.isRefreshingExplorer = ko.observable<boolean>(true);
this.isRefreshingExplorer.subscribe((isRefreshing: boolean) => { this.isRefreshingExplorer.subscribe((isRefreshing: boolean) => {
@@ -300,7 +297,6 @@ export default class Explorer {
} }
}); });
this.isAccountReady = ko.observable<boolean>(false); this.isAccountReady = ko.observable<boolean>(false);
this.selfServeType = ko.observable<SelfServeType>(undefined);
this._isInitializingNotebooks = false; this._isInitializingNotebooks = false;
this._isInitializingSparkConnectionInfo = false; this._isInitializingSparkConnectionInfo = false;
this.arcadiaToken = ko.observable<string>(); this.arcadiaToken = ko.observable<string>();
@@ -323,9 +319,9 @@ export default class Explorer {
if (isAccountReady) { if (isAccountReady) {
this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(true); this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(true);
RouteHandler.getInstance().initHandler(); RouteHandler.getInstance().initHandler();
this.notebookWorkspaceManager = new NotebookWorkspaceManager(); this.notebookWorkspaceManager = new NotebookWorkspaceManager(this.armEndpoint());
this.arcadiaWorkspaces = ko.observableArray(); this.arcadiaWorkspaces = ko.observableArray();
this._arcadiaManager = new ArcadiaResourceManager(); this._arcadiaManager = new ArcadiaResourceManager(this.armEndpoint());
this._isAfecFeatureRegistered(Constants.AfecFeatures.StorageAnalytics).then(isRegistered => this._isAfecFeatureRegistered(Constants.AfecFeatures.StorageAnalytics).then(isRegistered =>
this.hasStorageAnalyticsAfecFeature(isRegistered) this.hasStorageAnalyticsAfecFeature(isRegistered)
); );
@@ -375,6 +371,7 @@ export default class Explorer {
this.features = ko.observable(); this.features = ko.observable();
this.serverId = ko.observable<string>(); this.serverId = ko.observable<string>();
this.armEndpoint = ko.observable<string>(undefined);
this.queriesClient = new QueriesClient(this); this.queriesClient = new QueriesClient(this);
this.isTryCosmosDBSubscription = ko.observable<boolean>(false); this.isTryCosmosDBSubscription = ko.observable<boolean>(false);
@@ -407,11 +404,13 @@ export default class Explorer {
this.isGalleryPublishEnabled = ko.computed<boolean>(() => this.isGalleryPublishEnabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableGalleryPublish) this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
); );
this.isCodeOfConductEnabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableCodeOfConduct)
);
this.isLinkInjectionEnabled = ko.computed<boolean>(() => this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableLinkInjection) this.isFeatureEnabled(Constants.Features.enableLinkInjection)
); );
this.isGitHubPaneEnabled = ko.observable<boolean>(false); this.isGitHubPaneEnabled = ko.observable<boolean>(false);
this.isMongoIndexingEnabled = ko.observable<boolean>(false);
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false); this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false); this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
@@ -422,8 +421,6 @@ export default class Explorer {
this.isSchemaEnabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSchema)); this.isSchemaEnabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSchema));
this.isNotificationConsoleExpanded = ko.observable<boolean>(false); this.isNotificationConsoleExpanded = ko.observable<boolean>(false);
this.isAutoscaleDefaultEnabled = ko.observable<boolean>(false);
this.databases = ko.observableArray<ViewModels.Database>(); this.databases = ko.observableArray<ViewModels.Database>();
this.canSaveQueries = ko.computed<boolean>(() => { this.canSaveQueries = ko.computed<boolean>(() => {
const savedQueriesDatabase: ViewModels.Database = _.find( const savedQueriesDatabase: ViewModels.Database = _.find(
@@ -705,7 +702,6 @@ export default class Explorer {
}); });
this.uploadItemsPaneAdapter = new UploadItemsPaneAdapter(this); this.uploadItemsPaneAdapter = new UploadItemsPaneAdapter(this);
this.selfServeComponentAdapter = new SelfServeComponentAdapter(this);
this.loadQueryPane = new LoadQueryPane({ this.loadQueryPane = new LoadQueryPane({
id: "loadquerypane", id: "loadquerypane",
@@ -881,7 +877,6 @@ export default class Explorer {
}); });
this.commandBarComponentAdapter = new CommandBarComponentAdapter(this); this.commandBarComponentAdapter = new CommandBarComponentAdapter(this);
this.selfServeLoadingComponentAdapter = new SelfServeLoadingComponentAdapter();
this.notificationConsoleComponentAdapter = new NotificationConsoleComponentAdapter(this); this.notificationConsoleComponentAdapter = new NotificationConsoleComponentAdapter(this);
this._initSettings(); this._initSettings();
@@ -1021,7 +1016,9 @@ export default class Explorer {
this.isSynapseLinkUpdating(true); this.isSynapseLinkUpdating(true);
this._closeSynapseLinkModalDialog(); this._closeSynapseLinkModalDialog();
const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(this.databaseAccount().id); const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate(
this.databaseAccount().id
);
try { try {
const databaseAccount: DataModels.DatabaseAccount = await resourceProviderClient.patchAsync( const databaseAccount: DataModels.DatabaseAccount = await resourceProviderClient.patchAsync(
@@ -1761,59 +1758,61 @@ export default class Explorer {
inputs.extensionEndpoint = configContext.PROXY_PATH; inputs.extensionEndpoint = configContext.PROXY_PATH;
} }
this.initDataExplorerWithFrameInputs(inputs); const initPromise: Q.Promise<void> = inputs ? this.initDataExplorerWithFrameInputs(inputs) : Q();
const openAction: ActionContracts.DataExplorerAction = message.openAction; initPromise.then(() => {
if (!!openAction) { const openAction: ActionContracts.DataExplorerAction = message.openAction;
if (this.isRefreshingExplorer()) { if (!!openAction) {
const subscription = this.databases.subscribe((databases: ViewModels.Database[]) => { if (this.isRefreshingExplorer()) {
const subscription = this.databases.subscribe((databases: ViewModels.Database[]) => {
handleOpenAction(openAction, this.nonSystemDatabases(), this);
subscription.dispose();
});
} else {
handleOpenAction(openAction, this.nonSystemDatabases(), this); handleOpenAction(openAction, this.nonSystemDatabases(), this);
subscription.dispose(); }
});
} else {
handleOpenAction(openAction, this.nonSystemDatabases(), this);
} }
} if (message.actionType === ActionContracts.ActionType.TransmitCachedData) {
if (message.actionType === ActionContracts.ActionType.TransmitCachedData) { handleCachedDataMessage(message);
handleCachedDataMessage(message); return;
return; }
} if (message.type) {
if (message.type) { switch (message.type) {
switch (message.type) { case MessageTypes.UpdateLocationHash:
case MessageTypes.UpdateLocationHash: if (!message.locationHash) {
if (!message.locationHash) { break;
break; }
} hasher.replaceHash(message.locationHash);
hasher.replaceHash(message.locationHash); RouteHandler.getInstance().parseHash(message.locationHash);
RouteHandler.getInstance().parseHash(message.locationHash); break;
break; case MessageTypes.SendNotification:
case MessageTypes.SendNotification: if (!message.message) {
if (!message.message) { break;
break; }
} NotificationConsoleUtils.logConsoleMessage(
NotificationConsoleUtils.logConsoleMessage( message.consoleDataType || ConsoleDataType.Info,
message.consoleDataType || ConsoleDataType.Info, message.message,
message.message, message.id
message.id );
); break;
break; case MessageTypes.ClearNotification:
case MessageTypes.ClearNotification: if (!message.id) {
if (!message.id) { break;
break; }
} NotificationConsoleUtils.clearInProgressMessageWithId(message.id);
NotificationConsoleUtils.clearInProgressMessageWithId(message.id); break;
break; case MessageTypes.LoadingStatus:
case MessageTypes.LoadingStatus: if (!message.text) {
if (!message.text) { break;
break; }
} this._setLoadingStatusText(message.text, message.title);
this._setLoadingStatusText(message.text, message.title); break;
break; }
return;
} }
return;
}
this.splashScreenAdapter.forceRender(); this.splashScreenAdapter.forceRender();
});
} }
public findSelectedDatabase(): ViewModels.Database { public findSelectedDatabase(): ViewModels.Database {
@@ -1853,28 +1852,8 @@ export default class Explorer {
return false; return false;
} }
public setSelfServeType(inputs: ViewModels.DataExplorerInputsFrame): void { public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): Q.Promise<void> {
const selfServeFeature = inputs.features[Constants.Features.selfServeType];
if (selfServeFeature) {
// self serve type received from query string
const selfServeType = SelfServeType[selfServeFeature?.toLowerCase() as keyof typeof SelfServeType];
this.selfServeType(selfServeType ? selfServeType : SelfServeType.invalid);
} else if (inputs.selfServeType) {
// self serve type received from portal
this.selfServeType(inputs.selfServeType);
} else {
this.selfServeType(SelfServeType.none);
}
}
public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): void {
if (inputs != null) { 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 authorizationToken = inputs.authorizationToken || "";
const masterKey = inputs.masterKey || ""; const masterKey = inputs.masterKey || "";
const databaseAccount = inputs.databaseAccount || null; const databaseAccount = inputs.databaseAccount || null;
@@ -1883,19 +1862,25 @@ export default class Explorer {
} }
this.features(inputs.features); this.features(inputs.features);
this.serverId(inputs.serverId); this.serverId(inputs.serverId);
this.armEndpoint(EnvironmentUtility.normalizeArmEndpointUri(inputs.csmEndpoint || configContext.ARM_ENDPOINT));
this.databaseAccount(databaseAccount); this.databaseAccount(databaseAccount);
this.subscriptionType(inputs.subscriptionType); this.subscriptionType(inputs.subscriptionType);
this.quotaId(inputs.quotaId);
this.hasWriteAccess(inputs.hasWriteAccess); this.hasWriteAccess(inputs.hasWriteAccess);
this.flight(inputs.addCollectionDefaultFlight); this.flight(inputs.addCollectionDefaultFlight);
this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription); this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription);
this.isAuthWithResourceToken(inputs.isAuthWithresourceToken); this.isAuthWithResourceToken(inputs.isAuthWithresourceToken);
this.setFeatureFlagsFromFlights(inputs.flights); this.setFeatureFlagsFromFlights(inputs.flights);
this.setSelfServeType(inputs);
if (!!inputs.dataExplorerVersion) {
this.parentFrameDataExplorerVersion(inputs.dataExplorerVersion);
}
this._importExplorerConfigComplete = true; this._importExplorerConfigComplete = true;
updateConfigContext({ updateConfigContext({
BACKEND_ENDPOINT: inputs.extensionEndpoint || "", BACKEND_ENDPOINT: inputs.extensionEndpoint || "",
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT) ARM_ENDPOINT: this.armEndpoint()
}); });
updateUserContext({ updateUserContext({
@@ -1904,8 +1889,7 @@ export default class Explorer {
databaseAccount, databaseAccount,
resourceGroup: inputs.resourceGroup, resourceGroup: inputs.resourceGroup,
subscriptionId: inputs.subscriptionId, subscriptionId: inputs.subscriptionId,
subscriptionType: inputs.subscriptionType, subscriptionType: inputs.subscriptionType
quotaId: inputs.quotaId
}); });
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.LoadDatabaseAccount, Action.LoadDatabaseAccount,
@@ -1919,18 +1903,13 @@ export default class Explorer {
this.isAccountReady(true); this.isAccountReady(true);
} }
return Q();
} }
public setFeatureFlagsFromFlights(flights: readonly string[]): void { public setFeatureFlagsFromFlights(flights: readonly string[]): void {
if (!flights) { if (!flights) {
return; return;
} }
if (flights.indexOf(Constants.Flights.AutoscaleTest) !== -1) {
this.isAutoscaleDefaultEnabled(true);
}
if (flights.indexOf(Constants.Flights.MongoIndexing) !== -1) {
this.isMongoIndexingEnabled(true);
}
} }
public findSelectedCollection(): ViewModels.Collection { public findSelectedCollection(): ViewModels.Collection {
@@ -2297,6 +2276,7 @@ export default class Explorer {
name, name,
content, content,
parentDomElement, parentDomElement,
this.isCodeOfConductEnabled(),
this.isLinkInjectionEnabled() this.isLinkInjectionEnabled()
); );
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter; this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
@@ -2587,7 +2567,7 @@ export default class Explorer {
public _refreshSparkEnabledStateForAccount = async (): Promise<void> => { public _refreshSparkEnabledStateForAccount = async (): Promise<void> => {
const subscriptionId = userContext.subscriptionId; const subscriptionId = userContext.subscriptionId;
const armEndpoint = configContext.ARM_ENDPOINT; const armEndpoint = this.armEndpoint();
const authType = window.authType as AuthType; const authType = window.authType as AuthType;
if (!subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) { if (!subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
// explorer is not aware of the database account yet // explorer is not aware of the database account yet
@@ -2596,7 +2576,7 @@ export default class Explorer {
} }
const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${Constants.AfecFeatures.Spark}`; const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${Constants.AfecFeatures.Spark}`;
const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri); const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate(featureUri);
try { try {
const sparkNotebooksFeature: DataModels.AfecFeature = await resourceProviderClient.getAsync( const sparkNotebooksFeature: DataModels.AfecFeature = await resourceProviderClient.getAsync(
featureUri, featureUri,
@@ -2616,7 +2596,7 @@ export default class Explorer {
public _isAfecFeatureRegistered = async (featureName: string): Promise<boolean> => { public _isAfecFeatureRegistered = async (featureName: string): Promise<boolean> => {
const subscriptionId = userContext.subscriptionId; const subscriptionId = userContext.subscriptionId;
const armEndpoint = configContext.ARM_ENDPOINT; const armEndpoint = this.armEndpoint();
const authType = window.authType as AuthType; const authType = window.authType as AuthType;
if (!featureName || !subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) { if (!featureName || !subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
// explorer is not aware of the database account yet // explorer is not aware of the database account yet
@@ -2624,7 +2604,7 @@ export default class Explorer {
} }
const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${featureName}`; const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${featureName}`;
const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri); const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate(featureUri);
try { try {
const featureStatus: DataModels.AfecFeature = await resourceProviderClient.getAsync( const featureStatus: DataModels.AfecFeature = await resourceProviderClient.getAsync(
featureUri, featureUri,
@@ -3049,25 +3029,4 @@ 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;
});
}
} }

View File

@@ -11,9 +11,7 @@ export class ArraysByKeyCache<T> {
public constructor(maxNbElements: number) { public constructor(maxNbElements: number) {
this.maxNbElements = maxNbElements; this.maxNbElements = maxNbElements;
this.keyQueue = []; this.clear();
this.cache = {};
this.totalElements = 0;
} }
public clear(): void { public clear(): void {
@@ -60,7 +58,7 @@ export class ArraysByKeyCache<T> {
* @param startIndex * @param startIndex
* @param pageSize * @param pageSize
*/ */
public retrieve(key: string, startIndex: number, pageSize: number): T[] | null { public retrieve(key: string, startIndex: number, pageSize: number): T[] {
if (!this.cache.hasOwnProperty(key)) { if (!this.cache.hasOwnProperty(key)) {
return null; return null;
} }
@@ -79,10 +77,8 @@ export class ArraysByKeyCache<T> {
private reduceCacheSize(): void { private reduceCacheSize(): void {
// remove an key and its array // remove an key and its array
const oldKey = this.keyQueue.shift(); const oldKey = this.keyQueue.shift();
if (oldKey) { this.totalElements -= this.cache[oldKey].length;
this.totalElements -= this.cache[oldKey].length; delete this.cache[oldKey];
delete this.cache[oldKey];
}
} }
/** /**

View File

@@ -413,13 +413,13 @@ export class GraphData<V extends GremlinVertex, E extends GremlinEdge> {
* @param node * @param node
* @param prop * @param prop
*/ */
public static getNodePropValue(node: D3Node, prop: string): undefined | string | number | boolean { public static getNodePropValue(node: D3Node, prop: string): string | number | boolean {
if (node.hasOwnProperty(prop)) { if (node.hasOwnProperty(prop)) {
return (node as any)[prop]; return (node as any)[prop];
} }
// This is DocDB specific // This is DocDB specific
if (node.properties && node.properties.hasOwnProperty(prop)) { if (node.hasOwnProperty("properties") && node.properties.hasOwnProperty(prop)) {
return node.properties[prop][0]["value"]; return node.properties[prop][0]["value"];
} }
@@ -496,7 +496,7 @@ export class GraphData<V extends GremlinVertex, E extends GremlinEdge> {
* Get list of children ids of a given vertex * Get list of children ids of a given vertex
* @param vertex * @param vertex
*/ */
public static getChildrenId(vertex: GremlinVertex): string[] { private static getChildrenId(vertex: GremlinVertex): string[] {
const ids = <any>{}; // HashSet const ids = <any>{}; // HashSet
if (vertex.hasOwnProperty("outE")) { if (vertex.hasOwnProperty("outE")) {
let outE = vertex.outE; let outE = vertex.outE;

View File

@@ -1,5 +1,4 @@
jest.mock("../../../Common/dataAccess/queryDocuments"); jest.mock("../../../Common/DocumentClientUtilityBase");
jest.mock("../../../Common/dataAccess/queryDocumentsPage");
import React from "react"; import React from "react";
import * as sinon from "sinon"; import * as sinon from "sinon";
import { mount, ReactWrapper } from "enzyme"; import { mount, ReactWrapper } from "enzyme";
@@ -13,8 +12,7 @@ import * as DataModels from "../../../Contracts/DataModels";
import * as StorageUtility from "../../../Shared/StorageUtility"; import * as StorageUtility from "../../../Shared/StorageUtility";
import GraphTab from "../../Tabs/GraphTab"; import GraphTab from "../../Tabs/GraphTab";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { queryDocuments } from "../../../Common/dataAccess/queryDocuments"; import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase";
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
describe("Check whether query result is vertex array", () => { describe("Check whether query result is vertex array", () => {
it("should reject null as vertex array", () => { it("should reject null as vertex array", () => {
@@ -301,12 +299,12 @@ describe("GraphExplorer", () => {
ignoreD3Update: boolean ignoreD3Update: boolean
): GraphExplorer => { ): GraphExplorer => {
(queryDocuments as jest.Mock).mockImplementation((container: any, query: string, options: any) => { (queryDocuments as jest.Mock).mockImplementation((container: any, query: string, options: any) => {
return { return Q.resolve({
_query: query, _query: query,
nextItem: (callback: (error: any, document: DataModels.DocumentId) => void): void => {}, nextItem: (callback: (error: any, document: DataModels.DocumentId) => void): void => {},
hasMoreResults: () => false, hasMoreResults: () => false,
executeNext: (callback: (error: any, documents: DataModels.DocumentId[], headers: any) => void): void => {} executeNext: (callback: (error: any, documents: DataModels.DocumentId[], headers: any) => void): void => {}
}; });
}); });
(queryDocumentsPage as jest.Mock).mockImplementation( (queryDocumentsPage as jest.Mock).mockImplementation(
(rid: string, iterator: any, firstItemIndex: number, options: any) => { (rid: string, iterator: any, firstItemIndex: number, options: any) => {

View File

@@ -28,10 +28,8 @@ import * as Constants from "../../../Common/Constants";
import { InputProperty } from "../../../Contracts/ViewModels"; import { InputProperty } from "../../../Contracts/ViewModels";
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos"; import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif"; import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
import { queryDocuments } from "../../../Common/dataAccess/queryDocuments"; import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase";
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
import { FeedOptions } from "@azure/cosmos";
export interface GraphAccessor { export interface GraphAccessor {
applyFilter: () => void; applyFilter: () => void;
@@ -727,32 +725,26 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
/** /**
* Execute DocDB query and get all results * Execute DocDB query and get all results
*/ */
public async executeNonPagedDocDbQuery(query: string): Promise<DataModels.DocumentId[]> { public executeNonPagedDocDbQuery(query: string): Q.Promise<DataModels.DocumentId[]> {
try { // TODO maxItemCount: this reduces throttling, but won't cap the # of results
// TODO maxItemCount: this reduces throttling, but won't cap the # of results return queryDocuments(this.props.databaseId, this.props.collectionId, query, {
const iterator: QueryIterator<ItemDefinition & Resource> = queryDocuments( maxItemCount: GraphExplorer.PAGE_ALL,
this.props.databaseId, enableCrossPartitionQuery:
this.props.collectionId, StorageUtility.LocalStorageUtility.getEntryString(StorageUtility.StorageKey.IsCrossPartitionQueryEnabled) ===
query, "true"
{ }).then(
maxItemCount: GraphExplorer.PAGE_ALL, (iterator: QueryIterator<ItemDefinition & Resource>) => {
enableCrossPartitionQuery: return iterator.fetchNext().then(response => response.resources);
StorageUtility.LocalStorageUtility.getEntryString( },
StorageUtility.StorageKey.IsCrossPartitionQueryEnabled (reason: any) => {
) === "true" GraphExplorer.reportToConsole(
} as FeedOptions ConsoleDataType.Error,
); `Failed to execute non-paged query ${query}. Reason:${reason}`,
const response = await iterator.fetchNext(); reason
);
return response?.resources; return null;
} catch (error) { }
GraphExplorer.reportToConsole( );
ConsoleDataType.Error,
`Failed to execute non-paged query ${query}. Reason:${error}`,
error
);
return null;
}
} }
/** /**
@@ -872,7 +864,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
/** /**
* User executes query * User executes query
*/ */
public async submitQuery(query: string): Promise<void> { public submitQuery(query: string): void {
// Clear any progress indicator // Clear any progress indicator
this.executeCounter = 0; this.executeCounter = 0;
this.setState({ this.setState({
@@ -890,22 +882,24 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
// Remember query // Remember query
this.pushToLatestQueryFragments(query); this.pushToLatestQueryFragments(query);
try { let backendPromise;
let result: UserQueryResult;
if (query.toLocaleLowerCase() === "g.V()".toLocaleLowerCase()) {
result = await this.executeDocDbGVQuery();
} else {
result = await this.executeGremlinQuery(query);
}
this.queryTotalRequestCharge = result.requestCharge; if (query.toLocaleLowerCase() === "g.V()".toLocaleLowerCase()) {
} catch (error) { backendPromise = this.executeDocDbGVQuery();
const errorMsg = `Failure in submitting query: ${query}: ${getErrorMessage(error)}`; } else {
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg); backendPromise = this.executeGremlinQuery(query);
this.setState({
filterQueryError: errorMsg
});
} }
backendPromise.then(
(result: UserQueryResult) => (this.queryTotalRequestCharge = result.requestCharge),
(error: any) => {
const errorMsg = `Failure in submitting query: ${query}: ${getErrorMessage(error)}`;
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg);
this.setState({
filterQueryError: errorMsg
});
}
);
} }
/** /**
@@ -1396,7 +1390,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
/** /**
* Update possible vertices to display in UI * Update possible vertices to display in UI
*/ */
private updatePossibleVertices(): Promise<PossibleVertex[]> { private updatePossibleVertices(): Q.Promise<PossibleVertex[]> {
const highlightedNodeId = this.state.highlightedNode ? this.state.highlightedNode.id : null; const highlightedNodeId = this.state.highlightedNode ? this.state.highlightedNode.id : null;
const q = `SELECT c.id, c["${this.props.graphConfigUiData.nodeCaptionChoice() || const q = `SELECT c.id, c["${this.props.graphConfigUiData.nodeCaptionChoice() ||
@@ -1727,81 +1721,85 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
); );
} }
private async executeDocDbGVQuery(): Promise<UserQueryResult> { private executeDocDbGVQuery(): Q.Promise<UserQueryResult> {
let query = "select root.id from root where IS_DEFINED(root._isEdge) = false order by root._ts desc"; let query = "select root.id from root where IS_DEFINED(root._isEdge) = false order by root._ts desc";
if (this.props.collectionPartitionKeyProperty) { 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`; query = `select root.id, root.${this.props.collectionPartitionKeyProperty} from root where IS_DEFINED(root._isEdge) = false order by root._ts asc`;
} }
try { return queryDocuments(this.props.databaseId, this.props.collectionId, query, {
const iterator: QueryIterator<ItemDefinition & Resource> = queryDocuments( maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
this.props.databaseId, enableCrossPartitionQuery: LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
this.props.collectionId, })
query, .then(
{ (iterator: QueryIterator<ItemDefinition & Resource>) => {
maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE, this.currentDocDBQueryInfo = {
enableCrossPartitionQuery: iterator: iterator,
LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true" index: 0,
} as FeedOptions query: query
); };
this.currentDocDBQueryInfo = { },
iterator: iterator, (reason: any) => {
index: 0, GraphExplorer.reportToConsole(
query: query ConsoleDataType.Error,
}; `Failed to execute CosmosDB query: ${query} reason:${reason}`
return await this.loadMoreRootNodes(); );
} catch (error) { }
GraphExplorer.reportToConsole( )
ConsoleDataType.Error, .then(() => this.loadMoreRootNodes());
`Failed to execute CosmosDB query: ${query} reason:${error}`
);
throw error;
}
} }
private async loadMoreRootNodes(): Promise<UserQueryResult> { private loadMoreRootNodes(): Q.Promise<UserQueryResult> {
if (!this.currentDocDBQueryInfo) { if (!this.currentDocDBQueryInfo) {
return undefined; return Q.resolve(null);
} }
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 const queryInfoStr = `${this.currentDocDBQueryInfo.query} (${this.currentDocDBQueryInfo.index + 1}-${this
.currentDocDBQueryInfo.index + GraphExplorer.ROOT_LIST_PAGE_SIZE})`; .currentDocDBQueryInfo.index + GraphExplorer.ROOT_LIST_PAGE_SIZE})`;
const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${queryInfoStr}`); const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${queryInfoStr}`);
try { return queryDocumentsPage(
const results: ViewModels.QueryResults = await queryDocumentsPage( this.props.collectionId,
this.props.collectionId, this.currentDocDBQueryInfo.iterator,
this.currentDocDBQueryInfo.iterator, this.currentDocDBQueryInfo.index,
this.currentDocDBQueryInfo.index {
); enableCrossPartitionQuery:
LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
GraphExplorer.clearConsoleProgress(id); }
this.currentDocDBQueryInfo.index = results.lastItemIndex + 1; )
this.setState({ hasMoreRoots: results.hasMoreResults }); .then((results: ViewModels.QueryResults) => {
RU = results.requestCharge.toString(); GraphExplorer.clearConsoleProgress(id);
GraphExplorer.reportToConsole( this.currentDocDBQueryInfo.index = results.lastItemIndex + 1;
ConsoleDataType.Info, this.setState({ hasMoreRoots: results.hasMoreResults });
`Executed: ${queryInfoStr} ${GremlinClient.GremlinClient.getRequestChargeString(RU)}` RU = results.requestCharge.toString();
); GraphExplorer.reportToConsole(
const pkIds: string[] = (results.documents || []).map((item: DataModels.DocumentId) => ConsoleDataType.Info,
GraphExplorer.getPkIdFromDocumentId(item, this.props.collectionPartitionKeyProperty) `Executed: ${queryInfoStr} ${GremlinClient.GremlinClient.getRequestChargeString(RU)}`
); );
const documents = results.documents || [];
const arg = pkIds.join(","); return documents.map(
await this.executeGremlinQuery(`g.V(${arg})`); (item: DataModels.DocumentId) => {
return GraphExplorer.getPkIdFromDocumentId(item, this.props.collectionPartitionKeyProperty);
return { requestCharge: RU }; },
} catch (error) { (reason: any) => {
GraphExplorer.clearConsoleProgress(id); // Failure
const errorMsg = `Failed to query: ${this.currentDocDBQueryInfo.query}. Reason:${getErrorMessage(error)}`; GraphExplorer.clearConsoleProgress(id);
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg); const errorMsg = `Failed to query: ${this.currentDocDBQueryInfo.query}. Reason:${reason}`;
this.setState({ GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg);
filterQueryError: errorMsg this.setState({
}); filterQueryError: errorMsg
this.setFilterQueryStatus(FilterQueryStatus.ErrorResult); });
throw error; this.setFilterQueryStatus(FilterQueryStatus.ErrorResult);
} throw reason;
}
);
})
.then((pkIds: string[]) => {
const arg = pkIds.join(",");
return this.executeGremlinQuery(`g.V(${arg})`);
})
.then(() => ({ requestCharge: RU }));
} }
private executeGremlinQuery(query: string): Q.Promise<UserQueryResult> { private executeGremlinQuery(query: string): Q.Promise<UserQueryResult> {

View File

@@ -8,7 +8,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
let mockExplorer: Explorer; let mockExplorer: Explorer;
describe("Enable Azure Synapse Link Button", () => { describe("Enable Azure Synapse Link Button", () => {
const enableAzureSynapseLinkBtnLabel = "Enable Azure Synapse Link"; const enableAzureSynapseLinkBtnLabel = "Enable Azure Synapse Link (Preview)";
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;

View File

@@ -269,7 +269,7 @@ export class CommandBarComponentButtonFactory {
return null; return null;
} }
const label = "Enable Azure Synapse Link"; const label = "Enable Azure Synapse Link (Preview)";
return { return {
iconSrc: SynapseIcon, iconSrc: SynapseIcon,
iconAlt: label, iconAlt: label,

View File

@@ -99,22 +99,7 @@
.notificationConsoleControls { .notificationConsoleControls {
padding: @MediumSpace; padding: @MediumSpace;
margin-left:@DefaultSpace; 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 { #consoleFilterLabel {
padding: 4px; padding: 4px;
} }
@@ -122,7 +107,6 @@
.consoleSplitter { .consoleSplitter {
border-left: 1px solid @BaseMedium; border-left: 1px solid @BaseMedium;
margin: @MediumSpace; margin: @MediumSpace;
height: 20px;
} }
.clearNotificationsButton { .clearNotificationsButton {

View File

@@ -5,14 +5,14 @@
import * as React from "react"; import * as React from "react";
import { ClientDefaults, KeyCodes } from "../../../Common/Constants"; import { ClientDefaults, KeyCodes } from "../../../Common/Constants";
import AnimateHeight from "react-animate-height"; import AnimateHeight from "react-animate-height";
import { Dropdown, IDropdownOption } from "office-ui-fabric-react";
import LoadingIcon from "../../../../images/loading.svg"; import LoadingIcon from "../../../../images/loading.svg";
import ErrorBlackIcon from "../../../../images/error_black.svg"; import ErrorBlackIcon from "../../../../images/error_black.svg";
import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg"; import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg";
import InfoIcon from "../../../../images/info_color.svg"; import InfoIcon from "../../../../images/info_color.svg";
import ErrorRedIcon from "../../../../images/error_red.svg"; import ErrorRedIcon from "../../../../images/error_red.svg";
import ClearIcon from "../../../../images/Clear.svg";
import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif"; import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif";
import ClearIcon from "../../../../images/Clear.svg";
import ChevronUpIcon from "../../../../images/QueryBuilder/CollapseChevronUp_16x.png"; import ChevronUpIcon from "../../../../images/QueryBuilder/CollapseChevronUp_16x.png";
import ChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png"; import ChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
@@ -53,21 +53,16 @@ export class NotificationConsoleComponent extends React.Component<
NotificationConsoleComponentState NotificationConsoleComponentState
> { > {
private static readonly transitionDurationMs = 200; private static readonly transitionDurationMs = 200;
private static readonly FilterOptions = [ private static readonly FilterOptions = ["All", "In Progress", "Info", "Error"];
{ key: "All", text: "All" }, private headerTimeoutId: number;
{ key: "In Progress", text: "In progress" }, private prevHeaderStatus: string;
{ key: "Info", text: "Info" }, private consoleHeaderElement: HTMLElement;
{ key: "Error", text: "Error" }
];
private headerTimeoutId?: number;
private prevHeaderStatus: string | null;
private consoleHeaderElement?: HTMLElement;
constructor(props: NotificationConsoleComponentProps) { constructor(props: NotificationConsoleComponentProps) {
super(props); super(props);
this.state = { this.state = {
headerStatus: "", headerStatus: "",
selectedFilter: NotificationConsoleComponent.FilterOptions[0].key || "", selectedFilter: NotificationConsoleComponent.FilterOptions[0],
isExpanded: props.isConsoleExpanded isExpanded: props.isConsoleExpanded
}; };
this.prevHeaderStatus = null; this.prevHeaderStatus = null;
@@ -99,10 +94,6 @@ export class NotificationConsoleComponent extends React.Component<
} }
} }
public setElememntRef = (element: HTMLElement) => {
this.consoleHeaderElement = element;
};
public render(): JSX.Element { public render(): JSX.Element {
const numInProgress = this.props.consoleData.filter((data: ConsoleData) => data.type === ConsoleDataType.InProgress) const numInProgress = this.props.consoleData.filter((data: ConsoleData) => data.type === ConsoleDataType.InProgress)
.length; .length;
@@ -114,7 +105,7 @@ export class NotificationConsoleComponent extends React.Component<
<div className="notificationConsoleContainer"> <div className="notificationConsoleContainer">
<div <div
className="notificationConsoleHeader" className="notificationConsoleHeader"
ref={this.setElememntRef} ref={(element: HTMLElement) => (this.consoleHeaderElement = element)}
onClick={(event: React.MouseEvent<HTMLDivElement>) => this.expandCollapseConsole()} onClick={(event: React.MouseEvent<HTMLDivElement>) => this.expandCollapseConsole()}
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)} onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
tabIndex={0} tabIndex={0}
@@ -159,15 +150,20 @@ export class NotificationConsoleComponent extends React.Component<
> >
<div className="notificationConsoleContents"> <div className="notificationConsoleContents">
<div className="notificationConsoleControls"> <div className="notificationConsoleControls">
<Dropdown <label id="consoleFilterLabel">Filter</label>
label="Filter:" <select
role="combobox"
selectedKey={this.state.selectedFilter}
options={NotificationConsoleComponent.FilterOptions}
onChange={this.onFilterSelected.bind(this)}
aria-labelledby="consoleFilterLabel" aria-labelledby="consoleFilterLabel"
role="combobox"
aria-label={this.state.selectedFilter} aria-label={this.state.selectedFilter}
/> value={this.state.selectedFilter}
onChange={this.onFilterSelected.bind(this)}
>
{NotificationConsoleComponent.FilterOptions.map((value: string) => (
<option value={value} key={value}>
{value}
</option>
))}
</select>
<span className="consoleSplitter" /> <span className="consoleSplitter" />
<span <span
className="clearNotificationsButton" className="clearNotificationsButton"
@@ -224,12 +220,12 @@ export class NotificationConsoleComponent extends React.Component<
)); ));
} }
private onFilterSelected = (event: React.ChangeEvent<HTMLSelectElement>, option: IDropdownOption): void => { private onFilterSelected(event: React.ChangeEvent<HTMLSelectElement>): void {
this.setState({ selectedFilter: String(option.key) }); this.setState({ selectedFilter: event.target.value });
}; }
private getFilteredConsoleData(): ConsoleData[] { private getFilteredConsoleData(): ConsoleData[] {
let filterType: ConsoleDataType | null = null; let filterType: ConsoleDataType = null;
switch (this.state.selectedFilter) { switch (this.state.selectedFilter) {
case "All": case "All":
@@ -276,7 +272,7 @@ export class NotificationConsoleComponent extends React.Component<
private onConsoleWasExpanded = (): void => { private onConsoleWasExpanded = (): void => {
this.props.onConsoleExpandedChange(this.state.isExpanded); this.props.onConsoleExpandedChange(this.state.isExpanded);
if (this.state.isExpanded && this.consoleHeaderElement) { if (this.state.isExpanded) {
this.consoleHeaderElement.focus(); this.consoleHeaderElement.focus();
} }
}; };

View File

@@ -110,34 +110,43 @@ exports[`NotificationConsoleComponent renders the console (expanded) 1`] = `
<div <div
className="notificationConsoleControls" className="notificationConsoleControls"
> >
<StyledWithResponsiveMode <label
id="consoleFilterLabel"
>
Filter
</label>
<select
aria-label="All" aria-label="All"
aria-labelledby="consoleFilterLabel" aria-labelledby="consoleFilterLabel"
label="Filter:"
onChange={[Function]} onChange={[Function]}
options={
Array [
Object {
"key": "All",
"text": "All",
},
Object {
"key": "In Progress",
"text": "In progress",
},
Object {
"key": "Info",
"text": "Info",
},
Object {
"key": "Error",
"text": "Error",
},
]
}
role="combobox" role="combobox"
selectedKey="All" value="All"
/> >
<option
key="All"
value="All"
>
All
</option>
<option
key="In Progress"
value="In Progress"
>
In Progress
</option>
<option
key="Info"
value="Info"
>
Info
</option>
<option
key="Error"
value="Error"
>
Error
</option>
</select>
<span <span
className="consoleSplitter" className="consoleSplitter"
/> />

View File

@@ -1,17 +1,18 @@
import { Observable, of } from "rxjs"; import { Observable, of } from "rxjs";
import { AjaxRequest, AjaxResponse } from "rxjs/ajax"; import { AjaxResponse } from "rxjs/ajax";
import { ServerConfig } from "rx-jupyter";
let fakeAjaxResponse: AjaxResponse = { let fakeAjaxResponse: AjaxResponse = {
originalEvent: <Event>(<unknown>undefined), originalEvent: undefined,
xhr: new XMLHttpRequest(), xhr: new XMLHttpRequest(),
request: <AjaxRequest>(<unknown>null), request: null,
status: 200, status: 200,
response: {}, response: {},
responseText: "", responseText: null,
responseType: "json" responseType: "json"
}; };
export const sessions = { export const sessions = {
create: (serverConfig: unknown, body: object): Observable<AjaxResponse> => of(fakeAjaxResponse), create: (serverConfig: ServerConfig, body: object): Observable<AjaxResponse> => of(fakeAjaxResponse),
__setResponse: (response: AjaxResponse) => { __setResponse: (response: AjaxResponse) => {
fakeAjaxResponse = response; fakeAjaxResponse = response;
}, },

View File

@@ -350,7 +350,9 @@ export const launchWebSocketKernelEpic = (
} as any, } as any,
name: "", name: "",
path: content.filepath.replace(/^\/+/g, ""), path: content.filepath.replace(/^\/+/g, ""),
type: "notebook" type: "notebook",
endpoint:"https://dech-notebooks-demo-7.documents.azure.com:443/",
token:"" //to fill
}; };
return sessions.create(serverConfig, sessionPayload).pipe( return sessions.create(serverConfig, sessionPayload).pipe(
@@ -808,7 +810,7 @@ const closeUnsupportedMimetypesEpic = (
const filepath = action.payload.filepath; const filepath = action.payload.filepath;
// Close tab and show error message // Close tab and show error message
explorer.tabsManager.closeTabsByComparator( explorer.tabsManager.closeTabsByComparator(
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath) tab => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
); );
const msg = `${filepath} cannot be rendered. Please download the file, in order to view it outside of Data Explorer.`; const msg = `${filepath} cannot be rendered. Please download the file, in order to view it outside of Data Explorer.`;
explorer.showOkModalDialog("File cannot be rendered", msg); explorer.showOkModalDialog("File cannot be rendered", msg);
@@ -836,7 +838,7 @@ const closeContentFailedToFetchEpic = (
const filepath = action.payload.filepath; const filepath = action.payload.filepath;
// Close tab and show error message // Close tab and show error message
explorer.tabsManager.closeTabsByComparator( explorer.tabsManager.closeTabsByComparator(
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath) tab => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
); );
const msg = `Failed to load file: ${filepath}.`; const msg = `Failed to load file: ${filepath}.`;
explorer.showOkModalDialog("Failure to load", msg); explorer.showOkModalDialog("Failure to load", msg);

View File

@@ -11,7 +11,7 @@ import { stringifyNotebook } from "@nteract/commutable";
export class NotebookContentClient { export class NotebookContentClient {
constructor( constructor(
private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>, private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>,
public notebookBasePath: ko.Observable<string>, private notebookBasePath: ko.Observable<string>,
private contentProvider: IContentProvider private contentProvider: IContentProvider
) {} ) {}
@@ -117,11 +117,8 @@ export class NotebookContentClient {
private async checkIfFilepathExists(filepath: string): Promise<boolean> { private async checkIfFilepathExists(filepath: string): Promise<boolean> {
const parentDirPath = NotebookUtil.getParentPath(filepath); const parentDirPath = NotebookUtil.getParentPath(filepath);
if (parentDirPath) { const items = await this.fetchNotebookFiles(parentDirPath);
const items = await this.fetchNotebookFiles(parentDirPath); return items.some(value => FileSystemUtil.isPathEqual(value.path, filepath));
return items.some(value => FileSystemUtil.isPathEqual(value.path, filepath));
}
return false;
} }
/** /**
@@ -192,7 +189,7 @@ export class NotebookContentClient {
const dir = xhr.response; const dir = xhr.response;
const item = NotebookUtil.createNotebookContentItem(dir.name, dir.path, dir.type); const item = NotebookUtil.createNotebookContentItem(dir.name, dir.path, dir.type);
item.parent = parent; item.parent = parent;
parent.children?.push(item); parent.children.push(item);
return item; return item;
}); });
} }
@@ -228,7 +225,7 @@ export class NotebookContentClient {
* Convert rx-jupyter type to our type * Convert rx-jupyter type to our type
* @param type * @param type
*/ */
public static getType(type: FileType): NotebookContentItemType { private static getType(type: FileType): NotebookContentItemType {
switch (type) { switch (type) {
case "directory": case "directory":
return NotebookContentItemType.Directory; return NotebookContentItemType.Directory;

View File

@@ -128,9 +128,17 @@ export default class NotebookManager {
name: string, name: string,
content: string | ImmutableNotebook, content: string | ImmutableNotebook,
parentDomElement: HTMLElement, parentDomElement: HTMLElement,
isCodeOfConductEnabled: boolean,
isLinkInjectionEnabled: boolean isLinkInjectionEnabled: boolean
): Promise<void> { ): Promise<void> {
await this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement, isLinkInjectionEnabled); await this.publishNotebookPaneAdapter.open(
name,
getFullName(),
content,
parentDomElement,
isCodeOfConductEnabled,
isLinkInjectionEnabled
);
} }
public openCopyNotebookPane(name: string, content: string): void { public openCopyNotebookPane(name: string, content: string): void {

View File

@@ -152,8 +152,7 @@
maxAutoPilotThroughputSet: sharedAutoPilotThroughput, maxAutoPilotThroughputSet: sharedAutoPilotThroughput,
autoPilotUsageCost: autoPilotUsageCost, autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue, canExceedMaximumValue: canExceedMaximumValue,
showAutoPilot: !isFreeTierAccount(), showAutoPilot: !isFreeTierAccount()
freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip
}"> }">
</throughput-input-autopilot-v3> </throughput-input-autopilot-v3>
</div> </div>
@@ -244,6 +243,38 @@
</div> </div>
<!-- Unlimited Button Content - Start --> <!-- Unlimited Button Content - Start -->
<div class="tabcontent" data-bind="visible: isUnlimitedStorageSelected() || databaseHasSharedOffer()"> <div class="tabcontent" data-bind="visible: isUnlimitedStorageSelected() || databaseHasSharedOffer()">
<div data-bind="visible: rupmVisible">
<div class="tabs">
<p>
<span class="mandatoryStar">*</span>
<span class="addCollectionLabel">RU/m</span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information">
<span class="tooltiptext throughputRuInfo">
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.
</span>
</span>
</p>
<div tabindex="0" data-bind="event: { keydown: onRupmOptionsKeyDown }" aria-label="RU/m">
<div class="tab">
<input type="radio" id="rupmOn2" name="rupmcoll2" value="on" class="radio"
data-bind="checked: rupm">
<label for="rupmOn2">ON</label>
</div>
<div class="tab">
<input type="radio" id="rupmOff2" name="rupmcoll2" value="off" class="radio"
data-bind="checked: rupm">
<label for="rupmOff2">OFF</label>
</div>
</div>
</div>
</div>
<div data-bind="visible: partitionKeyVisible"> <div data-bind="visible: partitionKeyVisible">
<p> <p>
<span class="mandatoryStar">*</span> <span class="mandatoryStar">*</span>
@@ -257,7 +288,7 @@
range of values and is likely to have evenly distributed access patterns.</span> range of values and is likely to have evenly distributed access patterns.</span>
</span> </span>
</p> </p>
<input type="text" id="addCollection-partitionKeyValue" data-test="addCollection-partitionKeyValue" aria-required="true" size="40" <input type="text" id="partitionKeyValue" data-test="addCollection-partitionKeyValue" aria-required="true" size="40"
class="textfontclr collid" data-bind="textInput: partitionKey, class="textfontclr collid" data-bind="textInput: partitionKey,
attr: { attr: {
placeholder: partitionKeyPlaceholder, placeholder: partitionKeyPlaceholder,
@@ -334,8 +365,7 @@
maxAutoPilotThroughputSet: autoPilotThroughput, maxAutoPilotThroughputSet: autoPilotThroughput,
autoPilotUsageCost: autoPilotUsageCost, autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue, canExceedMaximumValue: canExceedMaximumValue,
showAutoPilot: !isFixedStorageSelected(), showAutoPilot: !isFixedStorageSelected()
freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip
}"> }">
</throughput-input-autopilot-v3> </throughput-input-autopilot-v3>
</div> </div>

View File

@@ -74,7 +74,7 @@ describe("Add Collection Pane", () => {
explorer.databaseAccount(mockFreeTierDatabaseAccount); explorer.databaseAccount(mockFreeTierDatabaseAccount);
const addCollectionPane = explorer.addCollectionPane as AddCollectionPane; const addCollectionPane = explorer.addCollectionPane as AddCollectionPane;
expect(addCollectionPane.isFreeTierAccount()).toBe(true); expect(addCollectionPane.isFreeTierAccount()).toBe(true);
expect(addCollectionPane.upsellMessage()).toContain("With free tier"); expect(addCollectionPane.upsellMessage()).toContain("With free tier discount");
expect(addCollectionPane.upsellAnchorUrl()).toBe(Constants.Urls.freeTierInformation); expect(addCollectionPane.upsellAnchorUrl()).toBe(Constants.Urls.freeTierInformation);
expect(addCollectionPane.upsellAnchorText()).toBe("Learn more"); expect(addCollectionPane.upsellAnchorText()).toBe("Learn more");
}); });

View File

@@ -16,7 +16,6 @@ import { ContextualPaneBase } from "./ContextualPaneBase";
import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent"; import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent";
import { createCollection } from "../../Common/dataAccess/createCollection"; import { createCollection } from "../../Common/dataAccess/createCollection";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { userContext } from "../../UserContext";
export interface AddCollectionPaneOptions extends ViewModels.PaneOptions { export interface AddCollectionPaneOptions extends ViewModels.PaneOptions {
isPreferredApiTable: ko.Computed<boolean>; isPreferredApiTable: ko.Computed<boolean>;
@@ -43,6 +42,8 @@ export default class AddCollectionPane extends ContextualPaneBase {
public partitionKeyVisible: ko.Computed<boolean>; public partitionKeyVisible: ko.Computed<boolean>;
public partitionKeyPattern: ko.Computed<string>; public partitionKeyPattern: ko.Computed<string>;
public partitionKeyTitle: ko.Computed<string>; public partitionKeyTitle: ko.Computed<string>;
public rupm: ko.Observable<string>;
public rupmVisible: ko.Observable<boolean>;
public storage: ko.Observable<string>; public storage: ko.Observable<string>;
public throughputSinglePartition: ViewModels.Editable<number>; public throughputSinglePartition: ViewModels.Editable<number>;
public throughputMultiPartition: ViewModels.Editable<number>; public throughputMultiPartition: ViewModels.Editable<number>;
@@ -89,7 +90,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
public isSynapseLinkUpdating: ko.Computed<boolean>; public isSynapseLinkUpdating: ko.Computed<boolean>;
public canExceedMaximumValue: ko.PureComputed<boolean>; public canExceedMaximumValue: ko.PureComputed<boolean>;
public ruToolTipText: ko.Computed<string>; public ruToolTipText: ko.Computed<string>;
public freeTierExceedThroughputTooltip: ko.Computed<string>;
public canConfigureThroughput: ko.PureComputed<boolean>; public canConfigureThroughput: ko.PureComputed<boolean>;
public showUpsellMessage: ko.PureComputed<boolean>; public showUpsellMessage: ko.PureComputed<boolean>;
public shouldCreateMongoWildcardIndex: ko.Observable<boolean>; public shouldCreateMongoWildcardIndex: ko.Observable<boolean>;
@@ -100,6 +100,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
super(options); super(options);
this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText()); this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText());
this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled()); this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled());
this.showUpsellMessage = ko.pureComputed(() => !this.container.isServerlessEnabled());
this.formWarnings = ko.observable<string>(); this.formWarnings = ko.observable<string>();
this.collectionId = ko.observable<string>(); this.collectionId = ko.observable<string>();
this.databaseId = ko.observable<string>(); this.databaseId = ko.observable<string>();
@@ -142,6 +143,12 @@ export default class AddCollectionPane extends ContextualPaneBase {
} }
return ""; return "";
}); });
this.rupm = ko.observable<string>(Constants.RUPMStates.off);
this.rupmVisible = ko.observable<boolean>(false);
const featureSubcription = this.container.features.subscribe(() => {
this.rupmVisible(this.container.isFeatureEnabled(Constants.Features.enableRupm));
featureSubcription.dispose();
});
this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue()); this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue());
@@ -194,6 +201,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
account.properties.readLocations.length) || account.properties.readLocations.length) ||
1; 1;
const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false; const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false;
const rupmEnabled: boolean = this.rupm() === Constants.RUPMStates.on;
let throughputSpendAckText: string; let throughputSpendAckText: string;
let estimatedSpend: string; let estimatedSpend: string;
@@ -203,15 +211,23 @@ export default class AddCollectionPane extends ContextualPaneBase {
serverId, serverId,
regions, regions,
multimaster, multimaster,
rupmEnabled,
this.isSharedAutoPilotSelected() this.isSharedAutoPilotSelected()
); );
estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster); estimatedSpend = PricingUtils.getEstimatedSpendHtml(
offerThroughput,
serverId,
regions,
multimaster,
rupmEnabled
);
} else { } else {
throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString( throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString(
this.sharedAutoPilotThroughput(), this.sharedAutoPilotThroughput(),
serverId, serverId,
regions, regions,
multimaster, multimaster,
rupmEnabled,
this.isSharedAutoPilotSelected() this.isSharedAutoPilotSelected()
); );
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
@@ -248,6 +264,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
account.properties.readLocations.length) || account.properties.readLocations.length) ||
1; 1;
const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false; const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false;
const rupmEnabled: boolean = this.rupm() === Constants.RUPMStates.on;
let throughputSpendAckText: string; let throughputSpendAckText: string;
let estimatedSpend: string; let estimatedSpend: string;
@@ -257,13 +274,15 @@ export default class AddCollectionPane extends ContextualPaneBase {
serverId, serverId,
regions, regions,
multimaster, multimaster,
rupmEnabled,
this.isAutoPilotSelected() this.isAutoPilotSelected()
); );
estimatedSpend = PricingUtils.getEstimatedSpendHtml( estimatedSpend = PricingUtils.getEstimatedSpendHtml(
this.throughputMultiPartition(), this.throughputMultiPartition(),
serverId, serverId,
regions, regions,
multimaster multimaster,
rupmEnabled
); );
} else { } else {
throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString( throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString(
@@ -271,6 +290,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
serverId, serverId,
regions, regions,
multimaster, multimaster,
rupmEnabled,
this.isAutoPilotSelected() this.isAutoPilotSelected()
); );
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
@@ -481,20 +501,8 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.resetData(); this.resetData();
}); });
this.freeTierExceedThroughputTooltip = ko.pureComputed<string>(() =>
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<string>(() => { this.upsellMessage = ko.pureComputed<string>(() => {
return PricingUtils.getUpsellMessage( return PricingUtils.getUpsellMessage(this.container.serverId(), this.isFreeTierAccount());
this.container.serverId(),
this.isFreeTierAccount(),
this.container.isFirstResourceCreated(),
this.container.defaultExperience(),
true
);
}); });
this.upsellMessageAriaLabel = ko.pureComputed<string>(() => { this.upsellMessageAriaLabel = ko.pureComputed<string>(() => {
@@ -546,23 +554,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
return isFreeTierAccount; 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<boolean>(() => { this.showIndexingOptionsForSharedThroughput = ko.computed<boolean>(() => {
const newDatabaseWithSharedOffer = this.databaseCreateNew() && this.databaseCreateNewShared(); const newDatabaseWithSharedOffer = this.databaseCreateNew() && this.databaseCreateNewShared();
const existingDatabaseWithSharedOffer = !this.databaseCreateNew() && this.databaseHasSharedOffer(); const existingDatabaseWithSharedOffer = !this.databaseCreateNew() && this.databaseHasSharedOffer();
@@ -654,7 +645,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
}); });
}); });
this.shouldCreateMongoWildcardIndex = ko.observable(this.container.isMongoIndexingEnabled()); this.shouldCreateMongoWildcardIndex = ko.observable(false);
} }
public getSharedThroughputDefault(): boolean { public getSharedThroughputDefault(): boolean {
@@ -679,7 +670,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
// TODO: Figure out if a database level partition split is about to happen once shared throughput read is available // TODO: Figure out if a database level partition split is about to happen once shared throughput read is available
this.formWarnings(""); this.formWarnings("");
this.databaseCreateNewShared(this.getSharedThroughputDefault()); this.databaseCreateNewShared(this.getSharedThroughputDefault());
this.shouldCreateMongoWildcardIndex(this.container.isMongoIndexingEnabled());
if (this.isPreferredApiTable() && !databaseId) { if (this.isPreferredApiTable() && !databaseId) {
databaseId = SharedConstants.CollectionCreation.TablesAPIDefaultDatabase; databaseId = SharedConstants.CollectionCreation.TablesAPIDefaultDatabase;
} }
@@ -696,10 +686,11 @@ export default class AddCollectionPane extends ContextualPaneBase {
storage: this.storage(), storage: this.storage(),
offerThroughput: this._getThroughput(), offerThroughput: this._getThroughput(),
partitionKey: this.partitionKey(), partitionKey: this.partitionKey(),
databaseId: this.databaseId() databaseId: this.databaseId(),
rupm: this.rupm()
}), }),
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: { defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u", storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
throughput: this._getThroughput(), throughput: this._getThroughput(),
@@ -797,11 +788,12 @@ export default class AddCollectionPane extends ContextualPaneBase {
id: this.collectionId(), id: this.collectionId(),
storage: this.storage(), storage: this.storage(),
partitionKey, partitionKey,
rupm: this.rupm(),
uniqueKeyPolicy, uniqueKeyPolicy,
collectionWithThroughputInShared: this.collectionWithThroughputInShared() collectionWithThroughputInShared: this.collectionWithThroughputInShared()
}), }),
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: { defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u", storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
throughput: offerThroughput, throughput: offerThroughput,
@@ -871,11 +863,12 @@ export default class AddCollectionPane extends ContextualPaneBase {
id: this.collectionId(), id: this.collectionId(),
storage: this.storage(), storage: this.storage(),
partitionKey, partitionKey,
rupm: this.rupm(),
uniqueKeyPolicy, uniqueKeyPolicy,
collectionWithThroughputInShared: this.collectionWithThroughputInShared() collectionWithThroughputInShared: this.collectionWithThroughputInShared()
}), }),
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: { defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u", storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
throughput: offerThroughput, throughput: offerThroughput,
@@ -905,11 +898,12 @@ export default class AddCollectionPane extends ContextualPaneBase {
id: this.collectionId(), id: this.collectionId(),
storage: this.storage(), storage: this.storage(),
partitionKey, partitionKey,
rupm: this.rupm(),
uniqueKeyPolicy, uniqueKeyPolicy,
collectionWithThroughputInShared: this.collectionWithThroughputInShared() collectionWithThroughputInShared: this.collectionWithThroughputInShared()
}, },
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: { defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u", storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
throughput: offerThroughput, throughput: offerThroughput,
@@ -929,13 +923,11 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.databaseId(""); this.databaseId("");
this.partitionKey(""); this.partitionKey("");
this.throughputSpendAck(false); this.throughputSpendAck(false);
this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled()); this.isAutoPilotSelected(false);
this.isSharedAutoPilotSelected(this.container.isAutoscaleDefaultEnabled()); this.isSharedAutoPilotSelected(false);
this.autoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); this.autoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
this.shouldCreateMongoWildcardIndex = ko.observable(this.container.isMongoIndexingEnabled());
this.uniqueKeys([]); this.uniqueKeys([]);
this.useIndexingForSharedThroughput(true); this.useIndexingForSharedThroughput(true);
@@ -989,6 +981,20 @@ export default class AddCollectionPane extends ContextualPaneBase {
return true; 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() { public onEnableSynapseLinkButtonClicked() {
this.container.openEnableSynapseLinkDialog(); this.container.openEnableSynapseLinkDialog();
} }
@@ -1012,6 +1018,16 @@ export default class AddCollectionPane extends ContextualPaneBase {
} }
const throughput = this._getThroughput(); 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()) { if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !this.throughputSpendAck()) {
this.formErrors(`Please acknowledge the estimated daily spend.`); this.formErrors(`Please acknowledge the estimated daily spend.`);
return false; return false;

View File

@@ -114,8 +114,7 @@
maxAutoPilotThroughputSet: maxAutoPilotThroughputSet, maxAutoPilotThroughputSet: maxAutoPilotThroughputSet,
autoPilotUsageCost: autoPilotUsageCost, autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue, canExceedMaximumValue: canExceedMaximumValue,
showAutoPilot: !isFreeTierAccount(), showAutoPilot: !isFreeTierAccount()
freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip
}"> }">
</throughput-input-autopilot-v3> </throughput-input-autopilot-v3>
<p data-bind="visible: canRequestSupport"> <p data-bind="visible: canRequestSupport">

View File

@@ -77,7 +77,7 @@ describe("Add Database Pane", () => {
explorer.databaseAccount(mockFreeTierDatabaseAccount); explorer.databaseAccount(mockFreeTierDatabaseAccount);
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane; const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.isFreeTierAccount()).toBe(true); expect(addDatabasePane.isFreeTierAccount()).toBe(true);
expect(addDatabasePane.upsellMessage()).toContain("With free tier"); expect(addDatabasePane.upsellMessage()).toContain("With free tier discount");
expect(addDatabasePane.upsellAnchorUrl()).toBe(Constants.Urls.freeTierInformation); expect(addDatabasePane.upsellAnchorUrl()).toBe(Constants.Urls.freeTierInformation);
expect(addDatabasePane.upsellAnchorText()).toBe("Learn more"); expect(addDatabasePane.upsellAnchorText()).toBe("Learn more");
}); });

View File

@@ -13,7 +13,6 @@ import { createDatabase } from "../../Common/dataAccess/createDatabase";
import { configContext, Platform } from "../../ConfigContext"; import { configContext, Platform } from "../../ConfigContext";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { SubscriptionType } from "../../Contracts/SubscriptionType"; import { SubscriptionType } from "../../Contracts/SubscriptionType";
import { userContext } from "../../UserContext";
export default class AddDatabasePane extends ContextualPaneBase { export default class AddDatabasePane extends ContextualPaneBase {
public defaultExperience: ko.Computed<string>; public defaultExperience: ko.Computed<string>;
@@ -44,7 +43,6 @@ export default class AddDatabasePane extends ContextualPaneBase {
public autoPilotUsageCost: ko.Computed<string>; public autoPilotUsageCost: ko.Computed<string>;
public canExceedMaximumValue: ko.PureComputed<boolean>; public canExceedMaximumValue: ko.PureComputed<boolean>;
public ruToolTipText: ko.Computed<string>; public ruToolTipText: ko.Computed<string>;
public freeTierExceedThroughputTooltip: ko.Computed<string>;
public isFreeTierAccount: ko.Computed<boolean>; public isFreeTierAccount: ko.Computed<boolean>;
public canConfigureThroughput: ko.PureComputed<boolean>; public canConfigureThroughput: ko.PureComputed<boolean>;
public showUpsellMessage: ko.PureComputed<boolean>; public showUpsellMessage: ko.PureComputed<boolean>;
@@ -55,6 +53,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
this.databaseId = ko.observable<string>(); this.databaseId = ko.observable<string>();
this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText()); this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText());
this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled()); this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled());
this.showUpsellMessage = ko.pureComputed(() => !this.container.isServerlessEnabled());
this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue()); this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue());
@@ -134,12 +133,19 @@ export default class AddDatabasePane extends ContextualPaneBase {
let estimatedSpendAcknowledge: string; let estimatedSpendAcknowledge: string;
let estimatedSpend: string; let estimatedSpend: string;
if (!this.isAutoPilotSelected()) { if (!this.isAutoPilotSelected()) {
estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster); estimatedSpend = PricingUtils.getEstimatedSpendHtml(
offerThroughput,
serverId,
regions,
multimaster,
false /*rupmEnabled*/
);
estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
offerThroughput, offerThroughput,
serverId, serverId,
regions, regions,
multimaster, multimaster,
false /*rupmEnabled*/,
this.isAutoPilotSelected() this.isAutoPilotSelected()
); );
} else { } else {
@@ -154,6 +160,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
serverId, serverId,
regions, regions,
multimaster, multimaster,
false /*rupmEnabled*/,
this.isAutoPilotSelected() this.isAutoPilotSelected()
); );
} }
@@ -182,18 +189,6 @@ export default class AddDatabasePane extends ContextualPaneBase {
return isFreeTierAccount; return isFreeTierAccount;
}); });
this.showUpsellMessage = ko.pureComputed(() => {
if (this.container.isServerlessEnabled()) {
return false;
}
if (this.isFreeTierAccount()) {
return this.databaseCreateNewShared();
}
return true;
});
this.maxThroughputRUText = ko.pureComputed(() => { this.maxThroughputRUText = ko.pureComputed(() => {
return this.maxThroughputRU().toLocaleString(); return this.maxThroughputRU().toLocaleString();
}); });
@@ -231,20 +226,8 @@ export default class AddDatabasePane extends ContextualPaneBase {
this.resetData(); this.resetData();
}); });
this.freeTierExceedThroughputTooltip = ko.pureComputed<string>(() =>
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<string>(() => { this.upsellMessage = ko.pureComputed<string>(() => {
return PricingUtils.getUpsellMessage( return PricingUtils.getUpsellMessage(this.container.serverId(), this.isFreeTierAccount());
this.container.serverId(),
this.isFreeTierAccount(),
this.container.isFirstResourceCreated(),
this.container.defaultExperience(),
false
);
}); });
this.upsellMessageAriaLabel = ko.pureComputed<string>(() => { this.upsellMessageAriaLabel = ko.pureComputed<string>(() => {
@@ -275,7 +258,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
databaseAccountName: this.container.databaseAccount().name, databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(), defaultExperience: this.container.defaultExperience(),
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: { defaultsCheck: {
throughput: this.throughput(), throughput: this.throughput(),
flight: this.container.flight() flight: this.container.flight()
@@ -303,7 +286,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
}), }),
offerThroughput, offerThroughput,
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: { defaultsCheck: {
flight: this.container.flight() flight: this.container.flight()
}, },
@@ -337,7 +320,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
public resetData() { public resetData() {
this.databaseId(""); this.databaseId("");
this.databaseCreateNewShared(this.getSharedThroughputDefault()); this.databaseCreateNewShared(this.getSharedThroughputDefault());
this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled()); this.isAutoPilotSelected(false);
this.maxAutoPilotThroughputSet(AutoPilotUtils.minAutoPilotThroughput); this.maxAutoPilotThroughputSet(AutoPilotUtils.minAutoPilotThroughput);
this._updateThroughputLimitByDatabase(); this._updateThroughputLimitByDatabase();
this.throughputSpendAck(false); this.throughputSpendAck(false);
@@ -367,7 +350,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
}), }),
offerThroughput: offerThroughput, offerThroughput: offerThroughput,
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: { defaultsCheck: {
flight: this.container.flight() flight: this.container.flight()
}, },
@@ -391,7 +374,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
}), }),
offerThroughput: offerThroughput, offerThroughput: offerThroughput,
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: { defaultsCheck: {
flight: this.container.flight() flight: this.container.flight()
}, },

View File

@@ -15,7 +15,6 @@ import { HashMap } from "../../Common/HashMap";
import { configContext, Platform } from "../../ConfigContext"; import { configContext, Platform } from "../../ConfigContext";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { SubscriptionType } from "../../Contracts/SubscriptionType"; import { SubscriptionType } from "../../Contracts/SubscriptionType";
import { userContext } from "../../UserContext";
export default class CassandraAddCollectionPane extends ContextualPaneBase { export default class CassandraAddCollectionPane extends ContextualPaneBase {
public createTableQuery: ko.Observable<string>; public createTableQuery: ko.Observable<string>;
@@ -139,12 +138,19 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
let estimatedSpend: string; let estimatedSpend: string;
let estimatedDedicatedSpendAcknowledge: string; let estimatedDedicatedSpendAcknowledge: string;
if (!this.isAutoPilotSelected()) { if (!this.isAutoPilotSelected()) {
estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster); estimatedSpend = PricingUtils.getEstimatedSpendHtml(
offerThroughput,
serverId,
regions,
multimaster,
false /*rupmEnabled*/
);
estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
offerThroughput, offerThroughput,
serverId, serverId,
regions, regions,
multimaster, multimaster,
false /*rupmEnabled*/,
this.isAutoPilotSelected() this.isAutoPilotSelected()
); );
} else { } else {
@@ -159,6 +165,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
serverId, serverId,
regions, regions,
multimaster, multimaster,
false /*rupmEnabled*/,
this.isAutoPilotSelected() this.isAutoPilotSelected()
); );
} }
@@ -183,12 +190,19 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
let estimatedSpend: string; let estimatedSpend: string;
let estimatedSharedSpendAcknowledge: string; let estimatedSharedSpendAcknowledge: string;
if (!this.isSharedAutoPilotSelected()) { if (!this.isSharedAutoPilotSelected()) {
estimatedSpend = PricingUtils.getEstimatedSpendHtml(this.keyspaceThroughput(), serverId, regions, multimaster); estimatedSpend = PricingUtils.getEstimatedSpendHtml(
this.keyspaceThroughput(),
serverId,
regions,
multimaster,
false /*rupmEnabled*/
);
estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
this.keyspaceThroughput(), this.keyspaceThroughput(),
serverId, serverId,
regions, regions,
multimaster, multimaster,
false /*rupmEnabled*/,
this.isSharedAutoPilotSelected() this.isSharedAutoPilotSelected()
); );
} else { } else {
@@ -203,6 +217,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
serverId, serverId,
regions, regions,
multimaster, multimaster,
false /*rupmEnabled*/,
this.isSharedAutoPilotSelected() this.isSharedAutoPilotSelected()
); );
} }
@@ -297,10 +312,11 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
storage: Constants.BackendDefaults.multiPartitionStorageInGb, storage: Constants.BackendDefaults.multiPartitionStorageInGb,
offerThroughput: this.throughput(), offerThroughput: this.throughput(),
partitionKey: "", partitionKey: "",
databaseId: this.keyspaceId() databaseId: this.keyspaceId(),
rupm: false
}), }),
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: { defaultsCheck: {
storage: "u", storage: "u",
throughput: this.throughput(), throughput: this.throughput(),
@@ -350,11 +366,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
offerThroughput: this.throughput(), offerThroughput: this.throughput(),
partitionKey: "", partitionKey: "",
databaseId: this.keyspaceId(), databaseId: this.keyspaceId(),
rupm: false,
hasDedicatedThroughput: this.dedicateTableThroughput() hasDedicatedThroughput: this.dedicateTableThroughput()
}), }),
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: { defaultsCheck: {
storage: "u", storage: "u",
throughput: this.throughput(), throughput: this.throughput(),
@@ -396,11 +413,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
offerThroughput: this.throughput(), offerThroughput: this.throughput(),
partitionKey: "", partitionKey: "",
databaseId: this.keyspaceId(), databaseId: this.keyspaceId(),
rupm: false,
hasDedicatedThroughput: this.dedicateTableThroughput() hasDedicatedThroughput: this.dedicateTableThroughput()
}), }),
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: { defaultsCheck: {
storage: "u", storage: "u",
throughput: this.throughput(), throughput: this.throughput(),
@@ -426,11 +444,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
offerThroughput: this.throughput(), offerThroughput: this.throughput(),
partitionKey: "", partitionKey: "",
databaseId: this.keyspaceId(), databaseId: this.keyspaceId(),
rupm: false,
hasDedicatedThroughput: this.dedicateTableThroughput() hasDedicatedThroughput: this.dedicateTableThroughput()
}, },
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: { defaultsCheck: {
storage: "u", storage: "u",
throughput: this.throughput(), throughput: this.throughput(),
@@ -451,8 +470,8 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
public resetData() { public resetData() {
super.resetData(); super.resetData();
const throughputDefaults = this.container.collectionCreationDefaults.throughput; const throughputDefaults = this.container.collectionCreationDefaults.throughput;
this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled()); this.isAutoPilotSelected(false);
this.isSharedAutoPilotSelected(this.container.isAutoscaleDefaultEnabled()); this.isSharedAutoPilotSelected(false);
this.selectedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); this.selectedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
this.throughput(AddCollectionUtility.getMaxThroughput(this.container.collectionCreationDefaults, this.container)); this.throughput(AddCollectionUtility.getMaxThroughput(this.container.collectionCreationDefaults, this.container));

View File

@@ -98,21 +98,26 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
author: string, author: string,
notebookContent: string | ImmutableNotebook, notebookContent: string | ImmutableNotebook,
parentDomElement: HTMLElement, parentDomElement: HTMLElement,
isCodeOfConductEnabled: boolean,
isLinkInjectionEnabled: boolean isLinkInjectionEnabled: boolean
): Promise<void> { ): Promise<void> {
try { if (isCodeOfConductEnabled) {
const response = await this.junoClient.isCodeOfConductAccepted(); try {
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { const response = await this.junoClient.isCodeOfConductAccepted();
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`); 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; this.isCodeOfConductAccepted = response.data;
} catch (error) { } catch (error) {
handleError( handleError(
error, error,
"PublishNotebookPaneAdapter/isCodeOfConductAccepted", "PublishNotebookPaneAdapter/isCodeOfConductAccepted",
"Failed to check if code of conduct was accepted" "Failed to check if code of conduct was accepted"
); );
}
} else {
this.isCodeOfConductAccepted = true;
} }
this.name = name; this.name = name;

View File

@@ -47,7 +47,7 @@
padding: 32px 16px; padding: 32px 16px;
display: flex; display: flex;
background-color: @BaseLight; background-color: @BaseLight;
border: 1px solid #949494; border: 1px solid #E5E5E5;
box-sizing: border-box; box-sizing: border-box;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
border-radius: 4px; border-radius: 4px;

View File

@@ -1,5 +1,5 @@
abstract class CacheBase<T> { abstract class CacheBase<T> {
public data: T[] | null; public data: T[];
public sortOrder: any; public sortOrder: any;
public serverCallInProgress: boolean; public serverCallInProgress: boolean;

Some files were not shown because too many files have changed in this diff Show More