mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-27 12:51:41 +00:00
Compare commits
28 Commits
remove-ru-
...
hosted-msa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e41230e8c4 | ||
|
|
c3058ee5a9 | ||
|
|
b000631a0c | ||
|
|
e8f4c8f93c | ||
|
|
16bde97e47 | ||
|
|
6da43ee27b | ||
|
|
ebae484b8f | ||
|
|
dfb1b50621 | ||
|
|
f54e8eb692 | ||
|
|
ea39c1d092 | ||
|
|
c21f42159f | ||
|
|
31e4b49f11 | ||
|
|
40491ec9c5 | ||
|
|
e133df18dd | ||
|
|
0532ed26a2 | ||
|
|
fd60c9c15e | ||
|
|
04ab1f3918 | ||
|
|
b784ac0f86 | ||
|
|
28899f63d7 | ||
|
|
9cbf632577 | ||
|
|
17fd2185dc | ||
|
|
a93c8509cd | ||
|
|
5c93c11bd9 | ||
|
|
85d2378d3a | ||
|
|
84b6075ee8 | ||
|
|
d880723be9 | ||
|
|
4ce9dcc024 | ||
|
|
addcfedd5e |
@@ -3,7 +3,11 @@ PORTAL_RUNNER_PASSWORD=
|
|||||||
PORTAL_RUNNER_SUBSCRIPTION=
|
PORTAL_RUNNER_SUBSCRIPTION=
|
||||||
PORTAL_RUNNER_RESOURCE_GROUP=
|
PORTAL_RUNNER_RESOURCE_GROUP=
|
||||||
PORTAL_RUNNER_DATABASE_ACCOUNT=
|
PORTAL_RUNNER_DATABASE_ACCOUNT=
|
||||||
|
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY=
|
||||||
PORTAL_RUNNER_CONNECTION_STRING=
|
PORTAL_RUNNER_CONNECTION_STRING=
|
||||||
|
NOTEBOOKS_TEST_RUNNER_TENANT_ID=
|
||||||
|
NOTEBOOKS_TEST_RUNNER_CLIENT_ID=
|
||||||
|
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET=
|
||||||
CASSANDRA_CONNECTION_STRING=
|
CASSANDRA_CONNECTION_STRING=
|
||||||
MONGO_CONNECTION_STRING=
|
MONGO_CONNECTION_STRING=
|
||||||
TABLES_CONNECTION_STRING=
|
TABLES_CONNECTION_STRING=
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -43,7 +42,6 @@ src/Contracts/ViewModels.ts
|
|||||||
src/Controls/Heatmap/Heatmap.test.ts
|
src/Controls/Heatmap/Heatmap.test.ts
|
||||||
src/Controls/Heatmap/Heatmap.ts
|
src/Controls/Heatmap/Heatmap.ts
|
||||||
src/Controls/Heatmap/HeatmapDatatypes.ts
|
src/Controls/Heatmap/HeatmapDatatypes.ts
|
||||||
src/Definitions/adal.d.ts
|
|
||||||
src/Definitions/datatables.d.ts
|
src/Definitions/datatables.d.ts
|
||||||
src/Definitions/gif.d.ts
|
src/Definitions/gif.d.ts
|
||||||
src/Definitions/globals.d.ts
|
src/Definitions/globals.d.ts
|
||||||
@@ -202,8 +200,6 @@ src/Explorer/Tabs/QueryTab.test.ts
|
|||||||
src/Explorer/Tabs/QueryTab.ts
|
src/Explorer/Tabs/QueryTab.ts
|
||||||
src/Explorer/Tabs/QueryTablesTab.ts
|
src/Explorer/Tabs/QueryTablesTab.ts
|
||||||
src/Explorer/Tabs/ScriptTabBase.ts
|
src/Explorer/Tabs/ScriptTabBase.ts
|
||||||
src/Explorer/Tabs/SettingsTab.test.ts
|
|
||||||
src/Explorer/Tabs/SettingsTab.ts
|
|
||||||
src/Explorer/Tabs/SparkMasterTab.ts
|
src/Explorer/Tabs/SparkMasterTab.ts
|
||||||
src/Explorer/Tabs/StoredProcedureTab.ts
|
src/Explorer/Tabs/StoredProcedureTab.ts
|
||||||
src/Explorer/Tabs/TabComponents.ts
|
src/Explorer/Tabs/TabComponents.ts
|
||||||
@@ -290,8 +286,6 @@ src/Utils/DatabaseAccountUtils.ts
|
|||||||
src/Utils/JunoUtils.ts
|
src/Utils/JunoUtils.ts
|
||||||
src/Utils/MessageValidation.ts
|
src/Utils/MessageValidation.ts
|
||||||
src/Utils/NotebookConfigurationUtils.ts
|
src/Utils/NotebookConfigurationUtils.ts
|
||||||
src/Utils/OfferUtils.test.ts
|
|
||||||
src/Utils/OfferUtils.ts
|
|
||||||
src/Utils/PricingUtils.test.ts
|
src/Utils/PricingUtils.test.ts
|
||||||
src/Utils/QueryUtils.test.ts
|
src/Utils/QueryUtils.test.ts
|
||||||
src/Utils/QueryUtils.ts
|
src/Utils/QueryUtils.ts
|
||||||
@@ -396,19 +390,5 @@ src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx
|
|||||||
src/GalleryViewer/Cards/GalleryCardComponent.tsx
|
src/GalleryViewer/Cards/GalleryCardComponent.tsx
|
||||||
src/GalleryViewer/GalleryViewer.tsx
|
src/GalleryViewer/GalleryViewer.tsx
|
||||||
src/GalleryViewer/GalleryViewerComponent.tsx
|
src/GalleryViewer/GalleryViewerComponent.tsx
|
||||||
cypress/integration/dataexplorer/CASSANDRA/addCollection.spec.ts
|
|
||||||
cypress/integration/dataexplorer/GRAPH/addCollection.spec.ts
|
|
||||||
cypress/integration/dataexplorer/ci-tests/addCollectionPane.spec.ts
|
|
||||||
cypress/integration/dataexplorer/ci-tests/createDatabase.spec.ts
|
|
||||||
cypress/integration/dataexplorer/ci-tests/deleteCollection.spec.ts
|
|
||||||
cypress/integration/dataexplorer/ci-tests/deleteDatabase.spec.ts
|
|
||||||
cypress/integration/dataexplorer/MONGO/addCollection.spec.ts
|
|
||||||
cypress/integration/dataexplorer/MONGO/addCollectionAutopilot.spec.ts
|
|
||||||
cypress/integration/dataexplorer/MONGO/addCollectionExistingDatabase.spec.ts
|
|
||||||
cypress/integration/dataexplorer/MONGO/provisionDatabaseThroughput.spec.ts
|
|
||||||
cypress/integration/dataexplorer/SQL/addCollection.spec.ts
|
|
||||||
cypress/integration/dataexplorer/TABLE/addCollection.spec.ts
|
|
||||||
cypress/integration/notebook/newNotebook.spec.ts
|
|
||||||
cypress/integration/notebook/resourceTree.spec.ts
|
|
||||||
__mocks__/monaco-editor.ts
|
__mocks__/monaco-editor.ts
|
||||||
src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx
|
src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx
|
||||||
67
.github/workflows/ci.yml
vendored
67
.github/workflows/ci.yml
vendored
@@ -79,32 +79,32 @@ jobs:
|
|||||||
name: dist
|
name: dist
|
||||||
path: dist/
|
path: dist/
|
||||||
endtoendemulator:
|
endtoendemulator:
|
||||||
name: "End To End Tests | Emulator | SQL"
|
name: "End To End Emulator Tests"
|
||||||
needs: [lint, format, compile, unittest]
|
needs: [lint, format, compile, unittest]
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: southpolesteve/cosmos-emulator-github-action@v1
|
|
||||||
- name: Use Node.js 12.x
|
- name: Use Node.js 12.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: 12.x
|
||||||
- name: Restore Cypress Binary Cache
|
- uses: southpolesteve/cosmos-emulator-github-action@v1
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/.cache/Cypress
|
|
||||||
key: ${{ runner.os }}-cypress-binary-cache
|
|
||||||
- name: End to End Tests
|
- name: End to End Tests
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
npm start &
|
npm start &
|
||||||
npm ci --prefix ./cypress
|
npm run wait-for-server
|
||||||
npm run test:ci --prefix ./cypress -- --spec ./integration/dataexplorer/ci-tests/createDatabase.spec.ts
|
npx jest -c ./jest.config.e2e.js --detectOpenHandles sql
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
EMULATOR_ENDPOINT: https://0.0.0.0:8081/
|
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/explorer.html?platform=Emulator"
|
||||||
|
PLATFORM: "Emulator"
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
- uses: actions/upload-artifact@v2
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
name: screenshots
|
||||||
|
path: failed-*
|
||||||
accessibility:
|
accessibility:
|
||||||
name: "Accessibility | Hosted"
|
name: "Accessibility | Hosted"
|
||||||
needs: [lint, format, compile, unittest]
|
needs: [lint, format, compile, unittest]
|
||||||
@@ -123,13 +123,13 @@ jobs:
|
|||||||
sudo sysctl -p
|
sudo sysctl -p
|
||||||
npm ci
|
npm ci
|
||||||
npm start &
|
npm start &
|
||||||
npx wait-on -i 5000 https-get://0.0.0.0:1234/
|
npx wait-on -i 5000 https-get://0.0.0.0:1234/
|
||||||
node utils/accesibilityCheck.js
|
node utils/accesibilityCheck.js
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
endtoendpuppeteer:
|
endtoendhosted:
|
||||||
name: "End to end puppeteer tests"
|
name: "End to End Hosted Tests"
|
||||||
needs: [lint, format, compile, unittest]
|
needs: [lint, format, compile, unittest]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -138,7 +138,7 @@ jobs:
|
|||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: 12.x
|
||||||
- name: End to End Puppeteer Tests
|
- name: End to End Hosted Tests
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
npm start &
|
npm start &
|
||||||
@@ -147,19 +147,27 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
|
PORTAL_RUNNER_SUBSCRIPTION: ${{ secrets.PORTAL_RUNNER_SUBSCRIPTION }}
|
||||||
|
PORTAL_RUNNER_RESOURCE_GROUP: ${{ secrets.PORTAL_RUNNER_RESOURCE_GROUP }}
|
||||||
|
PORTAL_RUNNER_DATABASE_ACCOUNT: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT }}
|
||||||
|
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY }}
|
||||||
|
NOTEBOOKS_TEST_RUNNER_TENANT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_TENANT_ID }}
|
||||||
|
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
|
||||||
|
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
|
||||||
PORTAL_RUNNER_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
|
PORTAL_RUNNER_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
|
||||||
MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
|
MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
|
||||||
CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }}
|
CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }}
|
||||||
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, endtoendpuppeteer]
|
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 }}
|
||||||
@@ -183,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, endtoendpuppeteer]
|
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 }}
|
||||||
@@ -205,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"
|
||||||
|
|||||||
25
.github/workflows/runners.yml
vendored
25
.github/workflows/runners.yml
vendored
@@ -1,25 +0,0 @@
|
|||||||
name: Runners
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 * 1 * *"
|
|
||||||
jobs:
|
|
||||||
sqlcreatecollection:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: "SQL | Create Collection"
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-node@v1
|
|
||||||
- run: npm ci
|
|
||||||
- run: npm run test:e2e
|
|
||||||
env:
|
|
||||||
PORTAL_RUNNER_APP_INSIGHTS_KEY: ${{ secrets.PORTAL_RUNNER_APP_INSIGHTS_KEY }}
|
|
||||||
PORTAL_RUNNER_USERNAME: ${{ secrets.PORTAL_RUNNER_USERNAME }}
|
|
||||||
PORTAL_RUNNER_PASSWORD: ${{ secrets.PORTAL_RUNNER_PASSWORD }}
|
|
||||||
PORTAL_RUNNER_SUBSCRIPTION: 69e02f2d-f059-4409-9eac-97e8a276ae2c
|
|
||||||
PORTAL_RUNNER_RESOURCE_GROUP: runners
|
|
||||||
PORTAL_RUNNER_DATABASE_ACCOUNT: portal-sql-runner
|
|
||||||
- uses: actions/upload-artifact@v2
|
|
||||||
if: failure()
|
|
||||||
with:
|
|
||||||
name: screenshots
|
|
||||||
path: failure.png
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,9 +9,6 @@ pkg/DataExplorer/*
|
|||||||
test/out/*
|
test/out/*
|
||||||
workers/**/*.js
|
workers/**/*.js
|
||||||
*.trx
|
*.trx
|
||||||
cypress/videos
|
|
||||||
cypress/screenshots
|
|
||||||
cypress/fixtures
|
|
||||||
notebookapp/*
|
notebookapp/*
|
||||||
Contracts/*
|
Contracts/*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
BIN
.vs/slnx.sqlite
Normal file
BIN
.vs/slnx.sqlite
Normal file
Binary file not shown.
49
README.md
49
README.md
@@ -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
|
||||||
|
|
||||||
@@ -76,17 +57,7 @@ Unit tests are located adjacent to the code under test and run with [Jest](https
|
|||||||
|
|
||||||
#### End to End CI Tests
|
#### End to End CI Tests
|
||||||
|
|
||||||
[Cypress](https://www.cypress.io/) is used for end to end tests and are contained in `cypress/`. Currently, it operates as sub project with its own typescript config and dependencies. It also only operates against the emulator. To run cypress tests:
|
Jest and Puppeteer are used for end to end browser based tests and are contained in `test/`. To run these tests locally:
|
||||||
|
|
||||||
1. Ensure the emulator is running
|
|
||||||
2. Start cosmos explorer in emulator mode: `PLATFORM=Emulator npm run watch`
|
|
||||||
3. Move into `cypress/` folder: `cd cypress`
|
|
||||||
4. Install dependencies: `npm install`
|
|
||||||
5. Run cypress headless(`npm run test`) or in interactive mode(`npm run test:debug`)
|
|
||||||
|
|
||||||
#### End to End Production Tests
|
|
||||||
|
|
||||||
Jest and Puppeteer are used for end to end production runners and are contained in `test/`. To run these tests locally:
|
|
||||||
|
|
||||||
1. Copy .env.example to .env
|
1. Copy .env.example to .env
|
||||||
2. Update the values in .env including your local data explorer endpoint (ask a teammate/codeowner for help with .env values)
|
2. Update the values in .env including your local data explorer endpoint (ask a teammate/codeowner for help with .env values)
|
||||||
|
|||||||
4
cypress/.gitignore
vendored
4
cypress/.gitignore
vendored
@@ -1,4 +0,0 @@
|
|||||||
cypress.env.json
|
|
||||||
cypress/report
|
|
||||||
cypress/screenshots
|
|
||||||
cypress/videos
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
// Cleans up old databases from previous test runs
|
|
||||||
const { CosmosClient } = require("@azure/cosmos");
|
|
||||||
|
|
||||||
// TODO: Add support for other API connection strings
|
|
||||||
const mongoRegex = RegExp("mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com");
|
|
||||||
|
|
||||||
async function cleanup() {
|
|
||||||
const connectionString = process.env.CYPRESS_CONNECTION_STRING;
|
|
||||||
if (!connectionString) {
|
|
||||||
throw new Error("Connection string not provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
let client;
|
|
||||||
switch (true) {
|
|
||||||
case connectionString.includes("mongodb://"): {
|
|
||||||
const [, key, accountName] = connectionString.match(mongoRegex);
|
|
||||||
client = new CosmosClient({
|
|
||||||
key,
|
|
||||||
endpoint: `https://${accountName}.documents.azure.com:443/`
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// TODO: Add support for other API connection strings
|
|
||||||
default:
|
|
||||||
client = new CosmosClient(connectionString);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await client.databases.readAll().fetchAll();
|
|
||||||
return Promise.all(
|
|
||||||
response.resources.map(async db => {
|
|
||||||
const dbTimestamp = new Date(db._ts * 1000);
|
|
||||||
const twentyMinutesAgo = new Date(Date.now() - 1000 * 60 * 20);
|
|
||||||
if (dbTimestamp < twentyMinutesAgo) {
|
|
||||||
await client.database(db.id).delete();
|
|
||||||
console.log(`DELETED: ${db.id} | Timestamp: ${dbTimestamp}`);
|
|
||||||
} else {
|
|
||||||
console.log(`SKIPPED: ${db.id} | Timestamp: ${dbTimestamp}`);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup()
|
|
||||||
.then(() => {
|
|
||||||
process.exit(0);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"integrationFolder": "./integration",
|
|
||||||
"pluginsFile": false,
|
|
||||||
"fixturesFolder": false,
|
|
||||||
"supportFile": "./support/index.js",
|
|
||||||
"defaultCommandTimeout": 90000,
|
|
||||||
"chromeWebSecurity": false,
|
|
||||||
"reporter": "mochawesome",
|
|
||||||
"reporterOptions": {
|
|
||||||
"reportDir": "cypress/report",
|
|
||||||
"json": true,
|
|
||||||
"overwrite": false,
|
|
||||||
"html": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
// 1. Click on "New Container" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Cassandra API Test - createDatabase", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString(connectionString.constants.cassandra);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new table in Cassandra API", () => {
|
|
||||||
const keyspaceId = `KeyspaceId${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const tableId = `TableId112`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Table"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[id="keyspace-id"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.type(keyspaceId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[class="textfontclr"]')
|
|
||||||
.type(tableId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('data-test="addCollection-createCollection"')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", tableId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
// 1. Click on "New Graph" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Graph API Test", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString(connectionString.constants.graph);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new graph in Graph API", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const graphId = `TestGraph${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const partitionKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Graph"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(graphId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(partitionKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId)
|
|
||||||
.click()
|
|
||||||
.should("contain", graphId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
// 1. Click on "New Container" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// // 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Mongo API Test - createDatabase", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new collection in Mongo API", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Collection"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(sharedKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find("#submitBtnAddCollection")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId)
|
|
||||||
.click()
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
// 1. Click on "New Container" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Mongo API Test", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
});
|
|
||||||
|
|
||||||
it.skip("Create a new collection in Mongo API - Autopilot", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Collection"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="throughputModeContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.and(input => {
|
|
||||||
expect(input.get(0).textContent, "first item").contains("Autopilot (preview)");
|
|
||||||
expect(input.get(1).textContent, "second item").contains("Manual");
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[id="newContainer-databaseThroughput-autoPilotRadio"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('select[name="autoPilotTiers"]')
|
|
||||||
// .eq(1).should('contain', '4,000 RU/s');
|
|
||||||
// // .select('4,000 RU/s').should('have.value', '1');
|
|
||||||
|
|
||||||
.find('option[value="2"]')
|
|
||||||
.then($element => $element.get(1).setAttribute("selected", "selected"));
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(sharedKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId)
|
|
||||||
.click()
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Mongo API Test", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
});
|
|
||||||
|
|
||||||
it.skip("Create a new collection in existing database in Mongo API", () => {
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('span[class="nodeLabel"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.then($span => {
|
|
||||||
const dbId1 = $span.text();
|
|
||||||
cy.log("DBBB", dbId1);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Collection"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-existingDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-existingDatabase"]')
|
|
||||||
.type(dbId1);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(sharedKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.click()
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context.skip("Mongo API Test", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new collection in Mongo API - Provision database throughput", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Collection"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find(".createNewDatabaseOrUseExisting")
|
|
||||||
.should("have.length", 2)
|
|
||||||
.and(input => {
|
|
||||||
expect(input.get(0).textContent, "first item").contains("Create new");
|
|
||||||
expect(input.get(1).textContent, "second item").contains("Use existing");
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(sharedKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId)
|
|
||||||
.click()
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new collection - without provision database throughput", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionIdTitle = `Add Collection`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Collection"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.uncheck();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[id="tab2"]')
|
|
||||||
.check({ force: true });
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(sharedKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId)
|
|
||||||
.click()
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new collection - without provision database throughput Fixed Storage Capacity", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Collection"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.uncheck();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[id="tab1"]')
|
|
||||||
.check({ force: true });
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId)
|
|
||||||
.click()
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
// 1. Click on "New Container" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("SQL API Test", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new container in SQL API", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Container"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(sharedKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find("#submitBtnAddCollection")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
// 1. Click on "New Container" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Table API Test", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString(connectionString.constants.table);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new table in Table API", () => {
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Table"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// 1. Click on "New Container" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Emulator - createDatabase", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("http://localhost:1234/explorer.html");
|
|
||||||
});
|
|
||||||
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionIdTitle = `Add Collection`;
|
|
||||||
const partitionKey = `PartitionKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
it("Create a new collection", () => {
|
|
||||||
cy.contains("New Container").click();
|
|
||||||
|
|
||||||
// cy.contains(collectionIdTitle);
|
|
||||||
|
|
||||||
cy.get(".createNewDatabaseOrUseExisting")
|
|
||||||
.should("have.length", 2)
|
|
||||||
.and(input => {
|
|
||||||
expect(input.get(0).textContent, "first item").contains("Create new");
|
|
||||||
expect(input.get(1).textContent, "second item").contains("Use existing");
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get('input[data-test="addCollection-createNewDatabase"]').check();
|
|
||||||
|
|
||||||
cy.get('input[data-test="addCollection-newDatabaseId"]').type(dbId);
|
|
||||||
|
|
||||||
cy.get('input[data-test="addCollection-collectionId"]').type(collectionId);
|
|
||||||
|
|
||||||
cy.get('input[data-test="databaseThroughputValue"]').should("have.value", "400");
|
|
||||||
|
|
||||||
cy.get('input[data-test="addCollection-partitionKeyValue"]').type(partitionKey);
|
|
||||||
|
|
||||||
cy.get('input[data-test="addCollection-createCollection"]').click();
|
|
||||||
|
|
||||||
cy.get('div[data-test="resourceTreeId"]').should("exist");
|
|
||||||
|
|
||||||
cy.get('div[data-test="resourceTree-collectionsTree"]').should("contain", dbId);
|
|
||||||
|
|
||||||
cy.get('div[data-test="databaseList"]').should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
// 1. Click on "New Database" on the command bar
|
|
||||||
// 2. a Pane with the title "Add Database" should appear on the right side of the screen
|
|
||||||
// i. It includes an input box for the database Id.
|
|
||||||
// ii. It includes a checkbox called "Provision throughput".
|
|
||||||
// iii. Whe the checkbox is marked, a new input with a throughput control let's you customize RU at the database level
|
|
||||||
// 3. Create a database WITHOUT "Provision throughput" checked.
|
|
||||||
// 4. It should appear in the Data Explorer list.
|
|
||||||
// 5. Repeat steps 1-3 but create a database WITH "Provision throughput" with the default RU value.
|
|
||||||
// 6. It should appear in the Data Explorer list.
|
|
||||||
// 7. If expanded, it should have the list item called "Scale", that once clicked, it should show the "Scale" tab.
|
|
||||||
// 8. Inside that tab, a throughput control will let you change the RU value within the permited range.
|
|
||||||
// 9. If you change the value, it should enable the "Save" button.
|
|
||||||
// 10. Click "Save" and verify that the process completes without error.
|
|
||||||
// 11. Close the tab and reopen it and verify that the input contains the last saved value.%
|
|
||||||
|
|
||||||
const crypto = require("crypto");
|
|
||||||
const client = require("../../../utilities/cosmosClient");
|
|
||||||
const randomString = crypto.randomBytes(2).toString("hex");
|
|
||||||
const databaseId = `TestDB-${randomString}`;
|
|
||||||
const collectionId = `TestColl-${randomString}`;
|
|
||||||
|
|
||||||
context("Emulator - Create database -> container -> item", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
const { resources } = await client.databases.readAll().fetchAll();
|
|
||||||
for (const database of resources) {
|
|
||||||
await client.database(database.id).delete();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("creates a new database", () => {
|
|
||||||
cy.visit("https://0.0.0.0:1234/explorer.html?platform=Emulator");
|
|
||||||
cy.contains("New Container").click();
|
|
||||||
cy.get("[data-test=addCollection-newDatabaseId]").click();
|
|
||||||
cy.get("[data-test=addCollection-newDatabaseId]").type(databaseId);
|
|
||||||
cy.get("[data-test=addCollection-collectionId]").click();
|
|
||||||
cy.get("[data-test=addCollection-collectionId]").type(collectionId);
|
|
||||||
cy.get("[data-test=addCollection-partitionKeyValue]").click();
|
|
||||||
cy.get("[data-test=addCollection-partitionKeyValue]").type("/pk");
|
|
||||||
cy.get('input[name="createCollection"]').click();
|
|
||||||
cy.get(".dataResourceTree").should("contain", databaseId);
|
|
||||||
cy.get(".dataResourceTree")
|
|
||||||
.contains(databaseId)
|
|
||||||
.click();
|
|
||||||
cy.get(".dataResourceTree").should("contain", collectionId);
|
|
||||||
cy.get(".dataResourceTree")
|
|
||||||
.contains(collectionId)
|
|
||||||
.click();
|
|
||||||
cy.get(".dataResourceTree")
|
|
||||||
.contains("Items")
|
|
||||||
.click();
|
|
||||||
cy.get(".dataResourceTree")
|
|
||||||
.contains("Items")
|
|
||||||
.click();
|
|
||||||
cy.wait(1000); // React rendering inside KO causes some weird async rendering that makes this test flaky without waiting
|
|
||||||
cy.get(".commandBarContainer")
|
|
||||||
.contains("New Item")
|
|
||||||
.click();
|
|
||||||
cy.wait(1000); // React rendering inside KO causes some weird async rendering that makes this test flaky without waiting
|
|
||||||
cy.get(".commandBarContainer")
|
|
||||||
.contains("Save")
|
|
||||||
.click();
|
|
||||||
cy.wait(1000); // React rendering inside KO causes some weird async rendering that makes this test flaky without waiting
|
|
||||||
cy.get(".documentsGridHeaderContainer").should("contain", "replace_with_new_document_id");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
// 1. Click last database in the resource tree
|
|
||||||
// 2. Click the last collection within the database
|
|
||||||
// 3. Select the context menu within the collection
|
|
||||||
// 4. Select "Delete Container" option in the dropdown
|
|
||||||
// 5. On Selection, Delete Container pane opens on the right side
|
|
||||||
// 6. Enter the same collection id that is to be deleted and click ok
|
|
||||||
// 7. Now, the resource tree refreshes, the deleted collection should not appear under the database
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Emulator - deleteCollection", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("http://localhost:1234/explorer.html");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Delete a collection", () => {
|
|
||||||
cy.get(".databaseId")
|
|
||||||
.last()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get(".collectionList")
|
|
||||||
.last()
|
|
||||||
.then($id => {
|
|
||||||
const collectionId = $id.text();
|
|
||||||
|
|
||||||
cy.get('span[data-test="collectionEllipsisMenu"]').should("exist");
|
|
||||||
|
|
||||||
cy.get('span[data-test="collectionEllipsisMenu"]')
|
|
||||||
.invoke("show")
|
|
||||||
.last()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get('div[data-test="collectionContextMenu"]')
|
|
||||||
.contains("Delete Container")
|
|
||||||
.click({ force: true });
|
|
||||||
|
|
||||||
cy.get('input[data-test="confirmCollectionId"]').type(collectionId.trim());
|
|
||||||
|
|
||||||
cy.get('input[data-test="deleteCollection"]').click();
|
|
||||||
|
|
||||||
cy.get('div[data-test="databaseList"]').should("not.contain", collectionId);
|
|
||||||
|
|
||||||
cy.get('div[data-test="databaseMenu"]').should("not.contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
// 1. Click last database in the resource tree
|
|
||||||
// 2. Select the context menu within the database
|
|
||||||
// 4. Select "Delete Database" option in the dropdown
|
|
||||||
// 5. On Selection, Delete Database pane opens on the right side
|
|
||||||
// 6. Enter the same database id that is to be deleted and click ok
|
|
||||||
// 7. Now, the resource tree refreshes, the deleted database should not appear in the resource tree
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Emulator - deleteDatabase", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
let db_rid = "";
|
|
||||||
const date = new Date().toUTCString();
|
|
||||||
let authToken = "";
|
|
||||||
cy.visit("http://localhost:1234/explorer.html");
|
|
||||||
|
|
||||||
// Creating auth token for collection creation
|
|
||||||
cy.request({
|
|
||||||
method: "GET",
|
|
||||||
url: "https://localhost:8081/_explorer/authorization/post/dbs/",
|
|
||||||
headers: {
|
|
||||||
"x-ms-date": date,
|
|
||||||
authorization: "-"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
authToken = response.body.Token; // Getting auth token for collection creation
|
|
||||||
return new Cypress.Promise((resolve, reject) => {
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
cy.request({
|
|
||||||
method: "POST",
|
|
||||||
url: "https://localhost:8081/dbs",
|
|
||||||
headers: {
|
|
||||||
"x-ms-date": date,
|
|
||||||
authorization: authToken,
|
|
||||||
"x-ms-version": "2018-12-31"
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
id: dbId
|
|
||||||
}
|
|
||||||
}).then(response => {
|
|
||||||
cy.log("Response", response);
|
|
||||||
db_rid = response.body._rid;
|
|
||||||
return new Cypress.Promise((resolve, reject) => {
|
|
||||||
cy.log("Rid", db_rid);
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Delete a database", () => {
|
|
||||||
cy.get('span[data-test="refreshTree"]').click();
|
|
||||||
|
|
||||||
cy.get(".databaseId")
|
|
||||||
.last()
|
|
||||||
.then($id => {
|
|
||||||
const dbId = $id.text();
|
|
||||||
|
|
||||||
cy.get('span[data-test="databaseEllipsisMenu"]').should("exist");
|
|
||||||
|
|
||||||
cy.get('span[data-test="databaseEllipsisMenu"]')
|
|
||||||
.invoke("show")
|
|
||||||
.last()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get('div[data-test="databaseContextMenu"]')
|
|
||||||
.contains("Delete Database")
|
|
||||||
.click({ force: true });
|
|
||||||
|
|
||||||
cy.get('input[data-test="confirmDatabaseId"]').type(dbId.trim());
|
|
||||||
|
|
||||||
cy.get('input[data-test="deleteDatabase"]').click();
|
|
||||||
|
|
||||||
cy.get('div[data-test="databaseList"]').should("not.contain", dbId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# Notebook end-to-end tests
|
|
||||||
This describes how to run the tests locally
|
|
||||||
|
|
||||||
## Stand up a local notebook container instance:
|
|
||||||
Instructions on how to build and run the container [here](https://microsoft.sharepoint.com/teams/DocDB/_layouts/OneNote.aspx?id=%2Fteams%2FDocDB%2FSiteAssets%2FDocDB%20Team%20Notebook&wd=target%28Tools%20_%20SDK%2FPortal%2FDevelopment.one%7CF800BE8E-1E31-48FE-90D7-EF698EF88112%2FHow%20to%20build%20notebook%20service%7C4BAA153B-422C-41E2-B997-F3FCE02CD743%2F%29)
|
|
||||||
|
|
||||||
## Run a local data explorer
|
|
||||||
Instructions are in [`DataExplorer/README.md`](https://msdata.visualstudio.com/CosmosDB/_git/cosmosdb-dataexplorer?path=%2FProduct%2FPortal%2FDataExplorer%2FREADME.md&_a=preview).
|
|
||||||
|
|
||||||
Make sure you can run Data Explorer locally from the web browser.
|
|
||||||
|
|
||||||
## Run cypress tests
|
|
||||||
1. Edit the URL for your DataExplorer in the `.spec.ts` file
|
|
||||||
2. Run the test:
|
|
||||||
```bash
|
|
||||||
cd DataExplorer/cypress
|
|
||||||
npm i
|
|
||||||
npm t -- --spec 'integration/notebook/newNotebook.spec.ts'
|
|
||||||
```
|
|
||||||
|
|
||||||
To run in Debug mode:
|
|
||||||
```
|
|
||||||
npm run test:debug
|
|
||||||
```
|
|
||||||
This opens Cypress UI
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
* The tests are recorded in the `videos` folder.
|
|
||||||
* Cypress does not support hover: workarounds [here](https://docs.cypress.io/api/commands/hover.html#Workarounds).
|
|
||||||
|
|
||||||
|
|
||||||
## References
|
|
||||||
* [Cypress API](https://docs.cypress.io/api/api/table-of-contents.html)
|
|
||||||
* [Cypress cookbook](https://docs.cypress.io/faq/questions/using-cypress-faq.html#How-do-I-get-an-element%E2%80%99s-text-contents)
|
|
||||||
* [Cypress best practices](https://docs.cypress.io/guides/references/best-practices.html#Selecting-Elements)
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
// THIS ADDS A NEW NOTEBOOK TO YOUR NOTEBOOKS
|
|
||||||
context("New Notebook smoke test", () => {
|
|
||||||
const timeout = 15000; // in ms
|
|
||||||
const explorerUrl =
|
|
||||||
"https://localhost:1234/explorer.html?feature.notebookserverurl=https%3A%2F%2Flocalhost%3A10001%2F12345%2Fnotebook&feature.notebookServerToken=token&feature.enablenotebooks=true";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for UI to be ready
|
|
||||||
*/
|
|
||||||
const waitForReady = () => {
|
|
||||||
cy.get(".splashScreenContainer", { timeout }).should("be.visible");
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit(explorerUrl);
|
|
||||||
waitForReady();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new notebook and run some code", () => {
|
|
||||||
// Create new notebook
|
|
||||||
cy.contains("New Notebook").click();
|
|
||||||
|
|
||||||
// Check tab name
|
|
||||||
cy.get("li.tabList .tabNavText").should($span => {
|
|
||||||
const text = $span.text();
|
|
||||||
expect(text).to.match(/^Untitled.*\.ipynb$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for python3 | idle status
|
|
||||||
cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should($p => {
|
|
||||||
const text = $p.text();
|
|
||||||
expect(text).to.match(/^python3.*idle$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Click on a cell
|
|
||||||
cy.get(".cell-container")
|
|
||||||
.as("cellContainer")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Type in some code
|
|
||||||
cy.get("@cellContainer").type("2+4");
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
cy.get('[data-test="Run"]')
|
|
||||||
.first()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Verify results
|
|
||||||
cy.get("@cellContainer").within(() => {
|
|
||||||
cy.get("pre code span").should("contain", "6");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Restart kernel
|
|
||||||
cy.get('[data-test="Run"] button')
|
|
||||||
.eq(-1)
|
|
||||||
.click();
|
|
||||||
cy.get("li")
|
|
||||||
.contains("Restart Kernel")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Wait for python3 | restarting status
|
|
||||||
cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should($p => {
|
|
||||||
const text = $p.text();
|
|
||||||
expect(text).to.match(/^python3.*restarting$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for python3 | idle status
|
|
||||||
cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should($p => {
|
|
||||||
const text = $p.text();
|
|
||||||
expect(text).to.match(/^python3.*idle$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Click on a cell
|
|
||||||
cy.get(".cell-container")
|
|
||||||
.as("cellContainer")
|
|
||||||
.find(".input")
|
|
||||||
.as("codeInput")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Type in some code
|
|
||||||
cy.get("@codeInput").type("{backspace}{backspace}{backspace}4+5");
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
cy.get('[data-test="Run"]')
|
|
||||||
.first()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Verify results
|
|
||||||
cy.get("@cellContainer").within(() => {
|
|
||||||
cy.get("pre code span").should("contain", "9");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
context("Resource tree notebook file manipulation", () => {
|
|
||||||
const timeout = 15000; // in ms
|
|
||||||
const explorerUrl =
|
|
||||||
"https://localhost:1234/explorer.html?feature.notebookserverurl=https%3A%2F%2Flocalhost%3A10001%2F12345%2Fnotebook&feature.notebookServerToken=token&feature.enablenotebooks=true";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for UI to be ready
|
|
||||||
*/
|
|
||||||
const waitForReady = () => {
|
|
||||||
cy.get(".splashScreenContainer", { timeout }).should("be.visible");
|
|
||||||
};
|
|
||||||
|
|
||||||
const clickContextMenuAndSelectOption = (nodeLabel, option) => {
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${nodeLabel}"]`)
|
|
||||||
.find("button.treeMenuEllipsis")
|
|
||||||
.click();
|
|
||||||
cy.get('[data-test="treeComponentMenuItemContainer"]')
|
|
||||||
.contains(option)
|
|
||||||
.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
const createFolder = folder => {
|
|
||||||
clickContextMenuAndSelectOption("My Notebooks/", "New Directory");
|
|
||||||
|
|
||||||
cy.get("#stringInputPane").within(() => {
|
|
||||||
cy.get('input[name="collectionIdConfirmation"]').type(folder);
|
|
||||||
cy.get("form").submit();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteItem = nodeName => {
|
|
||||||
clickContextMenuAndSelectOption(`${nodeName}`, "Delete");
|
|
||||||
cy.get(".ms-Dialog-main")
|
|
||||||
.contains("Delete")
|
|
||||||
.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit(explorerUrl);
|
|
||||||
waitForReady();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create and remove a directory", () => {
|
|
||||||
const folder = "e2etest_folder1";
|
|
||||||
createFolder(folder);
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).should("exist");
|
|
||||||
deleteItem(`${folder}/`);
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).should("not.exist");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create and rename a directory", () => {
|
|
||||||
const folder = "e2etest_folder2";
|
|
||||||
const renamedFolder = "e2etest_folder2_renamed";
|
|
||||||
createFolder(folder);
|
|
||||||
|
|
||||||
// Rename
|
|
||||||
clickContextMenuAndSelectOption(`${folder}/`, "Rename");
|
|
||||||
cy.get("#stringInputPane").within(() => {
|
|
||||||
cy.get('input[name="collectionIdConfirmation"]')
|
|
||||||
.clear()
|
|
||||||
.type(renamedFolder);
|
|
||||||
cy.get("form").submit();
|
|
||||||
});
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${renamedFolder}/"]`).should("exist");
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).should("not.exist");
|
|
||||||
|
|
||||||
deleteItem(`${renamedFolder}/`);
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${renamedFolder}/"]`).should("not.exist");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a notebook inside a directory", () => {
|
|
||||||
const folder = "e2etest_folder3";
|
|
||||||
const newNotebookName = "Untitled.ipynb";
|
|
||||||
createFolder(folder);
|
|
||||||
clickContextMenuAndSelectOption(`${folder}/`, "New Notebook");
|
|
||||||
|
|
||||||
// Verify tab is open
|
|
||||||
cy.get(".tabList")
|
|
||||||
.contains(newNotebookName)
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
// Close tab
|
|
||||||
cy.get(`.tabList[title="notebooks/${folder}/${newNotebookName}"]`)
|
|
||||||
.find(".cancelButton")
|
|
||||||
.click();
|
|
||||||
// When running from command line, closing the tab is too fast
|
|
||||||
cy.get("body").then($body => {
|
|
||||||
if ($body.find(".ms-Dialog-main").length) {
|
|
||||||
// For some reason, this does not work
|
|
||||||
// cy.get(".ms-Dialog-main").contains("Close").click();
|
|
||||||
cy.get(".ms-Dialog-main .ms-Button--primary").click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Expand folder node
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).click();
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("exist");
|
|
||||||
|
|
||||||
// Delete notebook
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`)
|
|
||||||
.find("button.treeMenuEllipsis")
|
|
||||||
.click();
|
|
||||||
cy.get('[data-test="treeComponentMenuItemContainer"]')
|
|
||||||
.contains("Delete")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Confirm
|
|
||||||
cy.get(".ms-Dialog-main")
|
|
||||||
.contains("Delete")
|
|
||||||
.click();
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("not.exist");
|
|
||||||
|
|
||||||
deleteItem(`${folder}/`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create and rename a notebook inside a directory", () => {
|
|
||||||
const folder = "e2etest_folder4";
|
|
||||||
const newNotebookName = "Untitled.ipynb";
|
|
||||||
const renamedNotebookName = "mynotebook.ipynb";
|
|
||||||
createFolder(folder);
|
|
||||||
clickContextMenuAndSelectOption(`${folder}/`, "New Notebook");
|
|
||||||
|
|
||||||
// Close tab
|
|
||||||
cy.get(`.tabList[title="notebooks/${folder}/${newNotebookName}"]`)
|
|
||||||
.find(".cancelButton")
|
|
||||||
.click();
|
|
||||||
cy.get("body").then($body => {
|
|
||||||
if ($body.find(".ms-Dialog-main").length) {
|
|
||||||
// For some reason, this does not work
|
|
||||||
// cy.get(".ms-Dialog-main").contains("Close").click();
|
|
||||||
cy.get(".ms-Dialog-main .ms-Button--primary").click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Expand folder node
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).click();
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("exist");
|
|
||||||
|
|
||||||
// Rename notebook
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`)
|
|
||||||
.find("button.treeMenuEllipsis")
|
|
||||||
.click();
|
|
||||||
cy.get('[data-test="treeComponentMenuItemContainer"]')
|
|
||||||
.contains("Rename")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get("#stringInputPane").within(() => {
|
|
||||||
cy.get('input[name="collectionIdConfirmation"]')
|
|
||||||
.clear()
|
|
||||||
.type(renamedNotebookName);
|
|
||||||
cy.get("form").submit();
|
|
||||||
});
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("not.exist");
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${renamedNotebookName}"]`).should("exist");
|
|
||||||
|
|
||||||
// Delete notebook
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${renamedNotebookName}"]`)
|
|
||||||
.find("button.treeMenuEllipsis")
|
|
||||||
.click();
|
|
||||||
cy.get('[data-test="treeComponentMenuItemContainer"]')
|
|
||||||
.contains("Delete")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Confirm
|
|
||||||
cy.get(".ms-Dialog-main")
|
|
||||||
.contains("Delete")
|
|
||||||
.click();
|
|
||||||
// Give it time to settle
|
|
||||||
cy.wait(1000);
|
|
||||||
deleteItem(`${folder}/`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
3066
cypress/package-lock.json
generated
3066
cypress/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "cosmos-explorer-cypress",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "cypress run",
|
|
||||||
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
|
|
||||||
"test:sql": "cypress run --browser chrome --spec \"./integration/dataexplorer/SQL/*\"",
|
|
||||||
"test:ci": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/ https-get://0.0.0.0:8081/_explorer/index.html && cypress run --browser edge --headless",
|
|
||||||
"test:debug": "cypress open"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"cypress": "^4.8.0",
|
|
||||||
"mocha": "^7.0.1",
|
|
||||||
"mochawesome": "^4.1.0",
|
|
||||||
"mochawesome-merge": "^4.0.1",
|
|
||||||
"mochawesome-report-generator": "^4.1.0",
|
|
||||||
"typescript": "3.4.3",
|
|
||||||
"wait-on": "^4.0.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@microsoft/applicationinsights-web": "^2.5.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
let appInsightsLib = require("@microsoft/applicationinsights-web");
|
|
||||||
|
|
||||||
const appInsights = new appInsightsLib.ApplicationInsights({
|
|
||||||
config: {
|
|
||||||
instrumentationKey: "fe61c39f-7d32-4488-a191-b13621965315"
|
|
||||||
/* ...Other Configuration Options... */
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
appInsights.loadAppInsights();
|
|
||||||
|
|
||||||
Cypress.on("fail", (error, runnable) => {
|
|
||||||
// App Insights will record the fail tests for Create Collection
|
|
||||||
let message = JSON.stringify(runnable.title);
|
|
||||||
appInsights.trackTrace({
|
|
||||||
message: `${message}`,
|
|
||||||
properties: {
|
|
||||||
passed: false,
|
|
||||||
error: error
|
|
||||||
}
|
|
||||||
});
|
|
||||||
throw error; // throw error to have test still fail
|
|
||||||
});
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"strict": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"module": "commonjs",
|
|
||||||
"target": "es5",
|
|
||||||
"lib": ["es5", "dom", "es6"],
|
|
||||||
"types": ["cypress", "node"]
|
|
||||||
},
|
|
||||||
"include": ["**/*.ts", "**/*.spec.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
loginUsingConnectionString: function() {
|
|
||||||
const prodUrl = Cypress.env("TEST_ENDPOINT") || "https://localhost:1234/hostedExplorer.html";
|
|
||||||
const timeout = 15000;
|
|
||||||
|
|
||||||
cy.visit(prodUrl);
|
|
||||||
cy.get('iframe[id="explorerMenu"]').should("be.visible");
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find("#connectExplorer")
|
|
||||||
.should("exist")
|
|
||||||
.find("div[class='connectExplorer']")
|
|
||||||
.should("exist")
|
|
||||||
.find("p[class='welcomeText']")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
cy.wrap($body.find("div > p.switchConnectTypeText"))
|
|
||||||
.should("exist")
|
|
||||||
.last()
|
|
||||||
.click({ force: true });
|
|
||||||
|
|
||||||
const secret = Cypress.env("CONNECTION_STRING");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find("input[class='inputToken']")
|
|
||||||
.should("exist")
|
|
||||||
.type(secret, {
|
|
||||||
force: true
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.wrap($body.find("input[value='Connect']"), { timeout })
|
|
||||||
.first()
|
|
||||||
.click({ force: true });
|
|
||||||
|
|
||||||
cy.wait(15000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
const { CosmosClient } = require("@azure/cosmos");
|
|
||||||
|
|
||||||
module.exports = new CosmosClient({
|
|
||||||
endpoint: "https://0.0.0.0:8081",
|
|
||||||
key: "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
|
|
||||||
});
|
|
||||||
1963
externals/adal.js
vendored
1963
externals/adal.js
vendored
File diff suppressed because it is too large
Load Diff
3687
package-lock.json
generated
3687
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -4,8 +4,12 @@
|
|||||||
"description": "Cosmos Explorer",
|
"description": "Cosmos Explorer",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@azure/arm-cosmosdb": "9.1.0",
|
||||||
"@azure/cosmos": "3.9.0",
|
"@azure/cosmos": "3.9.0",
|
||||||
"@azure/cosmos-language-service": "0.0.4",
|
"@azure/cosmos-language-service": "0.0.5",
|
||||||
|
"@azure/identity": "1.1.0",
|
||||||
|
"@azure/msal-browser": "2.8.0",
|
||||||
|
"@azure/msal-react": "1.0.0-alpha.1",
|
||||||
"@jupyterlab/services": "6.0.0-rc.2",
|
"@jupyterlab/services": "6.0.0-rc.2",
|
||||||
"@jupyterlab/terminal": "3.0.0-rc.2",
|
"@jupyterlab/terminal": "3.0.0-rc.2",
|
||||||
"@microsoft/applicationinsights-web": "2.5.9",
|
"@microsoft/applicationinsights-web": "2.5.9",
|
||||||
@@ -66,7 +70,7 @@
|
|||||||
"jquery-ui-dist": "1.12.1",
|
"jquery-ui-dist": "1.12.1",
|
||||||
"knockout": "3.5.1",
|
"knockout": "3.5.1",
|
||||||
"mkdirp": "1.0.4",
|
"mkdirp": "1.0.4",
|
||||||
"monaco-editor": "0.15.6",
|
"monaco-editor": "0.18.1",
|
||||||
"object.entries": "1.1.0",
|
"object.entries": "1.1.0",
|
||||||
"office-ui-fabric-react": "7.134.1",
|
"office-ui-fabric-react": "7.134.1",
|
||||||
"p-retry": "4.2.0",
|
"p-retry": "4.2.0",
|
||||||
@@ -115,7 +119,7 @@
|
|||||||
"@types/prop-types": "15.5.8",
|
"@types/prop-types": "15.5.8",
|
||||||
"@types/puppeteer": "3.0.1",
|
"@types/puppeteer": "3.0.1",
|
||||||
"@types/q": "1.5.1",
|
"@types/q": "1.5.1",
|
||||||
"@types/react": "16.9.49",
|
"@types/react": "16.9.56",
|
||||||
"@types/react-dom": "16.0.7",
|
"@types/react-dom": "16.0.7",
|
||||||
"@types/react-notification-system": "0.2.39",
|
"@types/react-notification-system": "0.2.39",
|
||||||
"@types/react-redux": "7.1.7",
|
"@types/react-redux": "7.1.7",
|
||||||
@@ -126,7 +130,6 @@
|
|||||||
"@types/webfontloader": "1.6.29",
|
"@types/webfontloader": "1.6.29",
|
||||||
"@typescript-eslint/eslint-plugin": "4.0.1",
|
"@typescript-eslint/eslint-plugin": "4.0.1",
|
||||||
"@typescript-eslint/parser": "4.0.1",
|
"@typescript-eslint/parser": "4.0.1",
|
||||||
"adal-angular": "1.0.15",
|
|
||||||
"axe-puppeteer": "1.1.0",
|
"axe-puppeteer": "1.1.0",
|
||||||
"babel-jest": "24.9.0",
|
"babel-jest": "24.9.0",
|
||||||
"babel-loader": "8.1.0",
|
"babel-loader": "8.1.0",
|
||||||
@@ -194,8 +197,8 @@
|
|||||||
"compile": "tsc",
|
"compile": "tsc",
|
||||||
"compile:contracts": "tsc -p ./tsconfig.contracts.json",
|
"compile:contracts": "tsc -p ./tsconfig.contracts.json",
|
||||||
"compile:strict": "tsc -p ./tsconfig.strict.json",
|
"compile:strict": "tsc -p ./tsconfig.strict.json",
|
||||||
"format": "prettier --write \"{src,cypress,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
"format": "prettier --write \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
||||||
"format:check": "prettier --check \"{src,cypress,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
"format:check": "prettier --check \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
||||||
"lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"",
|
"lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"",
|
||||||
"build:contracts": "npm run compile:contracts",
|
"build:contracts": "npm run compile:contracts",
|
||||||
"strictEligibleFiles": "node ./strict-migration-tools/index.js",
|
"strictEligibleFiles": "node ./strict-migration-tools/index.js",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
"offerThroughput": 400,
|
"offerThroughput": 400,
|
||||||
"databaseLevelThroughput": false,
|
"databaseLevelThroughput": false,
|
||||||
"collectionId": "Persons",
|
"collectionId": "Persons",
|
||||||
"rupmEnabled": false,
|
|
||||||
"partitionKey": { "kind": "Hash", "paths": ["/name"] },
|
"partitionKey": { "kind": "Hash", "paths": ["/name"] },
|
||||||
"data": [
|
"data": [
|
||||||
"g.addV('person').property(id, '1').property('name', 'Eva').property('age', 44)",
|
"g.addV('person').property(id, '1').property('name', 'Eva').property('age', 44)",
|
||||||
@@ -13,4 +12,4 @@
|
|||||||
"g.V('1').addE('knows').to(g.V('2')).outV().addE('knows').to(g.V('3'))",
|
"g.V('1').addE('knows').to(g.V('2')).outV().addE('knows').to(g.V('3'))",
|
||||||
"g.V('3').addE('knows').to(g.V('4'))"
|
"g.V('3').addE('knows').to(g.V('4'))"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,13 +108,11 @@ export class CapabilityNames {
|
|||||||
export class Features {
|
export class Features {
|
||||||
public static readonly cosmosdb = "cosmosdb";
|
public static readonly cosmosdb = "cosmosdb";
|
||||||
public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy";
|
public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy";
|
||||||
public static readonly enableRupm = "enablerupm";
|
|
||||||
public static readonly executeSproc = "dataexplorerexecutesproc";
|
public static readonly executeSproc = "dataexplorerexecutesproc";
|
||||||
public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
|
public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
|
||||||
public static readonly enableTtl = "enablettl";
|
public static readonly enableTtl = "enablettl";
|
||||||
public static readonly enableNotebooks = "enablenotebooks";
|
public static readonly enableNotebooks = "enablenotebooks";
|
||||||
public static readonly enableGalleryPublish = "enablegallerypublish";
|
public static readonly enableGalleryPublish = "enablegallerypublish";
|
||||||
public static readonly enableCodeOfConduct = "enablecodeofconduct";
|
|
||||||
public static readonly enableLinkInjection = "enablelinkinjection";
|
public static readonly enableLinkInjection = "enablelinkinjection";
|
||||||
public static readonly enableSpark = "enablespark";
|
public static readonly enableSpark = "enablespark";
|
||||||
public static readonly livyEndpoint = "livyendpoint";
|
public static readonly livyEndpoint = "livyendpoint";
|
||||||
@@ -125,7 +123,9 @@ export class Features {
|
|||||||
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
|
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
|
||||||
public static readonly ttl90Days = "ttl90days";
|
public static readonly ttl90Days = "ttl90days";
|
||||||
public static readonly enableRightPanelV2 = "enablerightpanelv2";
|
public static readonly enableRightPanelV2 = "enablerightpanelv2";
|
||||||
|
public static readonly enableSchema = "enableschema";
|
||||||
public static readonly enableSDKoperations = "enablesdkoperations";
|
public static readonly enableSDKoperations = "enablesdkoperations";
|
||||||
|
public static readonly showMinRUSurvey = "showminrusurvey";
|
||||||
}
|
}
|
||||||
|
|
||||||
// flight names returned from the portal are always lowercase
|
// flight names returned from the portal are always lowercase
|
||||||
@@ -179,11 +179,6 @@ export class CassandraBackend {
|
|||||||
public static readonly guestSchemaApi: string = "api/guest/cassandra/schema";
|
public static readonly guestSchemaApi: string = "api/guest/cassandra/schema";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RUPMStates {
|
|
||||||
public static on: string = "on";
|
|
||||||
public static off: string = "off";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Queries {
|
export class Queries {
|
||||||
public static CustomPageOption: string = "custom";
|
public static CustomPageOption: string = "custom";
|
||||||
public static UnlimitedPageOption: string = "unlimited";
|
public static UnlimitedPageOption: string = "unlimited";
|
||||||
|
|||||||
@@ -1,182 +0,0 @@
|
|||||||
import {
|
|
||||||
ConflictDefinition,
|
|
||||||
FeedOptions,
|
|
||||||
ItemDefinition,
|
|
||||||
OfferDefinition,
|
|
||||||
QueryIterator,
|
|
||||||
Resource
|
|
||||||
} from "@azure/cosmos";
|
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
|
||||||
import Q from "q";
|
|
||||||
import { configContext, Platform } from "../ConfigContext";
|
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
|
||||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
|
||||||
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 { OfferUtils } from "../Utils/OfferUtils";
|
|
||||||
import * as Constants from "./Constants";
|
|
||||||
import { client } from "./CosmosClient";
|
|
||||||
import * as HeadersUtility from "./HeadersUtility";
|
|
||||||
import { sendCachedDataMessage } from "./MessageHandler";
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
10
src/Common/DocumentUtility.ts
Normal file
10
src/Common/DocumentUtility.ts
Normal 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";
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ARMError } from "../Utils/arm/request";
|
import { ARMError } from "../Utils/arm/request";
|
||||||
import { HttpStatusCodes } from "./Constants";
|
import { HttpStatusCodes } from "./Constants";
|
||||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||||
import { SubscriptionType } from "../Contracts/ViewModels";
|
import { SubscriptionType } from "../Contracts/SubscriptionType";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import { logError } from "./Logger";
|
import { logError } from "./Logger";
|
||||||
import { sendMessage } from "./MessageHandler";
|
import { sendMessage } from "./MessageHandler";
|
||||||
|
|||||||
64
src/Common/OfferUtility.test.ts
Normal file
64
src/Common/OfferUtility.test.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import * as OfferUtility from "./OfferUtility";
|
||||||
|
import { SDKOfferDefinition, Offer } from "../Contracts/DataModels";
|
||||||
|
import { OfferResponse } from "@azure/cosmos";
|
||||||
|
|
||||||
|
describe("parseSDKOfferResponse", () => {
|
||||||
|
it("manual throughput", () => {
|
||||||
|
const mockOfferDefinition = {
|
||||||
|
content: {
|
||||||
|
offerThroughput: 500,
|
||||||
|
collectionThroughputInfo: {
|
||||||
|
minimumRUForCollection: 400,
|
||||||
|
numPhysicalPartitions: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
id: "test"
|
||||||
|
} as SDKOfferDefinition;
|
||||||
|
|
||||||
|
const mockResponse = {
|
||||||
|
resource: mockOfferDefinition
|
||||||
|
} as OfferResponse;
|
||||||
|
|
||||||
|
const expectedResult: Offer = {
|
||||||
|
manualThroughput: 500,
|
||||||
|
autoscaleMaxThroughput: undefined,
|
||||||
|
minimumThroughput: 400,
|
||||||
|
id: "test",
|
||||||
|
offerDefinition: mockOfferDefinition,
|
||||||
|
offerReplacePending: false
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("autoscale throughput", () => {
|
||||||
|
const mockOfferDefinition = {
|
||||||
|
content: {
|
||||||
|
offerThroughput: 400,
|
||||||
|
collectionThroughputInfo: {
|
||||||
|
minimumRUForCollection: 400,
|
||||||
|
numPhysicalPartitions: 1
|
||||||
|
},
|
||||||
|
offerAutopilotSettings: {
|
||||||
|
maxThroughput: 5000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
id: "test"
|
||||||
|
} as SDKOfferDefinition;
|
||||||
|
|
||||||
|
const mockResponse = {
|
||||||
|
resource: mockOfferDefinition
|
||||||
|
} as OfferResponse;
|
||||||
|
|
||||||
|
const expectedResult: Offer = {
|
||||||
|
manualThroughput: undefined,
|
||||||
|
autoscaleMaxThroughput: 5000,
|
||||||
|
minimumThroughput: 400,
|
||||||
|
id: "test",
|
||||||
|
offerDefinition: mockOfferDefinition,
|
||||||
|
offerReplacePending: false
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
34
src/Common/OfferUtility.ts
Normal file
34
src/Common/OfferUtility.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Offer, SDKOfferDefinition } from "../Contracts/DataModels";
|
||||||
|
import { OfferResponse } from "@azure/cosmos";
|
||||||
|
import { HttpHeaders } from "./Constants";
|
||||||
|
|
||||||
|
export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => {
|
||||||
|
const offerDefinition: SDKOfferDefinition = offerResponse?.resource;
|
||||||
|
const offerContent = offerDefinition.content;
|
||||||
|
if (!offerContent) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const minimumThroughput = offerContent.collectionThroughputInfo?.minimumRUForCollection;
|
||||||
|
const autopilotSettings = offerContent.offerAutopilotSettings;
|
||||||
|
|
||||||
|
if (autopilotSettings) {
|
||||||
|
return {
|
||||||
|
id: offerDefinition.id,
|
||||||
|
autoscaleMaxThroughput: autopilotSettings.maxThroughput,
|
||||||
|
manualThroughput: undefined,
|
||||||
|
minimumThroughput,
|
||||||
|
offerDefinition,
|
||||||
|
offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: offerDefinition.id,
|
||||||
|
autoscaleMaxThroughput: undefined,
|
||||||
|
manualThroughput: offerContent.offerThroughput,
|
||||||
|
minimumThroughput,
|
||||||
|
offerDefinition,
|
||||||
|
offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true"
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -16,7 +16,7 @@ const notificationsPath = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const fetchPortalNotifications = async (): Promise<DataModels.Notification[]> => {
|
export const fetchPortalNotifications = async (): Promise<DataModels.Notification[]> => {
|
||||||
if (configContext.platform === Platform.Emulator) {
|
if (configContext.platform === Platform.Emulator || configContext.platform === Platform.Hosted) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
25
src/Common/dataAccess/createDocument.ts
Normal file
25
src/Common/dataAccess/createDocument.ts
Normal 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();
|
||||||
|
}
|
||||||
|
};
|
||||||
36
src/Common/dataAccess/deleteConflict.ts
Normal file
36
src/Common/dataAccess/deleteConflict.ts
Normal 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];
|
||||||
|
};
|
||||||
25
src/Common/dataAccess/deleteDocument.ts
Normal file
25
src/Common/dataAccess/deleteDocument.ts
Normal 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();
|
||||||
|
}
|
||||||
|
};
|
||||||
48
src/Common/dataAccess/executeStoredProcedure.ts
Normal file
48
src/Common/dataAccess/executeStoredProcedure.ts
Normal 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();
|
||||||
|
}
|
||||||
|
};
|
||||||
92
src/Common/dataAccess/getCollectionDataUsageSize.ts
Normal file
92
src/Common/dataAccess/getCollectionDataUsageSize.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { AuthType } from "../../AuthType";
|
||||||
|
import { armRequest } from "../../Utils/arm/request";
|
||||||
|
import { configContext } from "../../ConfigContext";
|
||||||
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
|
interface TimeSeriesData {
|
||||||
|
data: {
|
||||||
|
timeStamp: string;
|
||||||
|
total: number;
|
||||||
|
}[];
|
||||||
|
metadatavalues: {
|
||||||
|
name: {
|
||||||
|
localizedValue: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MetricsData {
|
||||||
|
displayDescription: string;
|
||||||
|
errorCode: string;
|
||||||
|
id: string;
|
||||||
|
name: {
|
||||||
|
value: string;
|
||||||
|
localizedValue: string;
|
||||||
|
};
|
||||||
|
timeseries: TimeSeriesData[];
|
||||||
|
type: string;
|
||||||
|
unit: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MetricsResponse {
|
||||||
|
cost: number;
|
||||||
|
interval: string;
|
||||||
|
namespace: string;
|
||||||
|
resourceregion: string;
|
||||||
|
timespan: string;
|
||||||
|
value: MetricsData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => {
|
||||||
|
if (window.authType !== AuthType.AAD) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscriptionId = userContext.subscriptionId;
|
||||||
|
const resourceGroup = userContext.resourceGroup;
|
||||||
|
const accountName = userContext.databaseAccount.name;
|
||||||
|
const filter = `DatabaseName eq '${databaseName}' and CollectionName eq '${containerName}'`;
|
||||||
|
const metricNames = "DataUsage,IndexUsage";
|
||||||
|
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/providers/microsoft.insights/metrics`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const metricsResponse: MetricsResponse = await armRequest({
|
||||||
|
host: configContext.ARM_ENDPOINT,
|
||||||
|
path,
|
||||||
|
method: "GET",
|
||||||
|
apiVersion: "2018-01-01",
|
||||||
|
queryParams: {
|
||||||
|
filter,
|
||||||
|
metricNames
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (metricsResponse?.value?.length !== 2) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataUsageData: MetricsData = metricsResponse.value[0];
|
||||||
|
const indexUsagedata: MetricsData = metricsResponse.value[1];
|
||||||
|
const dataUsageSizeInKb: number = getUsageSizeInKb(dataUsageData);
|
||||||
|
const indexUsageSizeInKb: number = getUsageSizeInKb(indexUsagedata);
|
||||||
|
|
||||||
|
return dataUsageSizeInKb + indexUsageSizeInKb;
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, "getCollectionUsageSize");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUsageSizeInKb = (metricsData: MetricsData): number => {
|
||||||
|
if (metricsData?.errorCode !== "Success") {
|
||||||
|
throw Error(`Get collection usage size failed: ${metricsData.errorCode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeSeriesData: TimeSeriesData = metricsData?.timeseries?.[0];
|
||||||
|
const usageSizeInBytes: number = timeSeriesData?.data?.[0]?.total;
|
||||||
|
|
||||||
|
return usageSizeInBytes ? usageSizeInBytes / 1024 : 0;
|
||||||
|
};
|
||||||
14
src/Common/dataAccess/queryConflicts.ts
Normal file
14
src/Common/dataAccess/queryConflicts.ts
Normal 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);
|
||||||
|
};
|
||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
34
src/Common/dataAccess/queryDocuments.ts
Normal file
34
src/Common/dataAccess/queryDocuments.ts
Normal 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;
|
||||||
|
};
|
||||||
26
src/Common/dataAccess/queryDocumentsPage.ts
Normal file
26
src/Common/dataAccess/queryDocumentsPage.ts
Normal 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();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
import * as DataModels from "../../Contracts/DataModels";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
import { HttpHeaders } from "../Constants";
|
import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels";
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
|
||||||
import { client } from "../CosmosClient";
|
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
import { getSqlContainerThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
import { getSqlContainerThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||||
import { getMongoDBCollectionThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
import { getMongoDBCollectionThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||||
@@ -11,50 +8,22 @@ import { getCassandraTableThroughput } from "../../Utils/arm/generatedClients/20
|
|||||||
import { getGremlinGraphThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
import { getGremlinGraphThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||||
import { getTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
|
import { getTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { readOffers } from "./readOffers";
|
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export const readCollectionOffer = async (
|
export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise<Offer> => {
|
||||||
params: DataModels.ReadCollectionOfferParams
|
|
||||||
): Promise<DataModels.OfferWithHeaders> => {
|
|
||||||
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
|
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
|
||||||
let offerId = params.offerId;
|
|
||||||
if (!offerId) {
|
|
||||||
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
|
||||||
try {
|
|
||||||
offerId = await getCollectionOfferIdWithARM(params.databaseId, params.collectionId);
|
|
||||||
} catch (error) {
|
|
||||||
clearMessage();
|
|
||||||
if (error.code !== "NotFound") {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
offerId = await getCollectionOfferIdWithSDK(params.collectionResourceId);
|
|
||||||
if (!offerId) {
|
|
||||||
clearMessage();
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: RequestOptions = {
|
|
||||||
initialHeaders: {
|
|
||||||
[HttpHeaders.populateCollectionThroughputInfo]: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await client()
|
if (
|
||||||
.offer(offerId)
|
window.authType === AuthType.AAD &&
|
||||||
.read(options);
|
!userContext.useSDKOperations &&
|
||||||
return (
|
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||||
response && {
|
) {
|
||||||
...response.resource,
|
return await readCollectionOfferWithARM(params.databaseId, params.collectionId);
|
||||||
headers: response.headers
|
}
|
||||||
}
|
|
||||||
);
|
return await readOfferWithSDK(params.offerId, params.collectionResourceId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "ReadCollectionOffer", `Error while querying offer for collection ${params.collectionId}`);
|
handleError(error, "ReadCollectionOffer", `Error while querying offer for collection ${params.collectionId}`);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -63,61 +32,92 @@ export const readCollectionOffer = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCollectionOfferIdWithARM = async (databaseId: string, collectionId: string): Promise<string> => {
|
const readCollectionOfferWithARM = async (databaseId: string, collectionId: string): Promise<Offer> => {
|
||||||
let rpResponse;
|
|
||||||
const subscriptionId = userContext.subscriptionId;
|
const subscriptionId = userContext.subscriptionId;
|
||||||
const resourceGroup = userContext.resourceGroup;
|
const resourceGroup = userContext.resourceGroup;
|
||||||
const accountName = userContext.databaseAccount.name;
|
const accountName = userContext.databaseAccount.name;
|
||||||
const defaultExperience = userContext.defaultExperience;
|
const defaultExperience = userContext.defaultExperience;
|
||||||
switch (defaultExperience) {
|
|
||||||
case DefaultAccountExperienceType.DocumentDB:
|
let rpResponse;
|
||||||
rpResponse = await getSqlContainerThroughput(
|
try {
|
||||||
subscriptionId,
|
switch (defaultExperience) {
|
||||||
resourceGroup,
|
case DefaultAccountExperienceType.DocumentDB:
|
||||||
accountName,
|
rpResponse = await getSqlContainerThroughput(
|
||||||
databaseId,
|
subscriptionId,
|
||||||
collectionId
|
resourceGroup,
|
||||||
);
|
accountName,
|
||||||
break;
|
databaseId,
|
||||||
case DefaultAccountExperienceType.MongoDB:
|
collectionId
|
||||||
rpResponse = await getMongoDBCollectionThroughput(
|
);
|
||||||
subscriptionId,
|
break;
|
||||||
resourceGroup,
|
case DefaultAccountExperienceType.MongoDB:
|
||||||
accountName,
|
rpResponse = await getMongoDBCollectionThroughput(
|
||||||
databaseId,
|
subscriptionId,
|
||||||
collectionId
|
resourceGroup,
|
||||||
);
|
accountName,
|
||||||
break;
|
databaseId,
|
||||||
case DefaultAccountExperienceType.Cassandra:
|
collectionId
|
||||||
rpResponse = await getCassandraTableThroughput(
|
);
|
||||||
subscriptionId,
|
break;
|
||||||
resourceGroup,
|
case DefaultAccountExperienceType.Cassandra:
|
||||||
accountName,
|
rpResponse = await getCassandraTableThroughput(
|
||||||
databaseId,
|
subscriptionId,
|
||||||
collectionId
|
resourceGroup,
|
||||||
);
|
accountName,
|
||||||
break;
|
databaseId,
|
||||||
case DefaultAccountExperienceType.Graph:
|
collectionId
|
||||||
rpResponse = await getGremlinGraphThroughput(
|
);
|
||||||
subscriptionId,
|
break;
|
||||||
resourceGroup,
|
case DefaultAccountExperienceType.Graph:
|
||||||
accountName,
|
rpResponse = await getGremlinGraphThroughput(
|
||||||
databaseId,
|
subscriptionId,
|
||||||
collectionId
|
resourceGroup,
|
||||||
);
|
accountName,
|
||||||
break;
|
databaseId,
|
||||||
case DefaultAccountExperienceType.Table:
|
collectionId
|
||||||
rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
case DefaultAccountExperienceType.Table:
|
||||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== "NotFound") {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return rpResponse?.name;
|
const resource = rpResponse?.properties?.resource;
|
||||||
};
|
if (resource) {
|
||||||
|
const offerId: string = rpResponse.name;
|
||||||
|
const minimumThroughput: number =
|
||||||
|
typeof resource.minimumThroughput === "string"
|
||||||
|
? parseInt(resource.minimumThroughput)
|
||||||
|
: resource.minimumThroughput;
|
||||||
|
const autoscaleSettings = resource.autoscaleSettings;
|
||||||
|
|
||||||
const getCollectionOfferIdWithSDK = async (collectionResourceId: string): Promise<string> => {
|
if (autoscaleSettings) {
|
||||||
const offers = await readOffers();
|
return {
|
||||||
const offer = offers.find(offer => offer.resource === collectionResourceId);
|
id: offerId,
|
||||||
return offer?.id;
|
autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
|
||||||
|
manualThroughput: undefined,
|
||||||
|
minimumThroughput,
|
||||||
|
offerReplacePending: resource.offerReplacePending === "true"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: offerId,
|
||||||
|
autoscaleMaxThroughput: undefined,
|
||||||
|
manualThroughput: resource.throughput,
|
||||||
|
minimumThroughput,
|
||||||
|
offerReplacePending: resource.offerReplacePending === "true"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import * as DataModels from "../../Contracts/DataModels";
|
|
||||||
import * as HeadersUtility from "../HeadersUtility";
|
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
|
||||||
import { ContainerDefinition, Resource } from "@azure/cosmos";
|
|
||||||
import { HttpHeaders } from "../Constants";
|
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
|
||||||
import { client } from "../CosmosClient";
|
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
|
||||||
|
|
||||||
interface ResourceWithStatistics {
|
|
||||||
statistics: DataModels.Statistic[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const readCollectionQuotaInfo = async (
|
|
||||||
collection: ViewModels.Collection
|
|
||||||
): Promise<DataModels.CollectionQuotaInfo> => {
|
|
||||||
const clearMessage = logConsoleProgress(`Querying containers for database ${collection.id}`);
|
|
||||||
const options: RequestOptions = {};
|
|
||||||
options.populateQuotaInfo = true;
|
|
||||||
options.initialHeaders = options.initialHeaders || {};
|
|
||||||
options.initialHeaders[HttpHeaders.populatePartitionStatistics] = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await client()
|
|
||||||
.database(collection.databaseId)
|
|
||||||
.container(collection.id())
|
|
||||||
.read(options);
|
|
||||||
const quota: DataModels.CollectionQuotaInfo = HeadersUtility.getQuota(response.headers);
|
|
||||||
const resource = response.resource as ContainerDefinition & Resource & ResourceWithStatistics;
|
|
||||||
quota["usageSizeInKB"] = resource.statistics.reduce(
|
|
||||||
(previousValue: number, currentValue: DataModels.Statistic) => previousValue + currentValue.sizeInKB,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
quota["numPartitions"] = resource.statistics.length;
|
|
||||||
quota["uniqueKeyPolicy"] = collection.uniqueKeyPolicy; // TODO: Remove after refactoring (#119617)
|
|
||||||
|
|
||||||
return quota;
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, "ReadCollectionQuotaInfo", `Error while querying quota info for container ${collection.id}`);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
clearMessage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,51 +1,28 @@
|
|||||||
import * as DataModels from "../../Contracts/DataModels";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
import { HttpHeaders } from "../Constants";
|
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
|
||||||
import { client } from "../CosmosClient";
|
|
||||||
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||||
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||||
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
||||||
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { readOffers } from "./readOffers";
|
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export const readDatabaseOffer = async (
|
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
|
||||||
params: DataModels.ReadDatabaseOfferParams
|
|
||||||
): Promise<DataModels.OfferWithHeaders> => {
|
|
||||||
const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`);
|
const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`);
|
||||||
let offerId = params.offerId;
|
|
||||||
if (!offerId) {
|
|
||||||
offerId = await (window.authType === AuthType.AAD &&
|
|
||||||
!userContext.useSDKOperations &&
|
|
||||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
|
||||||
? getDatabaseOfferIdWithARM(params.databaseId)
|
|
||||||
: getDatabaseOfferIdWithSDK(params.databaseResourceId));
|
|
||||||
if (!offerId) {
|
|
||||||
clearMessage();
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: RequestOptions = {
|
|
||||||
initialHeaders: {
|
|
||||||
[HttpHeaders.populateCollectionThroughputInfo]: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await client()
|
if (
|
||||||
.offer(offerId)
|
window.authType === AuthType.AAD &&
|
||||||
.read(options);
|
!userContext.useSDKOperations &&
|
||||||
return (
|
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||||
response && {
|
) {
|
||||||
...response.resource,
|
return await readDatabaseOfferWithARM(params.databaseId);
|
||||||
headers: response.headers
|
}
|
||||||
}
|
|
||||||
);
|
return await readOfferWithSDK(params.offerId, params.databaseResourceId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "ReadDatabaseOffer", `Error while querying offer for database ${params.databaseId}`);
|
handleError(error, "ReadDatabaseOffer", `Error while querying offer for database ${params.databaseId}`);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -54,13 +31,13 @@ export const readDatabaseOffer = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> => {
|
const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
|
||||||
let rpResponse;
|
|
||||||
const subscriptionId = userContext.subscriptionId;
|
const subscriptionId = userContext.subscriptionId;
|
||||||
const resourceGroup = userContext.resourceGroup;
|
const resourceGroup = userContext.resourceGroup;
|
||||||
const accountName = userContext.databaseAccount.name;
|
const accountName = userContext.databaseAccount.name;
|
||||||
const defaultExperience = userContext.defaultExperience;
|
const defaultExperience = userContext.defaultExperience;
|
||||||
|
|
||||||
|
let rpResponse;
|
||||||
try {
|
try {
|
||||||
switch (defaultExperience) {
|
switch (defaultExperience) {
|
||||||
case DefaultAccountExperienceType.DocumentDB:
|
case DefaultAccountExperienceType.DocumentDB:
|
||||||
@@ -78,18 +55,41 @@ const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> =>
|
|||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return rpResponse?.name;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code !== "NotFound") {
|
if (error.code !== "NotFound") {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const getDatabaseOfferIdWithSDK = async (databaseResourceId: string): Promise<string> => {
|
const resource = rpResponse?.properties?.resource;
|
||||||
const offers = await readOffers();
|
if (resource) {
|
||||||
const offer = offers.find(offer => offer.resource === databaseResourceId);
|
const offerId: string = rpResponse.name;
|
||||||
return offer?.id;
|
const minimumThroughput: number =
|
||||||
|
typeof resource.minimumThroughput === "string"
|
||||||
|
? parseInt(resource.minimumThroughput)
|
||||||
|
: resource.minimumThroughput;
|
||||||
|
const autoscaleSettings = resource.autoscaleSettings;
|
||||||
|
|
||||||
|
if (autoscaleSettings) {
|
||||||
|
return {
|
||||||
|
id: offerId,
|
||||||
|
autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
|
||||||
|
manualThroughput: undefined,
|
||||||
|
minimumThroughput,
|
||||||
|
offerReplacePending: resource.offerReplacePending === "true"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: offerId,
|
||||||
|
autoscaleMaxThroughput: undefined,
|
||||||
|
manualThroughput: resource.throughput,
|
||||||
|
minimumThroughput,
|
||||||
|
offerReplacePending: resource.offerReplacePending === "true"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|||||||
27
src/Common/dataAccess/readDocument.ts
Normal file
27
src/Common/dataAccess/readDocument.ts
Normal 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();
|
||||||
|
}
|
||||||
|
};
|
||||||
29
src/Common/dataAccess/readOfferWithSDK.ts
Normal file
29
src/Common/dataAccess/readOfferWithSDK.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { HttpHeaders } from "../Constants";
|
||||||
|
import { Offer } from "../../Contracts/DataModels";
|
||||||
|
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { parseSDKOfferResponse } from "../OfferUtility";
|
||||||
|
import { readOffers } from "./readOffers";
|
||||||
|
|
||||||
|
export const readOfferWithSDK = async (offerId: string, resourceId: string): Promise<Offer> => {
|
||||||
|
if (!offerId) {
|
||||||
|
const offers = await readOffers();
|
||||||
|
const offer = offers.find(offer => offer.resource === resourceId);
|
||||||
|
|
||||||
|
if (!offer) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
offerId = offer.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: RequestOptions = {
|
||||||
|
initialHeaders: {
|
||||||
|
[HttpHeaders.populateCollectionThroughputInfo]: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const response = await client()
|
||||||
|
.offer(offerId)
|
||||||
|
.read(options);
|
||||||
|
|
||||||
|
return parseSDKOfferResponse(response);
|
||||||
|
};
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Offer } from "../../Contracts/DataModels";
|
import { SDKOfferDefinition } from "../../Contracts/DataModels";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { handleError, getErrorMessage } from "../ErrorHandlingUtils";
|
import { handleError, getErrorMessage } from "../ErrorHandlingUtils";
|
||||||
|
|
||||||
export const readOffers = async (): Promise<Offer[]> => {
|
export const readOffers = async (): Promise<SDKOfferDefinition[]> => {
|
||||||
const clearMessage = logConsoleProgress(`Querying offers`);
|
const clearMessage = logConsoleProgress(`Querying offers`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
32
src/Common/dataAccess/updateDocument.ts
Normal file
32
src/Common/dataAccess/updateDocument.ts
Normal 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();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
import { HttpHeaders } from "../Constants";
|
import { HttpHeaders } from "../Constants";
|
||||||
import { Offer, UpdateOfferParams } from "../../Contracts/DataModels";
|
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||||
import { OfferDefinition } from "@azure/cosmos";
|
import { OfferDefinition } from "@azure/cosmos";
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||||
import { ThroughputSettingsUpdateParameters } from "../../Utils/arm/generatedClients/2020-04-01/types";
|
import { ThroughputSettingsUpdateParameters } from "../../Utils/arm/generatedClients/2020-04-01/types";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import { parseSDKOfferResponse } from "../OfferUtility";
|
||||||
import { readCollectionOffer } from "./readCollectionOffer";
|
import { readCollectionOffer } from "./readCollectionOffer";
|
||||||
import { readDatabaseOffer } from "./readDatabaseOffer";
|
import { readDatabaseOffer } from "./readDatabaseOffer";
|
||||||
import {
|
import {
|
||||||
@@ -373,21 +374,21 @@ const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpd
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> => {
|
const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> => {
|
||||||
const currentOffer = params.currentOffer;
|
const sdkOfferDefinition = params.currentOffer.offerDefinition;
|
||||||
const newOffer: Offer = {
|
const newOffer: SDKOfferDefinition = {
|
||||||
content: {
|
content: {
|
||||||
offerThroughput: undefined,
|
offerThroughput: undefined,
|
||||||
offerIsRUPerMinuteThroughputEnabled: false
|
offerIsRUPerMinuteThroughputEnabled: false
|
||||||
},
|
},
|
||||||
_etag: undefined,
|
_etag: undefined,
|
||||||
_ts: undefined,
|
_ts: undefined,
|
||||||
_rid: currentOffer._rid,
|
_rid: sdkOfferDefinition._rid,
|
||||||
_self: currentOffer._self,
|
_self: sdkOfferDefinition._self,
|
||||||
id: currentOffer.id,
|
id: sdkOfferDefinition.id,
|
||||||
offerResourceId: currentOffer.offerResourceId,
|
offerResourceId: sdkOfferDefinition.offerResourceId,
|
||||||
offerVersion: currentOffer.offerVersion,
|
offerVersion: sdkOfferDefinition.offerVersion,
|
||||||
offerType: currentOffer.offerType,
|
offerType: sdkOfferDefinition.offerType,
|
||||||
resource: currentOffer.resource
|
resource: sdkOfferDefinition.resource
|
||||||
};
|
};
|
||||||
|
|
||||||
if (params.autopilotThroughput) {
|
if (params.autopilotThroughput) {
|
||||||
@@ -415,5 +416,6 @@ const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> =>
|
|||||||
.offer(params.currentOffer.id)
|
.offer(params.currentOffer.id)
|
||||||
// TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660)
|
// TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660)
|
||||||
.replace((newOffer as unknown) as OfferDefinition, options);
|
.replace((newOffer as unknown) as OfferDefinition, options);
|
||||||
return sdkResponse?.resource;
|
|
||||||
|
return parseSDKOfferResponse(sdkResponse);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { updateOfferThroughputBeyondLimit } from "./updateOfferThroughputBeyondLimit";
|
|
||||||
|
|
||||||
describe("updateOfferThroughputBeyondLimit", () => {
|
|
||||||
it("should call fetch", async () => {
|
|
||||||
window.fetch = jest.fn(() => {
|
|
||||||
return {
|
|
||||||
ok: true
|
|
||||||
};
|
|
||||||
});
|
|
||||||
window.dataExplorer = {
|
|
||||||
logConsoleData: jest.fn(),
|
|
||||||
deleteInProgressConsoleDataWithId: jest.fn()
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
} as any;
|
|
||||||
await updateOfferThroughputBeyondLimit({
|
|
||||||
subscriptionId: "foo",
|
|
||||||
resourceGroup: "foo",
|
|
||||||
databaseAccountName: "foo",
|
|
||||||
databaseName: "foo",
|
|
||||||
throughput: 1000000000,
|
|
||||||
offerIsRUPerMinuteThroughputEnabled: false
|
|
||||||
});
|
|
||||||
expect(window.fetch).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import { Platform, configContext } from "../../ConfigContext";
|
|
||||||
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
|
||||||
import { AutoPilotOfferSettings } from "../../Contracts/DataModels";
|
|
||||||
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
|
||||||
import { HttpHeaders } from "../Constants";
|
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
|
||||||
|
|
||||||
interface UpdateOfferThroughputRequest {
|
|
||||||
subscriptionId: string;
|
|
||||||
resourceGroup: string;
|
|
||||||
databaseAccountName: string;
|
|
||||||
databaseName: string;
|
|
||||||
collectionName?: string;
|
|
||||||
throughput: number;
|
|
||||||
offerIsRUPerMinuteThroughputEnabled: boolean;
|
|
||||||
offerAutopilotSettings?: AutoPilotOfferSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateOfferThroughputBeyondLimit(request: UpdateOfferThroughputRequest): Promise<void> {
|
|
||||||
if (configContext.platform !== Platform.Portal) {
|
|
||||||
throw new Error("Updating throughput beyond specified limit is not supported on this platform");
|
|
||||||
}
|
|
||||||
|
|
||||||
const resourceDescriptionInfo = request.collectionName
|
|
||||||
? `database ${request.databaseName} and container ${request.collectionName}`
|
|
||||||
: `database ${request.databaseName}`;
|
|
||||||
|
|
||||||
const clearMessage = logConsoleProgress(
|
|
||||||
`Requesting increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const url = `${configContext.BACKEND_ENDPOINT}/api/offerthroughputrequest/updatebeyondspecifiedlimit`;
|
|
||||||
const authorizationHeader = getAuthorizationHeader();
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(request),
|
|
||||||
headers: { [authorizationHeader.header]: authorizationHeader.token, [HttpHeaders.contentType]: "application/json" }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
logConsoleInfo(
|
|
||||||
`Successfully requested an increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
|
|
||||||
);
|
|
||||||
clearMessage();
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const error = await response.json();
|
|
||||||
handleError(
|
|
||||||
error,
|
|
||||||
"updateOfferThroughputBeyondLimit",
|
|
||||||
`Failed to request an increase in throughput for ${request.throughput}`
|
|
||||||
);
|
|
||||||
clearMessage();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
@@ -88,6 +88,38 @@ export interface Resource {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IType {
|
||||||
|
name: string;
|
||||||
|
code: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDataField {
|
||||||
|
dataType: IType;
|
||||||
|
hasNulls: boolean;
|
||||||
|
isArray: boolean;
|
||||||
|
schemaType: IType;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
maxRepetitionLevel: number;
|
||||||
|
maxDefinitionLevel: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISchema {
|
||||||
|
id: string;
|
||||||
|
accountName: string;
|
||||||
|
resource: string;
|
||||||
|
fields: IDataField[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISchemaRequest {
|
||||||
|
id: string;
|
||||||
|
subscriptionId: string;
|
||||||
|
resourceGroup: string;
|
||||||
|
accountName: string;
|
||||||
|
resource: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Collection extends Resource {
|
export interface Collection extends Resource {
|
||||||
defaultTtl?: number;
|
defaultTtl?: number;
|
||||||
indexingPolicy?: IndexingPolicy;
|
indexingPolicy?: IndexingPolicy;
|
||||||
@@ -98,6 +130,8 @@ export interface Collection extends Resource {
|
|||||||
changeFeedPolicy?: ChangeFeedPolicy;
|
changeFeedPolicy?: ChangeFeedPolicy;
|
||||||
analyticalStorageTtl?: number;
|
analyticalStorageTtl?: number;
|
||||||
geospatialConfig?: GeospatialConfig;
|
geospatialConfig?: GeospatialConfig;
|
||||||
|
schema?: ISchema;
|
||||||
|
requestSchema?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Database extends Resource {
|
export interface Database extends Resource {
|
||||||
@@ -174,12 +208,21 @@ export interface QueryMetrics {
|
|||||||
vmExecutionTime: any;
|
vmExecutionTime: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Offer extends Resource {
|
export interface Offer {
|
||||||
|
id: string;
|
||||||
|
autoscaleMaxThroughput: number;
|
||||||
|
manualThroughput: number;
|
||||||
|
minimumThroughput: number;
|
||||||
|
offerDefinition?: SDKOfferDefinition;
|
||||||
|
offerReplacePending: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SDKOfferDefinition extends Resource {
|
||||||
offerVersion?: string;
|
offerVersion?: string;
|
||||||
offerType?: string;
|
offerType?: string;
|
||||||
content?: {
|
content?: {
|
||||||
offerThroughput: number;
|
offerThroughput: number;
|
||||||
offerIsRUPerMinuteThroughputEnabled: boolean;
|
offerIsRUPerMinuteThroughputEnabled?: boolean;
|
||||||
collectionThroughputInfo?: OfferThroughputInfo;
|
collectionThroughputInfo?: OfferThroughputInfo;
|
||||||
offerAutopilotSettings?: AutoPilotOfferSettings;
|
offerAutopilotSettings?: AutoPilotOfferSettings;
|
||||||
};
|
};
|
||||||
@@ -187,22 +230,6 @@ export interface Offer extends Resource {
|
|||||||
offerResourceId?: string;
|
offerResourceId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OfferWithHeaders extends Offer {
|
|
||||||
headers: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CollectionQuotaInfo {
|
|
||||||
storedProcedures: number;
|
|
||||||
triggers: number;
|
|
||||||
functions: number;
|
|
||||||
documentsSize: number;
|
|
||||||
collectionSize: number;
|
|
||||||
documentsCount: number;
|
|
||||||
usageSizeInKB: number;
|
|
||||||
numPartitions: number;
|
|
||||||
uniqueKeyPolicy?: UniqueKeyPolicy; // TODO: This should ideally not be a part of the collection quota. Remove after refactoring. (#119617)
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OfferThroughputInfo {
|
export interface OfferThroughputInfo {
|
||||||
minimumRUForCollection: number;
|
minimumRUForCollection: number;
|
||||||
numPhysicalPartitions: number;
|
numPhysicalPartitions: number;
|
||||||
@@ -221,7 +248,6 @@ export interface CreateDatabaseAndCollectionRequest {
|
|||||||
collectionId: string;
|
collectionId: string;
|
||||||
offerThroughput: number;
|
offerThroughput: number;
|
||||||
databaseLevelThroughput: boolean;
|
databaseLevelThroughput: boolean;
|
||||||
rupmEnabled?: boolean;
|
|
||||||
partitionKey?: PartitionKey;
|
partitionKey?: PartitionKey;
|
||||||
indexingPolicy?: IndexingPolicy;
|
indexingPolicy?: IndexingPolicy;
|
||||||
uniqueKeyPolicy?: UniqueKeyPolicy;
|
uniqueKeyPolicy?: UniqueKeyPolicy;
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export enum MessageTypes {
|
|||||||
GetArcadiaToken,
|
GetArcadiaToken,
|
||||||
CreateWorkspace,
|
CreateWorkspace,
|
||||||
CreateSparkPool,
|
CreateSparkPool,
|
||||||
RefreshDatabaseAccount
|
RefreshDatabaseAccount,
|
||||||
|
InitTestExplorer
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Versions, ActionContracts, Diagnostics };
|
export { Versions, ActionContracts, Diagnostics };
|
||||||
|
|||||||
7
src/Contracts/SubscriptionType.ts
Normal file
7
src/Contracts/SubscriptionType.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export enum SubscriptionType {
|
||||||
|
Benefits,
|
||||||
|
EA,
|
||||||
|
Free,
|
||||||
|
Internal,
|
||||||
|
PAYG
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import Trigger from "../Explorer/Tree/Trigger";
|
|||||||
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
|
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
|
||||||
import { UploadDetails } from "../workers/upload/definitions";
|
import { UploadDetails } from "../workers/upload/definitions";
|
||||||
import * as DataModels from "./DataModels";
|
import * as DataModels from "./DataModels";
|
||||||
|
import { SubscriptionType } from "./SubscriptionType";
|
||||||
|
|
||||||
export interface TokenProvider {
|
export interface TokenProvider {
|
||||||
getAuthHeader(): Promise<Headers>;
|
getAuthHeader(): Promise<Headers>;
|
||||||
@@ -115,9 +116,11 @@ export interface CollectionBase extends TreeNode {
|
|||||||
export interface Collection extends CollectionBase {
|
export interface Collection extends CollectionBase {
|
||||||
defaultTtl: ko.Observable<number>;
|
defaultTtl: ko.Observable<number>;
|
||||||
analyticalStorageTtl: ko.Observable<number>;
|
analyticalStorageTtl: ko.Observable<number>;
|
||||||
|
schema?: DataModels.ISchema;
|
||||||
|
requestSchema?: () => void;
|
||||||
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
||||||
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
||||||
quotaInfo: ko.Observable<DataModels.CollectionQuotaInfo>;
|
usageSizeInKB: ko.Observable<number>;
|
||||||
offer: ko.Observable<DataModels.Offer>;
|
offer: ko.Observable<DataModels.Offer>;
|
||||||
conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
||||||
changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
||||||
@@ -358,7 +361,8 @@ export enum CollectionTabKind {
|
|||||||
SparkMasterTab = 16,
|
SparkMasterTab = 16,
|
||||||
Gallery = 17,
|
Gallery = 17,
|
||||||
NotebookViewer = 18,
|
NotebookViewer = 18,
|
||||||
SettingsV2 = 19
|
Schema = 19,
|
||||||
|
SettingsV2 = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TerminalKind {
|
export enum TerminalKind {
|
||||||
@@ -412,14 +416,6 @@ export interface ThroughputDefaults {
|
|||||||
shared: number;
|
shared: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SubscriptionType {
|
|
||||||
Benefits,
|
|
||||||
EA,
|
|
||||||
Free,
|
|
||||||
Internal,
|
|
||||||
PAYG
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MonacoEditorSettings {
|
export class MonacoEditorSettings {
|
||||||
public readonly language: string;
|
public readonly language: string;
|
||||||
public readonly readOnly: boolean;
|
public readonly readOnly: boolean;
|
||||||
|
|||||||
383
src/Definitions/adal.d.ts
vendored
383
src/Definitions/adal.d.ts
vendored
@@ -1,383 +0,0 @@
|
|||||||
// Type definitions for adal-angular 1.0.1.1
|
|
||||||
// Project: https://github.com/AzureAD/azure-activedirectory-library-for-js#readme
|
|
||||||
// Definitions by: Daniel Perez Alvarez <https://github.com/unindented>
|
|
||||||
// Anthony Ciccarello <https://github.com/aciccarello>
|
|
||||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
|
||||||
|
|
||||||
//This is a customized version of adal on top of version 1.0.1 which does not support multi tenant
|
|
||||||
// Customized version add tenantId to stored tokens so when tenant change, adal will refetch instead of read from sessionStorage
|
|
||||||
|
|
||||||
// In module contexts the class constructor function is the exported object
|
|
||||||
// export = AuthenticationContext;
|
|
||||||
|
|
||||||
// This class is defined globally in not in a module context
|
|
||||||
declare class AuthenticationContext {
|
|
||||||
instance: string;
|
|
||||||
config: AuthenticationContext.Options;
|
|
||||||
callback: AuthenticationContext.TokenCallback;
|
|
||||||
popUp: boolean;
|
|
||||||
isAngular: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enum for request type
|
|
||||||
*/
|
|
||||||
REQUEST_TYPE: AuthenticationContext.RequestType;
|
|
||||||
RESPONSE_TYPE: AuthenticationContext.ResponseType;
|
|
||||||
CONSTANTS: AuthenticationContext.Constants;
|
|
||||||
|
|
||||||
constructor(options: AuthenticationContext.Options);
|
|
||||||
/**
|
|
||||||
* Initiates the login process by redirecting the user to Azure AD authorization endpoint.
|
|
||||||
*/
|
|
||||||
login(): void;
|
|
||||||
/**
|
|
||||||
* Returns whether a login is in progress.
|
|
||||||
*/
|
|
||||||
loginInProgress(): boolean;
|
|
||||||
/**
|
|
||||||
* Gets token for the specified resource from the cache.
|
|
||||||
* @param resource A URI that identifies the resource for which the token is requested.
|
|
||||||
* @param tenantId tenant Id.
|
|
||||||
*/
|
|
||||||
getCachedToken(resource: string, tenantId: string): string;
|
|
||||||
/**
|
|
||||||
* If user object exists, returns it. Else creates a new user object by decoding `id_token` from the cache.
|
|
||||||
*/
|
|
||||||
getCachedUser(): AuthenticationContext.UserInfo;
|
|
||||||
/**
|
|
||||||
* Adds the passed callback to the array of callbacks for the specified resource.
|
|
||||||
* @param resource A URI that identifies the resource for which the token is requested.
|
|
||||||
* @param expectedState A unique identifier (guid).
|
|
||||||
* @param callback The callback provided by the caller. It will be called with token or error.
|
|
||||||
*/
|
|
||||||
registerCallback(
|
|
||||||
expectedState: string,
|
|
||||||
resource: string,
|
|
||||||
callback: AuthenticationContext.TokenCallback,
|
|
||||||
tenantId: string
|
|
||||||
): void;
|
|
||||||
/**
|
|
||||||
* Acquires token from the cache if it is not expired. Otherwise sends request to AAD to obtain a new token.
|
|
||||||
* @param resource Resource URI identifying the target resource.
|
|
||||||
* @param callback The callback provided by the caller. It will be called with token or error.
|
|
||||||
*/
|
|
||||||
acquireToken(resource: string, tenantId: string, callback: AuthenticationContext.TokenCallback): void;
|
|
||||||
/**
|
|
||||||
* Acquires token (interactive flow using a popup window) by sending request to AAD to obtain a new token.
|
|
||||||
* @param resource Resource URI identifying the target resource.
|
|
||||||
* @param extraQueryParameters Query parameters to add to the authentication request.
|
|
||||||
* @param claims Claims to add to the authentication request.
|
|
||||||
* @param callback The callback provided by the caller. It will be called with token or error.
|
|
||||||
*/
|
|
||||||
acquireTokenPopup(
|
|
||||||
resource: string,
|
|
||||||
tenantId: string,
|
|
||||||
extraQueryParameters: string | null | undefined,
|
|
||||||
claims: string | null | undefined,
|
|
||||||
callback: AuthenticationContext.TokenCallback
|
|
||||||
): void;
|
|
||||||
/**
|
|
||||||
* Acquires token (interactive flow using a redirect) by sending request to AAD to obtain a new token. In this case the callback passed in the authentication request constructor will be called.
|
|
||||||
* @param resource Resource URI identifying the target resource.
|
|
||||||
* @param extraQueryParameters Query parameters to add to the authentication request.
|
|
||||||
* @param claims Claims to add to the authentication request.
|
|
||||||
*/
|
|
||||||
acquireTokenRedirect(
|
|
||||||
resource: string,
|
|
||||||
tenantId: string,
|
|
||||||
extraQueryParameters?: string | null,
|
|
||||||
claims?: string | null
|
|
||||||
): void;
|
|
||||||
/**
|
|
||||||
* Redirects the browser to Azure AD authorization endpoint.
|
|
||||||
* @param urlNavigate URL of the authorization endpoint.
|
|
||||||
*/
|
|
||||||
promptUser(urlNavigate: string): void;
|
|
||||||
/**
|
|
||||||
* Clears cache items.
|
|
||||||
*/
|
|
||||||
clearCache(): void;
|
|
||||||
/**
|
|
||||||
* Clears cache items for a given resource.
|
|
||||||
* @param resource Resource URI identifying the target resource.
|
|
||||||
*/
|
|
||||||
clearCacheForResource(resource: string): void;
|
|
||||||
/**
|
|
||||||
* Redirects user to logout endpoint. After logout, it will redirect to `postLogoutRedirectUri` if added as a property on the config object.
|
|
||||||
*/
|
|
||||||
logOut(): void;
|
|
||||||
/**
|
|
||||||
* Calls the passed in callback with the user object or error message related to the user.
|
|
||||||
* @param callback The callback provided by the caller. It will be called with user or error.
|
|
||||||
*/
|
|
||||||
getUser(callback: AuthenticationContext.UserCallback): void;
|
|
||||||
/**
|
|
||||||
* Checks if the URL fragment contains access token, id token or error description.
|
|
||||||
* @param hash Hash passed from redirect page.
|
|
||||||
*/
|
|
||||||
isCallback(hash: string): boolean;
|
|
||||||
/**
|
|
||||||
* Gets login error.
|
|
||||||
*/
|
|
||||||
getLoginError(): string;
|
|
||||||
/**
|
|
||||||
* Creates a request info object from the URL fragment and returns it.
|
|
||||||
*/
|
|
||||||
getRequestInfo(hash: string): AuthenticationContext.RequestInfo;
|
|
||||||
/**
|
|
||||||
* Saves token or error received in the response from AAD in the cache. In case of `id_token`, it also creates the user object.
|
|
||||||
*/
|
|
||||||
saveTokenFromHash(requestInfo: AuthenticationContext.RequestInfo): void;
|
|
||||||
/**
|
|
||||||
* Gets resource for given endpoint if mapping is provided with config.
|
|
||||||
* @param endpoint Resource URI identifying the target resource.
|
|
||||||
*/
|
|
||||||
getResourceForEndpoint(resource: string): string;
|
|
||||||
/**
|
|
||||||
* This method must be called for processing the response received from AAD. It extracts the hash, processes the token or error, saves it in the cache and calls the callbacks with the result.
|
|
||||||
* @param hash Hash fragment of URL. Defaults to `window.location.hash`.
|
|
||||||
*/
|
|
||||||
handleWindowCallback(hash?: string): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks the logging Level, constructs the log message and logs it. Users need to implement/override this method to turn on logging.
|
|
||||||
* @param level Level can be set 0, 1, 2 and 3 which turns on 'error', 'warning', 'info' or 'verbose' level logging respectively.
|
|
||||||
* @param message Message to log.
|
|
||||||
* @param error Error to log.
|
|
||||||
*/
|
|
||||||
log(level: AuthenticationContext.LoggingLevel, message: string, error: any): void;
|
|
||||||
/**
|
|
||||||
* Logs messages when logging level is set to 0.
|
|
||||||
* @param message Message to log.
|
|
||||||
* @param error Error to log.
|
|
||||||
*/
|
|
||||||
error(message: string, error: any): void;
|
|
||||||
/**
|
|
||||||
* Logs messages when logging level is set to 1.
|
|
||||||
* @param message Message to log.
|
|
||||||
*/
|
|
||||||
warn(message: string): void;
|
|
||||||
/**
|
|
||||||
* Logs messages when logging level is set to 2.
|
|
||||||
* @param message Message to log.
|
|
||||||
*/
|
|
||||||
info(message: string): void;
|
|
||||||
/**
|
|
||||||
* Logs messages when logging level is set to 3.
|
|
||||||
* @param message Message to log.
|
|
||||||
*/
|
|
||||||
verbose(message: string): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs Pii messages when Logging Level is set to 0 and window.piiLoggingEnabled is set to true.
|
|
||||||
* @param message Message to log.
|
|
||||||
* @param error Error to log.
|
|
||||||
*/
|
|
||||||
errorPii(message: string, error: any): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs Pii messages when Logging Level is set to 1 and window.piiLoggingEnabled is set to true.
|
|
||||||
* @param message Message to log.
|
|
||||||
*/
|
|
||||||
warnPii(message: string): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs messages when Logging Level is set to 2 and window.piiLoggingEnabled is set to true.
|
|
||||||
* @param message Message to log.
|
|
||||||
*/
|
|
||||||
infoPii(message: string): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs messages when Logging Level is set to 3 and window.piiLoggingEnabled is set to true.
|
|
||||||
* @param message Message to log.
|
|
||||||
*/
|
|
||||||
verbosePii(message: string): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare namespace AuthenticationContext {
|
|
||||||
function inject(config: Options): AuthenticationContext;
|
|
||||||
|
|
||||||
type LoggingLevel = 0 | 1 | 2 | 3;
|
|
||||||
|
|
||||||
type RequestType = "LOGIN" | "RENEW_TOKEN" | "UNKNOWN";
|
|
||||||
|
|
||||||
type ResponseType = "id_token token" | "token";
|
|
||||||
|
|
||||||
interface RequestInfo {
|
|
||||||
/**
|
|
||||||
* Object comprising of fields such as id_token/error, session_state, state, e.t.c.
|
|
||||||
*/
|
|
||||||
parameters: any;
|
|
||||||
/**
|
|
||||||
* Request type.
|
|
||||||
*/
|
|
||||||
requestType: RequestType;
|
|
||||||
/**
|
|
||||||
* Whether state is valid.
|
|
||||||
*/
|
|
||||||
stateMatch: boolean;
|
|
||||||
/**
|
|
||||||
* Unique guid used to match the response with the request.
|
|
||||||
*/
|
|
||||||
stateResponse: string;
|
|
||||||
/**
|
|
||||||
* Whether `requestType` contains `id_token`, `access_token` or error.
|
|
||||||
*/
|
|
||||||
valid: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UserInfo {
|
|
||||||
/**
|
|
||||||
* Username assigned from UPN or email.
|
|
||||||
*/
|
|
||||||
userName: string;
|
|
||||||
/**
|
|
||||||
* Properties parsed from `id_token`.
|
|
||||||
*/
|
|
||||||
profile: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
type TokenCallback = (errorDesc: string | null, token: string | null, error: any) => void;
|
|
||||||
|
|
||||||
type UserCallback = (errorDesc: string | null, user: UserInfo | null) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration options for Authentication Context
|
|
||||||
*/
|
|
||||||
interface Options {
|
|
||||||
/**
|
|
||||||
* Client ID assigned to your app by Azure Active Directory.
|
|
||||||
*/
|
|
||||||
clientId: string;
|
|
||||||
/**
|
|
||||||
* Endpoint at which you expect to receive tokens.Defaults to `window.location.href`.
|
|
||||||
*/
|
|
||||||
redirectUri?: string;
|
|
||||||
/**
|
|
||||||
* Azure Active Directory instance. Defaults to `https://login.microsoftonline.com/`.
|
|
||||||
*/
|
|
||||||
instance?: string;
|
|
||||||
/**
|
|
||||||
* Your target tenant. Defaults to `common`.
|
|
||||||
*/
|
|
||||||
tenant?: string;
|
|
||||||
/**
|
|
||||||
* Query parameters to add to the authentication request.
|
|
||||||
*/
|
|
||||||
extraQueryParameter?: string;
|
|
||||||
/**
|
|
||||||
* Unique identifier used to map the request with the response. Defaults to RFC4122 version 4 guid (128 bits).
|
|
||||||
*/
|
|
||||||
correlationId?: string;
|
|
||||||
/**
|
|
||||||
* User defined function of handling the navigation to Azure AD authorization endpoint in case of login.
|
|
||||||
*/
|
|
||||||
displayCall?: (url: string) => void;
|
|
||||||
/**
|
|
||||||
* Set this to true to enable login in a popup winodow instead of a full redirect. Defaults to `false`.
|
|
||||||
*/
|
|
||||||
popUp?: boolean;
|
|
||||||
/**
|
|
||||||
* Set this to the resource to request on login. Defaults to `clientId`.
|
|
||||||
*/
|
|
||||||
loginResource?: string;
|
|
||||||
/**
|
|
||||||
* Set this to redirect the user to a custom login page.
|
|
||||||
*/
|
|
||||||
localLoginUrl?: string;
|
|
||||||
/**
|
|
||||||
* Redirects to start page after login. Defaults to `true`.
|
|
||||||
*/
|
|
||||||
navigateToLoginRequestUrl?: boolean;
|
|
||||||
/**
|
|
||||||
* Set this to redirect the user to a custom logout page.
|
|
||||||
*/
|
|
||||||
logOutUri?: string;
|
|
||||||
/**
|
|
||||||
* Redirects the user to postLogoutRedirectUri after logout. Defaults to `redirectUri`.
|
|
||||||
*/
|
|
||||||
postLogoutRedirectUri?: string;
|
|
||||||
/**
|
|
||||||
* Sets browser storage to either 'localStorage' or sessionStorage'. Defaults to `sessionStorage`.
|
|
||||||
*/
|
|
||||||
cacheLocation?: "localStorage" | "sessionStorage";
|
|
||||||
/**
|
|
||||||
* Array of keywords or URIs. Adal will attach a token to outgoing requests that have these keywords or URIs.
|
|
||||||
*/
|
|
||||||
endpoints?: { [resource: string]: string };
|
|
||||||
/**
|
|
||||||
* Array of keywords or URIs. Adal will not attach a token to outgoing requests that have these keywords or URIs.
|
|
||||||
*/
|
|
||||||
anonymousEndpoints?: string[];
|
|
||||||
/**
|
|
||||||
* If the cached token is about to be expired in the expireOffsetSeconds (in seconds), Adal will renew the token instead of using the cached token. Defaults to 300 seconds.
|
|
||||||
*/
|
|
||||||
expireOffsetSeconds?: number;
|
|
||||||
/**
|
|
||||||
* The number of milliseconds of inactivity before a token renewal response from AAD should be considered timed out. Defaults to 6 seconds.
|
|
||||||
*/
|
|
||||||
loadFrameTimeout?: number;
|
|
||||||
/**
|
|
||||||
* Callback to be invoked when a token is acquired.
|
|
||||||
*/
|
|
||||||
callback?: TokenCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LoggingConfig {
|
|
||||||
level: LoggingLevel;
|
|
||||||
log: (message: string) => void;
|
|
||||||
piiLoggingEnabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enum for storage constants
|
|
||||||
*/
|
|
||||||
interface Constants {
|
|
||||||
ACCESS_TOKEN: "access_token";
|
|
||||||
EXPIRES_IN: "expires_in";
|
|
||||||
ID_TOKEN: "id_token";
|
|
||||||
ERROR_DESCRIPTION: "error_description";
|
|
||||||
SESSION_STATE: "session_state";
|
|
||||||
STORAGE: {
|
|
||||||
TOKEN_KEYS: "adal.token.keys";
|
|
||||||
ACCESS_TOKEN_KEY: "adal.access.token.key";
|
|
||||||
EXPIRATION_KEY: "adal.expiration.key";
|
|
||||||
STATE_LOGIN: "adal.state.login";
|
|
||||||
STATE_RENEW: "adal.state.renew";
|
|
||||||
NONCE_IDTOKEN: "adal.nonce.idtoken";
|
|
||||||
SESSION_STATE: "adal.session.state";
|
|
||||||
USERNAME: "adal.username";
|
|
||||||
IDTOKEN: "adal.idtoken";
|
|
||||||
ERROR: "adal.error";
|
|
||||||
ERROR_DESCRIPTION: "adal.error.description";
|
|
||||||
LOGIN_REQUEST: "adal.login.request";
|
|
||||||
LOGIN_ERROR: "adal.login.error";
|
|
||||||
RENEW_STATUS: "adal.token.renew.status";
|
|
||||||
};
|
|
||||||
RESOURCE_DELIMETER: "|";
|
|
||||||
LOADFRAME_TIMEOUT: "6000";
|
|
||||||
TOKEN_RENEW_STATUS_CANCELED: "Canceled";
|
|
||||||
TOKEN_RENEW_STATUS_COMPLETED: "Completed";
|
|
||||||
TOKEN_RENEW_STATUS_IN_PROGRESS: "In Progress";
|
|
||||||
LOGGING_LEVEL: {
|
|
||||||
ERROR: 0;
|
|
||||||
WARN: 1;
|
|
||||||
INFO: 2;
|
|
||||||
VERBOSE: 3;
|
|
||||||
};
|
|
||||||
LEVEL_STRING_MAP: {
|
|
||||||
0: "ERROR:";
|
|
||||||
1: "WARNING:";
|
|
||||||
2: "INFO:";
|
|
||||||
3: "VERBOSE:";
|
|
||||||
};
|
|
||||||
POPUP_WIDTH: 483;
|
|
||||||
POPUP_HEIGHT: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// declare global {
|
|
||||||
// interface Window {
|
|
||||||
// Logging: AuthenticationContext.LoggingConfig;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@@ -44,10 +44,6 @@ describe("Component Registerer", () => {
|
|||||||
expect(ko.components.isRegistered("user-defined-function-tab")).toBe(true);
|
expect(ko.components.isRegistered("user-defined-function-tab")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should register settings-tab component", () => {
|
|
||||||
expect(ko.components.isRegistered("settings-tab")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should register settings-tab-v2 component", () => {
|
it("should register settings-tab-v2 component", () => {
|
||||||
expect(ko.components.isRegistered("settings-tab-v2")).toBe(true);
|
expect(ko.components.isRegistered("settings-tab-v2")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTa
|
|||||||
ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab());
|
ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab());
|
||||||
ko.components.register("trigger-tab", new TabComponents.TriggerTab());
|
ko.components.register("trigger-tab", new TabComponents.TriggerTab());
|
||||||
ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab());
|
ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab());
|
||||||
ko.components.register("settings-tab", new TabComponents.SettingsTab());
|
|
||||||
ko.components.register("settings-tab-v2", new TabComponents.SettingsTabV2());
|
ko.components.register("settings-tab-v2", new TabComponents.SettingsTabV2());
|
||||||
ko.components.register("query-tab", new TabComponents.QueryTab());
|
ko.components.register("query-tab", new TabComponents.QueryTab());
|
||||||
ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab());
|
ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab());
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export interface CommandButtonComponentProps {
|
|||||||
/**
|
/**
|
||||||
* Label for the button
|
* Label for the button
|
||||||
*/
|
*/
|
||||||
commandButtonLabel: string;
|
commandButtonLabel?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if this button opens a tab or pane, false otherwise.
|
* True if this button opens a tab or pane, false otherwise.
|
||||||
|
|||||||
@@ -44,12 +44,10 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
|||||||
onChange?: (_?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => void;
|
onChange?: (_?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => void;
|
||||||
}[] = [
|
}[] = [
|
||||||
{ key: "feature.enablechangefeedpolicy", label: "Enable change feed policy", value: "true" },
|
{ key: "feature.enablechangefeedpolicy", label: "Enable change feed policy", value: "true" },
|
||||||
{ key: "feature.enablerupm", label: "Enable RUPM", value: "true" },
|
|
||||||
{ key: "feature.dataexplorerexecutesproc", label: "Execute stored procedure", value: "true" },
|
{ key: "feature.dataexplorerexecutesproc", label: "Execute stored procedure", value: "true" },
|
||||||
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
|
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
|
||||||
{ key: "feature.enablettl", label: "Enable TTL", value: "true" },
|
{ key: "feature.enablettl", label: "Enable TTL", value: "true" },
|
||||||
{ key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" },
|
{ key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" },
|
||||||
{ key: "feature.enablecodeofconduct", label: "Enable Code Of Conduct Acknowledgement", value: "true" },
|
|
||||||
{
|
{
|
||||||
key: "feature.enableLinkInjection",
|
key: "feature.enableLinkInjection",
|
||||||
label: "Enable Injecting Notebook Viewer Link into the first cell",
|
label: "Enable Injecting Notebook Viewer Link into the first cell",
|
||||||
|
|||||||
@@ -131,12 +131,6 @@ exports[`Feature panel renders all flags 1`] = `
|
|||||||
label="Enable change feed policy"
|
label="Enable change feed policy"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
/>
|
/>
|
||||||
<StyledCheckboxBase
|
|
||||||
checked={false}
|
|
||||||
key="feature.enablerupm"
|
|
||||||
label="Enable RUPM"
|
|
||||||
onChange={[Function]}
|
|
||||||
/>
|
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
checked={false}
|
checked={false}
|
||||||
key="feature.dataexplorerexecutesproc"
|
key="feature.dataexplorerexecutesproc"
|
||||||
@@ -163,14 +157,14 @@ exports[`Feature panel renders all flags 1`] = `
|
|||||||
/>
|
/>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
checked={false}
|
checked={false}
|
||||||
key="feature.enablecodeofconduct"
|
key="feature.enableLinkInjection"
|
||||||
label="Enable Code Of Conduct Acknowledgement"
|
label="Enable Injecting Notebook Viewer Link into the first cell"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
/>
|
/>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
checked={false}
|
checked={false}
|
||||||
key="feature.enableLinkInjection"
|
key="feature.canexceedmaximumvalue"
|
||||||
label="Enable Injecting Notebook Viewer Link into the first cell"
|
label="Can exceed max value"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -178,12 +172,6 @@ exports[`Feature panel renders all flags 1`] = `
|
|||||||
className="checkboxRow"
|
className="checkboxRow"
|
||||||
horizontalAlign="space-between"
|
horizontalAlign="space-between"
|
||||||
>
|
>
|
||||||
<StyledCheckboxBase
|
|
||||||
checked={false}
|
|
||||||
key="feature.canexceedmaximumvalue"
|
|
||||||
label="Can exceed max value"
|
|
||||||
onChange={[Function]}
|
|
||||||
/>
|
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
checked={false}
|
checked={false}
|
||||||
key="feature.enablefixedcollectionwithsharedthroughput"
|
key="feature.enablefixedcollectionwithsharedthroughput"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Dropdown,
|
Dropdown,
|
||||||
FocusZone,
|
FocusZone,
|
||||||
|
FontIcon,
|
||||||
FontWeights,
|
FontWeights,
|
||||||
IDropdownOption,
|
IDropdownOption,
|
||||||
IPageSpecification,
|
IPageSpecification,
|
||||||
@@ -16,7 +17,7 @@ import {
|
|||||||
Text
|
Text
|
||||||
} from "office-ui-fabric-react";
|
} from "office-ui-fabric-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { IGalleryItem, JunoClient, IJunoResponse, IPublicGalleryData } from "../../../Juno/JunoClient";
|
import { IGalleryItem, IJunoResponse, IPublicGalleryData, JunoClient } from "../../../Juno/JunoClient";
|
||||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||||
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
|
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
|
||||||
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
|
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
|
||||||
@@ -136,7 +137,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
const tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
const tabs: GalleryTabInfo[] = [this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
||||||
|
|
||||||
if (this.props.container?.isGalleryPublishEnabled()) {
|
if (this.props.container?.isGalleryPublishEnabled()) {
|
||||||
tabs.push(
|
tabs.push(
|
||||||
@@ -146,7 +147,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
this.state.isCodeOfConductAccepted
|
this.state.isCodeOfConductAccepted
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
tabs.push(this.createTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
tabs.push(this.createFavoritesTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
||||||
|
|
||||||
// explicitly checking if isCodeOfConductAccepted is not false, as it is initially undefined.
|
// explicitly checking if isCodeOfConductAccepted is not false, as it is initially undefined.
|
||||||
// Displaying code of conduct component on gallery load should not be the default behavior.
|
// Displaying code of conduct component on gallery load should not be the default behavior.
|
||||||
@@ -183,6 +184,27 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isEmptyData = (data: IGalleryItem[]): boolean => {
|
||||||
|
return !data || data.length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
private createEmptyTabContent = (iconName: string, line1: string, line2: string): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Stack horizontalAlign="center" tokens={{ childrenGap: 10 }}>
|
||||||
|
<FontIcon iconName={iconName} style={{ fontSize: 100, color: "lightgray", marginTop: 20 }} />
|
||||||
|
<Text styles={{ root: { fontWeight: FontWeights.semibold } }}>{line1}</Text>
|
||||||
|
<Text>{line2}</Text>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private createSamplesTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
|
||||||
|
return {
|
||||||
|
tab,
|
||||||
|
content: this.createSearchBarHeader(this.createCardsTabContent(data))
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
private createPublicGalleryTab(
|
private createPublicGalleryTab(
|
||||||
tab: GalleryTab,
|
tab: GalleryTab,
|
||||||
data: IGalleryItem[],
|
data: IGalleryItem[],
|
||||||
@@ -194,17 +216,29 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private createTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
|
private createFavoritesTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
|
||||||
return {
|
return {
|
||||||
tab,
|
tab,
|
||||||
content: this.createSearchBarHeader(this.createCardsTabContent(data))
|
content: this.isEmptyData(data)
|
||||||
|
? this.createEmptyTabContent(
|
||||||
|
"ContactHeart",
|
||||||
|
"You have not liked anything",
|
||||||
|
"Like any notebook from Official Samples or Public gallery"
|
||||||
|
)
|
||||||
|
: this.createSearchBarHeader(this.createCardsTabContent(data))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private createPublishedNotebooksTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
|
private createPublishedNotebooksTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
|
||||||
return {
|
return {
|
||||||
tab,
|
tab,
|
||||||
content: this.createPublishedNotebooksTabContent(data)
|
content: this.isEmptyData(data)
|
||||||
|
? this.createEmptyTabContent(
|
||||||
|
"Contact",
|
||||||
|
"You have not published anything",
|
||||||
|
"Publish your sample notebooks to share your published work with others"
|
||||||
|
)
|
||||||
|
: this.createPublishedNotebooksTabContent(data)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -364,9 +398,9 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
||||||
if (!offline) {
|
if (!offline) {
|
||||||
try {
|
try {
|
||||||
let response: IJunoResponse<IPublicGalleryData> | IJunoResponse<IGalleryItem[]>;
|
let response: IJunoResponse<IGalleryItem[]> | IJunoResponse<IPublicGalleryData>;
|
||||||
if (this.props.container.isCodeOfConductEnabled()) {
|
if (this.props.container) {
|
||||||
response = await this.props.junoClient.fetchPublicNotebooks();
|
response = await this.props.junoClient.getPublicGalleryData();
|
||||||
this.isCodeOfConductAccepted = response.data?.metadata.acceptedCodeOfConduct;
|
this.isCodeOfConductAccepted = response.data?.metadata.acceptedCodeOfConduct;
|
||||||
this.publicNotebooks = response.data?.notebooksData;
|
this.publicNotebooks = response.data?.notebooksData;
|
||||||
} else {
|
} else {
|
||||||
@@ -568,7 +602,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
|
|
||||||
private deleteItem = async (data: IGalleryItem): Promise<void> => {
|
private deleteItem = async (data: IGalleryItem): Promise<void> => {
|
||||||
GalleryUtils.deleteItem(this.props.container, this.props.junoClient, data, item => {
|
GalleryUtils.deleteItem(this.props.container, this.props.junoClient, data, item => {
|
||||||
this.publishedNotebooks = this.publishedNotebooks.filter(notebook => item.id !== notebook.id);
|
this.publishedNotebooks = this.publishedNotebooks?.filter(notebook => item.id !== notebook.id);
|
||||||
this.refreshSelectedTab(item);
|
this.refreshSelectedTab(item);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -89,12 +89,12 @@ describe("SettingsComponent", () => {
|
|||||||
it("auto pilot helper functions pass on correct value", () => {
|
it("auto pilot helper functions pass on correct value", () => {
|
||||||
const newCollection = { ...collection };
|
const newCollection = { ...collection };
|
||||||
newCollection.offer = ko.observable<DataModels.Offer>({
|
newCollection.offer = ko.observable<DataModels.Offer>({
|
||||||
content: {
|
autoscaleMaxThroughput: 10000,
|
||||||
offerAutopilotSettings: {
|
manualThroughput: undefined,
|
||||||
maxThroughput: 10000
|
minimumThroughput: 400,
|
||||||
}
|
id: "test",
|
||||||
}
|
offerReplacePending: false
|
||||||
} as DataModels.Offer);
|
});
|
||||||
|
|
||||||
const props = { ...baseProps };
|
const props = { ...baseProps };
|
||||||
props.settingsTab.collection = newCollection;
|
props.settingsTab.collection = newCollection;
|
||||||
@@ -187,21 +187,6 @@ describe("SettingsComponent", () => {
|
|||||||
expect(settingsComponentInstance.hasConflictResolution()).toEqual(true);
|
expect(settingsComponentInstance.hasConflictResolution()).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("isOfferReplacePending", () => {
|
|
||||||
let settingsComponentInstance = new SettingsComponent(baseProps);
|
|
||||||
expect(settingsComponentInstance.isOfferReplacePending()).toEqual(undefined);
|
|
||||||
|
|
||||||
const newCollection = { ...collection };
|
|
||||||
newCollection.offer = ko.observable({
|
|
||||||
headers: { "x-ms-offer-replace-pending": true }
|
|
||||||
} as DataModels.OfferWithHeaders);
|
|
||||||
const props = { ...baseProps };
|
|
||||||
props.settingsTab.collection = newCollection;
|
|
||||||
|
|
||||||
settingsComponentInstance = new SettingsComponent(props);
|
|
||||||
expect(settingsComponentInstance.isOfferReplacePending()).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("save calls updateCollection, updateMongoDBCollectionThroughRP and updateOffer", async () => {
|
it("save calls updateCollection, updateMongoDBCollectionThroughRP and updateOffer", async () => {
|
||||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
||||||
wrapper.setState({ isSubSettingsSaveable: true, isScaleSaveable: true, isMongoIndexingPolicySaveable: true });
|
wrapper.setState({ isSubSettingsSaveable: true, isScaleSaveable: true, isMongoIndexingPolicySaveable: true });
|
||||||
|
|||||||
@@ -2,28 +2,23 @@ import * as React from "react";
|
|||||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import * as SharedConstants from "../../../Shared/Constants";
|
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import DiscardIcon from "../../../../images/discard.svg";
|
import DiscardIcon from "../../../../images/discard.svg";
|
||||||
import SaveIcon from "../../../../images/save-cosmos.svg";
|
import SaveIcon from "../../../../images/save-cosmos.svg";
|
||||||
import { traceStart, traceFailure, traceSuccess, trace } from "../../../Shared/Telemetry/TelemetryProcessor";
|
import { traceStart, traceFailure, traceSuccess, trace } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||||
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
|
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { userContext } from "../../../UserContext";
|
|
||||||
import { updateOfferThroughputBeyondLimit } from "../../../Common/dataAccess/updateOfferThroughputBeyondLimit";
|
|
||||||
import SettingsTab from "../../Tabs/SettingsTabV2";
|
import SettingsTab from "../../Tabs/SettingsTabV2";
|
||||||
import { throughputUnit } from "./SettingsRenderUtils";
|
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
|
||||||
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
|
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
|
||||||
import {
|
import {
|
||||||
MongoIndexingPolicyComponent,
|
MongoIndexingPolicyComponent,
|
||||||
MongoIndexingPolicyComponentProps
|
MongoIndexingPolicyComponentProps
|
||||||
} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
|
} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
|
||||||
import {
|
import {
|
||||||
getMaxRUs,
|
|
||||||
hasDatabaseSharedThroughput,
|
hasDatabaseSharedThroughput,
|
||||||
GeospatialConfigType,
|
GeospatialConfigType,
|
||||||
TtlType,
|
TtlType,
|
||||||
@@ -49,6 +44,7 @@ import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/genera
|
|||||||
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
|
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
|
||||||
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
|
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
|
||||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||||
|
import { isEmpty } from "underscore";
|
||||||
|
|
||||||
interface SettingsV2TabInfo {
|
interface SettingsV2TabInfo {
|
||||||
tab: SettingsV2TabTypes;
|
tab: SettingsV2TabTypes;
|
||||||
@@ -227,7 +223,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
|
|
||||||
public loadMongoIndexes = async (): Promise<void> => {
|
public loadMongoIndexes = async (): Promise<void> => {
|
||||||
if (
|
if (
|
||||||
this.container.isMongoIndexEditorEnabled() &&
|
|
||||||
this.container.isPreferredApiMongoDB() &&
|
this.container.isPreferredApiMongoDB() &&
|
||||||
this.container.isEnableMongoCapabilityPresent() &&
|
this.container.isEnableMongoCapabilityPresent() &&
|
||||||
this.container.databaseAccount()
|
this.container.databaseAccount()
|
||||||
@@ -275,19 +270,14 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
};
|
};
|
||||||
|
|
||||||
private setAutoPilotStates = (): void => {
|
private setAutoPilotStates = (): void => {
|
||||||
const offer = this.collection?.offer && this.collection.offer();
|
const autoscaleMaxThroughput = this.collection?.offer()?.autoscaleMaxThroughput;
|
||||||
const offerAutopilotSettings = offer?.content?.offerAutopilotSettings;
|
|
||||||
|
|
||||||
if (
|
if (autoscaleMaxThroughput && AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
|
||||||
offerAutopilotSettings &&
|
|
||||||
offerAutopilotSettings.maxThroughput &&
|
|
||||||
AutoPilotUtils.isValidAutoPilotThroughput(offerAutopilotSettings.maxThroughput)
|
|
||||||
) {
|
|
||||||
this.setState({
|
this.setState({
|
||||||
isAutoPilotSelected: true,
|
isAutoPilotSelected: true,
|
||||||
wasAutopilotOriginallySet: true,
|
wasAutopilotOriginallySet: true,
|
||||||
autoPilotThroughput: offerAutopilotSettings.maxThroughput,
|
autoPilotThroughput: autoscaleMaxThroughput,
|
||||||
autoPilotThroughputBaseline: offerAutopilotSettings.maxThroughput
|
autoPilotThroughputBaseline: autoscaleMaxThroughput
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -305,12 +295,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
!!this.collection.conflictResolutionPolicy();
|
!!this.collection.conflictResolutionPolicy();
|
||||||
|
|
||||||
public isOfferReplacePending = (): boolean => {
|
public isOfferReplacePending = (): boolean => {
|
||||||
const offer = this.collection?.offer && this.collection.offer();
|
return this.collection?.offer()?.offerReplacePending;
|
||||||
return (
|
|
||||||
offer &&
|
|
||||||
Object.keys(offer).find(value => value === "headers") &&
|
|
||||||
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public onSaveClick = async (): Promise<void> => {
|
public onSaveClick = async (): Promise<void> => {
|
||||||
@@ -448,103 +433,34 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.isScaleSaveable) {
|
if (this.state.isScaleSaveable) {
|
||||||
const newThroughput = this.state.throughput;
|
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||||
const newOffer: DataModels.Offer = { ...this.collection.offer() };
|
databaseId: this.collection.databaseId,
|
||||||
const originalThroughputValue: number = this.state.throughput;
|
collectionId: this.collection.id(),
|
||||||
|
currentOffer: this.collection.offer(),
|
||||||
if (newOffer.content) {
|
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
||||||
newOffer.content.offerThroughput = newThroughput;
|
manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput
|
||||||
} else {
|
};
|
||||||
newOffer.content = {
|
if (this.hasProvisioningTypeChanged()) {
|
||||||
offerThroughput: newThroughput,
|
|
||||||
offerIsRUPerMinuteThroughputEnabled: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const headerOptions: RequestOptions = { initialHeaders: {} };
|
|
||||||
|
|
||||||
if (this.state.isAutoPilotSelected) {
|
|
||||||
newOffer.content.offerAutopilotSettings = {
|
|
||||||
maxThroughput: this.state.autoPilotThroughput
|
|
||||||
};
|
|
||||||
|
|
||||||
// user has changed from provisioned --> autoscale
|
|
||||||
if (this.hasProvisioningTypeChanged()) {
|
|
||||||
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToAutopilot] = "true";
|
|
||||||
delete newOffer.content.offerAutopilotSettings;
|
|
||||||
} else {
|
|
||||||
delete newOffer.content.offerThroughput;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
isAutoPilotSelected: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// user has changed from autoscale --> provisioned
|
|
||||||
if (this.hasProvisioningTypeChanged()) {
|
|
||||||
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToManualThroughput] = "true";
|
|
||||||
} else {
|
|
||||||
delete newOffer.content.offerAutopilotSettings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
getMaxRUs(this.collection, this.container) <=
|
|
||||||
SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
|
||||||
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
|
||||||
this.container
|
|
||||||
) {
|
|
||||||
const requestPayload = {
|
|
||||||
subscriptionId: userContext.subscriptionId,
|
|
||||||
databaseAccountName: userContext.databaseAccount.name,
|
|
||||||
resourceGroup: userContext.resourceGroup,
|
|
||||||
databaseName: this.collection.databaseId,
|
|
||||||
collectionName: this.collection.id(),
|
|
||||||
throughput: newThroughput,
|
|
||||||
offerIsRUPerMinuteThroughputEnabled: false
|
|
||||||
};
|
|
||||||
|
|
||||||
await updateOfferThroughputBeyondLimit(requestPayload);
|
|
||||||
this.collection.offer().content.offerThroughput = originalThroughputValue;
|
|
||||||
this.setState({
|
|
||||||
isScaleSaveable: false,
|
|
||||||
isScaleDiscardable: false,
|
|
||||||
throughput: originalThroughputValue,
|
|
||||||
throughputBaseline: originalThroughputValue,
|
|
||||||
initialNotification: {
|
|
||||||
description: `Throughput update for ${newThroughput} ${throughputUnit}`
|
|
||||||
} as DataModels.Notification
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
|
||||||
databaseId: this.collection.databaseId,
|
|
||||||
collectionId: this.collection.id(),
|
|
||||||
currentOffer: this.collection.offer(),
|
|
||||||
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
|
||||||
manualThroughput: this.state.isAutoPilotSelected ? undefined : newThroughput
|
|
||||||
};
|
|
||||||
if (this.hasProvisioningTypeChanged()) {
|
|
||||||
if (this.state.isAutoPilotSelected) {
|
|
||||||
updateOfferParams.migrateToAutoPilot = true;
|
|
||||||
} else {
|
|
||||||
updateOfferParams.migrateToManual = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
|
||||||
this.collection.offer(updatedOffer);
|
|
||||||
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
|
||||||
if (this.state.isAutoPilotSelected) {
|
if (this.state.isAutoPilotSelected) {
|
||||||
this.setState({
|
updateOfferParams.migrateToAutoPilot = true;
|
||||||
autoPilotThroughput: updatedOffer.content.offerAutopilotSettings.maxThroughput,
|
|
||||||
autoPilotThroughputBaseline: updatedOffer.content.offerAutopilotSettings.maxThroughput
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
updateOfferParams.migrateToManual = true;
|
||||||
throughput: updatedOffer.content.offerThroughput,
|
|
||||||
throughputBaseline: updatedOffer.content.offerThroughput
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||||
|
this.collection.offer(updatedOffer);
|
||||||
|
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
||||||
|
if (this.state.isAutoPilotSelected) {
|
||||||
|
this.setState({
|
||||||
|
autoPilotThroughput: updatedOffer.autoscaleMaxThroughput,
|
||||||
|
autoPilotThroughputBaseline: updatedOffer.autoscaleMaxThroughput
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
throughput: updatedOffer.manualThroughput,
|
||||||
|
throughputBaseline: updatedOffer.manualThroughput
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.container.isRefreshingExplorer(false);
|
this.container.isRefreshingExplorer(false);
|
||||||
this.setBaseline();
|
this.setBaseline();
|
||||||
@@ -809,7 +725,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const offerThroughput = this.collection?.offer && this.collection.offer()?.content?.offerThroughput;
|
const offerThroughput = this.collection.offer()?.manualThroughput;
|
||||||
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
||||||
? ChangeFeedPolicyState.On
|
? ChangeFeedPolicyState.On
|
||||||
: ChangeFeedPolicyState.Off;
|
: ChangeFeedPolicyState.Off;
|
||||||
@@ -1000,15 +916,18 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||||
content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />
|
content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />
|
||||||
});
|
});
|
||||||
} else if (
|
} else if (this.container.isPreferredApiMongoDB()) {
|
||||||
this.container.isMongoIndexEditorEnabled() &&
|
if (isEmpty(this.container.features())) {
|
||||||
this.container.isPreferredApiMongoDB() &&
|
tabs.push({
|
||||||
this.container.isEnableMongoCapabilityPresent()
|
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||||
) {
|
content: mongoIndexingPolicyAADError
|
||||||
tabs.push({
|
});
|
||||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
} else if (this.container.isEnableMongoCapabilityPresent()) {
|
||||||
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />
|
tabs.push({
|
||||||
});
|
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||||
|
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.hasConflictResolution()) {
|
if (this.hasConflictResolution()) {
|
||||||
|
|||||||
@@ -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, true)}
|
{getEstimatedSpendingElement(estimatedSpendingColumns, estimatedSpendingItems, 1000, 2, priceBreakdown, false)}
|
||||||
|
|
||||||
{getEstimatedAutoscaleSpendElement(1000, "mooncake", 2, false)}
|
|
||||||
|
|
||||||
{manualToAutoscaleDisclaimerElement}
|
{manualToAutoscaleDisclaimerElement}
|
||||||
{ttlWarning}
|
{ttlWarning}
|
||||||
@@ -42,7 +66,7 @@ class SettingsRenderUtilsTestComponent extends React.Component {
|
|||||||
{updateThroughputDelayedApplyWarningMessage}
|
{updateThroughputDelayedApplyWarningMessage}
|
||||||
|
|
||||||
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||||
{getThroughputApplyShortDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
{getThroughputApplyShortDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection")}
|
||||||
{getThroughputApplyLongDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
{getThroughputApplyLongDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||||
|
|
||||||
{getToolTipContainer(<span>Sample Text</span>)}
|
{getToolTipContainer(<span>Sample Text</span>)}
|
||||||
@@ -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("$");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,64 +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
|
||||||
rupmEnabled: boolean
|
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
const hourlyPrice: number = computeRUUsagePriceHourly(serverId, rupmEnabled, 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -266,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,
|
||||||
@@ -319,14 +367,13 @@ export const getThroughputApplyShortDelayMessage = (
|
|||||||
throughput: number,
|
throughput: number,
|
||||||
throughputUnit: string,
|
throughputUnit: string,
|
||||||
databaseName: string,
|
databaseName: string,
|
||||||
collectionName: string,
|
collectionName: string
|
||||||
targetThroughput: number
|
|
||||||
): JSX.Element => (
|
): JSX.Element => (
|
||||||
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
|
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
|
||||||
A request to increase the throughput is currently in progress. This operation will take some time to complete.
|
A request to increase the throughput is currently in progress. This operation will take some time to complete.
|
||||||
<br />
|
<br />
|
||||||
Database: {databaseName}, Container: {collectionName}{" "}
|
Database: {databaseName}, Container: {collectionName}{" "}
|
||||||
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, targetThroughput)}
|
{getCurrentThroughput(isAutoscale, throughput, throughputUnit)}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Text,
|
Text,
|
||||||
SelectionMode,
|
SelectionMode,
|
||||||
IDetailsRowProps,
|
|
||||||
DetailsRow,
|
|
||||||
IColumn,
|
IColumn,
|
||||||
MessageBar,
|
MessageBar,
|
||||||
MessageBarType,
|
MessageBarType,
|
||||||
@@ -21,12 +19,11 @@ import {
|
|||||||
mongoIndexingPolicyDisclaimer,
|
mongoIndexingPolicyDisclaimer,
|
||||||
mediumWidthStackStyles,
|
mediumWidthStackStyles,
|
||||||
subComponentStackProps,
|
subComponentStackProps,
|
||||||
transparentDetailsRowStyles,
|
|
||||||
createAndAddMongoIndexStackProps,
|
createAndAddMongoIndexStackProps,
|
||||||
separatorStyles,
|
separatorStyles,
|
||||||
mongoIndexingPolicyAADError,
|
|
||||||
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 {
|
||||||
@@ -40,7 +37,6 @@ import {
|
|||||||
} from "../../SettingsUtils";
|
} from "../../SettingsUtils";
|
||||||
import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
|
import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
|
||||||
import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent";
|
import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent";
|
||||||
import { AuthType } from "../../../../../AuthType";
|
|
||||||
import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
||||||
|
|
||||||
export interface MongoIndexingPolicyComponentProps {
|
export interface MongoIndexingPolicyComponentProps {
|
||||||
@@ -142,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
|
||||||
@@ -255,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()}
|
||||||
@@ -281,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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -321,7 +313,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
|||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return window.authType !== AuthType.AAD ? mongoIndexingPolicyAADError : <Spinner size={SpinnerSize.large} />;
|
return <Spinner size={SpinnerSize.large} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ describe("ScaleComponent", () => {
|
|||||||
} as DataModels.Notification
|
} as DataModels.Notification
|
||||||
};
|
};
|
||||||
|
|
||||||
it("renders with correct intiial notification", () => {
|
it("renders with correct initial notification", () => {
|
||||||
let wrapper = shallow(<ScaleComponent {...baseProps} />);
|
let wrapper = shallow(<ScaleComponent {...baseProps} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
|
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
|
||||||
@@ -54,16 +54,13 @@ describe("ScaleComponent", () => {
|
|||||||
|
|
||||||
const newCollection = { ...collection };
|
const newCollection = { ...collection };
|
||||||
const maxThroughput = 5000;
|
const maxThroughput = 5000;
|
||||||
const targetMaxThroughput = 50000;
|
|
||||||
newCollection.offer = ko.observable({
|
newCollection.offer = ko.observable({
|
||||||
content: {
|
manualThroughput: undefined,
|
||||||
offerAutopilotSettings: {
|
autoscaleMaxThroughput: maxThroughput,
|
||||||
maxThroughput: maxThroughput,
|
minimumThroughput: 400,
|
||||||
targetMaxThroughput: targetMaxThroughput
|
id: "offer",
|
||||||
}
|
offerReplacePending: true
|
||||||
},
|
});
|
||||||
headers: { "x-ms-offer-replace-pending": true }
|
|
||||||
} as DataModels.OfferWithHeaders);
|
|
||||||
const newProps = {
|
const newProps = {
|
||||||
...baseProps,
|
...baseProps,
|
||||||
initialNotification: undefined as DataModels.Notification,
|
initialNotification: undefined as DataModels.Notification,
|
||||||
@@ -73,7 +70,6 @@ describe("ScaleComponent", () => {
|
|||||||
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true);
|
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true);
|
||||||
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(false);
|
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(false);
|
||||||
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(maxThroughput);
|
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(maxThroughput);
|
||||||
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(targetMaxThroughput);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("autoScale disabled", () => {
|
it("autoScale disabled", () => {
|
||||||
@@ -109,11 +105,6 @@ describe("ScaleComponent", () => {
|
|||||||
expect(scaleComponent.isAutoScaleEnabled()).toEqual(true);
|
expect(scaleComponent.isAutoScaleEnabled()).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getMaxRUThroughputInputLimit", () => {
|
|
||||||
const scaleComponent = new ScaleComponent(baseProps);
|
|
||||||
expect(scaleComponent.getMaxRUThroughputInputLimit()).toEqual(40000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getThroughputTitle", () => {
|
it("getThroughputTitle", () => {
|
||||||
let scaleComponent = new ScaleComponent(baseProps);
|
let scaleComponent = new ScaleComponent(baseProps);
|
||||||
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
|
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
|
||||||
@@ -138,14 +129,8 @@ describe("ScaleComponent", () => {
|
|||||||
|
|
||||||
it("getThroughputWarningMessage", () => {
|
it("getThroughputWarningMessage", () => {
|
||||||
const throughputBeyondLimit = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million + 1000;
|
const throughputBeyondLimit = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million + 1000;
|
||||||
const throughputBeyondMaxRus = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million - 1000;
|
|
||||||
|
|
||||||
const newProps = { ...baseProps, container: nonNationalCloudContainer, throughput: throughputBeyondLimit };
|
const newProps = { ...baseProps, container: nonNationalCloudContainer, throughput: throughputBeyondLimit };
|
||||||
let scaleComponent = new ScaleComponent(newProps);
|
const scaleComponent = new ScaleComponent(newProps);
|
||||||
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputBeyondLimitWarningMessage");
|
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputBeyondLimitWarningMessage");
|
||||||
|
|
||||||
newProps.throughput = throughputBeyondMaxRus;
|
|
||||||
scaleComponent = new ScaleComponent(newProps);
|
|
||||||
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputDelayedApplyWarningMessage");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ import {
|
|||||||
throughputUnit,
|
throughputUnit,
|
||||||
getThroughputApplyLongDelayMessage,
|
getThroughputApplyLongDelayMessage,
|
||||||
getThroughputApplyShortDelayMessage,
|
getThroughputApplyShortDelayMessage,
|
||||||
updateThroughputBeyondLimitWarningMessage,
|
updateThroughputBeyondLimitWarningMessage
|
||||||
updateThroughputDelayedApplyWarningMessage
|
|
||||||
} from "../SettingsRenderUtils";
|
} from "../SettingsRenderUtils";
|
||||||
import { getMaxRUs, getMinRUs, hasDatabaseSharedThroughput } from "../SettingsUtils";
|
import { hasDatabaseSharedThroughput } from "../SettingsUtils";
|
||||||
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
|
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
|
||||||
import { Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
|
import { Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
|
||||||
import { configContext, Platform } from "../../../../ConfigContext";
|
import { configContext, Platform } from "../../../../ConfigContext";
|
||||||
@@ -62,11 +61,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private getStorageCapacityTitle = (): JSX.Element => {
|
private getStorageCapacityTitle = (): JSX.Element => {
|
||||||
// Mongo container with system partition key still treat as "Fixed"
|
const capacity: string = this.props.isFixedContainer ? "Fixed" : "Unlimited";
|
||||||
const isFixed =
|
|
||||||
!this.props.collection.partitionKey ||
|
|
||||||
(this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey);
|
|
||||||
const capacity: string = isFixed ? "Fixed" : "Unlimited";
|
|
||||||
return (
|
return (
|
||||||
<Stack {...titleAndInputStackProps}>
|
<Stack {...titleAndInputStackProps}>
|
||||||
<Label>Storage capacity</Label>
|
<Label>Storage capacity</Label>
|
||||||
@@ -75,12 +70,26 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
public getMaxRUThroughputInputLimit = (): number => {
|
public getMaxRUs = (): number => {
|
||||||
if (configContext.platform === Platform.Hosted && this.props.collection.partitionKey) {
|
if (this.props.container?.isTryCosmosDBSubscription()) {
|
||||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
return Constants.TryCosmosExperience.maxRU;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getMaxRUs(this.props.collection, this.props.container);
|
if (this.props.isFixedContainer) {
|
||||||
|
return SharedConstants.CollectionCreation.MaxRUPerPartition;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||||
|
};
|
||||||
|
|
||||||
|
public getMinRUs = (): number => {
|
||||||
|
if (this.props.container?.isTryCosmosDBSubscription()) {
|
||||||
|
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
this.props.collection.offer()?.minimumThroughput || SharedConstants.CollectionCreation.DefaultCollectionRUs400
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
public getThroughputTitle = (): string => {
|
public getThroughputTitle = (): string => {
|
||||||
@@ -88,11 +97,8 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
return AutoPilotUtils.getAutoPilotHeaderText();
|
return AutoPilotUtils.getAutoPilotHeaderText();
|
||||||
}
|
}
|
||||||
|
|
||||||
const minThroughput: string = getMinRUs(this.props.collection, this.props.container).toLocaleString();
|
const minThroughput: string = this.getMinRUs().toLocaleString();
|
||||||
const maxThroughput: string =
|
const maxThroughput: string = !this.props.isFixedContainer ? "unlimited" : this.getMaxRUs().toLocaleString();
|
||||||
this.canThroughputExceedMaximumValue() && !this.props.isFixedContainer
|
|
||||||
? "unlimited"
|
|
||||||
: getMaxRUs(this.props.collection, this.props.container).toLocaleString();
|
|
||||||
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`;
|
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,26 +115,15 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
return this.getLongDelayMessage();
|
return this.getLongDelayMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
const offer = this.props.collection?.offer && this.props.collection.offer();
|
const offer = this.props.collection?.offer();
|
||||||
if (
|
if (offer?.offerReplacePending) {
|
||||||
offer &&
|
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
|
||||||
Object.keys(offer).find(value => {
|
|
||||||
return value === "headers";
|
|
||||||
}) &&
|
|
||||||
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
|
||||||
) {
|
|
||||||
const throughput = offer?.content?.offerAutopilotSettings?.maxThroughput;
|
|
||||||
|
|
||||||
const targetThroughput =
|
|
||||||
offer.content?.offerAutopilotSettings?.targetMaxThroughput || offer?.content?.offerThroughput;
|
|
||||||
|
|
||||||
return getThroughputApplyShortDelayMessage(
|
return getThroughputApplyShortDelayMessage(
|
||||||
this.props.isAutoPilotSelected,
|
this.props.isAutoPilotSelected,
|
||||||
throughput,
|
throughput,
|
||||||
throughputUnit,
|
throughputUnit,
|
||||||
this.props.collection.databaseId,
|
this.props.collection.databaseId,
|
||||||
this.props.collection.id(),
|
this.props.collection.id()
|
||||||
targetThroughput
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,21 +133,12 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
public getThroughputWarningMessage = (): JSX.Element => {
|
public getThroughputWarningMessage = (): JSX.Element => {
|
||||||
const throughputExceedsBackendLimits: boolean =
|
const throughputExceedsBackendLimits: boolean =
|
||||||
this.canThroughputExceedMaximumValue() &&
|
this.canThroughputExceedMaximumValue() &&
|
||||||
getMaxRUs(this.props.collection, this.props.container) <=
|
|
||||||
SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
|
||||||
this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||||
|
|
||||||
if (throughputExceedsBackendLimits && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
|
if (throughputExceedsBackendLimits && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
|
||||||
return updateThroughputBeyondLimitWarningMessage;
|
return updateThroughputBeyondLimitWarningMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
const throughputExceedsMaxValue: boolean =
|
|
||||||
!this.isEmulator && this.props.throughput > getMaxRUs(this.props.collection, this.props.container);
|
|
||||||
|
|
||||||
if (throughputExceedsMaxValue && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
|
|
||||||
return updateThroughputDelayedApplyWarningMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -183,8 +169,8 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
throughput={this.props.throughput}
|
throughput={this.props.throughput}
|
||||||
throughputBaseline={this.props.throughputBaseline}
|
throughputBaseline={this.props.throughputBaseline}
|
||||||
onThroughputChange={this.props.onThroughputChange}
|
onThroughputChange={this.props.onThroughputChange}
|
||||||
minimum={getMinRUs(this.props.collection, this.props.container)}
|
minimum={this.getMinRUs()}
|
||||||
maximum={this.getMaxRUThroughputInputLimit()}
|
maximum={this.getMaxRUs()}
|
||||||
isEnabled={!hasDatabaseSharedThroughput(this.props.collection)}
|
isEnabled={!hasDatabaseSharedThroughput(this.props.collection)}
|
||||||
canExceedMaximumValue={this.canThroughputExceedMaximumValue()}
|
canExceedMaximumValue={this.canThroughputExceedMaximumValue()}
|
||||||
label={this.getThroughputTitle()}
|
label={this.getThroughputTitle()}
|
||||||
@@ -200,6 +186,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
onScaleSaveableChange={this.props.onScaleSaveableChange}
|
onScaleSaveableChange={this.props.onScaleSaveableChange}
|
||||||
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
|
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
|
||||||
getThroughputWarningMessage={this.getThroughputWarningMessage}
|
getThroughputWarningMessage={this.getThroughputWarningMessage}
|
||||||
|
usageSizeInKB={this.props.collection.usageSizeInKB()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ describe("ThroughputInputAutoPilotV3Component", () => {
|
|||||||
minimum: 10000,
|
minimum: 10000,
|
||||||
maximum: 400,
|
maximum: 400,
|
||||||
step: 100,
|
step: 100,
|
||||||
|
usageSizeInKB: 10000,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
isEmulator: false,
|
isEmulator: false,
|
||||||
spendAckChecked: false,
|
spendAckChecked: false,
|
||||||
@@ -53,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", () => {
|
||||||
@@ -71,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", () => {
|
||||||
|
|||||||
@@ -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,13 +28,19 @@ 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";
|
||||||
import * as SharedConstants from "../../../../../Shared/Constants";
|
import * as SharedConstants from "../../../../../Shared/Constants";
|
||||||
import * as DataModels from "../../../../../Contracts/DataModels";
|
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 { SubscriptionType } from "../../../../../Contracts/SubscriptionType";
|
||||||
|
import { usageInGB, calculateEstimateNumber } from "../../../../../Utils/PricingUtils";
|
||||||
|
import { Features } from "../../../../../Common/Constants";
|
||||||
|
|
||||||
export interface ThroughputInputAutoPilotV3Props {
|
export interface ThroughputInputAutoPilotV3Props {
|
||||||
databaseAccount: DataModels.DatabaseAccount;
|
databaseAccount: DataModels.DatabaseAccount;
|
||||||
@@ -60,6 +71,7 @@ export interface ThroughputInputAutoPilotV3Props {
|
|||||||
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
||||||
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
||||||
getThroughputWarningMessage: () => JSX.Element;
|
getThroughputWarningMessage: () => JSX.Element;
|
||||||
|
usageSizeInKB: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ThroughputInputAutoPilotV3State {
|
interface ThroughputInputAutoPilotV3State {
|
||||||
@@ -160,34 +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,
|
||||||
false
|
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 <></>;
|
||||||
@@ -224,6 +445,29 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
option?: IChoiceGroupOption
|
option?: IChoiceGroupOption
|
||||||
): void => this.props.onAutoPilotSelected(option.key === "true");
|
): void => this.props.onAutoPilotSelected(option.key === "true");
|
||||||
|
|
||||||
|
private minRUperGBSurvey = (): JSX.Element => {
|
||||||
|
const href = `https://ncv.microsoft.com/vRBTO37jmO?ctx={"AzureSubscriptionId":"${userContext.subscriptionId}","CosmosDBAccountName":"${userContext.databaseAccount?.name}"}`;
|
||||||
|
const oneTBinKB = 1000000000;
|
||||||
|
const minRUperGB = 10;
|
||||||
|
const featureFlagEnabled = window.dataExplorer?.isFeatureEnabled(Features.showMinRUSurvey);
|
||||||
|
const collectionIsEligible =
|
||||||
|
userContext.subscriptionType !== SubscriptionType.Internal &&
|
||||||
|
this.props.usageSizeInKB > oneTBinKB &&
|
||||||
|
this.props.minimum >= usageInGB(this.props.usageSizeInKB) * minRUperGB;
|
||||||
|
if (featureFlagEnabled || collectionIsEligible) {
|
||||||
|
return (
|
||||||
|
<Text>
|
||||||
|
Need to scale below {this.props.minimum} RU/s? Reach out by filling{" "}
|
||||||
|
<a target="_blank" rel="noreferrer" href={href}>
|
||||||
|
this questionnaire
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
private renderThroughputModeChoices = (): JSX.Element => {
|
private renderThroughputModeChoices = (): JSX.Element => {
|
||||||
const labelId = "settingsV2RadioButtonLabelId";
|
const labelId = "settingsV2RadioButtonLabelId";
|
||||||
return (
|
return (
|
||||||
@@ -275,6 +519,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
onChange={this.onAutoPilotThroughputChange}
|
onChange={this.onAutoPilotThroughputChange}
|
||||||
/>
|
/>
|
||||||
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
|
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
|
||||||
|
{this.minRUperGBSurvey()}
|
||||||
{this.props.spendAckVisible && (
|
{this.props.spendAckVisible && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="spendAckCheckBox"
|
id="spendAckCheckBox"
|
||||||
@@ -290,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"
|
||||||
@@ -305,15 +556,13 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
}
|
}
|
||||||
onChange={this.onThroughputChange}
|
onChange={this.onThroughputChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{this.props.getThroughputWarningMessage() && (
|
{this.props.getThroughputWarningMessage() && (
|
||||||
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
|
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
|
||||||
{this.props.getThroughputWarningMessage()}
|
{this.props.getThroughputWarningMessage()}
|
||||||
</MessageBar>
|
</MessageBar>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!this.props.isEmulator && this.getRequestUnitsUsageCost()}
|
{!this.props.isEmulator && this.getRequestUnitsUsageCost()}
|
||||||
|
{this.minRUperGBSurvey()}
|
||||||
{this.props.spendAckVisible && (
|
{this.props.spendAckVisible && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="spendAckCheckBox"
|
id="spendAckCheckBox"
|
||||||
@@ -323,14 +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()}
|
||||||
|
|||||||
@@ -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>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`ScaleComponent renders with correct intiial notification 1`] = `
|
exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||||
<Stack
|
<Stack
|
||||||
tokens={
|
tokens={
|
||||||
Object {
|
Object {
|
||||||
@@ -48,7 +48,7 @@ exports[`ScaleComponent renders with correct intiial notification 1`] = `
|
|||||||
label="Throughput (6,000 - unlimited RU/s)"
|
label="Throughput (6,000 - unlimited RU/s)"
|
||||||
maxAutoPilotThroughput={4000}
|
maxAutoPilotThroughput={4000}
|
||||||
maxAutoPilotThroughputBaseline={4000}
|
maxAutoPilotThroughputBaseline={4000}
|
||||||
maximum={40000}
|
maximum={1000000}
|
||||||
minimum={6000}
|
minimum={6000}
|
||||||
onAutoPilotSelected={[Function]}
|
onAutoPilotSelected={[Function]}
|
||||||
onMaxAutoPilotThroughputChange={[Function]}
|
onMaxAutoPilotThroughputChange={[Function]}
|
||||||
@@ -58,6 +58,7 @@ exports[`ScaleComponent renders with correct intiial notification 1`] = `
|
|||||||
spendAckChecked={false}
|
spendAckChecked={false}
|
||||||
throughput={1000}
|
throughput={1000}
|
||||||
throughputBaseline={1000}
|
throughputBaseline={1000}
|
||||||
|
usageSizeInKB={100}
|
||||||
wasAutopilotOriginallySet={true}
|
wasAutopilotOriginallySet={true}
|
||||||
/>
|
/>
|
||||||
<Stack
|
<Stack
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { collection, container } from "./TestUtils";
|
import { collection } from "./TestUtils";
|
||||||
import {
|
import {
|
||||||
getMaxRUs,
|
|
||||||
getMinRUs,
|
|
||||||
getMongoIndexType,
|
getMongoIndexType,
|
||||||
getMongoNotification,
|
getMongoNotification,
|
||||||
getSanitizedInputValue,
|
getSanitizedInputValue,
|
||||||
@@ -23,16 +21,6 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
|||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
|
|
||||||
describe("SettingsUtils", () => {
|
describe("SettingsUtils", () => {
|
||||||
it("getMaxRUs", () => {
|
|
||||||
expect(collection.offer().content.collectionThroughputInfo.numPhysicalPartitions).toEqual(4);
|
|
||||||
expect(getMaxRUs(collection, container)).toEqual(40000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getMinRUs", () => {
|
|
||||||
expect(collection.offer().content.collectionThroughputInfo.numPhysicalPartitions).toEqual(4);
|
|
||||||
expect(getMinRUs(collection, container)).toEqual(6000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("hasDatabaseSharedThroughput", () => {
|
it("hasDatabaseSharedThroughput", () => {
|
||||||
expect(hasDatabaseSharedThroughput(collection)).toEqual(undefined);
|
expect(hasDatabaseSharedThroughput(collection)).toEqual(undefined);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import * as SharedConstants from "../../../Shared/Constants";
|
|
||||||
import * as PricingUtils from "../../../Utils/PricingUtils";
|
|
||||||
|
|
||||||
import Explorer from "../../Explorer";
|
|
||||||
import { MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
|
import { MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
|
||||||
|
|
||||||
const zeroValue = 0;
|
const zeroValue = 0;
|
||||||
@@ -71,57 +67,6 @@ export const hasDatabaseSharedThroughput = (collection: ViewModels.Collection):
|
|||||||
return database?.isDatabaseShared() && !collection.offer();
|
return database?.isDatabaseShared() && !collection.offer();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMaxRUs = (collection: ViewModels.Collection, container: Explorer): number => {
|
|
||||||
const isTryCosmosDBSubscription = container?.isTryCosmosDBSubscription() || false;
|
|
||||||
if (isTryCosmosDBSubscription) {
|
|
||||||
return Constants.TryCosmosExperience.maxRU;
|
|
||||||
}
|
|
||||||
|
|
||||||
const numPartitionsFromOffer: number =
|
|
||||||
collection?.offer && collection.offer()?.content?.collectionThroughputInfo?.numPhysicalPartitions;
|
|
||||||
|
|
||||||
const numPartitionsFromQuotaInfo: number = collection?.quotaInfo()?.numPartitions;
|
|
||||||
|
|
||||||
const numPartitions = numPartitionsFromOffer ?? numPartitionsFromQuotaInfo ?? 1;
|
|
||||||
|
|
||||||
return SharedConstants.CollectionCreation.MaxRUPerPartition * numPartitions;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getMinRUs = (collection: ViewModels.Collection, container: Explorer): number => {
|
|
||||||
const isTryCosmosDBSubscription = container?.isTryCosmosDBSubscription();
|
|
||||||
if (isTryCosmosDBSubscription) {
|
|
||||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
|
||||||
}
|
|
||||||
|
|
||||||
const offerContent = collection?.offer && collection.offer()?.content;
|
|
||||||
|
|
||||||
if (offerContent?.offerAutopilotSettings) {
|
|
||||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
|
||||||
}
|
|
||||||
|
|
||||||
const collectionThroughputInfo: DataModels.OfferThroughputInfo = offerContent?.collectionThroughputInfo;
|
|
||||||
|
|
||||||
if (collectionThroughputInfo?.minimumRUForCollection > 0) {
|
|
||||||
return collectionThroughputInfo.minimumRUForCollection;
|
|
||||||
}
|
|
||||||
|
|
||||||
const numPartitions = collectionThroughputInfo?.numPhysicalPartitions ?? collection.quotaInfo()?.numPartitions;
|
|
||||||
|
|
||||||
if (!numPartitions || numPartitions === 1) {
|
|
||||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseRU = SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
|
||||||
|
|
||||||
const quotaInKb = collection.quotaInfo().collectionSize;
|
|
||||||
const quotaInGb = PricingUtils.usageInGB(quotaInKb);
|
|
||||||
|
|
||||||
const perPartitionGBQuota: number = Math.max(10, quotaInGb / numPartitions);
|
|
||||||
const baseRUbyPartitions: number = ((numPartitions * perPartitionGBQuota) / 10) * 100;
|
|
||||||
|
|
||||||
return Math.max(baseRU, baseRUbyPartitions);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const parseConflictResolutionMode = (modeFromBackend: string): DataModels.ConflictResolutionMode => {
|
export const parseConflictResolutionMode = (modeFromBackend: string): DataModels.ConflictResolutionMode => {
|
||||||
// Backend can contain different casing as it does case-insensitive comparisson
|
// Backend can contain different casing as it does case-insensitive comparisson
|
||||||
if (!modeFromBackend) {
|
if (!modeFromBackend) {
|
||||||
|
|||||||
@@ -18,17 +18,14 @@ export const collection = ({
|
|||||||
excludedPaths: []
|
excludedPaths: []
|
||||||
}),
|
}),
|
||||||
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
|
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
|
||||||
quotaInfo: ko.observable<DataModels.CollectionQuotaInfo>({} as DataModels.CollectionQuotaInfo),
|
usageSizeInKB: ko.observable(100),
|
||||||
offer: ko.observable<DataModels.Offer>({
|
offer: ko.observable<DataModels.Offer>({
|
||||||
content: {
|
autoscaleMaxThroughput: undefined,
|
||||||
offerThroughput: 10000,
|
manualThroughput: 10000,
|
||||||
offerIsRUPerMinuteThroughputEnabled: false,
|
minimumThroughput: 6000,
|
||||||
collectionThroughputInfo: {
|
id: "offer",
|
||||||
minimumRUForCollection: 6000,
|
offerReplacePending: false
|
||||||
numPhysicalPartitions: 4
|
}),
|
||||||
} as DataModels.OfferThroughputInfo
|
|
||||||
}
|
|
||||||
} as DataModels.Offer),
|
|
||||||
conflictResolutionPolicy: ko.observable<DataModels.ConflictResolutionPolicy>(
|
conflictResolutionPolicy: ko.observable<DataModels.ConflictResolutionPolicy>(
|
||||||
{} as DataModels.ConflictResolutionPolicy
|
{} as DataModels.ConflictResolutionPolicy
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -133,8 +133,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKeyVisible": [Function],
|
"partitionKeyVisible": [Function],
|
||||||
"requestUnitsUsageCost": [Function],
|
"requestUnitsUsageCost": [Function],
|
||||||
"ruToolTipText": [Function],
|
"ruToolTipText": [Function],
|
||||||
"rupm": [Function],
|
|
||||||
"rupmVisible": [Function],
|
|
||||||
"sharedAutoPilotThroughput": [Function],
|
"sharedAutoPilotThroughput": [Function],
|
||||||
"sharedThroughputRangeText": [Function],
|
"sharedThroughputRangeText": [Function],
|
||||||
"shouldCreateMongoWildcardIndex": [Function],
|
"shouldCreateMongoWildcardIndex": [Function],
|
||||||
@@ -622,8 +620,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKeyVisible": [Function],
|
"partitionKeyVisible": [Function],
|
||||||
"requestUnitsUsageCost": [Function],
|
"requestUnitsUsageCost": [Function],
|
||||||
"ruToolTipText": [Function],
|
"ruToolTipText": [Function],
|
||||||
"rupm": [Function],
|
|
||||||
"rupmVisible": [Function],
|
|
||||||
"sharedAutoPilotThroughput": [Function],
|
"sharedAutoPilotThroughput": [Function],
|
||||||
"sharedThroughputRangeText": [Function],
|
"sharedThroughputRangeText": [Function],
|
||||||
"shouldCreateMongoWildcardIndex": [Function],
|
"shouldCreateMongoWildcardIndex": [Function],
|
||||||
@@ -735,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],
|
||||||
@@ -947,7 +942,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"hasWriteAccess": [Function],
|
"hasWriteAccess": [Function],
|
||||||
"isAccountReady": [Function],
|
"isAccountReady": [Function],
|
||||||
"isAuthWithResourceToken": [Function],
|
"isAuthWithResourceToken": [Function],
|
||||||
"isCodeOfConductEnabled": [Function],
|
|
||||||
"isCopyNotebookPaneEnabled": [Function],
|
"isCopyNotebookPaneEnabled": [Function],
|
||||||
"isEnableMongoCapabilityPresent": [Function],
|
"isEnableMongoCapabilityPresent": [Function],
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
@@ -956,7 +950,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isHostedDataExplorerEnabled": [Function],
|
"isHostedDataExplorerEnabled": [Function],
|
||||||
"isLeftPaneExpanded": [Function],
|
"isLeftPaneExpanded": [Function],
|
||||||
"isLinkInjectionEnabled": [Function],
|
"isLinkInjectionEnabled": [Function],
|
||||||
"isMongoIndexEditorEnabled": [Function],
|
|
||||||
"isNotebookEnabled": [Function],
|
"isNotebookEnabled": [Function],
|
||||||
"isNotebooksEnabledForAccount": [Function],
|
"isNotebooksEnabledForAccount": [Function],
|
||||||
"isNotificationConsoleExpanded": [Function],
|
"isNotificationConsoleExpanded": [Function],
|
||||||
@@ -971,8 +964,8 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isRefreshingExplorer": [Function],
|
"isRefreshingExplorer": [Function],
|
||||||
"isResourceTokenCollectionNodeSelected": [Function],
|
"isResourceTokenCollectionNodeSelected": [Function],
|
||||||
"isRightPanelV2Enabled": [Function],
|
"isRightPanelV2Enabled": [Function],
|
||||||
|
"isSchemaEnabled": [Function],
|
||||||
"isServerlessEnabled": [Function],
|
"isServerlessEnabled": [Function],
|
||||||
"isSettingsV2Enabled": [Function],
|
|
||||||
"isSparkEnabled": [Function],
|
"isSparkEnabled": [Function],
|
||||||
"isSparkEnabledForAccount": [Function],
|
"isSparkEnabledForAccount": [Function],
|
||||||
"isSynapseLinkUpdating": [Function],
|
"isSynapseLinkUpdating": [Function],
|
||||||
@@ -1029,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],
|
||||||
@@ -1055,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],
|
||||||
@@ -1301,9 +1292,9 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"version": 2,
|
"version": 2,
|
||||||
},
|
},
|
||||||
"partitionKeyProperty": "partitionKey",
|
"partitionKeyProperty": "partitionKey",
|
||||||
"quotaInfo": [Function],
|
|
||||||
"readSettings": [Function],
|
"readSettings": [Function],
|
||||||
"uniqueKeyPolicy": Object {},
|
"uniqueKeyPolicy": Object {},
|
||||||
|
"usageSizeInKB": [Function],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
container={
|
container={
|
||||||
@@ -1413,8 +1404,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKeyVisible": [Function],
|
"partitionKeyVisible": [Function],
|
||||||
"requestUnitsUsageCost": [Function],
|
"requestUnitsUsageCost": [Function],
|
||||||
"ruToolTipText": [Function],
|
"ruToolTipText": [Function],
|
||||||
"rupm": [Function],
|
|
||||||
"rupmVisible": [Function],
|
|
||||||
"sharedAutoPilotThroughput": [Function],
|
"sharedAutoPilotThroughput": [Function],
|
||||||
"sharedThroughputRangeText": [Function],
|
"sharedThroughputRangeText": [Function],
|
||||||
"shouldCreateMongoWildcardIndex": [Function],
|
"shouldCreateMongoWildcardIndex": [Function],
|
||||||
@@ -1902,8 +1891,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKeyVisible": [Function],
|
"partitionKeyVisible": [Function],
|
||||||
"requestUnitsUsageCost": [Function],
|
"requestUnitsUsageCost": [Function],
|
||||||
"ruToolTipText": [Function],
|
"ruToolTipText": [Function],
|
||||||
"rupm": [Function],
|
|
||||||
"rupmVisible": [Function],
|
|
||||||
"sharedAutoPilotThroughput": [Function],
|
"sharedAutoPilotThroughput": [Function],
|
||||||
"sharedThroughputRangeText": [Function],
|
"sharedThroughputRangeText": [Function],
|
||||||
"shouldCreateMongoWildcardIndex": [Function],
|
"shouldCreateMongoWildcardIndex": [Function],
|
||||||
@@ -2015,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],
|
||||||
@@ -2227,7 +2213,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"hasWriteAccess": [Function],
|
"hasWriteAccess": [Function],
|
||||||
"isAccountReady": [Function],
|
"isAccountReady": [Function],
|
||||||
"isAuthWithResourceToken": [Function],
|
"isAuthWithResourceToken": [Function],
|
||||||
"isCodeOfConductEnabled": [Function],
|
|
||||||
"isCopyNotebookPaneEnabled": [Function],
|
"isCopyNotebookPaneEnabled": [Function],
|
||||||
"isEnableMongoCapabilityPresent": [Function],
|
"isEnableMongoCapabilityPresent": [Function],
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
@@ -2236,7 +2221,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isHostedDataExplorerEnabled": [Function],
|
"isHostedDataExplorerEnabled": [Function],
|
||||||
"isLeftPaneExpanded": [Function],
|
"isLeftPaneExpanded": [Function],
|
||||||
"isLinkInjectionEnabled": [Function],
|
"isLinkInjectionEnabled": [Function],
|
||||||
"isMongoIndexEditorEnabled": [Function],
|
|
||||||
"isNotebookEnabled": [Function],
|
"isNotebookEnabled": [Function],
|
||||||
"isNotebooksEnabledForAccount": [Function],
|
"isNotebooksEnabledForAccount": [Function],
|
||||||
"isNotificationConsoleExpanded": [Function],
|
"isNotificationConsoleExpanded": [Function],
|
||||||
@@ -2251,8 +2235,8 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isRefreshingExplorer": [Function],
|
"isRefreshingExplorer": [Function],
|
||||||
"isResourceTokenCollectionNodeSelected": [Function],
|
"isResourceTokenCollectionNodeSelected": [Function],
|
||||||
"isRightPanelV2Enabled": [Function],
|
"isRightPanelV2Enabled": [Function],
|
||||||
|
"isSchemaEnabled": [Function],
|
||||||
"isServerlessEnabled": [Function],
|
"isServerlessEnabled": [Function],
|
||||||
"isSettingsV2Enabled": [Function],
|
|
||||||
"isSparkEnabled": [Function],
|
"isSparkEnabled": [Function],
|
||||||
"isSparkEnabledForAccount": [Function],
|
"isSparkEnabledForAccount": [Function],
|
||||||
"isSynapseLinkUpdating": [Function],
|
"isSynapseLinkUpdating": [Function],
|
||||||
@@ -2309,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],
|
||||||
@@ -2335,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],
|
||||||
@@ -2706,8 +2688,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKeyVisible": [Function],
|
"partitionKeyVisible": [Function],
|
||||||
"requestUnitsUsageCost": [Function],
|
"requestUnitsUsageCost": [Function],
|
||||||
"ruToolTipText": [Function],
|
"ruToolTipText": [Function],
|
||||||
"rupm": [Function],
|
|
||||||
"rupmVisible": [Function],
|
|
||||||
"sharedAutoPilotThroughput": [Function],
|
"sharedAutoPilotThroughput": [Function],
|
||||||
"sharedThroughputRangeText": [Function],
|
"sharedThroughputRangeText": [Function],
|
||||||
"shouldCreateMongoWildcardIndex": [Function],
|
"shouldCreateMongoWildcardIndex": [Function],
|
||||||
@@ -3195,8 +3175,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKeyVisible": [Function],
|
"partitionKeyVisible": [Function],
|
||||||
"requestUnitsUsageCost": [Function],
|
"requestUnitsUsageCost": [Function],
|
||||||
"ruToolTipText": [Function],
|
"ruToolTipText": [Function],
|
||||||
"rupm": [Function],
|
|
||||||
"rupmVisible": [Function],
|
|
||||||
"sharedAutoPilotThroughput": [Function],
|
"sharedAutoPilotThroughput": [Function],
|
||||||
"sharedThroughputRangeText": [Function],
|
"sharedThroughputRangeText": [Function],
|
||||||
"shouldCreateMongoWildcardIndex": [Function],
|
"shouldCreateMongoWildcardIndex": [Function],
|
||||||
@@ -3308,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],
|
||||||
@@ -3520,7 +3497,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"hasWriteAccess": [Function],
|
"hasWriteAccess": [Function],
|
||||||
"isAccountReady": [Function],
|
"isAccountReady": [Function],
|
||||||
"isAuthWithResourceToken": [Function],
|
"isAuthWithResourceToken": [Function],
|
||||||
"isCodeOfConductEnabled": [Function],
|
|
||||||
"isCopyNotebookPaneEnabled": [Function],
|
"isCopyNotebookPaneEnabled": [Function],
|
||||||
"isEnableMongoCapabilityPresent": [Function],
|
"isEnableMongoCapabilityPresent": [Function],
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
@@ -3529,7 +3505,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isHostedDataExplorerEnabled": [Function],
|
"isHostedDataExplorerEnabled": [Function],
|
||||||
"isLeftPaneExpanded": [Function],
|
"isLeftPaneExpanded": [Function],
|
||||||
"isLinkInjectionEnabled": [Function],
|
"isLinkInjectionEnabled": [Function],
|
||||||
"isMongoIndexEditorEnabled": [Function],
|
|
||||||
"isNotebookEnabled": [Function],
|
"isNotebookEnabled": [Function],
|
||||||
"isNotebooksEnabledForAccount": [Function],
|
"isNotebooksEnabledForAccount": [Function],
|
||||||
"isNotificationConsoleExpanded": [Function],
|
"isNotificationConsoleExpanded": [Function],
|
||||||
@@ -3544,8 +3519,8 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isRefreshingExplorer": [Function],
|
"isRefreshingExplorer": [Function],
|
||||||
"isResourceTokenCollectionNodeSelected": [Function],
|
"isResourceTokenCollectionNodeSelected": [Function],
|
||||||
"isRightPanelV2Enabled": [Function],
|
"isRightPanelV2Enabled": [Function],
|
||||||
|
"isSchemaEnabled": [Function],
|
||||||
"isServerlessEnabled": [Function],
|
"isServerlessEnabled": [Function],
|
||||||
"isSettingsV2Enabled": [Function],
|
|
||||||
"isSparkEnabled": [Function],
|
"isSparkEnabled": [Function],
|
||||||
"isSparkEnabledForAccount": [Function],
|
"isSparkEnabledForAccount": [Function],
|
||||||
"isSynapseLinkUpdating": [Function],
|
"isSynapseLinkUpdating": [Function],
|
||||||
@@ -3602,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],
|
||||||
@@ -3628,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],
|
||||||
@@ -3874,9 +3847,9 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"version": 2,
|
"version": 2,
|
||||||
},
|
},
|
||||||
"partitionKeyProperty": "partitionKey",
|
"partitionKeyProperty": "partitionKey",
|
||||||
"quotaInfo": [Function],
|
|
||||||
"readSettings": [Function],
|
"readSettings": [Function],
|
||||||
"uniqueKeyPolicy": Object {},
|
"uniqueKeyPolicy": Object {},
|
||||||
|
"usageSizeInKB": [Function],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
container={
|
container={
|
||||||
@@ -3986,8 +3959,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKeyVisible": [Function],
|
"partitionKeyVisible": [Function],
|
||||||
"requestUnitsUsageCost": [Function],
|
"requestUnitsUsageCost": [Function],
|
||||||
"ruToolTipText": [Function],
|
"ruToolTipText": [Function],
|
||||||
"rupm": [Function],
|
|
||||||
"rupmVisible": [Function],
|
|
||||||
"sharedAutoPilotThroughput": [Function],
|
"sharedAutoPilotThroughput": [Function],
|
||||||
"sharedThroughputRangeText": [Function],
|
"sharedThroughputRangeText": [Function],
|
||||||
"shouldCreateMongoWildcardIndex": [Function],
|
"shouldCreateMongoWildcardIndex": [Function],
|
||||||
@@ -4475,8 +4446,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKeyVisible": [Function],
|
"partitionKeyVisible": [Function],
|
||||||
"requestUnitsUsageCost": [Function],
|
"requestUnitsUsageCost": [Function],
|
||||||
"ruToolTipText": [Function],
|
"ruToolTipText": [Function],
|
||||||
"rupm": [Function],
|
|
||||||
"rupmVisible": [Function],
|
|
||||||
"sharedAutoPilotThroughput": [Function],
|
"sharedAutoPilotThroughput": [Function],
|
||||||
"sharedThroughputRangeText": [Function],
|
"sharedThroughputRangeText": [Function],
|
||||||
"shouldCreateMongoWildcardIndex": [Function],
|
"shouldCreateMongoWildcardIndex": [Function],
|
||||||
@@ -4588,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],
|
||||||
@@ -4800,7 +4768,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"hasWriteAccess": [Function],
|
"hasWriteAccess": [Function],
|
||||||
"isAccountReady": [Function],
|
"isAccountReady": [Function],
|
||||||
"isAuthWithResourceToken": [Function],
|
"isAuthWithResourceToken": [Function],
|
||||||
"isCodeOfConductEnabled": [Function],
|
|
||||||
"isCopyNotebookPaneEnabled": [Function],
|
"isCopyNotebookPaneEnabled": [Function],
|
||||||
"isEnableMongoCapabilityPresent": [Function],
|
"isEnableMongoCapabilityPresent": [Function],
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
@@ -4809,7 +4776,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isHostedDataExplorerEnabled": [Function],
|
"isHostedDataExplorerEnabled": [Function],
|
||||||
"isLeftPaneExpanded": [Function],
|
"isLeftPaneExpanded": [Function],
|
||||||
"isLinkInjectionEnabled": [Function],
|
"isLinkInjectionEnabled": [Function],
|
||||||
"isMongoIndexEditorEnabled": [Function],
|
|
||||||
"isNotebookEnabled": [Function],
|
"isNotebookEnabled": [Function],
|
||||||
"isNotebooksEnabledForAccount": [Function],
|
"isNotebooksEnabledForAccount": [Function],
|
||||||
"isNotificationConsoleExpanded": [Function],
|
"isNotificationConsoleExpanded": [Function],
|
||||||
@@ -4824,8 +4790,8 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isRefreshingExplorer": [Function],
|
"isRefreshingExplorer": [Function],
|
||||||
"isResourceTokenCollectionNodeSelected": [Function],
|
"isResourceTokenCollectionNodeSelected": [Function],
|
||||||
"isRightPanelV2Enabled": [Function],
|
"isRightPanelV2Enabled": [Function],
|
||||||
|
"isSchemaEnabled": [Function],
|
||||||
"isServerlessEnabled": [Function],
|
"isServerlessEnabled": [Function],
|
||||||
"isSettingsV2Enabled": [Function],
|
|
||||||
"isSparkEnabled": [Function],
|
"isSparkEnabled": [Function],
|
||||||
"isSparkEnabledForAccount": [Function],
|
"isSparkEnabledForAccount": [Function],
|
||||||
"isSynapseLinkUpdating": [Function],
|
"isSynapseLinkUpdating": [Function],
|
||||||
@@ -4882,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],
|
||||||
@@ -4908,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],
|
||||||
|
|||||||
@@ -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.29
|
"key": "costType",
|
||||||
hourly
|
"maxWidth": 200,
|
||||||
/
|
"minWidth": 100,
|
||||||
¥
|
"name": "",
|
||||||
31.06
|
},
|
||||||
daily
|
Object {
|
||||||
/
|
"fieldName": "hourly",
|
||||||
¥
|
"isResizable": true,
|
||||||
944.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={
|
||||||
@@ -227,7 +261,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
, Container:
|
, Container:
|
||||||
sampleCollection
|
sampleCollection
|
||||||
|
|
||||||
, Current manual throughput: 1000 RU/s, Target manual throughput: 2000
|
, Current manual throughput: 1000 RU/s
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
id="throughputApplyLongDelayMessage"
|
id="throughputApplyLongDelayMessage"
|
||||||
|
|||||||
@@ -126,6 +126,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div data-bind="visible: !isAutoPilotSelected()">
|
<div data-bind="visible: !isAutoPilotSelected()">
|
||||||
|
<p>
|
||||||
|
<span
|
||||||
|
>Estimate your required throughput with
|
||||||
|
<a target="_blank" href="https://cosmos.azure.com/capacitycalculator/">capacity calculator</a></span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
<div data-bind="setTemplateReady: true">
|
<div data-bind="setTemplateReady: true">
|
||||||
<input
|
<input
|
||||||
data-bind="
|
data-bind="
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -87,6 +87,7 @@ import { updateUserContext, userContext } from "../UserContext";
|
|||||||
import { stringToBlob } from "../Utils/BlobUtils";
|
import { stringToBlob } from "../Utils/BlobUtils";
|
||||||
import { IChoiceGroupProps } from "office-ui-fabric-react";
|
import { IChoiceGroupProps } from "office-ui-fabric-react";
|
||||||
import { getErrorMessage, handleError, getErrorStack } from "../Common/ErrorHandlingUtils";
|
import { getErrorMessage, handleError, getErrorStack } from "../Common/ErrorHandlingUtils";
|
||||||
|
import { SubscriptionType } from "../Contracts/SubscriptionType";
|
||||||
|
|
||||||
BindingHandlersRegisterer.registerBindingHandlers();
|
BindingHandlersRegisterer.registerBindingHandlers();
|
||||||
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
||||||
@@ -119,8 +120,7 @@ export default class Explorer {
|
|||||||
|
|
||||||
public databaseAccount: ko.Observable<DataModels.DatabaseAccount>;
|
public databaseAccount: ko.Observable<DataModels.DatabaseAccount>;
|
||||||
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults;
|
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults;
|
||||||
public subscriptionType: ko.Observable<ViewModels.SubscriptionType>;
|
public subscriptionType: ko.Observable<SubscriptionType>;
|
||||||
public quotaId: ko.Observable<string>;
|
|
||||||
public defaultExperience: ko.Observable<string>;
|
public defaultExperience: ko.Observable<string>;
|
||||||
public isPreferredApiDocumentDB: ko.Computed<boolean>;
|
public isPreferredApiDocumentDB: ko.Computed<boolean>;
|
||||||
public isPreferredApiCassandra: ko.Computed<boolean>;
|
public isPreferredApiCassandra: ko.Computed<boolean>;
|
||||||
@@ -134,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
|
||||||
@@ -203,10 +201,7 @@ export default class Explorer {
|
|||||||
|
|
||||||
// features
|
// features
|
||||||
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
||||||
public isCodeOfConductEnabled: ko.Computed<boolean>;
|
|
||||||
public isLinkInjectionEnabled: ko.Computed<boolean>;
|
public isLinkInjectionEnabled: ko.Computed<boolean>;
|
||||||
public isSettingsV2Enabled: ko.Observable<boolean>;
|
|
||||||
public isMongoIndexEditorEnabled: ko.Observable<boolean>;
|
|
||||||
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||||
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
||||||
public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
|
public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
|
||||||
@@ -225,6 +220,7 @@ export default class Explorer {
|
|||||||
public shareTokenCopyHelperText: ko.Observable<string>;
|
public shareTokenCopyHelperText: ko.Observable<string>;
|
||||||
public shouldShowDataAccessExpiryDialog: ko.Observable<boolean>;
|
public shouldShowDataAccessExpiryDialog: ko.Observable<boolean>;
|
||||||
public shouldShowContextSwitchPrompt: ko.Observable<boolean>;
|
public shouldShowContextSwitchPrompt: ko.Observable<boolean>;
|
||||||
|
public isSchemaEnabled: ko.Computed<boolean>;
|
||||||
|
|
||||||
// Notebooks
|
// Notebooks
|
||||||
public isNotebookEnabled: ko.Observable<boolean>;
|
public isNotebookEnabled: ko.Observable<boolean>;
|
||||||
@@ -278,10 +274,7 @@ export default class Explorer {
|
|||||||
this.refreshTreeTitle = ko.observable<string>("Refresh collections");
|
this.refreshTreeTitle = ko.observable<string>("Refresh collections");
|
||||||
|
|
||||||
this.databaseAccount = ko.observable<DataModels.DatabaseAccount>();
|
this.databaseAccount = ko.observable<DataModels.DatabaseAccount>();
|
||||||
this.subscriptionType = ko.observable<ViewModels.SubscriptionType>(
|
this.subscriptionType = ko.observable<SubscriptionType>(SharedConstants.CollectionCreation.DefaultSubscriptionType);
|
||||||
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) => {
|
||||||
@@ -321,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)
|
||||||
);
|
);
|
||||||
@@ -373,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);
|
||||||
|
|
||||||
@@ -406,14 +398,9 @@ export default class Explorer {
|
|||||||
this.isGalleryPublishEnabled = ko.computed<boolean>(() =>
|
this.isGalleryPublishEnabled = ko.computed<boolean>(() =>
|
||||||
this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
|
this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
|
||||||
);
|
);
|
||||||
this.isCodeOfConductEnabled = ko.computed<boolean>(() =>
|
|
||||||
this.isFeatureEnabled(Constants.Features.enableCodeOfConduct)
|
|
||||||
);
|
|
||||||
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
|
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
|
||||||
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
|
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
|
||||||
);
|
);
|
||||||
this.isSettingsV2Enabled = ko.observable(false);
|
|
||||||
this.isMongoIndexEditorEnabled = ko.observable(false);
|
|
||||||
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
|
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
|
||||||
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
|
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||||
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
|
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||||
@@ -422,6 +409,7 @@ export default class Explorer {
|
|||||||
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
|
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.isSchemaEnabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSchema));
|
||||||
this.isNotificationConsoleExpanded = ko.observable<boolean>(false);
|
this.isNotificationConsoleExpanded = ko.observable<boolean>(false);
|
||||||
|
|
||||||
this.databases = ko.observableArray<ViewModels.Database>();
|
this.databases = ko.observableArray<ViewModels.Database>();
|
||||||
@@ -1019,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(
|
||||||
@@ -1733,6 +1719,7 @@ export default class Explorer {
|
|||||||
case MessageTypes.SendNotification:
|
case MessageTypes.SendNotification:
|
||||||
case MessageTypes.ClearNotification:
|
case MessageTypes.ClearNotification:
|
||||||
case MessageTypes.LoadingStatus:
|
case MessageTypes.LoadingStatus:
|
||||||
|
case MessageTypes.InitTestExplorer:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1760,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 {
|
||||||
@@ -1854,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;
|
||||||
@@ -1864,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({
|
||||||
@@ -1890,7 +1874,9 @@ export default class Explorer {
|
|||||||
masterKey,
|
masterKey,
|
||||||
databaseAccount,
|
databaseAccount,
|
||||||
resourceGroup: inputs.resourceGroup,
|
resourceGroup: inputs.resourceGroup,
|
||||||
subscriptionId: inputs.subscriptionId
|
subscriptionId: inputs.subscriptionId,
|
||||||
|
subscriptionType: inputs.subscriptionType,
|
||||||
|
quotaId: inputs.quotaId
|
||||||
});
|
});
|
||||||
TelemetryProcessor.traceSuccess(
|
TelemetryProcessor.traceSuccess(
|
||||||
Action.LoadDatabaseAccount,
|
Action.LoadDatabaseAccount,
|
||||||
@@ -1904,21 +1890,12 @@ export default class Explorer {
|
|||||||
|
|
||||||
this.isAccountReady(true);
|
this.isAccountReady(true);
|
||||||
}
|
}
|
||||||
return Q();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public setFeatureFlagsFromFlights(flights: readonly string[]): void {
|
public setFeatureFlagsFromFlights(flights: readonly string[]): void {
|
||||||
if (!flights) {
|
if (!flights) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flights.indexOf(Constants.Flights.SettingsV2) !== -1) {
|
|
||||||
this.isSettingsV2Enabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flights.indexOf(Constants.Flights.MongoIndexEditor) !== -1) {
|
|
||||||
this.isMongoIndexEditorEnabled(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public findSelectedCollection(): ViewModels.Collection {
|
public findSelectedCollection(): ViewModels.Collection {
|
||||||
@@ -2285,7 +2262,6 @@ export default class Explorer {
|
|||||||
name,
|
name,
|
||||||
content,
|
content,
|
||||||
parentDomElement,
|
parentDomElement,
|
||||||
this.isCodeOfConductEnabled(),
|
|
||||||
this.isLinkInjectionEnabled()
|
this.isLinkInjectionEnabled()
|
||||||
);
|
);
|
||||||
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
|
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
|
||||||
@@ -2576,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
|
||||||
@@ -2585,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,
|
||||||
@@ -2605,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
|
||||||
@@ -2613,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,
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import "./MeControlComponent.less";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { DefaultButton, BaseButton, IButtonProps } from "office-ui-fabric-react/lib/Button";
|
import { DefaultButton, BaseButton, IButtonProps } from "office-ui-fabric-react/lib/Button";
|
||||||
import { DirectionalHint, IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu";
|
import { DirectionalHint, IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu";
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user