Compare commits

...

13 Commits

Author SHA1 Message Date
Chris-MS-896
417a6151be Merge branch 'master' into v-yiqcao/throughoutValidation 2020-12-31 18:59:42 -06:00
Steve Faulkner
cc63cdc1fd Remove dependency on canvas (#354) 2020-12-26 21:56:37 -06:00
Chris-MS-896
3c25788e72 'minor change' 2020-12-22 22:11:24 -06:00
Chris-MS-896
1c8deb1fcc no message 2020-12-22 21:53:28 -06:00
artrejo
4664038df0 Add onChange event to throughput text field 2020-12-22 16:59:50 -08:00
Steve Faulkner
c3058ee5a9 Check for undefined query results (#350) 2020-12-18 19:55:32 -06:00
Steve Faulkner
b000631a0c Revert web.config changes (#349) 2020-12-18 19:26:10 -06:00
vchske
e8f4c8f93c Cost Estimate Changes (#342)
* Initial change of estimated cost to table format

* Converted cost estimate to table format and added different data for current vs updated cost estimates.

* lint fixes

* Changed the names of some interfaces

* Refactored a unit call to use an argument interface to avoid future confusion.

* Changed the severity of the save warning

* Format fix

* Fixed test due to styling change

Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2020-12-18 16:15:55 -08:00
Steve Faulkner
16bde97e47 Rewrite URL for IE users (#340) 2020-12-18 16:08:40 -06:00
Steve Faulkner
6da43ee27b Publish IE specific Nuget package (#347)
* Publish IE specific Nuget package

* Require ally tests to pass
2020-12-17 17:41:38 -06:00
Gahl Levy
ebae484b8f Fix duplicate settings tabs (#343)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2020-12-17 13:01:36 -08:00
Steve Faulkner
dfb1b50621 Explorer.ts Cleanup (#341)
Co-authored-by: victor-meng <56978073+victor-meng@users.noreply.github.com>
2020-12-16 20:00:39 -06:00
victor-meng
f54e8eb692 Move queryDocuments out of DataAccessUtility (#334) 2020-12-16 15:27:17 -08:00
80 changed files with 2347 additions and 2098 deletions

View File

@@ -14,7 +14,6 @@ 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,6 +101,7 @@ 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-*
@@ -159,13 +160,14 @@ 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] needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted, accessibility]
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
@@ -189,7 +191,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] needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted, accessibility]
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
@@ -211,3 +213,28 @@ 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"

BIN
.vs/slnx.sqlite Normal file

Binary file not shown.

View File

@@ -13,29 +13,18 @@ UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), ht
### Watch mode ### Watch mode
Run `npm run watch` to start the development server and automatically rebuild on changes Run `npm start` to start the development server and automatically rebuild on changes
### Specifying Development Platform ### Hosted Development (https://cosmos.azure.com)
Setting the environment variable `PLATFORM` during the build process will force the explorer to load the specified platform. By default in development it will run in `Hosted` mode. Valid options: - Visit: `https://localhost:1234/hostedExplorer.html`
- Local sign in via AAD will NOT work. Connection string only in dev mode. Use the Portal if you need AAD auth.
- Hosted - The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine.
- Emulator
- 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
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. - Start the Cosmos Emulator
- 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
@@ -55,16 +44,8 @@ The Cosmos emulator currently only runs in Windows environments. You can still d
### Portal Development ### Portal Development
The Cosmos Portal that consumes this repo is not currently open source. If you have access to this project, `npm run build` will copy the built files over to the portal where they will be loaded by the portal development environment - Visit: https://ms.portal.azure.com/?dataExplorerSource=https%3A%2F%2Flocalhost%3A1234%2Fexplorer.html
- You may have to manually visit https://localhost:1234/explorer.html first and click through any SSL certificate warnings
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

7
canvas/README.md Normal file
View File

@@ -0,0 +1,7 @@
# Why?
This adds a mock module for `canvas`. Nteract has a ignored require and undeclared dependency on this module. `cavnas` is a server side node module and is not used in browser side code for nteract.
Installing it locally (`npm install canvas`) will resolve the problem, but it is a native module so it is flaky depending on the system, node version, processor arch, etc. This module provides a simpler, more robust solution.
Remove this workaround if [this bug](https://github.com/nteract/any-vega/issues/2) ever gets resolved

1
canvas/index.js Normal file
View File

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

11
canvas/package.json Normal file
View File

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

220
package-lock.json generated
View File

@@ -5393,11 +5393,6 @@
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz",
"integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==" "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q=="
}, },
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
},
"abort-controller": { "abort-controller": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@@ -5641,6 +5636,7 @@
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
"integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
"optional": true,
"requires": { "requires": {
"delegates": "^1.0.0", "delegates": "^1.0.0",
"readable-stream": "^2.0.6" "readable-stream": "^2.0.6"
@@ -6883,14 +6879,7 @@
"dev": true "dev": true
}, },
"canvas": { "canvas": {
"version": "2.6.1", "version": "file:canvas"
"resolved": "https://registry.npmjs.org/canvas/-/canvas-2.6.1.tgz",
"integrity": "sha512-S98rKsPcuhfTcYbtF53UIJhcbgIAK533d1kJKMwsMwAIFgfd58MOyxRud3kktlzWiEkFliaJtvyZCBtud/XVEA==",
"requires": {
"nan": "^2.14.0",
"node-pre-gyp": "^0.11.0",
"simple-get": "^3.0.3"
}
}, },
"capture-exit": { "capture-exit": {
"version": "2.0.0", "version": "2.0.0",
@@ -7454,7 +7443,8 @@
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
"optional": true
}, },
"constants-browserify": { "constants-browserify": {
"version": "1.0.0", "version": "1.0.0",
@@ -8435,6 +8425,7 @@
"version": "4.2.1", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
"integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==",
"optional": true,
"requires": { "requires": {
"mimic-response": "^2.0.0" "mimic-response": "^2.0.0"
} }
@@ -8460,7 +8451,8 @@
"deep-extend": { "deep-extend": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"optional": true
}, },
"deep-is": { "deep-is": {
"version": "0.1.3", "version": "0.1.3",
@@ -8652,7 +8644,8 @@
"delegates": { "delegates": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
"optional": true
}, },
"depd": { "depd": {
"version": "1.1.2", "version": "1.1.2",
@@ -8688,7 +8681,8 @@
"detect-libc": { "detect-libc": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
"optional": true
}, },
"detect-newline": { "detect-newline": {
"version": "2.1.0", "version": "2.1.0",
@@ -10674,14 +10668,6 @@
"universalify": "^0.1.0" "universalify": "^0.1.0"
} }
}, },
"fs-minipass": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
"integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
"requires": {
"minipass": "^2.6.0"
}
},
"fs-observable": { "fs-observable": {
"version": "4.1.14", "version": "4.1.14",
"resolved": "https://registry.npmjs.org/fs-observable/-/fs-observable-4.1.14.tgz", "resolved": "https://registry.npmjs.org/fs-observable/-/fs-observable-4.1.14.tgz",
@@ -10823,6 +10809,7 @@
"version": "2.7.4", "version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
"optional": true,
"requires": { "requires": {
"aproba": "^1.0.3", "aproba": "^1.0.3",
"console-control-strings": "^1.0.0", "console-control-strings": "^1.0.0",
@@ -10837,12 +10824,14 @@
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"optional": true
}, },
"is-fullwidth-code-point": { "is-fullwidth-code-point": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@@ -10851,6 +10840,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@@ -10861,6 +10851,7 @@
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"optional": true,
"requires": { "requires": {
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
@@ -11350,7 +11341,8 @@
"has-unicode": { "has-unicode": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
"optional": true
}, },
"has-value": { "has-value": {
"version": "1.0.0", "version": "1.0.0",
@@ -11832,14 +11824,6 @@
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
"integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==" "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw=="
}, },
"ignore-walk": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz",
"integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==",
"requires": {
"minimatch": "^3.0.4"
}
},
"image-size": { "image-size": {
"version": "0.5.5", "version": "0.5.5",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
@@ -15544,7 +15528,8 @@
"mimic-response": { "mimic-response": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
"integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==",
"optional": true
}, },
"min-document": { "min-document": {
"version": "2.19.0", "version": "2.19.0",
@@ -15601,15 +15586,6 @@
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
}, },
"minipass": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
}
},
"minipass-collect": { "minipass-collect": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz",
@@ -15679,14 +15655,6 @@
} }
} }
}, },
"minizlib": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
"integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
"requires": {
"minipass": "^2.9.0"
}
},
"mississippi": { "mississippi": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz",
@@ -15861,7 +15829,8 @@
"nan": { "nan": {
"version": "2.14.2", "version": "2.14.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
"optional": true
}, },
"nanomatch": { "nanomatch": {
"version": "1.2.13", "version": "1.2.13",
@@ -15911,26 +15880,6 @@
"semver": "^5.4.1" "semver": "^5.4.1"
} }
}, },
"needle": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.5.2.tgz",
"integrity": "sha512-LbRIwS9BfkPvNwNHlsA41Q29kL2L/6VaOJ0qisM5lLWsTV3nP15abO5ITL6L81zqFhzjRKDAYjpcBcwM0AVvLQ==",
"requires": {
"debug": "^3.2.6",
"iconv-lite": "^0.4.4",
"sax": "^1.2.4"
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"requires": {
"ms": "^2.1.1"
}
}
}
},
"negotiator": { "negotiator": {
"version": "0.6.2", "version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
@@ -16099,41 +16048,6 @@
"which": "^1.3.0" "which": "^1.3.0"
} }
}, },
"node-pre-gyp": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz",
"integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==",
"requires": {
"detect-libc": "^1.0.2",
"mkdirp": "^0.5.1",
"needle": "^2.2.1",
"nopt": "^4.0.1",
"npm-packlist": "^1.1.6",
"npmlog": "^4.0.2",
"rc": "^1.2.7",
"rimraf": "^2.6.1",
"semver": "^5.3.0",
"tar": "^4"
},
"dependencies": {
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
},
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"requires": {
"glob": "^7.1.3"
}
}
}
},
"node-releases": { "node-releases": {
"version": "1.1.66", "version": "1.1.66",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.66.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.66.tgz",
@@ -16146,15 +16060,6 @@
"integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=", "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=",
"optional": true "optional": true
}, },
"nopt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
"integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
"requires": {
"abbrev": "1",
"osenv": "^0.1.4"
}
},
"normalize-package-data": { "normalize-package-data": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
@@ -16179,29 +16084,6 @@
"resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz", "resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz",
"integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==" "integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg=="
}, },
"npm-bundled": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz",
"integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==",
"requires": {
"npm-normalize-package-bin": "^1.0.1"
}
},
"npm-normalize-package-bin": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz",
"integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA=="
},
"npm-packlist": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz",
"integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==",
"requires": {
"ignore-walk": "^3.0.1",
"npm-bundled": "^1.0.1",
"npm-normalize-package-bin": "^1.0.1"
}
},
"npm-run-path": { "npm-run-path": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
@@ -16214,6 +16096,7 @@
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"optional": true,
"requires": { "requires": {
"are-we-there-yet": "~1.1.2", "are-we-there-yet": "~1.1.2",
"console-control-strings": "~1.1.0", "console-control-strings": "~1.1.0",
@@ -16605,7 +16488,8 @@
"os-homedir": { "os-homedir": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
"dev": true
}, },
"os-locale": { "os-locale": {
"version": "1.4.0", "version": "1.4.0",
@@ -16624,20 +16508,6 @@
"windows-release": "^3.1.0" "windows-release": "^3.1.0"
} }
}, },
"os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
},
"osenv": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
"requires": {
"os-homedir": "^1.0.0",
"os-tmpdir": "^1.0.0"
}
},
"p-defer": { "p-defer": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
@@ -17690,6 +17560,7 @@
"version": "1.2.8", "version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"optional": true,
"requires": { "requires": {
"deep-extend": "^0.6.0", "deep-extend": "^0.6.0",
"ini": "~1.3.0", "ini": "~1.3.0",
@@ -19116,12 +18987,14 @@
"simple-concat": { "simple-concat": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"optional": true
}, },
"simple-get": { "simple-get": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz",
"integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==",
"optional": true,
"requires": { "requires": {
"decompress-response": "^4.2.0", "decompress-response": "^4.2.0",
"once": "^1.3.1", "once": "^1.3.1",
@@ -20113,30 +19986,6 @@
"integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==",
"dev": true "dev": true
}, },
"tar": {
"version": "4.4.13",
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz",
"integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
"requires": {
"chownr": "^1.1.1",
"fs-minipass": "^1.2.5",
"minipass": "^2.8.6",
"minizlib": "^1.2.1",
"mkdirp": "^0.5.0",
"safe-buffer": "^5.1.2",
"yallist": "^3.0.3"
},
"dependencies": {
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
}
}
},
"tar-fs": { "tar-fs": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
@@ -22192,6 +22041,7 @@
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
"optional": true,
"requires": { "requires": {
"string-width": "^1.0.2 || 2" "string-width": "^1.0.2 || 2"
}, },
@@ -22199,12 +22049,14 @@
"ansi-regex": { "ansi-regex": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
"optional": true
}, },
"string-width": { "string-width": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
"optional": true,
"requires": { "requires": {
"is-fullwidth-code-point": "^2.0.0", "is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^4.0.0" "strip-ansi": "^4.0.0"
@@ -22214,6 +22066,7 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"optional": true,
"requires": { "requires": {
"ansi-regex": "^3.0.0" "ansi-regex": "^3.0.0"
} }
@@ -22383,7 +22236,8 @@
"yallist": { "yallist": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true
}, },
"yargs": { "yargs": {
"version": "13.3.2", "version": "13.3.2",

View File

@@ -44,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": "2.6.1", "canvas": "file:./canvas",
"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",

View File

@@ -1,169 +0,0 @@
import { ConflictDefinition, FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import Q from "q";
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import ConflictId from "../Explorer/Tree/ConflictId";
import DocumentId from "../Explorer/Tree/DocumentId";
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
import * as Constants from "./Constants";
import { client } from "./CosmosClient";
export function getCommonQueryOptions(options: FeedOptions): any {
const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage);
options = options || {};
options.populateQueryMetrics = true;
options.enableScanInQuery = options.enableScanInQuery || true;
if (!options.partitionKey) {
options.forceQueryPlan = true;
}
options.maxItemCount =
options.maxItemCount ||
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
Constants.Queries.itemsPerPage;
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
return options;
}
export function queryDocuments(
databaseId: string,
containerId: string,
query: string,
options: any
): Q.Promise<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

@@ -1,217 +0,0 @@
import { ConflictDefinition, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import Q from "q";
import * as ViewModels from "../Contracts/ViewModels";
import ConflictId from "../Explorer/Tree/ConflictId";
import DocumentId from "../Explorer/Tree/DocumentId";
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import { logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
import * as Constants from "./Constants";
import * as DataAccessUtilityBase from "./DataAccessUtilityBase";
import { MinimalQueryIterator, nextPage } from "./IteratorUtilities";
import { handleError } from "./ErrorHandlingUtils";
// TODO: Log all promise resolutions and errors with verbosity levels
export function queryDocuments(
databaseId: string,
containerId: string,
query: string,
options: any
): Q.Promise<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

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

View File

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

View File

@@ -3,16 +3,18 @@ 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 { createDocument, deleteDocument, queryDocuments, queryDocumentsPage } from "./DocumentClientUtilityBase"; import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage";
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 = {
@@ -31,10 +33,7 @@ export class QueriesClient {
return Promise.resolve(queriesCollection.rawDataModel); return Promise.resolve(queriesCollection.rawDataModel);
} }
const id = NotificationConsoleUtils.logConsoleMessage( const clearMessage = NotificationConsoleUtils.logConsoleProgress("Setting up account for saving queries");
ConsoleDataType.InProgress,
"Setting up account for saving queries"
);
return createCollection({ return createCollection({
collectionId: SavedQueries.CollectionName, collectionId: SavedQueries.CollectionName,
createNewDatabase: true, createNewDatabase: true,
@@ -45,10 +44,7 @@ export class QueriesClient {
}) })
.then( .then(
(collection: DataModels.Collection) => { (collection: DataModels.Collection) => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleInfo("Successfully set up account for saving queries");
ConsoleDataType.Info,
"Successfully set up account for saving queries"
);
return Promise.resolve(collection); return Promise.resolve(collection);
}, },
(error: any) => { (error: any) => {
@@ -56,17 +52,14 @@ export class QueriesClient {
return Promise.reject(error); return Promise.reject(error);
} }
) )
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); .finally(() => clearMessage());
} }
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.logConsoleMessage( NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`);
ConsoleDataType.Error,
`Failed to save query ${query.queryName}: ${errorMessage}`
);
return Promise.reject(errorMessage); return Promise.reject(errorMessage);
} }
@@ -74,25 +67,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.logConsoleMessage( NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`);
ConsoleDataType.Error,
`Failed to save query ${query.queryName}: ${errorMessage}`
);
return Promise.reject(errorMessage); return Promise.reject(errorMessage);
} }
const id = NotificationConsoleUtils.logConsoleMessage( const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Saving query ${query.queryName}`);
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.logConsoleMessage( NotificationConsoleUtils.logConsoleInfo(`Successfully saved query ${query.queryName}`);
ConsoleDataType.Info,
`Successfully saved query ${query.queryName}`
);
return Promise.resolve(); return Promise.resolve();
}, },
(error: any) => { (error: any) => {
@@ -103,74 +87,65 @@ export class QueriesClient {
return Promise.reject(error); return Promise.reject(error);
} }
) )
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); .finally(() => clearMessage());
} }
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.logConsoleMessage( NotificationConsoleUtils.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`);
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 id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Fetching saved queries"); const clearMessage = NotificationConsoleUtils.logConsoleProgress("Fetching saved queries");
return queryDocuments(SavedQueries.DatabaseName, SavedQueries.CollectionName, this.fetchQueriesQuery(), options) const queryIterator: QueryIterator<ItemDefinition & Resource> = queryDocuments(
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(
(queryIterator: QueryIterator<ItemDefinition & Resource>) => { (results: ViewModels.QueryResults) => {
const fetchQueries = (firstItemIndex: number): Q.Promise<ViewModels.QueryResults> => let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => {
queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex, options); if (!document) {
return QueryUtils.queryAllPages(fetchQueries).then( return undefined;
(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(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); .finally(() => clearMessage());
} }
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.logConsoleMessage( NotificationConsoleUtils.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`);
ConsoleDataType.Error,
`Failed to fetch saved queries: ${errorMessage}`
);
return Promise.reject(errorMessage); return Promise.reject(errorMessage);
} }
@@ -178,16 +153,10 @@ 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.logConsoleMessage( NotificationConsoleUtils.logConsoleError(`Failed to delete query ${query.queryName}: ${errorMessage}`);
ConsoleDataType.Error,
`Failed to delete query ${query.queryName}: ${errorMessage}`
);
} }
const id = NotificationConsoleUtils.logConsoleMessage( const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting query ${query.queryName}`);
ConsoleDataType.InProgress,
`Deleting query ${query.queryName}`
);
query.id = query.queryName; query.id = query.queryName;
const documentId = new DocumentId( const documentId = new DocumentId(
{ {
@@ -201,10 +170,7 @@ export class QueriesClient {
return deleteDocument(queriesCollection, documentId) return deleteDocument(queriesCollection, documentId)
.then( .then(
() => { () => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleInfo(`Successfully deleted query ${query.queryName}`);
ConsoleDataType.Info,
`Successfully deleted query ${query.queryName}`
);
return Promise.resolve(); return Promise.resolve();
}, },
(error: any) => { (error: any) => {
@@ -212,7 +178,7 @@ export class QueriesClient {
return Promise.reject(error); return Promise.reject(error);
} }
) )
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); .finally(() => clearMessage());
} }
public getResourceId(): string { public getResourceId(): string {

View File

@@ -1,6 +1,5 @@
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

@@ -0,0 +1,25 @@
import { CollectionBase } from "../../Contracts/ViewModels";
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
export const createDocument = async (collection: CollectionBase, newDocument: unknown): Promise<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

@@ -0,0 +1,36 @@
import ConflictId from "../../Explorer/Tree/ConflictId";
import { CollectionBase } from "../../Contracts/ViewModels";
import { RequestOptions } from "@azure/cosmos";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
export const deleteConflict = async (collection: CollectionBase, conflictId: ConflictId): Promise<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

@@ -0,0 +1,25 @@
import { CollectionBase } from "../../Contracts/ViewModels";
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import DocumentId from "../../Explorer/Tree/DocumentId";
export const deleteDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<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

@@ -0,0 +1,48 @@
import { Collection } from "../../Contracts/ViewModels";
import { ClientDefaults, HttpHeaders } from "../Constants";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import StoredProcedure from "../../Explorer/Tree/StoredProcedure";
export interface ExecuteSprocResult {
result: StoredProcedure;
scriptLogs: string;
}
export const executeStoredProcedure = async (
collection: Collection,
storedProcedure: StoredProcedure,
partitionKeyValue: string,
params: string[]
): Promise<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

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

View File

@@ -1,13 +1,13 @@
import { getCommonQueryOptions } from "./DataAccessUtilityBase"; import { getCommonQueryOptions } from "./queryDocuments";
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", () => {
expect(getCommonQueryOptions({})).toMatchSnapshot(); expect(getCommonQueryOptions({})).toMatchSnapshot();
}); });
it("reads from localStorage", () => { it("reads from localStorage", () => {
LocalStorageUtility.setEntryNumber(StorageKey.ActualItemPerPage, 37); LocalStorageUtility.setEntryNumber(StorageKey.ActualItemPerPage, 37);
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, 17); LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, 17);
expect(getCommonQueryOptions({})).toMatchSnapshot(); expect(getCommonQueryOptions({})).toMatchSnapshot();
}); });
}); });

View File

@@ -0,0 +1,34 @@
import { Queries } from "../Constants";
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { client } from "../CosmosClient";
export const queryDocuments = (
databaseId: string,
containerId: string,
query: string,
options: FeedOptions
): QueryIterator<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

@@ -0,0 +1,26 @@
import { QueryResults } from "../../Contracts/ViewModels";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { MinimalQueryIterator, nextPage } from "../IteratorUtilities";
import { handleError } from "../ErrorHandlingUtils";
import { getEntityName } from "../DocumentUtility";
export const queryDocumentsPage = async (
resourceName: string,
documentsIterator: MinimalQueryIterator,
firstItemIndex: number
): Promise<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

@@ -0,0 +1,27 @@
import { Item } from "@azure/cosmos";
import { CollectionBase } from "../../Contracts/ViewModels";
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import DocumentId from "../../Explorer/Tree/DocumentId";
export const readDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<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

@@ -0,0 +1,32 @@
import { CollectionBase } from "../../Contracts/ViewModels";
import { Item } from "@azure/cosmos";
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import DocumentId from "../../Explorer/Tree/DocumentId";
export const updateDocument = async (
collection: CollectionBase,
documentId: DocumentId,
newDocument: Item
): Promise<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

@@ -362,7 +362,7 @@ export enum CollectionTabKind {
Gallery = 17, Gallery = 17,
NotebookViewer = 18, NotebookViewer = 18,
Schema = 19, Schema = 19,
SettingsV2 = 19 SettingsV2 = 20
} }
export enum TerminalKind { export enum TerminalKind {

View File

@@ -801,7 +801,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private onMaxAutoPilotThroughputChange = (newThroughput: number): void => private onMaxAutoPilotThroughputChange = (newThroughput: number): void =>
this.setState({ autoPilotThroughput: newThroughput }); this.setState({ autoPilotThroughput: newThroughput });
private onThroughputChange = (newThroughput: number): void => this.setState({ throughput: newThroughput }); private onThroughputChange = (newThroughput: number): void => {
if (newThroughput < 400) {
return alert("The value should not small than 400");
}
this.setState({ throughput: newThroughput });
};
private onAutoPilotSelected = (isAutoPilotSelected: boolean): void => private onAutoPilotSelected = (isAutoPilotSelected: boolean): void =>
this.setState({ isAutoPilotSelected: isAutoPilotSelected }); this.setState({ isAutoPilotSelected: isAutoPilotSelected });

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,
getEstimatedSpendElement, getEstimatedSpendingElement,
getEstimatedAutoscaleSpendElement,
manualToAutoscaleDisclaimerElement, manualToAutoscaleDisclaimerElement,
ttlWarning, ttlWarning,
indexingPolicynUnsavedWarningMessage, indexingPolicynUnsavedWarningMessage,
@@ -19,11 +19,37 @@ 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)}
@@ -31,9 +57,7 @@ class SettingsRenderUtilsTestComponent extends React.Component {
{getAutoPilotV3SpendElement(1000, true)} {getAutoPilotV3SpendElement(1000, true)}
{getAutoPilotV3SpendElement(undefined, true)} {getAutoPilotV3SpendElement(undefined, true)}
{getEstimatedSpendElement(1000, "mooncake", 2, false)} {getEstimatedSpendingElement(estimatedSpendingColumns, estimatedSpendingItems, 1000, 2, priceBreakdown, false)}
{getEstimatedAutoscaleSpendElement(1000, "mooncake", 2, false)}
{manualToAutoscaleDisclaimerElement} {manualToAutoscaleDisclaimerElement}
{ttlWarning} {ttlWarning}
@@ -69,4 +93,14 @@ 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,14 +3,13 @@ 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,
calculateEstimateNumber estimatedCostDisclaimer
} from "../../../Utils/PricingUtils"; } from "../../../Utils/PricingUtils";
import { import {
ITextFieldStyles, ITextFieldStyles,
@@ -32,10 +31,41 @@ 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 {
costType: JSX.Element;
}
export interface ManualEstimatedSpendingDisplayProps extends EstimatedSpendingDisplayProps {
hourly: JSX.Element;
daily: JSX.Element;
monthly: JSX.Element;
}
export interface AutoscaleEstimatedSpendingDisplayProps extends EstimatedSpendingDisplayProps {
minPerMonth: JSX.Element;
maxPerMonth: JSX.Element;
}
export interface PriceBreakdown {
hourlyPrice: number;
dailyPrice: number;
monthlyPrice: number;
pricePerRu: number;
currency: string;
currencySign: string;
}
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } }; export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } };
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = { export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
@@ -104,6 +134,16 @@ 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: {
@@ -130,6 +170,10 @@ export const messageBarStyles: Partial<IMessageBarStyles> = { root: { marginTop:
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,
@@ -165,63 +209,61 @@ export const getAutoPilotV3SpendElement = (
); );
}; };
export const getEstimatedAutoscaleSpendElement = ( export const getRuPriceBreakdown = (
throughput: number, throughput: number,
serverId: string, serverId: string,
regions: number, numberOfRegions: number,
multimaster: boolean isMultimaster: boolean,
): JSX.Element => { isAutoscale: boolean
const hourlyPrice: number = computeAutoscaleUsagePriceHourly(serverId, throughput, regions, multimaster); ): PriceBreakdown => {
const monthlyPrice: number = hourlyPrice * hoursInAMonth; const hourlyPrice: number = computeRUUsagePriceHourly({
const currency: string = getPriceCurrency(serverId); serverId: serverId,
const currencySign: string = getCurrencySign(serverId); requestUnits: throughput,
const pricePerRu = numberOfRegions: numberOfRegions,
getAutoscalePricePerRu(serverId, getMultimasterMultiplier(regions, multimaster)) * multimasterEnabled: isMultimaster,
getMultimasterMultiplier(regions, multimaster); isAutoscale: isAutoscale
});
return ( const basePricePerRu: number = isAutoscale
<Text id="autoscaleSpendElement"> ? getAutoscalePricePerRu(serverId, getMultimasterMultiplier(numberOfRegions, isMultimaster))
Estimated monthly cost ({currency}) is{" "} : getPricePerRu(serverId);
<b> return {
{currencySign} hourlyPrice: hourlyPrice,
{calculateEstimateNumber(monthlyPrice / 10)} dailyPrice: hourlyPrice * 24,
{` - `} monthlyPrice: hourlyPrice * hoursInAMonth,
{currencySign} pricePerRu: basePricePerRu * getMultimasterMultiplier(numberOfRegions, isMultimaster),
{calculateEstimateNumber(monthlyPrice)}{" "} currency: getPriceCurrency(serverId),
</b> currencySign: getCurrencySign(serverId)
({"regions: "} {regions}, {throughput / 10} - {throughput} RU/s, {currencySign} };
{pricePerRu}/RU)
</Text>
);
}; };
export const getEstimatedSpendElement = ( export const getEstimatedSpendingElement = (
estimatedSpendingColumns: IColumn[],
estimatedSpendingItems: EstimatedSpendingDisplayProps[],
throughput: number, throughput: number,
serverId: string, numberOfRegions: number,
regions: number, priceBreakdown: PriceBreakdown,
multimaster: boolean isAutoscale: boolean
): JSX.Element => { ): JSX.Element => {
const hourlyPrice: number = computeRUUsagePriceHourly(serverId, throughput, regions, multimaster); const ruRange: string = isAutoscale ? throughput / 10 + " RU/s - " : "";
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 (
<Text id="throughputSpendElement"> <Stack {...addMongoIndexStackProps} styles={mediumWidthStackStyles}>
Estimated cost ({currency}):{" "} <DetailsList
<b> disableSelectionZone
{currencySign} items={estimatedSpendingItems}
{calculateEstimateNumber(hourlyPrice)} hourly {` / `} columns={estimatedSpendingColumns}
{currencySign} selectionMode={SelectionMode.none}
{calculateEstimateNumber(dailyPrice)} daily {` / `} layoutMode={DetailsListLayoutMode.justified}
{currencySign} onRenderRow={onRenderRow}
{calculateEstimateNumber(monthlyPrice)} monthly{" "} />
</b> <Text id="throughputSpendElement">
({"regions: "} {regions}, {throughput}RU/s, {currencySign} ({"regions: "} {numberOfRegions}, {ruRange}
{pricePerRu}/RU) {throughput} RU/s, {priceBreakdown.currencySign}
</Text> {priceBreakdown.pricePerRu}/RU)
</Text>
<Text>
<em>{estimatedCostDisclaimer}</em>
</Text>
</Stack>
); );
}; };
@@ -265,6 +307,13 @@ 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

@@ -6,8 +6,6 @@ import {
IconButton, IconButton,
Text, Text,
SelectionMode, SelectionMode,
IDetailsRowProps,
DetailsRow,
IColumn, IColumn,
MessageBar, MessageBar,
MessageBarType, MessageBarType,
@@ -21,11 +19,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 {
@@ -140,10 +138,6 @@ 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
@@ -253,7 +247,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={this.onRenderRow} onRenderRow={onRenderRow}
layoutMode={DetailsListLayoutMode.justified} layoutMode={DetailsListLayoutMode.justified}
/> />
{this.renderIndexesToBeAdded()} {this.renderIndexesToBeAdded()}
@@ -279,7 +273,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={this.onRenderRow} onRenderRow={onRenderRow}
layoutMode={DetailsListLayoutMode.justified} layoutMode={DetailsListLayoutMode.justified}
/> />
)} )}

View File

@@ -54,7 +54,6 @@ 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", () => {
@@ -72,8 +71,7 @@ describe("ThroughputInputAutoPilotV3Component", () => {
wrapper.setProps({ wasAutopilotOriginallySet: true }); wrapper.setProps({ wasAutopilotOriginallySet: true });
wrapper.update(); wrapper.update();
expect(wrapper.exists("#autoscaleSpendElement")).toEqual(true); expect(wrapper.exists("#throughputSpendElement")).toEqual(true);
expect(wrapper.exists("#throughputSpendElement")).toEqual(false);
}); });
it("spendAck checkbox visible", () => { it("spendAck checkbox visible", () => {

View File

@@ -8,10 +8,15 @@ import {
checkBoxAndInputStackProps, checkBoxAndInputStackProps,
getChoiceGroupStyles, getChoiceGroupStyles,
messageBarStyles, messageBarStyles,
getEstimatedSpendElement, getEstimatedSpendingElement,
getEstimatedAutoscaleSpendElement,
getAutoPilotV3SpendElement, getAutoPilotV3SpendElement,
manualToAutoscaleDisclaimerElement manualToAutoscaleDisclaimerElement,
saveThroughputWarningMessage,
ManualEstimatedSpendingDisplayProps,
AutoscaleEstimatedSpendingDisplayProps,
PriceBreakdown,
getRuPriceBreakdown,
transparentDetailsHeaderStyle
} from "../../SettingsRenderUtils"; } from "../../SettingsRenderUtils";
import { import {
Text, Text,
@@ -23,7 +28,9 @@ import {
Label, Label,
Link, Link,
MessageBar, MessageBar,
MessageBarType MessageBarType,
FontIcon,
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";
@@ -32,7 +39,7 @@ 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 } from "../../../../../Utils/PricingUtils"; import { usageInGB, calculateEstimateNumber } from "../../../../../Utils/PricingUtils";
import { Features } from "../../../../../Common/Constants"; import { Features } from "../../../../../Common/Constants";
export interface ThroughputInputAutoPilotV3Props { export interface ThroughputInputAutoPilotV3Props {
@@ -165,33 +172,243 @@ 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 = getEstimatedSpendElement( estimatedSpend = this.getEstimatedManualSpendElement(
// 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 : offerThroughput, this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : this.props.throughputBaseline,
serverId, serverId,
regions, regions,
multimaster multimaster,
isDirty ? this.props.throughput : undefined
); );
} else { } else {
estimatedSpend = getEstimatedAutoscaleSpendElement( estimatedSpend = this.getEstimatedAutoscaleSpendElement(
this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline,
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 <></>;
@@ -318,6 +535,12 @@ 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"
@@ -349,13 +572,24 @@ 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 messageBarType={MessageBarType.warning}>{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,6 +8,21 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
} }
} }
> >
<StyledMessageBarBase
messageBarType={5}
>
<Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
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"
@@ -214,6 +229,19 @@ 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"
@@ -239,38 +267,142 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
type="number" type="number"
value="100" value="100"
/> />
<Text <Stack
id="throughputSpendElement" styles={
Object {
"root": Object {
"width": 600,
},
}
}
tokens={
Object {
"childrenGap": 10,
}
}
> >
Estimated cost ( <StyledWithViewportComponent
USD columns={
): Array [
Object {
<b> "fieldName": "costType",
$ "isResizable": true,
0.0080 "key": "costType",
hourly "maxWidth": 200,
/ "minWidth": 100,
$ "name": "",
0.19 "styles": Object {
daily "root": Object {
/ "selectors": Object {
$ ":hover": Object {
5.84 "background": "transparent",
monthly },
},
},
},
},
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
</Text>,
"hourly": <Text>
$
0.0080
</Text>,
"monthly": <Text>
$
5.84
</Text>,
},
]
}
layoutMode={1}
onRenderRow={[Function]}
selectionMode={0}
/>
<Text
id="throughputSpendElement"
>
(
regions:
</b> 1
( ,
regions: 100
RU/s,
1 $
, 0.00008
100 /RU)
RU/s, </Text>
$ <Text>
0.00008 <em>
/RU) *This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account
</Text> </em>
</Text>
</Stack>
<StyledCheckboxBase <StyledCheckboxBase
checked={false} checked={false}
id="spendAckCheckBox" id="spendAckCheckBox"
@@ -288,6 +420,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
} }
} }
/> />
<br />
</Stack> </Stack>
</Stack> </Stack>
`; `;
@@ -369,6 +502,19 @@ 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"
@@ -394,38 +540,143 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
type="number" type="number"
value="100" value="100"
/> />
<Text <Stack
id="throughputSpendElement" styles={
Object {
"root": Object {
"width": 600,
},
}
}
tokens={
Object {
"childrenGap": 10,
}
}
> >
Estimated cost ( <StyledWithViewportComponent
USD columns={
): Array [
Object {
<b> "fieldName": "costType",
$ "isResizable": true,
0.0080 "key": "costType",
hourly "maxWidth": 200,
/ "minWidth": 100,
$ "name": "",
0.19 "styles": Object {
daily "root": Object {
/ "selectors": Object {
$ ":hover": Object {
5.84 "background": "transparent",
monthly },
},
},
},
},
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
</Text>,
"hourly": <Text>
$
0.0080
</Text>,
"monthly": <Text>
$
5.84
</Text>,
},
]
}
layoutMode={1}
onRenderRow={[Function]}
selectionMode={0}
/>
<Text
id="throughputSpendElement"
>
(
regions:
</b> 1
( ,
regions: 100
RU/s,
1 $
, 0.00008
100 /RU)
RU/s, </Text>
$ <Text>
0.00008 <em>
/RU) *This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account
</Text> </em>
</Text>
</Stack>
<br />
</Stack> </Stack>
</Stack> </Stack>
`; `;

View File

@@ -731,7 +731,6 @@ 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],
@@ -1023,7 +1022,6 @@ 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],
@@ -1049,7 +1047,6 @@ 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],
@@ -2005,7 +2002,6 @@ 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],
@@ -2297,7 +2293,6 @@ 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],
@@ -2323,7 +2318,6 @@ 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],
@@ -3292,7 +3286,6 @@ 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],
@@ -3584,7 +3577,6 @@ 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],
@@ -3610,7 +3602,6 @@ 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],
@@ -4566,7 +4557,6 @@ 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],
@@ -4858,7 +4848,6 @@ 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],
@@ -4884,7 +4873,6 @@ 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],

View File

@@ -60,66 +60,100 @@ exports[`SettingsUtils functions render 1`] = `
</StyledLinkBase> </StyledLinkBase>
. .
</Text> </Text>
<Text <Stack
id="throughputSpendElement" styles={
Object {
"root": Object {
"width": 600,
},
}
}
tokens={
Object {
"childrenGap": 10,
}
}
> >
Estimated cost ( <StyledWithViewportComponent
RMB columns={
): Array [
Object {
<b> "fieldName": "costType",
¥ "isResizable": true,
1.02 "key": "costType",
hourly "maxWidth": 200,
/ "minWidth": 100,
¥ "name": "",
24.48 },
daily Object {
/ "fieldName": "hourly",
¥ "isResizable": true,
744.60 "key": "hourly",
monthly "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:
</b> 2
( ,
regions: 1000
RU/s,
2
,
1000
RU/s,
¥
0.00051
/RU)
</Text>
<Text
id="autoscaleSpendElement"
>
Estimated monthly cost (
RMB
) is
<b>
¥ ¥
111.69 0.00051
- /RU)
¥ </Text>
1116.90 <Text>
<em>
</b> *This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account
( </em>
regions: </Text>
</Stack>
2
,
100
-
1000
RU/s,
¥
0.000765
/RU)
</Text>
<Text <Text
id="manualToAutoscaleDisclaimerElement" id="manualToAutoscaleDisclaimerElement"
styles={ styles={

View File

@@ -267,6 +267,12 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel {
return true; return true;
}; };
public onThroughputCheck = (event: any) => {
if (event.value() < 400) {
window.alert("The value should not small than 400");
}
};
private _getSanitizedValue(): number { private _getSanitizedValue(): number {
let throughput = this.value(); let throughput = this.value();

View File

@@ -150,6 +150,9 @@
'aria-label': ariaLabel, 'aria-label': ariaLabel,
disabled: overrideWithAutoPilotSettings(), disabled: overrideWithAutoPilotSettings(),
required: isManualThroughputInputFieldRequired() required: isManualThroughputInputFieldRequired()
},
event:{
change: onThroughputCheck
}" }"
/> />
</div> </div>

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/DocumentClientUtilityBase"; import { createDocument } from "../../Common/dataAccess/createDocument";
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,12 +95,15 @@ 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
this.sampleDataFile.data.map(doc => { await Promise.all(
const subPromise = createDocument(collection, doc); this.sampleDataFile.data.map(async doc => {
subPromise.catch(reason => NotificationConsoleUtils.logConsoleError(reason)); try {
promises.push(subPromise); await createDocument(collection, doc);
}); } catch (error) {
await Promise.all(promises); NotificationConsoleUtils.logConsoleError(error);
}
})
);
} }
} }

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 EnvironmentUtility from "../Common/EnvironmentUtility"; import { normalizeArmEndpoint } 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";
@@ -121,7 +121,6 @@ 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>;
@@ -135,12 +134,10 @@ export default class Explorer {
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
@@ -278,7 +275,6 @@ 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) => {
@@ -318,9 +314,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.armEndpoint()); this.notebookWorkspaceManager = new NotebookWorkspaceManager();
this.arcadiaWorkspaces = ko.observableArray(); this.arcadiaWorkspaces = ko.observableArray();
this._arcadiaManager = new ArcadiaResourceManager(this.armEndpoint()); this._arcadiaManager = new ArcadiaResourceManager();
this._isAfecFeatureRegistered(Constants.AfecFeatures.StorageAnalytics).then(isRegistered => this._isAfecFeatureRegistered(Constants.AfecFeatures.StorageAnalytics).then(isRegistered =>
this.hasStorageAnalyticsAfecFeature(isRegistered) this.hasStorageAnalyticsAfecFeature(isRegistered)
); );
@@ -370,7 +366,6 @@ 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);
@@ -1012,9 +1007,7 @@ export default class Explorer {
this.isSynapseLinkUpdating(true); this.isSynapseLinkUpdating(true);
this._closeSynapseLinkModalDialog(); this._closeSynapseLinkModalDialog();
const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate( const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(this.databaseAccount().id);
this.databaseAccount().id
);
try { try {
const databaseAccount: DataModels.DatabaseAccount = await resourceProviderClient.patchAsync( const databaseAccount: DataModels.DatabaseAccount = await resourceProviderClient.patchAsync(
@@ -1754,61 +1747,59 @@ export default class Explorer {
inputs.extensionEndpoint = configContext.PROXY_PATH; inputs.extensionEndpoint = configContext.PROXY_PATH;
} }
const initPromise: Q.Promise<void> = inputs ? this.initDataExplorerWithFrameInputs(inputs) : Q(); this.initDataExplorerWithFrameInputs(inputs);
initPromise.then(() => { const openAction: ActionContracts.DataExplorerAction = message.openAction;
const openAction: ActionContracts.DataExplorerAction = message.openAction; if (!!openAction) {
if (!!openAction) { if (this.isRefreshingExplorer()) {
if (this.isRefreshingExplorer()) { const subscription = this.databases.subscribe((databases: ViewModels.Database[]) => {
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) { }
handleCachedDataMessage(message); if (message.actionType === ActionContracts.ActionType.TransmitCachedData) {
return; handleCachedDataMessage(message);
} return;
if (message.type) { }
switch (message.type) { if (message.type) {
case MessageTypes.UpdateLocationHash: switch (message.type) {
if (!message.locationHash) { case MessageTypes.UpdateLocationHash:
break; if (!message.locationHash) {
} break;
hasher.replaceHash(message.locationHash); }
RouteHandler.getInstance().parseHash(message.locationHash); hasher.replaceHash(message.locationHash);
break; RouteHandler.getInstance().parseHash(message.locationHash);
case MessageTypes.SendNotification: break;
if (!message.message) { case MessageTypes.SendNotification:
break; if (!message.message) {
} break;
NotificationConsoleUtils.logConsoleMessage( }
message.consoleDataType || ConsoleDataType.Info, NotificationConsoleUtils.logConsoleMessage(
message.message, message.consoleDataType || ConsoleDataType.Info,
message.id message.message,
); message.id
break; );
case MessageTypes.ClearNotification: break;
if (!message.id) { case MessageTypes.ClearNotification:
break; if (!message.id) {
} break;
NotificationConsoleUtils.clearInProgressMessageWithId(message.id); }
break; NotificationConsoleUtils.clearInProgressMessageWithId(message.id);
case MessageTypes.LoadingStatus: break;
if (!message.text) { case MessageTypes.LoadingStatus:
break; if (!message.text) {
} break;
this._setLoadingStatusText(message.text, message.title); }
break; this._setLoadingStatusText(message.text, message.title);
} break;
return;
} }
return;
}
this.splashScreenAdapter.forceRender(); this.splashScreenAdapter.forceRender();
});
} }
public findSelectedDatabase(): ViewModels.Database { public findSelectedDatabase(): ViewModels.Database {
@@ -1848,8 +1839,14 @@ export default class Explorer {
return false; return false;
} }
public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): Q.Promise<void> { 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;
@@ -1858,25 +1855,18 @@ 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);
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: this.armEndpoint() ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT)
}); });
updateUserContext({ updateUserContext({
@@ -1885,7 +1875,8 @@ 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,
@@ -1899,7 +1890,6 @@ export default class Explorer {
this.isAccountReady(true); this.isAccountReady(true);
} }
return Q();
} }
public setFeatureFlagsFromFlights(flights: readonly string[]): void { public setFeatureFlagsFromFlights(flights: readonly string[]): void {
@@ -2562,7 +2552,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 = this.armEndpoint(); const armEndpoint = configContext.ARM_ENDPOINT;
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
@@ -2571,7 +2561,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(this.armEndpoint()).getOrCreate(featureUri); const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri);
try { try {
const sparkNotebooksFeature: DataModels.AfecFeature = await resourceProviderClient.getAsync( const sparkNotebooksFeature: DataModels.AfecFeature = await resourceProviderClient.getAsync(
featureUri, featureUri,
@@ -2591,7 +2581,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 = this.armEndpoint(); const armEndpoint = configContext.ARM_ENDPOINT;
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
@@ -2599,7 +2589,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(this.armEndpoint()).getOrCreate(featureUri); const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri);
try { try {
const featureStatus: DataModels.AfecFeature = await resourceProviderClient.getAsync( const featureStatus: DataModels.AfecFeature = await resourceProviderClient.getAsync(
featureUri, featureUri,

View File

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

@@ -16,6 +16,7 @@ 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>;
@@ -668,7 +669,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
databaseId: this.databaseId() databaseId: this.databaseId()
}), }),
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(), subscriptionQuotaId: userContext.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(),
@@ -770,7 +771,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
collectionWithThroughputInShared: this.collectionWithThroughputInShared() collectionWithThroughputInShared: this.collectionWithThroughputInShared()
}), }),
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(), subscriptionQuotaId: userContext.quotaId,
defaultsCheck: { defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u", storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
throughput: offerThroughput, throughput: offerThroughput,
@@ -844,7 +845,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
collectionWithThroughputInShared: this.collectionWithThroughputInShared() collectionWithThroughputInShared: this.collectionWithThroughputInShared()
}), }),
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(), subscriptionQuotaId: userContext.quotaId,
defaultsCheck: { defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u", storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
throughput: offerThroughput, throughput: offerThroughput,
@@ -878,7 +879,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
collectionWithThroughputInShared: this.collectionWithThroughputInShared() collectionWithThroughputInShared: this.collectionWithThroughputInShared()
}, },
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(), subscriptionQuotaId: userContext.quotaId,
defaultsCheck: { defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u", storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
throughput: offerThroughput, throughput: offerThroughput,

View File

@@ -13,6 +13,7 @@ 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>;
@@ -250,7 +251,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: this.container.quotaId(), subscriptionQuotaId: userContext.quotaId,
defaultsCheck: { defaultsCheck: {
throughput: this.throughput(), throughput: this.throughput(),
flight: this.container.flight() flight: this.container.flight()
@@ -278,7 +279,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
}), }),
offerThroughput, offerThroughput,
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(), subscriptionQuotaId: userContext.quotaId,
defaultsCheck: { defaultsCheck: {
flight: this.container.flight() flight: this.container.flight()
}, },
@@ -342,7 +343,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
}), }),
offerThroughput: offerThroughput, offerThroughput: offerThroughput,
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(), subscriptionQuotaId: userContext.quotaId,
defaultsCheck: { defaultsCheck: {
flight: this.container.flight() flight: this.container.flight()
}, },
@@ -366,7 +367,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
}), }),
offerThroughput: offerThroughput, offerThroughput: offerThroughput,
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(), subscriptionQuotaId: userContext.quotaId,
defaultsCheck: { defaultsCheck: {
flight: this.container.flight() flight: this.container.flight()
}, },

View File

@@ -15,6 +15,7 @@ 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>;
@@ -299,7 +300,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
databaseId: this.keyspaceId() databaseId: this.keyspaceId()
}), }),
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(), subscriptionQuotaId: userContext.quotaId,
defaultsCheck: { defaultsCheck: {
storage: "u", storage: "u",
throughput: this.throughput(), throughput: this.throughput(),
@@ -353,7 +354,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
}), }),
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(), subscriptionQuotaId: userContext.quotaId,
defaultsCheck: { defaultsCheck: {
storage: "u", storage: "u",
throughput: this.throughput(), throughput: this.throughput(),
@@ -399,7 +400,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
}), }),
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(), subscriptionQuotaId: userContext.quotaId,
defaultsCheck: { defaultsCheck: {
storage: "u", storage: "u",
throughput: this.throughput(), throughput: this.throughput(),
@@ -429,7 +430,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
}, },
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(), subscriptionQuotaId: userContext.quotaId,
defaultsCheck: { defaultsCheck: {
storage: "u", storage: "u",
throughput: this.throughput(), throughput: this.throughput(),

View File

@@ -421,53 +421,47 @@ export default class TableEntityListViewModel extends DataTableViewModel {
* Note that this also means that we can get less entities than the requested download size in a successful call. * Note that this also means that we can get less entities than the requested download size in a successful call.
* See Microsoft Azure API Documentation at: https://msdn.microsoft.com/en-us/library/azure/dd135718.aspx * See Microsoft Azure API Documentation at: https://msdn.microsoft.com/en-us/library/azure/dd135718.aspx
*/ */
private prefetchData( private async prefetchData(
tableQuery: Entities.ITableQuery, tableQuery: Entities.ITableQuery,
downloadSize: number, downloadSize: number,
currentRetry: number = 0 currentRetry: number = 0
): Q.Promise<any> { ): Promise<IListTableEntitiesSegmentedResult> {
if (!this.cache.serverCallInProgress) { if (!this.cache.serverCallInProgress) {
this.cache.serverCallInProgress = true; this.cache.serverCallInProgress = true;
this.allDownloaded = false; this.allDownloaded = false;
this.lastPrefetchTime = new Date().getTime(); this.lastPrefetchTime = new Date().getTime();
var time = this.lastPrefetchTime; const time = this.lastPrefetchTime;
var promise: Q.Promise<IListTableEntitiesSegmentedResult>;
if (this._documentIterator && this.continuationToken) { if (this._documentIterator && this.continuationToken) {
// TODO handle Cassandra case // TODO handle Cassandra case
const response = await this._documentIterator.fetchNext();
const entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(response?.resources);
promise = Q(this._documentIterator.fetchNext().then(response => response.resources)).then( return {
(documents: any[]) => { Results: entities,
let entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(documents); ContinuationToken: this._documentIterator.hasMoreResults()
let finalEntities: IListTableEntitiesSegmentedResult = <IListTableEntitiesSegmentedResult>{ };
Results: entities,
ContinuationToken: this._documentIterator.hasMoreResults()
};
return Q.resolve(finalEntities);
}
);
} else if (this.continuationToken && this.queryTablesTab.container.isPreferredApiCassandra()) {
promise = this.queryTablesTab.container.tableDataClient.queryDocuments(
this.queryTablesTab.collection,
this.cqlQuery(),
true,
this.continuationToken
);
} else {
let query = this.sqlQuery();
if (this.queryTablesTab.container.isPreferredApiCassandra()) {
query = this.cqlQuery();
}
promise = this.queryTablesTab.container.tableDataClient.queryDocuments(
this.queryTablesTab.collection,
query,
true
);
} }
return promise
.then((result: IListTableEntitiesSegmentedResult) => { try {
let documents: IListTableEntitiesSegmentedResult;
if (this.continuationToken && this.queryTablesTab.container.isPreferredApiCassandra()) {
documents = await this.queryTablesTab.container.tableDataClient.queryDocuments(
this.queryTablesTab.collection,
this.cqlQuery(),
true,
this.continuationToken
);
} else {
const query = this.queryTablesTab.container.isPreferredApiCassandra() ? this.cqlQuery() : this.sqlQuery();
documents = await this.queryTablesTab.container.tableDataClient.queryDocuments(
this.queryTablesTab.collection,
query,
true
);
if (!this._documentIterator) { if (!this._documentIterator) {
this._documentIterator = result.iterator; this._documentIterator = documents.iterator;
} }
var actualDownloadSize: number = 0; var actualDownloadSize: number = 0;
@@ -478,11 +472,11 @@ export default class TableEntityListViewModel extends DataTableViewModel {
return Q.resolve(null); return Q.resolve(null);
} }
var entities = result.Results; var entities = documents.Results;
actualDownloadSize = entities.length; actualDownloadSize = entities.length;
// Queries can fetch no results and still return a continuation header. See prefetchAndRender() method. // Queries can fetch no results and still return a continuation header. See prefetchAndRender() method.
this.continuationToken = this.isCancelled ? null : result.ContinuationToken; this.continuationToken = this.isCancelled ? null : documents.ContinuationToken;
if (!this.continuationToken) { if (!this.continuationToken) {
this.allDownloaded = true; this.allDownloaded = true;
@@ -514,20 +508,22 @@ export default class TableEntityListViewModel extends DataTableViewModel {
// For #2.1, set prefetch exceeds maximum retry number and end prefetch. // For #2.1, set prefetch exceeds maximum retry number and end prefetch.
// For #2.2, go to next round prefetch. // For #2.2, go to next round prefetch.
if (this.allDownloaded || nextDownloadSize === 0) { if (this.allDownloaded || nextDownloadSize === 0) {
return Q.resolve(result); return documents;
} }
if (currentRetry >= TableEntityListViewModel._maximumNumberOfPrefetchRetries) { if (currentRetry >= TableEntityListViewModel._maximumNumberOfPrefetchRetries) {
result.ExceedMaximumRetries = true; documents.ExceedMaximumRetries = true;
return Q.resolve(result); return documents;
} }
return this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1);
}) return await this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1);
.catch((error: Error) => { }
this.cache.serverCallInProgress = false; } catch (error) {
return Q.reject(error); this.cache.serverCallInProgress = false;
}); throw error;
}
} }
return null;
return undefined;
} }
} }

View File

@@ -4,6 +4,7 @@ import Q from "q";
import { displayTokenRenewalPromptForStatus, getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; import { displayTokenRenewalPromptForStatus, getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { FeedOptions } from "@azure/cosmos";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import * as Entities from "./Entities"; import * as Entities from "./Entities";
import * as HeadersUtility from "../../Common/HeadersUtility"; import * as HeadersUtility from "../../Common/HeadersUtility";
@@ -12,9 +13,12 @@ import * as TableConstants from "./Constants";
import * as TableEntityProcessor from "./TableEntityProcessor"; import * as TableEntityProcessor from "./TableEntityProcessor";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { queryDocuments, deleteDocument, updateDocument, createDocument } from "../../Common/DocumentClientUtilityBase";
import { configContext } from "../../ConfigContext"; import { configContext } from "../../ConfigContext";
import { handleError } from "../../Common/ErrorHandlingUtils"; import { handleError } from "../../Common/ErrorHandlingUtils";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
import { updateDocument } from "../../Common/dataAccess/updateDocument";
export interface CassandraTableKeys { export interface CassandraTableKeys {
partitionKeys: CassandraTableKey[]; partitionKeys: CassandraTableKey[];
@@ -38,19 +42,19 @@ export abstract class TableDataClient {
collection: ViewModels.Collection, collection: ViewModels.Collection,
originalDocument: any, originalDocument: any,
newEntity: Entities.ITableEntity newEntity: Entities.ITableEntity
): Q.Promise<Entities.ITableEntity>; ): Promise<Entities.ITableEntity>;
public abstract queryDocuments( public abstract queryDocuments(
collection: ViewModels.Collection, collection: ViewModels.Collection,
query: string, query: string,
shouldNotify?: boolean, shouldNotify?: boolean,
paginationToken?: string paginationToken?: string
): Q.Promise<Entities.IListTableEntitiesResult>; ): Promise<Entities.IListTableEntitiesResult>;
public abstract deleteDocuments( public abstract deleteDocuments(
collection: ViewModels.Collection, collection: ViewModels.Collection,
entitiesToDelete: Entities.ITableEntity[] entitiesToDelete: Entities.ITableEntity[]
): Q.Promise<any>; ): Promise<any>;
} }
export class TablesAPIDataClient extends TableDataClient { export class TablesAPIDataClient extends TableDataClient {
@@ -74,77 +78,63 @@ export class TablesAPIDataClient extends TableDataClient {
return deferred.promise; return deferred.promise;
} }
public updateDocument( public async updateDocument(
collection: ViewModels.Collection, collection: ViewModels.Collection,
originalDocument: any, originalDocument: any,
entity: Entities.ITableEntity entity: Entities.ITableEntity
): Q.Promise<Entities.ITableEntity> { ): Promise<Entities.ITableEntity> {
const deferred = Q.defer<Entities.ITableEntity>(); try {
const newDocument = await updateDocument(
updateDocument( collection,
collection, originalDocument,
originalDocument, TableEntityProcessor.convertEntityToNewDocument(<Entities.ITableEntityForTablesAPI>entity)
TableEntityProcessor.convertEntityToNewDocument(<Entities.ITableEntityForTablesAPI>entity) );
).then( return TableEntityProcessor.convertDocumentsToEntities([newDocument])[0];
(newDocument: any) => { } catch (error) {
const newEntity = TableEntityProcessor.convertDocumentsToEntities([newDocument])[0]; handleError(error, "TablesAPIDataClient/updateDocument");
deferred.resolve(newEntity); throw error;
}, }
reason => {
deferred.reject(reason);
}
);
return deferred.promise;
} }
public queryDocuments( public async queryDocuments(
collection: ViewModels.Collection, collection: ViewModels.Collection,
query: string query: string
): Q.Promise<Entities.IListTableEntitiesResult> { ): Promise<Entities.IListTableEntitiesResult> {
const deferred = Q.defer<Entities.IListTableEntitiesResult>(); try {
const options = {
enableCrossPartitionQuery: HeadersUtility.shouldEnableCrossPartitionKey()
} as FeedOptions;
const iterator = queryDocuments(collection.databaseId, collection.id(), query, options);
const response = await iterator.fetchNext();
const documents = response?.resources;
const entities = TableEntityProcessor.convertDocumentsToEntities(documents);
let options: any = {}; return {
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); Results: entities,
queryDocuments(collection.databaseId, collection.id(), query, options).then( ContinuationToken: iterator.hasMoreResults(),
iterator => { iterator: iterator
iterator };
.fetchNext() } catch (error) {
.then(response => response.resources) handleError(error, "TablesAPIDataClient/queryDocuments", "Query documents failed");
.then( throw error;
(documents: any[] = []) => { }
let entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(documents);
let finalEntities: Entities.IListTableEntitiesResult = <Entities.IListTableEntitiesResult>{
Results: entities,
ContinuationToken: iterator.hasMoreResults(),
iterator: iterator
};
deferred.resolve(finalEntities);
},
reason => {
deferred.reject(reason);
}
);
},
reason => {
deferred.reject(reason);
}
);
return deferred.promise;
} }
public deleteDocuments(collection: ViewModels.Collection, entitiesToDelete: Entities.ITableEntity[]): Q.Promise<any> { public async deleteDocuments(
let documentsToDelete: any[] = TableEntityProcessor.convertEntitiesToDocuments( collection: ViewModels.Collection,
entitiesToDelete: Entities.ITableEntity[]
): Promise<any> {
const documentsToDelete: any[] = TableEntityProcessor.convertEntitiesToDocuments(
<Entities.ITableEntityForTablesAPI[]>entitiesToDelete, <Entities.ITableEntityForTablesAPI[]>entitiesToDelete,
collection collection
); );
let promiseArray: Q.Promise<any>[] = [];
documentsToDelete && await Promise.all(
documentsToDelete.forEach(document => { documentsToDelete?.map(async document => {
document.id = ko.observable<string>(document.id); document.id = ko.observable<string>(document.id);
let promise: Q.Promise<any> = deleteDocument(collection, document); await deleteDocument(collection, document);
promiseArray.push(promise); })
}); );
return Q.all(promiseArray);
} }
} }
@@ -180,10 +170,7 @@ export class CassandraAPIDataClient extends TableDataClient {
(data: any) => { (data: any) => {
entity[TableConstants.EntityKeyNames.RowKey] = entity[this.getCassandraPartitionKeyProperty(collection)]; entity[TableConstants.EntityKeyNames.RowKey] = entity[this.getCassandraPartitionKeyProperty(collection)];
entity[TableConstants.EntityKeyNames.RowKey]._ = entity[TableConstants.EntityKeyNames.RowKey]._.toString(); entity[TableConstants.EntityKeyNames.RowKey]._ = entity[TableConstants.EntityKeyNames.RowKey]._.toString();
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleInfo(`Successfully added new row to table ${collection.id()}`);
ConsoleDataType.Info,
`Successfully added new row to table ${collection.id()}`
);
deferred.resolve(entity); deferred.resolve(entity);
}, },
error => { error => {
@@ -197,181 +184,149 @@ export class CassandraAPIDataClient extends TableDataClient {
return deferred.promise; return deferred.promise;
} }
public updateDocument( public async updateDocument(
collection: ViewModels.Collection, collection: ViewModels.Collection,
originalDocument: any, originalDocument: any,
newEntity: Entities.ITableEntity newEntity: Entities.ITableEntity
): Q.Promise<Entities.ITableEntity> { ): Promise<Entities.ITableEntity> {
const notificationId = NotificationConsoleUtils.logConsoleMessage( const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Updating row ${originalDocument.RowKey._}`);
ConsoleDataType.InProgress,
`Updating row ${originalDocument.RowKey._}` try {
); let whereSegment = " WHERE";
const deferred = Q.defer<Entities.ITableEntity>(); let keys: CassandraTableKey[] = collection.cassandraKeys.partitionKeys.concat(
let promiseArray: Q.Promise<any>[] = []; collection.cassandraKeys.clusteringKeys
let query = `UPDATE ${collection.databaseId}.${collection.id()}`; );
let isChange: boolean = false; for (let keyIndex in keys) {
for (let property in newEntity) { const key = keys[keyIndex].property;
if (!originalDocument[property] || newEntity[property]._.toString() !== originalDocument[property]._.toString()) { const keyType = keys[keyIndex].type;
if (this.isStringType(newEntity[property].$)) { whereSegment += this.isStringType(keyType)
query = `${query} SET ${property} = '${newEntity[property]._}',`; ? ` ${key} = '${newEntity[key]._}' AND`
} else { : ` ${key} = ${newEntity[key]._} AND`;
query = `${query} SET ${property} = ${newEntity[property]._},`; }
whereSegment = whereSegment.slice(0, whereSegment.length - 4);
let updateQuery = `UPDATE ${collection.databaseId}.${collection.id()}`;
let isPropertyUpdated = false;
for (let property in newEntity) {
if (
!originalDocument[property] ||
newEntity[property]._.toString() !== originalDocument[property]._.toString()
) {
updateQuery += this.isStringType(newEntity[property].$)
? ` SET ${property} = '${newEntity[property]._}',`
: ` SET ${property} = ${newEntity[property]._},`;
isPropertyUpdated = true;
} }
isChange = true;
} }
}
query = query.slice(0, query.length - 1); if (isPropertyUpdated) {
let whereSegment = " WHERE"; updateQuery = updateQuery.slice(0, updateQuery.length - 1);
let keys: CassandraTableKey[] = collection.cassandraKeys.partitionKeys.concat( updateQuery += whereSegment;
collection.cassandraKeys.clusteringKeys await this.queryDocuments(collection, updateQuery);
);
for (let keyIndex in keys) {
const key = keys[keyIndex].property;
const keyType = keys[keyIndex].type;
if (this.isStringType(keyType)) {
whereSegment = `${whereSegment} ${key} = '${newEntity[key]._}' AND`;
} else {
whereSegment = `${whereSegment} ${key} = ${newEntity[key]._} AND`;
} }
}
whereSegment = whereSegment.slice(0, whereSegment.length - 4); let deleteQuery = `DELETE `;
query = query + whereSegment; let isPropertyDeleted = false;
if (isChange) { for (let property in originalDocument) {
promiseArray.push(this.queryDocuments(collection, query)); if (property !== TableConstants.EntityKeyNames.RowKey && !newEntity[property] && !!originalDocument[property]) {
} deleteQuery += ` ${property},`;
query = `DELETE `; isPropertyDeleted = true;
for (let property in originalDocument) {
if (property !== TableConstants.EntityKeyNames.RowKey && !newEntity[property] && !!originalDocument[property]) {
query = `${query} ${property},`;
}
}
if (query.length > 7) {
query = query.slice(0, query.length - 1);
query = `${query} FROM ${collection.databaseId}.${collection.id()}${whereSegment}`;
promiseArray.push(this.queryDocuments(collection, query));
}
Q.all(promiseArray)
.then(
(data: any) => {
newEntity[TableConstants.EntityKeyNames.RowKey] = originalDocument[TableConstants.EntityKeyNames.RowKey];
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully updated row ${newEntity.RowKey._}`
);
deferred.resolve(newEntity);
},
error => {
handleError(error, "UpdateRowCassandra", `Failed to update row ${newEntity.RowKey._}`);
deferred.reject(error);
} }
) }
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); if (isPropertyDeleted) {
}); deleteQuery = deleteQuery.slice(0, deleteQuery.length - 1);
return deferred.promise; deleteQuery += ` FROM ${collection.databaseId}.${collection.id()}${whereSegment}`;
await this.queryDocuments(collection, deleteQuery);
}
newEntity[TableConstants.EntityKeyNames.RowKey] = originalDocument[TableConstants.EntityKeyNames.RowKey];
NotificationConsoleUtils.logConsoleInfo(`Successfully updated row ${newEntity.RowKey._}`);
return newEntity;
} catch (error) {
handleError(error, "UpdateRowCassandra", "Failed to update row ${newEntity.RowKey._}");
throw error;
} finally {
clearMessage();
}
} }
public queryDocuments( public async queryDocuments(
collection: ViewModels.Collection, collection: ViewModels.Collection,
query: string, query: string,
shouldNotify?: boolean, shouldNotify?: boolean,
paginationToken?: string paginationToken?: string
): Q.Promise<Entities.IListTableEntitiesResult> { ): Promise<Entities.IListTableEntitiesResult> {
let notificationId: string; const clearMessage =
if (shouldNotify) { shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`);
notificationId = NotificationConsoleUtils.logConsoleMessage( try {
ConsoleDataType.InProgress, const authType = window.authType;
`Querying rows for table ${collection.id()}` const apiEndpoint: string =
); authType === AuthType.EncryptedToken
} ? Constants.CassandraBackend.guestQueryApi
const deferred = Q.defer<Entities.IListTableEntitiesResult>(); : Constants.CassandraBackend.queryApi;
const authType = window.authType; const data: any = await $.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
const apiEndpoint: string = type: "POST",
authType === AuthType.EncryptedToken data: {
? Constants.CassandraBackend.guestQueryApi accountName:
: Constants.CassandraBackend.queryApi; collection && collection.container.databaseAccount && collection.container.databaseAccount().name,
$.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, { cassandraEndpoint: this.trimCassandraEndpoint(
type: "POST", collection.container.databaseAccount().properties.cassandraEndpoint
data: { ),
accountName: collection && collection.container.databaseAccount && collection.container.databaseAccount().name, resourceId: collection.container.databaseAccount().id,
cassandraEndpoint: this.trimCassandraEndpoint( keyspaceId: collection.databaseId,
collection.container.databaseAccount().properties.cassandraEndpoint tableId: collection.id(),
), query,
resourceId: collection.container.databaseAccount().id, paginationToken
keyspaceId: collection.databaseId,
tableId: collection.id(),
query: query,
paginationToken: paginationToken
},
beforeSend: this.setAuthorizationHeader,
error: this.handleAjaxError,
cache: false
})
.then(
(data: any) => {
if (shouldNotify) {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully fetched ${data.result.length} rows for table ${collection.id()}`
);
}
deferred.resolve({
Results: data.result,
ContinuationToken: data.paginationToken
});
}, },
(error: any) => { beforeSend: this.setAuthorizationHeader,
if (shouldNotify) { error: this.handleAjaxError,
handleError(error, "QueryDocumentsCassandra", `Failed to query rows for table ${collection.id()}`); cache: false
}
deferred.reject(error);
}
)
.done(() => {
if (shouldNotify) {
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
}
}); });
return deferred.promise; shouldNotify &&
NotificationConsoleUtils.logConsoleInfo(
`Successfully fetched ${data.result.length} rows for table ${collection.id()}`
);
return {
Results: data.result,
ContinuationToken: data.paginationToken
};
} catch (error) {
shouldNotify &&
handleError(error, "QueryDocumentsCassandra", `Failed to query rows for table ${collection.id()}`);
throw error;
} finally {
clearMessage?.();
}
} }
public deleteDocuments(collection: ViewModels.Collection, entitiesToDelete: Entities.ITableEntity[]): Q.Promise<any> { public async deleteDocuments(
collection: ViewModels.Collection,
entitiesToDelete: Entities.ITableEntity[]
): Promise<any> {
const query = `DELETE FROM ${collection.databaseId}.${collection.id()} WHERE `; const query = `DELETE FROM ${collection.databaseId}.${collection.id()} WHERE `;
let promiseArray: Q.Promise<any>[] = []; const partitionKeyProperty = this.getCassandraPartitionKeyProperty(collection);
let partitionKeyProperty = this.getCassandraPartitionKeyProperty(collection);
for (let i = 0, len = entitiesToDelete.length; i < len; i++) { await Promise.all(
let currEntityToDelete: Entities.ITableEntity = entitiesToDelete[i]; entitiesToDelete.map(async (currEntityToDelete: Entities.ITableEntity) => {
let currQuery = query; const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting row ${currEntityToDelete.RowKey._}`);
let partitionKeyValue = currEntityToDelete[partitionKeyProperty]; const partitionKeyValue = currEntityToDelete[partitionKeyProperty];
if (partitionKeyValue._ != null && this.isStringType(partitionKeyValue.$)) { const currQuery =
currQuery = `${currQuery}${partitionKeyProperty} = '${partitionKeyValue._}' AND `; query + this.isStringType(partitionKeyValue.$)
} else { ? `${partitionKeyProperty} = '${partitionKeyValue._}'`
currQuery = `${currQuery}${partitionKeyProperty} = ${partitionKeyValue._} AND `; : `${partitionKeyProperty} = ${partitionKeyValue._}`;
}
currQuery = currQuery.slice(0, currQuery.length - 5); try {
const notificationId = NotificationConsoleUtils.logConsoleMessage( await this.queryDocuments(collection, currQuery);
ConsoleDataType.InProgress, NotificationConsoleUtils.logConsoleInfo(`Successfully deleted row ${currEntityToDelete.RowKey._}`);
`Deleting row ${currEntityToDelete.RowKey._}` } catch (error) {
); handleError(error, "DeleteRowCassandra", `Error while deleting row ${currEntityToDelete.RowKey._}`);
promiseArray.push( throw error;
this.queryDocuments(collection, currQuery) } finally {
.then( clearMessage();
() => { }
NotificationConsoleUtils.logConsoleMessage( })
ConsoleDataType.Info, );
`Successfully deleted row ${currEntityToDelete.RowKey._}`
);
},
error => {
handleError(error, "DeleteRowCassandra", `Error while deleting row ${currEntityToDelete.RowKey._}`);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
})
);
}
return Q.all(promiseArray);
} }
public createKeyspace( public createKeyspace(

View File

@@ -16,18 +16,16 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import SaveIcon from "../../../images/save-cosmos.svg"; import SaveIcon from "../../../images/save-cosmos.svg";
import DiscardIcon from "../../../images/discard.svg"; import DiscardIcon from "../../../images/discard.svg";
import DeleteIcon from "../../../images/delete.svg"; import DeleteIcon from "../../../images/delete.svg";
import { QueryIterator, ItemDefinition, Resource, ConflictDefinition } from "@azure/cosmos"; import { QueryIterator, Resource, ConflictDefinition, FeedOptions } from "@azure/cosmos";
import { MinimalQueryIterator } from "../../Common/IteratorUtilities"; import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import {
queryConflicts,
deleteConflict,
deleteDocument,
createDocument,
updateDocument
} from "../../Common/DocumentClientUtilityBase";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { updateDocument } from "../../Common/dataAccess/updateDocument";
import { deleteConflict } from "../../Common/dataAccess/deleteConflict";
import { queryConflicts } from "../../Common/dataAccess/queryConflicts";
export default class ConflictsTab extends TabsBase { export default class ConflictsTab extends TabsBase {
public selectedConflictId: ko.Observable<ConflictId>; public selectedConflictId: ko.Observable<ConflictId>;
@@ -225,25 +223,15 @@ export default class ConflictsTab extends TabsBase {
}); });
} }
public refreshDocumentsGrid(): Q.Promise<any> { public async refreshDocumentsGrid(): Promise<void> {
// clear documents grid try {
this.conflictIds([]); // clear documents grid
return this.createIterator() this.conflictIds([]);
.then( this._documentsIterator = this.createIterator();
// reset iterator await this.loadNextPage();
iterator => { } catch (error) {
this._documentsIterator = iterator; window.alert(getErrorMessage(error));
} }
)
.then(
// load documents
() => {
return this.loadNextPage();
}
)
.catch(error => {
window.alert(getErrorMessage(error));
});
} }
public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => { public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => {
@@ -265,9 +253,9 @@ export default class ConflictsTab extends TabsBase {
return Q(); return Q();
} }
public onAcceptChangesClick = (): Q.Promise<any> => { public onAcceptChangesClick = async (): Promise<void> => {
if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) { if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) {
return Q(); return;
} }
this.isExecutionError(false); this.isExecutionError(false);
@@ -285,81 +273,79 @@ export default class ConflictsTab extends TabsBase {
conflictResourceId: selectedConflict.resourceId conflictResourceId: selectedConflict.resourceId
}); });
let operationPromise: Q.Promise<any> = Q(); try {
if (selectedConflict.operationType === Constants.ConflictOperationType.Replace) { if (selectedConflict.operationType === Constants.ConflictOperationType.Replace) {
const documentContent = JSON.parse(this.selectedConflictContent()); const documentContent = JSON.parse(this.selectedConflictContent());
operationPromise = updateDocument( await updateDocument(
this.collection, this.collection,
selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty]), selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty]),
documentContent documentContent
); );
} }
if (selectedConflict.operationType === Constants.ConflictOperationType.Create) { if (selectedConflict.operationType === Constants.ConflictOperationType.Create) {
const documentContent = JSON.parse(this.selectedConflictContent()); const documentContent = JSON.parse(this.selectedConflictContent());
operationPromise = createDocument(this.collection, documentContent); await createDocument(this.collection, documentContent);
} }
if (selectedConflict.operationType === Constants.ConflictOperationType.Delete && !!this.selectedConflictContent()) { if (
const documentContent = JSON.parse(this.selectedConflictContent()); selectedConflict.operationType === Constants.ConflictOperationType.Delete &&
!!this.selectedConflictContent()
) {
const documentContent = JSON.parse(this.selectedConflictContent());
operationPromise = deleteDocument( await deleteDocument(
this.collection, this.collection,
selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty]) selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty])
); );
} }
return operationPromise await deleteConflict(this.collection, selectedConflict);
.then( this.conflictIds.remove((conflictId: ConflictId) => conflictId.rid === selectedConflict.rid);
() => { this.selectedConflictContent("");
return deleteConflict(this.collection, selectedConflict).then(() => { this.selectedConflictCurrent("");
this.conflictIds.remove((conflictId: ConflictId) => conflictId.rid === selectedConflict.rid); this.selectedConflictId(null);
this.selectedConflictContent(""); this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
this.selectedConflictCurrent(""); TelemetryProcessor.traceSuccess(
this.selectedConflictId(null); Action.ResolveConflict,
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); {
TelemetryProcessor.traceSuccess( databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
Action.ResolveConflict, defaultExperience: this.collection && this.collection.container.defaultExperience(),
{ dataExplorerArea: Constants.Areas.Tab,
databaseAccountName: this.collection && this.collection.container.databaseAccount().name, tabTitle: this.tabTitle(),
defaultExperience: this.collection && this.collection.container.defaultExperience(), conflictResourceType: selectedConflict.resourceType,
dataExplorerArea: Constants.Areas.Tab, conflictOperationType: selectedConflict.operationType,
tabTitle: this.tabTitle(), conflictResourceId: selectedConflict.resourceId
conflictResourceType: selectedConflict.resourceType,
conflictOperationType: selectedConflict.operationType,
conflictResourceId: selectedConflict.resourceId
},
startKey
);
});
}, },
error => { startKey
this.isExecutionError(true); );
const errorMessage = getErrorMessage(error); } catch (error) {
window.alert(errorMessage); this.isExecutionError(true);
TelemetryProcessor.traceFailure( const errorMessage = getErrorMessage(error);
Action.ResolveConflict, window.alert(errorMessage);
{ TelemetryProcessor.traceFailure(
databaseAccountName: this.collection && this.collection.container.databaseAccount().name, Action.ResolveConflict,
defaultExperience: this.collection && this.collection.container.defaultExperience(), {
dataExplorerArea: Constants.Areas.Tab, databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
tabTitle: this.tabTitle(), defaultExperience: this.collection && this.collection.container.defaultExperience(),
conflictResourceType: selectedConflict.resourceType, dataExplorerArea: Constants.Areas.Tab,
conflictOperationType: selectedConflict.operationType, tabTitle: this.tabTitle(),
conflictResourceId: selectedConflict.resourceId, conflictResourceType: selectedConflict.resourceType,
error: errorMessage, conflictOperationType: selectedConflict.operationType,
errorStack: getErrorStack(error) conflictResourceId: selectedConflict.resourceId,
}, error: errorMessage,
startKey errorStack: getErrorStack(error)
); },
} startKey
) );
.finally(() => this.isExecuting(false)); } finally {
this.isExecuting(false);
}
}; };
public onDeleteClick = (): Q.Promise<any> => { public onDeleteClick = async (): Promise<void> => {
this.isExecutionError(false); this.isExecutionError(false);
this.isExecuting(true); this.isExecuting(true);
@@ -375,50 +361,48 @@ export default class ConflictsTab extends TabsBase {
conflictResourceId: selectedConflict.resourceId conflictResourceId: selectedConflict.resourceId
}); });
return deleteConflict(this.collection, selectedConflict) try {
.then( await deleteConflict(this.collection, selectedConflict);
() => { this.conflictIds.remove((conflictId: ConflictId) => conflictId.rid === selectedConflict.rid);
this.conflictIds.remove((conflictId: ConflictId) => conflictId.rid === selectedConflict.rid); this.selectedConflictContent("");
this.selectedConflictContent(""); this.selectedConflictCurrent("");
this.selectedConflictCurrent(""); this.selectedConflictId(null);
this.selectedConflictId(null); this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); TelemetryProcessor.traceSuccess(
TelemetryProcessor.traceSuccess( Action.DeleteConflict,
Action.DeleteConflict, {
{ databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
databaseAccountName: this.collection && this.collection.container.databaseAccount().name, defaultExperience: this.collection && this.collection.container.defaultExperience(),
defaultExperience: this.collection && this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab,
dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(),
tabTitle: this.tabTitle(), conflictResourceType: selectedConflict.resourceType,
conflictResourceType: selectedConflict.resourceType, conflictOperationType: selectedConflict.operationType,
conflictOperationType: selectedConflict.operationType, conflictResourceId: selectedConflict.resourceId
conflictResourceId: selectedConflict.resourceId
},
startKey
);
}, },
error => { startKey
this.isExecutionError(true); );
const errorMessage = getErrorMessage(error); } catch (error) {
window.alert(errorMessage); this.isExecutionError(true);
TelemetryProcessor.traceFailure( const errorMessage = getErrorMessage(error);
Action.DeleteConflict, window.alert(errorMessage);
{ TelemetryProcessor.traceFailure(
databaseAccountName: this.collection && this.collection.container.databaseAccount().name, Action.DeleteConflict,
defaultExperience: this.collection && this.collection.container.defaultExperience(), {
dataExplorerArea: Constants.Areas.Tab, databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
tabTitle: this.tabTitle(), defaultExperience: this.collection && this.collection.container.defaultExperience(),
conflictResourceType: selectedConflict.resourceType, dataExplorerArea: Constants.Areas.Tab,
conflictOperationType: selectedConflict.operationType, tabTitle: this.tabTitle(),
conflictResourceId: selectedConflict.resourceId, conflictResourceType: selectedConflict.resourceType,
error: errorMessage, conflictOperationType: selectedConflict.operationType,
errorStack: getErrorStack(error) conflictResourceId: selectedConflict.resourceId,
}, error: errorMessage,
startKey errorStack: getErrorStack(error)
); },
} startKey
) );
.finally(() => this.isExecuting(false)); } finally {
this.isExecuting(false);
}
}; };
public onDiscardClick = (): Q.Promise<any> => { public onDiscardClick = (): Q.Promise<any> => {
@@ -445,60 +429,47 @@ export default class ConflictsTab extends TabsBase {
return Q(); return Q();
} }
public onTabClick(): Q.Promise<any> { public onTabClick(): void {
return super.onTabClick().then(() => { super.onTabClick();
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Conflicts); this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Conflicts);
});
} }
public onActivate(): Q.Promise<any> { public async onActivate(): Promise<void> {
return super.onActivate().then(() => { super.onActivate();
if (this._documentsIterator) {
return Q.resolve(this._documentsIterator);
}
return this.createIterator().then( if (!this._documentsIterator) {
(iterator: QueryIterator<ItemDefinition & Resource>) => { try {
this._documentsIterator = iterator; this._documentsIterator = await this.createIterator();
return this.loadNextPage(); await this.loadNextPage();
}, } catch (error) {
error => { if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { TelemetryProcessor.traceFailure(
TelemetryProcessor.traceFailure( Action.Tab,
Action.Tab, {
{ databaseAccountName: this.collection.container.databaseAccount().name,
databaseAccountName: this.collection.container.databaseAccount().name, databaseName: this.collection.databaseId,
databaseName: this.collection.databaseId, collectionName: this.collection.id(),
collectionName: this.collection.id(), defaultExperience: this.collection.container.defaultExperience(),
defaultExperience: this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab,
dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(),
tabTitle: this.tabTitle(), error: getErrorMessage(error),
error: getErrorMessage(error), errorStack: getErrorStack(error)
errorStack: getErrorStack(error) },
}, this.onLoadStartKey
this.onLoadStartKey );
); this.onLoadStartKey = null;
this.onLoadStartKey = null;
}
} }
); }
}); }
} }
public onRefreshClick(): Q.Promise<any> { public createIterator(): QueryIterator<ConflictDefinition & Resource> {
return this.refreshDocumentsGrid().then(() => {
this.selectedConflictContent("");
this.selectedConflictId(null);
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
});
}
public createIterator(): Q.Promise<QueryIterator<ConflictDefinition & Resource>> {
// TODO: Conflict Feed does not allow filtering atm // TODO: Conflict Feed does not allow filtering atm
const query: string = undefined; const query: string = undefined;
let options: any = {}; const options = {
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); enableCrossPartitionQuery: HeadersUtility.shouldEnableCrossPartitionKey()
return queryConflicts(this.collection.databaseId, this.collection.id(), query, options); };
return queryConflicts(this.collection.databaseId, this.collection.id(), query, options as FeedOptions);
} }
public loadNextPage(): Q.Promise<any> { public loadNextPage(): Q.Promise<any> {

View File

@@ -429,11 +429,10 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
return Q(); return Q();
}; };
public onActivate(): Q.Promise<any> { public async onActivate(): Promise<void> {
return super.onActivate().then(async () => { super.onActivate();
this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings); this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
await this.database.loadOffer(); await this.database.loadOffer();
});
} }
private _setBaseline() { private _setBaseline() {

View File

@@ -103,7 +103,7 @@
<button <button
class="filterbtnstyle queryButton" class="filterbtnstyle queryButton"
data-bind=" data-bind="
click: onApplyFilterClick, click: refreshDocumentsGrid,
enable: applyFilterButton.enabled" enable: applyFilterButton.enabled"
aria-label="Apply filter" aria-label="Apply filter"
tabindex="0" tabindex="0"

View File

@@ -19,19 +19,24 @@ import SaveIcon from "../../../images/save-cosmos.svg";
import DiscardIcon from "../../../images/discard.svg"; import DiscardIcon from "../../../images/discard.svg";
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg"; import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
import UploadIcon from "../../../images/Upload_16x16.svg"; import UploadIcon from "../../../images/Upload_16x16.svg";
import { extractPartitionKey, PartitionKeyDefinition, QueryIterator, ItemDefinition, Resource } from "@azure/cosmos"; import {
extractPartitionKey,
PartitionKeyDefinition,
QueryIterator,
ItemDefinition,
Resource,
Item
} from "@azure/cosmos";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import {
readDocument,
queryDocuments,
deleteDocument,
updateDocument,
createDocument
} from "../../Common/DocumentClientUtilityBase";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
import { readDocument } from "../../Common/dataAccess/readDocument";
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { updateDocument } from "../../Common/dataAccess/updateDocument";
import { createDocument } from "../../Common/dataAccess/createDocument";
export default class DocumentsTab extends TabsBase { export default class DocumentsTab extends TabsBase {
public selectedDocumentId: ko.Observable<DocumentId>; public selectedDocumentId: ko.Observable<DocumentId>;
@@ -369,36 +374,22 @@ export default class DocumentsTab extends TabsBase {
return true; return true;
}; };
public onApplyFilterClick(): Q.Promise<any> { public async refreshDocumentsGrid(): Promise<void> {
// clear documents grid // clear documents grid
this.documentIds([]); this.documentIds([]);
return this.createIterator()
.then(
// reset iterator
iterator => {
this._documentsIterator = iterator;
}
)
.then(
// load documents
() => {
return this.loadNextPage();
}
)
.then(() => {
// collapse filter
this.appliedFilter(this.filterContent());
this.isFilterExpanded(false);
const focusElement = document.getElementById("errorStatusIcon");
focusElement && focusElement.focus();
})
.catch(error => {
window.alert(getErrorMessage(error));
});
}
public refreshDocumentsGrid(): Q.Promise<any> { try {
return this.onApplyFilterClick(); // reset iterator
this._documentsIterator = this.createIterator();
// load documents
await this.loadNextPage();
// collapse filter
this.appliedFilter(this.filterContent());
this.isFilterExpanded(false);
document.getElementById("errorStatusIcon")?.focus();
} catch (error) {
window.alert(getErrorMessage(error));
}
} }
public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => { public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => {
@@ -434,7 +425,7 @@ export default class DocumentsTab extends TabsBase {
return Q(); return Q();
}; };
public onSaveNewDocumentClick = (): Q.Promise<any> => { public onSaveNewDocumentClick = (): Promise<any> => {
this.isExecutionError(false); this.isExecutionError(false);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, { const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, {
databaseAccountName: this.collection && this.collection.container.databaseAccount().name, databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
@@ -502,7 +493,7 @@ export default class DocumentsTab extends TabsBase {
return Q(); return Q();
}; };
public onSaveExisitingDocumentClick = (): Q.Promise<any> => { public onSaveExisitingDocumentClick = (): Promise<any> => {
const selectedDocumentId = this.selectedDocumentId(); const selectedDocumentId = this.selectedDocumentId();
const documentContent = JSON.parse(this.selectedDocumentContent()); const documentContent = JSON.parse(this.selectedDocumentContent());
@@ -571,17 +562,15 @@ export default class DocumentsTab extends TabsBase {
return Q(); return Q();
}; };
public onDeleteExisitingDocumentClick = (): Q.Promise<any> => { public onDeleteExisitingDocumentClick = async (): Promise<void> => {
const selectedDocumentId = this.selectedDocumentId(); const selectedDocumentId = this.selectedDocumentId();
const msg = !this.isPreferredApiMongoDB const msg = !this.isPreferredApiMongoDB
? "Are you sure you want to delete the selected item ?" ? "Are you sure you want to delete the selected item ?"
: "Are you sure you want to delete the selected document ?"; : "Are you sure you want to delete the selected document ?";
if (window.confirm(msg)) { if (window.confirm(msg)) {
return this._deleteDocument(selectedDocumentId); await this._deleteDocument(selectedDocumentId);
} }
return Q();
}; };
public onValidDocumentEdit(): Q.Promise<any> { public onValidDocumentEdit(): Q.Promise<any> {
@@ -617,63 +606,50 @@ export default class DocumentsTab extends TabsBase {
return Q(); return Q();
} }
public onTabClick(): Q.Promise<any> { public onTabClick(): void {
return super.onTabClick().then(() => { super.onTabClick();
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents); this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
});
} }
public onActivate(): Q.Promise<any> { public async onActivate(): Promise<void> {
return super.onActivate().then(() => { super.onActivate();
if (this._documentsIterator) {
return Q.resolve(this._documentsIterator);
}
return this.createIterator().then( if (!this._documentsIterator) {
(iterator: QueryIterator<ItemDefinition & Resource>) => { try {
this._documentsIterator = iterator; this._documentsIterator = this.createIterator();
return this.loadNextPage(); await this.loadNextPage();
}, } catch (error) {
error => { if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { TelemetryProcessor.traceFailure(
TelemetryProcessor.traceFailure( Action.Tab,
Action.Tab, {
{ databaseAccountName: this.collection.container.databaseAccount().name,
databaseAccountName: this.collection.container.databaseAccount().name, databaseName: this.collection.databaseId,
databaseName: this.collection.databaseId, collectionName: this.collection.id(),
collectionName: this.collection.id(), defaultExperience: this.collection.container.defaultExperience(),
defaultExperience: this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab,
dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(),
tabTitle: this.tabTitle(), error: getErrorMessage(error),
error: getErrorMessage(error), errorStack: getErrorStack(error)
errorStack: getErrorStack(error) },
}, this.onLoadStartKey
this.onLoadStartKey );
); this.onLoadStartKey = null;
this.onLoadStartKey = null;
}
} }
); }
}); }
} }
public onRefreshClick(): Q.Promise<any> {
return this.refreshDocumentsGrid().then(() => {
this.selectedDocumentContent("");
this.selectedDocumentId(null);
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
});
}
private _isIgnoreDirtyEditor = (): boolean => { private _isIgnoreDirtyEditor = (): boolean => {
var msg: string = "Changes will be lost. Do you want to continue?"; var msg: string = "Changes will be lost. Do you want to continue?";
return window.confirm(msg); return window.confirm(msg);
}; };
protected __deleteDocument(documentId: DocumentId): Q.Promise<any> { protected __deleteDocument(documentId: DocumentId): Promise<void> {
return deleteDocument(this.collection, documentId); return deleteDocument(this.collection, documentId);
} }
private _deleteDocument(selectedDocumentId: DocumentId): Q.Promise<any> { private _deleteDocument(selectedDocumentId: DocumentId): Promise<void> {
this.isExecutionError(false); this.isExecutionError(false);
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, { const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, {
databaseAccountName: this.collection && this.collection.container.databaseAccount().name, databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
@@ -684,7 +660,7 @@ export default class DocumentsTab extends TabsBase {
this.isExecuting(true); this.isExecuting(true);
return this.__deleteDocument(selectedDocumentId) return this.__deleteDocument(selectedDocumentId)
.then( .then(
(result: any) => { () => {
this.documentIds.remove((documentId: DocumentId) => documentId.rid === selectedDocumentId.rid); this.documentIds.remove((documentId: DocumentId) => documentId.rid === selectedDocumentId.rid);
this.selectedDocumentContent(""); this.selectedDocumentContent("");
this.selectedDocumentId(null); this.selectedDocumentId(null);
@@ -720,7 +696,7 @@ export default class DocumentsTab extends TabsBase {
.finally(() => this.isExecuting(false)); .finally(() => this.isExecuting(false));
} }
public createIterator(): Q.Promise<QueryIterator<ItemDefinition & Resource>> { public createIterator(): QueryIterator<ItemDefinition & Resource> {
let filters = this.lastFilterContents(); let filters = this.lastFilterContents();
const filter: string = this.filterContent().trim(); const filter: string = this.filterContent().trim();
const query: string = this.buildQuery(filter); const query: string = this.buildQuery(filter);
@@ -734,11 +710,10 @@ export default class DocumentsTab extends TabsBase {
return queryDocuments(this.collection.databaseId, this.collection.id(), query, options); return queryDocuments(this.collection.databaseId, this.collection.id(), query, options);
} }
public selectDocument(documentId: DocumentId): Q.Promise<any> { public async selectDocument(documentId: DocumentId): Promise<void> {
this.selectedDocumentId(documentId); this.selectedDocumentId(documentId);
return readDocument(this.collection, documentId).then((content: any) => { const content = await readDocument(this.collection, documentId);
this.initDocumentEditor(documentId, content); this.initDocumentEditor(documentId, content);
});
} }
public loadNextPage(): Q.Promise<any> { public loadNextPage(): Q.Promise<any> {

View File

@@ -114,10 +114,9 @@ export default class GraphTab extends TabsBase {
: `${account.name}.graphs.azure.com:443/`; : `${account.name}.graphs.azure.com:443/`;
} }
public onTabClick(): Q.Promise<any> { public onTabClick(): void {
return super.onTabClick().then(() => { super.onTabClick();
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Graph); this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Graph);
});
} }
/** /**

View File

@@ -289,7 +289,7 @@
<button <button
class="filterbtnstyle queryButton" class="filterbtnstyle queryButton"
data-bind=" data-bind="
click: onApplyFilterClick, click: refreshDocumentsGrid,
enable: applyFilterButton.enabled" enable: applyFilterButton.enabled"
> >
Apply Filter Apply Filter

View File

@@ -44,7 +44,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
super.buildCommandBarOptions(); super.buildCommandBarOptions();
} }
public onSaveNewDocumentClick = (): Q.Promise<any> => { public onSaveNewDocumentClick = (): Promise<any> => {
const documentContent = JSON.parse(this.selectedDocumentContent()); const documentContent = JSON.parse(this.selectedDocumentContent());
this.displayedError(""); this.displayedError("");
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, { const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, {
@@ -78,12 +78,12 @@ export default class MongoDocumentsTab extends DocumentsTab {
startKey startKey
); );
Logger.logError("Failed to save new document: Document shard key not defined", "MongoDocumentsTab"); Logger.logError("Failed to save new document: Document shard key not defined", "MongoDocumentsTab");
return Q.reject("Document without shard key"); throw new Error("Document without shard key");
} }
this.isExecutionError(false); this.isExecutionError(false);
this.isExecuting(true); this.isExecuting(true);
return Q(createDocument(this.collection.databaseId, this.collection, this.partitionKeyProperty, documentContent)) return createDocument(this.collection.databaseId, this.collection, this.partitionKeyProperty, documentContent)
.then( .then(
(savedDocument: any) => { (savedDocument: any) => {
let partitionKeyArray = extractPartitionKey( let partitionKeyArray = extractPartitionKey(
@@ -136,7 +136,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
.finally(() => this.isExecuting(false)); .finally(() => this.isExecuting(false));
}; };
public onSaveExisitingDocumentClick = (): Q.Promise<any> => { public onSaveExisitingDocumentClick = (): Promise<any> => {
const selectedDocumentId = this.selectedDocumentId(); const selectedDocumentId = this.selectedDocumentId();
const documentContent = this.selectedDocumentContent(); const documentContent = this.selectedDocumentContent();
this.isExecutionError(false); this.isExecutionError(false);
@@ -148,7 +148,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
tabTitle: this.tabTitle() tabTitle: this.tabTitle()
}); });
return Q(updateDocument(this.collection.databaseId, this.collection, selectedDocumentId, documentContent)) return updateDocument(this.collection.databaseId, this.collection, selectedDocumentId, documentContent)
.then( .then(
(updatedDocument: any) => { (updatedDocument: any) => {
let value: string = this.renderObjectForEditor(updatedDocument || {}, null, 4); let value: string = this.renderObjectForEditor(updatedDocument || {}, null, 4);
@@ -204,13 +204,10 @@ export default class MongoDocumentsTab extends DocumentsTab {
return filter || "{}"; return filter || "{}";
} }
public selectDocument(documentId: DocumentId): Q.Promise<any> { public async selectDocument(documentId: DocumentId): Promise<void> {
this.selectedDocumentId(documentId); this.selectedDocumentId(documentId);
return Q( const content = await readDocument(this.collection.databaseId, this.collection, documentId);
readDocument(this.collection.databaseId, this.collection, documentId).then((content: any) => { this.initDocumentEditor(documentId, content);
this.initDocumentEditor(documentId, content);
})
);
} }
public loadNextPage(): Q.Promise<any> { public loadNextPage(): Q.Promise<any> {
@@ -330,7 +327,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
return partitionKey; return partitionKey;
} }
protected __deleteDocument(documentId: DocumentId): Q.Promise<any> { protected __deleteDocument(documentId: DocumentId): Promise<void> {
return Q(deleteDocument(this.collection.databaseId, this.collection, documentId)); return deleteDocument(this.collection.databaseId, this.collection, documentId);
} }
} }

View File

@@ -53,10 +53,9 @@ export default class MongoShellTab extends TabsBase {
// } // }
} }
public onTabClick(): Q.Promise<any> { public onTabClick(): void {
return super.onTabClick().then(() => { super.onTabClick();
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents); this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
});
} }
public handleMessage(event: MessageEvent) { public handleMessage(event: MessageEvent) {

View File

@@ -103,7 +103,7 @@
</div> </div>
<json-editor <json-editor
params="{ content: queryResults, isReadOnly: true, ariaLabel: 'Query results' }" params="{ content: queryResults, isReadOnly: true, ariaLabel: 'Query results' }"
data-bind="visible: queryResults().length > 0 && isResultToggled() && allResultsMetadata().length > 0 && !error()" data-bind="visible: queryResults() && queryResults().length > 0 && isResultToggled() && allResultsMetadata().length > 0 && !error()"
> >
</json-editor> </json-editor>
<div <div

View File

@@ -15,9 +15,10 @@ import { QueryUtils } from "../../Utils/QueryUtils";
import SaveQueryIcon from "../../../images/save-cosmos.svg"; import SaveQueryIcon from "../../../images/save-cosmos.svg";
import { MinimalQueryIterator } from "../../Common/IteratorUtilities"; import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
import { queryDocuments, queryDocumentsPage } from "../../Common/DocumentClientUtilityBase";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
import { queryDocumentsPage } from "../../Common/dataAccess/queryDocumentsPage";
enum ToggleState { enum ToggleState {
Result, Result,
@@ -163,20 +164,19 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
this._buildCommandBarOptions(); this._buildCommandBarOptions();
} }
public onTabClick(): Q.Promise<any> { public onTabClick(): void {
return super.onTabClick().then(() => { super.onTabClick();
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Query); this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Query);
});
} }
public onExecuteQueryClick = (): Q.Promise<any> => { public onExecuteQueryClick = async (): Promise<void> => {
const sqlStatement: string = this.selectedContent() || this.sqlQueryEditorContent(); const sqlStatement: string = this.selectedContent() || this.sqlQueryEditorContent();
this.sqlStatementToExecute(sqlStatement); this.sqlStatementToExecute(sqlStatement);
this.allResultsMetadata([]); this.allResultsMetadata([]);
this.queryResults(""); this.queryResults("");
this._iterator = null; this._iterator = undefined;
return this._executeQueryDocumentsPage(0); await this._executeQueryDocumentsPage(0);
}; };
public onLoadQueryClick = (): void => { public onLoadQueryClick = (): void => {
@@ -191,13 +191,13 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
this.collection && this.collection.container && this.collection.container.browseQueriesPane.open(); this.collection && this.collection.container && this.collection.container.browseQueriesPane.open();
}; };
public onFetchNextPageClick(): Q.Promise<any> { public async onFetchNextPageClick(): Promise<void> {
const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || []; const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || [];
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1]; const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
const firstResultIndex: number = (metadata && Number(metadata.firstItemIndex)) || 1; const firstResultIndex: number = (metadata && Number(metadata.firstItemIndex)) || 1;
const itemCount: number = (metadata && Number(metadata.itemCount)) || 0; const itemCount: number = (metadata && Number(metadata.itemCount)) || 0;
return this._executeQueryDocumentsPage(firstResultIndex + itemCount - 1); await this._executeQueryDocumentsPage(firstResultIndex + itemCount - 1);
} }
public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => { public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => {
@@ -265,19 +265,18 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
return true; return true;
}; };
private _executeQueryDocumentsPage(firstItemIndex: number): Q.Promise<any> { private async _executeQueryDocumentsPage(firstItemIndex: number): Promise<any> {
this.error(""); this.error("");
this.roundTrips(undefined); this.roundTrips(undefined);
if (this._iterator == null) { if (this._iterator === undefined) {
const queryIteratorPromise = this._initIterator(); this._initIterator();
return queryIteratorPromise.finally(() => this._queryDocumentsPage(firstItemIndex));
} }
return this._queryDocumentsPage(firstItemIndex); await this._queryDocumentsPage(firstItemIndex);
} }
// TODO: Position and enable spinner when request is in progress // TODO: Position and enable spinner when request is in progress
private _queryDocumentsPage(firstItemIndex: number): Q.Promise<any> { private async _queryDocumentsPage(firstItemIndex: number): Promise<void> {
this.isExecutionError(false); this.isExecutionError(false);
this._resetAggregateQueryMetrics(); this._resetAggregateQueryMetrics();
const startKey: number = TelemetryProcessor.traceStart(Action.ExecuteQuery, { const startKey: number = TelemetryProcessor.traceStart(Action.ExecuteQuery, {
@@ -289,90 +288,90 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
let options: any = {}; let options: any = {};
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
const queryDocuments = (firstItemIndex: number) => const queryDocuments = async (firstItemIndex: number) =>
queryDocumentsPage(this.collection && this.collection.id(), this._iterator, firstItemIndex, options); await queryDocumentsPage(this.collection && this.collection.id(), this._iterator, firstItemIndex);
this.isExecuting(true); this.isExecuting(true);
return QueryUtils.queryPagesUntilContentPresent(firstItemIndex, queryDocuments)
.then(
(queryResults: ViewModels.QueryResults) => {
const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || [];
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
const resultsMetadata: ViewModels.QueryResultsMetadata = {
hasMoreResults: queryResults.hasMoreResults,
itemCount: queryResults.itemCount,
firstItemIndex: queryResults.firstItemIndex,
lastItemIndex: queryResults.lastItemIndex
};
this.allResultsMetadata.push(resultsMetadata);
this.activityId(queryResults.activityId);
this.roundTrips(queryResults.roundTrips);
this._updateQueryMetricsMap(queryResults.headers[Constants.HttpHeaders.queryMetrics]); try {
const queryResults: ViewModels.QueryResults = await QueryUtils.queryPagesUntilContentPresent(
firstItemIndex,
queryDocuments
);
const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || [];
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
const resultsMetadata: ViewModels.QueryResultsMetadata = {
hasMoreResults: queryResults.hasMoreResults,
itemCount: queryResults.itemCount,
firstItemIndex: queryResults.firstItemIndex,
lastItemIndex: queryResults.lastItemIndex
};
this.allResultsMetadata.push(resultsMetadata);
this.activityId(queryResults.activityId);
this.roundTrips(queryResults.roundTrips);
if (queryResults.itemCount == 0 && metadata != null && metadata.itemCount >= 0) { this._updateQueryMetricsMap(queryResults.headers[Constants.HttpHeaders.queryMetrics]);
// we let users query for the next page because the SDK sometimes specifies there are more elements
// even though there aren't any so we should not update the prior query results.
return;
}
const documents: any[] = queryResults.documents; if (queryResults.itemCount == 0 && metadata != null && metadata.itemCount >= 0) {
const results = this.renderObjectForEditor(documents, null, 4); // we let users query for the next page because the SDK sometimes specifies there are more elements
// even though there aren't any so we should not update the prior query results.
return;
}
const resultsDisplay: string = const documents: any[] = queryResults.documents;
queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`; const results = this.renderObjectForEditor(documents, null, 4);
this.showingDocumentsDisplayText(resultsDisplay);
this.requestChargeDisplayText(`${queryResults.requestCharge} RUs`);
if (!this.queryResults() && !results) { const resultsDisplay: string =
const errorMessage: string = JSON.stringify({ queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`;
error: `Returned no results after query execution`, this.showingDocumentsDisplayText(resultsDisplay);
accountName: this.collection && this.collection.container.databaseAccount(), this.requestChargeDisplayText(`${queryResults.requestCharge} RUs`);
databaseName: this.collection && this.collection.databaseId,
collectionName: this.collection && this.collection.id(),
sqlQuery: this.sqlStatementToExecute(),
hasMoreResults: resultsMetadata.hasMoreResults,
itemCount: resultsMetadata.itemCount,
responseHeaders: queryResults && queryResults.headers
});
Logger.logError(errorMessage, "QueryTab");
}
this.queryResults(results); if (!this.queryResults() && !results) {
const errorMessage: string = JSON.stringify({
error: `Returned no results after query execution`,
accountName: this.collection && this.collection.container.databaseAccount(),
databaseName: this.collection && this.collection.databaseId,
collectionName: this.collection && this.collection.id(),
sqlQuery: this.sqlStatementToExecute(),
hasMoreResults: resultsMetadata.hasMoreResults,
itemCount: resultsMetadata.itemCount,
responseHeaders: queryResults && queryResults.headers
});
Logger.logError(errorMessage, "QueryTab");
}
TelemetryProcessor.traceSuccess( this.queryResults(results);
Action.ExecuteQuery,
{ TelemetryProcessor.traceSuccess(
databaseAccountName: this.collection && this.collection.container.databaseAccount().name, Action.ExecuteQuery,
defaultExperience: this.collection && this.collection.container.defaultExperience(), {
dataExplorerArea: Constants.Areas.Tab, databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
tabTitle: this.tabTitle() defaultExperience: this.collection && this.collection.container.defaultExperience(),
}, dataExplorerArea: Constants.Areas.Tab,
startKey tabTitle: this.tabTitle()
);
}, },
(error: any) => { startKey
this.isExecutionError(true); );
const errorMessage = getErrorMessage(error); } catch (error) {
this.error(errorMessage); this.isExecutionError(true);
TelemetryProcessor.traceFailure( const errorMessage = getErrorMessage(error);
Action.ExecuteQuery, this.error(errorMessage);
{ TelemetryProcessor.traceFailure(
databaseAccountName: this.collection && this.collection.container.databaseAccount().name, Action.ExecuteQuery,
defaultExperience: this.collection && this.collection.container.defaultExperience(), {
dataExplorerArea: Constants.Areas.Tab, databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
tabTitle: this.tabTitle(), defaultExperience: this.collection && this.collection.container.defaultExperience(),
error: errorMessage, dataExplorerArea: Constants.Areas.Tab,
errorStack: getErrorStack(error) tabTitle: this.tabTitle(),
}, error: errorMessage,
startKey errorStack: getErrorStack(error)
); },
document.getElementById("error-display").focus(); startKey
} );
) document.getElementById("error-display").focus();
.finally(() => { } finally {
this.isExecuting(false); this.isExecuting(false);
this.togglesOnFocus(); this.togglesOnFocus();
}); }
} }
private _updateQueryMetricsMap(metricsMap: { [partitionKeyRange: string]: DataModels.QueryMetrics }): void { private _updateQueryMetricsMap(metricsMap: { [partitionKeyRange: string]: DataModels.QueryMetrics }): void {
@@ -477,16 +476,17 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
} }
} }
protected _initIterator(): Q.Promise<MinimalQueryIterator> { protected _initIterator(): void {
const options: any = QueryTab.getIteratorOptions(this.collection); const options: any = QueryTab.getIteratorOptions(this.collection);
if (this._resourceTokenPartitionKey) { if (this._resourceTokenPartitionKey) {
options.partitionKey = this._resourceTokenPartitionKey; options.partitionKey = this._resourceTokenPartitionKey;
} }
return Q( this._iterator = queryDocuments(
queryDocuments(this.collection.databaseId, this.collection.id(), this.sqlStatementToExecute(), options).then( this.collection.databaseId,
iterator => (this._iterator = iterator) this.collection.id(),
) this.sqlStatementToExecute(),
options
); );
} }

View File

@@ -161,17 +161,16 @@ export default class QueryTablesTab extends TabsBase {
return null; return null;
}; };
public onActivate(): Q.Promise<any> { public onActivate(): void {
return super.onActivate().then(() => { super.onActivate();
const columns = const columns =
!!this.tableEntityListViewModel() && !!this.tableEntityListViewModel() &&
!!this.tableEntityListViewModel().table && !!this.tableEntityListViewModel().table &&
this.tableEntityListViewModel().table.columns; this.tableEntityListViewModel().table.columns;
if (!!columns) { if (!!columns) {
columns.adjust(); columns.adjust();
$(window).resize(); $(window).resize();
} }
});
} }
protected getTabsButtons(): CommandButtonComponentProps[] { protected getTabsButtons(): CommandButtonComponentProps[] {

View File

@@ -186,12 +186,11 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
this._setBaselines(); this._setBaselines();
} }
public onTabClick(): Q.Promise<any> { public onTabClick(): void {
return super.onTabClick().then(() => { super.onTabClick();
if (this.isNew()) { if (this.isNew()) {
this.collection.selectedSubnodeKind(this.tabKind); this.collection.selectedSubnodeKind(this.tabKind);
} }
});
} }
public abstract onSaveClick: () => Promise<any>; public abstract onSaveClick: () => Promise<any>;

View File

@@ -42,54 +42,49 @@ export default class SettingsTabV2 extends TabsBase {
}); });
} }
public onActivate(): Q.Promise<unknown> { public async onActivate(): Promise<void> {
this.isExecuting(true); try {
this.currentCollection.loadOffer().then( this.isExecuting(true);
() => { await this.currentCollection.loadOffer();
// passed in options and set by parent as "Settings" by default // passed in options and set by parent as "Settings" by default
this.tabTitle(this.currentCollection.offer() ? "Settings" : "Scale & Settings"); this.tabTitle(this.currentCollection.offer() ? "Settings" : "Scale & Settings");
this.offerRead(true);
this.options.getPendingNotification.then(
(data: DataModels.Notification) => {
this.notification = data;
this.notificationRead(true);
this.isExecuting(false);
},
error => {
const errorMessage = getErrorMessage(error);
this.notification = undefined;
this.notificationRead(true);
this.isExecuting(false);
traceFailure(
Action.Tab,
{
databaseAccountName: this.options.collection.container.databaseAccount().name,
databaseName: this.options.collection.databaseId,
collectionName: this.options.collection.id(),
defaultExperience: this.options.collection.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle,
error: errorMessage,
errorStack: getErrorStack(error)
},
this.options.onLoadStartKey
);
logConsoleError(
`Error while fetching container settings for container ${this.options.collection.id()}: ${errorMessage}`
);
throw error;
}
);
},
() => {
this.offerRead(true);
this.isExecuting(false);
}
);
return super.onActivate().then(() => { this.options.getPendingNotification.then(
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.SettingsV2); (data: DataModels.Notification) => {
}); this.notification = data;
this.notificationRead(true);
},
error => {
const errorMessage = getErrorMessage(error);
this.notification = undefined;
this.notificationRead(true);
traceFailure(
Action.Tab,
{
databaseAccountName: this.options.collection.container.databaseAccount().name,
databaseName: this.options.collection.databaseId,
collectionName: this.options.collection.id(),
defaultExperience: this.options.collection.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle,
error: errorMessage,
errorStack: getErrorStack(error)
},
this.options.onLoadStartKey
);
logConsoleError(
`Error while fetching container settings for container ${this.options.collection.id()}: ${errorMessage}`
);
throw error;
}
);
} finally {
this.offerRead(true);
this.isExecuting(false);
}
super.onActivate();
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.SettingsV2);
} }
public getSettingsTabContainer(): Explorer { public getSettingsTabContainer(): Explorer {

View File

@@ -94,9 +94,8 @@ export default class TabsBase extends WaitsForTemplateViewModel {
}); });
} }
public onTabClick(): Q.Promise<any> { public onTabClick(): void {
this.getContainer().tabsManager.activateTab(this); this.getContainer().tabsManager.activateTab(this);
return Q();
} }
protected updateSelectedNode(): void { protected updateSelectedNode(): void {
@@ -128,7 +127,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
return this.onSpaceOrEnterKeyPress(event, () => this.onCloseTabButtonClick()); return this.onSpaceOrEnterKeyPress(event, () => this.onCloseTabButtonClick());
}; };
public onActivate(): Q.Promise<any> { public onActivate(): void {
this.updateSelectedNode(); this.updateSelectedNode();
if (!!this.collection) { if (!!this.collection) {
this.collection.selectedSubnodeKind(this.tabKind); this.collection.selectedSubnodeKind(this.tabKind);
@@ -151,7 +150,6 @@ export default class TabsBase extends WaitsForTemplateViewModel {
tabTitle: this.tabTitle(), tabTitle: this.tabTitle(),
tabId: this.tabId tabId: this.tabId
}); });
return Q();
} }
public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => { public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => {

View File

@@ -142,7 +142,7 @@
<notebook-viewer-tab params="{data: $data}"></notebook-viewer-tab> <notebook-viewer-tab params="{data: $data}"></notebook-viewer-tab>
<!-- /ko --> <!-- /ko -->
<!-- ko if: $data.tabKind === 19 --> <!-- ko if: $data.tabKind === 20 -->
<settings-tab-v2 params="{data: $data}"></settings-tab-v2> <settings-tab-v2 params="{data: $data}"></settings-tab-v2>
<!-- /ko --> <!-- /ko -->
</div> </div>

View File

@@ -8,7 +8,6 @@ import * as Constants from "../../Common/Constants";
import { readStoredProcedures } from "../../Common/dataAccess/readStoredProcedures"; import { readStoredProcedures } from "../../Common/dataAccess/readStoredProcedures";
import { readTriggers } from "../../Common/dataAccess/readTriggers"; import { readTriggers } from "../../Common/dataAccess/readTriggers";
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions"; import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
import { createDocument } from "../../Common/DocumentClientUtilityBase";
import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer"; import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer";
import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize"; import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize";
import * as Logger from "../../Common/Logger"; import * as Logger from "../../Common/Logger";
@@ -39,6 +38,7 @@ import Explorer from "../Explorer";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { fetchPortalNotifications } from "../../Common/PortalNotifications"; import { fetchPortalNotifications } from "../../Common/PortalNotifications";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { createDocument } from "../../Common/dataAccess/createDocument";
export default class Collection implements ViewModels.Collection { export default class Collection implements ViewModels.Collection {
public nodeKind: string; public nodeKind: string;
@@ -551,7 +551,7 @@ export default class Collection implements ViewModels.Collection {
const tabTitle = !this.offer() ? "Settings" : "Scale & Settings"; const tabTitle = !this.offer() ? "Settings" : "Scale & Settings";
const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification(); const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Settings, tab => { const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.SettingsV2, tab => {
return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id(); return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id();
}); });
@@ -1091,8 +1091,7 @@ export default class Collection implements ViewModels.Collection {
return deferred.promise; return deferred.promise;
} }
private _createDocumentsFromFile(fileName: string, documentContent: string): Q.Promise<UploadDetailsRecord> { private async _createDocumentsFromFile(fileName: string, documentContent: string): Promise<UploadDetailsRecord> {
const deferred: Q.Deferred<UploadDetailsRecord> = Q.defer();
const record: UploadDetailsRecord = { const record: UploadDetailsRecord = {
fileName: fileName, fileName: fileName,
numSucceeded: 0, numSucceeded: 0,
@@ -1102,39 +1101,25 @@ export default class Collection implements ViewModels.Collection {
try { try {
const content = JSON.parse(documentContent); const content = JSON.parse(documentContent);
const promises: Array<Q.Promise<any>> = [];
const triggerCreateDocument: (documentContent: any) => Q.Promise<any> = (documentContent: any) => {
return createDocument(this, documentContent).then(
doc => {
record.numSucceeded++;
return Q.resolve();
},
error => {
record.numFailed++;
record.errors = [...record.errors, getErrorMessage(error)];
return Q.resolve();
}
);
};
if (Array.isArray(content)) { if (Array.isArray(content)) {
for (let i = 0; i < content.length; i++) { await Promise.all(
promises.push(triggerCreateDocument(content[i])); content.map(async documentContent => {
} await createDocument(this, documentContent);
record.numSucceeded++;
})
);
} else { } else {
promises.push(triggerCreateDocument(content)); await createDocument(this, documentContent);
record.numSucceeded++;
} }
Q.all(promises).then(() => { return record;
deferred.resolve(record); } catch (error) {
});
} catch (e) {
record.numFailed++; record.numFailed++;
record.errors = [...record.errors, e.message]; record.errors = [...record.errors, error.message];
deferred.resolve(record); return record;
} }
return deferred.promise;
} }
private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> { private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> {

View File

@@ -6,7 +6,7 @@ import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { extractPartitionKey } from "@azure/cosmos"; import { extractPartitionKey } from "@azure/cosmos";
import ConflictsTab from "../Tabs/ConflictsTab"; import ConflictsTab from "../Tabs/ConflictsTab";
import { readDocument } from "../../Common/DocumentClientUtilityBase"; import { readDocument } from "../../Common/dataAccess/readDocument";
export default class ConflictId { export default class ConflictId {
public container: ConflictsTab; public container: ConflictsTab;
@@ -59,41 +59,42 @@ export default class ConflictId {
return; return;
} }
public loadConflict(): Q.Promise<any> { public async loadConflict(): Promise<void> {
const conflictsTab = this.container;
this.container.selectedConflictId(this); this.container.selectedConflictId(this);
if (this.operationType === Constants.ConflictOperationType.Create) { if (this.operationType === Constants.ConflictOperationType.Create) {
this.container.initDocumentEditorForCreate(this, this.content); this.container.initDocumentEditorForCreate(this, this.content);
return Q(); return;
} }
this.container.loadingConflictData(true); this.container.loadingConflictData(true);
return readDocument(this.container.collection, this.buildDocumentIdFromConflict(this.partitionKeyValue)).then(
(currentDocumentContent: any) => {
this.container.loadingConflictData(false);
if (this.operationType === Constants.ConflictOperationType.Replace) {
this.container.initDocumentEditorForReplace(this, this.content, currentDocumentContent);
} else {
this.container.initDocumentEditorForDelete(this, currentDocumentContent);
}
},
(reason: any) => {
this.container.loadingConflictData(false);
// Document could be deleted try {
if ( const currentDocumentContent = await readDocument(
reason && this.container.collection,
reason.code === Constants.HttpStatusCodes.NotFound && this.buildDocumentIdFromConflict(this.partitionKeyValue)
this.operationType === Constants.ConflictOperationType.Delete );
) {
this.container.initDocumentEditorForNoOp(this);
return Q();
}
return Q.reject(reason); if (this.operationType === Constants.ConflictOperationType.Replace) {
this.container.initDocumentEditorForReplace(this, this.content, currentDocumentContent);
} else {
this.container.initDocumentEditorForDelete(this, currentDocumentContent);
} }
); } catch (error) {
// Document could be deleted
if (
error &&
error.code === Constants.HttpStatusCodes.NotFound &&
this.operationType === Constants.ConflictOperationType.Delete
) {
this.container.initDocumentEditorForNoOp(this);
return;
}
throw error;
} finally {
this.container.loadingConflictData(false);
}
} }
public getPartitionKeyValueAsString(): string { public getPartitionKeyValueAsString(): string {

View File

@@ -65,7 +65,7 @@ export default class DocumentId {
return JSON.stringify(partitionKeyValue); return JSON.stringify(partitionKeyValue);
} }
public loadDocument(): Q.Promise<any> { public async loadDocument(): Promise<void> {
return this.container.selectDocument(this); await this.container.selectDocument(this);
} }
} }

View File

@@ -2,7 +2,7 @@ import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
import * as ko from "knockout"; import * as ko from "knockout";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { deleteStoredProcedure } from "../../Common/dataAccess/deleteStoredProcedure"; import { deleteStoredProcedure } from "../../Common/dataAccess/deleteStoredProcedure";
import { executeStoredProcedure } from "../../Common/DocumentClientUtilityBase"; import { executeStoredProcedure } from "../../Common/dataAccess/executeStoredProcedure";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";

View File

@@ -12,8 +12,8 @@ import { getErrorMessage } from "../Common/ErrorHandlingUtils";
export class NotebookWorkspaceManager { export class NotebookWorkspaceManager {
private resourceProviderClientFactory: IResourceProviderClientFactory<any>; private resourceProviderClientFactory: IResourceProviderClientFactory<any>;
constructor(private _armEndpoint: string) { constructor() {
this.resourceProviderClientFactory = new ResourceProviderClientFactory(this._armEndpoint); this.resourceProviderClientFactory = new ResourceProviderClientFactory();
} }
public async getNotebookWorkspacesAsync(cosmosdbResourceId: string): Promise<NotebookWorkspace[]> { public async getNotebookWorkspacesAsync(cosmosdbResourceId: string): Promise<NotebookWorkspace[]> {

View File

@@ -19,6 +19,7 @@ export function initializeExplorer(): Explorer {
cassandraEndpoint: "" cassandraEndpoint: ""
} }
}); });
explorer.isAccountReady(true); explorer.isAccountReady(true);
return explorer; return explorer;
} }

View File

@@ -268,7 +268,7 @@ export default class Main {
masterKey?: string /* master key extracted from connection string if available */, masterKey?: string /* master key extracted from connection string if available */,
account?: DatabaseAccount, account?: DatabaseAccount,
authorizationToken?: string /* access key */ authorizationToken?: string /* access key */
): Q.Promise<void> { ): void {
const serverId: string = AuthHeadersUtil.serverId; const serverId: string = AuthHeadersUtil.serverId;
const authType: string = (<any>window).authType; const authType: string = (<any>window).authType;
const accountResourceId = const accountResourceId =
@@ -373,7 +373,7 @@ export default class Main {
}); });
} }
return Q.reject(`Unsupported AuthType ${authType}`); throw new Error(`Unsupported AuthType ${authType}`);
} }
private static _instantiateExplorer(): Explorer { private static _instantiateExplorer(): Explorer {

View File

@@ -1,9 +1,23 @@
import "../../Explorer/Tables/DataTable/DataTableBindingManager"; import "../../Explorer/Tables/DataTable/DataTableBindingManager";
import Explorer from "../../Explorer/Explorer"; import Explorer from "../../Explorer/Explorer";
import { handleMessage } from "../../Controls/Heatmap/Heatmap";
export function initializeExplorer(): Explorer { export function initializeExplorer(): Explorer {
const explorer = new Explorer(); const explorer = new Explorer();
// In development mode, try to load the iframe message from session storage.
// This allows webpack hot reload to funciton properly
if (process.env.NODE_ENV === "development") {
const initMessage = sessionStorage.getItem("portalDataExplorerInitMessage");
if (initMessage) {
const message = JSON.parse(initMessage);
console.warn("Loaded cached portal iframe message from session storage");
console.dir(message);
explorer.initDataExplorerWithFrameInputs(message);
}
}
window.addEventListener("message", explorer.handleMessage.bind(explorer), false); window.addEventListener("message", explorer.handleMessage.bind(explorer), false);
return explorer; return explorer;
} }

View File

@@ -1,10 +1,14 @@
import { configContext } from "../ConfigContext";
import { IResourceProviderClientFactory, IResourceProviderClient } from "./IResourceProviderClient"; import { IResourceProviderClientFactory, IResourceProviderClient } from "./IResourceProviderClient";
import { ResourceProviderClient } from "./ResourceProviderClient"; import { ResourceProviderClient } from "./ResourceProviderClient";
export class ResourceProviderClientFactory implements IResourceProviderClientFactory<any> { export class ResourceProviderClientFactory implements IResourceProviderClientFactory<any> {
private armEndpoint: string;
private cachedClients: { [url: string]: IResourceProviderClient<any> } = {}; private cachedClients: { [url: string]: IResourceProviderClient<any> } = {};
constructor(private armEndpoint: string) {} constructor() {
this.armEndpoint = configContext.ARM_ENDPOINT;
}
public getOrCreate(url: string): IResourceProviderClient<any> { public getOrCreate(url: string): IResourceProviderClient<any> {
if (!url) { if (!url) {

View File

@@ -17,9 +17,7 @@ export class TabRouteHandler {
): void { ): void {
this._initRouter(); this._initRouter();
const parseHash = (newHash: string, oldHash: string) => this._tabRouter.parse(newHash); const parseHash = (newHash: string, oldHash: string) => this._tabRouter.parse(newHash);
const defaultRoutedCallback = (request: string, data: { route: any; params: string[]; isFirst: boolean }) => { const defaultRoutedCallback = (request: string, data: { route: any; params: string[]; isFirst: boolean }) => {};
console.log(request);
};
this._tabRouter.routed.add(onMatch || defaultRoutedCallback); this._tabRouter.routed.add(onMatch || defaultRoutedCallback);
hasher.initialized.add(parseHash); hasher.initialized.add(parseHash);
hasher.changed.add(parseHash); hasher.changed.add(parseHash);

View File

@@ -144,10 +144,6 @@ export class OfferPricing {
}; };
} }
export class GeneralResources {
public static loadingText: string = "Loading...";
}
export class CollectionCreation { export class CollectionCreation {
// TODO generate these values based on Product\Services\Documents\ImageStore\GatewayApplication\Settings.xml // TODO generate these values based on Product\Services\Documents\ImageStore\GatewayApplication\Settings.xml
public static readonly MinRUPerPartitionBelow7Partitions: number = 400; public static readonly MinRUPerPartitionBelow7Partitions: number = 400;
@@ -228,32 +224,6 @@ export class IndexingPolicies {
} }
export class SubscriptionUtilMappings { export class SubscriptionUtilMappings {
// TODO: Expose this through a web API from the portal
public static SubscriptionTypeMap: { [key: string]: SubscriptionType } = {
"AAD_2015-09-01": SubscriptionType.Free,
"AzureDynamics_2014-09-01": SubscriptionType.Free,
"AzureInOpen_2014-09-01": SubscriptionType.EA,
"AzurePass_2014-09-01": SubscriptionType.Free,
"BackupStorage_2014-09-01": SubscriptionType.PAYG,
"BizSpark_2014-09-01": SubscriptionType.Benefits,
"BizSparkPlus_2014-09-01": SubscriptionType.Benefits,
"CSP_2015-05-01": SubscriptionType.EA,
"Default_2014-09-01": SubscriptionType.PAYG,
"DevEssentials_2016-01-01": SubscriptionType.Benefits,
"DreamSpark_2015-02-01": SubscriptionType.Benefits,
"EnterpriseAgreement_2014-09-01": SubscriptionType.EA,
"FreeTrial_2014-09-01": SubscriptionType.Free,
"Internal_2014-09-01": SubscriptionType.Internal,
"LegacyMonetaryCommitment_2014-09-01": SubscriptionType.EA,
"LightweightTrial_2016-09-01": SubscriptionType.Free,
"MonetaryCommitment_2015-05-01": SubscriptionType.EA,
"MPN_2014-09-01": SubscriptionType.Benefits,
"MSDN_2014-09-01": SubscriptionType.Benefits,
"MSDNDevTest_2014-09-01": SubscriptionType.Benefits,
"PayAsYouGo_2014-09-01": SubscriptionType.PAYG,
"Sponsored_2016-01-01": SubscriptionType.Benefits
};
public static FreeTierSubscriptionIds: string[] = [ public static FreeTierSubscriptionIds: string[] = [
"b8f2ff04-0a81-4cf9-95ef-5828d16981d2", "b8f2ff04-0a81-4cf9-95ef-5828d16981d2",
"39b1fdff-e5b2-4f83-adb4-33cb3aabf5ea", "39b1fdff-e5b2-4f83-adb4-33cb3aabf5ea",
@@ -264,57 +234,6 @@ export class SubscriptionUtilMappings {
]; ];
} }
export class Offers {
public static offerTypeS1: string = "S1";
public static offerTypeS2: string = "S2";
public static offerTypeS3: string = "S3";
public static offerTypeStandard: string = "Standard";
}
export class OfferThoughput {
public static offerS1Throughput: number = 250;
public static offerS2Throughput: number = 1000;
public static offerS3Throughput: number = 2500;
}
export class OfferVersions {
public static offerV1: string = "V1";
public static offerV2: string = "V2";
}
export class InvalidOffers {
public static offerTypeInvalid: string = "Invalid";
public static offerTypeError: string = "Loading Error";
}
export class SpecTypes {
public static collection: string = "DocumentDbCollection";
}
export class CurrencyCodes {
public static usd: string = "USD";
public static rmb: string = "RMB";
}
export class ColorSchemes {
public static standard: string = "mediumBlue";
public static legacy: string = "yellowGreen";
}
export class FeatureIds {
public static storage: string = "storage";
public static sla: string = "sla";
public static partitioned: string = "partitioned";
public static singlePartitioned: string = "singlePartition";
public static legacySinglePartitioned: string = "legacySinglePartition";
}
export class FeatureIconNames {
public static storage: string = "SSD";
public static sla: string = "Monitoring";
public static productionReady: string = "ProductionReadyDb";
}
export class AutopilotDocumentation { export class AutopilotDocumentation {
public static Url: string = "https://aka.ms/cosmos-autoscale-info"; public static Url: string = "https://aka.ms/cosmos-autoscale-info";
} }

View File

@@ -8,14 +8,13 @@ import { ArmApiVersions, ArmResourceTypes } from "../Common/Constants";
import { IResourceProviderClient, IResourceProviderClientFactory } from "../ResourceProvider/IResourceProviderClient"; import { IResourceProviderClient, IResourceProviderClientFactory } from "../ResourceProvider/IResourceProviderClient";
import * as Logger from "../Common/Logger"; import * as Logger from "../Common/Logger";
import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory"; import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory";
import { configContext } from "../ConfigContext";
import { getErrorMessage } from "../Common/ErrorHandlingUtils"; import { getErrorMessage } from "../Common/ErrorHandlingUtils";
export class ArcadiaResourceManager { export class ArcadiaResourceManager {
private resourceProviderClientFactory: IResourceProviderClientFactory<any>; private resourceProviderClientFactory: IResourceProviderClientFactory<any>;
constructor(private armEndpoint = configContext.ARM_ENDPOINT) { constructor() {
this.resourceProviderClientFactory = new ResourceProviderClientFactory(this.armEndpoint); this.resourceProviderClientFactory = new ResourceProviderClientFactory();
} }
public async getWorkspacesAsync(arcadiaResourceId: string): Promise<ArcadiaWorkspace[]> { public async getWorkspacesAsync(arcadiaResourceId: string): Promise<ArcadiaWorkspace[]> {

View File

@@ -14,6 +14,7 @@ interface UserContext {
defaultExperience?: DefaultAccountExperienceType; defaultExperience?: DefaultAccountExperienceType;
useSDKOperations?: boolean; useSDKOperations?: boolean;
subscriptionType?: SubscriptionType; subscriptionType?: SubscriptionType;
quotaId?: string;
} }
const userContext: Readonly<UserContext> = {} as const; const userContext: Readonly<UserContext> = {} as const;

View File

@@ -25,39 +25,151 @@ describe("PricingUtils Tests", () => {
describe("computeRUUsagePriceHourly()", () => { describe("computeRUUsagePriceHourly()", () => {
it("should return 0 for NaN regions default cloud", () => { it("should return 0 for NaN regions default cloud", () => {
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, null, false); const value = PricingUtils.computeRUUsagePriceHourly({
serverId: "default",
requestUnits: 1,
numberOfRegions: null,
multimasterEnabled: false,
isAutoscale: false
});
expect(value).toBe(0);
});
it("should return 0 for NaN regions default cloud, autoscale", () => {
const value = PricingUtils.computeRUUsagePriceHourly({
serverId: "default",
requestUnits: 1,
numberOfRegions: null,
multimasterEnabled: false,
isAutoscale: true
});
expect(value).toBe(0); expect(value).toBe(0);
}); });
it("should return 0 for -1 regions", () => { it("should return 0 for -1 regions", () => {
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, -1, false); const value = PricingUtils.computeRUUsagePriceHourly({
serverId: "default",
requestUnits: 1,
numberOfRegions: -1,
multimasterEnabled: false,
isAutoscale: false
});
expect(value).toBe(0);
});
it("should return 0 for -1 regions, autoscale", () => {
const value = PricingUtils.computeRUUsagePriceHourly({
serverId: "default",
requestUnits: 1,
numberOfRegions: -1,
multimasterEnabled: false,
isAutoscale: true
});
expect(value).toBe(0); expect(value).toBe(0);
}); });
it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster disabled", () => { it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster disabled", () => {
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 1, false); const value = PricingUtils.computeRUUsagePriceHourly({
serverId: "default",
requestUnits: 1,
numberOfRegions: 1,
multimasterEnabled: false,
isAutoscale: false
});
expect(value).toBe(0.00008); expect(value).toBe(0.00008);
}); });
it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster disabled, autoscale", () => {
const value = PricingUtils.computeRUUsagePriceHourly({
serverId: "default",
requestUnits: 1,
numberOfRegions: 1,
multimasterEnabled: false,
isAutoscale: true
});
expect(value).toBe(0.00012);
});
it("should return 0.00051 for Mooncake cloud, 1RU, 1 region, multimaster disabled", () => { it("should return 0.00051 for Mooncake cloud, 1RU, 1 region, multimaster disabled", () => {
const value = PricingUtils.computeRUUsagePriceHourly("mooncake", 1, 1, false); const value = PricingUtils.computeRUUsagePriceHourly({
serverId: "mooncake",
requestUnits: 1,
numberOfRegions: 1,
multimasterEnabled: false,
isAutoscale: false
});
expect(value).toBe(0.00051); expect(value).toBe(0.00051);
}); });
it("should return 0.00051 for Mooncake cloud, 1RU, 1 region, multimaster disabled, autoscale", () => {
const value = PricingUtils.computeRUUsagePriceHourly({
serverId: "mooncake",
requestUnits: 1,
numberOfRegions: 1,
multimasterEnabled: false,
isAutoscale: true
});
expect(value).toBe(0.00076);
});
it("should return 0.00016 for default cloud, 1RU, 2 regions, multimaster disabled", () => { it("should return 0.00016 for default cloud, 1RU, 2 regions, multimaster disabled", () => {
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 2, false); const value = PricingUtils.computeRUUsagePriceHourly({
serverId: "default",
requestUnits: 1,
numberOfRegions: 2,
multimasterEnabled: false,
isAutoscale: false
});
expect(value).toBe(0.00016); expect(value).toBe(0.00016);
}); });
it("should return 0.00016 for default cloud, 1RU, 2 regions, multimaster disabled, autoscale", () => {
const value = PricingUtils.computeRUUsagePriceHourly({
serverId: "default",
requestUnits: 1,
numberOfRegions: 2,
multimasterEnabled: false,
isAutoscale: true
});
expect(value).toBe(0.00024);
});
it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster enabled", () => { it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster enabled", () => {
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 1, true); const value = PricingUtils.computeRUUsagePriceHourly({
serverId: "default",
requestUnits: 1,
numberOfRegions: 1,
multimasterEnabled: true,
isAutoscale: false
});
expect(value).toBe(0.00008); expect(value).toBe(0.00008);
}); });
it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster enabled, autoscale", () => {
const value = PricingUtils.computeRUUsagePriceHourly({
serverId: "default",
requestUnits: 1,
numberOfRegions: 1,
multimasterEnabled: true,
isAutoscale: true
});
expect(value).toBe(0.00012);
});
it("should return 0.00048 for default cloud, 1RU, 2 region, multimaster enabled", () => { it("should return 0.00048 for default cloud, 1RU, 2 region, multimaster enabled", () => {
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 2, true); const value = PricingUtils.computeRUUsagePriceHourly({
serverId: "default",
requestUnits: 1,
numberOfRegions: 2,
multimasterEnabled: true,
isAutoscale: false
});
expect(value).toBe(0.00048); expect(value).toBe(0.00048);
}); });
it("should return 0.00048 for default cloud, 1RU, 2 region, multimaster enabled, autoscale", () => {
const value = PricingUtils.computeRUUsagePriceHourly({
serverId: "default",
requestUnits: 1,
numberOfRegions: 2,
multimasterEnabled: true,
isAutoscale: true
});
expect(value).toBe(0.00096);
});
}); });
describe("getPriceCurrency()", () => { describe("getPriceCurrency()", () => {

View File

@@ -1,6 +1,17 @@
import * as AutoPilotUtils from "../Utils/AutoPilotUtils"; import * as AutoPilotUtils from "../Utils/AutoPilotUtils";
import * as Constants from "../Shared/Constants"; import * as Constants from "../Shared/Constants";
interface ComputeRUUsagePriceHourlyArgs {
serverId: string;
requestUnits: number;
numberOfRegions: number;
multimasterEnabled: boolean;
isAutoscale: boolean;
}
export const estimatedCostDisclaimer =
"*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account";
/** /**
* Anything that is not a number should return 0 * Anything that is not a number should return 0
* Otherwise, return numberOfRegions * Otherwise, return numberOfRegions
@@ -47,15 +58,16 @@ export function getMultimasterMultiplier(numberOfRegions: number, multimasterEna
return multimasterMultiplier; return multimasterMultiplier;
} }
export function computeRUUsagePriceHourly( export function computeRUUsagePriceHourly({
serverId: string, serverId,
requestUnits: number, requestUnits,
numberOfRegions: number, numberOfRegions,
multimasterEnabled: boolean multimasterEnabled,
): number { isAutoscale
}: ComputeRUUsagePriceHourlyArgs): number {
const regionMultiplier: number = getRegionMultiplier(numberOfRegions, multimasterEnabled); const regionMultiplier: number = getRegionMultiplier(numberOfRegions, multimasterEnabled);
const multimasterMultiplier: number = getMultimasterMultiplier(numberOfRegions, multimasterEnabled); const multimasterMultiplier: number = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
const pricePerRu = getPricePerRu(serverId); const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multimasterMultiplier) : getPricePerRu(serverId);
const ruCharge = requestUnits * pricePerRu * multimasterMultiplier * regionMultiplier; const ruCharge = requestUnits * pricePerRu * multimasterMultiplier * regionMultiplier;
return Number(ruCharge.toFixed(5)); return Number(ruCharge.toFixed(5));
@@ -159,28 +171,19 @@ export function getAutoPilotV3SpendHtml(maxAutoPilotThroughputSet: number, isDat
}' target='_blank' aria-label='Learn more about autoscale throughput'>Learn more</a>.`; }' target='_blank' aria-label='Learn more about autoscale throughput'>Learn more</a>.`;
} }
export function computeAutoscaleUsagePriceHourly(
serverId: string,
requestUnits: number,
numberOfRegions: number,
multimasterEnabled: boolean
): number {
const regionMultiplier: number = getRegionMultiplier(numberOfRegions, multimasterEnabled);
const multimasterMultiplier: number = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
const pricePerRu = getAutoscalePricePerRu(serverId, multimasterMultiplier);
const ruCharge = requestUnits * pricePerRu * multimasterMultiplier * regionMultiplier;
return Number(ruCharge.toFixed(5));
}
export function getEstimatedAutoscaleSpendHtml( export function getEstimatedAutoscaleSpendHtml(
throughput: number, throughput: number,
serverId: string, serverId: string,
regions: number, regions: number,
multimaster: boolean multimaster: boolean
): string { ): string {
const hourlyPrice: number = computeAutoscaleUsagePriceHourly(serverId, throughput, regions, multimaster); const hourlyPrice: number = computeRUUsagePriceHourly({
serverId: serverId,
requestUnits: throughput,
numberOfRegions: regions,
multimasterEnabled: multimaster,
isAutoscale: true
});
const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth; const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth;
const currency: string = getPriceCurrency(serverId); const currency: string = getPriceCurrency(serverId);
const currencySign: string = getCurrencySign(serverId); const currencySign: string = getCurrencySign(serverId);
@@ -203,7 +206,13 @@ export function getEstimatedSpendHtml(
regions: number, regions: number,
multimaster: boolean multimaster: boolean
): string { ): string {
const hourlyPrice: number = computeRUUsagePriceHourly(serverId, throughput, regions, multimaster); const hourlyPrice: number = computeRUUsagePriceHourly({
serverId: serverId,
requestUnits: throughput,
numberOfRegions: regions,
multimasterEnabled: multimaster,
isAutoscale: false
});
const dailyPrice: number = hourlyPrice * 24; const dailyPrice: number = hourlyPrice * 24;
const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth; const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth;
const currency: string = getPriceCurrency(serverId); const currency: string = getPriceCurrency(serverId);
@@ -217,7 +226,7 @@ export function getEstimatedSpendHtml(
`${currencySign}${calculateEstimateNumber(monthlyPrice)} monthly </b> ` + `${currencySign}${calculateEstimateNumber(monthlyPrice)} monthly </b> ` +
`(${regions} ${regions === 1 ? "region" : "regions"}, ${throughput}RU/s, ${currencySign}${pricePerRu}/RU)` + `(${regions} ${regions === 1 ? "region" : "regions"}, ${throughput}RU/s, ${currencySign}${pricePerRu}/RU)` +
`<p style='padding: 10px 0px 0px 0px;'>` + `<p style='padding: 10px 0px 0px 0px;'>` +
`<em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>` `<em>${estimatedCostDisclaimer}</em></p>`
); );
} }
@@ -228,9 +237,13 @@ export function getEstimatedSpendAcknowledgeString(
multimaster: boolean, multimaster: boolean,
isAutoscale: boolean isAutoscale: boolean
): string { ): string {
const hourlyPrice: number = isAutoscale const hourlyPrice: number = computeRUUsagePriceHourly({
? computeAutoscaleUsagePriceHourly(serverId, throughput, regions, multimaster) serverId: serverId,
: computeRUUsagePriceHourly(serverId, throughput, regions, multimaster); requestUnits: throughput,
numberOfRegions: regions,
multimasterEnabled: multimaster,
isAutoscale: isAutoscale
});
const dailyPrice: number = hourlyPrice * 24; const dailyPrice: number = hourlyPrice * 24;
const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth; const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth;
const currencySign: string = getCurrencySign(serverId); const currencySign: string = getCurrencySign(serverId);

View File

@@ -58,41 +58,36 @@ export class QueryUtils {
return projections.join(","); return projections.join(",");
} }
public static queryPagesUntilContentPresent( public static async queryPagesUntilContentPresent(
firstItemIndex: number, firstItemIndex: number,
queryItems: (itemIndex: number) => Q.Promise<ViewModels.QueryResults> queryItems: (itemIndex: number) => Promise<ViewModels.QueryResults>
): Q.Promise<ViewModels.QueryResults> { ): Promise<ViewModels.QueryResults> {
let roundTrips: number = 0; let roundTrips: number = 0;
let netRequestCharge: number = 0; let netRequestCharge: number = 0;
const doRequest = (itemIndex: number): Q.Promise<ViewModels.QueryResults> => const doRequest = async (itemIndex: number): Promise<ViewModels.QueryResults> => {
queryItems(itemIndex).then( const results: ViewModels.QueryResults = await queryItems(itemIndex);
(results: ViewModels.QueryResults) => { roundTrips = roundTrips + 1;
roundTrips = roundTrips + 1; results.roundTrips = roundTrips;
results.roundTrips = roundTrips; results.requestCharge = Number(results.requestCharge) + netRequestCharge;
results.requestCharge = Number(results.requestCharge) + netRequestCharge; netRequestCharge = Number(results.requestCharge);
netRequestCharge = Number(results.requestCharge); const resultsMetadata: ViewModels.QueryResultsMetadata = {
const resultsMetadata: ViewModels.QueryResultsMetadata = { hasMoreResults: results.hasMoreResults,
hasMoreResults: results.hasMoreResults, itemCount: results.itemCount,
itemCount: results.itemCount, firstItemIndex: results.firstItemIndex,
firstItemIndex: results.firstItemIndex, lastItemIndex: results.lastItemIndex
lastItemIndex: results.lastItemIndex };
}; if (resultsMetadata.itemCount === 0 && resultsMetadata.hasMoreResults) {
if (resultsMetadata.itemCount === 0 && resultsMetadata.hasMoreResults) { return await doRequest(resultsMetadata.lastItemIndex);
return doRequest(resultsMetadata.lastItemIndex); }
} return results;
return Q.resolve(results); };
},
(error: any) => {
return Q.reject(error);
}
);
return doRequest(firstItemIndex); return await doRequest(firstItemIndex);
} }
public static queryAllPages( public static async queryAllPages(
queryItems: (itemIndex: number) => Q.Promise<ViewModels.QueryResults> queryItems: (itemIndex: number) => Promise<ViewModels.QueryResults>
): Q.Promise<ViewModels.QueryResults> { ): Promise<ViewModels.QueryResults> {
const queryResults: ViewModels.QueryResults = { const queryResults: ViewModels.QueryResults = {
documents: [], documents: [],
activityId: undefined, activityId: undefined,
@@ -103,25 +98,20 @@ export class QueryUtils {
requestCharge: 0, requestCharge: 0,
roundTrips: 0 roundTrips: 0
}; };
const doRequest = (itemIndex: number): Q.Promise<ViewModels.QueryResults> => const doRequest = async (itemIndex: number): Promise<ViewModels.QueryResults> => {
queryItems(itemIndex).then( const results: ViewModels.QueryResults = await queryItems(itemIndex);
(results: ViewModels.QueryResults) => { const { requestCharge, hasMoreResults, itemCount, lastItemIndex, documents } = results;
const { requestCharge, hasMoreResults, itemCount, lastItemIndex, documents } = results; queryResults.roundTrips = queryResults.roundTrips + 1;
queryResults.roundTrips = queryResults.roundTrips + 1; queryResults.requestCharge = Number(queryResults.requestCharge) + Number(requestCharge);
queryResults.requestCharge = Number(queryResults.requestCharge) + Number(requestCharge); queryResults.hasMoreResults = hasMoreResults;
queryResults.hasMoreResults = hasMoreResults; queryResults.itemCount = queryResults.itemCount + itemCount;
queryResults.itemCount = queryResults.itemCount + itemCount; queryResults.lastItemIndex = lastItemIndex;
queryResults.lastItemIndex = lastItemIndex; queryResults.documents = queryResults.documents.concat(documents);
queryResults.documents = queryResults.documents.concat(documents); if (queryResults.hasMoreResults) {
if (queryResults.hasMoreResults) { return doRequest(queryResults.lastItemIndex + 1);
return doRequest(queryResults.lastItemIndex + 1); }
} return queryResults;
return Q.resolve(queryResults); };
},
(error: any) => {
return Q.reject(error);
}
);
return doRequest(0); return doRequest(0);
} }

View File

@@ -54,7 +54,7 @@ describe("Collection Add and Delete SQL spec", () => {
// validate created // validate created
// open database menu // open database menu
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.waitFor(LOADING_STATE_DELAY); await frame.waitFor(CREATE_DELAY);
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
const databases = await frame.$$(`div[class="databaseHeader main1 nodeItem "] > div[class="treeNodeHeader "]`); const databases = await frame.$$(`div[class="databaseHeader main1 nodeItem "] > div[class="treeNodeHeader "]`);
const selectedDbId = await frame.evaluate(element => { const selectedDbId = await frame.evaluate(element => {