mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-24 11:21:23 +00:00
Compare commits
4 Commits
steve-self
...
users/srna
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e8793d1b1 | ||
|
|
f41de718a0 | ||
|
|
5820afc25a | ||
|
|
6d94f18f9a |
@@ -3,11 +3,7 @@ PORTAL_RUNNER_PASSWORD=
|
||||
PORTAL_RUNNER_SUBSCRIPTION=
|
||||
PORTAL_RUNNER_RESOURCE_GROUP=
|
||||
PORTAL_RUNNER_DATABASE_ACCOUNT=
|
||||
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY=
|
||||
PORTAL_RUNNER_CONNECTION_STRING=
|
||||
NOTEBOOKS_TEST_RUNNER_TENANT_ID=
|
||||
NOTEBOOKS_TEST_RUNNER_CLIENT_ID=
|
||||
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET=
|
||||
CASSANDRA_CONNECTION_STRING=
|
||||
MONGO_CONNECTION_STRING=
|
||||
TABLES_CONNECTION_STRING=
|
||||
|
||||
@@ -14,6 +14,7 @@ src/Common/DataAccessUtilityBase.ts
|
||||
src/Common/DeleteFeedback.ts
|
||||
src/Common/DocumentClientUtilityBase.ts
|
||||
src/Common/EditableUtility.ts
|
||||
src/Common/EnvironmentUtility.ts
|
||||
src/Common/HashMap.test.ts
|
||||
src/Common/HashMap.ts
|
||||
src/Common/HeadersUtility.test.ts
|
||||
@@ -201,6 +202,8 @@ src/Explorer/Tabs/QueryTab.test.ts
|
||||
src/Explorer/Tabs/QueryTab.ts
|
||||
src/Explorer/Tabs/QueryTablesTab.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/StoredProcedureTab.ts
|
||||
src/Explorer/Tabs/TabComponents.ts
|
||||
@@ -287,6 +290,8 @@ src/Utils/DatabaseAccountUtils.ts
|
||||
src/Utils/JunoUtils.ts
|
||||
src/Utils/MessageValidation.ts
|
||||
src/Utils/NotebookConfigurationUtils.ts
|
||||
src/Utils/OfferUtils.test.ts
|
||||
src/Utils/OfferUtils.ts
|
||||
src/Utils/PricingUtils.test.ts
|
||||
src/Utils/QueryUtils.test.ts
|
||||
src/Utils/QueryUtils.ts
|
||||
@@ -391,5 +396,19 @@ src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx
|
||||
src/GalleryViewer/Cards/GalleryCardComponent.tsx
|
||||
src/GalleryViewer/GalleryViewer.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
|
||||
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
|
||||
path: dist/
|
||||
endtoendemulator:
|
||||
name: "End To End Emulator Tests"
|
||||
name: "End To End Tests | Emulator | SQL"
|
||||
needs: [lint, format, compile, unittest]
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: southpolesteve/cosmos-emulator-github-action@v1
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- uses: southpolesteve/cosmos-emulator-github-action@v1
|
||||
- name: Restore Cypress Binary Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/Cypress
|
||||
key: ${{ runner.os }}-cypress-binary-cache
|
||||
- name: End to End Tests
|
||||
run: |
|
||||
npm ci
|
||||
npm start &
|
||||
npm run wait-for-server
|
||||
npx jest -c ./jest.config.e2e.js --detectOpenHandles sql
|
||||
npm ci --prefix ./cypress
|
||||
npm run test:ci --prefix ./cypress -- --spec ./integration/dataexplorer/ci-tests/createDatabase.spec.ts
|
||||
shell: bash
|
||||
env:
|
||||
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/explorer.html?platform=Emulator"
|
||||
PLATFORM: "Emulator"
|
||||
EMULATOR_ENDPOINT: https://0.0.0.0:8081/
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: screenshots
|
||||
path: failed-*
|
||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
||||
accessibility:
|
||||
name: "Accessibility | Hosted"
|
||||
needs: [lint, format, compile, unittest]
|
||||
@@ -123,13 +123,13 @@ jobs:
|
||||
sudo sysctl -p
|
||||
npm ci
|
||||
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
|
||||
shell: bash
|
||||
env:
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
endtoendhosted:
|
||||
name: "End to End Hosted Tests"
|
||||
endtoendpuppeteer:
|
||||
name: "End to end puppeteer tests"
|
||||
needs: [lint, format, compile, unittest]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: End to End Hosted Tests
|
||||
- name: End to End Puppeteer Tests
|
||||
run: |
|
||||
npm ci
|
||||
npm start &
|
||||
@@ -147,27 +147,19 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
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 }}
|
||||
MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
|
||||
CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }}
|
||||
TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }}
|
||||
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/hostedExplorer.html"
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: screenshots
|
||||
path: failed-*
|
||||
nuget:
|
||||
name: Publish Nuget
|
||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted, accessibility]
|
||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendpuppeteer]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||
@@ -191,7 +183,7 @@ jobs:
|
||||
nugetmpac:
|
||||
name: Publish Nuget MPAC
|
||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted, accessibility]
|
||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendpuppeteer]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||
@@ -213,28 +205,3 @@ jobs:
|
||||
name: packages
|
||||
with:
|
||||
path: "*.nupkg"
|
||||
nugetie:
|
||||
name: Publish Nuget IE
|
||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted, accessibility]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
|
||||
steps:
|
||||
- uses: nuget/setup-nuget@v1
|
||||
with:
|
||||
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
||||
- name: Download Dist Folder
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
- run: cp ./configs/prod.json config.json
|
||||
- run: sed -i 's/Azure.Cosmos.DB.Data.Explorer/Azure.Cosmos.DB.Data.Explorer.IE/g' DataExplorer.nuspec
|
||||
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "GitHub" -Password "$AZURE_DEVOPS_PAT"
|
||||
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
||||
- run: nuget push -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
||||
- uses: actions/upload-artifact@v2
|
||||
name: packages
|
||||
with:
|
||||
path: "*.nupkg"
|
||||
|
||||
25
.github/workflows/runners.yml
vendored
Normal file
25
.github/workflows/runners.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Runners
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 * 1 * *"
|
||||
jobs:
|
||||
sqlcreatecollection:
|
||||
runs-on: ubuntu-latest
|
||||
name: "SQL | Create Collection"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- run: npm ci
|
||||
- run: npm run test:e2e
|
||||
env:
|
||||
PORTAL_RUNNER_APP_INSIGHTS_KEY: ${{ secrets.PORTAL_RUNNER_APP_INSIGHTS_KEY }}
|
||||
PORTAL_RUNNER_USERNAME: ${{ secrets.PORTAL_RUNNER_USERNAME }}
|
||||
PORTAL_RUNNER_PASSWORD: ${{ secrets.PORTAL_RUNNER_PASSWORD }}
|
||||
PORTAL_RUNNER_SUBSCRIPTION: 69e02f2d-f059-4409-9eac-97e8a276ae2c
|
||||
PORTAL_RUNNER_RESOURCE_GROUP: runners
|
||||
PORTAL_RUNNER_DATABASE_ACCOUNT: portal-sql-runner
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: screenshots
|
||||
path: failure.png
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,6 +9,9 @@ pkg/DataExplorer/*
|
||||
test/out/*
|
||||
workers/**/*.js
|
||||
*.trx
|
||||
cypress/videos
|
||||
cypress/screenshots
|
||||
cypress/fixtures
|
||||
notebookapp/*
|
||||
Contracts/*
|
||||
.DS_Store
|
||||
|
||||
BIN
.vs/slnx.sqlite
BIN
.vs/slnx.sqlite
Binary file not shown.
53
README.md
53
README.md
@@ -13,18 +13,29 @@ UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), ht
|
||||
|
||||
### Watch mode
|
||||
|
||||
Run `npm start` to start the development server and automatically rebuild on changes
|
||||
Run `npm run watch` to start the development server and automatically rebuild on changes
|
||||
|
||||
### Hosted Development (https://cosmos.azure.com)
|
||||
### Specifying Development Platform
|
||||
|
||||
- Visit: `https://localhost:1234/hostedExplorer.html`
|
||||
- Local sign in via AAD will NOT work. Connection string only in dev mode. Use the Portal if you need AAD auth.
|
||||
- The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine.
|
||||
Setting the environment variable `PLATFORM` during the build process will force the explorer to load the specified platform. By default in development it will run in `Hosted` mode. Valid options:
|
||||
|
||||
- Hosted
|
||||
- Emulator
|
||||
- Portal
|
||||
|
||||
`PLATFORM=Emulator npm run watch`
|
||||
|
||||
### Hosted Development
|
||||
|
||||
The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine.
|
||||
|
||||
To run pure hosted mode, in `webpack.config.js` change index HtmlWebpackPlugin to use hostedExplorer.html and change entry for index to use HostedExplorer.ts.
|
||||
|
||||
### Emulator Development
|
||||
|
||||
- Start the Cosmos Emulator
|
||||
- Visit: https://localhost:1234/index.html
|
||||
In a window environment, running `npm run build` will automatically copy the built files from `/dist` over to the default emulator install paths. In a non-windows environment you can specify an alternate endpoint using `EMULATOR_ENDPOINT` and webpack dev server will proxy requests for you.
|
||||
|
||||
`PLATFORM=Emulator EMULATOR_ENDPOINT=https://my-vm.azure.com:8081 npm run watch`
|
||||
|
||||
#### Setting up a Remote Emulator
|
||||
|
||||
@@ -44,8 +55,16 @@ The Cosmos emulator currently only runs in Windows environments. You can still d
|
||||
|
||||
### Portal Development
|
||||
|
||||
- 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
|
||||
The Cosmos Portal that consumes this repo is not currently open source. If you have access to this project, `npm run build` will copy the built files over to the portal where they will be loaded by the portal development environment
|
||||
|
||||
You can however load a local running instance of data explorer in the production portal.
|
||||
|
||||
1. Turn off browser SSL validation for localhost: chrome://flags/#allow-insecure-localhost OR Install valid SSL certs for localhost (on IE, follow these [instructions](https://www.technipages.com/ie-bypass-problem-with-this-websites-security-certificate) to install the localhost certificate in the right place)
|
||||
2. Allowlist `https://localhost:1234` domain for CORS in the Azure Cosmos DB portal
|
||||
3. Start the project in portal mode: `PLATFORM=Portal npm run watch`
|
||||
4. Load the portal using the following link: https://ms.portal.azure.com/?dataExplorerSource=https%3A%2F%2Flocalhost%3A1234%2Fexplorer.html
|
||||
|
||||
Live reload will occur, but data explorer will not properly integrate again with the parent iframe. You will have to manually reload the page.
|
||||
|
||||
### Testing
|
||||
|
||||
@@ -57,7 +76,17 @@ Unit tests are located adjacent to the code under test and run with [Jest](https
|
||||
|
||||
#### End to End CI Tests
|
||||
|
||||
Jest and Puppeteer are used for end to end browser based tests and are contained in `test/`. To run these tests locally:
|
||||
[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:
|
||||
|
||||
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
|
||||
2. Update the values in .env including your local data explorer endpoint (ask a teammate/codeowner for help with .env values)
|
||||
@@ -69,10 +98,6 @@ Jest and Puppeteer are used for end to end browser based tests and are contained
|
||||
|
||||
We generally adhere to the release strategy [documented by the Azure SDK Guidelines](https://azure.github.io/azure-sdk/policies_repobranching.html#release-branches). Most releases should happen from the master branch. If master contains commits that cannot be released, you may create a release from a `release/` or `hotfix/` branch. See linked documentation for more details.
|
||||
|
||||
### Architechture
|
||||
|
||||
[](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ3JhcGggTFJcbiAgaG9zdGVkKGh0dHBzOi8vY29zbW9zLmF6dXJlLmNvbSlcbiAgcG9ydGFsKFBvcnRhbClcbiAgZW11bGF0b3IoRW11bGF0b3IpXG4gIGFhZFtBQURdXG4gIHJlc291cmNlVG9rZW5bUmVzb3VyY2UgVG9rZW5dXG4gIGNvbm5lY3Rpb25TdHJpbmdbQ29ubmVjdGlvbiBTdHJpbmddXG4gIHBvcnRhbFRva2VuW0VuY3J5cHRlZCBQb3J0YWwgVG9rZW5dXG4gIG1hc3RlcktleVtNYXN0ZXIgS2V5XVxuICBhcm1bQVJNIFJlc291cmNlIFByb3ZpZGVyXVxuICBkYXRhcGxhbmVbRGF0YSBQbGFuZV1cbiAgcHJveHlbUG9ydGFsIEFQSSBQcm94eV1cbiAgc3FsW1NRTF1cbiAgbW9uZ29bTW9uZ29dXG4gIHRhYmxlc1tUYWJsZXNdXG4gIGNhc3NhbmRyYVtDYXNzYW5kcmFdXG4gIGdyYWZbR3JhcGhdXG5cblxuICBlbXVsYXRvciAtLT4gbWFzdGVyS2V5IC0tLS0-IGRhdGFwbGFuZVxuICBwb3J0YWwgLS0-IGFhZFxuICBob3N0ZWQgLS0-IHBvcnRhbFRva2VuICYgcmVzb3VyY2VUb2tlbiAmIGNvbm5lY3Rpb25TdHJpbmcgJiBhYWRcbiAgYWFkIC0tLT4gYXJtXG4gIGFhZCAtLS0-IGRhdGFwbGFuZVxuICBhYWQgLS0tPiBwcm94eVxuICByZXNvdXJjZVRva2VuIC0tLT4gc3FsIC0tPiBkYXRhcGxhbmVcbiAgcG9ydGFsVG9rZW4gLS0tPiBwcm94eVxuICBwcm94eSAtLT4gZGF0YXBsYW5lXG4gIGNvbm5lY3Rpb25TdHJpbmcgLS0-IHNxbCAmIG1vbmdvICYgY2Fzc2FuZHJhICYgZ3JhZiAmIHRhYmxlc1xuICBzcWwgLS0-IGRhdGFwbGFuZVxuICB0YWJsZXMgLS0-IGRhdGFwbGFuZVxuICBtb25nbyAtLT4gcHJveHlcbiAgY2Fzc2FuZHJhIC0tPiBwcm94eVxuICBncmFmIC0tPiBwcm94eVxuXG5cdFx0IiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQifSwidXBkYXRlRWRpdG9yIjpmYWxzZX0)
|
||||
|
||||
# Contributing
|
||||
|
||||
Please read the [contribution guidelines](./CONTRIBUTING.md).
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-react", "@babel/preset-typescript"],
|
||||
plugins: [["@babel/plugin-proposal-decorators", { legacy: true }]]
|
||||
presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-react", "@babel/preset-typescript"]
|
||||
};
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# Why?
|
||||
|
||||
This adds a mock module for `canvas`. Nteract has a ignored require and undeclared dependency on this module. `cavnas` is a server side node module and is not used in browser side code for nteract.
|
||||
|
||||
Installing it locally (`npm install canvas`) will resolve the problem, but it is a native module so it is flaky depending on the system, node version, processor arch, etc. This module provides a simpler, more robust solution.
|
||||
|
||||
Remove this workaround if [this bug](https://github.com/nteract/any-vega/issues/2) ever gets resolved
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = {}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"name": "canvas",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
4
cypress/.gitignore
vendored
Normal file
4
cypress/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
cypress.env.json
|
||||
cypress/report
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
51
cypress/cleanup.js
Normal file
51
cypress/cleanup.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// 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);
|
||||
});
|
||||
15
cypress/cypress.json
Normal file
15
cypress/cypress.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
81
cypress/integration/dataexplorer/GRAPH/addCollection.spec.ts
Normal file
81
cypress/integration/dataexplorer/GRAPH/addCollection.spec.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
80
cypress/integration/dataexplorer/MONGO/addCollection.spec.ts
Normal file
80
cypress/integration/dataexplorer/MONGO/addCollection.spec.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,203 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
79
cypress/integration/dataexplorer/SQL/addCollection.spec.ts
Normal file
79
cypress/integration/dataexplorer/SQL/addCollection.spec.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
60
cypress/integration/dataexplorer/TABLE/addCollection.spec.ts
Normal file
60
cypress/integration/dataexplorer/TABLE/addCollection.spec.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
// 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");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
35
cypress/integration/notebook/README.md
Normal file
35
cypress/integration/notebook/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# 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)
|
||||
93
cypress/integration/notebook/newNotebook.spec.ts
Normal file
93
cypress/integration/notebook/newNotebook.spec.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
// 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
172
cypress/integration/notebook/resourceTree.spec.ts
Normal file
172
cypress/integration/notebook/resourceTree.spec.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
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
Normal file
3066
cypress/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
cypress/package.json
Normal file
25
cypress/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
23
cypress/support/index.js
Normal file
23
cypress/support/index.js
Normal file
@@ -0,0 +1,23 @@
|
||||
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
|
||||
});
|
||||
11
cypress/tsconfig.json
Normal file
11
cypress/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"lib": ["es5", "dom", "es6"],
|
||||
"types": ["cypress", "node"]
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.spec.ts"]
|
||||
}
|
||||
41
cypress/utilities/connectionString.js
Normal file
41
cypress/utilities/connectionString.js
Normal file
@@ -0,0 +1,41 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
};
|
||||
6
cypress/utilities/cosmosClient.js
Normal file
6
cypress/utilities/cosmosClient.js
Normal file
@@ -0,0 +1,6 @@
|
||||
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-family: wf_segoe-ui_normal;
|
||||
src: url("../../fonts/segoe-ui/west-european/normal/latest.woff");
|
||||
font-family: wf_segoe-ui_normal;
|
||||
src: url('../../fonts/segoe-ui/west-european/normal/latest.woff');
|
||||
}
|
||||
|
||||
@DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
|
||||
@@ -20,26 +20,26 @@
|
||||
COLORS
|
||||
/******************************************************************************/
|
||||
|
||||
@AccentMediumHigh: #0058ad;
|
||||
@AccentMedium: #004e87;
|
||||
@AccentHigh: #1ebaed;
|
||||
@AccentExtraHigh: #55b3ff;
|
||||
@AccentLow: #edf6ff;
|
||||
@AccentMediumLow: #ddeefe;
|
||||
@AccentLight: #eef7ff;
|
||||
@AccentExtra: #ddf0ff;
|
||||
@AccentMediumHigh: #0058AD;
|
||||
@AccentMedium: #004E87;
|
||||
@AccentHigh: #1EBAED;
|
||||
@AccentExtraHigh: #55B3FF;
|
||||
@AccentLow: #EDF6FF;
|
||||
@AccentMediumLow: #DDEEFE;
|
||||
@AccentLight: #EEF7FF;
|
||||
@AccentExtra: #DDF0FF;
|
||||
|
||||
@SelectionHigh: #b91f26;
|
||||
@BaseLight: #ffffff;
|
||||
@SelectionHigh: #B91F26;
|
||||
@BaseLight: #FFFFFF;
|
||||
@BaseDark: #000000;
|
||||
@NotificationLow: #fff4ce;
|
||||
@NotificationHigh: #f9e9b0;
|
||||
@Purple1: #8a2da5;
|
||||
@NotificationLow: #FFF4CE;
|
||||
@NotificationHigh: #F9E9B0;
|
||||
@Purple1: #8A2DA5;
|
||||
@Dirty: #9b4f96;
|
||||
|
||||
@BaseLow: #f2f2f2;
|
||||
@BaseMediumLow: #e6e6e6;
|
||||
@BaseMedium: #cccccc;
|
||||
@BaseLow: #F2F2F2;
|
||||
@BaseMediumLow: #E6E6E6;
|
||||
@BaseMedium: #CCCCCC;
|
||||
@BaseMediumHigh: #767676;
|
||||
@BaseHigh: #393939;
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
|
||||
@ErrorColor: @SelectionHigh;
|
||||
|
||||
@SelectionColor: #3074b0;
|
||||
@SelectionColor: #3074B0;
|
||||
|
||||
@FocusColor: #605e5c;
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
@ImgWidth: 14px;
|
||||
@ImgHeight: 14px;
|
||||
|
||||
@toggleFontWeight: 700;
|
||||
@toggleFontWeight:700;
|
||||
|
||||
//Resource Tree
|
||||
@TreeLineHeight: 17px;
|
||||
@@ -144,16 +144,16 @@
|
||||
/**********************************************************************************/
|
||||
|
||||
.flex-display(@display: flex) {
|
||||
display: ~"-webkit-@{display}";
|
||||
display: ~"-ms-@{display}box"; // IE10 uses -ms-flexbox
|
||||
display: ~"-ms-@{display}"; // IE11
|
||||
display: @display;
|
||||
display: ~"-webkit-@{display}";
|
||||
display: ~"-ms-@{display}box"; // IE10 uses -ms-flexbox
|
||||
display: ~"-ms-@{display}"; // IE11
|
||||
display: @display;
|
||||
}
|
||||
|
||||
.flex-direction(@direction: column) {
|
||||
-webkit-flex-direction: @direction;
|
||||
-ms-flex-direction: @direction;
|
||||
flex-direction: @direction;
|
||||
-ms-flex-direction: @direction;
|
||||
flex-direction: @direction;
|
||||
}
|
||||
|
||||
/*************************************************************************************
|
||||
@@ -161,31 +161,32 @@
|
||||
**************************************************************************************/
|
||||
|
||||
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
|
||||
.selectedRadio,
|
||||
.selectedRadio:hover,
|
||||
.selectedRadio:active,
|
||||
.selectedRadio.dirty,
|
||||
.tab [type="radio"]:checked ~ label,
|
||||
.tab [type="radio"]:checked ~ label:hover {
|
||||
-ms-high-contrast-adjust: none;
|
||||
-webkit-text-fill-color: HighlightText;
|
||||
color: HighlightText;
|
||||
border-color: HighlightText;
|
||||
background-color: Highlight;
|
||||
}
|
||||
|
||||
.queryMetricsSummaryTuple {
|
||||
th,
|
||||
td {
|
||||
&:nth-child(2) {
|
||||
width: @IETableDataWidth;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 50%;
|
||||
}
|
||||
.selectedRadio,
|
||||
.selectedRadio:hover,
|
||||
.selectedRadio:active,
|
||||
.selectedRadio.dirty,
|
||||
.tab [type=radio]:checked ~ label,
|
||||
.tab [type=radio]:checked ~ label:hover {
|
||||
-ms-high-contrast-adjust: none;
|
||||
-webkit-text-fill-color: HighlightText;
|
||||
color: HighlightText;
|
||||
border-color: HighlightText;
|
||||
background-color: Highlight;
|
||||
}
|
||||
|
||||
.queryMetricsSummaryTuple {
|
||||
|
||||
th, td {
|
||||
|
||||
&:nth-child(2) {
|
||||
width: @IETableDataWidth;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/********************************************************************************************
|
||||
@@ -193,15 +194,15 @@
|
||||
*********************************************************************************************/
|
||||
|
||||
.hover() {
|
||||
background-color: @AccentLight;
|
||||
background-color: @AccentLight;
|
||||
}
|
||||
|
||||
.active() {
|
||||
background-color: @AccentExtra;
|
||||
background-color: @AccentExtra;
|
||||
}
|
||||
|
||||
.focus() {
|
||||
outline: 1px dashed @FocusColor;
|
||||
outline: 1px dashed @FocusColor;
|
||||
}
|
||||
|
||||
/************************************************************************************************
|
||||
@@ -211,87 +212,63 @@
|
||||
@ToggleWidth: 180px;
|
||||
|
||||
.toggleSwitch() {
|
||||
max-width: 100%;
|
||||
margin-bottom: @SmallSpace;
|
||||
padding: @SmallSpace;
|
||||
cursor: pointer;
|
||||
color: @BaseHigh;
|
||||
font-weight: 400;
|
||||
font-size: @mediumFontSize;
|
||||
font-family: @DataExplorerFont;
|
||||
max-width: 100%;
|
||||
margin-bottom: @SmallSpace;
|
||||
padding: @SmallSpace;
|
||||
cursor: pointer;
|
||||
color: @BaseHigh;
|
||||
font-weight: 400;
|
||||
font-size: @mediumFontSize;
|
||||
font-family: @DataExplorerFont;
|
||||
}
|
||||
|
||||
.selectedToggle() {
|
||||
border-bottom: 2px solid @BaseHigh;
|
||||
border-bottom: 2px solid @BaseHigh;
|
||||
}
|
||||
|
||||
.unselectedToggle() {
|
||||
color: @AccentMediumHigh;
|
||||
color: @AccentMediumHigh;
|
||||
}
|
||||
|
||||
/********************************************************************************************************
|
||||
Common Data Explorer Icons
|
||||
*********************************************************************************************************/
|
||||
.dataExplorerIcons() {
|
||||
cursor: pointer;
|
||||
width: @ImgWidth;
|
||||
height: @ImgHeight;
|
||||
cursor: pointer;
|
||||
width: @ImgWidth;
|
||||
height: @ImgHeight;
|
||||
}
|
||||
|
||||
/*********************************************************************************************************
|
||||
Info Tooltip
|
||||
**********************************************************************************************************/
|
||||
.infoTooltip() {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.tooltipText(@textColor: @BaseLight, @backgroundColor: @BaseHigh) {
|
||||
visibility: hidden;
|
||||
background-color: @backgroundColor;
|
||||
color: @textColor;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
left: @MediumSpace;
|
||||
padding: @MediumSpace;
|
||||
visibility: hidden;
|
||||
background-color: @backgroundColor;
|
||||
color: @textColor;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
left: @MediumSpace;
|
||||
padding: @MediumSpace;
|
||||
}
|
||||
|
||||
.tooltipTextAfter(@color: @BaseDark) {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
border-style: solid;
|
||||
border-color: transparent @color transparent transparent;
|
||||
left: 0px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: @InfoPointerColor transparent;
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
border-style: solid;
|
||||
border-color: transparent @color transparent transparent;
|
||||
left: 0px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: @InfoPointerColor transparent;
|
||||
}
|
||||
|
||||
.tooltipVisible() {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.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;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
3825
less/documentDB.less
3825
less/documentDB.less
File diff suppressed because it is too large
Load Diff
@@ -13,11 +13,6 @@
|
||||
@NavMediumSpace: 10px;
|
||||
@NavLargeSpace: 15px;
|
||||
|
||||
.skip-link {
|
||||
position: fixed;
|
||||
top: -200px;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
|
||||
padding: 0px;
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
@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 {
|
||||
height: 100%;
|
||||
flex: 0 0 auto;
|
||||
.main {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.resourceTreeScroll {
|
||||
|
||||
3905
package-lock.json
generated
3905
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@@ -4,12 +4,8 @@
|
||||
"description": "Cosmos Explorer",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@azure/arm-cosmosdb": "9.1.0",
|
||||
"@azure/cosmos": "3.9.0",
|
||||
"@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",
|
||||
"@azure/cosmos-language-service": "0.0.4",
|
||||
"@jupyterlab/services": "6.0.0-rc.2",
|
||||
"@jupyterlab/terminal": "3.0.0-rc.2",
|
||||
"@microsoft/applicationinsights-web": "2.5.9",
|
||||
@@ -46,7 +42,7 @@
|
||||
"applicationinsights": "1.8.0",
|
||||
"babel-polyfill": "6.26.0",
|
||||
"bootstrap": "3.4.1",
|
||||
"canvas": "file:./canvas",
|
||||
"canvas": "2.6.1",
|
||||
"clean-webpack-plugin": "0.1.19",
|
||||
"copy-webpack-plugin": "6.0.2",
|
||||
"crossroads": "0.12.2",
|
||||
@@ -70,7 +66,7 @@
|
||||
"jquery-ui-dist": "1.12.1",
|
||||
"knockout": "3.5.1",
|
||||
"mkdirp": "1.0.4",
|
||||
"monaco-editor": "0.18.1",
|
||||
"monaco-editor": "0.15.6",
|
||||
"object.entries": "1.1.0",
|
||||
"office-ui-fabric-react": "7.134.1",
|
||||
"p-retry": "4.2.0",
|
||||
@@ -87,7 +83,6 @@
|
||||
"react-notification-system": "0.2.17",
|
||||
"react-redux": "7.1.3",
|
||||
"redux": "4.0.4",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rx-jupyter": "5.5.12",
|
||||
"rxjs": "6.6.3",
|
||||
"styled-components": "4.3.2",
|
||||
@@ -120,7 +115,7 @@
|
||||
"@types/prop-types": "15.5.8",
|
||||
"@types/puppeteer": "3.0.1",
|
||||
"@types/q": "1.5.1",
|
||||
"@types/react": "16.9.56",
|
||||
"@types/react": "16.9.49",
|
||||
"@types/react-dom": "16.0.7",
|
||||
"@types/react-notification-system": "0.2.39",
|
||||
"@types/react-redux": "7.1.7",
|
||||
@@ -199,8 +194,8 @@
|
||||
"compile": "tsc",
|
||||
"compile:contracts": "tsc -p ./tsconfig.contracts.json",
|
||||
"compile:strict": "tsc -p ./tsconfig.strict.json",
|
||||
"format": "prettier --write \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
||||
"format:check": "prettier --check \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
||||
"format": "prettier --write \"{src,cypress,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
||||
"format:check": "prettier --check \"{src,cypress,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
||||
"lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"",
|
||||
"build:contracts": "npm run compile:contracts",
|
||||
"strictEligibleFiles": "node ./strict-migration-tools/index.js",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"offerThroughput": 400,
|
||||
"databaseLevelThroughput": false,
|
||||
"collectionId": "Persons",
|
||||
"rupmEnabled": false,
|
||||
"partitionKey": { "kind": "Hash", "paths": ["/name"] },
|
||||
"data": [
|
||||
"g.addV('person').property(id, '1').property('name', 'Eva').property('age', 44)",
|
||||
@@ -12,4 +13,4 @@
|
||||
"g.V('1').addE('knows').to(g.V('2')).outV().addE('knows').to(g.V('3'))",
|
||||
"g.V('3').addE('knows').to(g.V('4'))"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,437 +1,439 @@
|
||||
import { HashMap } from "./HashMap";
|
||||
|
||||
export class AuthorizationEndpoints {
|
||||
public static arm: string = "https://management.core.windows.net/";
|
||||
public static common: string = "https://login.windows.net/";
|
||||
}
|
||||
|
||||
export class CodeOfConductEndpoints {
|
||||
public static privacyStatement: string = "https://aka.ms/ms-privacy-policy";
|
||||
public static codeOfConduct: string = "https://aka.ms/cosmos-code-of-conduct";
|
||||
public static termsOfUse: string = "https://aka.ms/ms-terms-of-use";
|
||||
}
|
||||
|
||||
export class EndpointsRegex {
|
||||
public static readonly cassandra = [
|
||||
"AccountEndpoint=(.*).cassandra.cosmosdb.azure.com",
|
||||
"HostName=(.*).cassandra.cosmos.azure.com"
|
||||
];
|
||||
public static readonly mongo = "mongodb://.*:(.*)@(.*).documents.azure.com";
|
||||
public static readonly mongoCompute = "mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com";
|
||||
public static readonly sql = "AccountEndpoint=https://(.*).documents.azure.com";
|
||||
public static readonly table = "TableEndpoint=https://(.*).table.cosmosdb.azure.com";
|
||||
}
|
||||
|
||||
export class ApiEndpoints {
|
||||
public static runtimeProxy: string = "/api/RuntimeProxy";
|
||||
public static guestRuntimeProxy: string = "/api/guest/RuntimeProxy";
|
||||
}
|
||||
|
||||
export class ServerIds {
|
||||
public static localhost: string = "localhost";
|
||||
public static blackforest: string = "blackforest";
|
||||
public static fairfax: string = "fairfax";
|
||||
public static mooncake: string = "mooncake";
|
||||
public static productionPortal: string = "prod";
|
||||
public static dev: string = "dev";
|
||||
}
|
||||
|
||||
export class ArmApiVersions {
|
||||
public static readonly documentDB: string = "2015-11-06";
|
||||
public static readonly arcadia: string = "2019-06-01-preview";
|
||||
public static readonly arcadiaLivy: string = "2019-11-01-preview";
|
||||
public static readonly arm: string = "2015-11-01";
|
||||
public static readonly armFeatures: string = "2014-08-01-preview";
|
||||
public static readonly publicVersion = "2020-04-01";
|
||||
}
|
||||
|
||||
export class ArmResourceTypes {
|
||||
public static readonly notebookWorkspaces = "Microsoft.DocumentDB/databaseAccounts/notebookWorkspaces";
|
||||
public static readonly synapseWorkspaces = "Microsoft.Synapse/workspaces";
|
||||
}
|
||||
|
||||
export class BackendDefaults {
|
||||
public static partitionKeyKind: string = "Hash";
|
||||
public static singlePartitionStorageInGb: string = "10";
|
||||
public static multiPartitionStorageInGb: string = "100";
|
||||
public static maxChangeFeedRetentionDuration: number = 10;
|
||||
public static partitionKeyVersion = 2;
|
||||
}
|
||||
|
||||
export class ClientDefaults {
|
||||
public static requestTimeoutMs: number = 60000;
|
||||
public static portalCacheTimeoutMs: number = 10000;
|
||||
public static errorNotificationTimeoutMs: number = 5000;
|
||||
public static copyHelperTimeoutMs: number = 2000;
|
||||
public static waitForDOMElementMs: number = 500;
|
||||
public static cacheBustingTimeoutMs: number =
|
||||
10 /** minutes **/ * 60 /** to seconds **/ * 1000 /** to milliseconds **/;
|
||||
public static databaseThroughputIncreaseFactor: number = 100;
|
||||
public static readonly arcadiaTokenRefreshInterval: number =
|
||||
20 /** minutes **/ * 60 /** to seconds **/ * 1000 /** to milliseconds **/;
|
||||
public static readonly arcadiaTokenRefreshIntervalPaddingMs: number = 2000;
|
||||
}
|
||||
|
||||
export class AccountKind {
|
||||
public static DocumentDB: string = "DocumentDB";
|
||||
public static MongoDB: string = "MongoDB";
|
||||
public static Parse: string = "Parse";
|
||||
public static GlobalDocumentDB: string = "GlobalDocumentDB";
|
||||
public static Default: string = AccountKind.DocumentDB;
|
||||
}
|
||||
|
||||
export class CorrelationBackend {
|
||||
public static Url: string = "https://aka.ms/cosmosdbanalytics";
|
||||
}
|
||||
|
||||
export class DefaultAccountExperience {
|
||||
public static DocumentDB: string = "DocumentDB";
|
||||
public static Graph: string = "Graph";
|
||||
public static MongoDB: string = "MongoDB";
|
||||
public static ApiForMongoDB: string = "Azure Cosmos DB for MongoDB API";
|
||||
public static Table: string = "Table";
|
||||
public static Cassandra: string = "Cassandra";
|
||||
public static Default: string = DefaultAccountExperience.DocumentDB;
|
||||
}
|
||||
|
||||
export class CapabilityNames {
|
||||
public static EnableTable: string = "EnableTable";
|
||||
public static EnableGremlin: string = "EnableGremlin";
|
||||
public static EnableCassandra: string = "EnableCassandra";
|
||||
public static EnableAutoScale: string = "EnableAutoScale";
|
||||
public static readonly EnableNotebooks: string = "EnableNotebooks";
|
||||
public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics";
|
||||
public static readonly EnableMongo: string = "EnableMongo";
|
||||
public static readonly EnableServerless: string = "EnableServerless";
|
||||
}
|
||||
|
||||
export class Features {
|
||||
public static readonly cosmosdb = "cosmosdb";
|
||||
public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy";
|
||||
public static readonly executeSproc = "dataexplorerexecutesproc";
|
||||
public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
|
||||
public static readonly enableTtl = "enablettl";
|
||||
public static readonly enableNotebooks = "enablenotebooks";
|
||||
public static readonly enableGalleryPublish = "enablegallerypublish";
|
||||
public static readonly enableLinkInjection = "enablelinkinjection";
|
||||
public static readonly enableSpark = "enablespark";
|
||||
public static readonly livyEndpoint = "livyendpoint";
|
||||
public static readonly notebookServerUrl = "notebookserverurl";
|
||||
public static readonly notebookServerToken = "notebookservertoken";
|
||||
public static readonly notebookBasePath = "notebookbasepath";
|
||||
public static readonly canExceedMaximumValue = "canexceedmaximumvalue";
|
||||
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
|
||||
public static readonly ttl90Days = "ttl90days";
|
||||
public static readonly enableRightPanelV2 = "enablerightpanelv2";
|
||||
public static readonly enableSchema = "enableschema";
|
||||
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 {
|
||||
public static readonly SettingsV2 = "settingsv2";
|
||||
public static readonly MongoIndexEditor = "mongoindexeditor";
|
||||
public static readonly AutoscaleTest = "autoscaletest";
|
||||
public static readonly MongoIndexing = "mongoindexing";
|
||||
}
|
||||
|
||||
export class AfecFeatures {
|
||||
public static readonly Spark = "spark-public-preview";
|
||||
public static readonly Notebooks = "sparknotebooks-public-preview";
|
||||
public static readonly StorageAnalytics = "storageanalytics-public-preview";
|
||||
}
|
||||
|
||||
export class Spark {
|
||||
public static readonly MaxWorkerCount = 10;
|
||||
public static readonly SKUs: HashMap<string> = new HashMap({
|
||||
"Cosmos.Spark.D1s": "D1s / 1 core / 4GB RAM",
|
||||
"Cosmos.Spark.D2s": "D2s / 2 cores / 8GB RAM",
|
||||
"Cosmos.Spark.D4s": "D4s / 4 cores / 16GB 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 MongoDBAccounts {
|
||||
public static protocol: string = "https";
|
||||
public static defaultPort: string = "10255";
|
||||
}
|
||||
|
||||
export enum MongoBackendEndpointType {
|
||||
local,
|
||||
remote
|
||||
}
|
||||
|
||||
// TODO: 435619 Add default endpoints per cloud and use regional only when available
|
||||
export class CassandraBackend {
|
||||
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
|
||||
public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete";
|
||||
public static readonly queryApi: string = "api/cassandra";
|
||||
public static readonly guestQueryApi: string = "api/guest/cassandra";
|
||||
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";
|
||||
public static readonly guestSchemaApi: string = "api/guest/cassandra/schema";
|
||||
}
|
||||
|
||||
export class Queries {
|
||||
public static CustomPageOption: string = "custom";
|
||||
public static UnlimitedPageOption: string = "unlimited";
|
||||
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 readonly DefaultMaxDegreeOfParallelism = 6;
|
||||
}
|
||||
|
||||
export class SavedQueries {
|
||||
public static readonly CollectionName: string = "___Query";
|
||||
public static readonly DatabaseName: string = "___Cosmos";
|
||||
public static readonly OfferThroughput: number = 400;
|
||||
public static readonly PartitionKeyProperty: string = "id";
|
||||
}
|
||||
|
||||
export class DocumentsGridMetrics {
|
||||
public static DocumentsPerPage: number = 100;
|
||||
public static IndividualRowHeight: number = 34;
|
||||
public static BufferHeight: number = 28;
|
||||
public static SplitterMinWidth: number = 200;
|
||||
public static SplitterMaxWidth: number = 360;
|
||||
|
||||
public static DocumentEditorMinWidthRatio: number = 0.2;
|
||||
public static DocumentEditorMaxWidthRatio: number = 0.4;
|
||||
}
|
||||
|
||||
export class ExplorerMetrics {
|
||||
public static SplitterMinWidth: number = 240;
|
||||
public static SplitterMaxWidth: number = 400;
|
||||
public static CollapsedResourceTreeWidth: number = 36;
|
||||
}
|
||||
|
||||
export class SplitterMetrics {
|
||||
public static CollapsedPositionLeft: number = ExplorerMetrics.CollapsedResourceTreeWidth;
|
||||
}
|
||||
|
||||
export class Areas {
|
||||
public static ResourceTree: string = "Resource Tree";
|
||||
public static ContextualPane: string = "Contextual Pane";
|
||||
public static Tab: string = "Tab";
|
||||
public static ShareDialog: string = "Share Access Dialog";
|
||||
public static Notebook: string = "Notebook";
|
||||
}
|
||||
|
||||
export class HttpHeaders {
|
||||
public static activityId: string = "x-ms-activity-id";
|
||||
public static apiType: string = "x-ms-cosmos-apitype";
|
||||
public static authorization: string = "authorization";
|
||||
public static collectionIndexTransformationProgress: string =
|
||||
"x-ms-documentdb-collection-index-transformation-progress";
|
||||
public static continuation: string = "x-ms-continuation";
|
||||
public static correlationRequestId: string = "x-ms-correlation-request-id";
|
||||
public static enableScriptLogging: string = "x-ms-documentdb-script-enable-logging";
|
||||
public static guestAccessToken: string = "x-ms-encrypted-auth-token";
|
||||
public static getReadOnlyKey: string = "x-ms-get-read-only-key";
|
||||
public static connectionString: string = "x-ms-connection-string";
|
||||
public static msDate: string = "x-ms-date";
|
||||
public static location: string = "Location";
|
||||
public static contentType: string = "Content-Type";
|
||||
public static offerReplacePending: string = "x-ms-offer-replace-pending";
|
||||
public static user: string = "x-ms-user";
|
||||
public static populatePartitionStatistics: string = "x-ms-documentdb-populatepartitionstatistics";
|
||||
public static queryMetrics: string = "x-ms-documentdb-query-metrics";
|
||||
public static requestCharge: string = "x-ms-request-charge";
|
||||
public static resourceQuota: string = "x-ms-resource-quota";
|
||||
public static resourceUsage: string = "x-ms-resource-usage";
|
||||
public static retryAfterMs: string = "x-ms-retry-after-ms";
|
||||
public static scriptLogResults: string = "x-ms-documentdb-script-log-results";
|
||||
public static populateCollectionThroughputInfo = "x-ms-documentdb-populatecollectionthroughputinfo";
|
||||
public static supportSpatialLegacyCoordinates = "x-ms-documentdb-supportspatiallegacycoordinates";
|
||||
public static usePolygonsSmallerThanAHemisphere = "x-ms-documentdb-usepolygonssmallerthanahemisphere";
|
||||
public static autoPilotThroughput = "autoscaleSettings";
|
||||
public static autoPilotThroughputSDK = "x-ms-cosmos-offer-autopilot-settings";
|
||||
public static partitionKey: string = "x-ms-documentdb-partitionkey";
|
||||
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
|
||||
public static readonly MongoDB: number = 1;
|
||||
public static readonly Gremlin: number = 2;
|
||||
public static readonly Cassandra: number = 4;
|
||||
public static readonly Table: number = 8;
|
||||
public static readonly SQL: number = 16;
|
||||
}
|
||||
|
||||
export class HttpStatusCodes {
|
||||
public static readonly OK: number = 200;
|
||||
public static readonly Created: number = 201;
|
||||
public static readonly Accepted: number = 202;
|
||||
public static readonly NoContent: number = 204;
|
||||
public static readonly NotModified: number = 304;
|
||||
public static readonly Unauthorized: number = 401;
|
||||
public static readonly Forbidden: number = 403;
|
||||
public static readonly NotFound: number = 404;
|
||||
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 ServiceUnavailable: number = 503;
|
||||
public static readonly GatewayTimeout: number = 504;
|
||||
|
||||
public static readonly RetryableStatusCodes: number[] = [
|
||||
HttpStatusCodes.TooManyRequests,
|
||||
HttpStatusCodes.InternalServerError, // TODO: Handle all 500s on Portal backend and remove from retries list
|
||||
HttpStatusCodes.BadGateway,
|
||||
HttpStatusCodes.ServiceUnavailable,
|
||||
HttpStatusCodes.GatewayTimeout
|
||||
];
|
||||
}
|
||||
|
||||
export class Urls {
|
||||
public static feedbackEmail = "https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Data%20Explorer%20Feedback";
|
||||
public static autoscaleMigration = "https://aka.ms/cosmos-autoscale-migration";
|
||||
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}";
|
||||
public static collections: string = "/dbs/{db_id}/colls/{coll_id}";
|
||||
public static sprocHash: string = "/sprocs/";
|
||||
public static sprocs: string = HashRoutePrefixes.collections + HashRoutePrefixes.sprocHash + "{sproc_id}";
|
||||
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 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
|
||||
}
|
||||
|
||||
public static sprocWithIds(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
sprocId: string,
|
||||
stripFirstSlash: boolean = true
|
||||
): string {
|
||||
const transformedDatabasePrefix: string = this.sprocs.replace("{db_id}", databaseId);
|
||||
|
||||
const transformedSprocRoute: string = transformedDatabasePrefix
|
||||
.replace("{coll_id}", collectionId)
|
||||
.replace("{sproc_id}", sprocId);
|
||||
if (!!stripFirstSlash) {
|
||||
return transformedSprocRoute.replace("/", ""); // strip the first slash since hasher adds it
|
||||
}
|
||||
|
||||
return transformedSprocRoute;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public static docsWithIds(databaseId: string, collectionId: string, docId: string) {
|
||||
const transformedDatabasePrefix: string = this.docs.replace("{db_id}", databaseId);
|
||||
|
||||
return transformedDatabasePrefix
|
||||
.replace("{coll_id}", collectionId)
|
||||
.replace("{doc_id}", docId)
|
||||
.replace("/", ""); // strip the first slash since hasher adds it
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfigurationOverridesValues {
|
||||
public static IsBsonSchemaV2: string = "true";
|
||||
}
|
||||
|
||||
export class KeyCodes {
|
||||
public static Space: number = 32;
|
||||
public static Enter: number = 13;
|
||||
public static Escape: number = 27;
|
||||
public static UpArrow: number = 38;
|
||||
public static DownArrow: number = 40;
|
||||
public static LeftArrow: number = 37;
|
||||
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 {
|
||||
public static readonly Space = " ";
|
||||
public static readonly Enter = "Enter";
|
||||
public static readonly Escape = "Escape";
|
||||
public static readonly UpArrow = "ArrowUp";
|
||||
public static readonly DownArrow = "ArrowDown";
|
||||
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}";
|
||||
public static deleteUrl: string = "https://trycosmosdb.azure.com/api/resource/deleteportal?userId={0}";
|
||||
public static collectionsPerAccount: number = 3;
|
||||
public static maxRU: number = 5000;
|
||||
public static defaultRU: number = 3000;
|
||||
}
|
||||
|
||||
export class OfferVersions {
|
||||
public static V1: string = "V1";
|
||||
public static V2: string = "V2";
|
||||
}
|
||||
|
||||
export enum ConflictOperationType {
|
||||
Replace = "replace",
|
||||
Create = "create",
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export const EmulatorMasterKey =
|
||||
//[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");
|
||||
|
||||
export class Notebook {
|
||||
public static readonly defaultBasePath = "./notebooks";
|
||||
public static readonly heartbeatDelayMs = 5000;
|
||||
public static readonly kernelRestartInitialDelayMs = 1000;
|
||||
public static readonly kernelRestartMaxDelayMs = 20000;
|
||||
public static readonly autoSaveIntervalMs = 120000;
|
||||
}
|
||||
|
||||
export class SparkLibrary {
|
||||
public static readonly nameMinLength = 3;
|
||||
public static readonly nameMaxLength = 63;
|
||||
}
|
||||
|
||||
export class AnalyticalStorageTtl {
|
||||
public static readonly Days90: number = 7776000;
|
||||
public static readonly Infinite: number = -1;
|
||||
public static readonly Disabled: number = 0;
|
||||
}
|
||||
|
||||
export class TerminalQueryParams {
|
||||
public static readonly Terminal = "terminal";
|
||||
public static readonly Server = "server";
|
||||
public static readonly Token = "token";
|
||||
public static readonly SubscriptionId = "subscriptionId";
|
||||
public static readonly TerminalEndpoint = "terminalEndpoint";
|
||||
}
|
||||
import { HashMap } from "./HashMap";
|
||||
|
||||
export class AuthorizationEndpoints {
|
||||
public static arm: string = "https://management.core.windows.net/";
|
||||
public static common: string = "https://login.windows.net/";
|
||||
}
|
||||
|
||||
export class CodeOfConductEndpoints {
|
||||
public static privacyStatement: string = "https://aka.ms/ms-privacy-policy";
|
||||
public static codeOfConduct: string = "https://aka.ms/cosmos-code-of-conduct";
|
||||
public static termsOfUse: string = "https://aka.ms/ms-terms-of-use";
|
||||
}
|
||||
|
||||
export class EndpointsRegex {
|
||||
public static readonly cassandra = [
|
||||
"AccountEndpoint=(.*).cassandra.cosmosdb.azure.com",
|
||||
"HostName=(.*).cassandra.cosmos.azure.com"
|
||||
];
|
||||
public static readonly mongo = "mongodb://.*:(.*)@(.*).documents.azure.com";
|
||||
public static readonly mongoCompute = "mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com";
|
||||
public static readonly sql = "AccountEndpoint=https://(.*).documents.azure.com";
|
||||
public static readonly table = "TableEndpoint=https://(.*).table.cosmosdb.azure.com";
|
||||
}
|
||||
|
||||
export class ApiEndpoints {
|
||||
public static runtimeProxy: string = "/api/RuntimeProxy";
|
||||
public static guestRuntimeProxy: string = "/api/guest/RuntimeProxy";
|
||||
}
|
||||
|
||||
export class ServerIds {
|
||||
public static localhost: string = "localhost";
|
||||
public static blackforest: string = "blackforest";
|
||||
public static fairfax: string = "fairfax";
|
||||
public static mooncake: string = "mooncake";
|
||||
public static productionPortal: string = "prod";
|
||||
public static dev: string = "dev";
|
||||
}
|
||||
|
||||
export class ArmApiVersions {
|
||||
public static readonly documentDB: string = "2015-11-06";
|
||||
public static readonly arcadia: string = "2019-06-01-preview";
|
||||
public static readonly arcadiaLivy: string = "2019-11-01-preview";
|
||||
public static readonly arm: string = "2015-11-01";
|
||||
public static readonly armFeatures: string = "2014-08-01-preview";
|
||||
public static readonly publicVersion = "2020-04-01";
|
||||
}
|
||||
|
||||
export class ArmResourceTypes {
|
||||
public static readonly notebookWorkspaces = "Microsoft.DocumentDB/databaseAccounts/notebookWorkspaces";
|
||||
public static readonly synapseWorkspaces = "Microsoft.Synapse/workspaces";
|
||||
}
|
||||
|
||||
export class BackendDefaults {
|
||||
public static partitionKeyKind: string = "Hash";
|
||||
public static singlePartitionStorageInGb: string = "10";
|
||||
public static multiPartitionStorageInGb: string = "100";
|
||||
public static maxChangeFeedRetentionDuration: number = 10;
|
||||
public static partitionKeyVersion = 2;
|
||||
}
|
||||
|
||||
export class ClientDefaults {
|
||||
public static requestTimeoutMs: number = 60000;
|
||||
public static portalCacheTimeoutMs: number = 10000;
|
||||
public static errorNotificationTimeoutMs: number = 5000;
|
||||
public static copyHelperTimeoutMs: number = 2000;
|
||||
public static waitForDOMElementMs: number = 500;
|
||||
public static cacheBustingTimeoutMs: number =
|
||||
10 /** minutes **/ * 60 /** to seconds **/ * 1000 /** to milliseconds **/;
|
||||
public static databaseThroughputIncreaseFactor: number = 100;
|
||||
public static readonly arcadiaTokenRefreshInterval: number =
|
||||
20 /** minutes **/ * 60 /** to seconds **/ * 1000 /** to milliseconds **/;
|
||||
public static readonly arcadiaTokenRefreshIntervalPaddingMs: number = 2000;
|
||||
}
|
||||
|
||||
export class AccountKind {
|
||||
public static DocumentDB: string = "DocumentDB";
|
||||
public static MongoDB: string = "MongoDB";
|
||||
public static Parse: string = "Parse";
|
||||
public static GlobalDocumentDB: string = "GlobalDocumentDB";
|
||||
public static Default: string = AccountKind.DocumentDB;
|
||||
}
|
||||
|
||||
export class CorrelationBackend {
|
||||
public static Url: string = "https://aka.ms/cosmosdbanalytics";
|
||||
}
|
||||
|
||||
export class DefaultAccountExperience {
|
||||
public static DocumentDB: string = "DocumentDB";
|
||||
public static Graph: string = "Graph";
|
||||
public static MongoDB: string = "MongoDB";
|
||||
public static ApiForMongoDB: string = "Azure Cosmos DB for MongoDB API";
|
||||
public static Table: string = "Table";
|
||||
public static Cassandra: string = "Cassandra";
|
||||
public static Default: string = DefaultAccountExperience.DocumentDB;
|
||||
}
|
||||
|
||||
export class CapabilityNames {
|
||||
public static EnableTable: string = "EnableTable";
|
||||
public static EnableGremlin: string = "EnableGremlin";
|
||||
public static EnableCassandra: string = "EnableCassandra";
|
||||
public static EnableAutoScale: string = "EnableAutoScale";
|
||||
public static readonly EnableNotebooks: string = "EnableNotebooks";
|
||||
public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics";
|
||||
public static readonly EnableMongo: string = "EnableMongo";
|
||||
public static readonly EnableServerless: string = "EnableServerless";
|
||||
}
|
||||
|
||||
export class Features {
|
||||
public static readonly cosmosdb = "cosmosdb";
|
||||
public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy";
|
||||
public static readonly enableRupm = "enablerupm";
|
||||
public static readonly executeSproc = "dataexplorerexecutesproc";
|
||||
public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
|
||||
public static readonly enableTtl = "enablettl";
|
||||
public static readonly enableNotebooks = "enablenotebooks";
|
||||
public static readonly enableGalleryPublish = "enablegallerypublish";
|
||||
public static readonly enableCodeOfConduct = "enablecodeofconduct";
|
||||
public static readonly enableLinkInjection = "enablelinkinjection";
|
||||
public static readonly enableSpark = "enablespark";
|
||||
public static readonly livyEndpoint = "livyendpoint";
|
||||
public static readonly notebookServerUrl = "notebookserverurl";
|
||||
public static readonly notebookServerToken = "notebookservertoken";
|
||||
public static readonly notebookBasePath = "notebookbasepath";
|
||||
public static readonly canExceedMaximumValue = "canexceedmaximumvalue";
|
||||
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
|
||||
public static readonly ttl90Days = "ttl90days";
|
||||
public static readonly enableRightPanelV2 = "enablerightpanelv2";
|
||||
public static readonly enableSDKoperations = "enablesdkoperations";
|
||||
}
|
||||
|
||||
// flight names returned from the portal are always lowercase
|
||||
export class Flights {
|
||||
public static readonly SettingsV2 = "settingsv2";
|
||||
public static readonly MongoIndexEditor = "mongoindexeditor";
|
||||
}
|
||||
|
||||
export class AfecFeatures {
|
||||
public static readonly Spark = "spark-public-preview";
|
||||
public static readonly Notebooks = "sparknotebooks-public-preview";
|
||||
public static readonly StorageAnalytics = "storageanalytics-public-preview";
|
||||
}
|
||||
|
||||
export class Spark {
|
||||
public static readonly MaxWorkerCount = 10;
|
||||
public static readonly SKUs: HashMap<string> = new HashMap({
|
||||
"Cosmos.Spark.D1s": "D1s / 1 core / 4GB RAM",
|
||||
"Cosmos.Spark.D2s": "D2s / 2 cores / 8GB RAM",
|
||||
"Cosmos.Spark.D4s": "D4s / 4 cores / 16GB 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 MongoDBAccounts {
|
||||
public static protocol: string = "https";
|
||||
public static defaultPort: string = "10255";
|
||||
}
|
||||
|
||||
export enum MongoBackendEndpointType {
|
||||
local,
|
||||
remote
|
||||
}
|
||||
|
||||
// TODO: 435619 Add default endpoints per cloud and use regional only when available
|
||||
export class CassandraBackend {
|
||||
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
|
||||
public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete";
|
||||
public static readonly queryApi: string = "api/cassandra";
|
||||
public static readonly guestQueryApi: string = "api/guest/cassandra";
|
||||
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";
|
||||
public static readonly guestSchemaApi: string = "api/guest/cassandra/schema";
|
||||
}
|
||||
|
||||
export class RUPMStates {
|
||||
public static on: string = "on";
|
||||
public static off: string = "off";
|
||||
}
|
||||
|
||||
export class Queries {
|
||||
public static CustomPageOption: string = "custom";
|
||||
public static UnlimitedPageOption: string = "unlimited";
|
||||
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 readonly DefaultMaxDegreeOfParallelism = 6;
|
||||
}
|
||||
|
||||
export class SavedQueries {
|
||||
public static readonly CollectionName: string = "___Query";
|
||||
public static readonly DatabaseName: string = "___Cosmos";
|
||||
public static readonly OfferThroughput: number = 400;
|
||||
public static readonly PartitionKeyProperty: string = "id";
|
||||
}
|
||||
|
||||
export class DocumentsGridMetrics {
|
||||
public static DocumentsPerPage: number = 100;
|
||||
public static IndividualRowHeight: number = 34;
|
||||
public static BufferHeight: number = 28;
|
||||
public static SplitterMinWidth: number = 200;
|
||||
public static SplitterMaxWidth: number = 360;
|
||||
|
||||
public static DocumentEditorMinWidthRatio: number = 0.2;
|
||||
public static DocumentEditorMaxWidthRatio: number = 0.4;
|
||||
}
|
||||
|
||||
export class ExplorerMetrics {
|
||||
public static SplitterMinWidth: number = 240;
|
||||
public static SplitterMaxWidth: number = 400;
|
||||
public static CollapsedResourceTreeWidth: number = 36;
|
||||
}
|
||||
|
||||
export class SplitterMetrics {
|
||||
public static CollapsedPositionLeft: number = ExplorerMetrics.CollapsedResourceTreeWidth;
|
||||
}
|
||||
|
||||
export class Areas {
|
||||
public static ResourceTree: string = "Resource Tree";
|
||||
public static ContextualPane: string = "Contextual Pane";
|
||||
public static Tab: string = "Tab";
|
||||
public static ShareDialog: string = "Share Access Dialog";
|
||||
public static Notebook: string = "Notebook";
|
||||
}
|
||||
|
||||
export class HttpHeaders {
|
||||
public static activityId: string = "x-ms-activity-id";
|
||||
public static apiType: string = "x-ms-cosmos-apitype";
|
||||
public static authorization: string = "authorization";
|
||||
public static collectionIndexTransformationProgress: string =
|
||||
"x-ms-documentdb-collection-index-transformation-progress";
|
||||
public static continuation: string = "x-ms-continuation";
|
||||
public static correlationRequestId: string = "x-ms-correlation-request-id";
|
||||
public static enableScriptLogging: string = "x-ms-documentdb-script-enable-logging";
|
||||
public static guestAccessToken: string = "x-ms-encrypted-auth-token";
|
||||
public static getReadOnlyKey: string = "x-ms-get-read-only-key";
|
||||
public static connectionString: string = "x-ms-connection-string";
|
||||
public static msDate: string = "x-ms-date";
|
||||
public static location: string = "Location";
|
||||
public static contentType: string = "Content-Type";
|
||||
public static offerReplacePending: string = "x-ms-offer-replace-pending";
|
||||
public static user: string = "x-ms-user";
|
||||
public static populatePartitionStatistics: string = "x-ms-documentdb-populatepartitionstatistics";
|
||||
public static queryMetrics: string = "x-ms-documentdb-query-metrics";
|
||||
public static requestCharge: string = "x-ms-request-charge";
|
||||
public static resourceQuota: string = "x-ms-resource-quota";
|
||||
public static resourceUsage: string = "x-ms-resource-usage";
|
||||
public static retryAfterMs: string = "x-ms-retry-after-ms";
|
||||
public static scriptLogResults: string = "x-ms-documentdb-script-log-results";
|
||||
public static populateCollectionThroughputInfo = "x-ms-documentdb-populatecollectionthroughputinfo";
|
||||
public static supportSpatialLegacyCoordinates = "x-ms-documentdb-supportspatiallegacycoordinates";
|
||||
public static usePolygonsSmallerThanAHemisphere = "x-ms-documentdb-usepolygonssmallerthanahemisphere";
|
||||
public static autoPilotThroughput = "autoscaleSettings";
|
||||
public static autoPilotThroughputSDK = "x-ms-cosmos-offer-autopilot-settings";
|
||||
public static partitionKey: string = "x-ms-documentdb-partitionkey";
|
||||
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
|
||||
public static readonly MongoDB: number = 1;
|
||||
public static readonly Gremlin: number = 2;
|
||||
public static readonly Cassandra: number = 4;
|
||||
public static readonly Table: number = 8;
|
||||
public static readonly SQL: number = 16;
|
||||
}
|
||||
|
||||
export class HttpStatusCodes {
|
||||
public static readonly OK: number = 200;
|
||||
public static readonly Created: number = 201;
|
||||
public static readonly Accepted: number = 202;
|
||||
public static readonly NoContent: number = 204;
|
||||
public static readonly NotModified: number = 304;
|
||||
public static readonly Unauthorized: number = 401;
|
||||
public static readonly Forbidden: number = 403;
|
||||
public static readonly NotFound: number = 404;
|
||||
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 ServiceUnavailable: number = 503;
|
||||
public static readonly GatewayTimeout: number = 504;
|
||||
|
||||
public static readonly RetryableStatusCodes: number[] = [
|
||||
HttpStatusCodes.TooManyRequests,
|
||||
HttpStatusCodes.InternalServerError, // TODO: Handle all 500s on Portal backend and remove from retries list
|
||||
HttpStatusCodes.BadGateway,
|
||||
HttpStatusCodes.ServiceUnavailable,
|
||||
HttpStatusCodes.GatewayTimeout
|
||||
];
|
||||
}
|
||||
|
||||
export class Urls {
|
||||
public static feedbackEmail = "https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Data%20Explorer%20Feedback";
|
||||
public static autoscaleMigration = "https://aka.ms/cosmos-autoscale-migration";
|
||||
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}";
|
||||
public static collections: string = "/dbs/{db_id}/colls/{coll_id}";
|
||||
public static sprocHash: string = "/sprocs/";
|
||||
public static sprocs: string = HashRoutePrefixes.collections + HashRoutePrefixes.sprocHash + "{sproc_id}";
|
||||
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 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
|
||||
}
|
||||
|
||||
public static sprocWithIds(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
sprocId: string,
|
||||
stripFirstSlash: boolean = true
|
||||
): string {
|
||||
const transformedDatabasePrefix: string = this.sprocs.replace("{db_id}", databaseId);
|
||||
|
||||
const transformedSprocRoute: string = transformedDatabasePrefix
|
||||
.replace("{coll_id}", collectionId)
|
||||
.replace("{sproc_id}", sprocId);
|
||||
if (!!stripFirstSlash) {
|
||||
return transformedSprocRoute.replace("/", ""); // strip the first slash since hasher adds it
|
||||
}
|
||||
|
||||
return transformedSprocRoute;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public static docsWithIds(databaseId: string, collectionId: string, docId: string) {
|
||||
const transformedDatabasePrefix: string = this.docs.replace("{db_id}", databaseId);
|
||||
|
||||
return transformedDatabasePrefix
|
||||
.replace("{coll_id}", collectionId)
|
||||
.replace("{doc_id}", docId)
|
||||
.replace("/", ""); // strip the first slash since hasher adds it
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfigurationOverridesValues {
|
||||
public static IsBsonSchemaV2: string = "true";
|
||||
}
|
||||
|
||||
export class KeyCodes {
|
||||
public static Space: number = 32;
|
||||
public static Enter: number = 13;
|
||||
public static Escape: number = 27;
|
||||
public static UpArrow: number = 38;
|
||||
public static DownArrow: number = 40;
|
||||
public static LeftArrow: number = 37;
|
||||
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 {
|
||||
public static readonly Space = " ";
|
||||
public static readonly Enter = "Enter";
|
||||
public static readonly Escape = "Escape";
|
||||
public static readonly UpArrow = "ArrowUp";
|
||||
public static readonly DownArrow = "ArrowDown";
|
||||
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}";
|
||||
public static deleteUrl: string = "https://trycosmosdb.azure.com/api/resource/deleteportal?userId={0}";
|
||||
public static collectionsPerAccount: number = 3;
|
||||
public static maxRU: number = 5000;
|
||||
public static defaultRU: number = 3000;
|
||||
}
|
||||
|
||||
export class OfferVersions {
|
||||
public static V1: string = "V1";
|
||||
public static V2: string = "V2";
|
||||
}
|
||||
|
||||
export enum ConflictOperationType {
|
||||
Replace = "replace",
|
||||
Create = "create",
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export const EmulatorMasterKey =
|
||||
//[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");
|
||||
|
||||
export class Notebook {
|
||||
public static readonly defaultBasePath = "./notebooks";
|
||||
public static readonly heartbeatDelayMs = 5000;
|
||||
public static readonly kernelRestartInitialDelayMs = 1000;
|
||||
public static readonly kernelRestartMaxDelayMs = 20000;
|
||||
public static readonly autoSaveIntervalMs = 120000;
|
||||
}
|
||||
|
||||
export class SparkLibrary {
|
||||
public static readonly nameMinLength = 3;
|
||||
public static readonly nameMaxLength = 63;
|
||||
}
|
||||
|
||||
export class AnalyticalStorageTtl {
|
||||
public static readonly Days90: number = 7776000;
|
||||
public static readonly Infinite: number = -1;
|
||||
public static readonly Disabled: number = 0;
|
||||
}
|
||||
|
||||
export class TerminalQueryParams {
|
||||
public static readonly Terminal = "terminal";
|
||||
public static readonly Server = "server";
|
||||
public static readonly Token = "token";
|
||||
public static readonly SubscriptionId = "subscriptionId";
|
||||
public static readonly TerminalEndpoint = "terminalEndpoint";
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { getCommonQueryOptions } from "./queryDocuments";
|
||||
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||
|
||||
describe("getCommonQueryOptions", () => {
|
||||
it("builds the correct default options objects", () => {
|
||||
expect(getCommonQueryOptions({})).toMatchSnapshot();
|
||||
});
|
||||
it("reads from localStorage", () => {
|
||||
LocalStorageUtility.setEntryNumber(StorageKey.ActualItemPerPage, 37);
|
||||
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, 17);
|
||||
expect(getCommonQueryOptions({})).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
import { getCommonQueryOptions } from "./DataAccessUtilityBase";
|
||||
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
|
||||
|
||||
describe("getCommonQueryOptions", () => {
|
||||
it("builds the correct default options objects", () => {
|
||||
expect(getCommonQueryOptions({})).toMatchSnapshot();
|
||||
});
|
||||
it("reads from localStorage", () => {
|
||||
LocalStorageUtility.setEntryNumber(StorageKey.ActualItemPerPage, 37);
|
||||
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, 17);
|
||||
expect(getCommonQueryOptions({})).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
182
src/Common/DataAccessUtilityBase.ts
Normal file
182
src/Common/DataAccessUtilityBase.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
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);
|
||||
}
|
||||
217
src/Common/DocumentClientUtilityBase.ts
Normal file
217
src/Common/DocumentClientUtilityBase.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
import { ConflictDefinition, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import Q from "q";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import ConflictId from "../Explorer/Tree/ConflictId";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
||||
import * as Constants from "./Constants";
|
||||
import * as DataAccessUtilityBase from "./DataAccessUtilityBase";
|
||||
import { MinimalQueryIterator, nextPage } from "./IteratorUtilities";
|
||||
import { handleError } from "./ErrorHandlingUtils";
|
||||
|
||||
// TODO: Log all promise resolutions and errors with verbosity levels
|
||||
export function queryDocuments(
|
||||
databaseId: string,
|
||||
containerId: string,
|
||||
query: string,
|
||||
options: any
|
||||
): Q.Promise<QueryIterator<ItemDefinition & Resource>> {
|
||||
return DataAccessUtilityBase.queryDocuments(databaseId, containerId, query, options);
|
||||
}
|
||||
|
||||
export function queryConflicts(
|
||||
databaseId: string,
|
||||
containerId: string,
|
||||
query: string,
|
||||
options: any
|
||||
): Q.Promise<QueryIterator<ConflictDefinition & Resource>> {
|
||||
return DataAccessUtilityBase.queryConflicts(databaseId, containerId, query, options);
|
||||
}
|
||||
|
||||
export function getEntityName() {
|
||||
const defaultExperience =
|
||||
window.dataExplorer && window.dataExplorer.defaultExperience && window.dataExplorer.defaultExperience();
|
||||
if (defaultExperience === Constants.DefaultAccountExperience.MongoDB) {
|
||||
return "document";
|
||||
}
|
||||
return "item";
|
||||
}
|
||||
|
||||
export function executeStoredProcedure(
|
||||
collection: ViewModels.Collection,
|
||||
storedProcedure: StoredProcedure,
|
||||
partitionKeyValue: any,
|
||||
params: any[]
|
||||
): Q.Promise<any> {
|
||||
var deferred = Q.defer<any>();
|
||||
|
||||
const clearMessage = logConsoleProgress(`Executing stored procedure ${storedProcedure.id()}`);
|
||||
DataAccessUtilityBase.executeStoredProcedure(collection, storedProcedure, partitionKeyValue, params)
|
||||
.then(
|
||||
(response: any) => {
|
||||
deferred.resolve(response);
|
||||
logConsoleInfo(
|
||||
`Finished executing stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
|
||||
);
|
||||
},
|
||||
(error: any) => {
|
||||
handleError(
|
||||
error,
|
||||
"ExecuteStoredProcedure",
|
||||
`Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
|
||||
);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
clearMessage();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function queryDocumentsPage(
|
||||
resourceName: string,
|
||||
documentsIterator: MinimalQueryIterator,
|
||||
firstItemIndex: number,
|
||||
options: any
|
||||
): Q.Promise<ViewModels.QueryResults> {
|
||||
var deferred = Q.defer<ViewModels.QueryResults>();
|
||||
const entityName = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
|
||||
Q(nextPage(documentsIterator, firstItemIndex))
|
||||
.then(
|
||||
(result: ViewModels.QueryResults) => {
|
||||
const itemCount = (result.documents && result.documents.length) || 0;
|
||||
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
|
||||
deferred.resolve(result);
|
||||
},
|
||||
(error: any) => {
|
||||
handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
clearMessage();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
|
||||
var deferred = Q.defer<any>();
|
||||
const entityName = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Reading ${entityName} ${documentId.id()}`);
|
||||
DataAccessUtilityBase.readDocument(collection, documentId)
|
||||
.then(
|
||||
(document: any) => {
|
||||
deferred.resolve(document);
|
||||
},
|
||||
(error: any) => {
|
||||
handleError(error, "ReadDocument", `Failed to read ${entityName} ${documentId.id()}`);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
clearMessage();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function updateDocument(
|
||||
collection: ViewModels.CollectionBase,
|
||||
documentId: DocumentId,
|
||||
newDocument: any
|
||||
): Q.Promise<any> {
|
||||
var deferred = Q.defer<any>();
|
||||
const entityName = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Updating ${entityName} ${documentId.id()}`);
|
||||
DataAccessUtilityBase.updateDocument(collection, documentId, newDocument)
|
||||
.then(
|
||||
(updatedDocument: any) => {
|
||||
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);
|
||||
deferred.resolve(updatedDocument);
|
||||
},
|
||||
(error: any) => {
|
||||
handleError(error, "UpdateDocument", `Failed to update ${entityName} ${documentId.id()}`);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
clearMessage();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
|
||||
var deferred = Q.defer<any>();
|
||||
const entityName = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Creating new ${entityName} for container ${collection.id()}`);
|
||||
DataAccessUtilityBase.createDocument(collection, newDocument)
|
||||
.then(
|
||||
(savedDocument: any) => {
|
||||
logConsoleInfo(`Successfully created new ${entityName} for container ${collection.id()}`);
|
||||
deferred.resolve(savedDocument);
|
||||
},
|
||||
(error: any) => {
|
||||
handleError(error, "CreateDocument", `Error while creating new ${entityName} for container ${collection.id()}`);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
clearMessage();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
|
||||
var deferred = Q.defer<any>();
|
||||
const entityName = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Deleting ${entityName} ${documentId.id()}`);
|
||||
DataAccessUtilityBase.deleteDocument(collection, documentId)
|
||||
.then(
|
||||
(response: any) => {
|
||||
logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`);
|
||||
deferred.resolve(response);
|
||||
},
|
||||
(error: any) => {
|
||||
handleError(error, "DeleteDocument", `Error while deleting ${entityName} ${documentId.id()}`);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
clearMessage();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function deleteConflict(
|
||||
collection: ViewModels.CollectionBase,
|
||||
conflictId: ConflictId,
|
||||
options?: any
|
||||
): Q.Promise<any> {
|
||||
var deferred = Q.defer<any>();
|
||||
|
||||
const clearMessage = logConsoleProgress(`Deleting conflict ${conflictId.id()}`);
|
||||
DataAccessUtilityBase.deleteConflict(collection, conflictId, options)
|
||||
.then(
|
||||
(response: any) => {
|
||||
logConsoleInfo(`Successfully deleted conflict ${conflictId.id()}`);
|
||||
deferred.resolve(response);
|
||||
},
|
||||
(error: any) => {
|
||||
handleError(error, "DeleteConflict", `Error while deleting conflict ${conflictId.id()}`);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
clearMessage();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { DefaultAccountExperienceType } from "../DefaultAccountExperienceType";
|
||||
import { userContext } from "../UserContext";
|
||||
|
||||
export const getEntityName = (): string => {
|
||||
if (userContext.defaultExperience === DefaultAccountExperienceType.MongoDB) {
|
||||
return "document";
|
||||
}
|
||||
|
||||
return "item";
|
||||
};
|
||||
@@ -1,6 +1,8 @@
|
||||
export function normalizeArmEndpoint(uri: string): string {
|
||||
if (uri && uri.slice(-1) !== "/") {
|
||||
return `${uri}/`;
|
||||
export default class EnvironmentUtility {
|
||||
public static normalizeArmEndpointUri(uri: string): string {
|
||||
if (uri && uri.slice(-1) !== "/") {
|
||||
return `${uri}/`;
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ARMError } from "../Utils/arm/request";
|
||||
import { HttpStatusCodes } from "./Constants";
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import { SubscriptionType } from "../Contracts/SubscriptionType";
|
||||
import { SubscriptionType } from "../Contracts/ViewModels";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
import { logError } from "./Logger";
|
||||
import { sendMessage } from "./MessageHandler";
|
||||
@@ -21,7 +21,7 @@ export const handleError = (error: string | ARMError | Error, area: string, cons
|
||||
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;
|
||||
return replaceKnownError(errorMessage);
|
||||
};
|
||||
@@ -45,10 +45,10 @@ const sendNotificationForError = (errorMessage: string, errorCode: number | stri
|
||||
const replaceKnownError = (errorMessage: string): string => {
|
||||
if (
|
||||
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.";
|
||||
} 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.";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
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[]> => {
|
||||
if (configContext.platform === Platform.Emulator || configContext.platform === Platform.Hosted) {
|
||||
if (configContext.platform === Platform.Emulator) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@@ -3,18 +3,16 @@ import * as _ from "underscore";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import DocumentsTab from "../Explorer/Tabs/DocumentsTab";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import { QueryUtils } from "../Utils/QueryUtils";
|
||||
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
|
||||
import { userContext } from "../UserContext";
|
||||
import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage";
|
||||
import { createDocument, deleteDocument, queryDocuments, queryDocumentsPage } from "./DocumentClientUtilityBase";
|
||||
import { createCollection } from "./dataAccess/createCollection";
|
||||
import { handleError } from "./ErrorHandlingUtils";
|
||||
import { createDocument } from "./dataAccess/createDocument";
|
||||
import { deleteDocument } from "./dataAccess/deleteDocument";
|
||||
import { queryDocuments } from "./dataAccess/queryDocuments";
|
||||
|
||||
export class QueriesClient {
|
||||
private static readonly PartitionKey: DataModels.PartitionKey = {
|
||||
@@ -33,7 +31,10 @@ export class QueriesClient {
|
||||
return Promise.resolve(queriesCollection.rawDataModel);
|
||||
}
|
||||
|
||||
const clearMessage = NotificationConsoleUtils.logConsoleProgress("Setting up account for saving queries");
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
"Setting up account for saving queries"
|
||||
);
|
||||
return createCollection({
|
||||
collectionId: SavedQueries.CollectionName,
|
||||
createNewDatabase: true,
|
||||
@@ -44,7 +45,10 @@ export class QueriesClient {
|
||||
})
|
||||
.then(
|
||||
(collection: DataModels.Collection) => {
|
||||
NotificationConsoleUtils.logConsoleInfo("Successfully set up account for saving queries");
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
"Successfully set up account for saving queries"
|
||||
);
|
||||
return Promise.resolve(collection);
|
||||
},
|
||||
(error: any) => {
|
||||
@@ -52,14 +56,17 @@ export class QueriesClient {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => clearMessage());
|
||||
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
||||
}
|
||||
|
||||
public async saveQuery(query: DataModels.Query): Promise<void> {
|
||||
const queriesCollection = this.findQueriesCollection();
|
||||
if (!queriesCollection) {
|
||||
const errorMessage: string = "Account not set up to perform saved query operations";
|
||||
NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to save query ${query.queryName}: ${errorMessage}`
|
||||
);
|
||||
return Promise.reject(errorMessage);
|
||||
}
|
||||
|
||||
@@ -67,16 +74,25 @@ export class QueriesClient {
|
||||
this.validateQuery(query);
|
||||
} catch (error) {
|
||||
const errorMessage: string = "Invalid query specified";
|
||||
NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to save query ${query.queryName}: ${errorMessage}`
|
||||
);
|
||||
return Promise.reject(errorMessage);
|
||||
}
|
||||
|
||||
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Saving query ${query.queryName}`);
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Saving query ${query.queryName}`
|
||||
);
|
||||
query.id = query.queryName;
|
||||
return createDocument(queriesCollection, query)
|
||||
.then(
|
||||
(savedQuery: DataModels.Query) => {
|
||||
NotificationConsoleUtils.logConsoleInfo(`Successfully saved query ${query.queryName}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully saved query ${query.queryName}`
|
||||
);
|
||||
return Promise.resolve();
|
||||
},
|
||||
(error: any) => {
|
||||
@@ -87,65 +103,74 @@ export class QueriesClient {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => clearMessage());
|
||||
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
||||
}
|
||||
|
||||
public async getQueries(): Promise<DataModels.Query[]> {
|
||||
const queriesCollection = this.findQueriesCollection();
|
||||
if (!queriesCollection) {
|
||||
const errorMessage: string = "Account not set up to perform saved query operations";
|
||||
NotificationConsoleUtils.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to fetch saved queries: ${errorMessage}`
|
||||
);
|
||||
return Promise.reject(errorMessage);
|
||||
}
|
||||
|
||||
const options: any = { enableCrossPartitionQuery: true };
|
||||
const clearMessage = NotificationConsoleUtils.logConsoleProgress("Fetching saved queries");
|
||||
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)
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Fetching saved queries");
|
||||
return queryDocuments(SavedQueries.DatabaseName, SavedQueries.CollectionName, this.fetchQueriesQuery(), options)
|
||||
.then(
|
||||
(results: ViewModels.QueryResults) => {
|
||||
let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => {
|
||||
if (!document) {
|
||||
return undefined;
|
||||
(queryIterator: QueryIterator<ItemDefinition & Resource>) => {
|
||||
const fetchQueries = (firstItemIndex: number): Q.Promise<ViewModels.QueryResults> =>
|
||||
queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex, options);
|
||||
return QueryUtils.queryAllPages(fetchQueries).then(
|
||||
(results: ViewModels.QueryResults) => {
|
||||
let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => {
|
||||
if (!document) {
|
||||
return undefined;
|
||||
}
|
||||
const { id, resourceId, query, queryName } = document;
|
||||
const parsedQuery: DataModels.Query = {
|
||||
resourceId: resourceId,
|
||||
queryName: queryName,
|
||||
query: query,
|
||||
id: id
|
||||
};
|
||||
try {
|
||||
this.validateQuery(parsedQuery);
|
||||
return parsedQuery;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
queries = _.reject(queries, (parsedQuery: DataModels.Query) => !parsedQuery);
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully fetched saved queries");
|
||||
return Promise.resolve(queries);
|
||||
},
|
||||
(error: any) => {
|
||||
handleError(error, "getSavedQueries", "Failed to fetch saved queries");
|
||||
return Promise.reject(error);
|
||||
}
|
||||
const { id, resourceId, query, queryName } = document;
|
||||
const parsedQuery: DataModels.Query = {
|
||||
resourceId: resourceId,
|
||||
queryName: queryName,
|
||||
query: query,
|
||||
id: id
|
||||
};
|
||||
try {
|
||||
this.validateQuery(parsedQuery);
|
||||
return parsedQuery;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
queries = _.reject(queries, (parsedQuery: DataModels.Query) => !parsedQuery);
|
||||
NotificationConsoleUtils.logConsoleInfo("Successfully fetched saved queries");
|
||||
return Promise.resolve(queries);
|
||||
);
|
||||
},
|
||||
(error: any) => {
|
||||
// should never get into this state but we handle this regardless
|
||||
handleError(error, "getSavedQueries", "Failed to fetch saved queries");
|
||||
return Promise.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => clearMessage());
|
||||
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
||||
}
|
||||
|
||||
public async deleteQuery(query: DataModels.Query): Promise<void> {
|
||||
const queriesCollection = this.findQueriesCollection();
|
||||
if (!queriesCollection) {
|
||||
const errorMessage: string = "Account not set up to perform saved query operations";
|
||||
NotificationConsoleUtils.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to fetch saved queries: ${errorMessage}`
|
||||
);
|
||||
return Promise.reject(errorMessage);
|
||||
}
|
||||
|
||||
@@ -153,10 +178,16 @@ export class QueriesClient {
|
||||
this.validateQuery(query);
|
||||
} catch (error) {
|
||||
const errorMessage: string = "Invalid query specified";
|
||||
NotificationConsoleUtils.logConsoleError(`Failed to delete query ${query.queryName}: ${errorMessage}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to delete query ${query.queryName}: ${errorMessage}`
|
||||
);
|
||||
}
|
||||
|
||||
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting query ${query.queryName}`);
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Deleting query ${query.queryName}`
|
||||
);
|
||||
query.id = query.queryName;
|
||||
const documentId = new DocumentId(
|
||||
{
|
||||
@@ -170,7 +201,10 @@ export class QueriesClient {
|
||||
return deleteDocument(queriesCollection, documentId)
|
||||
.then(
|
||||
() => {
|
||||
NotificationConsoleUtils.logConsoleInfo(`Successfully deleted query ${query.queryName}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully deleted query ${query.queryName}`
|
||||
);
|
||||
return Promise.resolve();
|
||||
},
|
||||
(error: any) => {
|
||||
@@ -178,7 +212,7 @@ export class QueriesClient {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => clearMessage());
|
||||
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
||||
}
|
||||
|
||||
public getResourceId(): string {
|
||||
|
||||
@@ -23,10 +23,10 @@ export class Splitter {
|
||||
public splitterId: string;
|
||||
public leftSideId: string;
|
||||
|
||||
public splitter!: HTMLElement;
|
||||
public leftSide!: HTMLElement;
|
||||
public lastX!: number;
|
||||
public lastWidth!: number;
|
||||
public splitter: HTMLElement;
|
||||
public leftSide: HTMLElement;
|
||||
public lastX: number;
|
||||
public lastWidth: number;
|
||||
|
||||
private isCollapsed: ko.Observable<boolean>;
|
||||
private bounds: SplitterBounds;
|
||||
@@ -42,10 +42,9 @@ export class Splitter {
|
||||
}
|
||||
|
||||
public initialize() {
|
||||
if (document.getElementById(this.splitterId) !== null && document.getElementById(this.leftSideId) != null) {
|
||||
this.splitter = <HTMLElement>document.getElementById(this.splitterId);
|
||||
this.leftSide = <HTMLElement>document.getElementById(this.leftSideId);
|
||||
}
|
||||
this.splitter = document.getElementById(this.splitterId);
|
||||
this.leftSide = document.getElementById(this.leftSideId);
|
||||
|
||||
const isVerticalSplitter: boolean = this.direction === SplitterDirection.Vertical;
|
||||
const splitterOptions: JQueryUI.ResizableOptions = {
|
||||
animate: true,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
jest.mock("../../Utils/arm/request");
|
||||
jest.mock("../CosmosClient");
|
||||
jest.mock("../DataAccessUtilityBase");
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
import { client } from "../CosmosClient";
|
||||
import { getEntityName } from "../DocumentUtility";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
|
||||
export const createDocument = async (collection: CollectionBase, newDocument: unknown): Promise<unknown> => {
|
||||
const entityName = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Creating new ${entityName} for container ${collection.id()}`);
|
||||
|
||||
try {
|
||||
const response = await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.items.create(newDocument);
|
||||
|
||||
logConsoleInfo(`Successfully created new ${entityName} for container ${collection.id()}`);
|
||||
return response?.resource;
|
||||
} catch (error) {
|
||||
handleError(error, "CreateDocument", `Error while creating new ${entityName} for container ${collection.id()}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import ConflictId from "../../Explorer/Tree/ConflictId";
|
||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
import { RequestOptions } from "@azure/cosmos";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
||||
|
||||
export const deleteConflict = async (collection: CollectionBase, conflictId: ConflictId): Promise<void> => {
|
||||
const clearMessage = logConsoleProgress(`Deleting conflict ${conflictId.id()}`);
|
||||
|
||||
try {
|
||||
const options = {
|
||||
partitionKey: getPartitionKeyHeaderForConflict(conflictId)
|
||||
};
|
||||
|
||||
await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.conflict(conflictId.id())
|
||||
.delete(options as RequestOptions);
|
||||
logConsoleInfo(`Successfully deleted conflict ${conflictId.id()}`);
|
||||
} catch (error) {
|
||||
handleError(error, "DeleteConflict", `Error while deleting conflict ${conflictId.id()}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
};
|
||||
|
||||
const getPartitionKeyHeaderForConflict = (conflictId: ConflictId): unknown => {
|
||||
if (!conflictId.partitionKey) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return conflictId.partitionKeyValue === undefined ? [{}] : [conflictId.partitionKeyValue];
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
import { client } from "../CosmosClient";
|
||||
import { getEntityName } from "../DocumentUtility";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||
|
||||
export const deleteDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<void> => {
|
||||
const entityName: string = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Deleting ${entityName} ${documentId.id()}`);
|
||||
|
||||
try {
|
||||
await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.item(documentId.id(), documentId.partitionKeyValue)
|
||||
.delete();
|
||||
logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`);
|
||||
} catch (error) {
|
||||
handleError(error, "DeleteDocument", `Error while deleting ${entityName} ${documentId.id()}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Collection } from "../../Contracts/ViewModels";
|
||||
import { ClientDefaults, HttpHeaders } from "../Constants";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
||||
import StoredProcedure from "../../Explorer/Tree/StoredProcedure";
|
||||
|
||||
export interface ExecuteSprocResult {
|
||||
result: StoredProcedure;
|
||||
scriptLogs: string;
|
||||
}
|
||||
|
||||
export const executeStoredProcedure = async (
|
||||
collection: Collection,
|
||||
storedProcedure: StoredProcedure,
|
||||
partitionKeyValue: string,
|
||||
params: string[]
|
||||
): Promise<ExecuteSprocResult> => {
|
||||
const clearMessage = logConsoleProgress(`Executing stored procedure ${storedProcedure.id()}`);
|
||||
const timeout = setTimeout(() => {
|
||||
throw Error(`Request timed out while executing stored procedure ${storedProcedure.id()}`);
|
||||
}, ClientDefaults.requestTimeoutMs);
|
||||
|
||||
try {
|
||||
const response = await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.scripts.storedProcedure(storedProcedure.id())
|
||||
.execute(partitionKeyValue, params, { enableScriptLogging: true });
|
||||
clearTimeout(timeout);
|
||||
logConsoleInfo(
|
||||
`Finished executing stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
|
||||
);
|
||||
return {
|
||||
result: response.resource,
|
||||
scriptLogs: response.headers[HttpHeaders.scriptLogResults] as string
|
||||
};
|
||||
} catch (error) {
|
||||
handleError(
|
||||
error,
|
||||
"ExecuteStoredProcedure",
|
||||
`Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
};
|
||||
@@ -1,92 +0,0 @@
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { armRequest } from "../../Utils/arm/request";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
interface TimeSeriesData {
|
||||
data: {
|
||||
timeStamp: string;
|
||||
total: number;
|
||||
}[];
|
||||
metadatavalues: {
|
||||
name: {
|
||||
localizedValue: string;
|
||||
value: string;
|
||||
};
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface MetricsData {
|
||||
displayDescription: string;
|
||||
errorCode: string;
|
||||
id: string;
|
||||
name: {
|
||||
value: string;
|
||||
localizedValue: string;
|
||||
};
|
||||
timeseries: TimeSeriesData[];
|
||||
type: string;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
interface MetricsResponse {
|
||||
cost: number;
|
||||
interval: string;
|
||||
namespace: string;
|
||||
resourceregion: string;
|
||||
timespan: string;
|
||||
value: MetricsData[];
|
||||
}
|
||||
|
||||
export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => {
|
||||
if (window.authType !== AuthType.AAD) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const accountName = userContext.databaseAccount.name;
|
||||
const filter = `DatabaseName eq '${databaseName}' and CollectionName eq '${containerName}'`;
|
||||
const metricNames = "DataUsage,IndexUsage";
|
||||
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/providers/microsoft.insights/metrics`;
|
||||
|
||||
try {
|
||||
const metricsResponse: MetricsResponse = await armRequest({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path,
|
||||
method: "GET",
|
||||
apiVersion: "2018-01-01",
|
||||
queryParams: {
|
||||
filter,
|
||||
metricNames
|
||||
}
|
||||
});
|
||||
|
||||
if (metricsResponse?.value?.length !== 2) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const dataUsageData: MetricsData = metricsResponse.value[0];
|
||||
const indexUsagedata: MetricsData = metricsResponse.value[1];
|
||||
const dataUsageSizeInKb: number = getUsageSizeInKb(dataUsageData);
|
||||
const indexUsageSizeInKb: number = getUsageSizeInKb(indexUsagedata);
|
||||
|
||||
return dataUsageSizeInKb + indexUsageSizeInKb;
|
||||
} catch (error) {
|
||||
handleError(error, "getCollectionUsageSize");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const getUsageSizeInKb = (metricsData: MetricsData): number => {
|
||||
if (metricsData?.errorCode !== "Success") {
|
||||
throw Error(`Get collection usage size failed: ${metricsData.errorCode}`);
|
||||
}
|
||||
|
||||
const timeSeriesData: TimeSeriesData = metricsData?.timeseries?.[0];
|
||||
const usageSizeInBytes: number = timeSeriesData?.data?.[0]?.total;
|
||||
|
||||
return usageSizeInBytes ? usageSizeInBytes / 1024 : 0;
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
import { ConflictDefinition, FeedOptions, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import { client } from "../CosmosClient";
|
||||
|
||||
export const queryConflicts = (
|
||||
databaseId: string,
|
||||
containerId: string,
|
||||
query: string,
|
||||
options: FeedOptions
|
||||
): QueryIterator<ConflictDefinition & Resource> => {
|
||||
return client()
|
||||
.database(databaseId)
|
||||
.container(containerId)
|
||||
.conflicts.query(query, options);
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Queries } from "../Constants";
|
||||
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||
import { client } from "../CosmosClient";
|
||||
|
||||
export const queryDocuments = (
|
||||
databaseId: string,
|
||||
containerId: string,
|
||||
query: string,
|
||||
options: FeedOptions
|
||||
): QueryIterator<ItemDefinition & Resource> => {
|
||||
options = getCommonQueryOptions(options);
|
||||
return client()
|
||||
.database(databaseId)
|
||||
.container(containerId)
|
||||
.items.query(query, options);
|
||||
};
|
||||
|
||||
export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => {
|
||||
const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage);
|
||||
options = options || {};
|
||||
options.populateQueryMetrics = true;
|
||||
options.enableScanInQuery = options.enableScanInQuery || true;
|
||||
if (!options.partitionKey) {
|
||||
options.forceQueryPlan = true;
|
||||
}
|
||||
options.maxItemCount =
|
||||
options.maxItemCount ||
|
||||
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
|
||||
Queries.itemsPerPage;
|
||||
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
|
||||
|
||||
return options;
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import { QueryResults } from "../../Contracts/ViewModels";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { MinimalQueryIterator, nextPage } from "../IteratorUtilities";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { getEntityName } from "../DocumentUtility";
|
||||
|
||||
export const queryDocumentsPage = async (
|
||||
resourceName: string,
|
||||
documentsIterator: MinimalQueryIterator,
|
||||
firstItemIndex: number
|
||||
): Promise<QueryResults> => {
|
||||
const entityName = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
|
||||
|
||||
try {
|
||||
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex);
|
||||
const itemCount = (result.documents && result.documents.length) || 0;
|
||||
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,9 @@
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { getSqlContainerThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import { getMongoDBCollectionThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||
@@ -8,22 +11,50 @@ import { getCassandraTableThroughput } from "../../Utils/arm/generatedClients/20
|
||||
import { getGremlinGraphThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||
import { getTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||
import { readOffers } from "./readOffers";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise<Offer> => {
|
||||
export const readCollectionOffer = async (
|
||||
params: DataModels.ReadCollectionOfferParams
|
||||
): Promise<DataModels.OfferWithHeaders> => {
|
||||
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 {
|
||||
if (
|
||||
window.authType === AuthType.AAD &&
|
||||
!userContext.useSDKOperations &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||
) {
|
||||
return await readCollectionOfferWithARM(params.databaseId, params.collectionId);
|
||||
}
|
||||
|
||||
return await readOfferWithSDK(params.offerId, params.collectionResourceId);
|
||||
const response = await client()
|
||||
.offer(offerId)
|
||||
.read(options);
|
||||
return (
|
||||
response && {
|
||||
...response.resource,
|
||||
headers: response.headers
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
handleError(error, "ReadCollectionOffer", `Error while querying offer for collection ${params.collectionId}`);
|
||||
throw error;
|
||||
@@ -32,92 +63,61 @@ export const readCollectionOffer = async (params: ReadCollectionOfferParams): Pr
|
||||
}
|
||||
};
|
||||
|
||||
const readCollectionOfferWithARM = async (databaseId: string, collectionId: string): Promise<Offer> => {
|
||||
const getCollectionOfferIdWithARM = async (databaseId: string, collectionId: string): Promise<string> => {
|
||||
let rpResponse;
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const accountName = userContext.databaseAccount.name;
|
||||
const defaultExperience = userContext.defaultExperience;
|
||||
|
||||
let rpResponse;
|
||||
try {
|
||||
switch (defaultExperience) {
|
||||
case DefaultAccountExperienceType.DocumentDB:
|
||||
rpResponse = await getSqlContainerThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.MongoDB:
|
||||
rpResponse = await getMongoDBCollectionThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Cassandra:
|
||||
rpResponse = await getCassandraTableThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Graph:
|
||||
rpResponse = await getGremlinGraphThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Table:
|
||||
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;
|
||||
switch (defaultExperience) {
|
||||
case DefaultAccountExperienceType.DocumentDB:
|
||||
rpResponse = await getSqlContainerThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.MongoDB:
|
||||
rpResponse = await getMongoDBCollectionThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Cassandra:
|
||||
rpResponse = await getCassandraTableThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Graph:
|
||||
rpResponse = await getGremlinGraphThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Table:
|
||||
rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
return rpResponse?.name;
|
||||
};
|
||||
|
||||
const getCollectionOfferIdWithSDK = async (collectionResourceId: string): Promise<string> => {
|
||||
const offers = await readOffers();
|
||||
const offer = offers.find(offer => offer.resource === collectionResourceId);
|
||||
return offer?.id;
|
||||
};
|
||||
|
||||
45
src/Common/dataAccess/readCollectionQuotaInfo.ts
Normal file
45
src/Common/dataAccess/readCollectionQuotaInfo.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as HeadersUtility from "../HeadersUtility";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { ContainerDefinition, Resource } from "@azure/cosmos";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
|
||||
interface ResourceWithStatistics {
|
||||
statistics: DataModels.Statistic[];
|
||||
}
|
||||
|
||||
export const readCollectionQuotaInfo = async (
|
||||
collection: ViewModels.Collection
|
||||
): Promise<DataModels.CollectionQuotaInfo> => {
|
||||
const clearMessage = logConsoleProgress(`Querying containers for database ${collection.id}`);
|
||||
const options: RequestOptions = {};
|
||||
options.populateQuotaInfo = true;
|
||||
options.initialHeaders = options.initialHeaders || {};
|
||||
options.initialHeaders[HttpHeaders.populatePartitionStatistics] = true;
|
||||
|
||||
try {
|
||||
const response = await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.read(options);
|
||||
const quota: DataModels.CollectionQuotaInfo = HeadersUtility.getQuota(response.headers);
|
||||
const resource = response.resource as ContainerDefinition & Resource & ResourceWithStatistics;
|
||||
quota["usageSizeInKB"] = resource.statistics.reduce(
|
||||
(previousValue: number, currentValue: DataModels.Statistic) => previousValue + currentValue.sizeInKB,
|
||||
0
|
||||
);
|
||||
quota["numPartitions"] = resource.statistics.length;
|
||||
quota["uniqueKeyPolicy"] = collection.uniqueKeyPolicy; // TODO: Remove after refactoring (#119617)
|
||||
|
||||
return quota;
|
||||
} catch (error) {
|
||||
handleError(error, "ReadCollectionQuotaInfo", `Error while querying quota info for container ${collection.id}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
};
|
||||
@@ -1,28 +1,51 @@
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { client } from "../CosmosClient";
|
||||
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
||||
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||
import { readOffers } from "./readOffers";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
|
||||
export const readDatabaseOffer = async (
|
||||
params: DataModels.ReadDatabaseOfferParams
|
||||
): Promise<DataModels.OfferWithHeaders> => {
|
||||
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 {
|
||||
if (
|
||||
window.authType === AuthType.AAD &&
|
||||
!userContext.useSDKOperations &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||
) {
|
||||
return await readDatabaseOfferWithARM(params.databaseId);
|
||||
}
|
||||
|
||||
return await readOfferWithSDK(params.offerId, params.databaseResourceId);
|
||||
const response = await client()
|
||||
.offer(offerId)
|
||||
.read(options);
|
||||
return (
|
||||
response && {
|
||||
...response.resource,
|
||||
headers: response.headers
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
handleError(error, "ReadDatabaseOffer", `Error while querying offer for database ${params.databaseId}`);
|
||||
throw error;
|
||||
@@ -31,13 +54,13 @@ export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promis
|
||||
}
|
||||
};
|
||||
|
||||
const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
|
||||
const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> => {
|
||||
let rpResponse;
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const accountName = userContext.databaseAccount.name;
|
||||
const defaultExperience = userContext.defaultExperience;
|
||||
|
||||
let rpResponse;
|
||||
try {
|
||||
switch (defaultExperience) {
|
||||
case DefaultAccountExperienceType.DocumentDB:
|
||||
@@ -55,41 +78,18 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
|
||||
default:
|
||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||
}
|
||||
|
||||
return rpResponse?.name;
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
const getDatabaseOfferIdWithSDK = async (databaseResourceId: string): Promise<string> => {
|
||||
const offers = await readOffers();
|
||||
const offer = offers.find(offer => offer.resource === databaseResourceId);
|
||||
return offer?.id;
|
||||
};
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Item } from "@azure/cosmos";
|
||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
import { client } from "../CosmosClient";
|
||||
import { getEntityName } from "../DocumentUtility";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||
|
||||
export const readDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<Item> => {
|
||||
const entityName = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Reading ${entityName} ${documentId.id()}`);
|
||||
|
||||
try {
|
||||
const response = await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.item(documentId.id(), documentId.partitionKeyValue)
|
||||
.read();
|
||||
|
||||
return response?.resource;
|
||||
} catch (error) {
|
||||
handleError(error, "ReadDocument", `Failed to read ${entityName} ${documentId.id()}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
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 { SDKOfferDefinition } from "../../Contracts/DataModels";
|
||||
import { Offer } from "../../Contracts/DataModels";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError, getErrorMessage } from "../ErrorHandlingUtils";
|
||||
|
||||
export const readOffers = async (): Promise<SDKOfferDefinition[]> => {
|
||||
export const readOffers = async (): Promise<Offer[]> => {
|
||||
const clearMessage = logConsoleProgress(`Querying offers`);
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
import { Item } from "@azure/cosmos";
|
||||
import { client } from "../CosmosClient";
|
||||
import { getEntityName } from "../DocumentUtility";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||
|
||||
export const updateDocument = async (
|
||||
collection: CollectionBase,
|
||||
documentId: DocumentId,
|
||||
newDocument: Item
|
||||
): Promise<Item> => {
|
||||
const entityName = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Updating ${entityName} ${documentId.id()}`);
|
||||
|
||||
try {
|
||||
const response = await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.item(documentId.id(), documentId.partitionKeyValue)
|
||||
.replace(newDocument);
|
||||
|
||||
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);
|
||||
return response?.resource;
|
||||
} catch (error) {
|
||||
handleError(error, "UpdateDocument", `Failed to update ${entityName} ${documentId.id()}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
};
|
||||
@@ -1,14 +1,13 @@
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||
import { Offer, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||
import { OfferDefinition } from "@azure/cosmos";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { ThroughputSettingsUpdateParameters } from "../../Utils/arm/generatedClients/2020-04-01/types";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { parseSDKOfferResponse } from "../OfferUtility";
|
||||
import { readCollectionOffer } from "./readCollectionOffer";
|
||||
import { readDatabaseOffer } from "./readDatabaseOffer";
|
||||
import {
|
||||
@@ -374,21 +373,21 @@ const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpd
|
||||
};
|
||||
|
||||
const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> => {
|
||||
const sdkOfferDefinition = params.currentOffer.offerDefinition;
|
||||
const newOffer: SDKOfferDefinition = {
|
||||
const currentOffer = params.currentOffer;
|
||||
const newOffer: Offer = {
|
||||
content: {
|
||||
offerThroughput: undefined,
|
||||
offerIsRUPerMinuteThroughputEnabled: false
|
||||
},
|
||||
_etag: undefined,
|
||||
_ts: undefined,
|
||||
_rid: sdkOfferDefinition._rid,
|
||||
_self: sdkOfferDefinition._self,
|
||||
id: sdkOfferDefinition.id,
|
||||
offerResourceId: sdkOfferDefinition.offerResourceId,
|
||||
offerVersion: sdkOfferDefinition.offerVersion,
|
||||
offerType: sdkOfferDefinition.offerType,
|
||||
resource: sdkOfferDefinition.resource
|
||||
_rid: currentOffer._rid,
|
||||
_self: currentOffer._self,
|
||||
id: currentOffer.id,
|
||||
offerResourceId: currentOffer.offerResourceId,
|
||||
offerVersion: currentOffer.offerVersion,
|
||||
offerType: currentOffer.offerType,
|
||||
resource: currentOffer.resource
|
||||
};
|
||||
|
||||
if (params.autopilotThroughput) {
|
||||
@@ -416,6 +415,5 @@ const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> =>
|
||||
.offer(params.currentOffer.id)
|
||||
// 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);
|
||||
|
||||
return parseSDKOfferResponse(sdkResponse);
|
||||
return sdkResponse?.resource;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
57
src/Common/dataAccess/updateOfferThroughputBeyondLimit.ts
Normal file
57
src/Common/dataAccess/updateOfferThroughputBeyondLimit.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
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,38 +88,6 @@ export interface Resource {
|
||||
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 {
|
||||
defaultTtl?: number;
|
||||
indexingPolicy?: IndexingPolicy;
|
||||
@@ -130,8 +98,6 @@ export interface Collection extends Resource {
|
||||
changeFeedPolicy?: ChangeFeedPolicy;
|
||||
analyticalStorageTtl?: number;
|
||||
geospatialConfig?: GeospatialConfig;
|
||||
schema?: ISchema;
|
||||
requestSchema?: () => void;
|
||||
}
|
||||
|
||||
export interface Database extends Resource {
|
||||
@@ -208,21 +174,12 @@ export interface QueryMetrics {
|
||||
vmExecutionTime: any;
|
||||
}
|
||||
|
||||
export interface Offer {
|
||||
id: string;
|
||||
autoscaleMaxThroughput: number | undefined;
|
||||
manualThroughput: number | undefined;
|
||||
minimumThroughput: number | undefined;
|
||||
offerDefinition?: SDKOfferDefinition;
|
||||
offerReplacePending: boolean;
|
||||
}
|
||||
|
||||
export interface SDKOfferDefinition extends Resource {
|
||||
export interface Offer extends Resource {
|
||||
offerVersion?: string;
|
||||
offerType?: string;
|
||||
content?: {
|
||||
offerThroughput: number;
|
||||
offerIsRUPerMinuteThroughputEnabled?: boolean;
|
||||
offerIsRUPerMinuteThroughputEnabled: boolean;
|
||||
collectionThroughputInfo?: OfferThroughputInfo;
|
||||
offerAutopilotSettings?: AutoPilotOfferSettings;
|
||||
};
|
||||
@@ -230,6 +187,22 @@ export interface SDKOfferDefinition extends Resource {
|
||||
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 {
|
||||
minimumRUForCollection: number;
|
||||
numPhysicalPartitions: number;
|
||||
@@ -248,6 +221,7 @@ export interface CreateDatabaseAndCollectionRequest {
|
||||
collectionId: string;
|
||||
offerThroughput: number;
|
||||
databaseLevelThroughput: boolean;
|
||||
rupmEnabled?: boolean;
|
||||
partitionKey?: PartitionKey;
|
||||
indexingPolicy?: IndexingPolicy;
|
||||
uniqueKeyPolicy?: UniqueKeyPolicy;
|
||||
|
||||
@@ -32,8 +32,7 @@ export enum MessageTypes {
|
||||
GetArcadiaToken,
|
||||
CreateWorkspace,
|
||||
CreateSparkPool,
|
||||
RefreshDatabaseAccount,
|
||||
InitTestExplorer
|
||||
RefreshDatabaseAccount
|
||||
}
|
||||
|
||||
export { Versions, ActionContracts, Diagnostics };
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export enum SubscriptionType {
|
||||
Benefits,
|
||||
EA,
|
||||
Free,
|
||||
Internal,
|
||||
PAYG
|
||||
}
|
||||
@@ -15,10 +15,8 @@ import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
||||
import Trigger from "../Explorer/Tree/Trigger";
|
||||
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
|
||||
import { SelfServeType } from "../SelfServe/SelfServeUtils";
|
||||
import { UploadDetails } from "../workers/upload/definitions";
|
||||
import * as DataModels from "./DataModels";
|
||||
import { SubscriptionType } from "./SubscriptionType";
|
||||
|
||||
export interface TokenProvider {
|
||||
getAuthHeader(): Promise<Headers>;
|
||||
@@ -117,11 +115,9 @@ export interface CollectionBase extends TreeNode {
|
||||
export interface Collection extends CollectionBase {
|
||||
defaultTtl: ko.Observable<number>;
|
||||
analyticalStorageTtl: ko.Observable<number>;
|
||||
schema?: DataModels.ISchema;
|
||||
requestSchema?: () => void;
|
||||
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
||||
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
||||
usageSizeInKB: ko.Observable<number>;
|
||||
quotaInfo: ko.Observable<DataModels.CollectionQuotaInfo>;
|
||||
offer: ko.Observable<DataModels.Offer>;
|
||||
conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
||||
changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
||||
@@ -166,6 +162,7 @@ export interface Collection extends CollectionBase {
|
||||
loadStoredProcedures(): Promise<any>;
|
||||
loadTriggers(): Promise<any>;
|
||||
loadOffer(): Promise<void>;
|
||||
loadAutopilotOfferWithRetry(): Promise<DataModels.Offer>;
|
||||
|
||||
createStoredProcedureNode(data: StoredProcedureDefinition & Resource): StoredProcedure;
|
||||
createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction;
|
||||
@@ -362,8 +359,7 @@ export enum CollectionTabKind {
|
||||
SparkMasterTab = 16,
|
||||
Gallery = 17,
|
||||
NotebookViewer = 18,
|
||||
Schema = 19,
|
||||
SettingsV2 = 20
|
||||
SettingsV2 = 19
|
||||
}
|
||||
|
||||
export enum TerminalKind {
|
||||
@@ -396,7 +392,6 @@ export interface DataExplorerInputsFrame {
|
||||
isAuthWithresourceToken?: boolean;
|
||||
defaultCollectionThroughput?: CollectionCreationDefaults;
|
||||
flights?: readonly string[];
|
||||
selfServeType?: SelfServeType;
|
||||
}
|
||||
|
||||
export interface CollectionCreationDefaults {
|
||||
@@ -418,6 +413,14 @@ export interface ThroughputDefaults {
|
||||
shared: number;
|
||||
}
|
||||
|
||||
export enum SubscriptionType {
|
||||
Benefits,
|
||||
EA,
|
||||
Free,
|
||||
Internal,
|
||||
PAYG
|
||||
}
|
||||
|
||||
export class MonacoEditorSettings {
|
||||
public readonly language: string;
|
||||
public readonly readOnly: boolean;
|
||||
|
||||
@@ -44,6 +44,10 @@ describe("Component Registerer", () => {
|
||||
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", () => {
|
||||
expect(ko.components.isRegistered("settings-tab-v2")).toBe(true);
|
||||
});
|
||||
|
||||
@@ -31,6 +31,7 @@ ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTa
|
||||
ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab());
|
||||
ko.components.register("trigger-tab", new TabComponents.TriggerTab());
|
||||
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("query-tab", new TabComponents.QueryTab());
|
||||
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.
|
||||
*/
|
||||
class CollapsiblePanelViewModel {
|
||||
public params: CollapsiblePanelParams;
|
||||
private params: CollapsiblePanelParams;
|
||||
private isCollapsed: ko.Observable<boolean>;
|
||||
|
||||
public constructor(params: CollapsiblePanelParams) {
|
||||
@@ -50,7 +50,7 @@ class CollapsiblePanelViewModel {
|
||||
this.isCollapsed = params.isCollapsed || ko.observable(false);
|
||||
}
|
||||
|
||||
public toggleCollapse(): void {
|
||||
private toggleCollapse(): void {
|
||||
this.isCollapsed(!this.isCollapsed());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,11 +44,12 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
||||
onChange?: (_?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => void;
|
||||
}[] = [
|
||||
{ key: "feature.enablechangefeedpolicy", label: "Enable change feed policy", value: "true" },
|
||||
{ key: "feature.enablerupm", label: "Enable RUPM", value: "true" },
|
||||
{ key: "feature.dataexplorerexecutesproc", label: "Execute stored procedure", value: "true" },
|
||||
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
|
||||
{ key: "feature.enablettl", label: "Enable TTL", value: "true" },
|
||||
{ key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" },
|
||||
{ key: "feature.selfServeType", label: "Self serve feature", value: "sample" },
|
||||
{ key: "feature.enablecodeofconduct", label: "Enable Code Of Conduct Acknowledgement", value: "true" },
|
||||
{
|
||||
key: "feature.enableLinkInjection",
|
||||
label: "Enable Injecting Notebook Viewer Link into the first cell",
|
||||
|
||||
@@ -131,6 +131,12 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
label="Enable change feed policy"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enablerupm"
|
||||
label="Enable RUPM"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.dataexplorerexecutesproc"
|
||||
@@ -157,8 +163,8 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.selfServeType"
|
||||
label="Self serve feature"
|
||||
key="feature.enablecodeofconduct"
|
||||
label="Enable Code Of Conduct Acknowledgement"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
|
||||
@@ -71,7 +71,7 @@ interface InputTypeaheadParams {
|
||||
/**
|
||||
* This function gets called when pressing ENTER on the input box
|
||||
*/
|
||||
submitFct?: (inputValue: string | null, selection: Item | null) => void;
|
||||
submitFct?: (inputValue: string, selection: Item) => void;
|
||||
|
||||
/**
|
||||
* Typehead comes with a Search button that we normally remove.
|
||||
@@ -88,8 +88,8 @@ interface OnClickItem {
|
||||
}
|
||||
|
||||
interface Cache {
|
||||
inputValue: string | null;
|
||||
selection: Item | null;
|
||||
inputValue: string;
|
||||
selection: Item;
|
||||
}
|
||||
|
||||
class InputTypeaheadViewModel {
|
||||
@@ -98,12 +98,15 @@ class InputTypeaheadViewModel {
|
||||
private params: InputTypeaheadParams;
|
||||
|
||||
private cache: Cache;
|
||||
private inputValue: string;
|
||||
private selection: Item;
|
||||
|
||||
public constructor(params: InputTypeaheadParams) {
|
||||
this.instanceNumber = InputTypeaheadViewModel.instanceCount++;
|
||||
this.params = params;
|
||||
|
||||
this.params.choices.subscribe(this.initializeTypeahead.bind(this));
|
||||
|
||||
this.cache = {
|
||||
inputValue: null,
|
||||
selection: null
|
||||
@@ -158,7 +161,7 @@ class InputTypeaheadViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
($ as any).typeahead(options);
|
||||
$.typeahead(options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,11 +177,11 @@ class InputTypeaheadViewModel {
|
||||
* Use ko's "template: afterRender" callback to do that without actually using any template.
|
||||
* Another way is to call it within setTimeout() in constructor.
|
||||
*/
|
||||
public afterRender(): void {
|
||||
private afterRender(): void {
|
||||
this.initializeTypeahead();
|
||||
}
|
||||
|
||||
public submit(): void {
|
||||
private submit(): void {
|
||||
if (this.params.submitFct) {
|
||||
this.params.submitFct(this.cache.inputValue, this.cache.selection);
|
||||
}
|
||||
|
||||
@@ -59,12 +59,10 @@ export class JsonEditorViewModel extends WaitsForTemplateViewModel {
|
||||
this.params = params;
|
||||
|
||||
this.params.content.subscribe((newValue: string) => {
|
||||
if (newValue) {
|
||||
if (!!this.editor) {
|
||||
this.editor.getModel().setValue(newValue);
|
||||
} else {
|
||||
this.createEditor(newValue, this.configureEditor.bind(this));
|
||||
}
|
||||
if (!!this.editor) {
|
||||
this.editor.getModel().setValue(newValue);
|
||||
} else {
|
||||
this.createEditor(newValue, this.configureEditor.bind(this));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
Dropdown,
|
||||
FocusZone,
|
||||
FontIcon,
|
||||
FontWeights,
|
||||
IDropdownOption,
|
||||
IPageSpecification,
|
||||
@@ -17,7 +16,7 @@ import {
|
||||
Text
|
||||
} from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { IGalleryItem, IJunoResponse, IPublicGalleryData, JunoClient } from "../../../Juno/JunoClient";
|
||||
import { IGalleryItem, JunoClient, IJunoResponse, IPublicGalleryData } from "../../../Juno/JunoClient";
|
||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
|
||||
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
|
||||
@@ -137,7 +136,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const tabs: GalleryTabInfo[] = [this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
||||
const tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
||||
|
||||
if (this.props.container?.isGalleryPublishEnabled()) {
|
||||
tabs.push(
|
||||
@@ -147,7 +146,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
this.state.isCodeOfConductAccepted
|
||||
)
|
||||
);
|
||||
tabs.push(this.createFavoritesTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
||||
tabs.push(this.createTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
||||
|
||||
// explicitly checking if isCodeOfConductAccepted is not false, as it is initially undefined.
|
||||
// Displaying code of conduct component on gallery load should not be the default behavior.
|
||||
@@ -184,27 +183,6 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
);
|
||||
}
|
||||
|
||||
private isEmptyData = (data: IGalleryItem[]): boolean => {
|
||||
return !data || data.length === 0;
|
||||
};
|
||||
|
||||
private createEmptyTabContent = (iconName: string, line1: string, line2: string): JSX.Element => {
|
||||
return (
|
||||
<Stack horizontalAlign="center" tokens={{ childrenGap: 10 }}>
|
||||
<FontIcon iconName={iconName} style={{ fontSize: 100, color: "lightgray", marginTop: 20 }} />
|
||||
<Text styles={{ root: { fontWeight: FontWeights.semibold } }}>{line1}</Text>
|
||||
<Text>{line2}</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
private createSamplesTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
|
||||
return {
|
||||
tab,
|
||||
content: this.createSearchBarHeader(this.createCardsTabContent(data))
|
||||
};
|
||||
};
|
||||
|
||||
private createPublicGalleryTab(
|
||||
tab: GalleryTab,
|
||||
data: IGalleryItem[],
|
||||
@@ -216,29 +194,17 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
};
|
||||
}
|
||||
|
||||
private createFavoritesTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
|
||||
private createTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
|
||||
return {
|
||||
tab,
|
||||
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))
|
||||
content: this.createSearchBarHeader(this.createCardsTabContent(data))
|
||||
};
|
||||
}
|
||||
|
||||
private createPublishedNotebooksTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
|
||||
return {
|
||||
tab,
|
||||
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)
|
||||
content: this.createPublishedNotebooksTabContent(data)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -398,9 +364,9 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
||||
if (!offline) {
|
||||
try {
|
||||
let response: IJunoResponse<IGalleryItem[]> | IJunoResponse<IPublicGalleryData>;
|
||||
if (this.props.container) {
|
||||
response = await this.props.junoClient.getPublicGalleryData();
|
||||
let response: IJunoResponse<IPublicGalleryData> | IJunoResponse<IGalleryItem[]>;
|
||||
if (this.props.container.isCodeOfConductEnabled()) {
|
||||
response = await this.props.junoClient.fetchPublicNotebooks();
|
||||
this.isCodeOfConductAccepted = response.data?.metadata.acceptedCodeOfConduct;
|
||||
this.publicNotebooks = response.data?.notebooksData;
|
||||
} else {
|
||||
@@ -602,7 +568,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
|
||||
private deleteItem = async (data: IGalleryItem): Promise<void> => {
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -89,12 +89,12 @@ describe("SettingsComponent", () => {
|
||||
it("auto pilot helper functions pass on correct value", () => {
|
||||
const newCollection = { ...collection };
|
||||
newCollection.offer = ko.observable<DataModels.Offer>({
|
||||
autoscaleMaxThroughput: 10000,
|
||||
manualThroughput: undefined,
|
||||
minimumThroughput: 400,
|
||||
id: "test",
|
||||
offerReplacePending: false
|
||||
});
|
||||
content: {
|
||||
offerAutopilotSettings: {
|
||||
maxThroughput: 10000
|
||||
}
|
||||
}
|
||||
} as DataModels.Offer);
|
||||
|
||||
const props = { ...baseProps };
|
||||
props.settingsTab.collection = newCollection;
|
||||
@@ -187,6 +187,21 @@ describe("SettingsComponent", () => {
|
||||
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 () => {
|
||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
||||
wrapper.setState({ isSubSettingsSaveable: true, isScaleSaveable: true, isMongoIndexingPolicySaveable: true });
|
||||
@@ -231,7 +246,7 @@ describe("SettingsComponent", () => {
|
||||
|
||||
it("getUpdatedConflictResolutionPolicy", () => {
|
||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
||||
const conflictResolutionPolicyPath = "/_ts";
|
||||
const conflictResolutionPolicyPath = "_ts";
|
||||
const conflictResolutionPolicyProcedure = "sample_sproc";
|
||||
const expectSprocPath =
|
||||
"/dbs/" + collection.databaseId + "/colls/" + collection.id() + "/sprocs/" + conflictResolutionPolicyProcedure;
|
||||
|
||||
@@ -2,23 +2,28 @@ import * as React from "react";
|
||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as SharedConstants from "../../../Shared/Constants";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import DiscardIcon from "../../../../images/discard.svg";
|
||||
import SaveIcon from "../../../../images/save-cosmos.svg";
|
||||
import { traceStart, traceFailure, traceSuccess, trace } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import Explorer from "../../Explorer";
|
||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { updateOfferThroughputBeyondLimit } from "../../../Common/dataAccess/updateOfferThroughputBeyondLimit";
|
||||
import SettingsTab from "../../Tabs/SettingsTabV2";
|
||||
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
|
||||
import { throughputUnit } from "./SettingsRenderUtils";
|
||||
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
|
||||
import {
|
||||
MongoIndexingPolicyComponent,
|
||||
MongoIndexingPolicyComponentProps
|
||||
} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
|
||||
import {
|
||||
getMaxRUs,
|
||||
hasDatabaseSharedThroughput,
|
||||
GeospatialConfigType,
|
||||
TtlType,
|
||||
@@ -44,7 +49,6 @@ import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/genera
|
||||
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
|
||||
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
|
||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||
import { isEmpty } from "underscore";
|
||||
|
||||
interface SettingsV2TabInfo {
|
||||
tab: SettingsV2TabTypes;
|
||||
@@ -138,8 +142,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
|
||||
// Mongo container with system partition key still treat as "Fixed"
|
||||
this.isFixedContainer =
|
||||
this.container.isPreferredApiMongoDB() &&
|
||||
(!this.collection.partitionKey || this.collection.partitionKey.systemKey);
|
||||
!this.collection.partitionKey ||
|
||||
(this.container.isPreferredApiMongoDB() && this.collection.partitionKey.systemKey);
|
||||
|
||||
this.state = {
|
||||
throughput: undefined,
|
||||
@@ -223,6 +227,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
|
||||
public loadMongoIndexes = async (): Promise<void> => {
|
||||
if (
|
||||
this.container.isMongoIndexEditorEnabled() &&
|
||||
this.container.isPreferredApiMongoDB() &&
|
||||
this.container.isEnableMongoCapabilityPresent() &&
|
||||
this.container.databaseAccount()
|
||||
@@ -270,14 +275,19 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
};
|
||||
|
||||
private setAutoPilotStates = (): void => {
|
||||
const autoscaleMaxThroughput = this.collection?.offer()?.autoscaleMaxThroughput;
|
||||
const offer = this.collection?.offer && this.collection.offer();
|
||||
const offerAutopilotSettings = offer?.content?.offerAutopilotSettings;
|
||||
|
||||
if (autoscaleMaxThroughput && AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
|
||||
if (
|
||||
offerAutopilotSettings &&
|
||||
offerAutopilotSettings.maxThroughput &&
|
||||
AutoPilotUtils.isValidAutoPilotThroughput(offerAutopilotSettings.maxThroughput)
|
||||
) {
|
||||
this.setState({
|
||||
isAutoPilotSelected: true,
|
||||
wasAutopilotOriginallySet: true,
|
||||
autoPilotThroughput: autoscaleMaxThroughput,
|
||||
autoPilotThroughputBaseline: autoscaleMaxThroughput
|
||||
autoPilotThroughput: offerAutopilotSettings.maxThroughput,
|
||||
autoPilotThroughputBaseline: offerAutopilotSettings.maxThroughput
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -295,7 +305,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
!!this.collection.conflictResolutionPolicy();
|
||||
|
||||
public isOfferReplacePending = (): boolean => {
|
||||
return this.collection?.offer()?.offerReplacePending;
|
||||
const offer = this.collection?.offer && this.collection.offer();
|
||||
return (
|
||||
offer &&
|
||||
Object.keys(offer).find(value => value === "headers") &&
|
||||
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
||||
);
|
||||
};
|
||||
|
||||
public onSaveClick = async (): Promise<void> => {
|
||||
@@ -433,33 +448,105 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
}
|
||||
|
||||
if (this.state.isScaleSaveable) {
|
||||
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 : this.state.throughput
|
||||
};
|
||||
if (this.hasProvisioningTypeChanged()) {
|
||||
if (this.state.isAutoPilotSelected) {
|
||||
updateOfferParams.migrateToAutoPilot = true;
|
||||
} else {
|
||||
updateOfferParams.migrateToManual = true;
|
||||
}
|
||||
const newThroughput = this.state.throughput;
|
||||
const newOffer: DataModels.Offer = { ...this.collection.offer() };
|
||||
const originalThroughputValue: number = this.state.throughput;
|
||||
|
||||
if (newOffer.content) {
|
||||
newOffer.content.offerThroughput = newThroughput;
|
||||
} else {
|
||||
newOffer.content = {
|
||||
offerThroughput: newThroughput,
|
||||
offerIsRUPerMinuteThroughputEnabled: false
|
||||
};
|
||||
}
|
||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||
this.collection.offer(updatedOffer);
|
||||
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
||||
|
||||
const headerOptions: RequestOptions = { initialHeaders: {} };
|
||||
|
||||
if (this.state.isAutoPilotSelected) {
|
||||
this.setState({
|
||||
autoPilotThroughput: updatedOffer.autoscaleMaxThroughput,
|
||||
autoPilotThroughputBaseline: updatedOffer.autoscaleMaxThroughput
|
||||
});
|
||||
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({
|
||||
throughput: updatedOffer.manualThroughput,
|
||||
throughputBaseline: updatedOffer.manualThroughput
|
||||
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);
|
||||
|
||||
if (this.state.isAutoPilotSelected) {
|
||||
const autoPilotOffer = await this.collection.loadAutopilotOfferWithRetry();
|
||||
this.setState({
|
||||
autoPilotThroughput: autoPilotOffer.content.offerAutopilotSettings.maxThroughput,
|
||||
autoPilotThroughputBaseline: autoPilotOffer.content.offerAutopilotSettings.maxThroughput
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
throughput: updatedOffer.content.offerThroughput,
|
||||
throughputBaseline: updatedOffer.content.offerThroughput
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
||||
}
|
||||
}
|
||||
this.container.isRefreshingExplorer(false);
|
||||
@@ -684,7 +771,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
|
||||
if (policy.mode === DataModels.ConflictResolutionMode.LastWriterWins) {
|
||||
policy.conflictResolutionPath = this.state.conflictResolutionPolicyPath;
|
||||
if (!policy.conflictResolutionPath?.startsWith("/")) {
|
||||
if (policy.conflictResolutionPath?.startsWith("/")) {
|
||||
policy.conflictResolutionPath = "/" + policy.conflictResolutionPath;
|
||||
}
|
||||
}
|
||||
@@ -725,7 +812,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
}
|
||||
}
|
||||
|
||||
const offerThroughput = this.collection.offer()?.manualThroughput;
|
||||
const offerThroughput = this.collection?.offer && this.collection.offer()?.content?.offerThroughput;
|
||||
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
||||
? ChangeFeedPolicyState.On
|
||||
: ChangeFeedPolicyState.Off;
|
||||
@@ -916,18 +1003,15 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||
content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />
|
||||
});
|
||||
} else if (this.container.isPreferredApiMongoDB()) {
|
||||
if (isEmpty(this.container.features())) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||
content: mongoIndexingPolicyAADError
|
||||
});
|
||||
} else if (this.container.isEnableMongoCapabilityPresent()) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
this.container.isMongoIndexEditorEnabled() &&
|
||||
this.container.isPreferredApiMongoDB() &&
|
||||
this.container.isEnableMongoCapabilityPresent()
|
||||
) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />
|
||||
});
|
||||
}
|
||||
|
||||
if (this.hasConflictResolution()) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { IColumn, Text } from "office-ui-fabric-react";
|
||||
import {
|
||||
getAutoPilotV3SpendElement,
|
||||
getEstimatedSpendingElement,
|
||||
getEstimatedSpendElement,
|
||||
getEstimatedAutoscaleSpendElement,
|
||||
manualToAutoscaleDisclaimerElement,
|
||||
ttlWarning,
|
||||
indexingPolicynUnsavedWarningMessage,
|
||||
@@ -19,37 +19,11 @@ import {
|
||||
mongoIndexingPolicyDisclaimer,
|
||||
mongoIndexingPolicyAADError,
|
||||
mongoIndexTransformationRefreshingMessage,
|
||||
renderMongoIndexTransformationRefreshMessage,
|
||||
ManualEstimatedSpendingDisplayProps,
|
||||
PriceBreakdown,
|
||||
getRuPriceBreakdown
|
||||
renderMongoIndexTransformationRefreshMessage
|
||||
} from "./SettingsRenderUtils";
|
||||
|
||||
class SettingsRenderUtilsTestComponent extends React.Component {
|
||||
public render(): JSX.Element {
|
||||
const estimatedSpendingColumns: IColumn[] = [
|
||||
{ key: "costType", name: "", fieldName: "costType", minWidth: 100, maxWidth: 200, isResizable: true },
|
||||
{ key: "hourly", name: "Hourly", fieldName: "hourly", minWidth: 100, maxWidth: 200, isResizable: true },
|
||||
{ key: "daily", name: "Daily", fieldName: "daily", minWidth: 100, maxWidth: 200, isResizable: true },
|
||||
{ key: "monthly", name: "Monthly", fieldName: "monthly", minWidth: 100, maxWidth: 200, isResizable: true }
|
||||
];
|
||||
const estimatedSpendingItems: ManualEstimatedSpendingDisplayProps[] = [
|
||||
{
|
||||
costType: <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 (
|
||||
<>
|
||||
{getAutoPilotV3SpendElement(1000, false)}
|
||||
@@ -57,7 +31,9 @@ class SettingsRenderUtilsTestComponent extends React.Component {
|
||||
{getAutoPilotV3SpendElement(1000, true)}
|
||||
{getAutoPilotV3SpendElement(undefined, true)}
|
||||
|
||||
{getEstimatedSpendingElement(estimatedSpendingColumns, estimatedSpendingItems, 1000, 2, priceBreakdown, false)}
|
||||
{getEstimatedSpendElement(1000, "mooncake", 2, false, true)}
|
||||
|
||||
{getEstimatedAutoscaleSpendElement(1000, "mooncake", 2, false)}
|
||||
|
||||
{manualToAutoscaleDisclaimerElement}
|
||||
{ttlWarning}
|
||||
@@ -66,7 +42,7 @@ class SettingsRenderUtilsTestComponent extends React.Component {
|
||||
{updateThroughputDelayedApplyWarningMessage}
|
||||
|
||||
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||
{getThroughputApplyShortDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection")}
|
||||
{getThroughputApplyShortDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||
{getThroughputApplyLongDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||
|
||||
{getToolTipContainer(<span>Sample Text</span>)}
|
||||
@@ -93,14 +69,4 @@ describe("SettingsUtils functions", () => {
|
||||
const wrapper = shallow(<SettingsRenderUtilsTestComponent />);
|
||||
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,13 +3,14 @@ import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||
import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants";
|
||||
import { Urls, StyleConstants } from "../../../Common/Constants";
|
||||
import {
|
||||
computeAutoscaleUsagePriceHourly,
|
||||
getPriceCurrency,
|
||||
getCurrencySign,
|
||||
getAutoscalePricePerRu,
|
||||
getMultimasterMultiplier,
|
||||
computeRUUsagePriceHourly,
|
||||
getPricePerRu,
|
||||
estimatedCostDisclaimer
|
||||
calculateEstimateNumber
|
||||
} from "../../../Utils/PricingUtils";
|
||||
import {
|
||||
ITextFieldStyles,
|
||||
@@ -31,42 +32,11 @@ import {
|
||||
MessageBarType,
|
||||
Stack,
|
||||
Spinner,
|
||||
SpinnerSize,
|
||||
DetailsList,
|
||||
IColumn,
|
||||
SelectionMode,
|
||||
DetailsListLayoutMode,
|
||||
IDetailsRowProps,
|
||||
DetailsRow,
|
||||
IDetailsColumnStyles
|
||||
SpinnerSize
|
||||
} from "office-ui-fabric-react";
|
||||
import { isDirtyTypes, isDirty } from "./SettingsUtils";
|
||||
|
||||
export interface EstimatedSpendingDisplayProps {
|
||||
costType: JSX.Element;
|
||||
}
|
||||
|
||||
export interface ManualEstimatedSpendingDisplayProps extends EstimatedSpendingDisplayProps {
|
||||
hourly: JSX.Element;
|
||||
daily: JSX.Element;
|
||||
monthly: JSX.Element;
|
||||
}
|
||||
|
||||
export interface AutoscaleEstimatedSpendingDisplayProps extends EstimatedSpendingDisplayProps {
|
||||
minPerMonth: JSX.Element;
|
||||
maxPerMonth: JSX.Element;
|
||||
}
|
||||
|
||||
export interface PriceBreakdown {
|
||||
hourlyPrice: number;
|
||||
dailyPrice: number;
|
||||
monthlyPrice: number;
|
||||
pricePerRu: number;
|
||||
currency: string;
|
||||
currencySign: string;
|
||||
}
|
||||
|
||||
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14 } };
|
||||
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } };
|
||||
|
||||
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
||||
label: {
|
||||
@@ -134,16 +104,6 @@ export const transparentDetailsRowStyles: Partial<IDetailsRowStyles> = {
|
||||
}
|
||||
};
|
||||
|
||||
export const transparentDetailsHeaderStyle: Partial<IDetailsColumnStyles> = {
|
||||
root: {
|
||||
selectors: {
|
||||
":hover": {
|
||||
background: "transparent"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const customDetailsListStyles: Partial<IDetailsListStyles> = {
|
||||
root: {
|
||||
selectors: {
|
||||
@@ -166,17 +126,10 @@ export const separatorStyles: Partial<ISeparatorStyles> = {
|
||||
]
|
||||
};
|
||||
|
||||
export const messageBarStyles: Partial<IMessageBarStyles> = {
|
||||
root: { marginTop: "5px", backgroundColor: "white" },
|
||||
text: { fontSize: 14 }
|
||||
};
|
||||
export const messageBarStyles: Partial<IMessageBarStyles> = { root: { marginTop: "5px" } };
|
||||
|
||||
export const throughputUnit = "RU/s";
|
||||
|
||||
export function onRenderRow(props: IDetailsRowProps): JSX.Element {
|
||||
return <DetailsRow {...props} styles={transparentDetailsRowStyles} />;
|
||||
}
|
||||
|
||||
export const getAutoPilotV3SpendElement = (
|
||||
maxAutoPilotThroughputSet: number,
|
||||
isDatabaseThroughput: boolean,
|
||||
@@ -212,61 +165,64 @@ export const getAutoPilotV3SpendElement = (
|
||||
);
|
||||
};
|
||||
|
||||
export const getRuPriceBreakdown = (
|
||||
export const getEstimatedAutoscaleSpendElement = (
|
||||
throughput: number,
|
||||
serverId: string,
|
||||
numberOfRegions: number,
|
||||
isMultimaster: boolean,
|
||||
isAutoscale: boolean
|
||||
): PriceBreakdown => {
|
||||
const hourlyPrice: number = computeRUUsagePriceHourly({
|
||||
serverId: serverId,
|
||||
requestUnits: throughput,
|
||||
numberOfRegions: numberOfRegions,
|
||||
multimasterEnabled: isMultimaster,
|
||||
isAutoscale: isAutoscale
|
||||
});
|
||||
const basePricePerRu: number = isAutoscale
|
||||
? getAutoscalePricePerRu(serverId, getMultimasterMultiplier(numberOfRegions, isMultimaster))
|
||||
: getPricePerRu(serverId);
|
||||
return {
|
||||
hourlyPrice: hourlyPrice,
|
||||
dailyPrice: hourlyPrice * 24,
|
||||
monthlyPrice: hourlyPrice * hoursInAMonth,
|
||||
pricePerRu: basePricePerRu * getMultimasterMultiplier(numberOfRegions, isMultimaster),
|
||||
currency: getPriceCurrency(serverId),
|
||||
currencySign: getCurrencySign(serverId)
|
||||
};
|
||||
regions: number,
|
||||
multimaster: boolean
|
||||
): JSX.Element => {
|
||||
const hourlyPrice: number = computeAutoscaleUsagePriceHourly(serverId, throughput, regions, multimaster);
|
||||
const monthlyPrice: number = hourlyPrice * hoursInAMonth;
|
||||
const currency: string = getPriceCurrency(serverId);
|
||||
const currencySign: string = getCurrencySign(serverId);
|
||||
const pricePerRu =
|
||||
getAutoscalePricePerRu(serverId, getMultimasterMultiplier(regions, multimaster)) *
|
||||
getMultimasterMultiplier(regions, multimaster);
|
||||
|
||||
return (
|
||||
<Text id="autoscaleSpendElement">
|
||||
Estimated monthly cost ({currency}) is{" "}
|
||||
<b>
|
||||
{currencySign}
|
||||
{calculateEstimateNumber(monthlyPrice / 10)}
|
||||
{` - `}
|
||||
{currencySign}
|
||||
{calculateEstimateNumber(monthlyPrice)}{" "}
|
||||
</b>
|
||||
({"regions: "} {regions}, {throughput / 10} - {throughput} RU/s, {currencySign}
|
||||
{pricePerRu}/RU)
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export const getEstimatedSpendingElement = (
|
||||
estimatedSpendingColumns: IColumn[],
|
||||
estimatedSpendingItems: EstimatedSpendingDisplayProps[],
|
||||
export const getEstimatedSpendElement = (
|
||||
throughput: number,
|
||||
numberOfRegions: number,
|
||||
priceBreakdown: PriceBreakdown,
|
||||
isAutoscale: boolean
|
||||
serverId: string,
|
||||
regions: number,
|
||||
multimaster: boolean,
|
||||
rupmEnabled: boolean
|
||||
): JSX.Element => {
|
||||
const ruRange: string = isAutoscale ? throughput / 10 + " RU/s - " : "";
|
||||
const hourlyPrice: number = computeRUUsagePriceHourly(serverId, rupmEnabled, throughput, regions, multimaster);
|
||||
const dailyPrice: number = hourlyPrice * 24;
|
||||
const monthlyPrice: number = hourlyPrice * hoursInAMonth;
|
||||
const currency: string = getPriceCurrency(serverId);
|
||||
const currencySign: string = getCurrencySign(serverId);
|
||||
const pricePerRu = getPricePerRu(serverId) * getMultimasterMultiplier(regions, multimaster);
|
||||
|
||||
return (
|
||||
<Stack {...addMongoIndexStackProps} styles={mediumWidthStackStyles}>
|
||||
<DetailsList
|
||||
disableSelectionZone
|
||||
items={estimatedSpendingItems}
|
||||
columns={estimatedSpendingColumns}
|
||||
selectionMode={SelectionMode.none}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
onRenderRow={onRenderRow}
|
||||
/>
|
||||
<Text id="throughputSpendElement">
|
||||
({"regions: "} {numberOfRegions}, {ruRange}
|
||||
{throughput} RU/s, {priceBreakdown.currencySign}
|
||||
{priceBreakdown.pricePerRu}/RU)
|
||||
</Text>
|
||||
<Text>
|
||||
<em>{estimatedCostDisclaimer}</em>
|
||||
</Text>
|
||||
</Stack>
|
||||
<Text id="throughputSpendElement">
|
||||
Estimated cost ({currency}):{" "}
|
||||
<b>
|
||||
{currencySign}
|
||||
{calculateEstimateNumber(hourlyPrice)} hourly {` / `}
|
||||
{currencySign}
|
||||
{calculateEstimateNumber(dailyPrice)} daily {` / `}
|
||||
{currencySign}
|
||||
{calculateEstimateNumber(monthlyPrice)} monthly{" "}
|
||||
</b>
|
||||
({"regions: "} {regions}, {throughput}RU/s, {currencySign}
|
||||
{pricePerRu}/RU)
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -310,13 +266,6 @@ export const updateThroughputDelayedApplyWarningMessage: JSX.Element = (
|
||||
</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 = (
|
||||
isAutoscale: boolean,
|
||||
throughput: number,
|
||||
@@ -370,13 +319,14 @@ export const getThroughputApplyShortDelayMessage = (
|
||||
throughput: number,
|
||||
throughputUnit: string,
|
||||
databaseName: string,
|
||||
collectionName: string
|
||||
collectionName: string,
|
||||
targetThroughput: number
|
||||
): JSX.Element => (
|
||||
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
|
||||
A request to increase the throughput is currently in progress. This operation will take some time to complete.
|
||||
<br />
|
||||
Database: {databaseName}, Container: {collectionName}{" "}
|
||||
{getCurrentThroughput(isAutoscale, throughput, throughputUnit)}
|
||||
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, targetThroughput)}
|
||||
</Text>
|
||||
);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ exports[`IndexingPolicyRefreshComponent renders 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 14,
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
IconButton,
|
||||
Text,
|
||||
SelectionMode,
|
||||
IDetailsRowProps,
|
||||
DetailsRow,
|
||||
IColumn,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
@@ -19,11 +21,12 @@ import {
|
||||
mongoIndexingPolicyDisclaimer,
|
||||
mediumWidthStackStyles,
|
||||
subComponentStackProps,
|
||||
transparentDetailsRowStyles,
|
||||
createAndAddMongoIndexStackProps,
|
||||
separatorStyles,
|
||||
mongoIndexingPolicyAADError,
|
||||
indexingPolicynUnsavedWarningMessage,
|
||||
infoAndToolTipTextStyle,
|
||||
onRenderRow
|
||||
infoAndToolTipTextStyle
|
||||
} from "../../SettingsRenderUtils";
|
||||
import { MongoIndex } from "../../../../../Utils/arm/generatedClients/2020-04-01/types";
|
||||
import {
|
||||
@@ -37,6 +40,7 @@ import {
|
||||
} from "../../SettingsUtils";
|
||||
import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
|
||||
import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import { AuthType } from "../../../../../AuthType";
|
||||
import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
||||
|
||||
export interface MongoIndexingPolicyComponentProps {
|
||||
@@ -138,6 +142,10 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
return undefined;
|
||||
};
|
||||
|
||||
private onRenderRow = (props: IDetailsRowProps): JSX.Element => {
|
||||
return <DetailsRow {...props} styles={transparentDetailsRowStyles} />;
|
||||
};
|
||||
|
||||
private getActionButton = (arrayPosition: number, isCurrentIndex: boolean): JSX.Element => {
|
||||
return isCurrentIndex ? (
|
||||
<IconButton
|
||||
@@ -247,7 +255,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
items={initialIndexes}
|
||||
columns={this.initialIndexesColumns}
|
||||
selectionMode={SelectionMode.none}
|
||||
onRenderRow={onRenderRow}
|
||||
onRenderRow={this.onRenderRow}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
/>
|
||||
{this.renderIndexesToBeAdded()}
|
||||
@@ -273,7 +281,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
items={indexesToBeDropped}
|
||||
columns={this.indexesToBeDroppedColumns}
|
||||
selectionMode={SelectionMode.none}
|
||||
onRenderRow={onRenderRow}
|
||||
onRenderRow={this.onRenderRow}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
/>
|
||||
)}
|
||||
@@ -313,7 +321,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
</Stack>
|
||||
);
|
||||
} else {
|
||||
return <Spinner size={SpinnerSize.large} />;
|
||||
return window.authType !== AuthType.AAD ? mongoIndexingPolicyAADError : <Spinner size={SpinnerSize.large} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ describe("ScaleComponent", () => {
|
||||
} as DataModels.Notification
|
||||
};
|
||||
|
||||
it("renders with correct initial notification", () => {
|
||||
it("renders with correct intiial notification", () => {
|
||||
let wrapper = shallow(<ScaleComponent {...baseProps} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
|
||||
@@ -54,13 +54,16 @@ describe("ScaleComponent", () => {
|
||||
|
||||
const newCollection = { ...collection };
|
||||
const maxThroughput = 5000;
|
||||
const targetMaxThroughput = 50000;
|
||||
newCollection.offer = ko.observable({
|
||||
manualThroughput: undefined,
|
||||
autoscaleMaxThroughput: maxThroughput,
|
||||
minimumThroughput: 400,
|
||||
id: "offer",
|
||||
offerReplacePending: true
|
||||
});
|
||||
content: {
|
||||
offerAutopilotSettings: {
|
||||
maxThroughput: maxThroughput,
|
||||
targetMaxThroughput: targetMaxThroughput
|
||||
}
|
||||
},
|
||||
headers: { "x-ms-offer-replace-pending": true }
|
||||
} as DataModels.OfferWithHeaders);
|
||||
const newProps = {
|
||||
...baseProps,
|
||||
initialNotification: undefined as DataModels.Notification,
|
||||
@@ -70,6 +73,7 @@ describe("ScaleComponent", () => {
|
||||
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true);
|
||||
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(false);
|
||||
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(maxThroughput);
|
||||
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(targetMaxThroughput);
|
||||
});
|
||||
|
||||
it("autoScale disabled", () => {
|
||||
@@ -105,6 +109,11 @@ describe("ScaleComponent", () => {
|
||||
expect(scaleComponent.isAutoScaleEnabled()).toEqual(true);
|
||||
});
|
||||
|
||||
it("getMaxRUThroughputInputLimit", () => {
|
||||
const scaleComponent = new ScaleComponent(baseProps);
|
||||
expect(scaleComponent.getMaxRUThroughputInputLimit()).toEqual(40000);
|
||||
});
|
||||
|
||||
it("getThroughputTitle", () => {
|
||||
let scaleComponent = new ScaleComponent(baseProps);
|
||||
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
|
||||
@@ -129,8 +138,14 @@ describe("ScaleComponent", () => {
|
||||
|
||||
it("getThroughputWarningMessage", () => {
|
||||
const throughputBeyondLimit = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million + 1000;
|
||||
const throughputBeyondMaxRus = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million - 1000;
|
||||
|
||||
const newProps = { ...baseProps, container: nonNationalCloudContainer, throughput: throughputBeyondLimit };
|
||||
const scaleComponent = new ScaleComponent(newProps);
|
||||
let scaleComponent = new ScaleComponent(newProps);
|
||||
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputBeyondLimitWarningMessage");
|
||||
|
||||
newProps.throughput = throughputBeyondMaxRus;
|
||||
scaleComponent = new ScaleComponent(newProps);
|
||||
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputDelayedApplyWarningMessage");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,11 +12,12 @@ import {
|
||||
throughputUnit,
|
||||
getThroughputApplyLongDelayMessage,
|
||||
getThroughputApplyShortDelayMessage,
|
||||
updateThroughputBeyondLimitWarningMessage
|
||||
updateThroughputBeyondLimitWarningMessage,
|
||||
updateThroughputDelayedApplyWarningMessage
|
||||
} from "../SettingsRenderUtils";
|
||||
import { hasDatabaseSharedThroughput } from "../SettingsUtils";
|
||||
import { getMaxRUs, getMinRUs, hasDatabaseSharedThroughput } from "../SettingsUtils";
|
||||
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
|
||||
import { Link, Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
|
||||
import { Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
|
||||
import { configContext, Platform } from "../../../../ConfigContext";
|
||||
|
||||
export interface ScaleComponentProps {
|
||||
@@ -61,7 +62,11 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
};
|
||||
|
||||
private getStorageCapacityTitle = (): JSX.Element => {
|
||||
const capacity: string = this.props.isFixedContainer ? "Fixed" : "Unlimited";
|
||||
// Mongo container with system partition key still treat as "Fixed"
|
||||
const isFixed =
|
||||
!this.props.collection.partitionKey ||
|
||||
(this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey);
|
||||
const capacity: string = isFixed ? "Fixed" : "Unlimited";
|
||||
return (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<Label>Storage capacity</Label>
|
||||
@@ -70,26 +75,12 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
);
|
||||
};
|
||||
|
||||
public getMaxRUs = (): number => {
|
||||
if (this.props.container?.isTryCosmosDBSubscription()) {
|
||||
return Constants.TryCosmosExperience.maxRU;
|
||||
public getMaxRUThroughputInputLimit = (): number => {
|
||||
if (configContext.platform === Platform.Hosted && this.props.collection.partitionKey) {
|
||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
return getMaxRUs(this.props.collection, this.props.container);
|
||||
};
|
||||
|
||||
public getThroughputTitle = (): string => {
|
||||
@@ -97,8 +88,11 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
return AutoPilotUtils.getAutoPilotHeaderText();
|
||||
}
|
||||
|
||||
const minThroughput: string = this.getMinRUs().toLocaleString();
|
||||
const maxThroughput: string = !this.props.isFixedContainer ? "unlimited" : this.getMaxRUs().toLocaleString();
|
||||
const minThroughput: string = getMinRUs(this.props.collection, this.props.container).toLocaleString();
|
||||
const maxThroughput: string =
|
||||
this.canThroughputExceedMaximumValue() && !this.props.isFixedContainer
|
||||
? "unlimited"
|
||||
: getMaxRUs(this.props.collection, this.props.container).toLocaleString();
|
||||
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`;
|
||||
};
|
||||
|
||||
@@ -115,15 +109,26 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
return this.getLongDelayMessage();
|
||||
}
|
||||
|
||||
const offer = this.props.collection?.offer();
|
||||
if (offer?.offerReplacePending) {
|
||||
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
|
||||
const offer = this.props.collection?.offer && this.props.collection.offer();
|
||||
if (
|
||||
offer &&
|
||||
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(
|
||||
this.props.isAutoPilotSelected,
|
||||
throughput,
|
||||
throughputUnit,
|
||||
this.props.collection.databaseId,
|
||||
this.props.collection.id()
|
||||
this.props.collection.id(),
|
||||
targetThroughput
|
||||
);
|
||||
}
|
||||
|
||||
@@ -133,12 +138,21 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
public getThroughputWarningMessage = (): JSX.Element => {
|
||||
const throughputExceedsBackendLimits: boolean =
|
||||
this.canThroughputExceedMaximumValue() &&
|
||||
getMaxRUs(this.props.collection, this.props.container) <=
|
||||
SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||
|
||||
if (throughputExceedsBackendLimits && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -165,20 +179,17 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
private getThroughputInputComponent = (): JSX.Element => (
|
||||
<ThroughputInputAutoPilotV3Component
|
||||
databaseAccount={this.props.container.databaseAccount()}
|
||||
databaseName={this.props.collection.databaseId}
|
||||
collectionName={this.props.collection.id()}
|
||||
serverId={this.props.container.serverId()}
|
||||
throughput={this.props.throughput}
|
||||
throughputBaseline={this.props.throughputBaseline}
|
||||
onThroughputChange={this.props.onThroughputChange}
|
||||
minimum={this.getMinRUs()}
|
||||
maximum={this.getMaxRUs()}
|
||||
minimum={getMinRUs(this.props.collection, this.props.container)}
|
||||
maximum={this.getMaxRUThroughputInputLimit()}
|
||||
isEnabled={!hasDatabaseSharedThroughput(this.props.collection)}
|
||||
canExceedMaximumValue={this.canThroughputExceedMaximumValue()}
|
||||
label={this.getThroughputTitle()}
|
||||
isEmulator={this.isEmulator}
|
||||
isFixed={this.props.isFixedContainer}
|
||||
isFreeTierAccount={this.isFreeTierAccount()}
|
||||
isAutoPilotSelected={this.props.isAutoPilotSelected}
|
||||
onAutoPilotSelected={this.props.onAutoPilotSelected}
|
||||
wasAutopilotOriginallySet={this.props.wasAutopilotOriginallySet}
|
||||
@@ -189,41 +200,12 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
onScaleSaveableChange={this.props.onScaleSaveableChange}
|
||||
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
|
||||
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 {
|
||||
return (
|
||||
<Stack {...subComponentStackProps}>
|
||||
{this.isFreeTierAccount() && (
|
||||
<MessageBar
|
||||
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
|
||||
styles={{ text: { fontSize: 14 } }}
|
||||
>
|
||||
{this.getFreeTierInfoMessage()}
|
||||
</MessageBar>
|
||||
)}
|
||||
{this.getInitialNotificationElement() && (
|
||||
<MessageBar messageBarType={MessageBarType.warning}>{this.getInitialNotificationElement()}</MessageBar>
|
||||
)}
|
||||
|
||||
@@ -13,7 +13,16 @@ import {
|
||||
} from "../SettingsUtils";
|
||||
import Explorer from "../../../Explorer";
|
||||
import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||
import { Label, Text, TextField, Stack, IChoiceGroupOption, ChoiceGroup, MessageBar } from "office-ui-fabric-react";
|
||||
import {
|
||||
Label,
|
||||
Text,
|
||||
TextField,
|
||||
Stack,
|
||||
IChoiceGroupOption,
|
||||
ChoiceGroup,
|
||||
MessageBar,
|
||||
MessageBarType
|
||||
} from "office-ui-fabric-react";
|
||||
import {
|
||||
getTextFieldStyles,
|
||||
changeFeedPolicyToolTip,
|
||||
@@ -181,10 +190,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
styles={getChoiceGroupStyles(this.props.timeToLive, this.props.timeToLiveBaseline)}
|
||||
/>
|
||||
{isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && (
|
||||
<MessageBar
|
||||
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
|
||||
styles={messageBarStyles}
|
||||
>
|
||||
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
|
||||
{ttlWarning}
|
||||
</MessageBar>
|
||||
)}
|
||||
|
||||
@@ -9,8 +9,6 @@ import * as DataModels from "../../../../../Contracts/DataModels";
|
||||
describe("ThroughputInputAutoPilotV3Component", () => {
|
||||
const baseProps: ThroughputInputAutoPilotV3Props = {
|
||||
databaseAccount: {} as DataModels.DatabaseAccount,
|
||||
databaseName: "test",
|
||||
collectionName: "test",
|
||||
serverId: undefined,
|
||||
wasAutopilotOriginallySet: false,
|
||||
throughput: 100,
|
||||
@@ -19,7 +17,6 @@ describe("ThroughputInputAutoPilotV3Component", () => {
|
||||
minimum: 10000,
|
||||
maximum: 400,
|
||||
step: 100,
|
||||
usageSizeInKB: 10000,
|
||||
isEnabled: true,
|
||||
isEmulator: false,
|
||||
spendAckChecked: false,
|
||||
@@ -28,7 +25,6 @@ describe("ThroughputInputAutoPilotV3Component", () => {
|
||||
spendAckVisible: false,
|
||||
showAsMandatory: true,
|
||||
isFixed: false,
|
||||
isFreeTierAccount: false,
|
||||
label: "label",
|
||||
infoBubbleText: "infoBubbleText",
|
||||
canExceedMaximumValue: true,
|
||||
@@ -57,6 +53,7 @@ describe("ThroughputInputAutoPilotV3Component", () => {
|
||||
expect(wrapper.exists("#throughputInput")).toEqual(true);
|
||||
expect(wrapper.exists("#autopilotInput")).toEqual(false);
|
||||
expect(wrapper.exists("#throughputSpendElement")).toEqual(true);
|
||||
expect(wrapper.exists("#autoscaleSpendElement")).toEqual(false);
|
||||
});
|
||||
|
||||
it("autopilot input visible", () => {
|
||||
@@ -74,7 +71,8 @@ describe("ThroughputInputAutoPilotV3Component", () => {
|
||||
|
||||
wrapper.setProps({ wasAutopilotOriginallySet: true });
|
||||
wrapper.update();
|
||||
expect(wrapper.exists("#throughputSpendElement")).toEqual(true);
|
||||
expect(wrapper.exists("#autoscaleSpendElement")).toEqual(true);
|
||||
expect(wrapper.exists("#throughputSpendElement")).toEqual(false);
|
||||
});
|
||||
|
||||
it("spendAck checkbox visible", () => {
|
||||
|
||||
@@ -8,15 +8,10 @@ import {
|
||||
checkBoxAndInputStackProps,
|
||||
getChoiceGroupStyles,
|
||||
messageBarStyles,
|
||||
getEstimatedSpendingElement,
|
||||
getEstimatedSpendElement,
|
||||
getEstimatedAutoscaleSpendElement,
|
||||
getAutoPilotV3SpendElement,
|
||||
manualToAutoscaleDisclaimerElement,
|
||||
saveThroughputWarningMessage,
|
||||
ManualEstimatedSpendingDisplayProps,
|
||||
AutoscaleEstimatedSpendingDisplayProps,
|
||||
PriceBreakdown,
|
||||
getRuPriceBreakdown,
|
||||
transparentDetailsHeaderStyle
|
||||
manualToAutoscaleDisclaimerElement
|
||||
} from "../../SettingsRenderUtils";
|
||||
import {
|
||||
Text,
|
||||
@@ -28,26 +23,16 @@ import {
|
||||
Label,
|
||||
Link,
|
||||
MessageBar,
|
||||
FontIcon,
|
||||
IColumn
|
||||
MessageBarType
|
||||
} from "office-ui-fabric-react";
|
||||
import { ToolTipLabelComponent } from "../ToolTipLabelComponent";
|
||||
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
|
||||
import * as SharedConstants from "../../../../../Shared/Constants";
|
||||
import * as DataModels from "../../../../../Contracts/DataModels";
|
||||
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 {
|
||||
databaseAccount: DataModels.DatabaseAccount;
|
||||
databaseName: string;
|
||||
collectionName: string;
|
||||
serverId: string;
|
||||
throughput: number;
|
||||
throughputBaseline: number;
|
||||
@@ -62,7 +47,6 @@ export interface ThroughputInputAutoPilotV3Props {
|
||||
spendAckVisible?: boolean;
|
||||
showAsMandatory?: boolean;
|
||||
isFixed: boolean;
|
||||
isFreeTierAccount: boolean;
|
||||
isEmulator: boolean;
|
||||
label: string;
|
||||
infoBubbleText?: string;
|
||||
@@ -76,12 +60,10 @@ export interface ThroughputInputAutoPilotV3Props {
|
||||
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
||||
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
||||
getThroughputWarningMessage: () => JSX.Element;
|
||||
usageSizeInKB: number;
|
||||
}
|
||||
|
||||
interface ThroughputInputAutoPilotV3State {
|
||||
spendAckChecked: boolean;
|
||||
exceedFreeTierThroughput: boolean;
|
||||
}
|
||||
|
||||
export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
@@ -155,9 +137,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
public constructor(props: ThroughputInputAutoPilotV3Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
spendAckChecked: this.props.spendAckChecked,
|
||||
exceedFreeTierThroughput:
|
||||
this.props.isFreeTierAccount && !this.props.isAutoPilotSelected && this.props.throughput > 400
|
||||
spendAckChecked: this.props.spendAckChecked
|
||||
};
|
||||
|
||||
this.step = this.props.step ?? ThroughputInputAutoPilotV3Component.defaultStep;
|
||||
@@ -180,243 +160,34 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const isDirty: boolean = this.IsComponentDirty().isDiscardable;
|
||||
const serverId: string = this.props.serverId;
|
||||
const offerThroughput: number = this.props.throughput;
|
||||
|
||||
const regions = account?.properties?.readLocations?.length || 1;
|
||||
const multimaster = account?.properties?.enableMultipleWriteLocations || false;
|
||||
|
||||
let estimatedSpend: JSX.Element;
|
||||
|
||||
if (!this.props.isAutoPilotSelected) {
|
||||
estimatedSpend = this.getEstimatedManualSpendElement(
|
||||
estimatedSpend = getEstimatedSpendElement(
|
||||
// if migrating from autoscale to manual, we use the autoscale RUs value as that is what will be set...
|
||||
this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : this.props.throughputBaseline,
|
||||
this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : offerThroughput,
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
isDirty ? this.props.throughput : undefined
|
||||
false
|
||||
);
|
||||
} else {
|
||||
estimatedSpend = this.getEstimatedAutoscaleSpendElement(
|
||||
this.props.maxAutoPilotThroughputBaseline,
|
||||
estimatedSpend = getEstimatedAutoscaleSpendElement(
|
||||
this.props.maxAutoPilotThroughput,
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
isDirty ? this.props.maxAutoPilotThroughput : undefined
|
||||
multimaster
|
||||
);
|
||||
}
|
||||
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 => {
|
||||
if (!this.props.maxAutoPilotThroughput) {
|
||||
return <></>;
|
||||
@@ -432,7 +203,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => {
|
||||
const newThroughput = getSanitizedInputValue(newValue);
|
||||
const newThroughput = getSanitizedInputValue(newValue, this.autoPilotInputMaxValue);
|
||||
this.props.onMaxAutoPilotThroughputChange(newThroughput);
|
||||
};
|
||||
|
||||
@@ -440,11 +211,10 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => {
|
||||
const newThroughput = getSanitizedInputValue(newValue);
|
||||
const newThroughput = getSanitizedInputValue(newValue, this.throughputInputMaxValue);
|
||||
if (this.overrideWithAutoPilotSettings()) {
|
||||
this.props.onMaxAutoPilotThroughputChange(newThroughput);
|
||||
} else {
|
||||
this.setState({ exceedFreeTierThroughput: this.props.isFreeTierAccount && newThroughput > 400 });
|
||||
this.props.onThroughputChange(newThroughput);
|
||||
}
|
||||
};
|
||||
@@ -452,42 +222,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
private onChoiceGroupChange = (
|
||||
event?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
): 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;
|
||||
};
|
||||
): void => this.props.onAutoPilotSelected(option.key === "true");
|
||||
|
||||
private renderThroughputModeChoices = (): JSX.Element => {
|
||||
const labelId = "settingsV2RadioButtonLabelId";
|
||||
@@ -500,10 +235,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
/>
|
||||
</Label>
|
||||
{this.overrideWithProvisionedThroughputSettings() && (
|
||||
<MessageBar
|
||||
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
|
||||
styles={messageBarStyles}
|
||||
>
|
||||
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
|
||||
{manualToAutoscaleDisclaimerElement}
|
||||
</MessageBar>
|
||||
)}
|
||||
@@ -543,7 +275,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
onChange={this.onAutoPilotThroughputChange}
|
||||
/>
|
||||
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
|
||||
{this.minRUperGBSurvey()}
|
||||
{this.props.spendAckVisible && (
|
||||
<Checkbox
|
||||
id="spendAckCheckBox"
|
||||
@@ -559,12 +290,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
|
||||
private renderThroughputInput = (): JSX.Element => (
|
||||
<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
|
||||
required
|
||||
type="number"
|
||||
@@ -580,26 +305,15 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
}
|
||||
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() && (
|
||||
<MessageBar
|
||||
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
|
||||
styles={messageBarStyles}
|
||||
>
|
||||
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
|
||||
{this.props.getThroughputWarningMessage()}
|
||||
</MessageBar>
|
||||
)}
|
||||
|
||||
{!this.props.isEmulator && this.getRequestUnitsUsageCost()}
|
||||
{this.minRUperGBSurvey()}
|
||||
|
||||
{this.props.spendAckVisible && (
|
||||
<Checkbox
|
||||
id="spendAckCheckBox"
|
||||
@@ -609,32 +323,14 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
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>}
|
||||
</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 {
|
||||
return (
|
||||
<Stack {...checkBoxAndInputStackProps}>
|
||||
{this.renderWarningMessage()}
|
||||
{this.renderThroughputModeChoices()}
|
||||
|
||||
{this.props.isAutoPilotSelected ? this.renderAutoPilotInput() : this.renderThroughputInput()}
|
||||
|
||||
@@ -8,26 +8,6 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledMessageBarBase
|
||||
messageBarIconProps={
|
||||
Object {
|
||||
"className": "messageBarWarningIcon",
|
||||
"iconName": "WarningSolid",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below before saving your changes
|
||||
</Text>
|
||||
</StyledMessageBarBase>
|
||||
<Stack>
|
||||
<StyledLabelBase
|
||||
id="settingsV2RadioButtonLabelId"
|
||||
@@ -39,7 +19,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 14,
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -50,21 +30,12 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
/>
|
||||
</StyledLabelBase>
|
||||
<StyledMessageBarBase
|
||||
messageBarIconProps={
|
||||
Object {
|
||||
"className": "messageBarInfoIcon",
|
||||
"iconName": "InfoSolid",
|
||||
}
|
||||
}
|
||||
messageBarType={5}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"backgroundColor": "white",
|
||||
"marginTop": "5px",
|
||||
},
|
||||
"text": Object {
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -73,7 +44,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 14,
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -185,7 +156,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 14,
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -243,19 +214,6 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text>
|
||||
Estimate your required throughput with
|
||||
<StyledLinkBase
|
||||
href="https://cosmos.azure.com/capacitycalculator/"
|
||||
target="_blank"
|
||||
>
|
||||
capacity calculator
|
||||
|
||||
<Component
|
||||
iconName="NavigateExternalInline"
|
||||
/>
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
<StyledTextFieldBase
|
||||
disabled={false}
|
||||
id="throughputInput"
|
||||
@@ -281,142 +239,38 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||
type="number"
|
||||
value="100"
|
||||
/>
|
||||
<Stack
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"width": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
<Text
|
||||
id="throughputSpendElement"
|
||||
>
|
||||
<StyledWithViewportComponent
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"fieldName": "costType",
|
||||
"isResizable": true,
|
||||
"key": "costType",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "",
|
||||
"styles": Object {
|
||||
"root": Object {
|
||||
"selectors": Object {
|
||||
":hover": Object {
|
||||
"background": "transparent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"fieldName": "hourly",
|
||||
"isResizable": true,
|
||||
"key": "hourly",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "Hourly",
|
||||
"styles": Object {
|
||||
"root": Object {
|
||||
"selectors": Object {
|
||||
":hover": Object {
|
||||
"background": "transparent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"fieldName": "daily",
|
||||
"isResizable": true,
|
||||
"key": "daily",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "Daily",
|
||||
"styles": Object {
|
||||
"root": Object {
|
||||
"selectors": Object {
|
||||
":hover": Object {
|
||||
"background": "transparent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"fieldName": "monthly",
|
||||
"isResizable": true,
|
||||
"key": "monthly",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "Monthly",
|
||||
"styles": Object {
|
||||
"root": Object {
|
||||
"selectors": Object {
|
||||
":hover": Object {
|
||||
"background": "transparent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
disableSelectionZone={true}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"costType": <Text>
|
||||
Current Cost
|
||||
</Text>,
|
||||
"daily": <Text>
|
||||
$
|
||||
|
||||
0.19
|
||||
</Text>,
|
||||
"hourly": <Text>
|
||||
$
|
||||
|
||||
0.0080
|
||||
</Text>,
|
||||
"monthly": <Text>
|
||||
$
|
||||
|
||||
5.84
|
||||
</Text>,
|
||||
},
|
||||
]
|
||||
}
|
||||
layoutMode={1}
|
||||
onRenderRow={[Function]}
|
||||
selectionMode={0}
|
||||
/>
|
||||
<Text
|
||||
id="throughputSpendElement"
|
||||
>
|
||||
(
|
||||
regions:
|
||||
|
||||
1
|
||||
,
|
||||
100
|
||||
RU/s,
|
||||
Estimated cost (
|
||||
USD
|
||||
):
|
||||
|
||||
<b>
|
||||
$
|
||||
0.00008
|
||||
/RU)
|
||||
</Text>
|
||||
<Text>
|
||||
<em>
|
||||
*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account
|
||||
</em>
|
||||
</Text>
|
||||
</Stack>
|
||||
0.0080
|
||||
hourly
|
||||
/
|
||||
$
|
||||
0.19
|
||||
daily
|
||||
/
|
||||
$
|
||||
5.84
|
||||
monthly
|
||||
|
||||
</b>
|
||||
(
|
||||
regions:
|
||||
|
||||
1
|
||||
,
|
||||
100
|
||||
RU/s,
|
||||
$
|
||||
0.00008
|
||||
/RU)
|
||||
</Text>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
id="spendAckCheckBox"
|
||||
@@ -434,7 +288,6 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||
}
|
||||
}
|
||||
/>
|
||||
<br />
|
||||
</Stack>
|
||||
</Stack>
|
||||
`;
|
||||
@@ -458,7 +311,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 14,
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -516,19 +369,6 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text>
|
||||
Estimate your required throughput with
|
||||
<StyledLinkBase
|
||||
href="https://cosmos.azure.com/capacitycalculator/"
|
||||
target="_blank"
|
||||
>
|
||||
capacity calculator
|
||||
|
||||
<Component
|
||||
iconName="NavigateExternalInline"
|
||||
/>
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
<StyledTextFieldBase
|
||||
disabled={false}
|
||||
id="throughputInput"
|
||||
@@ -554,143 +394,38 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
||||
type="number"
|
||||
value="100"
|
||||
/>
|
||||
<Stack
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"width": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
<Text
|
||||
id="throughputSpendElement"
|
||||
>
|
||||
<StyledWithViewportComponent
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"fieldName": "costType",
|
||||
"isResizable": true,
|
||||
"key": "costType",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "",
|
||||
"styles": Object {
|
||||
"root": Object {
|
||||
"selectors": Object {
|
||||
":hover": Object {
|
||||
"background": "transparent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"fieldName": "hourly",
|
||||
"isResizable": true,
|
||||
"key": "hourly",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "Hourly",
|
||||
"styles": Object {
|
||||
"root": Object {
|
||||
"selectors": Object {
|
||||
":hover": Object {
|
||||
"background": "transparent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"fieldName": "daily",
|
||||
"isResizable": true,
|
||||
"key": "daily",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "Daily",
|
||||
"styles": Object {
|
||||
"root": Object {
|
||||
"selectors": Object {
|
||||
":hover": Object {
|
||||
"background": "transparent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"fieldName": "monthly",
|
||||
"isResizable": true,
|
||||
"key": "monthly",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "Monthly",
|
||||
"styles": Object {
|
||||
"root": Object {
|
||||
"selectors": Object {
|
||||
":hover": Object {
|
||||
"background": "transparent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
disableSelectionZone={true}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"costType": <Text>
|
||||
Current Cost
|
||||
</Text>,
|
||||
"daily": <Text>
|
||||
$
|
||||
|
||||
0.19
|
||||
</Text>,
|
||||
"hourly": <Text>
|
||||
$
|
||||
|
||||
0.0080
|
||||
</Text>,
|
||||
"monthly": <Text>
|
||||
$
|
||||
|
||||
5.84
|
||||
</Text>,
|
||||
},
|
||||
]
|
||||
}
|
||||
layoutMode={1}
|
||||
onRenderRow={[Function]}
|
||||
selectionMode={0}
|
||||
/>
|
||||
<Text
|
||||
id="throughputSpendElement"
|
||||
>
|
||||
(
|
||||
regions:
|
||||
|
||||
1
|
||||
,
|
||||
100
|
||||
RU/s,
|
||||
Estimated cost (
|
||||
USD
|
||||
):
|
||||
|
||||
<b>
|
||||
$
|
||||
0.00008
|
||||
/RU)
|
||||
</Text>
|
||||
<Text>
|
||||
<em>
|
||||
*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account
|
||||
</em>
|
||||
</Text>
|
||||
</Stack>
|
||||
<br />
|
||||
0.0080
|
||||
hourly
|
||||
/
|
||||
$
|
||||
0.19
|
||||
daily
|
||||
/
|
||||
$
|
||||
5.84
|
||||
monthly
|
||||
|
||||
</b>
|
||||
(
|
||||
regions:
|
||||
|
||||
1
|
||||
,
|
||||
100
|
||||
RU/s,
|
||||
$
|
||||
0.00008
|
||||
/RU)
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||
exports[`ScaleComponent renders with correct intiial notification 1`] = `
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
@@ -16,7 +16,7 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 14,
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -40,8 +40,6 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||
>
|
||||
<ThroughputInputAutoPilotV3Component
|
||||
canExceedMaximumValue={true}
|
||||
collectionName="test"
|
||||
databaseName="test"
|
||||
getThroughputWarningMessage={[Function]}
|
||||
isAutoPilotSelected={false}
|
||||
isEmulator={false}
|
||||
@@ -50,7 +48,7 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||
label="Throughput (6,000 - unlimited RU/s)"
|
||||
maxAutoPilotThroughput={4000}
|
||||
maxAutoPilotThroughputBaseline={4000}
|
||||
maximum={1000000}
|
||||
maximum={40000}
|
||||
minimum={6000}
|
||||
onAutoPilotSelected={[Function]}
|
||||
onMaxAutoPilotThroughputChange={[Function]}
|
||||
@@ -60,7 +58,6 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||
spendAckChecked={false}
|
||||
throughput={1000}
|
||||
throughputBaseline={1000}
|
||||
usageSizeInKB={100}
|
||||
wasAutopilotOriginallySet={true}
|
||||
/>
|
||||
<Stack
|
||||
|
||||
@@ -136,7 +136,7 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 14,
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -412,7 +412,7 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 14,
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -952,7 +952,7 @@ exports[`SubSettingsComponent renders 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 14,
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1228,7 +1228,7 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 14,
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user