mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-28 21:32:05 +00:00
Compare commits
85 Commits
remove-ru-
...
steve-self
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c857b9aab9 | ||
|
|
cd45f2943d | ||
|
|
385c9f216f | ||
|
|
8c40df0fa1 | ||
|
|
d17508cc27 | ||
|
|
2ec2a891b4 | ||
|
|
fcbc9474ea | ||
|
|
b34628e9fc | ||
|
|
3fb4af53c8 | ||
|
|
81f861af39 | ||
|
|
9afa29cdb6 | ||
|
|
9a1e8b2d87 | ||
|
|
41f37055ef | ||
|
|
aa925d8d54 | ||
|
|
c9cea86225 | ||
|
|
318842624f | ||
|
|
babda4d9cb | ||
|
|
9d20a13dd4 | ||
|
|
3effbe6991 | ||
|
|
af53697ff4 | ||
|
|
b1ad80480e | ||
|
|
9247a6c4a2 | ||
|
|
767d46480e | ||
|
|
2d98c5d269 | ||
|
|
6627172a52 | ||
|
|
19fa5e17a5 | ||
|
|
a4a367a212 | ||
|
|
983c9201bb | ||
|
|
76d7f00a90 | ||
|
|
6490597736 | ||
|
|
229119e697 | ||
|
|
ceefd7c615 | ||
|
|
6e619175c6 | ||
|
|
08e8bf4bcf | ||
|
|
89dc0f394b | ||
|
|
30e0001b7f | ||
|
|
4a8f408112 | ||
|
|
e801364800 | ||
|
|
373327dc88 | ||
|
|
8333ee7ec4 | ||
|
|
97116175ab | ||
|
|
a55f2d0de9 | ||
|
|
d40b1aa9b5 | ||
|
|
cc63cdc1fd | ||
|
|
f770bb193e | ||
|
|
8cb8d10bc3 | ||
|
|
b298caf9ff | ||
|
|
a2022fbbac | ||
|
|
c3058ee5a9 | ||
|
|
b000631a0c | ||
|
|
e8f4c8f93c | ||
|
|
16bde97e47 | ||
|
|
6da43ee27b | ||
|
|
ebae484b8f | ||
|
|
dfb1b50621 | ||
|
|
f54e8eb692 | ||
|
|
ea39c1d092 | ||
|
|
c21f42159f | ||
|
|
b3b57462ef | ||
|
|
31e4b49f11 | ||
|
|
40491ec9c5 | ||
|
|
95fc75cb23 | ||
|
|
e133df18dd | ||
|
|
c97eb6018b | ||
|
|
90fb7e7d8f | ||
|
|
2dbde9c31a | ||
|
|
4381ea447c | ||
|
|
0532ed26a2 | ||
|
|
69b17f1a00 | ||
|
|
fd60c9c15e | ||
|
|
04ab1f3918 | ||
|
|
b784ac0f86 | ||
|
|
28899f63d7 | ||
|
|
8cf160d818 | ||
|
|
88d71d7070 | ||
|
|
84017660c1 | ||
|
|
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
|
||||||
@@ -202,8 +201,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 +287,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 +391,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.
53
README.md
53
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)
|
||||||
@@ -98,6 +69,10 @@ Jest and Puppeteer are used for end to end production runners and are contained
|
|||||||
|
|
||||||
We generally adhere to the release strategy [documented by the Azure SDK Guidelines](https://azure.github.io/azure-sdk/policies_repobranching.html#release-branches). Most releases should happen from the master branch. If master contains commits that cannot be released, you may create a release from a `release/` or `hotfix/` branch. See linked documentation for more details.
|
We generally adhere to the release strategy [documented by the Azure SDK Guidelines](https://azure.github.io/azure-sdk/policies_repobranching.html#release-branches). Most releases should happen from the master branch. If master contains commits that cannot be released, you may create a release from a `release/` or `hotfix/` branch. See linked documentation for more details.
|
||||||
|
|
||||||
|
### Architechture
|
||||||
|
|
||||||
|
[](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ3JhcGggTFJcbiAgaG9zdGVkKGh0dHBzOi8vY29zbW9zLmF6dXJlLmNvbSlcbiAgcG9ydGFsKFBvcnRhbClcbiAgZW11bGF0b3IoRW11bGF0b3IpXG4gIGFhZFtBQURdXG4gIHJlc291cmNlVG9rZW5bUmVzb3VyY2UgVG9rZW5dXG4gIGNvbm5lY3Rpb25TdHJpbmdbQ29ubmVjdGlvbiBTdHJpbmddXG4gIHBvcnRhbFRva2VuW0VuY3J5cHRlZCBQb3J0YWwgVG9rZW5dXG4gIG1hc3RlcktleVtNYXN0ZXIgS2V5XVxuICBhcm1bQVJNIFJlc291cmNlIFByb3ZpZGVyXVxuICBkYXRhcGxhbmVbRGF0YSBQbGFuZV1cbiAgcHJveHlbUG9ydGFsIEFQSSBQcm94eV1cbiAgc3FsW1NRTF1cbiAgbW9uZ29bTW9uZ29dXG4gIHRhYmxlc1tUYWJsZXNdXG4gIGNhc3NhbmRyYVtDYXNzYW5kcmFdXG4gIGdyYWZbR3JhcGhdXG5cblxuICBlbXVsYXRvciAtLT4gbWFzdGVyS2V5IC0tLS0-IGRhdGFwbGFuZVxuICBwb3J0YWwgLS0-IGFhZFxuICBob3N0ZWQgLS0-IHBvcnRhbFRva2VuICYgcmVzb3VyY2VUb2tlbiAmIGNvbm5lY3Rpb25TdHJpbmcgJiBhYWRcbiAgYWFkIC0tLT4gYXJtXG4gIGFhZCAtLS0-IGRhdGFwbGFuZVxuICBhYWQgLS0tPiBwcm94eVxuICByZXNvdXJjZVRva2VuIC0tLT4gc3FsIC0tPiBkYXRhcGxhbmVcbiAgcG9ydGFsVG9rZW4gLS0tPiBwcm94eVxuICBwcm94eSAtLT4gZGF0YXBsYW5lXG4gIGNvbm5lY3Rpb25TdHJpbmcgLS0-IHNxbCAmIG1vbmdvICYgY2Fzc2FuZHJhICYgZ3JhZiAmIHRhYmxlc1xuICBzcWwgLS0-IGRhdGFwbGFuZVxuICB0YWJsZXMgLS0-IGRhdGFwbGFuZVxuICBtb25nbyAtLT4gcHJveHlcbiAgY2Fzc2FuZHJhIC0tPiBwcm94eVxuICBncmFmIC0tPiBwcm94eVxuXG5cdFx0IiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQifSwidXBkYXRlRWRpdG9yIjpmYWxzZX0)
|
||||||
|
|
||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
Please read the [contribution guidelines](./CONTRIBUTING.md).
|
Please read the [contribution guidelines](./CONTRIBUTING.md).
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-react", "@babel/preset-typescript"]
|
presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-react", "@babel/preset-typescript"],
|
||||||
|
plugins: [["@babel/plugin-proposal-decorators", { legacy: true }]]
|
||||||
};
|
};
|
||||||
|
|||||||
7
canvas/README.md
Normal file
7
canvas/README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Why?
|
||||||
|
|
||||||
|
This adds a mock module for `canvas`. Nteract has a ignored require and undeclared dependency on this module. `cavnas` is a server side node module and is not used in browser side code for nteract.
|
||||||
|
|
||||||
|
Installing it locally (`npm install canvas`) will resolve the problem, but it is a native module so it is flaky depending on the system, node version, processor arch, etc. This module provides a simpler, more robust solution.
|
||||||
|
|
||||||
|
Remove this workaround if [this bug](https://github.com/nteract/any-vega/issues/2) ever gets resolved
|
||||||
1
canvas/index.js
Normal file
1
canvas/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
module.exports = {}
|
||||||
11
canvas/package.json
Normal file
11
canvas/package.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "canvas",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC"
|
||||||
|
}
|
||||||
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=="
|
|
||||||
});
|
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: wf_segoe-ui_normal;
|
font-family: wf_segoe-ui_normal;
|
||||||
src: url('../../fonts/segoe-ui/west-european/normal/latest.woff');
|
src: url("../../fonts/segoe-ui/west-european/normal/latest.woff");
|
||||||
}
|
}
|
||||||
|
|
||||||
@DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
|
@DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
|
||||||
@@ -20,26 +20,26 @@
|
|||||||
COLORS
|
COLORS
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
@AccentMediumHigh: #0058AD;
|
@AccentMediumHigh: #0058ad;
|
||||||
@AccentMedium: #004E87;
|
@AccentMedium: #004e87;
|
||||||
@AccentHigh: #1EBAED;
|
@AccentHigh: #1ebaed;
|
||||||
@AccentExtraHigh: #55B3FF;
|
@AccentExtraHigh: #55b3ff;
|
||||||
@AccentLow: #EDF6FF;
|
@AccentLow: #edf6ff;
|
||||||
@AccentMediumLow: #DDEEFE;
|
@AccentMediumLow: #ddeefe;
|
||||||
@AccentLight: #EEF7FF;
|
@AccentLight: #eef7ff;
|
||||||
@AccentExtra: #DDF0FF;
|
@AccentExtra: #ddf0ff;
|
||||||
|
|
||||||
@SelectionHigh: #B91F26;
|
@SelectionHigh: #b91f26;
|
||||||
@BaseLight: #FFFFFF;
|
@BaseLight: #ffffff;
|
||||||
@BaseDark: #000000;
|
@BaseDark: #000000;
|
||||||
@NotificationLow: #FFF4CE;
|
@NotificationLow: #fff4ce;
|
||||||
@NotificationHigh: #F9E9B0;
|
@NotificationHigh: #f9e9b0;
|
||||||
@Purple1: #8A2DA5;
|
@Purple1: #8a2da5;
|
||||||
@Dirty: #9b4f96;
|
@Dirty: #9b4f96;
|
||||||
|
|
||||||
@BaseLow: #F2F2F2;
|
@BaseLow: #f2f2f2;
|
||||||
@BaseMediumLow: #E6E6E6;
|
@BaseMediumLow: #e6e6e6;
|
||||||
@BaseMedium: #CCCCCC;
|
@BaseMedium: #cccccc;
|
||||||
@BaseMediumHigh: #767676;
|
@BaseMediumHigh: #767676;
|
||||||
@BaseHigh: #393939;
|
@BaseHigh: #393939;
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
|
|
||||||
@ErrorColor: @SelectionHigh;
|
@ErrorColor: @SelectionHigh;
|
||||||
|
|
||||||
@SelectionColor: #3074B0;
|
@SelectionColor: #3074b0;
|
||||||
|
|
||||||
@FocusColor: #605e5c;
|
@FocusColor: #605e5c;
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
@ImgWidth: 14px;
|
@ImgWidth: 14px;
|
||||||
@ImgHeight: 14px;
|
@ImgHeight: 14px;
|
||||||
|
|
||||||
@toggleFontWeight:700;
|
@toggleFontWeight: 700;
|
||||||
|
|
||||||
//Resource Tree
|
//Resource Tree
|
||||||
@TreeLineHeight: 17px;
|
@TreeLineHeight: 17px;
|
||||||
@@ -144,16 +144,16 @@
|
|||||||
/**********************************************************************************/
|
/**********************************************************************************/
|
||||||
|
|
||||||
.flex-display(@display: flex) {
|
.flex-display(@display: flex) {
|
||||||
display: ~"-webkit-@{display}";
|
display: ~"-webkit-@{display}";
|
||||||
display: ~"-ms-@{display}box"; // IE10 uses -ms-flexbox
|
display: ~"-ms-@{display}box"; // IE10 uses -ms-flexbox
|
||||||
display: ~"-ms-@{display}"; // IE11
|
display: ~"-ms-@{display}"; // IE11
|
||||||
display: @display;
|
display: @display;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-direction(@direction: column) {
|
.flex-direction(@direction: column) {
|
||||||
-webkit-flex-direction: @direction;
|
-webkit-flex-direction: @direction;
|
||||||
-ms-flex-direction: @direction;
|
-ms-flex-direction: @direction;
|
||||||
flex-direction: @direction;
|
flex-direction: @direction;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*************************************************************************************
|
/*************************************************************************************
|
||||||
@@ -161,32 +161,31 @@
|
|||||||
**************************************************************************************/
|
**************************************************************************************/
|
||||||
|
|
||||||
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
|
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
|
||||||
.selectedRadio,
|
.selectedRadio,
|
||||||
.selectedRadio:hover,
|
.selectedRadio:hover,
|
||||||
.selectedRadio:active,
|
.selectedRadio:active,
|
||||||
.selectedRadio.dirty,
|
.selectedRadio.dirty,
|
||||||
.tab [type=radio]:checked ~ label,
|
.tab [type="radio"]:checked ~ label,
|
||||||
.tab [type=radio]:checked ~ label:hover {
|
.tab [type="radio"]:checked ~ label:hover {
|
||||||
-ms-high-contrast-adjust: none;
|
-ms-high-contrast-adjust: none;
|
||||||
-webkit-text-fill-color: HighlightText;
|
-webkit-text-fill-color: HighlightText;
|
||||||
color: HighlightText;
|
color: HighlightText;
|
||||||
border-color: HighlightText;
|
border-color: HighlightText;
|
||||||
background-color: Highlight;
|
background-color: Highlight;
|
||||||
}
|
}
|
||||||
|
|
||||||
.queryMetricsSummaryTuple {
|
.queryMetricsSummaryTuple {
|
||||||
|
th,
|
||||||
th, td {
|
td {
|
||||||
|
&:nth-child(2) {
|
||||||
&:nth-child(2) {
|
width: @IETableDataWidth;
|
||||||
width: @IETableDataWidth;
|
}
|
||||||
}
|
|
||||||
|
&:nth-child(3) {
|
||||||
&:nth-child(3) {
|
width: 50%;
|
||||||
width: 50%;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/********************************************************************************************
|
/********************************************************************************************
|
||||||
@@ -194,15 +193,15 @@
|
|||||||
*********************************************************************************************/
|
*********************************************************************************************/
|
||||||
|
|
||||||
.hover() {
|
.hover() {
|
||||||
background-color: @AccentLight;
|
background-color: @AccentLight;
|
||||||
}
|
}
|
||||||
|
|
||||||
.active() {
|
.active() {
|
||||||
background-color: @AccentExtra;
|
background-color: @AccentExtra;
|
||||||
}
|
}
|
||||||
|
|
||||||
.focus() {
|
.focus() {
|
||||||
outline: 1px dashed @FocusColor;
|
outline: 1px dashed @FocusColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
/************************************************************************************************
|
/************************************************************************************************
|
||||||
@@ -212,63 +211,87 @@
|
|||||||
@ToggleWidth: 180px;
|
@ToggleWidth: 180px;
|
||||||
|
|
||||||
.toggleSwitch() {
|
.toggleSwitch() {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin-bottom: @SmallSpace;
|
margin-bottom: @SmallSpace;
|
||||||
padding: @SmallSpace;
|
padding: @SmallSpace;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: @BaseHigh;
|
color: @BaseHigh;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: @mediumFontSize;
|
font-size: @mediumFontSize;
|
||||||
font-family: @DataExplorerFont;
|
font-family: @DataExplorerFont;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectedToggle() {
|
.selectedToggle() {
|
||||||
border-bottom: 2px solid @BaseHigh;
|
border-bottom: 2px solid @BaseHigh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.unselectedToggle() {
|
.unselectedToggle() {
|
||||||
color: @AccentMediumHigh;
|
color: @AccentMediumHigh;
|
||||||
}
|
}
|
||||||
|
|
||||||
/********************************************************************************************************
|
/********************************************************************************************************
|
||||||
Common Data Explorer Icons
|
Common Data Explorer Icons
|
||||||
*********************************************************************************************************/
|
*********************************************************************************************************/
|
||||||
.dataExplorerIcons() {
|
.dataExplorerIcons() {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: @ImgWidth;
|
width: @ImgWidth;
|
||||||
height: @ImgHeight;
|
height: @ImgHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*********************************************************************************************************
|
/*********************************************************************************************************
|
||||||
Info Tooltip
|
Info Tooltip
|
||||||
**********************************************************************************************************/
|
**********************************************************************************************************/
|
||||||
.infoTooltip() {
|
.infoTooltip() {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltipText(@textColor: @BaseLight, @backgroundColor: @BaseHigh) {
|
.tooltipText(@textColor: @BaseLight, @backgroundColor: @BaseHigh) {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
background-color: @backgroundColor;
|
background-color: @backgroundColor;
|
||||||
color: @textColor;
|
color: @textColor;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
left: @MediumSpace;
|
left: @MediumSpace;
|
||||||
padding: @MediumSpace;
|
padding: @MediumSpace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltipTextAfter(@color: @BaseDark) {
|
.tooltipTextAfter(@color: @BaseDark) {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 100%;
|
right: 100%;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-color: transparent @color transparent transparent;
|
border-color: transparent @color transparent transparent;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
border-color: @InfoPointerColor transparent;
|
border-color: @InfoPointerColor transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltipVisible() {
|
.tooltipVisible() {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputTooltip() {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputTooltipText(@textColor: @BaseLight, @backgroundColor: @BaseHigh) {
|
||||||
|
background-color: @backgroundColor;
|
||||||
|
color: @textColor;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
padding: @MediumSpace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputTooltipTextAfter(@color: @BaseDark) {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
right: 100%;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: transparent @color transparent transparent;
|
||||||
|
left: 10px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-color: @InfoPointerColor transparent;
|
||||||
}
|
}
|
||||||
|
|||||||
3829
less/documentDB.less
3829
less/documentDB.less
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,11 @@
|
|||||||
@NavMediumSpace: 10px;
|
@NavMediumSpace: 10px;
|
||||||
@NavLargeSpace: 15px;
|
@NavLargeSpace: 15px;
|
||||||
|
|
||||||
|
.skip-link {
|
||||||
|
position: fixed;
|
||||||
|
top: -200px;
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-family: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
|
font-family: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
@import "./Common/Constants";
|
@import "./Common/Constants";
|
||||||
|
|
||||||
.main {
|
|
||||||
width: 100%;
|
|
||||||
float: left;
|
|
||||||
transition: all .0s ease-in-out;
|
|
||||||
-ms-transition: all 0s ease-in-out;
|
|
||||||
-webkit-transition: all 0s ease-in-out;
|
|
||||||
-moz-transition: all .0s ease-in-out;
|
|
||||||
height: 100%;
|
|
||||||
background-color: white;
|
|
||||||
border-left: 0px solid white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resourceTree {
|
.resourceTree {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
.main {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.resourceTreeScroll {
|
.resourceTreeScroll {
|
||||||
|
|||||||
3909
package-lock.json
generated
3909
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
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",
|
||||||
|
"@babel/plugin-proposal-class-properties": "7.12.1",
|
||||||
|
"@babel/plugin-proposal-decorators": "7.12.12",
|
||||||
"@jupyterlab/services": "6.0.0-rc.2",
|
"@jupyterlab/services": "6.0.0-rc.2",
|
||||||
"@jupyterlab/terminal": "3.0.0-rc.2",
|
"@jupyterlab/terminal": "3.0.0-rc.2",
|
||||||
"@microsoft/applicationinsights-web": "2.5.9",
|
"@microsoft/applicationinsights-web": "2.5.9",
|
||||||
@@ -42,7 +46,7 @@
|
|||||||
"applicationinsights": "1.8.0",
|
"applicationinsights": "1.8.0",
|
||||||
"babel-polyfill": "6.26.0",
|
"babel-polyfill": "6.26.0",
|
||||||
"bootstrap": "3.4.1",
|
"bootstrap": "3.4.1",
|
||||||
"canvas": "2.6.1",
|
"canvas": "file:./canvas",
|
||||||
"clean-webpack-plugin": "0.1.19",
|
"clean-webpack-plugin": "0.1.19",
|
||||||
"copy-webpack-plugin": "6.0.2",
|
"copy-webpack-plugin": "6.0.2",
|
||||||
"crossroads": "0.12.2",
|
"crossroads": "0.12.2",
|
||||||
@@ -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",
|
||||||
@@ -83,6 +87,7 @@
|
|||||||
"react-notification-system": "0.2.17",
|
"react-notification-system": "0.2.17",
|
||||||
"react-redux": "7.1.3",
|
"react-redux": "7.1.3",
|
||||||
"redux": "4.0.4",
|
"redux": "4.0.4",
|
||||||
|
"reflect-metadata": "0.1.13",
|
||||||
"rx-jupyter": "5.5.12",
|
"rx-jupyter": "5.5.12",
|
||||||
"rxjs": "6.6.3",
|
"rxjs": "6.6.3",
|
||||||
"styled-components": "4.3.2",
|
"styled-components": "4.3.2",
|
||||||
@@ -115,7 +120,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",
|
||||||
@@ -194,8 +199,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'))"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,439 +1,437 @@
|
|||||||
import { HashMap } from "./HashMap";
|
import { HashMap } from "./HashMap";
|
||||||
|
|
||||||
export class AuthorizationEndpoints {
|
export class AuthorizationEndpoints {
|
||||||
public static arm: string = "https://management.core.windows.net/";
|
public static arm: string = "https://management.core.windows.net/";
|
||||||
public static common: string = "https://login.windows.net/";
|
public static common: string = "https://login.windows.net/";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CodeOfConductEndpoints {
|
export class CodeOfConductEndpoints {
|
||||||
public static privacyStatement: string = "https://aka.ms/ms-privacy-policy";
|
public static privacyStatement: string = "https://aka.ms/ms-privacy-policy";
|
||||||
public static codeOfConduct: string = "https://aka.ms/cosmos-code-of-conduct";
|
public static codeOfConduct: string = "https://aka.ms/cosmos-code-of-conduct";
|
||||||
public static termsOfUse: string = "https://aka.ms/ms-terms-of-use";
|
public static termsOfUse: string = "https://aka.ms/ms-terms-of-use";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EndpointsRegex {
|
export class EndpointsRegex {
|
||||||
public static readonly cassandra = [
|
public static readonly cassandra = [
|
||||||
"AccountEndpoint=(.*).cassandra.cosmosdb.azure.com",
|
"AccountEndpoint=(.*).cassandra.cosmosdb.azure.com",
|
||||||
"HostName=(.*).cassandra.cosmos.azure.com"
|
"HostName=(.*).cassandra.cosmos.azure.com"
|
||||||
];
|
];
|
||||||
public static readonly mongo = "mongodb://.*:(.*)@(.*).documents.azure.com";
|
public static readonly mongo = "mongodb://.*:(.*)@(.*).documents.azure.com";
|
||||||
public static readonly mongoCompute = "mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com";
|
public static readonly mongoCompute = "mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com";
|
||||||
public static readonly sql = "AccountEndpoint=https://(.*).documents.azure.com";
|
public static readonly sql = "AccountEndpoint=https://(.*).documents.azure.com";
|
||||||
public static readonly table = "TableEndpoint=https://(.*).table.cosmosdb.azure.com";
|
public static readonly table = "TableEndpoint=https://(.*).table.cosmosdb.azure.com";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ApiEndpoints {
|
export class ApiEndpoints {
|
||||||
public static runtimeProxy: string = "/api/RuntimeProxy";
|
public static runtimeProxy: string = "/api/RuntimeProxy";
|
||||||
public static guestRuntimeProxy: string = "/api/guest/RuntimeProxy";
|
public static guestRuntimeProxy: string = "/api/guest/RuntimeProxy";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ServerIds {
|
export class ServerIds {
|
||||||
public static localhost: string = "localhost";
|
public static localhost: string = "localhost";
|
||||||
public static blackforest: string = "blackforest";
|
public static blackforest: string = "blackforest";
|
||||||
public static fairfax: string = "fairfax";
|
public static fairfax: string = "fairfax";
|
||||||
public static mooncake: string = "mooncake";
|
public static mooncake: string = "mooncake";
|
||||||
public static productionPortal: string = "prod";
|
public static productionPortal: string = "prod";
|
||||||
public static dev: string = "dev";
|
public static dev: string = "dev";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ArmApiVersions {
|
export class ArmApiVersions {
|
||||||
public static readonly documentDB: string = "2015-11-06";
|
public static readonly documentDB: string = "2015-11-06";
|
||||||
public static readonly arcadia: string = "2019-06-01-preview";
|
public static readonly arcadia: string = "2019-06-01-preview";
|
||||||
public static readonly arcadiaLivy: string = "2019-11-01-preview";
|
public static readonly arcadiaLivy: string = "2019-11-01-preview";
|
||||||
public static readonly arm: string = "2015-11-01";
|
public static readonly arm: string = "2015-11-01";
|
||||||
public static readonly armFeatures: string = "2014-08-01-preview";
|
public static readonly armFeatures: string = "2014-08-01-preview";
|
||||||
public static readonly publicVersion = "2020-04-01";
|
public static readonly publicVersion = "2020-04-01";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ArmResourceTypes {
|
export class ArmResourceTypes {
|
||||||
public static readonly notebookWorkspaces = "Microsoft.DocumentDB/databaseAccounts/notebookWorkspaces";
|
public static readonly notebookWorkspaces = "Microsoft.DocumentDB/databaseAccounts/notebookWorkspaces";
|
||||||
public static readonly synapseWorkspaces = "Microsoft.Synapse/workspaces";
|
public static readonly synapseWorkspaces = "Microsoft.Synapse/workspaces";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BackendDefaults {
|
export class BackendDefaults {
|
||||||
public static partitionKeyKind: string = "Hash";
|
public static partitionKeyKind: string = "Hash";
|
||||||
public static singlePartitionStorageInGb: string = "10";
|
public static singlePartitionStorageInGb: string = "10";
|
||||||
public static multiPartitionStorageInGb: string = "100";
|
public static multiPartitionStorageInGb: string = "100";
|
||||||
public static maxChangeFeedRetentionDuration: number = 10;
|
public static maxChangeFeedRetentionDuration: number = 10;
|
||||||
public static partitionKeyVersion = 2;
|
public static partitionKeyVersion = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ClientDefaults {
|
export class ClientDefaults {
|
||||||
public static requestTimeoutMs: number = 60000;
|
public static requestTimeoutMs: number = 60000;
|
||||||
public static portalCacheTimeoutMs: number = 10000;
|
public static portalCacheTimeoutMs: number = 10000;
|
||||||
public static errorNotificationTimeoutMs: number = 5000;
|
public static errorNotificationTimeoutMs: number = 5000;
|
||||||
public static copyHelperTimeoutMs: number = 2000;
|
public static copyHelperTimeoutMs: number = 2000;
|
||||||
public static waitForDOMElementMs: number = 500;
|
public static waitForDOMElementMs: number = 500;
|
||||||
public static cacheBustingTimeoutMs: number =
|
public static cacheBustingTimeoutMs: number =
|
||||||
10 /** minutes **/ * 60 /** to seconds **/ * 1000 /** to milliseconds **/;
|
10 /** minutes **/ * 60 /** to seconds **/ * 1000 /** to milliseconds **/;
|
||||||
public static databaseThroughputIncreaseFactor: number = 100;
|
public static databaseThroughputIncreaseFactor: number = 100;
|
||||||
public static readonly arcadiaTokenRefreshInterval: number =
|
public static readonly arcadiaTokenRefreshInterval: number =
|
||||||
20 /** minutes **/ * 60 /** to seconds **/ * 1000 /** to milliseconds **/;
|
20 /** minutes **/ * 60 /** to seconds **/ * 1000 /** to milliseconds **/;
|
||||||
public static readonly arcadiaTokenRefreshIntervalPaddingMs: number = 2000;
|
public static readonly arcadiaTokenRefreshIntervalPaddingMs: number = 2000;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AccountKind {
|
export class AccountKind {
|
||||||
public static DocumentDB: string = "DocumentDB";
|
public static DocumentDB: string = "DocumentDB";
|
||||||
public static MongoDB: string = "MongoDB";
|
public static MongoDB: string = "MongoDB";
|
||||||
public static Parse: string = "Parse";
|
public static Parse: string = "Parse";
|
||||||
public static GlobalDocumentDB: string = "GlobalDocumentDB";
|
public static GlobalDocumentDB: string = "GlobalDocumentDB";
|
||||||
public static Default: string = AccountKind.DocumentDB;
|
public static Default: string = AccountKind.DocumentDB;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CorrelationBackend {
|
export class CorrelationBackend {
|
||||||
public static Url: string = "https://aka.ms/cosmosdbanalytics";
|
public static Url: string = "https://aka.ms/cosmosdbanalytics";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DefaultAccountExperience {
|
export class DefaultAccountExperience {
|
||||||
public static DocumentDB: string = "DocumentDB";
|
public static DocumentDB: string = "DocumentDB";
|
||||||
public static Graph: string = "Graph";
|
public static Graph: string = "Graph";
|
||||||
public static MongoDB: string = "MongoDB";
|
public static MongoDB: string = "MongoDB";
|
||||||
public static ApiForMongoDB: string = "Azure Cosmos DB for MongoDB API";
|
public static ApiForMongoDB: string = "Azure Cosmos DB for MongoDB API";
|
||||||
public static Table: string = "Table";
|
public static Table: string = "Table";
|
||||||
public static Cassandra: string = "Cassandra";
|
public static Cassandra: string = "Cassandra";
|
||||||
public static Default: string = DefaultAccountExperience.DocumentDB;
|
public static Default: string = DefaultAccountExperience.DocumentDB;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CapabilityNames {
|
export class CapabilityNames {
|
||||||
public static EnableTable: string = "EnableTable";
|
public static EnableTable: string = "EnableTable";
|
||||||
public static EnableGremlin: string = "EnableGremlin";
|
public static EnableGremlin: string = "EnableGremlin";
|
||||||
public static EnableCassandra: string = "EnableCassandra";
|
public static EnableCassandra: string = "EnableCassandra";
|
||||||
public static EnableAutoScale: string = "EnableAutoScale";
|
public static EnableAutoScale: string = "EnableAutoScale";
|
||||||
public static readonly EnableNotebooks: string = "EnableNotebooks";
|
public static readonly EnableNotebooks: string = "EnableNotebooks";
|
||||||
public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics";
|
public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics";
|
||||||
public static readonly EnableMongo: string = "EnableMongo";
|
public static readonly EnableMongo: string = "EnableMongo";
|
||||||
public static readonly EnableServerless: string = "EnableServerless";
|
public static readonly EnableServerless: string = "EnableServerless";
|
||||||
}
|
}
|
||||||
|
|
||||||
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 enableLinkInjection = "enablelinkinjection";
|
||||||
public static readonly enableCodeOfConduct = "enablecodeofconduct";
|
public static readonly enableSpark = "enablespark";
|
||||||
public static readonly enableLinkInjection = "enablelinkinjection";
|
public static readonly livyEndpoint = "livyendpoint";
|
||||||
public static readonly enableSpark = "enablespark";
|
public static readonly notebookServerUrl = "notebookserverurl";
|
||||||
public static readonly livyEndpoint = "livyendpoint";
|
public static readonly notebookServerToken = "notebookservertoken";
|
||||||
public static readonly notebookServerUrl = "notebookserverurl";
|
public static readonly notebookBasePath = "notebookbasepath";
|
||||||
public static readonly notebookServerToken = "notebookservertoken";
|
public static readonly canExceedMaximumValue = "canexceedmaximumvalue";
|
||||||
public static readonly notebookBasePath = "notebookbasepath";
|
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
|
||||||
public static readonly canExceedMaximumValue = "canexceedmaximumvalue";
|
public static readonly ttl90Days = "ttl90days";
|
||||||
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
|
public static readonly enableRightPanelV2 = "enablerightpanelv2";
|
||||||
public static readonly ttl90Days = "ttl90days";
|
public static readonly enableSchema = "enableschema";
|
||||||
public static readonly enableRightPanelV2 = "enablerightpanelv2";
|
public static readonly enableSDKoperations = "enablesdkoperations";
|
||||||
public static readonly enableSDKoperations = "enablesdkoperations";
|
public static readonly showMinRUSurvey = "showminrusurvey";
|
||||||
}
|
public static readonly selfServeType = "selfservetype";
|
||||||
|
}
|
||||||
// flight names returned from the portal are always lowercase
|
|
||||||
export class Flights {
|
// flight names returned from the portal are always lowercase
|
||||||
public static readonly SettingsV2 = "settingsv2";
|
export class Flights {
|
||||||
public static readonly MongoIndexEditor = "mongoindexeditor";
|
public static readonly SettingsV2 = "settingsv2";
|
||||||
}
|
public static readonly MongoIndexEditor = "mongoindexeditor";
|
||||||
|
public static readonly AutoscaleTest = "autoscaletest";
|
||||||
export class AfecFeatures {
|
public static readonly MongoIndexing = "mongoindexing";
|
||||||
public static readonly Spark = "spark-public-preview";
|
}
|
||||||
public static readonly Notebooks = "sparknotebooks-public-preview";
|
|
||||||
public static readonly StorageAnalytics = "storageanalytics-public-preview";
|
export class AfecFeatures {
|
||||||
}
|
public static readonly Spark = "spark-public-preview";
|
||||||
|
public static readonly Notebooks = "sparknotebooks-public-preview";
|
||||||
export class Spark {
|
public static readonly StorageAnalytics = "storageanalytics-public-preview";
|
||||||
public static readonly MaxWorkerCount = 10;
|
}
|
||||||
public static readonly SKUs: HashMap<string> = new HashMap({
|
|
||||||
"Cosmos.Spark.D1s": "D1s / 1 core / 4GB RAM",
|
export class Spark {
|
||||||
"Cosmos.Spark.D2s": "D2s / 2 cores / 8GB RAM",
|
public static readonly MaxWorkerCount = 10;
|
||||||
"Cosmos.Spark.D4s": "D4s / 4 cores / 16GB RAM",
|
public static readonly SKUs: HashMap<string> = new HashMap({
|
||||||
"Cosmos.Spark.D8s": "D8s / 8 cores / 32GB RAM",
|
"Cosmos.Spark.D1s": "D1s / 1 core / 4GB RAM",
|
||||||
"Cosmos.Spark.D16s": "D16s / 16 cores / 64GB RAM",
|
"Cosmos.Spark.D2s": "D2s / 2 cores / 8GB RAM",
|
||||||
"Cosmos.Spark.D32s": "D32s / 32 cores / 128GB RAM",
|
"Cosmos.Spark.D4s": "D4s / 4 cores / 16GB RAM",
|
||||||
"Cosmos.Spark.D64s": "D64s / 64 cores / 256GB RAM"
|
"Cosmos.Spark.D8s": "D8s / 8 cores / 32GB RAM",
|
||||||
});
|
"Cosmos.Spark.D16s": "D16s / 16 cores / 64GB RAM",
|
||||||
}
|
"Cosmos.Spark.D32s": "D32s / 32 cores / 128GB RAM",
|
||||||
|
"Cosmos.Spark.D64s": "D64s / 64 cores / 256GB RAM"
|
||||||
export class TagNames {
|
});
|
||||||
public static defaultExperience: string = "defaultExperience";
|
}
|
||||||
}
|
|
||||||
|
export class TagNames {
|
||||||
export class MongoDBAccounts {
|
public static defaultExperience: string = "defaultExperience";
|
||||||
public static protocol: string = "https";
|
}
|
||||||
public static defaultPort: string = "10255";
|
|
||||||
}
|
export class MongoDBAccounts {
|
||||||
|
public static protocol: string = "https";
|
||||||
export enum MongoBackendEndpointType {
|
public static defaultPort: string = "10255";
|
||||||
local,
|
}
|
||||||
remote
|
|
||||||
}
|
export enum MongoBackendEndpointType {
|
||||||
|
local,
|
||||||
// TODO: 435619 Add default endpoints per cloud and use regional only when available
|
remote
|
||||||
export class CassandraBackend {
|
}
|
||||||
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
|
|
||||||
public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete";
|
// TODO: 435619 Add default endpoints per cloud and use regional only when available
|
||||||
public static readonly queryApi: string = "api/cassandra";
|
export class CassandraBackend {
|
||||||
public static readonly guestQueryApi: string = "api/guest/cassandra";
|
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
|
||||||
public static readonly keysApi: string = "api/cassandra/keys";
|
public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete";
|
||||||
public static readonly guestKeysApi: string = "api/guest/cassandra/keys";
|
public static readonly queryApi: string = "api/cassandra";
|
||||||
public static readonly schemaApi: string = "api/cassandra/schema";
|
public static readonly guestQueryApi: string = "api/guest/cassandra";
|
||||||
public static readonly guestSchemaApi: string = "api/guest/cassandra/schema";
|
public static readonly keysApi: string = "api/cassandra/keys";
|
||||||
}
|
public static readonly guestKeysApi: string = "api/guest/cassandra/keys";
|
||||||
|
public static readonly schemaApi: string = "api/cassandra/schema";
|
||||||
export class RUPMStates {
|
public static readonly guestSchemaApi: string = "api/guest/cassandra/schema";
|
||||||
public static on: string = "on";
|
}
|
||||||
public static off: string = "off";
|
|
||||||
}
|
export class Queries {
|
||||||
|
public static CustomPageOption: string = "custom";
|
||||||
export class Queries {
|
public static UnlimitedPageOption: string = "unlimited";
|
||||||
public static CustomPageOption: string = "custom";
|
public static itemsPerPage: number = 100;
|
||||||
public static UnlimitedPageOption: string = "unlimited";
|
public static unlimitedItemsPerPage: number = 100; // TODO: Figure out appropriate value so it works for accounts with a large number of partitions
|
||||||
public static itemsPerPage: number = 100;
|
|
||||||
public static unlimitedItemsPerPage: number = 100; // TODO: Figure out appropriate value so it works for accounts with a large number of partitions
|
public static QueryEditorMinHeightRatio: number = 0.1;
|
||||||
|
public static QueryEditorMaxHeightRatio: number = 0.4;
|
||||||
public static QueryEditorMinHeightRatio: number = 0.1;
|
public static readonly DefaultMaxDegreeOfParallelism = 6;
|
||||||
public static QueryEditorMaxHeightRatio: number = 0.4;
|
}
|
||||||
public static readonly DefaultMaxDegreeOfParallelism = 6;
|
|
||||||
}
|
export class SavedQueries {
|
||||||
|
public static readonly CollectionName: string = "___Query";
|
||||||
export class SavedQueries {
|
public static readonly DatabaseName: string = "___Cosmos";
|
||||||
public static readonly CollectionName: string = "___Query";
|
public static readonly OfferThroughput: number = 400;
|
||||||
public static readonly DatabaseName: string = "___Cosmos";
|
public static readonly PartitionKeyProperty: string = "id";
|
||||||
public static readonly OfferThroughput: number = 400;
|
}
|
||||||
public static readonly PartitionKeyProperty: string = "id";
|
|
||||||
}
|
export class DocumentsGridMetrics {
|
||||||
|
public static DocumentsPerPage: number = 100;
|
||||||
export class DocumentsGridMetrics {
|
public static IndividualRowHeight: number = 34;
|
||||||
public static DocumentsPerPage: number = 100;
|
public static BufferHeight: number = 28;
|
||||||
public static IndividualRowHeight: number = 34;
|
public static SplitterMinWidth: number = 200;
|
||||||
public static BufferHeight: number = 28;
|
public static SplitterMaxWidth: number = 360;
|
||||||
public static SplitterMinWidth: number = 200;
|
|
||||||
public static SplitterMaxWidth: number = 360;
|
public static DocumentEditorMinWidthRatio: number = 0.2;
|
||||||
|
public static DocumentEditorMaxWidthRatio: number = 0.4;
|
||||||
public static DocumentEditorMinWidthRatio: number = 0.2;
|
}
|
||||||
public static DocumentEditorMaxWidthRatio: number = 0.4;
|
|
||||||
}
|
export class ExplorerMetrics {
|
||||||
|
public static SplitterMinWidth: number = 240;
|
||||||
export class ExplorerMetrics {
|
public static SplitterMaxWidth: number = 400;
|
||||||
public static SplitterMinWidth: number = 240;
|
public static CollapsedResourceTreeWidth: number = 36;
|
||||||
public static SplitterMaxWidth: number = 400;
|
}
|
||||||
public static CollapsedResourceTreeWidth: number = 36;
|
|
||||||
}
|
export class SplitterMetrics {
|
||||||
|
public static CollapsedPositionLeft: number = ExplorerMetrics.CollapsedResourceTreeWidth;
|
||||||
export class SplitterMetrics {
|
}
|
||||||
public static CollapsedPositionLeft: number = ExplorerMetrics.CollapsedResourceTreeWidth;
|
|
||||||
}
|
export class Areas {
|
||||||
|
public static ResourceTree: string = "Resource Tree";
|
||||||
export class Areas {
|
public static ContextualPane: string = "Contextual Pane";
|
||||||
public static ResourceTree: string = "Resource Tree";
|
public static Tab: string = "Tab";
|
||||||
public static ContextualPane: string = "Contextual Pane";
|
public static ShareDialog: string = "Share Access Dialog";
|
||||||
public static Tab: string = "Tab";
|
public static Notebook: string = "Notebook";
|
||||||
public static ShareDialog: string = "Share Access Dialog";
|
}
|
||||||
public static Notebook: string = "Notebook";
|
|
||||||
}
|
export class HttpHeaders {
|
||||||
|
public static activityId: string = "x-ms-activity-id";
|
||||||
export class HttpHeaders {
|
public static apiType: string = "x-ms-cosmos-apitype";
|
||||||
public static activityId: string = "x-ms-activity-id";
|
public static authorization: string = "authorization";
|
||||||
public static apiType: string = "x-ms-cosmos-apitype";
|
public static collectionIndexTransformationProgress: string =
|
||||||
public static authorization: string = "authorization";
|
"x-ms-documentdb-collection-index-transformation-progress";
|
||||||
public static collectionIndexTransformationProgress: string =
|
public static continuation: string = "x-ms-continuation";
|
||||||
"x-ms-documentdb-collection-index-transformation-progress";
|
public static correlationRequestId: string = "x-ms-correlation-request-id";
|
||||||
public static continuation: string = "x-ms-continuation";
|
public static enableScriptLogging: string = "x-ms-documentdb-script-enable-logging";
|
||||||
public static correlationRequestId: string = "x-ms-correlation-request-id";
|
public static guestAccessToken: string = "x-ms-encrypted-auth-token";
|
||||||
public static enableScriptLogging: string = "x-ms-documentdb-script-enable-logging";
|
public static getReadOnlyKey: string = "x-ms-get-read-only-key";
|
||||||
public static guestAccessToken: string = "x-ms-encrypted-auth-token";
|
public static connectionString: string = "x-ms-connection-string";
|
||||||
public static getReadOnlyKey: string = "x-ms-get-read-only-key";
|
public static msDate: string = "x-ms-date";
|
||||||
public static connectionString: string = "x-ms-connection-string";
|
public static location: string = "Location";
|
||||||
public static msDate: string = "x-ms-date";
|
public static contentType: string = "Content-Type";
|
||||||
public static location: string = "Location";
|
public static offerReplacePending: string = "x-ms-offer-replace-pending";
|
||||||
public static contentType: string = "Content-Type";
|
public static user: string = "x-ms-user";
|
||||||
public static offerReplacePending: string = "x-ms-offer-replace-pending";
|
public static populatePartitionStatistics: string = "x-ms-documentdb-populatepartitionstatistics";
|
||||||
public static user: string = "x-ms-user";
|
public static queryMetrics: string = "x-ms-documentdb-query-metrics";
|
||||||
public static populatePartitionStatistics: string = "x-ms-documentdb-populatepartitionstatistics";
|
public static requestCharge: string = "x-ms-request-charge";
|
||||||
public static queryMetrics: string = "x-ms-documentdb-query-metrics";
|
public static resourceQuota: string = "x-ms-resource-quota";
|
||||||
public static requestCharge: string = "x-ms-request-charge";
|
public static resourceUsage: string = "x-ms-resource-usage";
|
||||||
public static resourceQuota: string = "x-ms-resource-quota";
|
public static retryAfterMs: string = "x-ms-retry-after-ms";
|
||||||
public static resourceUsage: string = "x-ms-resource-usage";
|
public static scriptLogResults: string = "x-ms-documentdb-script-log-results";
|
||||||
public static retryAfterMs: string = "x-ms-retry-after-ms";
|
public static populateCollectionThroughputInfo = "x-ms-documentdb-populatecollectionthroughputinfo";
|
||||||
public static scriptLogResults: string = "x-ms-documentdb-script-log-results";
|
public static supportSpatialLegacyCoordinates = "x-ms-documentdb-supportspatiallegacycoordinates";
|
||||||
public static populateCollectionThroughputInfo = "x-ms-documentdb-populatecollectionthroughputinfo";
|
public static usePolygonsSmallerThanAHemisphere = "x-ms-documentdb-usepolygonssmallerthanahemisphere";
|
||||||
public static supportSpatialLegacyCoordinates = "x-ms-documentdb-supportspatiallegacycoordinates";
|
public static autoPilotThroughput = "autoscaleSettings";
|
||||||
public static usePolygonsSmallerThanAHemisphere = "x-ms-documentdb-usepolygonssmallerthanahemisphere";
|
public static autoPilotThroughputSDK = "x-ms-cosmos-offer-autopilot-settings";
|
||||||
public static autoPilotThroughput = "autoscaleSettings";
|
public static partitionKey: string = "x-ms-documentdb-partitionkey";
|
||||||
public static autoPilotThroughputSDK = "x-ms-cosmos-offer-autopilot-settings";
|
public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput";
|
||||||
public static partitionKey: string = "x-ms-documentdb-partitionkey";
|
public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot";
|
||||||
public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput";
|
}
|
||||||
public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot";
|
|
||||||
}
|
export class ApiType {
|
||||||
|
// Mapped to hexadecimal values in the backend
|
||||||
export class ApiType {
|
public static readonly MongoDB: number = 1;
|
||||||
// Mapped to hexadecimal values in the backend
|
public static readonly Gremlin: number = 2;
|
||||||
public static readonly MongoDB: number = 1;
|
public static readonly Cassandra: number = 4;
|
||||||
public static readonly Gremlin: number = 2;
|
public static readonly Table: number = 8;
|
||||||
public static readonly Cassandra: number = 4;
|
public static readonly SQL: number = 16;
|
||||||
public static readonly Table: number = 8;
|
}
|
||||||
public static readonly SQL: number = 16;
|
|
||||||
}
|
export class HttpStatusCodes {
|
||||||
|
public static readonly OK: number = 200;
|
||||||
export class HttpStatusCodes {
|
public static readonly Created: number = 201;
|
||||||
public static readonly OK: number = 200;
|
public static readonly Accepted: number = 202;
|
||||||
public static readonly Created: number = 201;
|
public static readonly NoContent: number = 204;
|
||||||
public static readonly Accepted: number = 202;
|
public static readonly NotModified: number = 304;
|
||||||
public static readonly NoContent: number = 204;
|
public static readonly Unauthorized: number = 401;
|
||||||
public static readonly NotModified: number = 304;
|
public static readonly Forbidden: number = 403;
|
||||||
public static readonly Unauthorized: number = 401;
|
public static readonly NotFound: number = 404;
|
||||||
public static readonly Forbidden: number = 403;
|
public static readonly TooManyRequests: number = 429;
|
||||||
public static readonly NotFound: number = 404;
|
public static readonly Conflict: number = 409;
|
||||||
public static readonly TooManyRequests: number = 429;
|
|
||||||
public static readonly Conflict: number = 409;
|
public static readonly InternalServerError: number = 500;
|
||||||
|
public static readonly BadGateway: number = 502;
|
||||||
public static readonly InternalServerError: number = 500;
|
public static readonly ServiceUnavailable: number = 503;
|
||||||
public static readonly BadGateway: number = 502;
|
public static readonly GatewayTimeout: number = 504;
|
||||||
public static readonly ServiceUnavailable: number = 503;
|
|
||||||
public static readonly GatewayTimeout: number = 504;
|
public static readonly RetryableStatusCodes: number[] = [
|
||||||
|
HttpStatusCodes.TooManyRequests,
|
||||||
public static readonly RetryableStatusCodes: number[] = [
|
HttpStatusCodes.InternalServerError, // TODO: Handle all 500s on Portal backend and remove from retries list
|
||||||
HttpStatusCodes.TooManyRequests,
|
HttpStatusCodes.BadGateway,
|
||||||
HttpStatusCodes.InternalServerError, // TODO: Handle all 500s on Portal backend and remove from retries list
|
HttpStatusCodes.ServiceUnavailable,
|
||||||
HttpStatusCodes.BadGateway,
|
HttpStatusCodes.GatewayTimeout
|
||||||
HttpStatusCodes.ServiceUnavailable,
|
];
|
||||||
HttpStatusCodes.GatewayTimeout
|
}
|
||||||
];
|
|
||||||
}
|
export class Urls {
|
||||||
|
public static feedbackEmail = "https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Data%20Explorer%20Feedback";
|
||||||
export class Urls {
|
public static autoscaleMigration = "https://aka.ms/cosmos-autoscale-migration";
|
||||||
public static feedbackEmail = "https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Data%20Explorer%20Feedback";
|
public static freeTierInformation = "https://aka.ms/cosmos-free-tier";
|
||||||
public static autoscaleMigration = "https://aka.ms/cosmos-autoscale-migration";
|
public static cosmosPricing = "https://aka.ms/azure-cosmos-db-pricing";
|
||||||
public static freeTierInformation = "https://aka.ms/cosmos-free-tier";
|
}
|
||||||
public static cosmosPricing = "https://aka.ms/azure-cosmos-db-pricing";
|
|
||||||
}
|
export class HashRoutePrefixes {
|
||||||
|
public static databases: string = "/dbs/{db_id}";
|
||||||
export class HashRoutePrefixes {
|
public static collections: string = "/dbs/{db_id}/colls/{coll_id}";
|
||||||
public static databases: string = "/dbs/{db_id}";
|
public static sprocHash: string = "/sprocs/";
|
||||||
public static collections: string = "/dbs/{db_id}/colls/{coll_id}";
|
public static sprocs: string = HashRoutePrefixes.collections + HashRoutePrefixes.sprocHash + "{sproc_id}";
|
||||||
public static sprocHash: string = "/sprocs/";
|
public static docs: string = HashRoutePrefixes.collections + "/docs/{doc_id}/";
|
||||||
public static sprocs: string = HashRoutePrefixes.collections + HashRoutePrefixes.sprocHash + "{sproc_id}";
|
public static conflicts: string = HashRoutePrefixes.collections + "/conflicts";
|
||||||
public static docs: string = HashRoutePrefixes.collections + "/docs/{doc_id}/";
|
|
||||||
public static conflicts: string = HashRoutePrefixes.collections + "/conflicts";
|
public static databasesWithId(databaseId: string): string {
|
||||||
|
return this.databases.replace("{db_id}", databaseId).replace("/", ""); // strip the first slash since hasher adds it
|
||||||
public static databasesWithId(databaseId: string): string {
|
}
|
||||||
return this.databases.replace("{db_id}", databaseId).replace("/", ""); // strip the first slash since hasher adds it
|
|
||||||
}
|
public static collectionsWithIds(databaseId: string, collectionId: string): string {
|
||||||
|
const transformedDatabasePrefix: string = this.collections.replace("{db_id}", databaseId);
|
||||||
public static collectionsWithIds(databaseId: string, collectionId: string): string {
|
|
||||||
const transformedDatabasePrefix: string = this.collections.replace("{db_id}", databaseId);
|
return transformedDatabasePrefix.replace("{coll_id}", collectionId).replace("/", ""); // strip the first slash since hasher adds it
|
||||||
|
}
|
||||||
return transformedDatabasePrefix.replace("{coll_id}", collectionId).replace("/", ""); // strip the first slash since hasher adds it
|
|
||||||
}
|
public static sprocWithIds(
|
||||||
|
databaseId: string,
|
||||||
public static sprocWithIds(
|
collectionId: string,
|
||||||
databaseId: string,
|
sprocId: string,
|
||||||
collectionId: string,
|
stripFirstSlash: boolean = true
|
||||||
sprocId: string,
|
): string {
|
||||||
stripFirstSlash: boolean = true
|
const transformedDatabasePrefix: string = this.sprocs.replace("{db_id}", databaseId);
|
||||||
): string {
|
|
||||||
const transformedDatabasePrefix: string = this.sprocs.replace("{db_id}", databaseId);
|
const transformedSprocRoute: string = transformedDatabasePrefix
|
||||||
|
.replace("{coll_id}", collectionId)
|
||||||
const transformedSprocRoute: string = transformedDatabasePrefix
|
.replace("{sproc_id}", sprocId);
|
||||||
.replace("{coll_id}", collectionId)
|
if (!!stripFirstSlash) {
|
||||||
.replace("{sproc_id}", sprocId);
|
return transformedSprocRoute.replace("/", ""); // strip the first slash since hasher adds it
|
||||||
if (!!stripFirstSlash) {
|
}
|
||||||
return transformedSprocRoute.replace("/", ""); // strip the first slash since hasher adds it
|
|
||||||
}
|
return transformedSprocRoute;
|
||||||
|
}
|
||||||
return transformedSprocRoute;
|
|
||||||
}
|
public static conflictsWithIds(databaseId: string, collectionId: string) {
|
||||||
|
const transformedDatabasePrefix: string = this.conflicts.replace("{db_id}", databaseId);
|
||||||
public static conflictsWithIds(databaseId: string, collectionId: string) {
|
|
||||||
const transformedDatabasePrefix: string = this.conflicts.replace("{db_id}", databaseId);
|
return transformedDatabasePrefix.replace("{coll_id}", collectionId).replace("/", ""); // strip the first slash since hasher adds it;
|
||||||
|
}
|
||||||
return transformedDatabasePrefix.replace("{coll_id}", collectionId).replace("/", ""); // strip the first slash since hasher adds it;
|
|
||||||
}
|
public static docsWithIds(databaseId: string, collectionId: string, docId: string) {
|
||||||
|
const transformedDatabasePrefix: string = this.docs.replace("{db_id}", databaseId);
|
||||||
public static docsWithIds(databaseId: string, collectionId: string, docId: string) {
|
|
||||||
const transformedDatabasePrefix: string = this.docs.replace("{db_id}", databaseId);
|
return transformedDatabasePrefix
|
||||||
|
.replace("{coll_id}", collectionId)
|
||||||
return transformedDatabasePrefix
|
.replace("{doc_id}", docId)
|
||||||
.replace("{coll_id}", collectionId)
|
.replace("/", ""); // strip the first slash since hasher adds it
|
||||||
.replace("{doc_id}", docId)
|
}
|
||||||
.replace("/", ""); // strip the first slash since hasher adds it
|
}
|
||||||
}
|
|
||||||
}
|
export class ConfigurationOverridesValues {
|
||||||
|
public static IsBsonSchemaV2: string = "true";
|
||||||
export class ConfigurationOverridesValues {
|
}
|
||||||
public static IsBsonSchemaV2: string = "true";
|
|
||||||
}
|
export class KeyCodes {
|
||||||
|
public static Space: number = 32;
|
||||||
export class KeyCodes {
|
public static Enter: number = 13;
|
||||||
public static Space: number = 32;
|
public static Escape: number = 27;
|
||||||
public static Enter: number = 13;
|
public static UpArrow: number = 38;
|
||||||
public static Escape: number = 27;
|
public static DownArrow: number = 40;
|
||||||
public static UpArrow: number = 38;
|
public static LeftArrow: number = 37;
|
||||||
public static DownArrow: number = 40;
|
public static RightArrow: number = 39;
|
||||||
public static LeftArrow: number = 37;
|
public static Tab: number = 9;
|
||||||
public static RightArrow: number = 39;
|
}
|
||||||
public static Tab: number = 9;
|
|
||||||
}
|
// Normalized per: https://www.w3.org/TR/uievents-key/#named-key-attribute-values
|
||||||
|
export class NormalizedEventKey {
|
||||||
// Normalized per: https://www.w3.org/TR/uievents-key/#named-key-attribute-values
|
public static readonly Space = " ";
|
||||||
export class NormalizedEventKey {
|
public static readonly Enter = "Enter";
|
||||||
public static readonly Space = " ";
|
public static readonly Escape = "Escape";
|
||||||
public static readonly Enter = "Enter";
|
public static readonly UpArrow = "ArrowUp";
|
||||||
public static readonly Escape = "Escape";
|
public static readonly DownArrow = "ArrowDown";
|
||||||
public static readonly UpArrow = "ArrowUp";
|
public static readonly LeftArrow = "ArrowLeft";
|
||||||
public static readonly DownArrow = "ArrowDown";
|
public static readonly RightArrow = "ArrowRight";
|
||||||
public static readonly LeftArrow = "ArrowLeft";
|
}
|
||||||
public static readonly RightArrow = "ArrowRight";
|
|
||||||
}
|
export class TryCosmosExperience {
|
||||||
|
public static extendUrl: string = "https://trycosmosdb.azure.com/api/resource/extendportal?userId={0}";
|
||||||
export class TryCosmosExperience {
|
public static deleteUrl: string = "https://trycosmosdb.azure.com/api/resource/deleteportal?userId={0}";
|
||||||
public static extendUrl: string = "https://trycosmosdb.azure.com/api/resource/extendportal?userId={0}";
|
public static collectionsPerAccount: number = 3;
|
||||||
public static deleteUrl: string = "https://trycosmosdb.azure.com/api/resource/deleteportal?userId={0}";
|
public static maxRU: number = 5000;
|
||||||
public static collectionsPerAccount: number = 3;
|
public static defaultRU: number = 3000;
|
||||||
public static maxRU: number = 5000;
|
}
|
||||||
public static defaultRU: number = 3000;
|
|
||||||
}
|
export class OfferVersions {
|
||||||
|
public static V1: string = "V1";
|
||||||
export class OfferVersions {
|
public static V2: string = "V2";
|
||||||
public static V1: string = "V1";
|
}
|
||||||
public static V2: string = "V2";
|
|
||||||
}
|
export enum ConflictOperationType {
|
||||||
|
Replace = "replace",
|
||||||
export enum ConflictOperationType {
|
Create = "create",
|
||||||
Replace = "replace",
|
Delete = "delete"
|
||||||
Create = "create",
|
}
|
||||||
Delete = "delete"
|
|
||||||
}
|
export const EmulatorMasterKey =
|
||||||
|
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
|
||||||
export const EmulatorMasterKey =
|
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
|
||||||
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
|
|
||||||
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
|
// A variable @MyVariable defined in Constants.less is accessible as StyleConstants.MyVariable
|
||||||
|
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
|
||||||
// A variable @MyVariable defined in Constants.less is accessible as StyleConstants.MyVariable
|
|
||||||
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
|
export class Notebook {
|
||||||
|
public static readonly defaultBasePath = "./notebooks";
|
||||||
export class Notebook {
|
public static readonly heartbeatDelayMs = 5000;
|
||||||
public static readonly defaultBasePath = "./notebooks";
|
public static readonly kernelRestartInitialDelayMs = 1000;
|
||||||
public static readonly heartbeatDelayMs = 5000;
|
public static readonly kernelRestartMaxDelayMs = 20000;
|
||||||
public static readonly kernelRestartInitialDelayMs = 1000;
|
public static readonly autoSaveIntervalMs = 120000;
|
||||||
public static readonly kernelRestartMaxDelayMs = 20000;
|
}
|
||||||
public static readonly autoSaveIntervalMs = 120000;
|
|
||||||
}
|
export class SparkLibrary {
|
||||||
|
public static readonly nameMinLength = 3;
|
||||||
export class SparkLibrary {
|
public static readonly nameMaxLength = 63;
|
||||||
public static readonly nameMinLength = 3;
|
}
|
||||||
public static readonly nameMaxLength = 63;
|
|
||||||
}
|
export class AnalyticalStorageTtl {
|
||||||
|
public static readonly Days90: number = 7776000;
|
||||||
export class AnalyticalStorageTtl {
|
public static readonly Infinite: number = -1;
|
||||||
public static readonly Days90: number = 7776000;
|
public static readonly Disabled: number = 0;
|
||||||
public static readonly Infinite: number = -1;
|
}
|
||||||
public static readonly Disabled: number = 0;
|
|
||||||
}
|
export class TerminalQueryParams {
|
||||||
|
public static readonly Terminal = "terminal";
|
||||||
export class TerminalQueryParams {
|
public static readonly Server = "server";
|
||||||
public static readonly Terminal = "terminal";
|
public static readonly Token = "token";
|
||||||
public static readonly Server = "server";
|
public static readonly SubscriptionId = "subscriptionId";
|
||||||
public static readonly Token = "token";
|
public static readonly TerminalEndpoint = "terminalEndpoint";
|
||||||
public static readonly SubscriptionId = "subscriptionId";
|
}
|
||||||
public static readonly TerminalEndpoint = "terminalEndpoint";
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -21,7 +21,7 @@ export const handleError = (error: string | ARMError | Error, area: string, cons
|
|||||||
sendNotificationForError(errorMessage, errorCode);
|
sendNotificationForError(errorMessage, errorCode);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getErrorMessage = (error: string | Error): string => {
|
export const getErrorMessage = (error: string | Error = ""): string => {
|
||||||
const errorMessage = typeof error === "string" ? error : error.message;
|
const errorMessage = typeof error === "string" ? error : error.message;
|
||||||
return replaceKnownError(errorMessage);
|
return replaceKnownError(errorMessage);
|
||||||
};
|
};
|
||||||
@@ -45,10 +45,10 @@ const sendNotificationForError = (errorMessage: string, errorCode: number | stri
|
|||||||
const replaceKnownError = (errorMessage: string): string => {
|
const replaceKnownError = (errorMessage: string): string => {
|
||||||
if (
|
if (
|
||||||
window.dataExplorer?.subscriptionType() === SubscriptionType.Internal &&
|
window.dataExplorer?.subscriptionType() === SubscriptionType.Internal &&
|
||||||
errorMessage.indexOf("SharedOffer is Disabled for your account") >= 0
|
errorMessage?.indexOf("SharedOffer is Disabled for your account") >= 0
|
||||||
) {
|
) {
|
||||||
return "Database throughput is not supported for internal subscriptions.";
|
return "Database throughput is not supported for internal subscriptions.";
|
||||||
} else if (errorMessage.indexOf("Partition key paths must contain only valid") >= 0) {
|
} else if (errorMessage?.indexOf("Partition key paths must contain only valid") >= 0) {
|
||||||
return "Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character.";
|
return "Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
37
src/Common/OfferUtility.ts
Normal file
37
src/Common/OfferUtility.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Offer, SDKOfferDefinition } from "../Contracts/DataModels";
|
||||||
|
import { OfferResponse } from "@azure/cosmos";
|
||||||
|
import { HttpHeaders } from "./Constants";
|
||||||
|
|
||||||
|
export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer | undefined => {
|
||||||
|
const offerDefinition: SDKOfferDefinition | undefined = offerResponse?.resource;
|
||||||
|
if (!offerDefinition) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const offerContent = offerDefinition.content;
|
||||||
|
if (!offerContent) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const minimumThroughput = offerContent.collectionThroughputInfo?.minimumRUForCollection;
|
||||||
|
const autopilotSettings = offerContent.offerAutopilotSettings;
|
||||||
|
|
||||||
|
if (autopilotSettings && autopilotSettings.maxThroughput && minimumThroughput) {
|
||||||
|
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 {
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ export class Splitter {
|
|||||||
public splitterId: string;
|
public splitterId: string;
|
||||||
public leftSideId: string;
|
public leftSideId: string;
|
||||||
|
|
||||||
public splitter: HTMLElement;
|
public splitter!: HTMLElement;
|
||||||
public leftSide: HTMLElement;
|
public leftSide!: HTMLElement;
|
||||||
public lastX: number;
|
public lastX!: number;
|
||||||
public lastWidth: number;
|
public lastWidth!: number;
|
||||||
|
|
||||||
private isCollapsed: ko.Observable<boolean>;
|
private isCollapsed: ko.Observable<boolean>;
|
||||||
private bounds: SplitterBounds;
|
private bounds: SplitterBounds;
|
||||||
@@ -42,9 +42,10 @@ export class Splitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public initialize() {
|
public initialize() {
|
||||||
this.splitter = document.getElementById(this.splitterId);
|
if (document.getElementById(this.splitterId) !== null && document.getElementById(this.leftSideId) != null) {
|
||||||
this.leftSide = document.getElementById(this.leftSideId);
|
this.splitter = <HTMLElement>document.getElementById(this.splitterId);
|
||||||
|
this.leftSide = <HTMLElement>document.getElementById(this.leftSideId);
|
||||||
|
}
|
||||||
const isVerticalSplitter: boolean = this.direction === SplitterDirection.Vertical;
|
const isVerticalSplitter: boolean = this.direction === SplitterDirection.Vertical;
|
||||||
const splitterOptions: JQueryUI.ResizableOptions = {
|
const splitterOptions: JQueryUI.ResizableOptions = {
|
||||||
animate: true,
|
animate: true,
|
||||||
|
|||||||
@@ -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 | undefined;
|
||||||
|
manualThroughput: number | undefined;
|
||||||
|
minimumThroughput: number | undefined;
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -15,8 +15,10 @@ import DocumentId from "../Explorer/Tree/DocumentId";
|
|||||||
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
||||||
import Trigger from "../Explorer/Tree/Trigger";
|
import Trigger from "../Explorer/Tree/Trigger";
|
||||||
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
|
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
|
||||||
|
import { SelfServeType } from "../SelfServe/SelfServeUtils";
|
||||||
import { UploadDetails } from "../workers/upload/definitions";
|
import { UploadDetails } from "../workers/upload/definitions";
|
||||||
import * as DataModels from "./DataModels";
|
import * as DataModels from "./DataModels";
|
||||||
|
import { SubscriptionType } from "./SubscriptionType";
|
||||||
|
|
||||||
export interface TokenProvider {
|
export interface TokenProvider {
|
||||||
getAuthHeader(): Promise<Headers>;
|
getAuthHeader(): Promise<Headers>;
|
||||||
@@ -115,9 +117,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 +362,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 {
|
||||||
@@ -391,6 +396,7 @@ export interface DataExplorerInputsFrame {
|
|||||||
isAuthWithresourceToken?: boolean;
|
isAuthWithresourceToken?: boolean;
|
||||||
defaultCollectionThroughput?: CollectionCreationDefaults;
|
defaultCollectionThroughput?: CollectionCreationDefaults;
|
||||||
flights?: readonly string[];
|
flights?: readonly string[];
|
||||||
|
selfServeType?: SelfServeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionCreationDefaults {
|
export interface CollectionCreationDefaults {
|
||||||
@@ -412,14 +418,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;
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ interface CollapsiblePanelParams {
|
|||||||
* Use the optional "collapseToLeft" parameter to collapse to the left.
|
* Use the optional "collapseToLeft" parameter to collapse to the left.
|
||||||
*/
|
*/
|
||||||
class CollapsiblePanelViewModel {
|
class CollapsiblePanelViewModel {
|
||||||
private params: CollapsiblePanelParams;
|
public params: CollapsiblePanelParams;
|
||||||
private isCollapsed: ko.Observable<boolean>;
|
private isCollapsed: ko.Observable<boolean>;
|
||||||
|
|
||||||
public constructor(params: CollapsiblePanelParams) {
|
public constructor(params: CollapsiblePanelParams) {
|
||||||
@@ -50,7 +50,7 @@ class CollapsiblePanelViewModel {
|
|||||||
this.isCollapsed = params.isCollapsed || ko.observable(false);
|
this.isCollapsed = params.isCollapsed || ko.observable(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private toggleCollapse(): void {
|
public toggleCollapse(): void {
|
||||||
this.isCollapsed(!this.isCollapsed());
|
this.isCollapsed(!this.isCollapsed());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,12 +44,11 @@ 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.selfServeType", label: "Self serve feature", value: "sample" },
|
||||||
{
|
{
|
||||||
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,8 +157,8 @@ exports[`Feature panel renders all flags 1`] = `
|
|||||||
/>
|
/>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
checked={false}
|
checked={false}
|
||||||
key="feature.enablecodeofconduct"
|
key="feature.selfServeType"
|
||||||
label="Enable Code Of Conduct Acknowledgement"
|
label="Self serve feature"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
/>
|
/>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ interface InputTypeaheadParams {
|
|||||||
/**
|
/**
|
||||||
* This function gets called when pressing ENTER on the input box
|
* This function gets called when pressing ENTER on the input box
|
||||||
*/
|
*/
|
||||||
submitFct?: (inputValue: string, selection: Item) => void;
|
submitFct?: (inputValue: string | null, selection: Item | null) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Typehead comes with a Search button that we normally remove.
|
* Typehead comes with a Search button that we normally remove.
|
||||||
@@ -88,8 +88,8 @@ interface OnClickItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Cache {
|
interface Cache {
|
||||||
inputValue: string;
|
inputValue: string | null;
|
||||||
selection: Item;
|
selection: Item | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
class InputTypeaheadViewModel {
|
class InputTypeaheadViewModel {
|
||||||
@@ -98,15 +98,12 @@ class InputTypeaheadViewModel {
|
|||||||
private params: InputTypeaheadParams;
|
private params: InputTypeaheadParams;
|
||||||
|
|
||||||
private cache: Cache;
|
private cache: Cache;
|
||||||
private inputValue: string;
|
|
||||||
private selection: Item;
|
|
||||||
|
|
||||||
public constructor(params: InputTypeaheadParams) {
|
public constructor(params: InputTypeaheadParams) {
|
||||||
this.instanceNumber = InputTypeaheadViewModel.instanceCount++;
|
this.instanceNumber = InputTypeaheadViewModel.instanceCount++;
|
||||||
this.params = params;
|
this.params = params;
|
||||||
|
|
||||||
this.params.choices.subscribe(this.initializeTypeahead.bind(this));
|
this.params.choices.subscribe(this.initializeTypeahead.bind(this));
|
||||||
|
|
||||||
this.cache = {
|
this.cache = {
|
||||||
inputValue: null,
|
inputValue: null,
|
||||||
selection: null
|
selection: null
|
||||||
@@ -161,7 +158,7 @@ class InputTypeaheadViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$.typeahead(options);
|
($ as any).typeahead(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -177,11 +174,11 @@ class InputTypeaheadViewModel {
|
|||||||
* Use ko's "template: afterRender" callback to do that without actually using any template.
|
* Use ko's "template: afterRender" callback to do that without actually using any template.
|
||||||
* Another way is to call it within setTimeout() in constructor.
|
* Another way is to call it within setTimeout() in constructor.
|
||||||
*/
|
*/
|
||||||
private afterRender(): void {
|
public afterRender(): void {
|
||||||
this.initializeTypeahead();
|
this.initializeTypeahead();
|
||||||
}
|
}
|
||||||
|
|
||||||
private submit(): void {
|
public submit(): void {
|
||||||
if (this.params.submitFct) {
|
if (this.params.submitFct) {
|
||||||
this.params.submitFct(this.cache.inputValue, this.cache.selection);
|
this.params.submitFct(this.cache.inputValue, this.cache.selection);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,10 +59,12 @@ export class JsonEditorViewModel extends WaitsForTemplateViewModel {
|
|||||||
this.params = params;
|
this.params = params;
|
||||||
|
|
||||||
this.params.content.subscribe((newValue: string) => {
|
this.params.content.subscribe((newValue: string) => {
|
||||||
if (!!this.editor) {
|
if (newValue) {
|
||||||
this.editor.getModel().setValue(newValue);
|
if (!!this.editor) {
|
||||||
} else {
|
this.editor.getModel().setValue(newValue);
|
||||||
this.createEditor(newValue, this.configureEditor.bind(this));
|
} else {
|
||||||
|
this.createEditor(newValue, this.configureEditor.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 });
|
||||||
@@ -246,7 +231,7 @@ describe("SettingsComponent", () => {
|
|||||||
|
|
||||||
it("getUpdatedConflictResolutionPolicy", () => {
|
it("getUpdatedConflictResolutionPolicy", () => {
|
||||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
||||||
const conflictResolutionPolicyPath = "_ts";
|
const conflictResolutionPolicyPath = "/_ts";
|
||||||
const conflictResolutionPolicyProcedure = "sample_sproc";
|
const conflictResolutionPolicyProcedure = "sample_sproc";
|
||||||
const expectSprocPath =
|
const expectSprocPath =
|
||||||
"/dbs/" + collection.databaseId + "/colls/" + collection.id() + "/sprocs/" + conflictResolutionPolicyProcedure;
|
"/dbs/" + collection.databaseId + "/colls/" + collection.id() + "/sprocs/" + conflictResolutionPolicyProcedure;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -142,8 +138,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
|
|
||||||
// Mongo container with system partition key still treat as "Fixed"
|
// Mongo container with system partition key still treat as "Fixed"
|
||||||
this.isFixedContainer =
|
this.isFixedContainer =
|
||||||
!this.collection.partitionKey ||
|
this.container.isPreferredApiMongoDB() &&
|
||||||
(this.container.isPreferredApiMongoDB() && this.collection.partitionKey.systemKey);
|
(!this.collection.partitionKey || this.collection.partitionKey.systemKey);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
throughput: undefined,
|
throughput: undefined,
|
||||||
@@ -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();
|
||||||
@@ -768,7 +684,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
|
|
||||||
if (policy.mode === DataModels.ConflictResolutionMode.LastWriterWins) {
|
if (policy.mode === DataModels.ConflictResolutionMode.LastWriterWins) {
|
||||||
policy.conflictResolutionPath = this.state.conflictResolutionPolicyPath;
|
policy.conflictResolutionPath = this.state.conflictResolutionPolicyPath;
|
||||||
if (policy.conflictResolutionPath?.startsWith("/")) {
|
if (!policy.conflictResolutionPath?.startsWith("/")) {
|
||||||
policy.conflictResolutionPath = "/" + policy.conflictResolutionPath;
|
policy.conflictResolutionPath = "/" + policy.conflictResolutionPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,11 +31,42 @@ 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 const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } };
|
export interface EstimatedSpendingDisplayProps {
|
||||||
|
costType: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ManualEstimatedSpendingDisplayProps extends EstimatedSpendingDisplayProps {
|
||||||
|
hourly: JSX.Element;
|
||||||
|
daily: JSX.Element;
|
||||||
|
monthly: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutoscaleEstimatedSpendingDisplayProps extends EstimatedSpendingDisplayProps {
|
||||||
|
minPerMonth: JSX.Element;
|
||||||
|
maxPerMonth: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PriceBreakdown {
|
||||||
|
hourlyPrice: number;
|
||||||
|
dailyPrice: number;
|
||||||
|
monthlyPrice: number;
|
||||||
|
pricePerRu: number;
|
||||||
|
currency: string;
|
||||||
|
currencySign: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14 } };
|
||||||
|
|
||||||
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
||||||
label: {
|
label: {
|
||||||
@@ -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: {
|
||||||
@@ -126,10 +166,17 @@ export const separatorStyles: Partial<ISeparatorStyles> = {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
export const messageBarStyles: Partial<IMessageBarStyles> = { root: { marginTop: "5px" } };
|
export const messageBarStyles: Partial<IMessageBarStyles> = {
|
||||||
|
root: { marginTop: "5px", backgroundColor: "white" },
|
||||||
|
text: { fontSize: 14 }
|
||||||
|
};
|
||||||
|
|
||||||
export const throughputUnit = "RU/s";
|
export const throughputUnit = "RU/s";
|
||||||
|
|
||||||
|
export function onRenderRow(props: IDetailsRowProps): JSX.Element {
|
||||||
|
return <DetailsRow {...props} styles={transparentDetailsRowStyles} />;
|
||||||
|
}
|
||||||
|
|
||||||
export const getAutoPilotV3SpendElement = (
|
export const getAutoPilotV3SpendElement = (
|
||||||
maxAutoPilotThroughputSet: number,
|
maxAutoPilotThroughputSet: number,
|
||||||
isDatabaseThroughput: boolean,
|
isDatabaseThroughput: boolean,
|
||||||
@@ -165,64 +212,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 +310,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 +370,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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ exports[`IndexingPolicyRefreshComponent renders 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"fontSize": 12,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,12 +12,11 @@ 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 { Link, Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
|
||||||
import { configContext, Platform } from "../../../../ConfigContext";
|
import { configContext, Platform } from "../../../../ConfigContext";
|
||||||
|
|
||||||
export interface ScaleComponentProps {
|
export interface ScaleComponentProps {
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -179,17 +165,20 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
private getThroughputInputComponent = (): JSX.Element => (
|
private getThroughputInputComponent = (): JSX.Element => (
|
||||||
<ThroughputInputAutoPilotV3Component
|
<ThroughputInputAutoPilotV3Component
|
||||||
databaseAccount={this.props.container.databaseAccount()}
|
databaseAccount={this.props.container.databaseAccount()}
|
||||||
|
databaseName={this.props.collection.databaseId}
|
||||||
|
collectionName={this.props.collection.id()}
|
||||||
serverId={this.props.container.serverId()}
|
serverId={this.props.container.serverId()}
|
||||||
throughput={this.props.throughput}
|
throughput={this.props.throughput}
|
||||||
throughputBaseline={this.props.throughputBaseline}
|
throughputBaseline={this.props.throughputBaseline}
|
||||||
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()}
|
||||||
isEmulator={this.isEmulator}
|
isEmulator={this.isEmulator}
|
||||||
isFixed={this.props.isFixedContainer}
|
isFixed={this.props.isFixedContainer}
|
||||||
|
isFreeTierAccount={this.isFreeTierAccount()}
|
||||||
isAutoPilotSelected={this.props.isAutoPilotSelected}
|
isAutoPilotSelected={this.props.isAutoPilotSelected}
|
||||||
onAutoPilotSelected={this.props.onAutoPilotSelected}
|
onAutoPilotSelected={this.props.onAutoPilotSelected}
|
||||||
wasAutopilotOriginallySet={this.props.wasAutopilotOriginallySet}
|
wasAutopilotOriginallySet={this.props.wasAutopilotOriginallySet}
|
||||||
@@ -200,12 +189,41 @@ 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()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private isFreeTierAccount(): boolean {
|
||||||
|
const databaseAccount = this.props.container?.databaseAccount();
|
||||||
|
return databaseAccount?.properties?.enableFreeTier;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFreeTierInfoMessage(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Text>
|
||||||
|
With free tier, you will get the first 400 RU/s and 5 GB of storage in this account for free. To keep your
|
||||||
|
account free, keep the total RU/s across all resources in the account to 400 RU/s.
|
||||||
|
<Link
|
||||||
|
href="https://docs.microsoft.com/en-us/azure/cosmos-db/understand-your-bill#billing-examples-with-free-tier-accounts"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Learn more.
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Stack {...subComponentStackProps}>
|
<Stack {...subComponentStackProps}>
|
||||||
|
{this.isFreeTierAccount() && (
|
||||||
|
<MessageBar
|
||||||
|
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
|
||||||
|
styles={{ text: { fontSize: 14 } }}
|
||||||
|
>
|
||||||
|
{this.getFreeTierInfoMessage()}
|
||||||
|
</MessageBar>
|
||||||
|
)}
|
||||||
{this.getInitialNotificationElement() && (
|
{this.getInitialNotificationElement() && (
|
||||||
<MessageBar messageBarType={MessageBarType.warning}>{this.getInitialNotificationElement()}</MessageBar>
|
<MessageBar messageBarType={MessageBarType.warning}>{this.getInitialNotificationElement()}</MessageBar>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -13,16 +13,7 @@ import {
|
|||||||
} from "../SettingsUtils";
|
} from "../SettingsUtils";
|
||||||
import Explorer from "../../../Explorer";
|
import Explorer from "../../../Explorer";
|
||||||
import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||||
import {
|
import { Label, Text, TextField, Stack, IChoiceGroupOption, ChoiceGroup, MessageBar } from "office-ui-fabric-react";
|
||||||
Label,
|
|
||||||
Text,
|
|
||||||
TextField,
|
|
||||||
Stack,
|
|
||||||
IChoiceGroupOption,
|
|
||||||
ChoiceGroup,
|
|
||||||
MessageBar,
|
|
||||||
MessageBarType
|
|
||||||
} from "office-ui-fabric-react";
|
|
||||||
import {
|
import {
|
||||||
getTextFieldStyles,
|
getTextFieldStyles,
|
||||||
changeFeedPolicyToolTip,
|
changeFeedPolicyToolTip,
|
||||||
@@ -190,7 +181,10 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
|||||||
styles={getChoiceGroupStyles(this.props.timeToLive, this.props.timeToLiveBaseline)}
|
styles={getChoiceGroupStyles(this.props.timeToLive, this.props.timeToLiveBaseline)}
|
||||||
/>
|
/>
|
||||||
{isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && (
|
{isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && (
|
||||||
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
|
<MessageBar
|
||||||
|
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
|
||||||
|
styles={messageBarStyles}
|
||||||
|
>
|
||||||
{ttlWarning}
|
{ttlWarning}
|
||||||
</MessageBar>
|
</MessageBar>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import * as DataModels from "../../../../../Contracts/DataModels";
|
|||||||
describe("ThroughputInputAutoPilotV3Component", () => {
|
describe("ThroughputInputAutoPilotV3Component", () => {
|
||||||
const baseProps: ThroughputInputAutoPilotV3Props = {
|
const baseProps: ThroughputInputAutoPilotV3Props = {
|
||||||
databaseAccount: {} as DataModels.DatabaseAccount,
|
databaseAccount: {} as DataModels.DatabaseAccount,
|
||||||
|
databaseName: "test",
|
||||||
|
collectionName: "test",
|
||||||
serverId: undefined,
|
serverId: undefined,
|
||||||
wasAutopilotOriginallySet: false,
|
wasAutopilotOriginallySet: false,
|
||||||
throughput: 100,
|
throughput: 100,
|
||||||
@@ -17,6 +19,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,
|
||||||
@@ -25,6 +28,7 @@ describe("ThroughputInputAutoPilotV3Component", () => {
|
|||||||
spendAckVisible: false,
|
spendAckVisible: false,
|
||||||
showAsMandatory: true,
|
showAsMandatory: true,
|
||||||
isFixed: false,
|
isFixed: false,
|
||||||
|
isFreeTierAccount: false,
|
||||||
label: "label",
|
label: "label",
|
||||||
infoBubbleText: "infoBubbleText",
|
infoBubbleText: "infoBubbleText",
|
||||||
canExceedMaximumValue: true,
|
canExceedMaximumValue: true,
|
||||||
@@ -53,7 +57,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 +74,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,16 +28,26 @@ import {
|
|||||||
Label,
|
Label,
|
||||||
Link,
|
Link,
|
||||||
MessageBar,
|
MessageBar,
|
||||||
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";
|
||||||
|
|
||||||
|
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
|
||||||
export interface ThroughputInputAutoPilotV3Props {
|
export interface ThroughputInputAutoPilotV3Props {
|
||||||
databaseAccount: DataModels.DatabaseAccount;
|
databaseAccount: DataModels.DatabaseAccount;
|
||||||
|
databaseName: string;
|
||||||
|
collectionName: string;
|
||||||
serverId: string;
|
serverId: string;
|
||||||
throughput: number;
|
throughput: number;
|
||||||
throughputBaseline: number;
|
throughputBaseline: number;
|
||||||
@@ -47,6 +62,7 @@ export interface ThroughputInputAutoPilotV3Props {
|
|||||||
spendAckVisible?: boolean;
|
spendAckVisible?: boolean;
|
||||||
showAsMandatory?: boolean;
|
showAsMandatory?: boolean;
|
||||||
isFixed: boolean;
|
isFixed: boolean;
|
||||||
|
isFreeTierAccount: boolean;
|
||||||
isEmulator: boolean;
|
isEmulator: boolean;
|
||||||
label: string;
|
label: string;
|
||||||
infoBubbleText?: string;
|
infoBubbleText?: string;
|
||||||
@@ -60,10 +76,12 @@ 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 {
|
||||||
spendAckChecked: boolean;
|
spendAckChecked: boolean;
|
||||||
|
exceedFreeTierThroughput: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ThroughputInputAutoPilotV3Component extends React.Component<
|
export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||||
@@ -137,7 +155,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
public constructor(props: ThroughputInputAutoPilotV3Props) {
|
public constructor(props: ThroughputInputAutoPilotV3Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
spendAckChecked: this.props.spendAckChecked
|
spendAckChecked: this.props.spendAckChecked,
|
||||||
|
exceedFreeTierThroughput:
|
||||||
|
this.props.isFreeTierAccount && !this.props.isAutoPilotSelected && this.props.throughput > 400
|
||||||
};
|
};
|
||||||
|
|
||||||
this.step = this.props.step ?? ThroughputInputAutoPilotV3Component.defaultStep;
|
this.step = this.props.step ?? ThroughputInputAutoPilotV3Component.defaultStep;
|
||||||
@@ -160,34 +180,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 <></>;
|
||||||
@@ -203,7 +432,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
newValue?: string
|
newValue?: string
|
||||||
): void => {
|
): void => {
|
||||||
const newThroughput = getSanitizedInputValue(newValue, this.autoPilotInputMaxValue);
|
const newThroughput = getSanitizedInputValue(newValue);
|
||||||
this.props.onMaxAutoPilotThroughputChange(newThroughput);
|
this.props.onMaxAutoPilotThroughputChange(newThroughput);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -211,10 +440,11 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
newValue?: string
|
newValue?: string
|
||||||
): void => {
|
): void => {
|
||||||
const newThroughput = getSanitizedInputValue(newValue, this.throughputInputMaxValue);
|
const newThroughput = getSanitizedInputValue(newValue);
|
||||||
if (this.overrideWithAutoPilotSettings()) {
|
if (this.overrideWithAutoPilotSettings()) {
|
||||||
this.props.onMaxAutoPilotThroughputChange(newThroughput);
|
this.props.onMaxAutoPilotThroughputChange(newThroughput);
|
||||||
} else {
|
} else {
|
||||||
|
this.setState({ exceedFreeTierThroughput: this.props.isFreeTierAccount && newThroughput > 400 });
|
||||||
this.props.onThroughputChange(newThroughput);
|
this.props.onThroughputChange(newThroughput);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -222,7 +452,42 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
private onChoiceGroupChange = (
|
private onChoiceGroupChange = (
|
||||||
event?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
event?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||||
option?: IChoiceGroupOption
|
option?: IChoiceGroupOption
|
||||||
): void => this.props.onAutoPilotSelected(option.key === "true");
|
): void => {
|
||||||
|
this.props.onAutoPilotSelected(option.key === "true");
|
||||||
|
TelemetryProcessor.trace(Action.ToggleAutoscaleSetting, ActionModifiers.Mark, {
|
||||||
|
changedSelectedValueTo:
|
||||||
|
option.key === "true" ? ActionModifiers.ToggleAutoscaleOn : ActionModifiers.ToggleAutoscaleOff,
|
||||||
|
subscriptionId: userContext.subscriptionId,
|
||||||
|
databaseAccountName: this.props.databaseAccount?.name,
|
||||||
|
databaseName: this.props.databaseName,
|
||||||
|
collectionName: this.props.collectionName,
|
||||||
|
apiKind: userContext.defaultExperience,
|
||||||
|
dataExplorerArea: "Scale Tab V2"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private minRUperGBSurvey = (): JSX.Element => {
|
||||||
|
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";
|
||||||
@@ -235,7 +500,10 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
/>
|
/>
|
||||||
</Label>
|
</Label>
|
||||||
{this.overrideWithProvisionedThroughputSettings() && (
|
{this.overrideWithProvisionedThroughputSettings() && (
|
||||||
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
|
<MessageBar
|
||||||
|
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
|
||||||
|
styles={messageBarStyles}
|
||||||
|
>
|
||||||
{manualToAutoscaleDisclaimerElement}
|
{manualToAutoscaleDisclaimerElement}
|
||||||
</MessageBar>
|
</MessageBar>
|
||||||
)}
|
)}
|
||||||
@@ -275,6 +543,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 +559,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 +580,26 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
}
|
}
|
||||||
onChange={this.onThroughputChange}
|
onChange={this.onThroughputChange}
|
||||||
/>
|
/>
|
||||||
|
{this.state.exceedFreeTierThroughput && (
|
||||||
|
<MessageBar
|
||||||
|
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
|
||||||
|
styles={messageBarStyles}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
"Billing will apply if you provision more than 400 RU/s of manual throughput, or if the resource scales beyond 400 RU/s with autoscale."
|
||||||
|
}
|
||||||
|
</MessageBar>
|
||||||
|
)}
|
||||||
{this.props.getThroughputWarningMessage() && (
|
{this.props.getThroughputWarningMessage() && (
|
||||||
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
|
<MessageBar
|
||||||
|
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
|
||||||
|
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 +609,32 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
onChange={this.onSpendAckChecked}
|
onChange={this.onSpendAckChecked}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<br />
|
||||||
{this.props.isFixed && <p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>}
|
{this.props.isFixed && <p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private renderWarningMessage = (): JSX.Element => {
|
||||||
|
let warningMessage: JSX.Element;
|
||||||
|
if (this.IsComponentDirty().isDiscardable) {
|
||||||
|
warningMessage = saveThroughputWarningMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{warningMessage && (
|
||||||
|
<MessageBar messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}>
|
||||||
|
{warningMessage}
|
||||||
|
</MessageBar>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Stack {...checkBoxAndInputStackProps}>
|
<Stack {...checkBoxAndInputStackProps}>
|
||||||
|
{this.renderWarningMessage()}
|
||||||
{this.renderThroughputModeChoices()}
|
{this.renderThroughputModeChoices()}
|
||||||
|
|
||||||
{this.props.isAutoPilotSelected ? this.renderAutoPilotInput() : this.renderThroughputInput()}
|
{this.props.isAutoPilotSelected ? this.renderAutoPilotInput() : this.renderThroughputInput()}
|
||||||
|
|||||||
@@ -8,6 +8,26 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<StyledMessageBarBase
|
||||||
|
messageBarIconProps={
|
||||||
|
Object {
|
||||||
|
"className": "messageBarWarningIcon",
|
||||||
|
"iconName": "WarningSolid",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"fontSize": 14,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below before saving your changes
|
||||||
|
</Text>
|
||||||
|
</StyledMessageBarBase>
|
||||||
<Stack>
|
<Stack>
|
||||||
<StyledLabelBase
|
<StyledLabelBase
|
||||||
id="settingsV2RadioButtonLabelId"
|
id="settingsV2RadioButtonLabelId"
|
||||||
@@ -19,7 +39,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"fontSize": 12,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,12 +50,21 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
|||||||
/>
|
/>
|
||||||
</StyledLabelBase>
|
</StyledLabelBase>
|
||||||
<StyledMessageBarBase
|
<StyledMessageBarBase
|
||||||
messageBarType={5}
|
messageBarIconProps={
|
||||||
|
Object {
|
||||||
|
"className": "messageBarInfoIcon",
|
||||||
|
"iconName": "InfoSolid",
|
||||||
|
}
|
||||||
|
}
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"backgroundColor": "white",
|
||||||
"marginTop": "5px",
|
"marginTop": "5px",
|
||||||
},
|
},
|
||||||
|
"text": Object {
|
||||||
|
"fontSize": 14,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -44,7 +73,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"fontSize": 12,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,7 +185,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"fontSize": 12,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,6 +243,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 +281,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 +434,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<br />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
`;
|
`;
|
||||||
@@ -311,7 +458,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"fontSize": 12,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,6 +516,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 +554,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 {
|
||||||
@@ -16,7 +16,7 @@ exports[`ScaleComponent renders with correct intiial notification 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"fontSize": 12,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,6 +40,8 @@ exports[`ScaleComponent renders with correct intiial notification 1`] = `
|
|||||||
>
|
>
|
||||||
<ThroughputInputAutoPilotV3Component
|
<ThroughputInputAutoPilotV3Component
|
||||||
canExceedMaximumValue={true}
|
canExceedMaximumValue={true}
|
||||||
|
collectionName="test"
|
||||||
|
databaseName="test"
|
||||||
getThroughputWarningMessage={[Function]}
|
getThroughputWarningMessage={[Function]}
|
||||||
isAutoPilotSelected={false}
|
isAutoPilotSelected={false}
|
||||||
isEmulator={false}
|
isEmulator={false}
|
||||||
@@ -48,7 +50,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 +60,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
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"fontSize": 12,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -412,7 +412,7 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"fontSize": 12,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -952,7 +952,7 @@ exports[`SubSettingsComponent renders 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"fontSize": 12,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1228,7 +1228,7 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"fontSize": 12,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user