mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-05 18:47:41 +00:00
Compare commits
22 Commits
remove-unu
...
user/swvis
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35b15a1e20 | ||
|
|
17fd2185dc | ||
|
|
a93c8509cd | ||
|
|
5c93c11bd9 | ||
|
|
85d2378d3a | ||
|
|
84b6075ee8 | ||
|
|
d880723be9 | ||
|
|
4ce9dcc024 | ||
|
|
addcfedd5e | ||
|
|
a133134b8b | ||
|
|
79dec6a8a8 | ||
|
|
53a8cea95e | ||
|
|
5f1f7a8266 | ||
|
|
a009a8ba5f | ||
|
|
3e782527d0 | ||
|
|
e6ca1d25c9 | ||
|
|
473f722dcc | ||
|
|
5741802c25 | ||
|
|
e2e58f73b1 | ||
|
|
79769e9689 | ||
|
|
542abf4d3a | ||
|
|
4bee46ce55 |
11
.env.example
11
.env.example
@@ -1,7 +1,14 @@
|
|||||||
# These options are only needed when if running end to end tests locally
|
|
||||||
PORTAL_RUNNER_USERNAME=
|
PORTAL_RUNNER_USERNAME=
|
||||||
PORTAL_RUNNER_PASSWORD=
|
PORTAL_RUNNER_PASSWORD=
|
||||||
PORTAL_RUNNER_SUBSCRIPTION=
|
PORTAL_RUNNER_SUBSCRIPTION=
|
||||||
PORTAL_RUNNER_RESOURCE_GROUP=
|
PORTAL_RUNNER_RESOURCE_GROUP=
|
||||||
PORTAL_RUNNER_DATABASE_ACCOUNT=
|
PORTAL_RUNNER_DATABASE_ACCOUNT=
|
||||||
PORTAL_RUNNER_CONNECTION_STRING=
|
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=
|
||||||
|
DATA_EXPLORER_ENDPOINT=https://localhost:1234/hostedExplorer.html
|
||||||
@@ -15,8 +15,6 @@ src/Common/DeleteFeedback.ts
|
|||||||
src/Common/DocumentClientUtilityBase.ts
|
src/Common/DocumentClientUtilityBase.ts
|
||||||
src/Common/EditableUtility.ts
|
src/Common/EditableUtility.ts
|
||||||
src/Common/EnvironmentUtility.ts
|
src/Common/EnvironmentUtility.ts
|
||||||
src/Common/ErrorParserUtility.test.ts
|
|
||||||
src/Common/ErrorParserUtility.ts
|
|
||||||
src/Common/HashMap.test.ts
|
src/Common/HashMap.test.ts
|
||||||
src/Common/HashMap.ts
|
src/Common/HashMap.ts
|
||||||
src/Common/HeadersUtility.test.ts
|
src/Common/HeadersUtility.test.ts
|
||||||
@@ -204,8 +202,6 @@ src/Explorer/Tabs/QueryTab.test.ts
|
|||||||
src/Explorer/Tabs/QueryTab.ts
|
src/Explorer/Tabs/QueryTab.ts
|
||||||
src/Explorer/Tabs/QueryTablesTab.ts
|
src/Explorer/Tabs/QueryTablesTab.ts
|
||||||
src/Explorer/Tabs/ScriptTabBase.ts
|
src/Explorer/Tabs/ScriptTabBase.ts
|
||||||
src/Explorer/Tabs/SettingsTab.test.ts
|
|
||||||
src/Explorer/Tabs/SettingsTab.ts
|
|
||||||
src/Explorer/Tabs/SparkMasterTab.ts
|
src/Explorer/Tabs/SparkMasterTab.ts
|
||||||
src/Explorer/Tabs/StoredProcedureTab.ts
|
src/Explorer/Tabs/StoredProcedureTab.ts
|
||||||
src/Explorer/Tabs/TabComponents.ts
|
src/Explorer/Tabs/TabComponents.ts
|
||||||
@@ -292,8 +288,6 @@ src/Utils/DatabaseAccountUtils.ts
|
|||||||
src/Utils/JunoUtils.ts
|
src/Utils/JunoUtils.ts
|
||||||
src/Utils/MessageValidation.ts
|
src/Utils/MessageValidation.ts
|
||||||
src/Utils/NotebookConfigurationUtils.ts
|
src/Utils/NotebookConfigurationUtils.ts
|
||||||
src/Utils/OfferUtils.test.ts
|
|
||||||
src/Utils/OfferUtils.ts
|
|
||||||
src/Utils/PricingUtils.test.ts
|
src/Utils/PricingUtils.test.ts
|
||||||
src/Utils/QueryUtils.test.ts
|
src/Utils/QueryUtils.test.ts
|
||||||
src/Utils/QueryUtils.ts
|
src/Utils/QueryUtils.ts
|
||||||
@@ -398,19 +392,5 @@ src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx
|
|||||||
src/GalleryViewer/Cards/GalleryCardComponent.tsx
|
src/GalleryViewer/Cards/GalleryCardComponent.tsx
|
||||||
src/GalleryViewer/GalleryViewer.tsx
|
src/GalleryViewer/GalleryViewer.tsx
|
||||||
src/GalleryViewer/GalleryViewerComponent.tsx
|
src/GalleryViewer/GalleryViewerComponent.tsx
|
||||||
cypress/integration/dataexplorer/CASSANDRA/addCollection.spec.ts
|
|
||||||
cypress/integration/dataexplorer/GRAPH/addCollection.spec.ts
|
|
||||||
cypress/integration/dataexplorer/ci-tests/addCollectionPane.spec.ts
|
|
||||||
cypress/integration/dataexplorer/ci-tests/createDatabase.spec.ts
|
|
||||||
cypress/integration/dataexplorer/ci-tests/deleteCollection.spec.ts
|
|
||||||
cypress/integration/dataexplorer/ci-tests/deleteDatabase.spec.ts
|
|
||||||
cypress/integration/dataexplorer/MONGO/addCollection.spec.ts
|
|
||||||
cypress/integration/dataexplorer/MONGO/addCollectionAutopilot.spec.ts
|
|
||||||
cypress/integration/dataexplorer/MONGO/addCollectionExistingDatabase.spec.ts
|
|
||||||
cypress/integration/dataexplorer/MONGO/provisionDatabaseThroughput.spec.ts
|
|
||||||
cypress/integration/dataexplorer/SQL/addCollection.spec.ts
|
|
||||||
cypress/integration/dataexplorer/TABLE/addCollection.spec.ts
|
|
||||||
cypress/integration/notebook/newNotebook.spec.ts
|
|
||||||
cypress/integration/notebook/resourceTree.spec.ts
|
|
||||||
__mocks__/monaco-editor.ts
|
__mocks__/monaco-editor.ts
|
||||||
src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx
|
src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx
|
||||||
46
.github/workflows/ci.yml
vendored
46
.github/workflows/ci.yml
vendored
@@ -79,32 +79,31 @@ jobs:
|
|||||||
name: dist
|
name: dist
|
||||||
path: dist/
|
path: dist/
|
||||||
endtoendemulator:
|
endtoendemulator:
|
||||||
name: "End To End Tests | Emulator | SQL"
|
name: "End To End Emulator Tests"
|
||||||
needs: [lint, format, compile, unittest]
|
needs: [lint, format, compile, unittest]
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: southpolesteve/cosmos-emulator-github-action@v1
|
|
||||||
- name: Use Node.js 12.x
|
- name: Use Node.js 12.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: 12.x
|
||||||
- name: Restore Cypress Binary Cache
|
- uses: southpolesteve/cosmos-emulator-github-action@v1
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/.cache/Cypress
|
|
||||||
key: ${{ runner.os }}-cypress-binary-cache
|
|
||||||
- name: End to End Tests
|
- name: End to End Tests
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
npm start &
|
npm start &
|
||||||
npm ci --prefix ./cypress
|
npm run wait-for-server
|
||||||
npm run test:ci --prefix ./cypress -- --spec ./integration/dataexplorer/ci-tests/createDatabase.spec.ts
|
npx jest -c ./jest.config.e2e.js --detectOpenHandles sql
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
EMULATOR_ENDPOINT: https://0.0.0.0:8081/
|
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/explorer.html?platform=Emulator"
|
||||||
|
PLATFORM: "Emulator"
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: screenshots
|
||||||
|
path: failed-*
|
||||||
accessibility:
|
accessibility:
|
||||||
name: "Accessibility | Hosted"
|
name: "Accessibility | Hosted"
|
||||||
needs: [lint, format, compile, unittest]
|
needs: [lint, format, compile, unittest]
|
||||||
@@ -123,13 +122,13 @@ jobs:
|
|||||||
sudo sysctl -p
|
sudo sysctl -p
|
||||||
npm ci
|
npm ci
|
||||||
npm start &
|
npm start &
|
||||||
npx wait-on -i 5000 https-get://0.0.0.0:1234/
|
npx wait-on -i 5000 https-get://0.0.0.0:1234/
|
||||||
node utils/accesibilityCheck.js
|
node utils/accesibilityCheck.js
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
endtoendpuppeteer:
|
endtoendhosted:
|
||||||
name: "End to end puppeteer tests"
|
name: "End to End Hosted Tests"
|
||||||
needs: [lint, format, compile, unittest]
|
needs: [lint, format, compile, unittest]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -138,7 +137,7 @@ jobs:
|
|||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: 12.x
|
||||||
- name: End to End Puppeteer Tests
|
- name: End to End Hosted Tests
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
npm start &
|
npm start &
|
||||||
@@ -147,13 +146,26 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
|
PORTAL_RUNNER_SUBSCRIPTION: ${{ secrets.PORTAL_RUNNER_SUBSCRIPTION }}
|
||||||
|
PORTAL_RUNNER_RESOURCE_GROUP: ${{ secrets.PORTAL_RUNNER_RESOURCE_GROUP }}
|
||||||
|
PORTAL_RUNNER_DATABASE_ACCOUNT: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT }}
|
||||||
|
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY }}
|
||||||
|
NOTEBOOKS_TEST_RUNNER_TENANT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_TENANT_ID }}
|
||||||
|
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
|
||||||
|
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
|
||||||
PORTAL_RUNNER_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
|
PORTAL_RUNNER_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
|
||||||
MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
|
MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
|
||||||
CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }}
|
CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }}
|
||||||
|
TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }}
|
||||||
|
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/hostedExplorer.html"
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: screenshots
|
||||||
|
path: failed-*
|
||||||
nuget:
|
nuget:
|
||||||
name: Publish Nuget
|
name: Publish Nuget
|
||||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendpuppeteer]
|
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||||
@@ -177,7 +189,7 @@ jobs:
|
|||||||
nugetmpac:
|
nugetmpac:
|
||||||
name: Publish Nuget MPAC
|
name: Publish Nuget MPAC
|
||||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendpuppeteer]
|
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,9 +9,6 @@ pkg/DataExplorer/*
|
|||||||
test/out/*
|
test/out/*
|
||||||
workers/**/*.js
|
workers/**/*.js
|
||||||
*.trx
|
*.trx
|
||||||
cypress/videos
|
|
||||||
cypress/screenshots
|
|
||||||
cypress/fixtures
|
|
||||||
notebookapp/*
|
notebookapp/*
|
||||||
Contracts/*
|
Contracts/*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
25
README.md
25
README.md
@@ -33,7 +33,7 @@ To run pure hosted mode, in `webpack.config.js` change index HtmlWebpackPlugin t
|
|||||||
|
|
||||||
### Emulator Development
|
### Emulator Development
|
||||||
|
|
||||||
In a window environment, running `npm run build` will automatically copy the built files from `/dist` over to the default emulator install paths. In a non-windows enironment you can specify an alternate endpoint using `EMULATOR_ENDPOINT` and webpack dev server will proxy requests for you.
|
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`
|
`PLATFORM=Emulator EMULATOR_ENDPOINT=https://my-vm.azure.com:8081 npm run watch`
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ The Cosmos Portal that consumes this repo is not currently open source. If you h
|
|||||||
You can however load a local running instance of data explorer in the production portal.
|
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)
|
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. Whitelist `https://localhost:1234` domain for CORS in the Azure Cosmos DB portal
|
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`
|
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
|
4. Load the portal using the following link: https://ms.portal.azure.com/?dataExplorerSource=https%3A%2F%2Flocalhost%3A1234%2Fexplorer.html
|
||||||
|
|
||||||
@@ -76,24 +76,17 @@ Unit tests are located adjacent to the code under test and run with [Jest](https
|
|||||||
|
|
||||||
#### End to End CI Tests
|
#### End to End CI Tests
|
||||||
|
|
||||||
[Cypress](https://www.cypress.io/) is used for end to end tests and are contained in `cypress/`. Currently, it operates as sub project with its own typescript config and dependencies. It also only operates against the emulator. To run cypress tests:
|
Jest and Puppeteer are used for end to end browser based tests and are contained in `test/`. To run these tests locally:
|
||||||
|
|
||||||
1. Ensure the emulator is running
|
1. Copy .env.example to .env
|
||||||
2. Start cosmos explorer in emulator mode: `PLATFORM=Emulator npm run watch`
|
2. Update the values in .env including your local data explorer endpoint (ask a teammate/codeowner for help with .env values)
|
||||||
3. Move into `cypress/` folder: `cd cypress`
|
3. Make sure all packages are installed `npm install`
|
||||||
4. Install dependencies: `npm install`
|
4. Run the server `npm run start` and wait for it to start
|
||||||
5. Run cypress headless(`npm run test`) or in interactive mode(`npm run test:debug`)
|
5. Run `npm run test:e2e`
|
||||||
|
|
||||||
#### End to End Production Runners
|
|
||||||
|
|
||||||
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 and fill in all variables
|
|
||||||
2. Run `npm run test:e2e`
|
|
||||||
|
|
||||||
### Releasing
|
### Releasing
|
||||||
|
|
||||||
We generally adhear to the release strategy [documented by the Azure SDK Guidelines](https://azure.github.io/azure-sdk/policies_repobranching.html#release-branches). Most releases should happen from the master branch. If master contains commits that cannot be released, you may create a release from a `release/` or `hotfix/` branch. See linked documentation for more details.
|
We generally adhere to the release strategy [documented by the Azure SDK Guidelines](https://azure.github.io/azure-sdk/policies_repobranching.html#release-branches). Most releases should happen from the master branch. If master contains commits that cannot be released, you may create a release from a `release/` or `hotfix/` branch. See linked documentation for more details.
|
||||||
|
|
||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
|
|||||||
4
cypress/.gitignore
vendored
4
cypress/.gitignore
vendored
@@ -1,4 +0,0 @@
|
|||||||
cypress.env.json
|
|
||||||
cypress/report
|
|
||||||
cypress/screenshots
|
|
||||||
cypress/videos
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
// Cleans up old databases from previous test runs
|
|
||||||
const { CosmosClient } = require("@azure/cosmos");
|
|
||||||
|
|
||||||
// TODO: Add support for other API connection strings
|
|
||||||
const mongoRegex = RegExp("mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com");
|
|
||||||
|
|
||||||
async function cleanup() {
|
|
||||||
const connectionString = process.env.CYPRESS_CONNECTION_STRING;
|
|
||||||
if (!connectionString) {
|
|
||||||
throw new Error("Connection string not provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
let client;
|
|
||||||
switch (true) {
|
|
||||||
case connectionString.includes("mongodb://"): {
|
|
||||||
const [, key, accountName] = connectionString.match(mongoRegex);
|
|
||||||
client = new CosmosClient({
|
|
||||||
key,
|
|
||||||
endpoint: `https://${accountName}.documents.azure.com:443/`
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// TODO: Add support for other API connection strings
|
|
||||||
default:
|
|
||||||
client = new CosmosClient(connectionString);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await client.databases.readAll().fetchAll();
|
|
||||||
return Promise.all(
|
|
||||||
response.resources.map(async db => {
|
|
||||||
const dbTimestamp = new Date(db._ts * 1000);
|
|
||||||
const twentyMinutesAgo = new Date(Date.now() - 1000 * 60 * 20);
|
|
||||||
if (dbTimestamp < twentyMinutesAgo) {
|
|
||||||
await client.database(db.id).delete();
|
|
||||||
console.log(`DELETED: ${db.id} | Timestamp: ${dbTimestamp}`);
|
|
||||||
} else {
|
|
||||||
console.log(`SKIPPED: ${db.id} | Timestamp: ${dbTimestamp}`);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup()
|
|
||||||
.then(() => {
|
|
||||||
process.exit(0);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"integrationFolder": "./integration",
|
|
||||||
"pluginsFile": false,
|
|
||||||
"fixturesFolder": false,
|
|
||||||
"supportFile": "./support/index.js",
|
|
||||||
"defaultCommandTimeout": 90000,
|
|
||||||
"chromeWebSecurity": false,
|
|
||||||
"reporter": "mochawesome",
|
|
||||||
"reporterOptions": {
|
|
||||||
"reportDir": "cypress/report",
|
|
||||||
"json": true,
|
|
||||||
"overwrite": false,
|
|
||||||
"html": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
// 1. Click on "New Container" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Cassandra API Test - createDatabase", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString(connectionString.constants.cassandra);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new table in Cassandra API", () => {
|
|
||||||
const keyspaceId = `KeyspaceId${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const tableId = `TableId112`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Table"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[id="keyspace-id"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.type(keyspaceId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[class="textfontclr"]')
|
|
||||||
.type(tableId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('data-test="addCollection-createCollection"')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", tableId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
// 1. Click on "New Graph" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Graph API Test", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString(connectionString.constants.graph);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new graph in Graph API", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const graphId = `TestGraph${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const partitionKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Graph"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(graphId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(partitionKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId)
|
|
||||||
.click()
|
|
||||||
.should("contain", graphId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
// 1. Click on "New Container" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// // 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Mongo API Test - createDatabase", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new collection in Mongo API", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Collection"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(sharedKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find("#submitBtnAddCollection")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId)
|
|
||||||
.click()
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
// 1. Click on "New Container" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Mongo API Test", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
});
|
|
||||||
|
|
||||||
it.skip("Create a new collection in Mongo API - Autopilot", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Collection"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="throughputModeContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.and(input => {
|
|
||||||
expect(input.get(0).textContent, "first item").contains("Autopilot (preview)");
|
|
||||||
expect(input.get(1).textContent, "second item").contains("Manual");
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[id="newContainer-databaseThroughput-autoPilotRadio"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('select[name="autoPilotTiers"]')
|
|
||||||
// .eq(1).should('contain', '4,000 RU/s');
|
|
||||||
// // .select('4,000 RU/s').should('have.value', '1');
|
|
||||||
|
|
||||||
.find('option[value="2"]')
|
|
||||||
.then($element => $element.get(1).setAttribute("selected", "selected"));
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(sharedKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId)
|
|
||||||
.click()
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Mongo API Test", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
});
|
|
||||||
|
|
||||||
it.skip("Create a new collection in existing database in Mongo API", () => {
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('span[class="nodeLabel"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.then($span => {
|
|
||||||
const dbId1 = $span.text();
|
|
||||||
cy.log("DBBB", dbId1);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Collection"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-existingDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-existingDatabase"]')
|
|
||||||
.type(dbId1);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(sharedKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.click()
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context.skip("Mongo API Test", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new collection in Mongo API - Provision database throughput", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Collection"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find(".createNewDatabaseOrUseExisting")
|
|
||||||
.should("have.length", 2)
|
|
||||||
.and(input => {
|
|
||||||
expect(input.get(0).textContent, "first item").contains("Create new");
|
|
||||||
expect(input.get(1).textContent, "second item").contains("Use existing");
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(sharedKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId)
|
|
||||||
.click()
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new collection - without provision database throughput", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionIdTitle = `Add Collection`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Collection"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.uncheck();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[id="tab2"]')
|
|
||||||
.check({ force: true });
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(sharedKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId)
|
|
||||||
.click()
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new collection - without provision database throughput Fixed Storage Capacity", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Collection"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.uncheck();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[id="tab1"]')
|
|
||||||
.check({ force: true });
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId)
|
|
||||||
.click()
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
// 1. Click on "New Container" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("SQL API Test", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new container in SQL API", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Container"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(sharedKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find("#submitBtnAddCollection")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
// 1. Click on "New Container" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Table API Test", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString(connectionString.constants.table);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new table in Table API", () => {
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Table"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// 1. Click on "New Container" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Emulator - createDatabase", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("http://localhost:1234/explorer.html");
|
|
||||||
});
|
|
||||||
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionIdTitle = `Add Collection`;
|
|
||||||
const partitionKey = `PartitionKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
it("Create a new collection", () => {
|
|
||||||
cy.contains("New Container").click();
|
|
||||||
|
|
||||||
// cy.contains(collectionIdTitle);
|
|
||||||
|
|
||||||
cy.get(".createNewDatabaseOrUseExisting")
|
|
||||||
.should("have.length", 2)
|
|
||||||
.and(input => {
|
|
||||||
expect(input.get(0).textContent, "first item").contains("Create new");
|
|
||||||
expect(input.get(1).textContent, "second item").contains("Use existing");
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get('input[data-test="addCollection-createNewDatabase"]').check();
|
|
||||||
|
|
||||||
cy.get('input[data-test="addCollection-newDatabaseId"]').type(dbId);
|
|
||||||
|
|
||||||
cy.get('input[data-test="addCollection-collectionId"]').type(collectionId);
|
|
||||||
|
|
||||||
cy.get('input[data-test="databaseThroughputValue"]').should("have.value", "400");
|
|
||||||
|
|
||||||
cy.get('input[data-test="addCollection-partitionKeyValue"]').type(partitionKey);
|
|
||||||
|
|
||||||
cy.get('input[data-test="addCollection-createCollection"]').click();
|
|
||||||
|
|
||||||
cy.get('div[data-test="resourceTreeId"]').should("exist");
|
|
||||||
|
|
||||||
cy.get('div[data-test="resourceTree-collectionsTree"]').should("contain", dbId);
|
|
||||||
|
|
||||||
cy.get('div[data-test="databaseList"]').should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
// 1. Click on "New Database" on the command bar
|
|
||||||
// 2. a Pane with the title "Add Database" should appear on the right side of the screen
|
|
||||||
// i. It includes an input box for the database Id.
|
|
||||||
// ii. It includes a checkbox called "Provision throughput".
|
|
||||||
// iii. Whe the checkbox is marked, a new input with a throughput control let's you customize RU at the database level
|
|
||||||
// 3. Create a database WITHOUT "Provision throughput" checked.
|
|
||||||
// 4. It should appear in the Data Explorer list.
|
|
||||||
// 5. Repeat steps 1-3 but create a database WITH "Provision throughput" with the default RU value.
|
|
||||||
// 6. It should appear in the Data Explorer list.
|
|
||||||
// 7. If expanded, it should have the list item called "Scale", that once clicked, it should show the "Scale" tab.
|
|
||||||
// 8. Inside that tab, a throughput control will let you change the RU value within the permited range.
|
|
||||||
// 9. If you change the value, it should enable the "Save" button.
|
|
||||||
// 10. Click "Save" and verify that the process completes without error.
|
|
||||||
// 11. Close the tab and reopen it and verify that the input contains the last saved value.%
|
|
||||||
|
|
||||||
const crypto = require("crypto");
|
|
||||||
const client = require("../../../utilities/cosmosClient");
|
|
||||||
const randomString = crypto.randomBytes(2).toString("hex");
|
|
||||||
const databaseId = `TestDB-${randomString}`;
|
|
||||||
const collectionId = `TestColl-${randomString}`;
|
|
||||||
|
|
||||||
context("Emulator - Create database -> container -> item", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
const { resources } = await client.databases.readAll().fetchAll();
|
|
||||||
for (const database of resources) {
|
|
||||||
await client.database(database.id).delete();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("creates a new database", () => {
|
|
||||||
cy.visit("https://0.0.0.0:1234/explorer.html?platform=Emulator");
|
|
||||||
cy.contains("New Container").click();
|
|
||||||
cy.get("[data-test=addCollection-newDatabaseId]").click();
|
|
||||||
cy.get("[data-test=addCollection-newDatabaseId]").type(databaseId);
|
|
||||||
cy.get("[data-test=addCollection-collectionId]").click();
|
|
||||||
cy.get("[data-test=addCollection-collectionId]").type(collectionId);
|
|
||||||
cy.get("[data-test=addCollection-partitionKeyValue]").click();
|
|
||||||
cy.get("[data-test=addCollection-partitionKeyValue]").type("/pk");
|
|
||||||
cy.get('input[name="createCollection"]').click();
|
|
||||||
cy.get(".dataResourceTree").should("contain", databaseId);
|
|
||||||
cy.get(".dataResourceTree")
|
|
||||||
.contains(databaseId)
|
|
||||||
.click();
|
|
||||||
cy.get(".dataResourceTree").should("contain", collectionId);
|
|
||||||
cy.get(".dataResourceTree")
|
|
||||||
.contains(collectionId)
|
|
||||||
.click();
|
|
||||||
cy.get(".dataResourceTree")
|
|
||||||
.contains("Items")
|
|
||||||
.click();
|
|
||||||
cy.get(".dataResourceTree")
|
|
||||||
.contains("Items")
|
|
||||||
.click();
|
|
||||||
cy.wait(1000); // React rendering inside KO causes some weird async rendering that makes this test flaky without waiting
|
|
||||||
cy.get(".commandBarContainer")
|
|
||||||
.contains("New Item")
|
|
||||||
.click();
|
|
||||||
cy.wait(1000); // React rendering inside KO causes some weird async rendering that makes this test flaky without waiting
|
|
||||||
cy.get(".commandBarContainer")
|
|
||||||
.contains("Save")
|
|
||||||
.click();
|
|
||||||
cy.wait(1000); // React rendering inside KO causes some weird async rendering that makes this test flaky without waiting
|
|
||||||
cy.get(".documentsGridHeaderContainer").should("contain", "replace_with_new_document_id");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
// 1. Click last database in the resource tree
|
|
||||||
// 2. Click the last collection within the database
|
|
||||||
// 3. Select the context menu within the collection
|
|
||||||
// 4. Select "Delete Container" option in the dropdown
|
|
||||||
// 5. On Selection, Delete Container pane opens on the right side
|
|
||||||
// 6. Enter the same collection id that is to be deleted and click ok
|
|
||||||
// 7. Now, the resource tree refreshes, the deleted collection should not appear under the database
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Emulator - deleteCollection", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("http://localhost:1234/explorer.html");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Delete a collection", () => {
|
|
||||||
cy.get(".databaseId")
|
|
||||||
.last()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get(".collectionList")
|
|
||||||
.last()
|
|
||||||
.then($id => {
|
|
||||||
const collectionId = $id.text();
|
|
||||||
|
|
||||||
cy.get('span[data-test="collectionEllipsisMenu"]').should("exist");
|
|
||||||
|
|
||||||
cy.get('span[data-test="collectionEllipsisMenu"]')
|
|
||||||
.invoke("show")
|
|
||||||
.last()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get('div[data-test="collectionContextMenu"]')
|
|
||||||
.contains("Delete Container")
|
|
||||||
.click({ force: true });
|
|
||||||
|
|
||||||
cy.get('input[data-test="confirmCollectionId"]').type(collectionId.trim());
|
|
||||||
|
|
||||||
cy.get('input[data-test="deleteCollection"]').click();
|
|
||||||
|
|
||||||
cy.get('div[data-test="databaseList"]').should("not.contain", collectionId);
|
|
||||||
|
|
||||||
cy.get('div[data-test="databaseMenu"]').should("not.contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
// 1. Click last database in the resource tree
|
|
||||||
// 2. Select the context menu within the database
|
|
||||||
// 4. Select "Delete Database" option in the dropdown
|
|
||||||
// 5. On Selection, Delete Database pane opens on the right side
|
|
||||||
// 6. Enter the same database id that is to be deleted and click ok
|
|
||||||
// 7. Now, the resource tree refreshes, the deleted database should not appear in the resource tree
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Emulator - deleteDatabase", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
let db_rid = "";
|
|
||||||
const date = new Date().toUTCString();
|
|
||||||
let authToken = "";
|
|
||||||
cy.visit("http://localhost:1234/explorer.html");
|
|
||||||
|
|
||||||
// Creating auth token for collection creation
|
|
||||||
cy.request({
|
|
||||||
method: "GET",
|
|
||||||
url: "https://localhost:8081/_explorer/authorization/post/dbs/",
|
|
||||||
headers: {
|
|
||||||
"x-ms-date": date,
|
|
||||||
authorization: "-"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
authToken = response.body.Token; // Getting auth token for collection creation
|
|
||||||
return new Cypress.Promise((resolve, reject) => {
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
cy.request({
|
|
||||||
method: "POST",
|
|
||||||
url: "https://localhost:8081/dbs",
|
|
||||||
headers: {
|
|
||||||
"x-ms-date": date,
|
|
||||||
authorization: authToken,
|
|
||||||
"x-ms-version": "2018-12-31"
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
id: dbId
|
|
||||||
}
|
|
||||||
}).then(response => {
|
|
||||||
cy.log("Response", response);
|
|
||||||
db_rid = response.body._rid;
|
|
||||||
return new Cypress.Promise((resolve, reject) => {
|
|
||||||
cy.log("Rid", db_rid);
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Delete a database", () => {
|
|
||||||
cy.get('span[data-test="refreshTree"]').click();
|
|
||||||
|
|
||||||
cy.get(".databaseId")
|
|
||||||
.last()
|
|
||||||
.then($id => {
|
|
||||||
const dbId = $id.text();
|
|
||||||
|
|
||||||
cy.get('span[data-test="databaseEllipsisMenu"]').should("exist");
|
|
||||||
|
|
||||||
cy.get('span[data-test="databaseEllipsisMenu"]')
|
|
||||||
.invoke("show")
|
|
||||||
.last()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get('div[data-test="databaseContextMenu"]')
|
|
||||||
.contains("Delete Database")
|
|
||||||
.click({ force: true });
|
|
||||||
|
|
||||||
cy.get('input[data-test="confirmDatabaseId"]').type(dbId.trim());
|
|
||||||
|
|
||||||
cy.get('input[data-test="deleteDatabase"]').click();
|
|
||||||
|
|
||||||
cy.get('div[data-test="databaseList"]').should("not.contain", dbId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# Notebook end-to-end tests
|
|
||||||
This describes how to run the tests locally
|
|
||||||
|
|
||||||
## Stand up a local notebook container instance:
|
|
||||||
Instructions on how to build and run the container [here](https://microsoft.sharepoint.com/teams/DocDB/_layouts/OneNote.aspx?id=%2Fteams%2FDocDB%2FSiteAssets%2FDocDB%20Team%20Notebook&wd=target%28Tools%20_%20SDK%2FPortal%2FDevelopment.one%7CF800BE8E-1E31-48FE-90D7-EF698EF88112%2FHow%20to%20build%20notebook%20service%7C4BAA153B-422C-41E2-B997-F3FCE02CD743%2F%29)
|
|
||||||
|
|
||||||
## Run a local data explorer
|
|
||||||
Instructions are in [`DataExplorer/README.md`](https://msdata.visualstudio.com/CosmosDB/_git/cosmosdb-dataexplorer?path=%2FProduct%2FPortal%2FDataExplorer%2FREADME.md&_a=preview).
|
|
||||||
|
|
||||||
Make sure you can run Data Explorer locally from the web browser.
|
|
||||||
|
|
||||||
## Run cypress tests
|
|
||||||
1. Edit the URL for your DataExplorer in the `.spec.ts` file
|
|
||||||
2. Run the test:
|
|
||||||
```bash
|
|
||||||
cd DataExplorer/cypress
|
|
||||||
npm i
|
|
||||||
npm t -- --spec 'integration/notebook/newNotebook.spec.ts'
|
|
||||||
```
|
|
||||||
|
|
||||||
To run in Debug mode:
|
|
||||||
```
|
|
||||||
npm run test:debug
|
|
||||||
```
|
|
||||||
This opens Cypress UI
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
* The tests are recorded in the `videos` folder.
|
|
||||||
* Cypress does not support hover: workarounds [here](https://docs.cypress.io/api/commands/hover.html#Workarounds).
|
|
||||||
|
|
||||||
|
|
||||||
## References
|
|
||||||
* [Cypress API](https://docs.cypress.io/api/api/table-of-contents.html)
|
|
||||||
* [Cypress cookbook](https://docs.cypress.io/faq/questions/using-cypress-faq.html#How-do-I-get-an-element%E2%80%99s-text-contents)
|
|
||||||
* [Cypress best practices](https://docs.cypress.io/guides/references/best-practices.html#Selecting-Elements)
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
// THIS ADDS A NEW NOTEBOOK TO YOUR NOTEBOOKS
|
|
||||||
context("New Notebook smoke test", () => {
|
|
||||||
const timeout = 15000; // in ms
|
|
||||||
const explorerUrl =
|
|
||||||
"https://localhost:1234/explorer.html?feature.notebookserverurl=https%3A%2F%2Flocalhost%3A10001%2F12345%2Fnotebook&feature.notebookServerToken=token&feature.enablenotebooks=true";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for UI to be ready
|
|
||||||
*/
|
|
||||||
const waitForReady = () => {
|
|
||||||
cy.get(".splashScreenContainer", { timeout }).should("be.visible");
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit(explorerUrl);
|
|
||||||
waitForReady();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new notebook and run some code", () => {
|
|
||||||
// Create new notebook
|
|
||||||
cy.contains("New Notebook").click();
|
|
||||||
|
|
||||||
// Check tab name
|
|
||||||
cy.get("li.tabList .tabNavText").should($span => {
|
|
||||||
const text = $span.text();
|
|
||||||
expect(text).to.match(/^Untitled.*\.ipynb$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for python3 | idle status
|
|
||||||
cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should($p => {
|
|
||||||
const text = $p.text();
|
|
||||||
expect(text).to.match(/^python3.*idle$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Click on a cell
|
|
||||||
cy.get(".cell-container")
|
|
||||||
.as("cellContainer")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Type in some code
|
|
||||||
cy.get("@cellContainer").type("2+4");
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
cy.get('[data-test="Run"]')
|
|
||||||
.first()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Verify results
|
|
||||||
cy.get("@cellContainer").within(() => {
|
|
||||||
cy.get("pre code span").should("contain", "6");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Restart kernel
|
|
||||||
cy.get('[data-test="Run"] button')
|
|
||||||
.eq(-1)
|
|
||||||
.click();
|
|
||||||
cy.get("li")
|
|
||||||
.contains("Restart Kernel")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Wait for python3 | restarting status
|
|
||||||
cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should($p => {
|
|
||||||
const text = $p.text();
|
|
||||||
expect(text).to.match(/^python3.*restarting$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for python3 | idle status
|
|
||||||
cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should($p => {
|
|
||||||
const text = $p.text();
|
|
||||||
expect(text).to.match(/^python3.*idle$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Click on a cell
|
|
||||||
cy.get(".cell-container")
|
|
||||||
.as("cellContainer")
|
|
||||||
.find(".input")
|
|
||||||
.as("codeInput")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Type in some code
|
|
||||||
cy.get("@codeInput").type("{backspace}{backspace}{backspace}4+5");
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
cy.get('[data-test="Run"]')
|
|
||||||
.first()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Verify results
|
|
||||||
cy.get("@cellContainer").within(() => {
|
|
||||||
cy.get("pre code span").should("contain", "9");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
context("Resource tree notebook file manipulation", () => {
|
|
||||||
const timeout = 15000; // in ms
|
|
||||||
const explorerUrl =
|
|
||||||
"https://localhost:1234/explorer.html?feature.notebookserverurl=https%3A%2F%2Flocalhost%3A10001%2F12345%2Fnotebook&feature.notebookServerToken=token&feature.enablenotebooks=true";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for UI to be ready
|
|
||||||
*/
|
|
||||||
const waitForReady = () => {
|
|
||||||
cy.get(".splashScreenContainer", { timeout }).should("be.visible");
|
|
||||||
};
|
|
||||||
|
|
||||||
const clickContextMenuAndSelectOption = (nodeLabel, option) => {
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${nodeLabel}"]`)
|
|
||||||
.find("button.treeMenuEllipsis")
|
|
||||||
.click();
|
|
||||||
cy.get('[data-test="treeComponentMenuItemContainer"]')
|
|
||||||
.contains(option)
|
|
||||||
.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
const createFolder = folder => {
|
|
||||||
clickContextMenuAndSelectOption("My Notebooks/", "New Directory");
|
|
||||||
|
|
||||||
cy.get("#stringInputPane").within(() => {
|
|
||||||
cy.get('input[name="collectionIdConfirmation"]').type(folder);
|
|
||||||
cy.get("form").submit();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteItem = nodeName => {
|
|
||||||
clickContextMenuAndSelectOption(`${nodeName}`, "Delete");
|
|
||||||
cy.get(".ms-Dialog-main")
|
|
||||||
.contains("Delete")
|
|
||||||
.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit(explorerUrl);
|
|
||||||
waitForReady();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create and remove a directory", () => {
|
|
||||||
const folder = "e2etest_folder1";
|
|
||||||
createFolder(folder);
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).should("exist");
|
|
||||||
deleteItem(`${folder}/`);
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).should("not.exist");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create and rename a directory", () => {
|
|
||||||
const folder = "e2etest_folder2";
|
|
||||||
const renamedFolder = "e2etest_folder2_renamed";
|
|
||||||
createFolder(folder);
|
|
||||||
|
|
||||||
// Rename
|
|
||||||
clickContextMenuAndSelectOption(`${folder}/`, "Rename");
|
|
||||||
cy.get("#stringInputPane").within(() => {
|
|
||||||
cy.get('input[name="collectionIdConfirmation"]')
|
|
||||||
.clear()
|
|
||||||
.type(renamedFolder);
|
|
||||||
cy.get("form").submit();
|
|
||||||
});
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${renamedFolder}/"]`).should("exist");
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).should("not.exist");
|
|
||||||
|
|
||||||
deleteItem(`${renamedFolder}/`);
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${renamedFolder}/"]`).should("not.exist");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a notebook inside a directory", () => {
|
|
||||||
const folder = "e2etest_folder3";
|
|
||||||
const newNotebookName = "Untitled.ipynb";
|
|
||||||
createFolder(folder);
|
|
||||||
clickContextMenuAndSelectOption(`${folder}/`, "New Notebook");
|
|
||||||
|
|
||||||
// Verify tab is open
|
|
||||||
cy.get(".tabList")
|
|
||||||
.contains(newNotebookName)
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
// Close tab
|
|
||||||
cy.get(`.tabList[title="notebooks/${folder}/${newNotebookName}"]`)
|
|
||||||
.find(".cancelButton")
|
|
||||||
.click();
|
|
||||||
// When running from command line, closing the tab is too fast
|
|
||||||
cy.get("body").then($body => {
|
|
||||||
if ($body.find(".ms-Dialog-main").length) {
|
|
||||||
// For some reason, this does not work
|
|
||||||
// cy.get(".ms-Dialog-main").contains("Close").click();
|
|
||||||
cy.get(".ms-Dialog-main .ms-Button--primary").click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Expand folder node
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).click();
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("exist");
|
|
||||||
|
|
||||||
// Delete notebook
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`)
|
|
||||||
.find("button.treeMenuEllipsis")
|
|
||||||
.click();
|
|
||||||
cy.get('[data-test="treeComponentMenuItemContainer"]')
|
|
||||||
.contains("Delete")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Confirm
|
|
||||||
cy.get(".ms-Dialog-main")
|
|
||||||
.contains("Delete")
|
|
||||||
.click();
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("not.exist");
|
|
||||||
|
|
||||||
deleteItem(`${folder}/`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create and rename a notebook inside a directory", () => {
|
|
||||||
const folder = "e2etest_folder4";
|
|
||||||
const newNotebookName = "Untitled.ipynb";
|
|
||||||
const renamedNotebookName = "mynotebook.ipynb";
|
|
||||||
createFolder(folder);
|
|
||||||
clickContextMenuAndSelectOption(`${folder}/`, "New Notebook");
|
|
||||||
|
|
||||||
// Close tab
|
|
||||||
cy.get(`.tabList[title="notebooks/${folder}/${newNotebookName}"]`)
|
|
||||||
.find(".cancelButton")
|
|
||||||
.click();
|
|
||||||
cy.get("body").then($body => {
|
|
||||||
if ($body.find(".ms-Dialog-main").length) {
|
|
||||||
// For some reason, this does not work
|
|
||||||
// cy.get(".ms-Dialog-main").contains("Close").click();
|
|
||||||
cy.get(".ms-Dialog-main .ms-Button--primary").click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Expand folder node
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).click();
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("exist");
|
|
||||||
|
|
||||||
// Rename notebook
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`)
|
|
||||||
.find("button.treeMenuEllipsis")
|
|
||||||
.click();
|
|
||||||
cy.get('[data-test="treeComponentMenuItemContainer"]')
|
|
||||||
.contains("Rename")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get("#stringInputPane").within(() => {
|
|
||||||
cy.get('input[name="collectionIdConfirmation"]')
|
|
||||||
.clear()
|
|
||||||
.type(renamedNotebookName);
|
|
||||||
cy.get("form").submit();
|
|
||||||
});
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("not.exist");
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${renamedNotebookName}"]`).should("exist");
|
|
||||||
|
|
||||||
// Delete notebook
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${renamedNotebookName}"]`)
|
|
||||||
.find("button.treeMenuEllipsis")
|
|
||||||
.click();
|
|
||||||
cy.get('[data-test="treeComponentMenuItemContainer"]')
|
|
||||||
.contains("Delete")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Confirm
|
|
||||||
cy.get(".ms-Dialog-main")
|
|
||||||
.contains("Delete")
|
|
||||||
.click();
|
|
||||||
// Give it time to settle
|
|
||||||
cy.wait(1000);
|
|
||||||
deleteItem(`${folder}/`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
3066
cypress/package-lock.json
generated
3066
cypress/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "cosmos-explorer-cypress",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "cypress run",
|
|
||||||
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
|
|
||||||
"test:sql": "cypress run --browser chrome --spec \"./integration/dataexplorer/SQL/*\"",
|
|
||||||
"test:ci": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/ https-get://0.0.0.0:8081/_explorer/index.html && cypress run --browser edge --headless",
|
|
||||||
"test:debug": "cypress open"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"cypress": "^4.8.0",
|
|
||||||
"mocha": "^7.0.1",
|
|
||||||
"mochawesome": "^4.1.0",
|
|
||||||
"mochawesome-merge": "^4.0.1",
|
|
||||||
"mochawesome-report-generator": "^4.1.0",
|
|
||||||
"typescript": "3.4.3",
|
|
||||||
"wait-on": "^4.0.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@microsoft/applicationinsights-web": "^2.5.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
let appInsightsLib = require("@microsoft/applicationinsights-web");
|
|
||||||
|
|
||||||
const appInsights = new appInsightsLib.ApplicationInsights({
|
|
||||||
config: {
|
|
||||||
instrumentationKey: "fe61c39f-7d32-4488-a191-b13621965315"
|
|
||||||
/* ...Other Configuration Options... */
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
appInsights.loadAppInsights();
|
|
||||||
|
|
||||||
Cypress.on("fail", (error, runnable) => {
|
|
||||||
// App Insights will record the fail tests for Create Collection
|
|
||||||
let message = JSON.stringify(runnable.title);
|
|
||||||
appInsights.trackTrace({
|
|
||||||
message: `${message}`,
|
|
||||||
properties: {
|
|
||||||
passed: false,
|
|
||||||
error: error
|
|
||||||
}
|
|
||||||
});
|
|
||||||
throw error; // throw error to have test still fail
|
|
||||||
});
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"strict": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"module": "commonjs",
|
|
||||||
"target": "es5",
|
|
||||||
"lib": ["es5", "dom", "es6"],
|
|
||||||
"types": ["cypress", "node"]
|
|
||||||
},
|
|
||||||
"include": ["**/*.ts", "**/*.spec.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
loginUsingConnectionString: function() {
|
|
||||||
const prodUrl = Cypress.env("TEST_ENDPOINT") || "https://localhost:1234/hostedExplorer.html";
|
|
||||||
const timeout = 15000;
|
|
||||||
|
|
||||||
cy.visit(prodUrl);
|
|
||||||
cy.get('iframe[id="explorerMenu"]').should("be.visible");
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find("#connectExplorer")
|
|
||||||
.should("exist")
|
|
||||||
.find("div[class='connectExplorer']")
|
|
||||||
.should("exist")
|
|
||||||
.find("p[class='welcomeText']")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
cy.wrap($body.find("div > p.switchConnectTypeText"))
|
|
||||||
.should("exist")
|
|
||||||
.last()
|
|
||||||
.click({ force: true });
|
|
||||||
|
|
||||||
const secret = Cypress.env("CONNECTION_STRING");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find("input[class='inputToken']")
|
|
||||||
.should("exist")
|
|
||||||
.type(secret, {
|
|
||||||
force: true
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.wrap($body.find("input[value='Connect']"), { timeout })
|
|
||||||
.first()
|
|
||||||
.click({ force: true });
|
|
||||||
|
|
||||||
cy.wait(15000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
const { CosmosClient } = require("@azure/cosmos");
|
|
||||||
|
|
||||||
module.exports = new CosmosClient({
|
|
||||||
endpoint: "https://0.0.0.0:8081",
|
|
||||||
key: "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
|
|
||||||
});
|
|
||||||
4608
package-lock.json
generated
4608
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -4,15 +4,17 @@
|
|||||||
"description": "Cosmos Explorer",
|
"description": "Cosmos Explorer",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@azure/arm-cosmosdb": "9.1.0",
|
||||||
"@azure/cosmos": "3.9.0",
|
"@azure/cosmos": "3.9.0",
|
||||||
"@azure/cosmos-language-service": "0.0.4",
|
"@azure/cosmos-language-service": "0.0.5",
|
||||||
|
"@azure/identity": "1.1.0",
|
||||||
"@jupyterlab/services": "6.0.0-rc.2",
|
"@jupyterlab/services": "6.0.0-rc.2",
|
||||||
"@jupyterlab/terminal": "3.0.0-rc.2",
|
"@jupyterlab/terminal": "3.0.0-rc.2",
|
||||||
"@microsoft/applicationinsights-web": "2.5.9",
|
"@microsoft/applicationinsights-web": "2.5.9",
|
||||||
"@nteract/commutable": "7.3.2",
|
"@nteract/commutable": "7.3.2",
|
||||||
"@nteract/connected-components": "6.8.2",
|
"@nteract/connected-components": "6.8.2",
|
||||||
"@nteract/core": "15.1.0",
|
"@nteract/core": "15.1.0",
|
||||||
"@nteract/data-explorer": "8.0.3",
|
"@nteract/data-explorer": "8.2.9",
|
||||||
"@nteract/directory-listing": "2.0.6",
|
"@nteract/directory-listing": "2.0.6",
|
||||||
"@nteract/dropdown-menu": "1.0.1",
|
"@nteract/dropdown-menu": "1.0.1",
|
||||||
"@nteract/editor": "10.1.2",
|
"@nteract/editor": "10.1.2",
|
||||||
@@ -21,7 +23,7 @@
|
|||||||
"@nteract/jupyter-widgets": "2.0.0",
|
"@nteract/jupyter-widgets": "2.0.0",
|
||||||
"@nteract/logos": "1.0.0",
|
"@nteract/logos": "1.0.0",
|
||||||
"@nteract/markdown": "4.4.0",
|
"@nteract/markdown": "4.4.0",
|
||||||
"@nteract/monaco-editor": "3.2.0",
|
"@nteract/monaco-editor": "3.2.2",
|
||||||
"@nteract/octicons": "2.0.0",
|
"@nteract/octicons": "2.0.0",
|
||||||
"@nteract/outputs": "3.0.9",
|
"@nteract/outputs": "3.0.9",
|
||||||
"@nteract/presentational-components": "3.0.7",
|
"@nteract/presentational-components": "3.0.7",
|
||||||
@@ -66,7 +68,7 @@
|
|||||||
"jquery-ui-dist": "1.12.1",
|
"jquery-ui-dist": "1.12.1",
|
||||||
"knockout": "3.5.1",
|
"knockout": "3.5.1",
|
||||||
"mkdirp": "1.0.4",
|
"mkdirp": "1.0.4",
|
||||||
"monaco-editor": "0.15.6",
|
"monaco-editor": "0.18.1",
|
||||||
"object.entries": "1.1.0",
|
"object.entries": "1.1.0",
|
||||||
"office-ui-fabric-react": "7.134.1",
|
"office-ui-fabric-react": "7.134.1",
|
||||||
"p-retry": "4.2.0",
|
"p-retry": "4.2.0",
|
||||||
@@ -115,7 +117,7 @@
|
|||||||
"@types/prop-types": "15.5.8",
|
"@types/prop-types": "15.5.8",
|
||||||
"@types/puppeteer": "3.0.1",
|
"@types/puppeteer": "3.0.1",
|
||||||
"@types/q": "1.5.1",
|
"@types/q": "1.5.1",
|
||||||
"@types/react": "16.9.49",
|
"@types/react": "16.9.56",
|
||||||
"@types/react-dom": "16.0.7",
|
"@types/react-dom": "16.0.7",
|
||||||
"@types/react-notification-system": "0.2.39",
|
"@types/react-notification-system": "0.2.39",
|
||||||
"@types/react-redux": "7.1.7",
|
"@types/react-redux": "7.1.7",
|
||||||
@@ -194,8 +196,8 @@
|
|||||||
"compile": "tsc",
|
"compile": "tsc",
|
||||||
"compile:contracts": "tsc -p ./tsconfig.contracts.json",
|
"compile:contracts": "tsc -p ./tsconfig.contracts.json",
|
||||||
"compile:strict": "tsc -p ./tsconfig.strict.json",
|
"compile:strict": "tsc -p ./tsconfig.strict.json",
|
||||||
"format": "prettier --write \"{src,cypress,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
"format": "prettier --write \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
||||||
"format:check": "prettier --check \"{src,cypress,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
"format:check": "prettier --check \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
||||||
"lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"",
|
"lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"",
|
||||||
"build:contracts": "npm run compile:contracts",
|
"build:contracts": "npm run compile:contracts",
|
||||||
"strictEligibleFiles": "node ./strict-migration-tools/index.js",
|
"strictEligibleFiles": "node ./strict-migration-tools/index.js",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { AutopilotTier } from "../Contracts/DataModels";
|
|
||||||
import { HashMap } from "./HashMap";
|
import { HashMap } from "./HashMap";
|
||||||
|
|
||||||
export class AuthorizationEndpoints {
|
export class AuthorizationEndpoints {
|
||||||
@@ -124,10 +123,11 @@ export class Features {
|
|||||||
public static readonly notebookBasePath = "notebookbasepath";
|
public static readonly notebookBasePath = "notebookbasepath";
|
||||||
public static readonly canExceedMaximumValue = "canexceedmaximumvalue";
|
public static readonly canExceedMaximumValue = "canexceedmaximumvalue";
|
||||||
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
|
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
|
||||||
public static readonly enableAutoPilotV2 = "enableautopilotv2";
|
|
||||||
public static readonly ttl90Days = "ttl90days";
|
public static readonly ttl90Days = "ttl90days";
|
||||||
public static readonly enableRightPanelV2 = "enablerightpanelv2";
|
public static readonly enableRightPanelV2 = "enablerightpanelv2";
|
||||||
|
public static readonly enableSchema = "enableschema";
|
||||||
public static readonly enableSDKoperations = "enablesdkoperations";
|
public static readonly enableSDKoperations = "enablesdkoperations";
|
||||||
|
public static readonly showMinRUSurvey = "showminrusurvey";
|
||||||
}
|
}
|
||||||
|
|
||||||
// flight names returned from the portal are always lowercase
|
// flight names returned from the portal are always lowercase
|
||||||
@@ -262,7 +262,6 @@ export class HttpHeaders {
|
|||||||
public static usePolygonsSmallerThanAHemisphere = "x-ms-documentdb-usepolygonssmallerthanahemisphere";
|
public static usePolygonsSmallerThanAHemisphere = "x-ms-documentdb-usepolygonssmallerthanahemisphere";
|
||||||
public static autoPilotThroughput = "autoscaleSettings";
|
public static autoPilotThroughput = "autoscaleSettings";
|
||||||
public static autoPilotThroughputSDK = "x-ms-cosmos-offer-autopilot-settings";
|
public static autoPilotThroughputSDK = "x-ms-cosmos-offer-autopilot-settings";
|
||||||
public static autoPilotTier = "x-ms-cosmos-offer-autopilot-tier";
|
|
||||||
public static partitionKey: string = "x-ms-documentdb-partitionkey";
|
public static partitionKey: string = "x-ms-documentdb-partitionkey";
|
||||||
public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput";
|
public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput";
|
||||||
public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot";
|
public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot";
|
||||||
@@ -407,54 +406,6 @@ export enum ConflictOperationType {
|
|||||||
Delete = "delete"
|
Delete = "delete"
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AutoPilot {
|
|
||||||
public static tier1Text: string = "4,000 RU/s";
|
|
||||||
public static tier2Text: string = "20,000 RU/s";
|
|
||||||
public static tier3Text: string = "100,000 RU/s";
|
|
||||||
public static tier4Text: string = "500,000 RU/s";
|
|
||||||
|
|
||||||
public static tierText = {
|
|
||||||
[AutopilotTier.Tier1]: "Tier 1",
|
|
||||||
[AutopilotTier.Tier2]: "Tier 2",
|
|
||||||
[AutopilotTier.Tier3]: "Tier 3",
|
|
||||||
[AutopilotTier.Tier4]: "Tier 4"
|
|
||||||
};
|
|
||||||
|
|
||||||
public static tierMaxRus = {
|
|
||||||
[AutopilotTier.Tier1]: 2000,
|
|
||||||
[AutopilotTier.Tier2]: 20000,
|
|
||||||
[AutopilotTier.Tier3]: 100000,
|
|
||||||
[AutopilotTier.Tier4]: 500000
|
|
||||||
};
|
|
||||||
|
|
||||||
public static tierMinRus = {
|
|
||||||
[AutopilotTier.Tier1]: 0,
|
|
||||||
[AutopilotTier.Tier2]: 0,
|
|
||||||
[AutopilotTier.Tier3]: 0,
|
|
||||||
[AutopilotTier.Tier4]: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
public static tierStorageInGB = {
|
|
||||||
[AutopilotTier.Tier1]: 50,
|
|
||||||
[AutopilotTier.Tier2]: 200,
|
|
||||||
[AutopilotTier.Tier3]: 1000,
|
|
||||||
[AutopilotTier.Tier4]: 5000
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DataExplorerVersions {
|
|
||||||
public static readonly v_1_0_0: string = "1.0.0";
|
|
||||||
public static readonly v_1_0_1: string = "1.0.1";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DataExplorerFeatures {
|
|
||||||
public static offerCache: string = "OfferCache";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DataExplorerFeaturesVersions: any = {
|
|
||||||
OfferCache: DataExplorerVersions.v_1_0_1
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EmulatorMasterKey =
|
export const EmulatorMasterKey =
|
||||||
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
|
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
|
||||||
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
|
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as Cosmos from "@azure/cosmos";
|
import * as Cosmos from "@azure/cosmos";
|
||||||
import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos";
|
import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos";
|
||||||
import { configContext, Platform } from "../ConfigContext";
|
import { configContext, Platform } from "../ConfigContext";
|
||||||
|
import { getErrorMessage } from "./ErrorHandlingUtils";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
@@ -69,7 +70,7 @@ export async function getTokenFromAuthService(verb: string, resourceType: string
|
|||||||
const result = JSON.parse(await response.json());
|
const result = JSON.parse(await response.json());
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleError(`Failed to get authorization headers for ${resourceType}: ${error.message}`);
|
logConsoleError(`Failed to get authorization headers for ${resourceType}: ${getErrorMessage(error)}`);
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,13 @@
|
|||||||
import {
|
import { ConflictDefinition, FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
ConflictDefinition,
|
|
||||||
FeedOptions,
|
|
||||||
ItemDefinition,
|
|
||||||
OfferDefinition,
|
|
||||||
QueryIterator,
|
|
||||||
Resource
|
|
||||||
} from "@azure/cosmos";
|
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
|
||||||
import Q from "q";
|
import Q from "q";
|
||||||
import { configContext, Platform } from "../ConfigContext";
|
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import ConflictId from "../Explorer/Tree/ConflictId";
|
import ConflictId from "../Explorer/Tree/ConflictId";
|
||||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||||
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
||||||
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
|
||||||
import { OfferUtils } from "../Utils/OfferUtils";
|
|
||||||
import * as Constants from "./Constants";
|
import * as Constants from "./Constants";
|
||||||
import { client } from "./CosmosClient";
|
import { client } from "./CosmosClient";
|
||||||
import * as HeadersUtility from "./HeadersUtility";
|
|
||||||
import { sendCachedDataMessage } from "./MessageHandler";
|
|
||||||
|
|
||||||
export function getCommonQueryOptions(options: FeedOptions): any {
|
export function getCommonQueryOptions(options: FeedOptions): any {
|
||||||
const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage);
|
const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage);
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ export function executeStoredProcedure(
|
|||||||
(error: any) => {
|
(error: any) => {
|
||||||
handleError(
|
handleError(
|
||||||
error,
|
error,
|
||||||
`Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`,
|
"ExecuteStoredProcedure",
|
||||||
"ExecuteStoredProcedure"
|
`Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
|
||||||
);
|
);
|
||||||
deferred.reject(error);
|
deferred.reject(error);
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ export function queryDocumentsPage(
|
|||||||
deferred.resolve(result);
|
deferred.resolve(result);
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
handleError(error, `Failed to query ${entityName} for container ${resourceName}`, "QueryDocumentsPage");
|
handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`);
|
||||||
deferred.reject(error);
|
deferred.reject(error);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -109,7 +109,7 @@ export function readDocument(collection: ViewModels.CollectionBase, documentId:
|
|||||||
deferred.resolve(document);
|
deferred.resolve(document);
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
handleError(error, `Failed to read ${entityName} ${documentId.id()}`, "ReadDocument");
|
handleError(error, "ReadDocument", `Failed to read ${entityName} ${documentId.id()}`);
|
||||||
deferred.reject(error);
|
deferred.reject(error);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -135,7 +135,7 @@ export function updateDocument(
|
|||||||
deferred.resolve(updatedDocument);
|
deferred.resolve(updatedDocument);
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
handleError(error, `Failed to update ${entityName} ${documentId.id()}`, "UpdateDocument");
|
handleError(error, "UpdateDocument", `Failed to update ${entityName} ${documentId.id()}`);
|
||||||
deferred.reject(error);
|
deferred.reject(error);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -157,7 +157,7 @@ export function createDocument(collection: ViewModels.CollectionBase, newDocumen
|
|||||||
deferred.resolve(savedDocument);
|
deferred.resolve(savedDocument);
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
handleError(error, `Error while creating new ${entityName} for container ${collection.id()}`, "CreateDocument");
|
handleError(error, "CreateDocument", `Error while creating new ${entityName} for container ${collection.id()}`);
|
||||||
deferred.reject(error);
|
deferred.reject(error);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -179,7 +179,7 @@ export function deleteDocument(collection: ViewModels.CollectionBase, documentId
|
|||||||
deferred.resolve(response);
|
deferred.resolve(response);
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
handleError(error, `Error while deleting ${entityName} ${documentId.id()}`, "DeleteDocument");
|
handleError(error, "DeleteDocument", `Error while deleting ${entityName} ${documentId.id()}`);
|
||||||
deferred.reject(error);
|
deferred.reject(error);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -205,7 +205,7 @@ export function deleteConflict(
|
|||||||
deferred.resolve(response);
|
deferred.resolve(response);
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
handleError(error, `Error while deleting conflict ${conflictId.id()}`, "DeleteConflict");
|
handleError(error, "DeleteConflict", `Error while deleting conflict ${conflictId.id()}`);
|
||||||
deferred.reject(error);
|
deferred.reject(error);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,56 @@
|
|||||||
import { CosmosError, sendNotificationForError } from "./dataAccess/sendNotificationForError";
|
import { ARMError } from "../Utils/arm/request";
|
||||||
|
import { HttpStatusCodes } from "./Constants";
|
||||||
|
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||||
|
import { SubscriptionType } from "../Contracts/SubscriptionType";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import { logError } from "./Logger";
|
import { logError } from "./Logger";
|
||||||
import { replaceKnownError } from "./ErrorParserUtility";
|
import { sendMessage } from "./MessageHandler";
|
||||||
|
|
||||||
export const handleError = (error: CosmosError, consoleErrorPrefix: string, area: string): void => {
|
export const handleError = (error: string | ARMError | Error, area: string, consoleErrorPrefix?: string): void => {
|
||||||
const sanitizedErrorMsg = replaceKnownError(error.message);
|
const errorMessage = getErrorMessage(error);
|
||||||
logConsoleError(`${consoleErrorPrefix}:\n ${sanitizedErrorMsg}`);
|
const errorCode = error instanceof ARMError ? error.code : undefined;
|
||||||
logError(sanitizedErrorMsg, area, error.code);
|
|
||||||
sendNotificationForError(error);
|
// logs error to data explorer console
|
||||||
|
const consoleErrorMessage = consoleErrorPrefix ? `${consoleErrorPrefix}:\n ${errorMessage}` : errorMessage;
|
||||||
|
logConsoleError(consoleErrorMessage);
|
||||||
|
|
||||||
|
// logs error to both app insight and kusto
|
||||||
|
logError(errorMessage, area, errorCode);
|
||||||
|
|
||||||
|
// checks for errors caused by firewall and sends them to portal to handle
|
||||||
|
sendNotificationForError(errorMessage, errorCode);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getErrorMessage = (error: string | Error): string => {
|
||||||
|
const errorMessage = typeof error === "string" ? error : error.message;
|
||||||
|
return replaceKnownError(errorMessage);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getErrorStack = (error: string | Error): string => {
|
||||||
|
return typeof error === "string" ? undefined : error.stack;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendNotificationForError = (errorMessage: string, errorCode: number | string): void => {
|
||||||
|
if (errorCode === HttpStatusCodes.Forbidden) {
|
||||||
|
if (errorMessage?.toLowerCase().indexOf("sharedoffer is disabled for your account") > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendMessage({
|
||||||
|
type: MessageTypes.ForbiddenError,
|
||||||
|
reason: errorMessage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const replaceKnownError = (errorMessage: string): string => {
|
||||||
|
if (
|
||||||
|
window.dataExplorer?.subscriptionType() === SubscriptionType.Internal &&
|
||||||
|
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) {
|
||||||
|
return "Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorMessage;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import * as ErrorParserUtility from "./ErrorParserUtility";
|
|
||||||
|
|
||||||
describe("Error Parser Utility", () => {
|
|
||||||
describe("shouldEnableCrossPartitionKeyForResourceWithPartitionKey()", () => {
|
|
||||||
it("should parse a backend error correctly", () => {
|
|
||||||
// A fake error matching what is thrown by the SDK on a bad collection create request
|
|
||||||
const innerMessage =
|
|
||||||
"The partition key component definition path '/asdwqr31 @#$#$WRadf' could not be accepted, failed near position '10'. Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character.";
|
|
||||||
const message = `Message: {\"Errors\":[\"${innerMessage}\"]}\r\nActivityId: 97b2e684-7505-4921-85f6-2513b9b28220, Request URI: /apps/89fdcf25-2a0b-4d2a-aab6-e161e565b26f/services/54911149-7bb1-4e7d-a1fa-22c8b36a4bb9/partitions/cc2a7a04-5f5a-4709-bcf7-8509b264963f/replicas/132304018743619218p, RequestStats: , SDK: Microsoft.Azure.Documents.Common/2.10.0`;
|
|
||||||
const err = new Error(message) as any;
|
|
||||||
err.code = 400;
|
|
||||||
err.body = {
|
|
||||||
code: "BadRequest",
|
|
||||||
message
|
|
||||||
};
|
|
||||||
err.headers = {};
|
|
||||||
err.activityId = "97b2e684-7505-4921-85f6-2513b9b28220";
|
|
||||||
|
|
||||||
const parsedError = ErrorParserUtility.parse(err);
|
|
||||||
expect(parsedError.length).toBe(1);
|
|
||||||
expect(parsedError[0].message).toBe(innerMessage);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import * as DataModels from "../Contracts/DataModels";
|
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
|
||||||
|
|
||||||
export function replaceKnownError(err: string): string {
|
|
||||||
if (
|
|
||||||
window.dataExplorer.subscriptionType() === ViewModels.SubscriptionType.Internal &&
|
|
||||||
err.indexOf("SharedOffer is Disabled for your account") >= 0
|
|
||||||
) {
|
|
||||||
return "Database throughput is not supported for internal subscriptions.";
|
|
||||||
} else if (err.indexOf("Partition key paths must contain only valid") >= 0) {
|
|
||||||
return "Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character.";
|
|
||||||
}
|
|
||||||
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parse(err: any): DataModels.ErrorDataModel[] {
|
|
||||||
try {
|
|
||||||
return _parse(err);
|
|
||||||
} catch (e) {
|
|
||||||
return [<DataModels.ErrorDataModel>{ message: JSON.stringify(err) }];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _parse(err: any): DataModels.ErrorDataModel[] {
|
|
||||||
var normalizedErrors: DataModels.ErrorDataModel[] = [];
|
|
||||||
if (err.message && !err.code) {
|
|
||||||
normalizedErrors.push(err);
|
|
||||||
} else {
|
|
||||||
const innerErrors: any[] = _getInnerErrors(err.message);
|
|
||||||
normalizedErrors = innerErrors.map(innerError =>
|
|
||||||
typeof innerError === "string" ? { message: innerError } : innerError
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalizedErrors;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _getInnerErrors(message: string): any[] {
|
|
||||||
/*
|
|
||||||
The backend error message has an inner-message which is a stringified object.
|
|
||||||
|
|
||||||
For SQL errors, the "errors" property is an array of SqlErrorDataModel.
|
|
||||||
Example:
|
|
||||||
"Message: {"Errors":["Resource with specified id or name already exists"]}\r\nActivityId: 80005000008d40b6a, Request URI: /apps/19000c000c0a0005/services/mctestdocdbprod-MasterService-0-00066ab9937/partitions/900005f9000e676fb8/replicas/13000000000955p"
|
|
||||||
For non-SQL errors the "Errors" propery is an array of string.
|
|
||||||
Example:
|
|
||||||
"Message: {"errors":[{"severity":"Error","location":{"start":7,"end":8},"code":"SC1001","message":"Syntax error, incorrect syntax near '.'."}]}\r\nActivityId: d3300016d4084e310a, Request URI: /apps/12401f9e1df77/services/dc100232b1f44545/partitions/f86f3bc0001a2f78/replicas/13085003638s"
|
|
||||||
*/
|
|
||||||
|
|
||||||
let innerMessage: any = null;
|
|
||||||
|
|
||||||
const singleLineMessage = message.replace(/[\r\n]|\r|\n/g, "");
|
|
||||||
try {
|
|
||||||
// Multi-Partition error flavor
|
|
||||||
const regExp = /^(.*)ActivityId: (.*)/g;
|
|
||||||
const regString = regExp.exec(singleLineMessage);
|
|
||||||
const innerMessageString = regString[1];
|
|
||||||
innerMessage = JSON.parse(innerMessageString);
|
|
||||||
} catch (e) {
|
|
||||||
// Single-partition error flavor
|
|
||||||
const regExp = /^Message: (.*)ActivityId: (.*), Request URI: (.*)/g;
|
|
||||||
const regString = regExp.exec(singleLineMessage);
|
|
||||||
const innerMessageString = regString[1];
|
|
||||||
innerMessage = JSON.parse(innerMessageString);
|
|
||||||
}
|
|
||||||
|
|
||||||
return innerMessage.errors ? innerMessage.errors : innerMessage.Errors;
|
|
||||||
}
|
|
||||||
@@ -21,14 +21,8 @@ export function logWarning(message: string, area: string, code?: number): void {
|
|||||||
return _logEntry(entry);
|
return _logEntry(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logError(message: string | Error, area: string, code?: number): void {
|
export function logError(errorMessage: string, area: string, code?: number | string): void {
|
||||||
let logMessage: string;
|
const entry: Diagnostics.LogEntry = _generateLogEntry(Diagnostics.LogEntryLevel.Error, errorMessage, area, code);
|
||||||
if (typeof message === "string") {
|
|
||||||
logMessage = message;
|
|
||||||
} else {
|
|
||||||
logMessage = JSON.stringify(message, Object.getOwnPropertyNames(message));
|
|
||||||
}
|
|
||||||
const entry: Diagnostics.LogEntry = _generateLogEntry(Diagnostics.LogEntryLevel.Error, logMessage, area, code);
|
|
||||||
return _logEntry(entry);
|
return _logEntry(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +53,7 @@ function _generateLogEntry(
|
|||||||
level: Diagnostics.LogEntryLevel,
|
level: Diagnostics.LogEntryLevel,
|
||||||
message: string,
|
message: string,
|
||||||
area: string,
|
area: string,
|
||||||
code?: number
|
code?: number | string
|
||||||
): Diagnostics.LogEntry {
|
): Diagnostics.LogEntry {
|
||||||
return {
|
return {
|
||||||
timestamp: new Date().getUTCSeconds(),
|
timestamp: new Date().getUTCSeconds(),
|
||||||
|
|||||||
62
src/Common/OfferUtility.test.ts
Normal file
62
src/Common/OfferUtility.test.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
33
src/Common/OfferUtility.ts
Normal file
33
src/Common/OfferUtility.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Offer, SDKOfferDefinition } from "../Contracts/DataModels";
|
||||||
|
import { OfferResponse } from "@azure/cosmos";
|
||||||
|
|
||||||
|
export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => {
|
||||||
|
const offerDefinition: SDKOfferDefinition = offerResponse?.resource;
|
||||||
|
const offerContent = offerDefinition.content;
|
||||||
|
if (!offerContent) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const minimumThroughput = offerContent.collectionThroughputInfo?.minimumRUForCollection;
|
||||||
|
const autopilotSettings = offerContent.offerAutopilotSettings;
|
||||||
|
|
||||||
|
if (autopilotSettings) {
|
||||||
|
return {
|
||||||
|
id: offerDefinition.id,
|
||||||
|
autoscaleMaxThroughput: autopilotSettings.maxThroughput,
|
||||||
|
manualThroughput: undefined,
|
||||||
|
minimumThroughput,
|
||||||
|
offerDefinition,
|
||||||
|
headers: offerResponse.headers
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: offerDefinition.id,
|
||||||
|
autoscaleMaxThroughput: undefined,
|
||||||
|
manualThroughput: offerContent.offerThroughput,
|
||||||
|
minimumThroughput,
|
||||||
|
offerDefinition,
|
||||||
|
headers: offerResponse.headers
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -16,7 +16,7 @@ const notificationsPath = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const fetchPortalNotifications = async (): Promise<DataModels.Notification[]> => {
|
export const fetchPortalNotifications = async (): Promise<DataModels.Notification[]> => {
|
||||||
if (configContext.platform === Platform.Emulator) {
|
if (configContext.platform === Platform.Emulator || configContext.platform === Platform.Hosted) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
|
|||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { createDocument, deleteDocument, queryDocuments, queryDocumentsPage } from "./DocumentClientUtilityBase";
|
import { createDocument, deleteDocument, queryDocuments, queryDocumentsPage } from "./DocumentClientUtilityBase";
|
||||||
import { createCollection } from "./dataAccess/createCollection";
|
import { createCollection } from "./dataAccess/createCollection";
|
||||||
import * as ErrorParserUtility from "./ErrorParserUtility";
|
import { handleError } from "./ErrorHandlingUtils";
|
||||||
import * as Logger from "./Logger";
|
|
||||||
|
|
||||||
export class QueriesClient {
|
export class QueriesClient {
|
||||||
private static readonly PartitionKey: DataModels.PartitionKey = {
|
private static readonly PartitionKey: DataModels.PartitionKey = {
|
||||||
@@ -53,13 +52,8 @@ export class QueriesClient {
|
|||||||
return Promise.resolve(collection);
|
return Promise.resolve(collection);
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
const stringifiedError: string = error.message;
|
handleError(error, "setupQueriesCollection", "Failed to set up account for saving queries");
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
return Promise.reject(error);
|
||||||
ConsoleDataType.Error,
|
|
||||||
`Failed to set up account for saving queries: ${stringifiedError}`
|
|
||||||
);
|
|
||||||
Logger.logError(stringifiedError, "setupQueriesCollection");
|
|
||||||
return Promise.reject(stringifiedError);
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
||||||
@@ -102,19 +96,11 @@ export class QueriesClient {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
let errorMessage: string;
|
if (error.code === HttpStatusCodes.Conflict.toString()) {
|
||||||
const parsedError: DataModels.ErrorDataModel = ErrorParserUtility.parse(error)[0];
|
error = `Query ${query.queryName} already exists`;
|
||||||
if (parsedError.code === HttpStatusCodes.Conflict.toString()) {
|
|
||||||
errorMessage = `Query ${query.queryName} already exists`;
|
|
||||||
} else {
|
|
||||||
errorMessage = parsedError.message;
|
|
||||||
}
|
}
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
handleError(error, "saveQuery", `Failed to save query ${query.queryName}`);
|
||||||
ConsoleDataType.Error,
|
return Promise.reject(error);
|
||||||
`Failed to save query ${query.queryName}: ${errorMessage}`
|
|
||||||
);
|
|
||||||
Logger.logError(JSON.stringify(parsedError), "saveQuery");
|
|
||||||
return Promise.reject(errorMessage);
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
||||||
@@ -163,25 +149,15 @@ export class QueriesClient {
|
|||||||
return Promise.resolve(queries);
|
return Promise.resolve(queries);
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
const stringifiedError: string = error.message;
|
handleError(error, "getSavedQueries", "Failed to fetch saved queries");
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
return Promise.reject(error);
|
||||||
ConsoleDataType.Error,
|
|
||||||
`Failed to fetch saved queries: ${stringifiedError}`
|
|
||||||
);
|
|
||||||
Logger.logError(stringifiedError, "getSavedQueries");
|
|
||||||
return Promise.reject(stringifiedError);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
// should never get into this state but we handle this regardless
|
// should never get into this state but we handle this regardless
|
||||||
const stringifiedError: string = error.message;
|
handleError(error, "getSavedQueries", "Failed to fetch saved queries");
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
return Promise.reject(error);
|
||||||
ConsoleDataType.Error,
|
|
||||||
`Failed to fetch saved queries: ${stringifiedError}`
|
|
||||||
);
|
|
||||||
Logger.logError(stringifiedError, "getSavedQueries");
|
|
||||||
return Promise.reject(stringifiedError);
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
||||||
@@ -232,13 +208,8 @@ export class QueriesClient {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
const stringifiedError: string = error.message;
|
handleError(error, "deleteQuery", `Failed to delete query ${query.queryName}`);
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
return Promise.reject(error);
|
||||||
ConsoleDataType.Error,
|
|
||||||
`Failed to delete query ${query.queryName}: ${stringifiedError}`
|
|
||||||
);
|
|
||||||
Logger.logError(stringifiedError, "deleteQuery");
|
|
||||||
return Promise.reject(stringifiedError);
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
|
|||||||
logConsoleInfo(`Successfully created container ${params.collectionId}`);
|
logConsoleInfo(`Successfully created container ${params.collectionId}`);
|
||||||
return collection;
|
return collection;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Error while creating container ${params.collectionId}`, "CreateCollection");
|
handleError(error, "CreateCollection", `Error while creating container ${params.collectionId}`);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export async function createDatabase(params: DataModels.CreateDatabaseParams): P
|
|||||||
logConsoleInfo(`Successfully created database ${params.databaseId}`);
|
logConsoleInfo(`Successfully created database ${params.databaseId}`);
|
||||||
return database;
|
return database;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Error while creating database ${params.databaseId}`, "CreateDatabase");
|
handleError(error, "CreateDatabase", `Error while creating database ${params.databaseId}`);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export async function createStoredProcedure(
|
|||||||
.scripts.storedProcedures.create(storedProcedure);
|
.scripts.storedProcedures.create(storedProcedure);
|
||||||
return response?.resource;
|
return response?.resource;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Error while creating stored procedure ${storedProcedure.id}`, "CreateStoredProcedure");
|
handleError(error, "CreateStoredProcedure", `Error while creating stored procedure ${storedProcedure.id}`);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -7,9 +7,8 @@ import {
|
|||||||
} from "../../Utils/arm/generatedClients/2020-04-01/types";
|
} from "../../Utils/arm/generatedClients/2020-04-01/types";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { createUpdateSqlTrigger, getSqlTrigger } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
import { createUpdateSqlTrigger, getSqlTrigger } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
import { logError } from "../Logger";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { sendNotificationForError } from "./sendNotificationForError";
|
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export async function createTrigger(
|
export async function createTrigger(
|
||||||
@@ -66,9 +65,7 @@ export async function createTrigger(
|
|||||||
.scripts.triggers.create(trigger);
|
.scripts.triggers.create(trigger);
|
||||||
return response.resource;
|
return response.resource;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleError(`Error while creating trigger ${trigger.id}:\n ${error.message}`);
|
handleError(error, "CreateTrigger", `Error while creating trigger ${trigger.id}`);
|
||||||
logError(error.message, "CreateTrigger", error.code);
|
|
||||||
sendNotificationForError(error);
|
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ export async function createUserDefinedFunction(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(
|
handleError(
|
||||||
error,
|
error,
|
||||||
`Error while creating user defined function ${userDefinedFunction.id}`,
|
"CreateUserupdateUserDefinedFunction",
|
||||||
"CreateUserupdateUserDefinedFunction"
|
`Error while creating user defined function ${userDefinedFunction.id}`
|
||||||
);
|
);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export async function deleteCollection(databaseId: string, collectionId: string)
|
|||||||
}
|
}
|
||||||
logConsoleInfo(`Successfully deleted container ${collectionId}`);
|
logConsoleInfo(`Successfully deleted container ${collectionId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Error while deleting container ${collectionId}`, "DeleteCollection");
|
handleError(error, "DeleteCollection", `Error while deleting container ${collectionId}`);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export async function deleteDatabase(databaseId: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
logConsoleInfo(`Successfully deleted database ${databaseId}`);
|
logConsoleInfo(`Successfully deleted database ${databaseId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Error while deleting database ${databaseId}`, "DeleteDatabase");
|
handleError(error, "DeleteDatabase", `Error while deleting database ${databaseId}`);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export async function deleteStoredProcedure(
|
|||||||
.delete();
|
.delete();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Error while deleting stored procedure ${storedProcedureId}`, "DeleteStoredProcedure");
|
handleError(error, "DeleteStoredProcedure", `Error while deleting stored procedure ${storedProcedureId}`);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export async function deleteTrigger(databaseId: string, collectionId: string, tr
|
|||||||
.delete();
|
.delete();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Error while deleting trigger ${triggerId}`, "DeleteTrigger");
|
handleError(error, "DeleteTrigger", `Error while deleting trigger ${triggerId}`);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export async function deleteUserDefinedFunction(databaseId: string, collectionId
|
|||||||
.delete();
|
.delete();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Error while deleting user defined function ${id}`, "DeleteUserDefinedFunction");
|
handleError(error, "DeleteUserDefinedFunction", `Error while deleting user defined function ${id}`);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
28
src/Common/dataAccess/getIndexTransformationProgress.ts
Normal file
28
src/Common/dataAccess/getIndexTransformationProgress.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import * as Constants from "../Constants";
|
||||||
|
import { AuthType } from "../../AuthType";
|
||||||
|
|
||||||
|
export async function getIndexTransformationProgress(databaseId: string, collectionId: string): Promise<number> {
|
||||||
|
if (window.authType !== AuthType.AAD) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let indexTransformationPercentage: number;
|
||||||
|
const clearMessage = logConsoleProgress(`Reading container ${collectionId}`);
|
||||||
|
try {
|
||||||
|
const response = await client()
|
||||||
|
.database(databaseId)
|
||||||
|
.container(collectionId)
|
||||||
|
.read({ populateQuotaInfo: true });
|
||||||
|
|
||||||
|
indexTransformationPercentage = parseInt(
|
||||||
|
response.headers[Constants.HttpHeaders.collectionIndexTransformationProgress] as string
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, "ReadMongoDBCollection", `Error while reading container ${collectionId}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
clearMessage();
|
||||||
|
return indexTransformationPercentage;
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ export async function readCollection(databaseId: string, collectionId: string):
|
|||||||
.read();
|
.read();
|
||||||
collection = response.resource as DataModels.Collection;
|
collection = response.resource as DataModels.Collection;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Error while querying container ${collectionId}`, "ReadCollection");
|
handleError(error, "ReadCollection", `Error while querying container ${collectionId}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import * as DataModels from "../../Contracts/DataModels";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
import { HttpHeaders } from "../Constants";
|
import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels";
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
|
||||||
import { client } from "../CosmosClient";
|
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
import { getSqlContainerThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
import { getSqlContainerThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||||
import { getMongoDBCollectionThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
import { getMongoDBCollectionThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||||
@@ -11,113 +8,114 @@ import { getCassandraTableThroughput } from "../../Utils/arm/generatedClients/20
|
|||||||
import { getGremlinGraphThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
import { getGremlinGraphThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||||
import { getTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
|
import { getTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { readOffers } from "./readOffers";
|
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export const readCollectionOffer = async (
|
export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise<Offer> => {
|
||||||
params: DataModels.ReadCollectionOfferParams
|
|
||||||
): Promise<DataModels.OfferWithHeaders> => {
|
|
||||||
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
|
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
|
||||||
let offerId = params.offerId;
|
|
||||||
if (!offerId) {
|
|
||||||
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
|
||||||
try {
|
|
||||||
offerId = await getCollectionOfferIdWithARM(params.databaseId, params.collectionId);
|
|
||||||
} catch (error) {
|
|
||||||
clearMessage();
|
|
||||||
if (error.code !== "NotFound") {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
offerId = await getCollectionOfferIdWithSDK(params.collectionResourceId);
|
|
||||||
if (!offerId) {
|
|
||||||
clearMessage();
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: RequestOptions = {
|
|
||||||
initialHeaders: {
|
|
||||||
[HttpHeaders.populateCollectionThroughputInfo]: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await client()
|
if (
|
||||||
.offer(offerId)
|
window.authType === AuthType.AAD &&
|
||||||
.read(options);
|
!userContext.useSDKOperations &&
|
||||||
return (
|
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||||
response && {
|
) {
|
||||||
...response.resource,
|
return await readCollectionOfferWithARM(params.databaseId, params.collectionId);
|
||||||
headers: response.headers
|
}
|
||||||
}
|
|
||||||
);
|
return await readOfferWithSDK(params.offerId, params.collectionResourceId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Error while querying offer for collection ${params.collectionId}`, "ReadCollectionOffer");
|
handleError(error, "ReadCollectionOffer", `Error while querying offer for collection ${params.collectionId}`);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCollectionOfferIdWithARM = async (databaseId: string, collectionId: string): Promise<string> => {
|
const readCollectionOfferWithARM = async (databaseId: string, collectionId: string): Promise<Offer> => {
|
||||||
let rpResponse;
|
|
||||||
const subscriptionId = userContext.subscriptionId;
|
const subscriptionId = userContext.subscriptionId;
|
||||||
const resourceGroup = userContext.resourceGroup;
|
const resourceGroup = userContext.resourceGroup;
|
||||||
const accountName = userContext.databaseAccount.name;
|
const accountName = userContext.databaseAccount.name;
|
||||||
const defaultExperience = userContext.defaultExperience;
|
const defaultExperience = userContext.defaultExperience;
|
||||||
switch (defaultExperience) {
|
|
||||||
case DefaultAccountExperienceType.DocumentDB:
|
let rpResponse;
|
||||||
rpResponse = await getSqlContainerThroughput(
|
try {
|
||||||
subscriptionId,
|
switch (defaultExperience) {
|
||||||
resourceGroup,
|
case DefaultAccountExperienceType.DocumentDB:
|
||||||
accountName,
|
rpResponse = await getSqlContainerThroughput(
|
||||||
databaseId,
|
subscriptionId,
|
||||||
collectionId
|
resourceGroup,
|
||||||
);
|
accountName,
|
||||||
break;
|
databaseId,
|
||||||
case DefaultAccountExperienceType.MongoDB:
|
collectionId
|
||||||
rpResponse = await getMongoDBCollectionThroughput(
|
);
|
||||||
subscriptionId,
|
break;
|
||||||
resourceGroup,
|
case DefaultAccountExperienceType.MongoDB:
|
||||||
accountName,
|
rpResponse = await getMongoDBCollectionThroughput(
|
||||||
databaseId,
|
subscriptionId,
|
||||||
collectionId
|
resourceGroup,
|
||||||
);
|
accountName,
|
||||||
break;
|
databaseId,
|
||||||
case DefaultAccountExperienceType.Cassandra:
|
collectionId
|
||||||
rpResponse = await getCassandraTableThroughput(
|
);
|
||||||
subscriptionId,
|
break;
|
||||||
resourceGroup,
|
case DefaultAccountExperienceType.Cassandra:
|
||||||
accountName,
|
rpResponse = await getCassandraTableThroughput(
|
||||||
databaseId,
|
subscriptionId,
|
||||||
collectionId
|
resourceGroup,
|
||||||
);
|
accountName,
|
||||||
break;
|
databaseId,
|
||||||
case DefaultAccountExperienceType.Graph:
|
collectionId
|
||||||
rpResponse = await getGremlinGraphThroughput(
|
);
|
||||||
subscriptionId,
|
break;
|
||||||
resourceGroup,
|
case DefaultAccountExperienceType.Graph:
|
||||||
accountName,
|
rpResponse = await getGremlinGraphThroughput(
|
||||||
databaseId,
|
subscriptionId,
|
||||||
collectionId
|
resourceGroup,
|
||||||
);
|
accountName,
|
||||||
break;
|
databaseId,
|
||||||
case DefaultAccountExperienceType.Table:
|
collectionId
|
||||||
rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
case DefaultAccountExperienceType.Table:
|
||||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== "NotFound") {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return rpResponse?.name;
|
const resource = rpResponse?.properties?.resource;
|
||||||
};
|
if (resource) {
|
||||||
|
const offerId: string = rpResponse.name;
|
||||||
|
const minimumThroughput: number =
|
||||||
|
typeof resource.minimumThroughput === "string"
|
||||||
|
? parseInt(resource.minimumThroughput)
|
||||||
|
: resource.minimumThroughput;
|
||||||
|
const autoscaleSettings = resource.autoscaleSettings;
|
||||||
|
|
||||||
const getCollectionOfferIdWithSDK = async (collectionResourceId: string): Promise<string> => {
|
if (autoscaleSettings) {
|
||||||
const offers = await readOffers();
|
return {
|
||||||
const offer = offers.find(offer => offer.resource === collectionResourceId);
|
id: offerId,
|
||||||
return offer?.id;
|
autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
|
||||||
|
manualThroughput: undefined,
|
||||||
|
minimumThroughput
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: offerId,
|
||||||
|
autoscaleMaxThroughput: undefined,
|
||||||
|
manualThroughput: resource.throughput,
|
||||||
|
minimumThroughput
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const readCollectionQuotaInfo = async (
|
|||||||
|
|
||||||
return quota;
|
return quota;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Error while querying quota info for container ${collection.id}`, "ReadCollectionQuotaInfo");
|
handleError(error, "ReadCollectionQuotaInfo", `Error while querying quota info for container ${collection.id}`);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
|
|||||||
.fetchAll();
|
.fetchAll();
|
||||||
return sdkResponse.resources as DataModels.Collection[];
|
return sdkResponse.resources as DataModels.Collection[];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Error while querying containers for database ${databaseId}`, "ReadCollections");
|
handleError(error, "ReadCollections", `Error while querying containers for database ${databaseId}`);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -1,66 +1,43 @@
|
|||||||
import * as DataModels from "../../Contracts/DataModels";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
import { HttpHeaders } from "../Constants";
|
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
|
||||||
import { client } from "../CosmosClient";
|
|
||||||
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||||
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||||
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
||||||
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { readOffers } from "./readOffers";
|
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export const readDatabaseOffer = async (
|
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
|
||||||
params: DataModels.ReadDatabaseOfferParams
|
|
||||||
): Promise<DataModels.OfferWithHeaders> => {
|
|
||||||
const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`);
|
const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`);
|
||||||
let offerId = params.offerId;
|
|
||||||
if (!offerId) {
|
|
||||||
offerId = await (window.authType === AuthType.AAD &&
|
|
||||||
!userContext.useSDKOperations &&
|
|
||||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
|
||||||
? getDatabaseOfferIdWithARM(params.databaseId)
|
|
||||||
: getDatabaseOfferIdWithSDK(params.databaseResourceId));
|
|
||||||
if (!offerId) {
|
|
||||||
clearMessage();
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: RequestOptions = {
|
|
||||||
initialHeaders: {
|
|
||||||
[HttpHeaders.populateCollectionThroughputInfo]: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await client()
|
if (
|
||||||
.offer(offerId)
|
window.authType === AuthType.AAD &&
|
||||||
.read(options);
|
!userContext.useSDKOperations &&
|
||||||
return (
|
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||||
response && {
|
) {
|
||||||
...response.resource,
|
return await readDatabaseOfferWithARM(params.databaseId);
|
||||||
headers: response.headers
|
}
|
||||||
}
|
|
||||||
);
|
return await readOfferWithSDK(params.offerId, params.databaseResourceId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Error while querying offer for database ${params.databaseId}`, "ReadDatabaseOffer");
|
handleError(error, "ReadDatabaseOffer", `Error while querying offer for database ${params.databaseId}`);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> => {
|
const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
|
||||||
let rpResponse;
|
|
||||||
const subscriptionId = userContext.subscriptionId;
|
const subscriptionId = userContext.subscriptionId;
|
||||||
const resourceGroup = userContext.resourceGroup;
|
const resourceGroup = userContext.resourceGroup;
|
||||||
const accountName = userContext.databaseAccount.name;
|
const accountName = userContext.databaseAccount.name;
|
||||||
const defaultExperience = userContext.defaultExperience;
|
const defaultExperience = userContext.defaultExperience;
|
||||||
|
|
||||||
|
let rpResponse;
|
||||||
try {
|
try {
|
||||||
switch (defaultExperience) {
|
switch (defaultExperience) {
|
||||||
case DefaultAccountExperienceType.DocumentDB:
|
case DefaultAccountExperienceType.DocumentDB:
|
||||||
@@ -78,18 +55,39 @@ const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> =>
|
|||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return rpResponse?.name;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code !== "NotFound") {
|
if (error.code !== "NotFound") {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const getDatabaseOfferIdWithSDK = async (databaseResourceId: string): Promise<string> => {
|
const resource = rpResponse?.properties?.resource;
|
||||||
const offers = await readOffers();
|
if (resource) {
|
||||||
const offer = offers.find(offer => offer.resource === databaseResourceId);
|
const offerId: string = rpResponse.name;
|
||||||
return offer?.id;
|
const minimumThroughput: number =
|
||||||
|
typeof resource.minimumThroughput === "string"
|
||||||
|
? parseInt(resource.minimumThroughput)
|
||||||
|
: resource.minimumThroughput;
|
||||||
|
const autoscaleSettings = resource.autoscaleSettings;
|
||||||
|
|
||||||
|
if (autoscaleSettings) {
|
||||||
|
return {
|
||||||
|
id: offerId,
|
||||||
|
autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
|
||||||
|
manualThroughput: undefined,
|
||||||
|
minimumThroughput
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: offerId,
|
||||||
|
autoscaleMaxThroughput: undefined,
|
||||||
|
manualThroughput: resource.throughput,
|
||||||
|
minimumThroughput
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
|||||||
databases = sdkResponse.resources as DataModels.Database[];
|
databases = sdkResponse.resources as DataModels.Database[];
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Error while querying databases`, "ReadDatabases");
|
handleError(error, "ReadDatabases", `Error while querying databases`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { userContext } from "../../UserContext";
|
|||||||
import { getMongoDBCollection } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
import { getMongoDBCollection } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||||
import { MongoDBCollectionResource } from "../../Utils/arm/generatedClients/2020-04-01/types";
|
import { MongoDBCollectionResource } from "../../Utils/arm/generatedClients/2020-04-01/types";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import * as Constants from "../Constants";
|
|
||||||
import { client } from "../CosmosClient";
|
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
|
|
||||||
@@ -24,35 +22,9 @@ export async function readMongoDBCollectionThroughRP(
|
|||||||
const response = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
|
const response = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
|
||||||
collection = response.properties.resource;
|
collection = response.properties.resource;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Error while reading container ${collectionId}`, "ReadMongoDBCollection");
|
handleError(error, "ReadMongoDBCollection", `Error while reading container ${collectionId}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
clearMessage();
|
clearMessage();
|
||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMongoDBCollectionIndexTransformationProgress(
|
|
||||||
databaseId: string,
|
|
||||||
collectionId: string
|
|
||||||
): Promise<number> {
|
|
||||||
if (window.authType !== AuthType.AAD) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
let indexTransformationPercentage: number;
|
|
||||||
const clearMessage = logConsoleProgress(`Reading container ${collectionId}`);
|
|
||||||
try {
|
|
||||||
const response = await client()
|
|
||||||
.database(databaseId)
|
|
||||||
.container(collectionId)
|
|
||||||
.read({ populateQuotaInfo: true });
|
|
||||||
|
|
||||||
indexTransformationPercentage = parseInt(
|
|
||||||
response.headers[Constants.HttpHeaders.collectionIndexTransformationProgress] as string
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, `Error while reading container ${collectionId}`, "ReadMongoDBCollection");
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
clearMessage();
|
|
||||||
return indexTransformationPercentage;
|
|
||||||
}
|
|
||||||
|
|||||||
29
src/Common/dataAccess/readOfferWithSDK.ts
Normal file
29
src/Common/dataAccess/readOfferWithSDK.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { HttpHeaders } from "../Constants";
|
||||||
|
import { Offer } from "../../Contracts/DataModels";
|
||||||
|
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { parseSDKOfferResponse } from "../OfferUtility";
|
||||||
|
import { readOffers } from "./readOffers";
|
||||||
|
|
||||||
|
export const readOfferWithSDK = async (offerId: string, resourceId: string): Promise<Offer> => {
|
||||||
|
if (!offerId) {
|
||||||
|
const offers = await readOffers();
|
||||||
|
const offer = offers.find(offer => offer.resource === resourceId);
|
||||||
|
|
||||||
|
if (!offer) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
offerId = offer.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: RequestOptions = {
|
||||||
|
initialHeaders: {
|
||||||
|
[HttpHeaders.populateCollectionThroughputInfo]: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const response = await client()
|
||||||
|
.offer(offerId)
|
||||||
|
.read(options);
|
||||||
|
|
||||||
|
return parseSDKOfferResponse(response);
|
||||||
|
};
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Offer } from "../../Contracts/DataModels";
|
import { SDKOfferDefinition } from "../../Contracts/DataModels";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError, getErrorMessage } from "../ErrorHandlingUtils";
|
||||||
|
|
||||||
export const readOffers = async (): Promise<Offer[]> => {
|
export const readOffers = async (): Promise<SDKOfferDefinition[]> => {
|
||||||
const clearMessage = logConsoleProgress(`Querying offers`);
|
const clearMessage = logConsoleProgress(`Querying offers`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -13,11 +13,11 @@ export const readOffers = async (): Promise<Offer[]> => {
|
|||||||
return response?.resources;
|
return response?.resources;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// This should be removed when we can correctly identify if an account is serverless when connected using connection string too.
|
// This should be removed when we can correctly identify if an account is serverless when connected using connection string too.
|
||||||
if (error.message.includes("Reading or replacing offers is not supported for serverless accounts")) {
|
if (getErrorMessage(error)?.includes("Reading or replacing offers is not supported for serverless accounts")) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
handleError(error, `Error while querying offers`, "ReadOffers");
|
handleError(error, "ReadOffers", `Error while querying offers`);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export async function readStoredProcedures(
|
|||||||
.fetchAll();
|
.fetchAll();
|
||||||
return response?.resources;
|
return response?.resources;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Failed to query stored procedures for container ${collectionId}`, "ReadStoredProcedures");
|
handleError(error, "ReadStoredProcedures", `Failed to query stored procedures for container ${collectionId}`);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export async function readTriggers(
|
|||||||
.fetchAll();
|
.fetchAll();
|
||||||
return response?.resources;
|
return response?.resources;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Failed to query triggers for container ${collectionId}`, "ReadTriggers");
|
handleError(error, "ReadTriggers", `Failed to query triggers for container ${collectionId}`);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ export async function readUserDefinedFunctions(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(
|
handleError(
|
||||||
error,
|
error,
|
||||||
`Failed to query user defined functions for container ${collectionId}`,
|
"ReadUserDefinedFunctions",
|
||||||
"ReadUserDefinedFunctions"
|
`Failed to query user defined functions for container ${collectionId}`
|
||||||
);
|
);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import * as Constants from "../Constants";
|
|
||||||
import { sendMessage } from "../MessageHandler";
|
|
||||||
import { MessageTypes } from "../../Contracts/ExplorerContracts";
|
|
||||||
|
|
||||||
export interface CosmosError {
|
|
||||||
code: number;
|
|
||||||
message?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sendNotificationForError(error: CosmosError): void {
|
|
||||||
if (error && error.code === Constants.HttpStatusCodes.Forbidden) {
|
|
||||||
if (error.message && error.message.toLowerCase().indexOf("sharedoffer is disabled for your account") > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sendMessage({
|
|
||||||
type: MessageTypes.ForbiddenError,
|
|
||||||
reason: error && error.message ? error.message : error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -59,7 +59,7 @@ export async function updateCollection(
|
|||||||
logConsoleInfo(`Successfully updated container ${collectionId}`);
|
logConsoleInfo(`Successfully updated container ${collectionId}`);
|
||||||
return collection;
|
return collection;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Failed to update container ${collectionId}`, "UpdateCollection");
|
handleError(error, "UpdateCollection", `Failed to update container ${collectionId}`);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
import { HttpHeaders } from "../Constants";
|
import { HttpHeaders } from "../Constants";
|
||||||
import { Offer, UpdateOfferParams } from "../../Contracts/DataModels";
|
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||||
import { OfferDefinition } from "@azure/cosmos";
|
import { OfferDefinition } from "@azure/cosmos";
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||||
import { ThroughputSettingsUpdateParameters } from "../../Utils/arm/generatedClients/2020-04-01/types";
|
import { ThroughputSettingsUpdateParameters } from "../../Utils/arm/generatedClients/2020-04-01/types";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import { parseSDKOfferResponse } from "../OfferUtility";
|
||||||
import { readCollectionOffer } from "./readCollectionOffer";
|
import { readCollectionOffer } from "./readCollectionOffer";
|
||||||
import { readDatabaseOffer } from "./readDatabaseOffer";
|
import { readDatabaseOffer } from "./readDatabaseOffer";
|
||||||
import {
|
import {
|
||||||
@@ -72,7 +73,7 @@ export const updateOffer = async (params: UpdateOfferParams): Promise<Offer> =>
|
|||||||
logConsoleInfo(`Successfully updated offer for ${offerResourceText}`);
|
logConsoleInfo(`Successfully updated offer for ${offerResourceText}`);
|
||||||
return updatedOffer;
|
return updatedOffer;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Error updating offer for ${offerResourceText}`, "UpdateCollection");
|
handleError(error, "UpdateCollection", `Error updating offer for ${offerResourceText}`);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
@@ -373,21 +374,21 @@ const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpd
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> => {
|
const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> => {
|
||||||
const currentOffer = params.currentOffer;
|
const sdkOfferDefinition = params.currentOffer.offerDefinition;
|
||||||
const newOffer: Offer = {
|
const newOffer: SDKOfferDefinition = {
|
||||||
content: {
|
content: {
|
||||||
offerThroughput: undefined,
|
offerThroughput: undefined,
|
||||||
offerIsRUPerMinuteThroughputEnabled: false
|
offerIsRUPerMinuteThroughputEnabled: false
|
||||||
},
|
},
|
||||||
_etag: undefined,
|
_etag: undefined,
|
||||||
_ts: undefined,
|
_ts: undefined,
|
||||||
_rid: currentOffer._rid,
|
_rid: sdkOfferDefinition._rid,
|
||||||
_self: currentOffer._self,
|
_self: sdkOfferDefinition._self,
|
||||||
id: currentOffer.id,
|
id: sdkOfferDefinition.id,
|
||||||
offerResourceId: currentOffer.offerResourceId,
|
offerResourceId: sdkOfferDefinition.offerResourceId,
|
||||||
offerVersion: currentOffer.offerVersion,
|
offerVersion: sdkOfferDefinition.offerVersion,
|
||||||
offerType: currentOffer.offerType,
|
offerType: sdkOfferDefinition.offerType,
|
||||||
resource: currentOffer.resource
|
resource: sdkOfferDefinition.resource
|
||||||
};
|
};
|
||||||
|
|
||||||
if (params.autopilotThroughput) {
|
if (params.autopilotThroughput) {
|
||||||
@@ -415,5 +416,6 @@ const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> =>
|
|||||||
.offer(params.currentOffer.id)
|
.offer(params.currentOffer.id)
|
||||||
// TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660)
|
// TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660)
|
||||||
.replace((newOffer as unknown) as OfferDefinition, options);
|
.replace((newOffer as unknown) as OfferDefinition, options);
|
||||||
return sdkResponse?.resource;
|
|
||||||
|
return parseSDKOfferResponse(sdkResponse);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { updateOfferThroughputBeyondLimit } from "./updateOfferThroughputBeyondLimit";
|
|
||||||
|
|
||||||
describe("updateOfferThroughputBeyondLimit", () => {
|
|
||||||
it("should call fetch", async () => {
|
|
||||||
window.fetch = jest.fn(() => {
|
|
||||||
return {
|
|
||||||
ok: true
|
|
||||||
};
|
|
||||||
});
|
|
||||||
window.dataExplorer = {
|
|
||||||
logConsoleData: jest.fn(),
|
|
||||||
deleteInProgressConsoleDataWithId: jest.fn()
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
} as any;
|
|
||||||
await updateOfferThroughputBeyondLimit({
|
|
||||||
subscriptionId: "foo",
|
|
||||||
resourceGroup: "foo",
|
|
||||||
databaseAccountName: "foo",
|
|
||||||
databaseName: "foo",
|
|
||||||
throughput: 1000000000,
|
|
||||||
offerIsRUPerMinuteThroughputEnabled: false
|
|
||||||
});
|
|
||||||
expect(window.fetch).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { Platform, configContext } from "../../ConfigContext";
|
|
||||||
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
|
||||||
import { AutoPilotOfferSettings } from "../../Contracts/DataModels";
|
|
||||||
import { logConsoleProgress, logConsoleInfo, logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
|
||||||
import { HttpHeaders } from "../Constants";
|
|
||||||
|
|
||||||
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();
|
|
||||||
logConsoleError(`Failed to request an increase in throughput for ${request.throughput}: ${error.message}`);
|
|
||||||
clearMessage();
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
@@ -64,7 +64,7 @@ export async function updateStoredProcedure(
|
|||||||
.replace(storedProcedure);
|
.replace(storedProcedure);
|
||||||
return response?.resource;
|
return response?.resource;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Error while updating stored procedure ${storedProcedure.id}`, "UpdateStoredProcedure");
|
handleError(error, "UpdateStoredProcedure", `Error while updating stored procedure ${storedProcedure.id}`);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export async function updateTrigger(
|
|||||||
.replace(trigger);
|
.replace(trigger);
|
||||||
return response?.resource;
|
return response?.resource;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Error while updating trigger ${trigger.id}`, "UpdateTrigger");
|
handleError(error, "UpdateTrigger", `Error while updating trigger ${trigger.id}`);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -66,8 +66,8 @@ export async function updateUserDefinedFunction(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(
|
handleError(
|
||||||
error,
|
error,
|
||||||
`Error while updating user defined function ${userDefinedFunction.id}`,
|
"UpdateUserupdateUserDefinedFunction",
|
||||||
"UpdateUserupdateUserDefinedFunction"
|
`Error while updating user defined function ${userDefinedFunction.id}`
|
||||||
);
|
);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -88,6 +88,38 @@ export interface Resource {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IType {
|
||||||
|
name: string;
|
||||||
|
code: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDataField {
|
||||||
|
dataType: IType;
|
||||||
|
hasNulls: boolean;
|
||||||
|
isArray: boolean;
|
||||||
|
schemaType: IType;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
maxRepetitionLevel: number;
|
||||||
|
maxDefinitionLevel: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISchema {
|
||||||
|
id: string;
|
||||||
|
accountName: string;
|
||||||
|
resource: string;
|
||||||
|
fields: IDataField[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISchemaRequest {
|
||||||
|
id: string;
|
||||||
|
subscriptionId: string;
|
||||||
|
resourceGroup: string;
|
||||||
|
accountName: string;
|
||||||
|
resource: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Collection extends Resource {
|
export interface Collection extends Resource {
|
||||||
defaultTtl?: number;
|
defaultTtl?: number;
|
||||||
indexingPolicy?: IndexingPolicy;
|
indexingPolicy?: IndexingPolicy;
|
||||||
@@ -98,6 +130,8 @@ export interface Collection extends Resource {
|
|||||||
changeFeedPolicy?: ChangeFeedPolicy;
|
changeFeedPolicy?: ChangeFeedPolicy;
|
||||||
analyticalStorageTtl?: number;
|
analyticalStorageTtl?: number;
|
||||||
geospatialConfig?: GeospatialConfig;
|
geospatialConfig?: GeospatialConfig;
|
||||||
|
schema?: ISchema;
|
||||||
|
requestSchema?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Database extends Resource {
|
export interface Database extends Resource {
|
||||||
@@ -174,12 +208,21 @@ export interface QueryMetrics {
|
|||||||
vmExecutionTime: any;
|
vmExecutionTime: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Offer extends Resource {
|
export interface Offer {
|
||||||
|
id: string;
|
||||||
|
autoscaleMaxThroughput: number;
|
||||||
|
manualThroughput: number;
|
||||||
|
minimumThroughput: number;
|
||||||
|
offerDefinition?: SDKOfferDefinition;
|
||||||
|
headers?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SDKOfferDefinition extends Resource {
|
||||||
offerVersion?: string;
|
offerVersion?: string;
|
||||||
offerType?: string;
|
offerType?: string;
|
||||||
content?: {
|
content?: {
|
||||||
offerThroughput: number;
|
offerThroughput: number;
|
||||||
offerIsRUPerMinuteThroughputEnabled: boolean;
|
offerIsRUPerMinuteThroughputEnabled?: boolean;
|
||||||
collectionThroughputInfo?: OfferThroughputInfo;
|
collectionThroughputInfo?: OfferThroughputInfo;
|
||||||
offerAutopilotSettings?: AutoPilotOfferSettings;
|
offerAutopilotSettings?: AutoPilotOfferSettings;
|
||||||
};
|
};
|
||||||
@@ -187,10 +230,6 @@ export interface Offer extends Resource {
|
|||||||
offerResourceId?: string;
|
offerResourceId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OfferWithHeaders extends Offer {
|
|
||||||
headers: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CollectionQuotaInfo {
|
export interface CollectionQuotaInfo {
|
||||||
storedProcedures: number;
|
storedProcedures: number;
|
||||||
triggers: number;
|
triggers: number;
|
||||||
@@ -216,18 +255,6 @@ export interface UniqueKey {
|
|||||||
paths: string[];
|
paths: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returned by DocumentDb client proxy
|
|
||||||
// Inner errors in BackendErrorDataModel when error is in SQL syntax
|
|
||||||
export interface ErrorDataModel {
|
|
||||||
message: string;
|
|
||||||
severity?: string;
|
|
||||||
location?: {
|
|
||||||
start: string;
|
|
||||||
end: string;
|
|
||||||
};
|
|
||||||
code?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateDatabaseAndCollectionRequest {
|
export interface CreateDatabaseAndCollectionRequest {
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
collectionId: string;
|
collectionId: string;
|
||||||
@@ -239,21 +266,12 @@ export interface CreateDatabaseAndCollectionRequest {
|
|||||||
uniqueKeyPolicy?: UniqueKeyPolicy;
|
uniqueKeyPolicy?: UniqueKeyPolicy;
|
||||||
autoPilot?: AutoPilotCreationSettings;
|
autoPilot?: AutoPilotCreationSettings;
|
||||||
analyticalStorageTtl?: number;
|
analyticalStorageTtl?: number;
|
||||||
hasAutoPilotV2FeatureFlag?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AutoPilotCreationSettings {
|
export interface AutoPilotCreationSettings {
|
||||||
autopilotTier?: AutopilotTier;
|
|
||||||
maxThroughput?: number;
|
maxThroughput?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AutopilotTier {
|
|
||||||
Tier1 = 1,
|
|
||||||
Tier2 = 2,
|
|
||||||
Tier3 = 3,
|
|
||||||
Tier4 = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Query {
|
export interface Query {
|
||||||
id: string;
|
id: string;
|
||||||
resourceId: string;
|
resourceId: string;
|
||||||
@@ -262,9 +280,7 @@ export interface Query {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AutoPilotOfferSettings {
|
export interface AutoPilotOfferSettings {
|
||||||
tier?: AutopilotTier;
|
|
||||||
maximumTierThroughput?: number;
|
maximumTierThroughput?: number;
|
||||||
targetTier?: AutopilotTier;
|
|
||||||
maxThroughput?: number;
|
maxThroughput?: number;
|
||||||
targetMaxThroughput?: number;
|
targetMaxThroughput?: number;
|
||||||
}
|
}
|
||||||
@@ -491,7 +507,6 @@ export interface MongoParameters extends RpParameters {
|
|||||||
rid?: string;
|
rid?: string;
|
||||||
rtype?: string;
|
rtype?: string;
|
||||||
isAutoPilot?: Boolean;
|
isAutoPilot?: Boolean;
|
||||||
autoPilotTier?: string;
|
|
||||||
autoPilotThroughput?: string;
|
autoPilotThroughput?: string;
|
||||||
analyticalStorageTtl?: number;
|
analyticalStorageTtl?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export interface LogEntry {
|
|||||||
/**
|
/**
|
||||||
* The message code.
|
* The message code.
|
||||||
*/
|
*/
|
||||||
code?: number;
|
code?: number | string;
|
||||||
/**
|
/**
|
||||||
* Any additional data to be logged.
|
* Any additional data to be logged.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export enum MessageTypes {
|
|||||||
GetArcadiaToken,
|
GetArcadiaToken,
|
||||||
CreateWorkspace,
|
CreateWorkspace,
|
||||||
CreateSparkPool,
|
CreateSparkPool,
|
||||||
RefreshDatabaseAccount
|
RefreshDatabaseAccount,
|
||||||
|
InitTestExplorer
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Versions, ActionContracts, Diagnostics };
|
export { Versions, ActionContracts, Diagnostics };
|
||||||
|
|||||||
7
src/Contracts/SubscriptionType.ts
Normal file
7
src/Contracts/SubscriptionType.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export enum SubscriptionType {
|
||||||
|
Benefits,
|
||||||
|
EA,
|
||||||
|
Free,
|
||||||
|
Internal,
|
||||||
|
PAYG
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import Trigger from "../Explorer/Tree/Trigger";
|
|||||||
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
|
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
|
||||||
import { UploadDetails } from "../workers/upload/definitions";
|
import { UploadDetails } from "../workers/upload/definitions";
|
||||||
import * as DataModels from "./DataModels";
|
import * as DataModels from "./DataModels";
|
||||||
|
import { SubscriptionType } from "./SubscriptionType";
|
||||||
|
|
||||||
export interface TokenProvider {
|
export interface TokenProvider {
|
||||||
getAuthHeader(): Promise<Headers>;
|
getAuthHeader(): Promise<Headers>;
|
||||||
@@ -115,6 +116,8 @@ export interface CollectionBase extends TreeNode {
|
|||||||
export interface Collection extends CollectionBase {
|
export interface Collection extends CollectionBase {
|
||||||
defaultTtl: ko.Observable<number>;
|
defaultTtl: ko.Observable<number>;
|
||||||
analyticalStorageTtl: ko.Observable<number>;
|
analyticalStorageTtl: ko.Observable<number>;
|
||||||
|
schema?: DataModels.ISchema;
|
||||||
|
requestSchema?: () => void;
|
||||||
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
||||||
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
||||||
quotaInfo: ko.Observable<DataModels.CollectionQuotaInfo>;
|
quotaInfo: ko.Observable<DataModels.CollectionQuotaInfo>;
|
||||||
@@ -358,6 +361,7 @@ export enum CollectionTabKind {
|
|||||||
SparkMasterTab = 16,
|
SparkMasterTab = 16,
|
||||||
Gallery = 17,
|
Gallery = 17,
|
||||||
NotebookViewer = 18,
|
NotebookViewer = 18,
|
||||||
|
Schema = 19,
|
||||||
SettingsV2 = 19
|
SettingsV2 = 19
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,14 +416,6 @@ export interface ThroughputDefaults {
|
|||||||
shared: number;
|
shared: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SubscriptionType {
|
|
||||||
Benefits,
|
|
||||||
EA,
|
|
||||||
Free,
|
|
||||||
Internal,
|
|
||||||
PAYG
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MonacoEditorSettings {
|
export class MonacoEditorSettings {
|
||||||
public readonly language: string;
|
public readonly language: string;
|
||||||
public readonly readOnly: boolean;
|
public readonly readOnly: boolean;
|
||||||
|
|||||||
@@ -44,10 +44,6 @@ describe("Component Registerer", () => {
|
|||||||
expect(ko.components.isRegistered("user-defined-function-tab")).toBe(true);
|
expect(ko.components.isRegistered("user-defined-function-tab")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should register settings-tab component", () => {
|
|
||||||
expect(ko.components.isRegistered("settings-tab")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should register settings-tab-v2 component", () => {
|
it("should register settings-tab-v2 component", () => {
|
||||||
expect(ko.components.isRegistered("settings-tab-v2")).toBe(true);
|
expect(ko.components.isRegistered("settings-tab-v2")).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -123,8 +119,4 @@ describe("Component Registerer", () => {
|
|||||||
it("should register dynamic-list component", () => {
|
it("should register dynamic-list component", () => {
|
||||||
expect(ko.components.isRegistered("dynamic-list")).toBe(true);
|
expect(ko.components.isRegistered("dynamic-list")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should register throughput-input component", () => {
|
|
||||||
expect(ko.components.isRegistered("throughput-input")).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { InputTypeaheadComponent } from "./Controls/InputTypeahead/InputTypeahea
|
|||||||
import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent";
|
import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent";
|
||||||
import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponent";
|
import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponent";
|
||||||
import { TabsManagerKOComponent } from "./Tabs/TabsManager";
|
import { TabsManagerKOComponent } from "./Tabs/TabsManager";
|
||||||
import { ThroughputInputComponent } from "./Controls/ThroughputInput/ThroughputInputComponent";
|
|
||||||
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
|
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
|
||||||
|
|
||||||
ko.components.register("input-typeahead", new InputTypeaheadComponent());
|
ko.components.register("input-typeahead", new InputTypeaheadComponent());
|
||||||
@@ -23,7 +22,6 @@ ko.components.register("editor", new EditorComponent());
|
|||||||
ko.components.register("json-editor", new JsonEditorComponent());
|
ko.components.register("json-editor", new JsonEditorComponent());
|
||||||
ko.components.register("diff-editor", new DiffEditorComponent());
|
ko.components.register("diff-editor", new DiffEditorComponent());
|
||||||
ko.components.register("dynamic-list", DynamicListComponent);
|
ko.components.register("dynamic-list", DynamicListComponent);
|
||||||
ko.components.register("throughput-input", ThroughputInputComponent);
|
|
||||||
ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3);
|
ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3);
|
||||||
ko.components.register("tabs-manager", TabsManagerKOComponent());
|
ko.components.register("tabs-manager", TabsManagerKOComponent());
|
||||||
|
|
||||||
@@ -33,7 +31,6 @@ ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTa
|
|||||||
ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab());
|
ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab());
|
||||||
ko.components.register("trigger-tab", new TabComponents.TriggerTab());
|
ko.components.register("trigger-tab", new TabComponents.TriggerTab());
|
||||||
ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab());
|
ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab());
|
||||||
ko.components.register("settings-tab", new TabComponents.SettingsTab());
|
|
||||||
ko.components.register("settings-tab-v2", new TabComponents.SettingsTabV2());
|
ko.components.register("settings-tab-v2", new TabComponents.SettingsTabV2());
|
||||||
ko.components.register("query-tab", new TabComponents.QueryTab());
|
ko.components.register("query-tab", new TabComponents.QueryTab());
|
||||||
ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab());
|
ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab());
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ArcadiaWorkspace, SparkPool } from "../../../Contracts/DataModels";
|
import { ArcadiaWorkspace, SparkPool } from "../../../Contracts/DataModels";
|
||||||
import { DefaultButton, IButtonStyles } from "office-ui-fabric-react/lib/Button";
|
import { DefaultButton, IButtonStyles } from "office-ui-fabric-react/lib/Button";
|
||||||
import {
|
import { IContextualMenuItem, IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu";
|
||||||
IContextualMenuItem,
|
|
||||||
IContextualMenuProps,
|
|
||||||
ContextualMenuItemType
|
|
||||||
} from "office-ui-fabric-react/lib/ContextualMenu";
|
|
||||||
import * as Logger from "../../../Common/Logger";
|
import * as Logger from "../../../Common/Logger";
|
||||||
|
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
|
||||||
|
|
||||||
export interface ArcadiaMenuPickerProps {
|
export interface ArcadiaMenuPickerProps {
|
||||||
selectText?: string;
|
selectText?: string;
|
||||||
@@ -47,7 +44,7 @@ export class ArcadiaMenuPicker extends React.Component<ArcadiaMenuPickerProps, A
|
|||||||
selectedSparkPool: item.text
|
selectedSparkPool: item.text
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.logError(error, "ArcadiaMenuPicker/_onSparkPoolClicked");
|
Logger.logError(getErrorMessage(error), "ArcadiaMenuPicker/_onSparkPoolClicked");
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,12 +4,10 @@
|
|||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import * as Logger from "../../../Common/Logger";
|
|
||||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
|
||||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
|
||||||
import { StringUtils } from "../../../Utils/StringUtils";
|
import { StringUtils } from "../../../Utils/StringUtils";
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
import { TerminalQueryParams } from "../../../Common/Constants";
|
import { TerminalQueryParams } from "../../../Common/Constants";
|
||||||
|
import { handleError } from "../../../Common/ErrorHandlingUtils";
|
||||||
|
|
||||||
export interface NotebookTerminalComponentProps {
|
export interface NotebookTerminalComponentProps {
|
||||||
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
||||||
@@ -41,6 +39,10 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
|||||||
params.set(TerminalQueryParams.TerminalEndpoint, terminalEndpoint);
|
params.set(TerminalQueryParams.TerminalEndpoint, terminalEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
params.set("account","contoso-retail-mongodb");
|
||||||
|
params.set("port","10255");
|
||||||
|
//tofill
|
||||||
|
params.set("token","");
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,9 +73,10 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
|||||||
params: Map<string, string>
|
params: Map<string, string>
|
||||||
): string {
|
): string {
|
||||||
if (!serverInfo.notebookServerEndpoint) {
|
if (!serverInfo.notebookServerEndpoint) {
|
||||||
const error = "Notebook server endpoint not defined. Terminal will fail to connect to jupyter server.";
|
handleError(
|
||||||
Logger.logError(error, "NotebookTerminalComponent/createNotebookAppSrc");
|
"Notebook server endpoint not defined. Terminal will fail to connect to jupyter server.",
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
"NotebookTerminalComponent/createNotebookAppSrc"
|
||||||
|
);
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { JunoClient } from "../../../Juno/JunoClient";
|
import { JunoClient } from "../../../Juno/JunoClient";
|
||||||
import { HttpStatusCodes, CodeOfConductEndpoints } from "../../../Common/Constants";
|
import { HttpStatusCodes, CodeOfConductEndpoints } from "../../../Common/Constants";
|
||||||
import * as Logger from "../../../Common/Logger";
|
|
||||||
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
|
|
||||||
import { Stack, Text, Checkbox, PrimaryButton, Link } from "office-ui-fabric-react";
|
import { Stack, Text, Checkbox, PrimaryButton, Link } from "office-ui-fabric-react";
|
||||||
|
import { handleError } from "../../../Common/ErrorHandlingUtils";
|
||||||
|
|
||||||
export interface CodeOfConductComponentProps {
|
export interface CodeOfConductComponentProps {
|
||||||
junoClient: JunoClient;
|
junoClient: JunoClient;
|
||||||
@@ -45,9 +44,7 @@ export class CodeOfConductComponent extends React.Component<CodeOfConductCompone
|
|||||||
|
|
||||||
this.props.onAcceptCodeOfConduct(response.data);
|
this.props.onAcceptCodeOfConduct(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = `Failed to accept code of conduct: ${error}`;
|
handleError(error, "CodeOfConductComponent/acceptCodeOfConduct", "Failed to accept code of conduct");
|
||||||
Logger.logError(message, "CodeOfConductComponent/acceptCodeOfConduct");
|
|
||||||
logConsoleError(message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,11 +16,8 @@ import {
|
|||||||
Text
|
Text
|
||||||
} from "office-ui-fabric-react";
|
} from "office-ui-fabric-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as Logger from "../../../Common/Logger";
|
|
||||||
import { IGalleryItem, JunoClient, IJunoResponse, IPublicGalleryData } from "../../../Juno/JunoClient";
|
import { IGalleryItem, JunoClient, IJunoResponse, IPublicGalleryData } from "../../../Juno/JunoClient";
|
||||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
|
||||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
|
||||||
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
|
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
|
||||||
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
|
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
|
||||||
import "./GalleryViewerComponent.less";
|
import "./GalleryViewerComponent.less";
|
||||||
@@ -28,6 +25,7 @@ import { HttpStatusCodes } from "../../../Common/Constants";
|
|||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { CodeOfConductComponent } from "./CodeOfConductComponent";
|
import { CodeOfConductComponent } from "./CodeOfConductComponent";
|
||||||
import { InfoComponent } from "./InfoComponent/InfoComponent";
|
import { InfoComponent } from "./InfoComponent/InfoComponent";
|
||||||
|
import { handleError } from "../../../Common/ErrorHandlingUtils";
|
||||||
|
|
||||||
export interface GalleryViewerComponentProps {
|
export interface GalleryViewerComponentProps {
|
||||||
container?: Explorer;
|
container?: Explorer;
|
||||||
@@ -354,9 +352,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
|
|
||||||
this.sampleNotebooks = response.data;
|
this.sampleNotebooks = response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = `Failed to load sample notebooks: ${error}`;
|
handleError(error, "GalleryViewerComponent/loadSampleNotebooks", "Failed to load sample notebooks");
|
||||||
Logger.logError(message, "GalleryViewerComponent/loadSampleNotebooks");
|
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,9 +378,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
throw new Error(`Received HTTP ${response.status} when loading public notebooks`);
|
throw new Error(`Received HTTP ${response.status} when loading public notebooks`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = `Failed to load public notebooks: ${error}`;
|
handleError(error, "GalleryViewerComponent/loadPublicNotebooks", "Failed to load public notebooks");
|
||||||
Logger.logError(message, "GalleryViewerComponent/loadPublicNotebooks");
|
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,9 +398,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
|
|
||||||
this.favoriteNotebooks = response.data;
|
this.favoriteNotebooks = response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = `Failed to load favorite notebooks: ${error}`;
|
handleError(error, "GalleryViewerComponent/loadFavoriteNotebooks", "Failed to load favorite notebooks");
|
||||||
Logger.logError(message, "GalleryViewerComponent/loadFavoriteNotebooks");
|
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,9 +424,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
|
|
||||||
this.publishedNotebooks = response.data;
|
this.publishedNotebooks = response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = `Failed to load published notebooks: ${error}`;
|
handleError(error, "GalleryViewerComponent/loadPublishedNotebooks", "Failed to load published notebooks");
|
||||||
Logger.logError(message, "GalleryViewerComponent/loadPublishedNotebooks");
|
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import Explorer from "../../Explorer";
|
|||||||
import { NotebookV4 } from "@nteract/commutable/lib/v4";
|
import { NotebookV4 } from "@nteract/commutable/lib/v4";
|
||||||
import { SessionStorageUtility } from "../../../Shared/StorageUtility";
|
import { SessionStorageUtility } from "../../../Shared/StorageUtility";
|
||||||
import { DialogHost } from "../../../Utils/GalleryUtils";
|
import { DialogHost } from "../../../Utils/GalleryUtils";
|
||||||
|
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
|
||||||
|
|
||||||
export interface NotebookViewerComponentProps {
|
export interface NotebookViewerComponentProps {
|
||||||
container?: Explorer;
|
container?: Explorer;
|
||||||
@@ -100,9 +101,7 @@ export class NotebookViewerComponent extends React.Component<NotebookViewerCompo
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.setState({ showProgressBar: false });
|
this.setState({ showProgressBar: false });
|
||||||
const message = `Failed to load notebook content: ${error}`;
|
handleError(error, "NotebookViewerComponent/loadNotebookContent", "Failed to load notebook content");
|
||||||
Logger.logError(message, "NotebookViewerComponent/loadNotebookContent");
|
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcesso
|
|||||||
|
|
||||||
import SaveQueryBannerIcon from "../../../../images/save_query_banner.png";
|
import SaveQueryBannerIcon from "../../../../images/save_query_banner.png";
|
||||||
import { QueriesClient } from "../../../Common/QueriesClient";
|
import { QueriesClient } from "../../../Common/QueriesClient";
|
||||||
|
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||||
|
|
||||||
export interface QueriesGridComponentProps {
|
export interface QueriesGridComponentProps {
|
||||||
queriesClient: QueriesClient;
|
queriesClient: QueriesClient;
|
||||||
@@ -244,7 +245,9 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
|
|||||||
databaseAccountName: container && container.databaseAccount().name,
|
databaseAccountName: container && container.databaseAccount().name,
|
||||||
defaultExperience: container && container.defaultExperience(),
|
defaultExperience: container && container.defaultExperience(),
|
||||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||||
paneTitle: container && container.browseQueriesPane.title()
|
paneTitle: container && container.browseQueriesPane.title(),
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
errorStack: getErrorStack(error)
|
||||||
},
|
},
|
||||||
startKey
|
startKey
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import * as DataModels from "../../../Contracts/DataModels";
|
|||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
import { TtlType, isDirty } from "./SettingsUtils";
|
import { TtlType, isDirty } from "./SettingsUtils";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
jest.mock("../../../Common/dataAccess/readMongoDBCollection", () => ({
|
jest.mock("../../../Common/dataAccess/getIndexTransformationProgress", () => ({
|
||||||
getMongoDBCollectionIndexTransformationProgress: jest.fn().mockReturnValue(undefined)
|
getIndexTransformationProgress: jest.fn().mockReturnValue(undefined)
|
||||||
}));
|
}));
|
||||||
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
|
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
|
||||||
jest.mock("../../../Common/dataAccess/updateCollection", () => ({
|
jest.mock("../../../Common/dataAccess/updateCollection", () => ({
|
||||||
@@ -89,12 +89,11 @@ describe("SettingsComponent", () => {
|
|||||||
it("auto pilot helper functions pass on correct value", () => {
|
it("auto pilot helper functions pass on correct value", () => {
|
||||||
const newCollection = { ...collection };
|
const newCollection = { ...collection };
|
||||||
newCollection.offer = ko.observable<DataModels.Offer>({
|
newCollection.offer = ko.observable<DataModels.Offer>({
|
||||||
content: {
|
autoscaleMaxThroughput: 10000,
|
||||||
offerAutopilotSettings: {
|
manualThroughput: undefined,
|
||||||
maxThroughput: 10000
|
minimumThroughput: 400,
|
||||||
}
|
id: "test"
|
||||||
}
|
});
|
||||||
} as DataModels.Offer);
|
|
||||||
|
|
||||||
const props = { ...baseProps };
|
const props = { ...baseProps };
|
||||||
props.settingsTab.collection = newCollection;
|
props.settingsTab.collection = newCollection;
|
||||||
@@ -187,21 +186,6 @@ describe("SettingsComponent", () => {
|
|||||||
expect(settingsComponentInstance.hasConflictResolution()).toEqual(true);
|
expect(settingsComponentInstance.hasConflictResolution()).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("isOfferReplacePending", () => {
|
|
||||||
let settingsComponentInstance = new SettingsComponent(baseProps);
|
|
||||||
expect(settingsComponentInstance.isOfferReplacePending()).toEqual(undefined);
|
|
||||||
|
|
||||||
const newCollection = { ...collection };
|
|
||||||
newCollection.offer = ko.observable({
|
|
||||||
headers: { "x-ms-offer-replace-pending": true }
|
|
||||||
} as DataModels.OfferWithHeaders);
|
|
||||||
const props = { ...baseProps };
|
|
||||||
props.settingsTab.collection = newCollection;
|
|
||||||
|
|
||||||
settingsComponentInstance = new SettingsComponent(props);
|
|
||||||
expect(settingsComponentInstance.isOfferReplacePending()).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("save calls updateCollection, updateMongoDBCollectionThroughRP and updateOffer", async () => {
|
it("save calls updateCollection, updateMongoDBCollectionThroughRP and updateOffer", async () => {
|
||||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
||||||
wrapper.setState({ isSubSettingsSaveable: true, isScaleSaveable: true, isMongoIndexingPolicySaveable: true });
|
wrapper.setState({ isSubSettingsSaveable: true, isScaleSaveable: true, isMongoIndexingPolicySaveable: true });
|
||||||
|
|||||||
@@ -2,28 +2,23 @@ import * as React from "react";
|
|||||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import * as SharedConstants from "../../../Shared/Constants";
|
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import DiscardIcon from "../../../../images/discard.svg";
|
import DiscardIcon from "../../../../images/discard.svg";
|
||||||
import SaveIcon from "../../../../images/save-cosmos.svg";
|
import SaveIcon from "../../../../images/save-cosmos.svg";
|
||||||
import { traceStart, traceFailure, traceSuccess, trace } from "../../../Shared/Telemetry/TelemetryProcessor";
|
import { traceStart, traceFailure, traceSuccess, trace } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||||
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
|
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { userContext } from "../../../UserContext";
|
|
||||||
import { updateOfferThroughputBeyondLimit } from "../../../Common/dataAccess/updateOfferThroughputBeyondLimit";
|
|
||||||
import SettingsTab from "../../Tabs/SettingsTabV2";
|
import SettingsTab from "../../Tabs/SettingsTabV2";
|
||||||
import { throughputUnit } from "./SettingsRenderUtils";
|
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
|
||||||
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
|
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
|
||||||
import {
|
import {
|
||||||
MongoIndexingPolicyComponent,
|
MongoIndexingPolicyComponent,
|
||||||
MongoIndexingPolicyComponentProps
|
MongoIndexingPolicyComponentProps
|
||||||
} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
|
} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
|
||||||
import {
|
import {
|
||||||
getMaxRUs,
|
|
||||||
hasDatabaseSharedThroughput,
|
hasDatabaseSharedThroughput,
|
||||||
GeospatialConfigType,
|
GeospatialConfigType,
|
||||||
TtlType,
|
TtlType,
|
||||||
@@ -46,10 +41,10 @@ import { Pivot, PivotItem, IPivotProps, IPivotItemProps } from "office-ui-fabric
|
|||||||
import "./SettingsComponent.less";
|
import "./SettingsComponent.less";
|
||||||
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
|
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
|
||||||
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
|
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
|
||||||
import {
|
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
|
||||||
getMongoDBCollectionIndexTransformationProgress,
|
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
|
||||||
readMongoDBCollectionThroughRP
|
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||||
} from "../../../Common/dataAccess/readMongoDBCollection";
|
import { isEmpty } from "underscore";
|
||||||
|
|
||||||
interface SettingsV2TabInfo {
|
interface SettingsV2TabInfo {
|
||||||
tab: SettingsV2TabTypes;
|
tab: SettingsV2TabTypes;
|
||||||
@@ -125,7 +120,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private container: Explorer;
|
private container: Explorer;
|
||||||
private changeFeedPolicyVisible: boolean;
|
private changeFeedPolicyVisible: boolean;
|
||||||
private isFixedContainer: boolean;
|
private isFixedContainer: boolean;
|
||||||
private autoPilotTiersList: ViewModels.DropdownOption<DataModels.AutopilotTier>[];
|
|
||||||
private shouldShowIndexingPolicyEditor: boolean;
|
private shouldShowIndexingPolicyEditor: boolean;
|
||||||
public mongoDBCollectionResource: MongoDBCollectionResource;
|
public mongoDBCollectionResource: MongoDBCollectionResource;
|
||||||
|
|
||||||
@@ -212,6 +206,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
|
this.refreshIndexTransformationProgress();
|
||||||
this.loadMongoIndexes();
|
this.loadMongoIndexes();
|
||||||
this.setAutoPilotStates();
|
this.setAutoPilotStates();
|
||||||
this.setBaseline();
|
this.setBaseline();
|
||||||
@@ -228,12 +223,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
|
|
||||||
public loadMongoIndexes = async (): Promise<void> => {
|
public loadMongoIndexes = async (): Promise<void> => {
|
||||||
if (
|
if (
|
||||||
this.container.isMongoIndexEditorEnabled() &&
|
|
||||||
this.container.isPreferredApiMongoDB() &&
|
this.container.isPreferredApiMongoDB() &&
|
||||||
|
this.container.isEnableMongoCapabilityPresent() &&
|
||||||
this.container.databaseAccount()
|
this.container.databaseAccount()
|
||||||
) {
|
) {
|
||||||
await this.refreshIndexTransformationProgress();
|
|
||||||
|
|
||||||
this.mongoDBCollectionResource = await readMongoDBCollectionThroughRP(
|
this.mongoDBCollectionResource = await readMongoDBCollectionThroughRP(
|
||||||
this.collection.databaseId,
|
this.collection.databaseId,
|
||||||
this.collection.id()
|
this.collection.id()
|
||||||
@@ -248,10 +241,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
};
|
};
|
||||||
|
|
||||||
public refreshIndexTransformationProgress = async (): Promise<void> => {
|
public refreshIndexTransformationProgress = async (): Promise<void> => {
|
||||||
const currentProgress = await getMongoDBCollectionIndexTransformationProgress(
|
const currentProgress = await getIndexTransformationProgress(this.collection.databaseId, this.collection.id());
|
||||||
this.collection.databaseId,
|
|
||||||
this.collection.id()
|
|
||||||
);
|
|
||||||
this.setState({ indexTransformationProgress: currentProgress });
|
this.setState({ indexTransformationProgress: currentProgress });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -280,19 +270,14 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
};
|
};
|
||||||
|
|
||||||
private setAutoPilotStates = (): void => {
|
private setAutoPilotStates = (): void => {
|
||||||
const offer = this.collection?.offer && this.collection.offer();
|
const autoscaleMaxThroughput = this.collection?.offer()?.autoscaleMaxThroughput;
|
||||||
const offerAutopilotSettings = offer?.content?.offerAutopilotSettings;
|
|
||||||
|
|
||||||
if (
|
if (autoscaleMaxThroughput && AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
|
||||||
offerAutopilotSettings &&
|
|
||||||
offerAutopilotSettings.maxThroughput &&
|
|
||||||
AutoPilotUtils.isValidAutoPilotThroughput(offerAutopilotSettings.maxThroughput)
|
|
||||||
) {
|
|
||||||
this.setState({
|
this.setState({
|
||||||
isAutoPilotSelected: true,
|
isAutoPilotSelected: true,
|
||||||
wasAutopilotOriginallySet: true,
|
wasAutopilotOriginallySet: true,
|
||||||
autoPilotThroughput: offerAutopilotSettings.maxThroughput,
|
autoPilotThroughput: autoscaleMaxThroughput,
|
||||||
autoPilotThroughputBaseline: offerAutopilotSettings.maxThroughput
|
autoPilotThroughputBaseline: autoscaleMaxThroughput
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -310,12 +295,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
!!this.collection.conflictResolutionPolicy();
|
!!this.collection.conflictResolutionPolicy();
|
||||||
|
|
||||||
public isOfferReplacePending = (): boolean => {
|
public isOfferReplacePending = (): boolean => {
|
||||||
const offer = this.collection?.offer && this.collection.offer();
|
return !!this.collection?.offer()?.headers?.[Constants.HttpHeaders.offerReplacePending];
|
||||||
return (
|
|
||||||
offer &&
|
|
||||||
Object.keys(offer).find(value => value === "headers") &&
|
|
||||||
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public onSaveClick = async (): Promise<void> => {
|
public onSaveClick = async (): Promise<void> => {
|
||||||
@@ -351,6 +331,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const wasIndexingPolicyModified = this.state.isIndexingPolicyDirty;
|
||||||
newCollection.defaultTtl = defaultTtl;
|
newCollection.defaultTtl = defaultTtl;
|
||||||
|
|
||||||
newCollection.indexingPolicy = this.state.indexingPolicyContent;
|
newCollection.indexingPolicy = this.state.indexingPolicyContent;
|
||||||
@@ -386,6 +367,11 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.collection.conflictResolutionPolicy(updatedCollection.conflictResolutionPolicy);
|
this.collection.conflictResolutionPolicy(updatedCollection.conflictResolutionPolicy);
|
||||||
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
|
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
|
||||||
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
|
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
|
||||||
|
|
||||||
|
if (wasIndexingPolicyModified) {
|
||||||
|
await this.refreshIndexTransformationProgress();
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
isSubSettingsSaveable: false,
|
isSubSettingsSaveable: false,
|
||||||
isSubSettingsDiscardable: false,
|
isSubSettingsDiscardable: false,
|
||||||
@@ -395,124 +381,86 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.isMongoIndexingPolicySaveable && this.mongoDBCollectionResource) {
|
if (this.state.isMongoIndexingPolicySaveable && this.mongoDBCollectionResource) {
|
||||||
const newMongoIndexes = this.getMongoIndexesToSave();
|
try {
|
||||||
const newMongoCollection: MongoDBCollectionResource = {
|
const newMongoIndexes = this.getMongoIndexesToSave();
|
||||||
...this.mongoDBCollectionResource,
|
const newMongoCollection: MongoDBCollectionResource = {
|
||||||
indexes: newMongoIndexes
|
...this.mongoDBCollectionResource,
|
||||||
};
|
indexes: newMongoIndexes
|
||||||
this.mongoDBCollectionResource = await updateMongoDBCollectionThroughRP(
|
};
|
||||||
this.collection.databaseId,
|
|
||||||
this.collection.id(),
|
|
||||||
newMongoCollection
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.refreshIndexTransformationProgress();
|
this.mongoDBCollectionResource = await updateMongoDBCollectionThroughRP(
|
||||||
this.setState({
|
this.collection.databaseId,
|
||||||
isMongoIndexingPolicySaveable: false,
|
this.collection.id(),
|
||||||
indexesToDrop: [],
|
newMongoCollection
|
||||||
indexesToAdd: [],
|
);
|
||||||
currentMongoIndexes: [...this.mongoDBCollectionResource.indexes]
|
|
||||||
});
|
await this.refreshIndexTransformationProgress();
|
||||||
|
this.setState({
|
||||||
|
isMongoIndexingPolicySaveable: false,
|
||||||
|
indexesToDrop: [],
|
||||||
|
indexesToAdd: [],
|
||||||
|
currentMongoIndexes: [...this.mongoDBCollectionResource.indexes]
|
||||||
|
});
|
||||||
|
traceSuccess(
|
||||||
|
Action.MongoIndexUpdated,
|
||||||
|
{
|
||||||
|
databaseAccountName: this.container.databaseAccount()?.name,
|
||||||
|
databaseName: this.collection?.databaseId,
|
||||||
|
collectionName: this.collection?.id(),
|
||||||
|
defaultExperience: this.container.defaultExperience(),
|
||||||
|
dataExplorerArea: Constants.Areas.Tab,
|
||||||
|
tabTitle: this.props.settingsTab.tabTitle()
|
||||||
|
},
|
||||||
|
startKey
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
traceFailure(
|
||||||
|
Action.MongoIndexUpdated,
|
||||||
|
{
|
||||||
|
databaseAccountName: this.container.databaseAccount()?.name,
|
||||||
|
databaseName: this.collection?.databaseId,
|
||||||
|
collectionName: this.collection?.id(),
|
||||||
|
defaultExperience: this.container.defaultExperience(),
|
||||||
|
dataExplorerArea: Constants.Areas.Tab,
|
||||||
|
tabTitle: this.props.settingsTab.tabTitle(),
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
errorStack: getErrorStack(error)
|
||||||
|
},
|
||||||
|
startKey
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.isScaleSaveable) {
|
if (this.state.isScaleSaveable) {
|
||||||
const newThroughput = this.state.throughput;
|
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||||
const newOffer: DataModels.Offer = { ...this.collection.offer() };
|
databaseId: this.collection.databaseId,
|
||||||
const originalThroughputValue: number = this.state.throughput;
|
collectionId: this.collection.id(),
|
||||||
|
currentOffer: this.collection.offer(),
|
||||||
if (newOffer.content) {
|
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
||||||
newOffer.content.offerThroughput = newThroughput;
|
manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput
|
||||||
} else {
|
};
|
||||||
newOffer.content = {
|
if (this.hasProvisioningTypeChanged()) {
|
||||||
offerThroughput: newThroughput,
|
|
||||||
offerIsRUPerMinuteThroughputEnabled: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const headerOptions: RequestOptions = { initialHeaders: {} };
|
|
||||||
|
|
||||||
if (this.state.isAutoPilotSelected) {
|
|
||||||
newOffer.content.offerAutopilotSettings = {
|
|
||||||
maxThroughput: this.state.autoPilotThroughput
|
|
||||||
};
|
|
||||||
|
|
||||||
// user has changed from provisioned --> autoscale
|
|
||||||
if (this.hasProvisioningTypeChanged()) {
|
|
||||||
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToAutopilot] = "true";
|
|
||||||
delete newOffer.content.offerAutopilotSettings;
|
|
||||||
} else {
|
|
||||||
delete newOffer.content.offerThroughput;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
isAutoPilotSelected: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// user has changed from autoscale --> provisioned
|
|
||||||
if (this.hasProvisioningTypeChanged()) {
|
|
||||||
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToManualThroughput] = "true";
|
|
||||||
} else {
|
|
||||||
delete newOffer.content.offerAutopilotSettings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
getMaxRUs(this.collection, this.container) <=
|
|
||||||
SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
|
||||||
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
|
||||||
this.container
|
|
||||||
) {
|
|
||||||
const requestPayload = {
|
|
||||||
subscriptionId: userContext.subscriptionId,
|
|
||||||
databaseAccountName: userContext.databaseAccount.name,
|
|
||||||
resourceGroup: userContext.resourceGroup,
|
|
||||||
databaseName: this.collection.databaseId,
|
|
||||||
collectionName: this.collection.id(),
|
|
||||||
throughput: newThroughput,
|
|
||||||
offerIsRUPerMinuteThroughputEnabled: false
|
|
||||||
};
|
|
||||||
|
|
||||||
await updateOfferThroughputBeyondLimit(requestPayload);
|
|
||||||
this.collection.offer().content.offerThroughput = originalThroughputValue;
|
|
||||||
this.setState({
|
|
||||||
isScaleSaveable: false,
|
|
||||||
isScaleDiscardable: false,
|
|
||||||
throughput: originalThroughputValue,
|
|
||||||
throughputBaseline: originalThroughputValue,
|
|
||||||
initialNotification: {
|
|
||||||
description: `Throughput update for ${newThroughput} ${throughputUnit}`
|
|
||||||
} as DataModels.Notification
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
|
||||||
databaseId: this.collection.databaseId,
|
|
||||||
collectionId: this.collection.id(),
|
|
||||||
currentOffer: this.collection.offer(),
|
|
||||||
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
|
||||||
manualThroughput: this.state.isAutoPilotSelected ? undefined : newThroughput
|
|
||||||
};
|
|
||||||
if (this.hasProvisioningTypeChanged()) {
|
|
||||||
if (this.state.isAutoPilotSelected) {
|
|
||||||
updateOfferParams.migrateToAutoPilot = true;
|
|
||||||
} else {
|
|
||||||
updateOfferParams.migrateToManual = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
|
||||||
this.collection.offer(updatedOffer);
|
|
||||||
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
|
||||||
if (this.state.isAutoPilotSelected) {
|
if (this.state.isAutoPilotSelected) {
|
||||||
this.setState({
|
updateOfferParams.migrateToAutoPilot = true;
|
||||||
autoPilotThroughput: updatedOffer.content.offerAutopilotSettings.maxThroughput,
|
|
||||||
autoPilotThroughputBaseline: updatedOffer.content.offerAutopilotSettings.maxThroughput
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
updateOfferParams.migrateToManual = true;
|
||||||
throughput: updatedOffer.content.offerThroughput,
|
|
||||||
throughputBaseline: updatedOffer.content.offerThroughput
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||||
|
this.collection.offer(updatedOffer);
|
||||||
|
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
||||||
|
if (this.state.isAutoPilotSelected) {
|
||||||
|
this.setState({
|
||||||
|
autoPilotThroughput: updatedOffer.autoscaleMaxThroughput,
|
||||||
|
autoPilotThroughputBaseline: updatedOffer.autoscaleMaxThroughput
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
throughput: updatedOffer.manualThroughput,
|
||||||
|
throughputBaseline: updatedOffer.manualThroughput
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.container.isRefreshingExplorer(false);
|
this.container.isRefreshingExplorer(false);
|
||||||
this.setBaseline();
|
this.setBaseline();
|
||||||
@@ -529,10 +477,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
},
|
},
|
||||||
startKey
|
startKey
|
||||||
);
|
);
|
||||||
} catch (reason) {
|
} catch (error) {
|
||||||
this.container.isRefreshingExplorer(false);
|
this.container.isRefreshingExplorer(false);
|
||||||
this.props.settingsTab.isExecutionError(true);
|
this.props.settingsTab.isExecutionError(true);
|
||||||
console.error(reason);
|
console.error(error);
|
||||||
traceFailure(
|
traceFailure(
|
||||||
Action.SettingsV2Updated,
|
Action.SettingsV2Updated,
|
||||||
{
|
{
|
||||||
@@ -542,7 +490,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
defaultExperience: this.container.defaultExperience(),
|
defaultExperience: this.container.defaultExperience(),
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
dataExplorerArea: Constants.Areas.Tab,
|
||||||
tabTitle: this.props.settingsTab.tabTitle(),
|
tabTitle: this.props.settingsTab.tabTitle(),
|
||||||
error: reason
|
error: getErrorMessage(error),
|
||||||
|
errorStack: getErrorStack(error)
|
||||||
},
|
},
|
||||||
startKey
|
startKey
|
||||||
);
|
);
|
||||||
@@ -776,7 +725,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const offerThroughput = this.collection?.offer && this.collection.offer()?.content?.offerThroughput;
|
const offerThroughput = this.collection.offer()?.manualThroughput;
|
||||||
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
||||||
? ChangeFeedPolicyState.On
|
? ChangeFeedPolicyState.On
|
||||||
: ChangeFeedPolicyState.Off;
|
: ChangeFeedPolicyState.Off;
|
||||||
@@ -867,7 +816,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
collection: this.collection,
|
collection: this.collection,
|
||||||
container: this.container,
|
container: this.container,
|
||||||
isFixedContainer: this.isFixedContainer,
|
isFixedContainer: this.isFixedContainer,
|
||||||
autoPilotTiersList: this.autoPilotTiersList,
|
|
||||||
onThroughputChange: this.onThroughputChange,
|
onThroughputChange: this.onThroughputChange,
|
||||||
throughput: this.state.throughput,
|
throughput: this.state.throughput,
|
||||||
throughputBaseline: this.state.throughputBaseline,
|
throughputBaseline: this.state.throughputBaseline,
|
||||||
@@ -916,6 +864,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
indexingPolicyContentBaseline: this.state.indexingPolicyContentBaseline,
|
indexingPolicyContentBaseline: this.state.indexingPolicyContentBaseline,
|
||||||
onIndexingPolicyContentChange: this.onIndexingPolicyContentChange,
|
onIndexingPolicyContentChange: this.onIndexingPolicyContentChange,
|
||||||
logIndexingPolicySuccessMessage: this.logIndexingPolicySuccessMessage,
|
logIndexingPolicySuccessMessage: this.logIndexingPolicySuccessMessage,
|
||||||
|
indexTransformationProgress: this.state.indexTransformationProgress,
|
||||||
|
refreshIndexTransformationProgress: this.refreshIndexTransformationProgress,
|
||||||
onIndexingPolicyDirtyChange: this.onIndexingPolicyDirtyChange
|
onIndexingPolicyDirtyChange: this.onIndexingPolicyDirtyChange
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -966,15 +916,18 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||||
content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />
|
content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />
|
||||||
});
|
});
|
||||||
} else if (
|
} else if (this.container.isPreferredApiMongoDB()) {
|
||||||
this.container.isMongoIndexEditorEnabled() &&
|
if (isEmpty(this.container.features())) {
|
||||||
this.container.isPreferredApiMongoDB() &&
|
tabs.push({
|
||||||
this.container.isEnableMongoCapabilityPresent()
|
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||||
) {
|
content: mongoIndexingPolicyAADError
|
||||||
tabs.push({
|
});
|
||||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
} else if (this.container.isEnableMongoCapabilityPresent()) {
|
||||||
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />
|
tabs.push({
|
||||||
});
|
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||||
|
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.hasConflictResolution()) {
|
if (this.hasConflictResolution()) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
getEstimatedAutoscaleSpendElement,
|
getEstimatedAutoscaleSpendElement,
|
||||||
manualToAutoscaleDisclaimerElement,
|
manualToAutoscaleDisclaimerElement,
|
||||||
ttlWarning,
|
ttlWarning,
|
||||||
indexingPolicyTTLWarningMessage,
|
indexingPolicynUnsavedWarningMessage,
|
||||||
updateThroughputBeyondLimitWarningMessage,
|
updateThroughputBeyondLimitWarningMessage,
|
||||||
updateThroughputDelayedApplyWarningMessage,
|
updateThroughputDelayedApplyWarningMessage,
|
||||||
getThroughputApplyDelayedMessage,
|
getThroughputApplyDelayedMessage,
|
||||||
@@ -37,12 +37,12 @@ class SettingsRenderUtilsTestComponent extends React.Component {
|
|||||||
|
|
||||||
{manualToAutoscaleDisclaimerElement}
|
{manualToAutoscaleDisclaimerElement}
|
||||||
{ttlWarning}
|
{ttlWarning}
|
||||||
{indexingPolicyTTLWarningMessage}
|
{indexingPolicynUnsavedWarningMessage}
|
||||||
{updateThroughputBeyondLimitWarningMessage}
|
{updateThroughputBeyondLimitWarningMessage}
|
||||||
{updateThroughputDelayedApplyWarningMessage}
|
{updateThroughputDelayedApplyWarningMessage}
|
||||||
|
|
||||||
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||||
{getThroughputApplyShortDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
{getThroughputApplyShortDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection")}
|
||||||
{getThroughputApplyLongDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
{getThroughputApplyLongDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||||
|
|
||||||
{getToolTipContainer(<span>Sample Text</span>)}
|
{getToolTipContainer(<span>Sample Text</span>)}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import {
|
|||||||
} from "office-ui-fabric-react";
|
} from "office-ui-fabric-react";
|
||||||
import { isDirtyTypes, isDirty } from "./SettingsUtils";
|
import { isDirtyTypes, isDirty } from "./SettingsUtils";
|
||||||
|
|
||||||
const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } };
|
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } };
|
||||||
|
|
||||||
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
||||||
label: {
|
label: {
|
||||||
@@ -245,15 +245,9 @@ export const ttlWarning: JSX.Element = (
|
|||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const indexingPolicyTTLWarningMessage: JSX.Element = (
|
export const indexingPolicynUnsavedWarningMessage: JSX.Element = (
|
||||||
<Text styles={infoAndToolTipTextStyle}>
|
<Text styles={infoAndToolTipTextStyle}>
|
||||||
Changing the Indexing Policy impacts query results while the index transformation occurs. When a change is made and
|
You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes.
|
||||||
the indexing mode is set to consistent or lazy, queries return eventual results until the operation completes. For
|
|
||||||
more information see,{" "}
|
|
||||||
<Link target="_blank" href="https://aka.ms/cosmosdb/modify-index-policy">
|
|
||||||
Modifying Indexing Policies
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -325,14 +319,13 @@ export const getThroughputApplyShortDelayMessage = (
|
|||||||
throughput: number,
|
throughput: number,
|
||||||
throughputUnit: string,
|
throughputUnit: string,
|
||||||
databaseName: string,
|
databaseName: string,
|
||||||
collectionName: string,
|
collectionName: string
|
||||||
targetThroughput: number
|
|
||||||
): JSX.Element => (
|
): JSX.Element => (
|
||||||
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
|
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
|
||||||
A request to increase the throughput is currently in progress. This operation will take some time to complete.
|
A request to increase the throughput is currently in progress. This operation will take some time to complete.
|
||||||
<br />
|
<br />
|
||||||
Database: {databaseName}, Container: {collectionName}{" "}
|
Database: {databaseName}, Container: {collectionName}{" "}
|
||||||
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, targetThroughput)}
|
{getCurrentThroughput(isAutoscale, throughput, throughputUnit)}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -410,8 +403,8 @@ export const mongoIndexingPolicyAADError: JSX.Element = (
|
|||||||
|
|
||||||
export const mongoIndexTransformationRefreshingMessage: JSX.Element = (
|
export const mongoIndexTransformationRefreshingMessage: JSX.Element = (
|
||||||
<Stack horizontal {...mongoWarningStackProps}>
|
<Stack horizontal {...mongoWarningStackProps}>
|
||||||
<Text>Refreshing index transformation progress</Text>
|
<Text styles={infoAndToolTipTextStyle}>Refreshing index transformation progress</Text>
|
||||||
<Spinner size={SpinnerSize.medium} />
|
<Spinner size={SpinnerSize.small} />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -421,14 +414,14 @@ export const renderMongoIndexTransformationRefreshMessage = (
|
|||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
if (progress === 0) {
|
if (progress === 0) {
|
||||||
return (
|
return (
|
||||||
<Text>
|
<Text styles={infoAndToolTipTextStyle}>
|
||||||
{"You can make more indexing changes once the current index transformation is complete. "}
|
{"You can make more indexing changes once the current index transformation is complete. "}
|
||||||
<Link onClick={performRefresh}>{"Refresh to check if it has completed."}</Link>
|
<Link onClick={performRefresh}>{"Refresh to check if it has completed."}</Link>
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Text>
|
<Text styles={infoAndToolTipTextStyle}>
|
||||||
{`You can make more indexing changes once the current index transformation has completed. It is ${progress}% complete. `}
|
{`You can make more indexing changes once the current index transformation has completed. It is ${progress}% complete. `}
|
||||||
<Link onClick={performRefresh}>{"Refresh to check the progress."}</Link>
|
<Link onClick={performRefresh}>{"Refresh to check the progress."}</Link>
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ describe("IndexingPolicyComponent", () => {
|
|||||||
},
|
},
|
||||||
onIndexingPolicyDirtyChange: () => {
|
onIndexingPolicyDirtyChange: () => {
|
||||||
return;
|
return;
|
||||||
}
|
},
|
||||||
|
indexTransformationProgress: undefined,
|
||||||
|
refreshIndexTransformationProgress: () => new Promise(jest.fn())
|
||||||
};
|
};
|
||||||
|
|
||||||
it("renders", () => {
|
it("renders", () => {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as DataModels from "../../../../Contracts/DataModels";
|
import * as DataModels from "../../../../Contracts/DataModels";
|
||||||
import * as monaco from "monaco-editor";
|
import * as monaco from "monaco-editor";
|
||||||
import { isDirty } from "../SettingsUtils";
|
import { isDirty, isIndexTransforming } from "../SettingsUtils";
|
||||||
import { MessageBar, MessageBarType, Stack } from "office-ui-fabric-react";
|
import { MessageBar, MessageBarType, Stack } from "office-ui-fabric-react";
|
||||||
import { indexingPolicyTTLWarningMessage, titleAndInputStackProps } from "../SettingsRenderUtils";
|
import { indexingPolicynUnsavedWarningMessage, titleAndInputStackProps } from "../SettingsRenderUtils";
|
||||||
|
import { IndexingPolicyRefreshComponent } from "./IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
||||||
|
|
||||||
export interface IndexingPolicyComponentProps {
|
export interface IndexingPolicyComponentProps {
|
||||||
shouldDiscardIndexingPolicy: boolean;
|
shouldDiscardIndexingPolicy: boolean;
|
||||||
@@ -12,6 +13,8 @@ export interface IndexingPolicyComponentProps {
|
|||||||
indexingPolicyContentBaseline: DataModels.IndexingPolicy;
|
indexingPolicyContentBaseline: DataModels.IndexingPolicy;
|
||||||
onIndexingPolicyContentChange: (newIndexingPolicy: DataModels.IndexingPolicy) => void;
|
onIndexingPolicyContentChange: (newIndexingPolicy: DataModels.IndexingPolicy) => void;
|
||||||
logIndexingPolicySuccessMessage: () => void;
|
logIndexingPolicySuccessMessage: () => void;
|
||||||
|
indexTransformationProgress: number;
|
||||||
|
refreshIndexTransformationProgress: () => Promise<void>;
|
||||||
onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void;
|
onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +54,9 @@ export class IndexingPolicyComponent extends React.Component<
|
|||||||
if (!this.indexingPolicyEditor) {
|
if (!this.indexingPolicyEditor) {
|
||||||
this.createIndexingPolicyEditor();
|
this.createIndexingPolicyEditor();
|
||||||
} else {
|
} else {
|
||||||
|
this.indexingPolicyEditor.updateOptions({
|
||||||
|
readOnly: isIndexTransforming(this.props.indexTransformationProgress)
|
||||||
|
});
|
||||||
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
|
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
|
||||||
const value: string = JSON.stringify(this.props.indexingPolicyContent, undefined, 4);
|
const value: string = JSON.stringify(this.props.indexingPolicyContent, undefined, 4);
|
||||||
indexingPolicyEditorModel.setValue(value);
|
indexingPolicyEditorModel.setValue(value);
|
||||||
@@ -84,7 +90,7 @@ export class IndexingPolicyComponent extends React.Component<
|
|||||||
this.indexingPolicyEditor = monaco.editor.create(this.indexingPolicyDiv.current, {
|
this.indexingPolicyEditor = monaco.editor.create(this.indexingPolicyDiv.current, {
|
||||||
value: value,
|
value: value,
|
||||||
language: "json",
|
language: "json",
|
||||||
readOnly: false,
|
readOnly: isIndexTransforming(this.props.indexTransformationProgress),
|
||||||
ariaLabel: "Indexing Policy"
|
ariaLabel: "Indexing Policy"
|
||||||
});
|
});
|
||||||
if (this.indexingPolicyEditor) {
|
if (this.indexingPolicyEditor) {
|
||||||
@@ -108,8 +114,12 @@ export class IndexingPolicyComponent extends React.Component<
|
|||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Stack {...titleAndInputStackProps}>
|
<Stack {...titleAndInputStackProps}>
|
||||||
|
<IndexingPolicyRefreshComponent
|
||||||
|
indexTransformationProgress={this.props.indexTransformationProgress}
|
||||||
|
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
|
||||||
|
/>
|
||||||
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
|
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
|
||||||
<MessageBar messageBarType={MessageBarType.warning}>{indexingPolicyTTLWarningMessage}</MessageBar>
|
<MessageBar messageBarType={MessageBarType.warning}>{indexingPolicynUnsavedWarningMessage}</MessageBar>
|
||||||
)}
|
)}
|
||||||
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
|
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { shallow } from "enzyme";
|
||||||
|
import React from "react";
|
||||||
|
import { IndexingPolicyRefreshComponentProps, IndexingPolicyRefreshComponent } from "./IndexingPolicyRefreshComponent";
|
||||||
|
|
||||||
|
describe("IndexingPolicyRefreshComponent", () => {
|
||||||
|
it("renders", () => {
|
||||||
|
const props: IndexingPolicyRefreshComponentProps = {
|
||||||
|
indexTransformationProgress: 90,
|
||||||
|
refreshIndexTransformationProgress: () => new Promise(jest.fn())
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapper = shallow(<IndexingPolicyRefreshComponent {...props} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { MessageBar, MessageBarType } from "office-ui-fabric-react";
|
||||||
|
import {
|
||||||
|
mongoIndexTransformationRefreshingMessage,
|
||||||
|
renderMongoIndexTransformationRefreshMessage
|
||||||
|
} from "../../SettingsRenderUtils";
|
||||||
|
import { handleError } from "../../../../../Common/ErrorHandlingUtils";
|
||||||
|
import { isIndexTransforming } from "../../SettingsUtils";
|
||||||
|
|
||||||
|
export interface IndexingPolicyRefreshComponentProps {
|
||||||
|
indexTransformationProgress: number;
|
||||||
|
refreshIndexTransformationProgress: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IndexingPolicyRefreshComponentState {
|
||||||
|
isRefreshing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IndexingPolicyRefreshComponent extends React.Component<
|
||||||
|
IndexingPolicyRefreshComponentProps,
|
||||||
|
IndexingPolicyRefreshComponentState
|
||||||
|
> {
|
||||||
|
constructor(props: IndexingPolicyRefreshComponentProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
isRefreshing: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private onClickRefreshIndexingTransformationLink = async () => await this.refreshIndexTransformationProgress();
|
||||||
|
|
||||||
|
private renderIndexTransformationWarning = (): JSX.Element => {
|
||||||
|
if (this.state.isRefreshing) {
|
||||||
|
return mongoIndexTransformationRefreshingMessage;
|
||||||
|
} else if (isIndexTransforming(this.props.indexTransformationProgress)) {
|
||||||
|
return renderMongoIndexTransformationRefreshMessage(
|
||||||
|
this.props.indexTransformationProgress,
|
||||||
|
this.onClickRefreshIndexingTransformationLink
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
private refreshIndexTransformationProgress = async () => {
|
||||||
|
this.setState({ isRefreshing: true });
|
||||||
|
try {
|
||||||
|
await this.props.refreshIndexTransformationProgress();
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, "RefreshIndexTransformationProgress", "Refreshing index transformation progress failed");
|
||||||
|
} finally {
|
||||||
|
this.setState({ isRefreshing: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public render(): JSX.Element {
|
||||||
|
return this.renderIndexTransformationWarning() ? (
|
||||||
|
<MessageBar messageBarType={MessageBarType.warning}>{this.renderIndexTransformationWarning()}</MessageBar>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`IndexingPolicyRefreshComponent renders 1`] = `
|
||||||
|
<StyledMessageBarBase
|
||||||
|
messageBarType={5}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"fontSize": 12,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
You can make more indexing changes once the current index transformation has completed. It is 90% complete.
|
||||||
|
<StyledLinkBase
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
Refresh to check the progress.
|
||||||
|
</StyledLinkBase>
|
||||||
|
</Text>
|
||||||
|
</StyledMessageBarBase>
|
||||||
|
`;
|
||||||
@@ -2,6 +2,7 @@ import { shallow } from "enzyme";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { MongoIndexTypes, MongoNotificationMessage, MongoNotificationType } from "../../SettingsUtils";
|
import { MongoIndexTypes, MongoNotificationMessage, MongoNotificationType } from "../../SettingsUtils";
|
||||||
import { MongoIndexingPolicyComponent, MongoIndexingPolicyComponentProps } from "./MongoIndexingPolicyComponent";
|
import { MongoIndexingPolicyComponent, MongoIndexingPolicyComponentProps } from "./MongoIndexingPolicyComponent";
|
||||||
|
import { renderToString } from "react-dom/server";
|
||||||
|
|
||||||
describe("MongoIndexingPolicyComponent", () => {
|
describe("MongoIndexingPolicyComponent", () => {
|
||||||
const baseProps: MongoIndexingPolicyComponentProps = {
|
const baseProps: MongoIndexingPolicyComponentProps = {
|
||||||
@@ -21,10 +22,7 @@ describe("MongoIndexingPolicyComponent", () => {
|
|||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
indexTransformationProgress: undefined,
|
indexTransformationProgress: undefined,
|
||||||
refreshIndexTransformationProgress: () =>
|
refreshIndexTransformationProgress: () => new Promise(jest.fn()),
|
||||||
new Promise(() => {
|
|
||||||
return;
|
|
||||||
}),
|
|
||||||
onMongoIndexingPolicySaveableChange: () => {
|
onMongoIndexingPolicySaveableChange: () => {
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
@@ -38,16 +36,6 @@ describe("MongoIndexingPolicyComponent", () => {
|
|||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("isIndexingTransforming", () => {
|
|
||||||
const wrapper = shallow(<MongoIndexingPolicyComponent {...baseProps} />);
|
|
||||||
const mongoIndexingPolicyComponent = wrapper.instance() as MongoIndexingPolicyComponent;
|
|
||||||
expect(mongoIndexingPolicyComponent.isIndexingTransforming()).toEqual(false);
|
|
||||||
wrapper.setProps({ indexTransformationProgress: 50 });
|
|
||||||
expect(mongoIndexingPolicyComponent.isIndexingTransforming()).toEqual(true);
|
|
||||||
wrapper.setProps({ indexTransformationProgress: 100 });
|
|
||||||
expect(mongoIndexingPolicyComponent.isIndexingTransforming()).toEqual(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("AddMongoIndexProps test", () => {
|
describe("AddMongoIndexProps test", () => {
|
||||||
const wrapper = shallow(<MongoIndexingPolicyComponent {...baseProps} />);
|
const wrapper = shallow(<MongoIndexingPolicyComponent {...baseProps} />);
|
||||||
const mongoIndexingPolicyComponent = wrapper.instance() as MongoIndexingPolicyComponent;
|
const mongoIndexingPolicyComponent = wrapper.instance() as MongoIndexingPolicyComponent;
|
||||||
@@ -55,7 +43,7 @@ describe("MongoIndexingPolicyComponent", () => {
|
|||||||
it("defaults", () => {
|
it("defaults", () => {
|
||||||
expect(mongoIndexingPolicyComponent.isMongoIndexingPolicySaveable()).toEqual(false);
|
expect(mongoIndexingPolicyComponent.isMongoIndexingPolicySaveable()).toEqual(false);
|
||||||
expect(mongoIndexingPolicyComponent.isMongoIndexingPolicyDiscardable()).toEqual(false);
|
expect(mongoIndexingPolicyComponent.isMongoIndexingPolicyDiscardable()).toEqual(false);
|
||||||
expect(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage()).toEqual(undefined);
|
expect(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage()).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
const sampleWarning = "sampleWarning";
|
const sampleWarning = "sampleWarning";
|
||||||
@@ -113,9 +101,12 @@ describe("MongoIndexingPolicyComponent", () => {
|
|||||||
expect(mongoIndexingPolicyComponent.isMongoIndexingPolicyDiscardable()).toEqual(
|
expect(mongoIndexingPolicyComponent.isMongoIndexingPolicyDiscardable()).toEqual(
|
||||||
isMongoIndexingPolicyDiscardable
|
isMongoIndexingPolicyDiscardable
|
||||||
);
|
);
|
||||||
expect(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage()).toEqual(
|
if (mongoWarningNotificationMessage) {
|
||||||
mongoWarningNotificationMessage
|
const elementAsString = renderToString(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage());
|
||||||
);
|
expect(elementAsString).toContain(mongoWarningNotificationMessage);
|
||||||
|
} else {
|
||||||
|
expect(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage()).toBeUndefined();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,9 +24,8 @@ import {
|
|||||||
transparentDetailsRowStyles,
|
transparentDetailsRowStyles,
|
||||||
createAndAddMongoIndexStackProps,
|
createAndAddMongoIndexStackProps,
|
||||||
separatorStyles,
|
separatorStyles,
|
||||||
mongoIndexingPolicyAADError,
|
indexingPolicynUnsavedWarningMessage,
|
||||||
mongoIndexTransformationRefreshingMessage,
|
infoAndToolTipTextStyle
|
||||||
renderMongoIndexTransformationRefreshMessage
|
|
||||||
} from "../../SettingsRenderUtils";
|
} from "../../SettingsRenderUtils";
|
||||||
import { MongoIndex } from "../../../../../Utils/arm/generatedClients/2020-04-01/types";
|
import { MongoIndex } from "../../../../../Utils/arm/generatedClients/2020-04-01/types";
|
||||||
import {
|
import {
|
||||||
@@ -35,12 +34,12 @@ import {
|
|||||||
MongoIndexIdField,
|
MongoIndexIdField,
|
||||||
MongoNotificationType,
|
MongoNotificationType,
|
||||||
getMongoIndexType,
|
getMongoIndexType,
|
||||||
getMongoIndexTypeText
|
getMongoIndexTypeText,
|
||||||
|
isIndexTransforming
|
||||||
} from "../../SettingsUtils";
|
} from "../../SettingsUtils";
|
||||||
import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
|
import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
|
||||||
import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent";
|
import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent";
|
||||||
import { handleError } from "../../../../../Common/ErrorHandlingUtils";
|
import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
||||||
import { AuthType } from "../../../../../AuthType";
|
|
||||||
|
|
||||||
export interface MongoIndexingPolicyComponentProps {
|
export interface MongoIndexingPolicyComponentProps {
|
||||||
mongoIndexes: MongoIndex[];
|
mongoIndexes: MongoIndex[];
|
||||||
@@ -56,20 +55,13 @@ export interface MongoIndexingPolicyComponentProps {
|
|||||||
onMongoIndexingPolicyDiscardableChange: (isMongoIndexingPolicyDiscardable: boolean) => void;
|
onMongoIndexingPolicyDiscardableChange: (isMongoIndexingPolicyDiscardable: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MongoIndexingPolicyComponentState {
|
|
||||||
isRefreshingIndexTransformationProgress: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MongoIndexDisplayProps {
|
interface MongoIndexDisplayProps {
|
||||||
definition: JSX.Element;
|
definition: JSX.Element;
|
||||||
type: JSX.Element;
|
type: JSX.Element;
|
||||||
actionButton: JSX.Element;
|
actionButton: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MongoIndexingPolicyComponent extends React.Component<
|
export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingPolicyComponentProps> {
|
||||||
MongoIndexingPolicyComponentProps,
|
|
||||||
MongoIndexingPolicyComponentState
|
|
||||||
> {
|
|
||||||
private shouldCheckComponentIsDirty = true;
|
private shouldCheckComponentIsDirty = true;
|
||||||
private addMongoIndexComponentRefs: React.RefObject<AddMongoIndexComponent>[] = [];
|
private addMongoIndexComponentRefs: React.RefObject<AddMongoIndexComponent>[] = [];
|
||||||
private initialIndexesColumns: IColumn[] = [
|
private initialIndexesColumns: IColumn[] = [
|
||||||
@@ -98,13 +90,6 @@ export class MongoIndexingPolicyComponent extends React.Component<
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor(props: MongoIndexingPolicyComponentProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
isRefreshingIndexTransformationProgress: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: MongoIndexingPolicyComponentProps): void {
|
componentDidUpdate(prevProps: MongoIndexingPolicyComponentProps): void {
|
||||||
if (this.props.indexesToAdd.length > prevProps.indexesToAdd.length) {
|
if (this.props.indexesToAdd.length > prevProps.indexesToAdd.length) {
|
||||||
this.addMongoIndexComponentRefs[prevProps.indexesToAdd.length]?.current?.focus();
|
this.addMongoIndexComponentRefs[prevProps.indexesToAdd.length]?.current?.focus();
|
||||||
@@ -144,10 +129,15 @@ export class MongoIndexingPolicyComponent extends React.Component<
|
|||||||
return this.props.indexesToAdd.length > 0 || this.props.indexesToDrop.length > 0;
|
return this.props.indexesToAdd.length > 0 || this.props.indexesToDrop.length > 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
public getMongoWarningNotificationMessage = (): string => {
|
public getMongoWarningNotificationMessage = (): JSX.Element => {
|
||||||
return this.props.indexesToAdd.find(
|
const warningMessage = this.props.indexesToAdd.find(
|
||||||
addMongoIndexProps => addMongoIndexProps.notification?.type === MongoNotificationType.Warning
|
addMongoIndexProps => addMongoIndexProps.notification?.type === MongoNotificationType.Warning
|
||||||
)?.notification.message;
|
)?.notification.message;
|
||||||
|
|
||||||
|
if (warningMessage) {
|
||||||
|
return <Text styles={infoAndToolTipTextStyle}>{warningMessage}</Text>;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
private onRenderRow = (props: IDetailsRowProps): JSX.Element => {
|
private onRenderRow = (props: IDetailsRowProps): JSX.Element => {
|
||||||
@@ -159,7 +149,7 @@ export class MongoIndexingPolicyComponent extends React.Component<
|
|||||||
<IconButton
|
<IconButton
|
||||||
ariaLabel="Delete index Button"
|
ariaLabel="Delete index Button"
|
||||||
iconProps={{ iconName: "Delete" }}
|
iconProps={{ iconName: "Delete" }}
|
||||||
disabled={this.isIndexingTransforming()}
|
disabled={isIndexTransforming(this.props.indexTransformationProgress)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
this.props.onIndexDrop(arrayPosition);
|
this.props.onIndexDrop(arrayPosition);
|
||||||
}}
|
}}
|
||||||
@@ -230,7 +220,7 @@ export class MongoIndexingPolicyComponent extends React.Component<
|
|||||||
|
|
||||||
<AddMongoIndexComponent
|
<AddMongoIndexComponent
|
||||||
ref={this.addMongoIndexComponentRefs[indexesToAddLength]}
|
ref={this.addMongoIndexComponentRefs[indexesToAddLength]}
|
||||||
disabled={this.isIndexingTransforming()}
|
disabled={isIndexTransforming(this.props.indexTransformationProgress)}
|
||||||
position={indexesToAddLength}
|
position={indexesToAddLength}
|
||||||
key={indexesToAddLength}
|
key={indexesToAddLength}
|
||||||
description={undefined}
|
description={undefined}
|
||||||
@@ -298,55 +288,21 @@ export class MongoIndexingPolicyComponent extends React.Component<
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
private refreshIndexTransformationProgress = async () => {
|
|
||||||
this.setState({ isRefreshingIndexTransformationProgress: true });
|
|
||||||
try {
|
|
||||||
await this.props.refreshIndexTransformationProgress();
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, "Refreshing index transformation progress failed.", "RefreshIndexTransformationProgress");
|
|
||||||
} finally {
|
|
||||||
this.setState({ isRefreshingIndexTransformationProgress: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public isIndexingTransforming = (): boolean =>
|
|
||||||
// index transformation progress can be 0
|
|
||||||
this.props.indexTransformationProgress !== undefined && this.props.indexTransformationProgress !== 100;
|
|
||||||
|
|
||||||
private onClickRefreshIndexingTransformationLink = async () => await this.refreshIndexTransformationProgress();
|
|
||||||
|
|
||||||
private renderIndexTransformationWarning = (): JSX.Element => {
|
|
||||||
if (this.state.isRefreshingIndexTransformationProgress) {
|
|
||||||
return mongoIndexTransformationRefreshingMessage;
|
|
||||||
} else if (this.isIndexingTransforming()) {
|
|
||||||
return renderMongoIndexTransformationRefreshMessage(
|
|
||||||
this.props.indexTransformationProgress,
|
|
||||||
this.onClickRefreshIndexingTransformationLink
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
private renderWarningMessage = (): JSX.Element => {
|
private renderWarningMessage = (): JSX.Element => {
|
||||||
let warningMessage: string;
|
let warningMessage: JSX.Element;
|
||||||
if (this.getMongoWarningNotificationMessage()) {
|
if (this.getMongoWarningNotificationMessage()) {
|
||||||
warningMessage = this.getMongoWarningNotificationMessage();
|
warningMessage = this.getMongoWarningNotificationMessage();
|
||||||
} else if (this.isMongoIndexingPolicySaveable()) {
|
} else if (this.isMongoIndexingPolicySaveable()) {
|
||||||
warningMessage =
|
warningMessage = indexingPolicynUnsavedWarningMessage;
|
||||||
"You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes.";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{this.renderIndexTransformationWarning() && (
|
<IndexingPolicyRefreshComponent
|
||||||
<MessageBar messageBarType={MessageBarType.warning}>{this.renderIndexTransformationWarning()}</MessageBar>
|
indexTransformationProgress={this.props.indexTransformationProgress}
|
||||||
)}
|
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
|
||||||
|
/>
|
||||||
{warningMessage && (
|
{warningMessage && <MessageBar messageBarType={MessageBarType.warning}>{warningMessage}</MessageBar>}
|
||||||
<MessageBar messageBarType={MessageBarType.warning}>
|
|
||||||
<Text>{warningMessage}</Text>
|
|
||||||
</MessageBar>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -363,7 +319,7 @@ export class MongoIndexingPolicyComponent extends React.Component<
|
|||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return window.authType !== AuthType.AAD ? mongoIndexingPolicyAADError : <Spinner size={SpinnerSize.large} />;
|
return <Spinner size={SpinnerSize.large} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ exports[`MongoIndexingPolicyComponent renders 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<IndexingPolicyRefreshComponent
|
||||||
|
refreshIndexTransformationProgress={[Function]}
|
||||||
|
/>
|
||||||
<Text>
|
<Text>
|
||||||
For queries that filter on multiple properties, create multiple single field indexes instead of a compound index.
|
For queries that filter on multiple properties, create multiple single field indexes instead of a compound index.
|
||||||
<StyledLinkBase
|
<StyledLinkBase
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ describe("ScaleComponent", () => {
|
|||||||
collection: collection,
|
collection: collection,
|
||||||
container: container,
|
container: container,
|
||||||
isFixedContainer: false,
|
isFixedContainer: false,
|
||||||
autoPilotTiersList: [],
|
|
||||||
onThroughputChange: () => {
|
onThroughputChange: () => {
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
@@ -45,7 +44,7 @@ describe("ScaleComponent", () => {
|
|||||||
} as DataModels.Notification
|
} as DataModels.Notification
|
||||||
};
|
};
|
||||||
|
|
||||||
it("renders with correct intiial notification", () => {
|
it("renders with correct initial notification", () => {
|
||||||
let wrapper = shallow(<ScaleComponent {...baseProps} />);
|
let wrapper = shallow(<ScaleComponent {...baseProps} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
|
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
|
||||||
@@ -55,16 +54,13 @@ describe("ScaleComponent", () => {
|
|||||||
|
|
||||||
const newCollection = { ...collection };
|
const newCollection = { ...collection };
|
||||||
const maxThroughput = 5000;
|
const maxThroughput = 5000;
|
||||||
const targetMaxThroughput = 50000;
|
|
||||||
newCollection.offer = ko.observable({
|
newCollection.offer = ko.observable({
|
||||||
content: {
|
manualThroughput: undefined,
|
||||||
offerAutopilotSettings: {
|
autoscaleMaxThroughput: maxThroughput,
|
||||||
maxThroughput: maxThroughput,
|
minimumThroughput: 400,
|
||||||
targetMaxThroughput: targetMaxThroughput
|
id: "offer",
|
||||||
}
|
|
||||||
},
|
|
||||||
headers: { "x-ms-offer-replace-pending": true }
|
headers: { "x-ms-offer-replace-pending": true }
|
||||||
} as DataModels.OfferWithHeaders);
|
});
|
||||||
const newProps = {
|
const newProps = {
|
||||||
...baseProps,
|
...baseProps,
|
||||||
initialNotification: undefined as DataModels.Notification,
|
initialNotification: undefined as DataModels.Notification,
|
||||||
@@ -74,7 +70,6 @@ describe("ScaleComponent", () => {
|
|||||||
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true);
|
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true);
|
||||||
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(false);
|
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(false);
|
||||||
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(maxThroughput);
|
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(maxThroughput);
|
||||||
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(targetMaxThroughput);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("autoScale disabled", () => {
|
it("autoScale disabled", () => {
|
||||||
@@ -110,11 +105,6 @@ describe("ScaleComponent", () => {
|
|||||||
expect(scaleComponent.isAutoScaleEnabled()).toEqual(true);
|
expect(scaleComponent.isAutoScaleEnabled()).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getMaxRUThroughputInputLimit", () => {
|
|
||||||
const scaleComponent = new ScaleComponent(baseProps);
|
|
||||||
expect(scaleComponent.getMaxRUThroughputInputLimit()).toEqual(40000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getThroughputTitle", () => {
|
it("getThroughputTitle", () => {
|
||||||
let scaleComponent = new ScaleComponent(baseProps);
|
let scaleComponent = new ScaleComponent(baseProps);
|
||||||
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
|
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
|
||||||
@@ -139,14 +129,8 @@ describe("ScaleComponent", () => {
|
|||||||
|
|
||||||
it("getThroughputWarningMessage", () => {
|
it("getThroughputWarningMessage", () => {
|
||||||
const throughputBeyondLimit = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million + 1000;
|
const throughputBeyondLimit = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million + 1000;
|
||||||
const throughputBeyondMaxRus = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million - 1000;
|
|
||||||
|
|
||||||
const newProps = { ...baseProps, container: nonNationalCloudContainer, throughput: throughputBeyondLimit };
|
const newProps = { ...baseProps, container: nonNationalCloudContainer, throughput: throughputBeyondLimit };
|
||||||
let scaleComponent = new ScaleComponent(newProps);
|
const scaleComponent = new ScaleComponent(newProps);
|
||||||
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputBeyondLimitWarningMessage");
|
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputBeyondLimitWarningMessage");
|
||||||
|
|
||||||
newProps.throughput = throughputBeyondMaxRus;
|
|
||||||
scaleComponent = new ScaleComponent(newProps);
|
|
||||||
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputDelayedApplyWarningMessage");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user