mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-23 19:54:08 +00:00
Compare commits
2 Commits
user/swvis
...
remove-rup
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
123ec2e45c | ||
|
|
2f4abfa796 |
@@ -1,14 +1,7 @@
|
|||||||
|
# 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_DATABASE_ACCOUNT_KEY=
|
|
||||||
PORTAL_RUNNER_CONNECTION_STRING=
|
PORTAL_RUNNER_CONNECTION_STRING=
|
||||||
NOTEBOOKS_TEST_RUNNER_TENANT_ID=
|
|
||||||
NOTEBOOKS_TEST_RUNNER_CLIENT_ID=
|
|
||||||
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET=
|
|
||||||
CASSANDRA_CONNECTION_STRING=
|
|
||||||
MONGO_CONNECTION_STRING=
|
|
||||||
TABLES_CONNECTION_STRING=
|
|
||||||
DATA_EXPLORER_ENDPOINT=https://localhost:1234/hostedExplorer.html
|
|
||||||
@@ -15,6 +15,8 @@ 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
|
||||||
@@ -202,6 +204,8 @@ 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
|
||||||
@@ -288,6 +292,8 @@ 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
|
||||||
@@ -392,5 +398,19 @@ 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,31 +79,32 @@ jobs:
|
|||||||
name: dist
|
name: dist
|
||||||
path: dist/
|
path: dist/
|
||||||
endtoendemulator:
|
endtoendemulator:
|
||||||
name: "End To End Emulator Tests"
|
name: "End To End Tests | Emulator | SQL"
|
||||||
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
|
||||||
- uses: southpolesteve/cosmos-emulator-github-action@v1
|
- name: Restore Cypress Binary Cache
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ~/.cache/Cypress
|
||||||
|
key: ${{ runner.os }}-cypress-binary-cache
|
||||||
- name: End to End Tests
|
- name: End to End Tests
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
npm start &
|
npm start &
|
||||||
npm run wait-for-server
|
npm ci --prefix ./cypress
|
||||||
npx jest -c ./jest.config.e2e.js --detectOpenHandles sql
|
npm run test:ci --prefix ./cypress -- --spec ./integration/dataexplorer/ci-tests/createDatabase.spec.ts
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/explorer.html?platform=Emulator"
|
EMULATOR_ENDPOINT: https://0.0.0.0:8081/
|
||||||
PLATFORM: "Emulator"
|
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
- uses: actions/upload-artifact@v2
|
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
||||||
with:
|
|
||||||
name: screenshots
|
|
||||||
path: failed-*
|
|
||||||
accessibility:
|
accessibility:
|
||||||
name: "Accessibility | Hosted"
|
name: "Accessibility | Hosted"
|
||||||
needs: [lint, format, compile, unittest]
|
needs: [lint, format, compile, unittest]
|
||||||
@@ -122,13 +123,13 @@ jobs:
|
|||||||
sudo sysctl -p
|
sudo sysctl -p
|
||||||
npm ci
|
npm ci
|
||||||
npm start &
|
npm start &
|
||||||
npx wait-on -i 5000 https-get://0.0.0.0:1234/
|
npx wait-on -i 5000 https-get://0.0.0.0:1234/
|
||||||
node utils/accesibilityCheck.js
|
node utils/accesibilityCheck.js
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
endtoendhosted:
|
endtoendpuppeteer:
|
||||||
name: "End to End Hosted Tests"
|
name: "End to end puppeteer tests"
|
||||||
needs: [lint, format, compile, unittest]
|
needs: [lint, format, compile, unittest]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -137,7 +138,7 @@ jobs:
|
|||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: 12.x
|
||||||
- name: End to End Hosted Tests
|
- name: End to End Puppeteer Tests
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
npm start &
|
npm start &
|
||||||
@@ -146,26 +147,13 @@ 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, endtoendhosted]
|
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendpuppeteer]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||||
@@ -189,7 +177,7 @@ jobs:
|
|||||||
nugetmpac:
|
nugetmpac:
|
||||||
name: Publish Nuget MPAC
|
name: Publish Nuget MPAC
|
||||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted]
|
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendpuppeteer]
|
||||||
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,6 +9,9 @@ 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 environment 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 enironment 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. Allowlist `https://localhost:1234` domain for CORS in the Azure Cosmos DB portal
|
2. Whitelist `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,17 +76,24 @@ 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
|
||||||
|
|
||||||
Jest and Puppeteer are used for end to end browser based tests and are contained in `test/`. To run these tests locally:
|
[Cypress](https://www.cypress.io/) is used for end to end tests and are contained in `cypress/`. Currently, it operates as sub project with its own typescript config and dependencies. It also only operates against the emulator. To run cypress tests:
|
||||||
|
|
||||||
1. Copy .env.example to .env
|
1. Ensure the emulator is running
|
||||||
2. Update the values in .env including your local data explorer endpoint (ask a teammate/codeowner for help with .env values)
|
2. Start cosmos explorer in emulator mode: `PLATFORM=Emulator npm run watch`
|
||||||
3. Make sure all packages are installed `npm install`
|
3. Move into `cypress/` folder: `cd cypress`
|
||||||
4. Run the server `npm run start` and wait for it to start
|
4. Install dependencies: `npm install`
|
||||||
5. Run `npm run test:e2e`
|
5. Run cypress headless(`npm run test`) or in interactive mode(`npm run test:debug`)
|
||||||
|
|
||||||
|
#### 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 adhere to the release strategy [documented by the Azure SDK Guidelines](https://azure.github.io/azure-sdk/policies_repobranching.html#release-branches). Most releases should happen from the master branch. If master contains commits that cannot be released, you may create a release from a `release/` or `hotfix/` branch. See linked documentation for more details.
|
We generally 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.
|
||||||
|
|
||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
|
|||||||
4
cypress/.gitignore
vendored
Normal file
4
cypress/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
cypress.env.json
|
||||||
|
cypress/report
|
||||||
|
cypress/screenshots
|
||||||
|
cypress/videos
|
||||||
51
cypress/cleanup.js
Normal file
51
cypress/cleanup.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
// Cleans up old databases from previous test runs
|
||||||
|
const { CosmosClient } = require("@azure/cosmos");
|
||||||
|
|
||||||
|
// TODO: Add support for other API connection strings
|
||||||
|
const mongoRegex = RegExp("mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com");
|
||||||
|
|
||||||
|
async function cleanup() {
|
||||||
|
const connectionString = process.env.CYPRESS_CONNECTION_STRING;
|
||||||
|
if (!connectionString) {
|
||||||
|
throw new Error("Connection string not provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
let client;
|
||||||
|
switch (true) {
|
||||||
|
case connectionString.includes("mongodb://"): {
|
||||||
|
const [, key, accountName] = connectionString.match(mongoRegex);
|
||||||
|
client = new CosmosClient({
|
||||||
|
key,
|
||||||
|
endpoint: `https://${accountName}.documents.azure.com:443/`
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// TODO: Add support for other API connection strings
|
||||||
|
default:
|
||||||
|
client = new CosmosClient(connectionString);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await client.databases.readAll().fetchAll();
|
||||||
|
return Promise.all(
|
||||||
|
response.resources.map(async db => {
|
||||||
|
const dbTimestamp = new Date(db._ts * 1000);
|
||||||
|
const twentyMinutesAgo = new Date(Date.now() - 1000 * 60 * 20);
|
||||||
|
if (dbTimestamp < twentyMinutesAgo) {
|
||||||
|
await client.database(db.id).delete();
|
||||||
|
console.log(`DELETED: ${db.id} | Timestamp: ${dbTimestamp}`);
|
||||||
|
} else {
|
||||||
|
console.log(`SKIPPED: ${db.id} | Timestamp: ${dbTimestamp}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup()
|
||||||
|
.then(() => {
|
||||||
|
process.exit(0);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
15
cypress/cypress.json
Normal file
15
cypress/cypress.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"integrationFolder": "./integration",
|
||||||
|
"pluginsFile": false,
|
||||||
|
"fixturesFolder": false,
|
||||||
|
"supportFile": "./support/index.js",
|
||||||
|
"defaultCommandTimeout": 90000,
|
||||||
|
"chromeWebSecurity": false,
|
||||||
|
"reporter": "mochawesome",
|
||||||
|
"reporterOptions": {
|
||||||
|
"reportDir": "cypress/report",
|
||||||
|
"json": true,
|
||||||
|
"overwrite": false,
|
||||||
|
"html": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// 1. Click on "New Container" on the command bar.
|
||||||
|
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
||||||
|
// 3. It includes an input box for the database Id.
|
||||||
|
// 4. It includes a checkbox called "Create now".
|
||||||
|
// 5. When the checkbox is marked, enter new database id.
|
||||||
|
// 3. Create a database WITH "Provision throughput" checked.
|
||||||
|
// 4. Enter minimum throughput value of 400.
|
||||||
|
// 5. Enter container id to the container id text box.
|
||||||
|
// 6. Enter partition key to the partition key text box.
|
||||||
|
// 7. Click "OK" to create a new container.
|
||||||
|
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
||||||
|
|
||||||
|
const connectionString = require("../../../utilities/connectionString");
|
||||||
|
|
||||||
|
let crypt = require("crypto");
|
||||||
|
|
||||||
|
context("Cassandra API Test - createDatabase", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
connectionString.loginUsingConnectionString(connectionString.constants.cassandra);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Create a new table in Cassandra API", () => {
|
||||||
|
const keyspaceId = `KeyspaceId${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
const tableId = `TableId112`;
|
||||||
|
|
||||||
|
cy.get("iframe").then($element => {
|
||||||
|
const $body = $element.contents().find("body");
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[class="commandBarContainer"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.find('button[data-test="New Table"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[class="contextual-pane-in"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.find('span[id="containerTitle"]');
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[id="keyspace-id"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.type(keyspaceId);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[class="textfontclr"]')
|
||||||
|
.type(tableId);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="databaseThroughputValue"]')
|
||||||
|
.should("have.value", "400");
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('data-test="addCollection-createCollection"')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.wait(10000);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[data-test="resourceTreeId"]')
|
||||||
|
.should("exist")
|
||||||
|
.find('div[class="treeComponent dataResourceTree"]')
|
||||||
|
.should("contain", tableId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
81
cypress/integration/dataexplorer/GRAPH/addCollection.spec.ts
Normal file
81
cypress/integration/dataexplorer/GRAPH/addCollection.spec.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
// 1. Click on "New Graph" on the command bar.
|
||||||
|
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
||||||
|
// 3. It includes an input box for the database Id.
|
||||||
|
// 4. It includes a checkbox called "Create now".
|
||||||
|
// 5. When the checkbox is marked, enter new database id.
|
||||||
|
// 3. Create a database WITH "Provision throughput" checked.
|
||||||
|
// 4. Enter minimum throughput value of 400.
|
||||||
|
// 5. Enter container id to the container id text box.
|
||||||
|
// 6. Enter partition key to the partition key text box.
|
||||||
|
// 7. Click "OK" to create a new container.
|
||||||
|
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
||||||
|
|
||||||
|
const connectionString = require("../../../utilities/connectionString");
|
||||||
|
|
||||||
|
let crypt = require("crypto");
|
||||||
|
|
||||||
|
context("Graph API Test", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
connectionString.loginUsingConnectionString(connectionString.constants.graph);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Create a new graph in Graph API", () => {
|
||||||
|
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
const graphId = `TestGraph${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
const partitionKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
|
||||||
|
cy.get("iframe").then($element => {
|
||||||
|
const $body = $element.contents().find("body");
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[class="commandBarContainer"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.find('button[data-test="New Graph"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[class="contextual-pane-in"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.find('span[id="containerTitle"]');
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-createNewDatabase"]')
|
||||||
|
.check();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-newDatabaseId"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.type(dbId);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
||||||
|
.check();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="databaseThroughputValue"]')
|
||||||
|
.should("have.value", "400");
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-collectionId"]')
|
||||||
|
.type(graphId);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-partitionKeyValue"]')
|
||||||
|
.type(partitionKey);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-createCollection"]')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.wait(10000);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[data-test="resourceTreeId"]')
|
||||||
|
.should("exist")
|
||||||
|
.find('div[class="treeComponent dataResourceTree"]')
|
||||||
|
.should("contain", dbId)
|
||||||
|
.click()
|
||||||
|
.should("contain", graphId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
80
cypress/integration/dataexplorer/MONGO/addCollection.spec.ts
Normal file
80
cypress/integration/dataexplorer/MONGO/addCollection.spec.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// 1. Click on "New Container" on the command bar.
|
||||||
|
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
||||||
|
// 3. It includes an input box for the database Id.
|
||||||
|
// 4. It includes a checkbox called "Create now".
|
||||||
|
// 5. When the checkbox is marked, enter new database id.
|
||||||
|
// 3. Create a database WITH "Provision throughput" checked.
|
||||||
|
// 4. Enter minimum throughput value of 400.
|
||||||
|
// 5. Enter container id to the container id text box.
|
||||||
|
// 6. Enter partition key to the partition key text box.
|
||||||
|
// 7. Click "OK" to create a new container.
|
||||||
|
// // 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
||||||
|
|
||||||
|
const connectionString = require("../../../utilities/connectionString");
|
||||||
|
|
||||||
|
let crypt = require("crypto");
|
||||||
|
|
||||||
|
context("Mongo API Test - createDatabase", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
connectionString.loginUsingConnectionString();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Create a new collection in Mongo API", () => {
|
||||||
|
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
|
||||||
|
cy.get("iframe").then($element => {
|
||||||
|
const $body = $element.contents().find("body");
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[class="commandBarContainer"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.find('button[data-test="New Collection"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[class="contextual-pane-in"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.find('span[id="containerTitle"]');
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-createNewDatabase"]')
|
||||||
|
.check();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-newDatabaseId"]')
|
||||||
|
.type(dbId);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
||||||
|
.check();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-collectionId"]')
|
||||||
|
.type(collectionId);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="databaseThroughputValue"]')
|
||||||
|
.should("have.value", "400");
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-partitionKeyValue"]')
|
||||||
|
.type(sharedKey);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find("#submitBtnAddCollection")
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.wait(10000);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[data-test="resourceTreeId"]')
|
||||||
|
.should("exist")
|
||||||
|
.find('div[class="treeComponent dataResourceTree"]')
|
||||||
|
.should("contain", dbId)
|
||||||
|
.click()
|
||||||
|
.should("contain", collectionId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
// 1. Click on "New Container" on the command bar.
|
||||||
|
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
||||||
|
// 3. It includes an input box for the database Id.
|
||||||
|
// 4. It includes a checkbox called "Create now".
|
||||||
|
// 5. When the checkbox is marked, enter new database id.
|
||||||
|
// 3. Create a database WITH "Provision throughput" checked.
|
||||||
|
// 4. Enter minimum throughput value of 400.
|
||||||
|
// 5. Enter container id to the container id text box.
|
||||||
|
// 6. Enter partition key to the partition key text box.
|
||||||
|
// 7. Click "OK" to create a new container.
|
||||||
|
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
||||||
|
|
||||||
|
const connectionString = require("../../../utilities/connectionString");
|
||||||
|
|
||||||
|
let crypt = require("crypto");
|
||||||
|
|
||||||
|
context("Mongo API Test", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
connectionString.loginUsingConnectionString();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skip("Create a new collection in Mongo API - Autopilot", () => {
|
||||||
|
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
|
||||||
|
cy.get("iframe").then($element => {
|
||||||
|
const $body = $element.contents().find("body");
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[class="commandBarContainer"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.find('button[data-test="New Collection"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[class="contextual-pane-in"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.find('span[id="containerTitle"]');
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-createNewDatabase"]')
|
||||||
|
.check();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-newDatabaseId"]')
|
||||||
|
.type(dbId);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
||||||
|
.check();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[class="throughputModeContainer"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.and(input => {
|
||||||
|
expect(input.get(0).textContent, "first item").contains("Autopilot (preview)");
|
||||||
|
expect(input.get(1).textContent, "second item").contains("Manual");
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[id="newContainer-databaseThroughput-autoPilotRadio"]')
|
||||||
|
.check();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('select[name="autoPilotTiers"]')
|
||||||
|
// .eq(1).should('contain', '4,000 RU/s');
|
||||||
|
// // .select('4,000 RU/s').should('have.value', '1');
|
||||||
|
|
||||||
|
.find('option[value="2"]')
|
||||||
|
.then($element => $element.get(1).setAttribute("selected", "selected"));
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-collectionId"]')
|
||||||
|
.type(collectionId);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-partitionKeyValue"]')
|
||||||
|
.type(sharedKey);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-createCollection"]')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.wait(10000);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[data-test="resourceTreeId"]')
|
||||||
|
.should("exist")
|
||||||
|
.find('div[class="treeComponent dataResourceTree"]')
|
||||||
|
.should("contain", dbId)
|
||||||
|
.click()
|
||||||
|
.should("contain", collectionId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
const connectionString = require("../../../utilities/connectionString");
|
||||||
|
|
||||||
|
let crypt = require("crypto");
|
||||||
|
|
||||||
|
context("Mongo API Test", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
connectionString.loginUsingConnectionString();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skip("Create a new collection in existing database in Mongo API", () => {
|
||||||
|
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
|
||||||
|
cy.get("iframe").then($element => {
|
||||||
|
const $body = $element.contents().find("body");
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('span[class="nodeLabel"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.then($span => {
|
||||||
|
const dbId1 = $span.text();
|
||||||
|
cy.log("DBBB", dbId1);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[class="commandBarContainer"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.find('button[data-test="New Collection"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[class="contextual-pane-in"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.find('span[id="containerTitle"]');
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-existingDatabase"]')
|
||||||
|
.check();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-existingDatabase"]')
|
||||||
|
.type(dbId1);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-collectionId"]')
|
||||||
|
.type(collectionId);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-partitionKeyValue"]')
|
||||||
|
.type(sharedKey);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-createCollection"]')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.wait(10000);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[data-test="resourceTreeId"]')
|
||||||
|
.should("exist")
|
||||||
|
.find('div[class="treeComponent dataResourceTree"]')
|
||||||
|
.click()
|
||||||
|
.should("contain", collectionId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
const connectionString = require("../../../utilities/connectionString");
|
||||||
|
|
||||||
|
let crypt = require("crypto");
|
||||||
|
|
||||||
|
context.skip("Mongo API Test", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
connectionString.loginUsingConnectionString();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Create a new collection in Mongo API - Provision database throughput", () => {
|
||||||
|
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
|
||||||
|
cy.get("iframe").then($element => {
|
||||||
|
const $body = $element.contents().find("body");
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[class="commandBarContainer"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.find('button[data-test="New Collection"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[class="contextual-pane-in"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.find('span[id="containerTitle"]');
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find(".createNewDatabaseOrUseExisting")
|
||||||
|
.should("have.length", 2)
|
||||||
|
.and(input => {
|
||||||
|
expect(input.get(0).textContent, "first item").contains("Create new");
|
||||||
|
expect(input.get(1).textContent, "second item").contains("Use existing");
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-createNewDatabase"]')
|
||||||
|
.check();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
||||||
|
.check();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-newDatabaseId"]')
|
||||||
|
.type(dbId);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
||||||
|
.check();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="databaseThroughputValue"]')
|
||||||
|
.should("have.value", "400");
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-collectionId"]')
|
||||||
|
.type(collectionId);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-partitionKeyValue"]')
|
||||||
|
.type(sharedKey);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-createCollection"]')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.wait(10000);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[data-test="resourceTreeId"]')
|
||||||
|
.should("exist")
|
||||||
|
.find('div[class="treeComponent dataResourceTree"]')
|
||||||
|
.should("contain", dbId)
|
||||||
|
.click()
|
||||||
|
.should("contain", collectionId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Create a new collection - without provision database throughput", () => {
|
||||||
|
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
const collectionIdTitle = `Add Collection`;
|
||||||
|
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
|
||||||
|
cy.get("iframe").then($element => {
|
||||||
|
const $body = $element.contents().find("body");
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[class="commandBarContainer"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.find('button[data-test="New Collection"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[class="contextual-pane-in"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.find('span[id="containerTitle"]');
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-createNewDatabase"]')
|
||||||
|
.check();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-newDatabaseId"]')
|
||||||
|
.type(dbId);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
||||||
|
.uncheck();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-collectionId"]')
|
||||||
|
.type(collectionId);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[id="tab2"]')
|
||||||
|
.check({ force: true });
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-partitionKeyValue"]')
|
||||||
|
.type(sharedKey);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="databaseThroughputValue"]')
|
||||||
|
.should("have.value", "400");
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-createCollection"]')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.wait(10000);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[data-test="resourceTreeId"]')
|
||||||
|
.should("exist")
|
||||||
|
.find('div[class="treeComponent dataResourceTree"]')
|
||||||
|
.should("contain", dbId)
|
||||||
|
.click()
|
||||||
|
.should("contain", collectionId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Create a new collection - without provision database throughput Fixed Storage Capacity", () => {
|
||||||
|
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
|
||||||
|
cy.get("iframe").then($element => {
|
||||||
|
const $body = $element.contents().find("body");
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[class="commandBarContainer"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.find('button[data-test="New Collection"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[class="contextual-pane-in"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.find('span[id="containerTitle"]');
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-createNewDatabase"]')
|
||||||
|
.check();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-newDatabaseId"]')
|
||||||
|
.type(dbId);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
||||||
|
.uncheck();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-collectionId"]')
|
||||||
|
.type(collectionId);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[id="tab1"]')
|
||||||
|
.check({ force: true });
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="databaseThroughputValue"]')
|
||||||
|
.should("have.value", "400");
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-createCollection"]')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.wait(10000);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[data-test="resourceTreeId"]')
|
||||||
|
.should("exist")
|
||||||
|
.find('div[class="treeComponent dataResourceTree"]')
|
||||||
|
.should("contain", dbId)
|
||||||
|
.click()
|
||||||
|
.should("contain", collectionId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
79
cypress/integration/dataexplorer/SQL/addCollection.spec.ts
Normal file
79
cypress/integration/dataexplorer/SQL/addCollection.spec.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
// 1. Click on "New Container" on the command bar.
|
||||||
|
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
||||||
|
// 3. It includes an input box for the database Id.
|
||||||
|
// 4. It includes a checkbox called "Create now".
|
||||||
|
// 5. When the checkbox is marked, enter new database id.
|
||||||
|
// 3. Create a database WITH "Provision throughput" checked.
|
||||||
|
// 4. Enter minimum throughput value of 400.
|
||||||
|
// 5. Enter container id to the container id text box.
|
||||||
|
// 6. Enter partition key to the partition key text box.
|
||||||
|
// 7. Click "OK" to create a new container.
|
||||||
|
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
||||||
|
|
||||||
|
const connectionString = require("../../../utilities/connectionString");
|
||||||
|
|
||||||
|
let crypt = require("crypto");
|
||||||
|
|
||||||
|
context("SQL API Test", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
connectionString.loginUsingConnectionString();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Create a new container in SQL API", () => {
|
||||||
|
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
connectionString.loginUsingConnectionString();
|
||||||
|
|
||||||
|
cy.get("iframe").then($element => {
|
||||||
|
const $body = $element.contents().find("body");
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[class="commandBarContainer"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.find('button[data-test="New Container"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[class="contextual-pane-in"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.find('span[id="containerTitle"]');
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-createNewDatabase"]')
|
||||||
|
.check();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
||||||
|
.check();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-newDatabaseId"]')
|
||||||
|
.type(dbId);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-collectionId"]')
|
||||||
|
.type(collectionId);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="databaseThroughputValue"]')
|
||||||
|
.should("have.value", "400");
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-partitionKeyValue"]')
|
||||||
|
.type(sharedKey);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find("#submitBtnAddCollection")
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.wait(10000);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[data-test="resourceTreeId"]')
|
||||||
|
.should("exist")
|
||||||
|
.find('div[class="treeComponent dataResourceTree"]')
|
||||||
|
.should("contain", dbId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
60
cypress/integration/dataexplorer/TABLE/addCollection.spec.ts
Normal file
60
cypress/integration/dataexplorer/TABLE/addCollection.spec.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// 1. Click on "New Container" on the command bar.
|
||||||
|
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
||||||
|
// 3. It includes an input box for the database Id.
|
||||||
|
// 4. It includes a checkbox called "Create now".
|
||||||
|
// 5. When the checkbox is marked, enter new database id.
|
||||||
|
// 3. Create a database WITH "Provision throughput" checked.
|
||||||
|
// 4. Enter minimum throughput value of 400.
|
||||||
|
// 5. Enter container id to the container id text box.
|
||||||
|
// 6. Enter partition key to the partition key text box.
|
||||||
|
// 7. Click "OK" to create a new container.
|
||||||
|
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
||||||
|
|
||||||
|
const connectionString = require("../../../utilities/connectionString");
|
||||||
|
|
||||||
|
let crypt = require("crypto");
|
||||||
|
|
||||||
|
context("Table API Test", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
connectionString.loginUsingConnectionString(connectionString.constants.table);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Create a new table in Table API", () => {
|
||||||
|
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
|
||||||
|
cy.get("iframe").then($element => {
|
||||||
|
const $body = $element.contents().find("body");
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[class="commandBarContainer"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.find('button[data-test="New Table"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[class="contextual-pane-in"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.find('span[id="containerTitle"]');
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-collectionId"]')
|
||||||
|
.type(collectionId);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="databaseThroughputValue"]')
|
||||||
|
.should("have.value", "400");
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('input[data-test="addCollection-createCollection"]')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.wait(10000);
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find('div[data-test="resourceTreeId"]')
|
||||||
|
.should("exist")
|
||||||
|
.find('div[class="treeComponent dataResourceTree"]')
|
||||||
|
.should("contain", collectionId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// 1. Click on "New Container" on the command bar.
|
||||||
|
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
||||||
|
// 3. It includes an input box for the database Id.
|
||||||
|
// 4. It includes a checkbox called "Create now".
|
||||||
|
// 5. When the checkbox is marked, enter new database id.
|
||||||
|
// 3. Create a database WITH "Provision throughput" checked.
|
||||||
|
// 4. Enter minimum throughput value of 400.
|
||||||
|
// 5. Enter container id to the container id text box.
|
||||||
|
// 6. Enter partition key to the partition key text box.
|
||||||
|
// 7. Click "OK" to create a new container.
|
||||||
|
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
||||||
|
|
||||||
|
let crypt = require("crypto");
|
||||||
|
|
||||||
|
context("Emulator - createDatabase", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit("http://localhost:1234/explorer.html");
|
||||||
|
});
|
||||||
|
|
||||||
|
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
const collectionIdTitle = `Add Collection`;
|
||||||
|
const partitionKey = `PartitionKey${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
|
||||||
|
it("Create a new collection", () => {
|
||||||
|
cy.contains("New Container").click();
|
||||||
|
|
||||||
|
// cy.contains(collectionIdTitle);
|
||||||
|
|
||||||
|
cy.get(".createNewDatabaseOrUseExisting")
|
||||||
|
.should("have.length", 2)
|
||||||
|
.and(input => {
|
||||||
|
expect(input.get(0).textContent, "first item").contains("Create new");
|
||||||
|
expect(input.get(1).textContent, "second item").contains("Use existing");
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('input[data-test="addCollection-createNewDatabase"]').check();
|
||||||
|
|
||||||
|
cy.get('input[data-test="addCollection-newDatabaseId"]').type(dbId);
|
||||||
|
|
||||||
|
cy.get('input[data-test="addCollection-collectionId"]').type(collectionId);
|
||||||
|
|
||||||
|
cy.get('input[data-test="databaseThroughputValue"]').should("have.value", "400");
|
||||||
|
|
||||||
|
cy.get('input[data-test="addCollection-partitionKeyValue"]').type(partitionKey);
|
||||||
|
|
||||||
|
cy.get('input[data-test="addCollection-createCollection"]').click();
|
||||||
|
|
||||||
|
cy.get('div[data-test="resourceTreeId"]').should("exist");
|
||||||
|
|
||||||
|
cy.get('div[data-test="resourceTree-collectionsTree"]').should("contain", dbId);
|
||||||
|
|
||||||
|
cy.get('div[data-test="databaseList"]').should("contain", collectionId);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// 1. Click on "New Database" on the command bar
|
||||||
|
// 2. a Pane with the title "Add Database" should appear on the right side of the screen
|
||||||
|
// i. It includes an input box for the database Id.
|
||||||
|
// ii. It includes a checkbox called "Provision throughput".
|
||||||
|
// iii. Whe the checkbox is marked, a new input with a throughput control let's you customize RU at the database level
|
||||||
|
// 3. Create a database WITHOUT "Provision throughput" checked.
|
||||||
|
// 4. It should appear in the Data Explorer list.
|
||||||
|
// 5. Repeat steps 1-3 but create a database WITH "Provision throughput" with the default RU value.
|
||||||
|
// 6. It should appear in the Data Explorer list.
|
||||||
|
// 7. If expanded, it should have the list item called "Scale", that once clicked, it should show the "Scale" tab.
|
||||||
|
// 8. Inside that tab, a throughput control will let you change the RU value within the permited range.
|
||||||
|
// 9. If you change the value, it should enable the "Save" button.
|
||||||
|
// 10. Click "Save" and verify that the process completes without error.
|
||||||
|
// 11. Close the tab and reopen it and verify that the input contains the last saved value.%
|
||||||
|
|
||||||
|
const crypto = require("crypto");
|
||||||
|
const client = require("../../../utilities/cosmosClient");
|
||||||
|
const randomString = crypto.randomBytes(2).toString("hex");
|
||||||
|
const databaseId = `TestDB-${randomString}`;
|
||||||
|
const collectionId = `TestColl-${randomString}`;
|
||||||
|
|
||||||
|
context("Emulator - Create database -> container -> item", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
const { resources } = await client.databases.readAll().fetchAll();
|
||||||
|
for (const database of resources) {
|
||||||
|
await client.database(database.id).delete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates a new database", () => {
|
||||||
|
cy.visit("https://0.0.0.0:1234/explorer.html?platform=Emulator");
|
||||||
|
cy.contains("New Container").click();
|
||||||
|
cy.get("[data-test=addCollection-newDatabaseId]").click();
|
||||||
|
cy.get("[data-test=addCollection-newDatabaseId]").type(databaseId);
|
||||||
|
cy.get("[data-test=addCollection-collectionId]").click();
|
||||||
|
cy.get("[data-test=addCollection-collectionId]").type(collectionId);
|
||||||
|
cy.get("[data-test=addCollection-partitionKeyValue]").click();
|
||||||
|
cy.get("[data-test=addCollection-partitionKeyValue]").type("/pk");
|
||||||
|
cy.get('input[name="createCollection"]').click();
|
||||||
|
cy.get(".dataResourceTree").should("contain", databaseId);
|
||||||
|
cy.get(".dataResourceTree")
|
||||||
|
.contains(databaseId)
|
||||||
|
.click();
|
||||||
|
cy.get(".dataResourceTree").should("contain", collectionId);
|
||||||
|
cy.get(".dataResourceTree")
|
||||||
|
.contains(collectionId)
|
||||||
|
.click();
|
||||||
|
cy.get(".dataResourceTree")
|
||||||
|
.contains("Items")
|
||||||
|
.click();
|
||||||
|
cy.get(".dataResourceTree")
|
||||||
|
.contains("Items")
|
||||||
|
.click();
|
||||||
|
cy.wait(1000); // React rendering inside KO causes some weird async rendering that makes this test flaky without waiting
|
||||||
|
cy.get(".commandBarContainer")
|
||||||
|
.contains("New Item")
|
||||||
|
.click();
|
||||||
|
cy.wait(1000); // React rendering inside KO causes some weird async rendering that makes this test flaky without waiting
|
||||||
|
cy.get(".commandBarContainer")
|
||||||
|
.contains("Save")
|
||||||
|
.click();
|
||||||
|
cy.wait(1000); // React rendering inside KO causes some weird async rendering that makes this test flaky without waiting
|
||||||
|
cy.get(".documentsGridHeaderContainer").should("contain", "replace_with_new_document_id");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// 1. Click last database in the resource tree
|
||||||
|
// 2. Click the last collection within the database
|
||||||
|
// 3. Select the context menu within the collection
|
||||||
|
// 4. Select "Delete Container" option in the dropdown
|
||||||
|
// 5. On Selection, Delete Container pane opens on the right side
|
||||||
|
// 6. Enter the same collection id that is to be deleted and click ok
|
||||||
|
// 7. Now, the resource tree refreshes, the deleted collection should not appear under the database
|
||||||
|
|
||||||
|
let crypt = require("crypto");
|
||||||
|
|
||||||
|
context("Emulator - deleteCollection", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit("http://localhost:1234/explorer.html");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Delete a collection", () => {
|
||||||
|
cy.get(".databaseId")
|
||||||
|
.last()
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.get(".collectionList")
|
||||||
|
.last()
|
||||||
|
.then($id => {
|
||||||
|
const collectionId = $id.text();
|
||||||
|
|
||||||
|
cy.get('span[data-test="collectionEllipsisMenu"]').should("exist");
|
||||||
|
|
||||||
|
cy.get('span[data-test="collectionEllipsisMenu"]')
|
||||||
|
.invoke("show")
|
||||||
|
.last()
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.get('div[data-test="collectionContextMenu"]')
|
||||||
|
.contains("Delete Container")
|
||||||
|
.click({ force: true });
|
||||||
|
|
||||||
|
cy.get('input[data-test="confirmCollectionId"]').type(collectionId.trim());
|
||||||
|
|
||||||
|
cy.get('input[data-test="deleteCollection"]').click();
|
||||||
|
|
||||||
|
cy.get('div[data-test="databaseList"]').should("not.contain", collectionId);
|
||||||
|
|
||||||
|
cy.get('div[data-test="databaseMenu"]').should("not.contain", collectionId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
// 1. Click last database in the resource tree
|
||||||
|
// 2. Select the context menu within the database
|
||||||
|
// 4. Select "Delete Database" option in the dropdown
|
||||||
|
// 5. On Selection, Delete Database pane opens on the right side
|
||||||
|
// 6. Enter the same database id that is to be deleted and click ok
|
||||||
|
// 7. Now, the resource tree refreshes, the deleted database should not appear in the resource tree
|
||||||
|
|
||||||
|
let crypt = require("crypto");
|
||||||
|
|
||||||
|
context("Emulator - deleteDatabase", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
let db_rid = "";
|
||||||
|
const date = new Date().toUTCString();
|
||||||
|
let authToken = "";
|
||||||
|
cy.visit("http://localhost:1234/explorer.html");
|
||||||
|
|
||||||
|
// Creating auth token for collection creation
|
||||||
|
cy.request({
|
||||||
|
method: "GET",
|
||||||
|
url: "https://localhost:8081/_explorer/authorization/post/dbs/",
|
||||||
|
headers: {
|
||||||
|
"x-ms-date": date,
|
||||||
|
authorization: "-"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
authToken = response.body.Token; // Getting auth token for collection creation
|
||||||
|
return new Cypress.Promise((resolve, reject) => {
|
||||||
|
return resolve();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
cy.request({
|
||||||
|
method: "POST",
|
||||||
|
url: "https://localhost:8081/dbs",
|
||||||
|
headers: {
|
||||||
|
"x-ms-date": date,
|
||||||
|
authorization: authToken,
|
||||||
|
"x-ms-version": "2018-12-31"
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
id: dbId
|
||||||
|
}
|
||||||
|
}).then(response => {
|
||||||
|
cy.log("Response", response);
|
||||||
|
db_rid = response.body._rid;
|
||||||
|
return new Cypress.Promise((resolve, reject) => {
|
||||||
|
cy.log("Rid", db_rid);
|
||||||
|
return resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Delete a database", () => {
|
||||||
|
cy.get('span[data-test="refreshTree"]').click();
|
||||||
|
|
||||||
|
cy.get(".databaseId")
|
||||||
|
.last()
|
||||||
|
.then($id => {
|
||||||
|
const dbId = $id.text();
|
||||||
|
|
||||||
|
cy.get('span[data-test="databaseEllipsisMenu"]').should("exist");
|
||||||
|
|
||||||
|
cy.get('span[data-test="databaseEllipsisMenu"]')
|
||||||
|
.invoke("show")
|
||||||
|
.last()
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.get('div[data-test="databaseContextMenu"]')
|
||||||
|
.contains("Delete Database")
|
||||||
|
.click({ force: true });
|
||||||
|
|
||||||
|
cy.get('input[data-test="confirmDatabaseId"]').type(dbId.trim());
|
||||||
|
|
||||||
|
cy.get('input[data-test="deleteDatabase"]').click();
|
||||||
|
|
||||||
|
cy.get('div[data-test="databaseList"]').should("not.contain", dbId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
35
cypress/integration/notebook/README.md
Normal file
35
cypress/integration/notebook/README.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Notebook end-to-end tests
|
||||||
|
This describes how to run the tests locally
|
||||||
|
|
||||||
|
## Stand up a local notebook container instance:
|
||||||
|
Instructions on how to build and run the container [here](https://microsoft.sharepoint.com/teams/DocDB/_layouts/OneNote.aspx?id=%2Fteams%2FDocDB%2FSiteAssets%2FDocDB%20Team%20Notebook&wd=target%28Tools%20_%20SDK%2FPortal%2FDevelopment.one%7CF800BE8E-1E31-48FE-90D7-EF698EF88112%2FHow%20to%20build%20notebook%20service%7C4BAA153B-422C-41E2-B997-F3FCE02CD743%2F%29)
|
||||||
|
|
||||||
|
## Run a local data explorer
|
||||||
|
Instructions are in [`DataExplorer/README.md`](https://msdata.visualstudio.com/CosmosDB/_git/cosmosdb-dataexplorer?path=%2FProduct%2FPortal%2FDataExplorer%2FREADME.md&_a=preview).
|
||||||
|
|
||||||
|
Make sure you can run Data Explorer locally from the web browser.
|
||||||
|
|
||||||
|
## Run cypress tests
|
||||||
|
1. Edit the URL for your DataExplorer in the `.spec.ts` file
|
||||||
|
2. Run the test:
|
||||||
|
```bash
|
||||||
|
cd DataExplorer/cypress
|
||||||
|
npm i
|
||||||
|
npm t -- --spec 'integration/notebook/newNotebook.spec.ts'
|
||||||
|
```
|
||||||
|
|
||||||
|
To run in Debug mode:
|
||||||
|
```
|
||||||
|
npm run test:debug
|
||||||
|
```
|
||||||
|
This opens Cypress UI
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
* The tests are recorded in the `videos` folder.
|
||||||
|
* Cypress does not support hover: workarounds [here](https://docs.cypress.io/api/commands/hover.html#Workarounds).
|
||||||
|
|
||||||
|
|
||||||
|
## References
|
||||||
|
* [Cypress API](https://docs.cypress.io/api/api/table-of-contents.html)
|
||||||
|
* [Cypress cookbook](https://docs.cypress.io/faq/questions/using-cypress-faq.html#How-do-I-get-an-element%E2%80%99s-text-contents)
|
||||||
|
* [Cypress best practices](https://docs.cypress.io/guides/references/best-practices.html#Selecting-Elements)
|
||||||
93
cypress/integration/notebook/newNotebook.spec.ts
Normal file
93
cypress/integration/notebook/newNotebook.spec.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
// THIS ADDS A NEW NOTEBOOK TO YOUR NOTEBOOKS
|
||||||
|
context("New Notebook smoke test", () => {
|
||||||
|
const timeout = 15000; // in ms
|
||||||
|
const explorerUrl =
|
||||||
|
"https://localhost:1234/explorer.html?feature.notebookserverurl=https%3A%2F%2Flocalhost%3A10001%2F12345%2Fnotebook&feature.notebookServerToken=token&feature.enablenotebooks=true";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for UI to be ready
|
||||||
|
*/
|
||||||
|
const waitForReady = () => {
|
||||||
|
cy.get(".splashScreenContainer", { timeout }).should("be.visible");
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit(explorerUrl);
|
||||||
|
waitForReady();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Create a new notebook and run some code", () => {
|
||||||
|
// Create new notebook
|
||||||
|
cy.contains("New Notebook").click();
|
||||||
|
|
||||||
|
// Check tab name
|
||||||
|
cy.get("li.tabList .tabNavText").should($span => {
|
||||||
|
const text = $span.text();
|
||||||
|
expect(text).to.match(/^Untitled.*\.ipynb$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for python3 | idle status
|
||||||
|
cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should($p => {
|
||||||
|
const text = $p.text();
|
||||||
|
expect(text).to.match(/^python3.*idle$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click on a cell
|
||||||
|
cy.get(".cell-container")
|
||||||
|
.as("cellContainer")
|
||||||
|
.click();
|
||||||
|
|
||||||
|
// Type in some code
|
||||||
|
cy.get("@cellContainer").type("2+4");
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
cy.get('[data-test="Run"]')
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
|
|
||||||
|
// Verify results
|
||||||
|
cy.get("@cellContainer").within(() => {
|
||||||
|
cy.get("pre code span").should("contain", "6");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restart kernel
|
||||||
|
cy.get('[data-test="Run"] button')
|
||||||
|
.eq(-1)
|
||||||
|
.click();
|
||||||
|
cy.get("li")
|
||||||
|
.contains("Restart Kernel")
|
||||||
|
.click();
|
||||||
|
|
||||||
|
// Wait for python3 | restarting status
|
||||||
|
cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should($p => {
|
||||||
|
const text = $p.text();
|
||||||
|
expect(text).to.match(/^python3.*restarting$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for python3 | idle status
|
||||||
|
cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should($p => {
|
||||||
|
const text = $p.text();
|
||||||
|
expect(text).to.match(/^python3.*idle$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click on a cell
|
||||||
|
cy.get(".cell-container")
|
||||||
|
.as("cellContainer")
|
||||||
|
.find(".input")
|
||||||
|
.as("codeInput")
|
||||||
|
.click();
|
||||||
|
|
||||||
|
// Type in some code
|
||||||
|
cy.get("@codeInput").type("{backspace}{backspace}{backspace}4+5");
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
cy.get('[data-test="Run"]')
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
|
|
||||||
|
// Verify results
|
||||||
|
cy.get("@cellContainer").within(() => {
|
||||||
|
cy.get("pre code span").should("contain", "9");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
172
cypress/integration/notebook/resourceTree.spec.ts
Normal file
172
cypress/integration/notebook/resourceTree.spec.ts
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
context("Resource tree notebook file manipulation", () => {
|
||||||
|
const timeout = 15000; // in ms
|
||||||
|
const explorerUrl =
|
||||||
|
"https://localhost:1234/explorer.html?feature.notebookserverurl=https%3A%2F%2Flocalhost%3A10001%2F12345%2Fnotebook&feature.notebookServerToken=token&feature.enablenotebooks=true";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for UI to be ready
|
||||||
|
*/
|
||||||
|
const waitForReady = () => {
|
||||||
|
cy.get(".splashScreenContainer", { timeout }).should("be.visible");
|
||||||
|
};
|
||||||
|
|
||||||
|
const clickContextMenuAndSelectOption = (nodeLabel, option) => {
|
||||||
|
cy.get(`.treeNodeHeader[data-test="${nodeLabel}"]`)
|
||||||
|
.find("button.treeMenuEllipsis")
|
||||||
|
.click();
|
||||||
|
cy.get('[data-test="treeComponentMenuItemContainer"]')
|
||||||
|
.contains(option)
|
||||||
|
.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFolder = folder => {
|
||||||
|
clickContextMenuAndSelectOption("My Notebooks/", "New Directory");
|
||||||
|
|
||||||
|
cy.get("#stringInputPane").within(() => {
|
||||||
|
cy.get('input[name="collectionIdConfirmation"]').type(folder);
|
||||||
|
cy.get("form").submit();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteItem = nodeName => {
|
||||||
|
clickContextMenuAndSelectOption(`${nodeName}`, "Delete");
|
||||||
|
cy.get(".ms-Dialog-main")
|
||||||
|
.contains("Delete")
|
||||||
|
.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit(explorerUrl);
|
||||||
|
waitForReady();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Create and remove a directory", () => {
|
||||||
|
const folder = "e2etest_folder1";
|
||||||
|
createFolder(folder);
|
||||||
|
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).should("exist");
|
||||||
|
deleteItem(`${folder}/`);
|
||||||
|
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).should("not.exist");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Create and rename a directory", () => {
|
||||||
|
const folder = "e2etest_folder2";
|
||||||
|
const renamedFolder = "e2etest_folder2_renamed";
|
||||||
|
createFolder(folder);
|
||||||
|
|
||||||
|
// Rename
|
||||||
|
clickContextMenuAndSelectOption(`${folder}/`, "Rename");
|
||||||
|
cy.get("#stringInputPane").within(() => {
|
||||||
|
cy.get('input[name="collectionIdConfirmation"]')
|
||||||
|
.clear()
|
||||||
|
.type(renamedFolder);
|
||||||
|
cy.get("form").submit();
|
||||||
|
});
|
||||||
|
cy.get(`.treeNodeHeader[data-test="${renamedFolder}/"]`).should("exist");
|
||||||
|
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).should("not.exist");
|
||||||
|
|
||||||
|
deleteItem(`${renamedFolder}/`);
|
||||||
|
cy.get(`.treeNodeHeader[data-test="${renamedFolder}/"]`).should("not.exist");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Create a notebook inside a directory", () => {
|
||||||
|
const folder = "e2etest_folder3";
|
||||||
|
const newNotebookName = "Untitled.ipynb";
|
||||||
|
createFolder(folder);
|
||||||
|
clickContextMenuAndSelectOption(`${folder}/`, "New Notebook");
|
||||||
|
|
||||||
|
// Verify tab is open
|
||||||
|
cy.get(".tabList")
|
||||||
|
.contains(newNotebookName)
|
||||||
|
.should("exist");
|
||||||
|
|
||||||
|
// Close tab
|
||||||
|
cy.get(`.tabList[title="notebooks/${folder}/${newNotebookName}"]`)
|
||||||
|
.find(".cancelButton")
|
||||||
|
.click();
|
||||||
|
// When running from command line, closing the tab is too fast
|
||||||
|
cy.get("body").then($body => {
|
||||||
|
if ($body.find(".ms-Dialog-main").length) {
|
||||||
|
// For some reason, this does not work
|
||||||
|
// cy.get(".ms-Dialog-main").contains("Close").click();
|
||||||
|
cy.get(".ms-Dialog-main .ms-Button--primary").click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expand folder node
|
||||||
|
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).click();
|
||||||
|
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("exist");
|
||||||
|
|
||||||
|
// Delete notebook
|
||||||
|
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`)
|
||||||
|
.find("button.treeMenuEllipsis")
|
||||||
|
.click();
|
||||||
|
cy.get('[data-test="treeComponentMenuItemContainer"]')
|
||||||
|
.contains("Delete")
|
||||||
|
.click();
|
||||||
|
|
||||||
|
// Confirm
|
||||||
|
cy.get(".ms-Dialog-main")
|
||||||
|
.contains("Delete")
|
||||||
|
.click();
|
||||||
|
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("not.exist");
|
||||||
|
|
||||||
|
deleteItem(`${folder}/`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Create and rename a notebook inside a directory", () => {
|
||||||
|
const folder = "e2etest_folder4";
|
||||||
|
const newNotebookName = "Untitled.ipynb";
|
||||||
|
const renamedNotebookName = "mynotebook.ipynb";
|
||||||
|
createFolder(folder);
|
||||||
|
clickContextMenuAndSelectOption(`${folder}/`, "New Notebook");
|
||||||
|
|
||||||
|
// Close tab
|
||||||
|
cy.get(`.tabList[title="notebooks/${folder}/${newNotebookName}"]`)
|
||||||
|
.find(".cancelButton")
|
||||||
|
.click();
|
||||||
|
cy.get("body").then($body => {
|
||||||
|
if ($body.find(".ms-Dialog-main").length) {
|
||||||
|
// For some reason, this does not work
|
||||||
|
// cy.get(".ms-Dialog-main").contains("Close").click();
|
||||||
|
cy.get(".ms-Dialog-main .ms-Button--primary").click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expand folder node
|
||||||
|
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).click();
|
||||||
|
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("exist");
|
||||||
|
|
||||||
|
// Rename notebook
|
||||||
|
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`)
|
||||||
|
.find("button.treeMenuEllipsis")
|
||||||
|
.click();
|
||||||
|
cy.get('[data-test="treeComponentMenuItemContainer"]')
|
||||||
|
.contains("Rename")
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.get("#stringInputPane").within(() => {
|
||||||
|
cy.get('input[name="collectionIdConfirmation"]')
|
||||||
|
.clear()
|
||||||
|
.type(renamedNotebookName);
|
||||||
|
cy.get("form").submit();
|
||||||
|
});
|
||||||
|
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("not.exist");
|
||||||
|
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${renamedNotebookName}"]`).should("exist");
|
||||||
|
|
||||||
|
// Delete notebook
|
||||||
|
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${renamedNotebookName}"]`)
|
||||||
|
.find("button.treeMenuEllipsis")
|
||||||
|
.click();
|
||||||
|
cy.get('[data-test="treeComponentMenuItemContainer"]')
|
||||||
|
.contains("Delete")
|
||||||
|
.click();
|
||||||
|
|
||||||
|
// Confirm
|
||||||
|
cy.get(".ms-Dialog-main")
|
||||||
|
.contains("Delete")
|
||||||
|
.click();
|
||||||
|
// Give it time to settle
|
||||||
|
cy.wait(1000);
|
||||||
|
deleteItem(`${folder}/`);
|
||||||
|
});
|
||||||
|
});
|
||||||
3066
cypress/package-lock.json
generated
Normal file
3066
cypress/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
cypress/package.json
Normal file
25
cypress/package.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "cosmos-explorer-cypress",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "cypress run",
|
||||||
|
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
|
||||||
|
"test:sql": "cypress run --browser chrome --spec \"./integration/dataexplorer/SQL/*\"",
|
||||||
|
"test:ci": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/ https-get://0.0.0.0:8081/_explorer/index.html && cypress run --browser edge --headless",
|
||||||
|
"test:debug": "cypress open"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"cypress": "^4.8.0",
|
||||||
|
"mocha": "^7.0.1",
|
||||||
|
"mochawesome": "^4.1.0",
|
||||||
|
"mochawesome-merge": "^4.0.1",
|
||||||
|
"mochawesome-report-generator": "^4.1.0",
|
||||||
|
"typescript": "3.4.3",
|
||||||
|
"wait-on": "^4.0.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@microsoft/applicationinsights-web": "^2.5.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
23
cypress/support/index.js
Normal file
23
cypress/support/index.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
let appInsightsLib = require("@microsoft/applicationinsights-web");
|
||||||
|
|
||||||
|
const appInsights = new appInsightsLib.ApplicationInsights({
|
||||||
|
config: {
|
||||||
|
instrumentationKey: "fe61c39f-7d32-4488-a191-b13621965315"
|
||||||
|
/* ...Other Configuration Options... */
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
appInsights.loadAppInsights();
|
||||||
|
|
||||||
|
Cypress.on("fail", (error, runnable) => {
|
||||||
|
// App Insights will record the fail tests for Create Collection
|
||||||
|
let message = JSON.stringify(runnable.title);
|
||||||
|
appInsights.trackTrace({
|
||||||
|
message: `${message}`,
|
||||||
|
properties: {
|
||||||
|
passed: false,
|
||||||
|
error: error
|
||||||
|
}
|
||||||
|
});
|
||||||
|
throw error; // throw error to have test still fail
|
||||||
|
});
|
||||||
11
cypress/tsconfig.json
Normal file
11
cypress/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["es5", "dom", "es6"],
|
||||||
|
"types": ["cypress", "node"]
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts", "**/*.spec.ts"]
|
||||||
|
}
|
||||||
41
cypress/utilities/connectionString.js
Normal file
41
cypress/utilities/connectionString.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
module.exports = {
|
||||||
|
loginUsingConnectionString: function() {
|
||||||
|
const prodUrl = Cypress.env("TEST_ENDPOINT") || "https://localhost:1234/hostedExplorer.html";
|
||||||
|
const timeout = 15000;
|
||||||
|
|
||||||
|
cy.visit(prodUrl);
|
||||||
|
cy.get('iframe[id="explorerMenu"]').should("be.visible");
|
||||||
|
|
||||||
|
cy.get("iframe").then($element => {
|
||||||
|
const $body = $element.contents().find("body");
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find("#connectExplorer")
|
||||||
|
.should("exist")
|
||||||
|
.find("div[class='connectExplorer']")
|
||||||
|
.should("exist")
|
||||||
|
.find("p[class='welcomeText']")
|
||||||
|
.should("exist");
|
||||||
|
|
||||||
|
cy.wrap($body.find("div > p.switchConnectTypeText"))
|
||||||
|
.should("exist")
|
||||||
|
.last()
|
||||||
|
.click({ force: true });
|
||||||
|
|
||||||
|
const secret = Cypress.env("CONNECTION_STRING");
|
||||||
|
|
||||||
|
cy.wrap($body)
|
||||||
|
.find("input[class='inputToken']")
|
||||||
|
.should("exist")
|
||||||
|
.type(secret, {
|
||||||
|
force: true
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.wrap($body.find("input[value='Connect']"), { timeout })
|
||||||
|
.first()
|
||||||
|
.click({ force: true });
|
||||||
|
|
||||||
|
cy.wait(15000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
6
cypress/utilities/cosmosClient.js
Normal file
6
cypress/utilities/cosmosClient.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
const { CosmosClient } = require("@azure/cosmos");
|
||||||
|
|
||||||
|
module.exports = new CosmosClient({
|
||||||
|
endpoint: "https://0.0.0.0:8081",
|
||||||
|
key: "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
|
||||||
|
});
|
||||||
4604
package-lock.json
generated
4604
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -4,17 +4,15 @@
|
|||||||
"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.5",
|
"@azure/cosmos-language-service": "0.0.4",
|
||||||
"@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.2.9",
|
"@nteract/data-explorer": "8.0.3",
|
||||||
"@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",
|
||||||
@@ -23,7 +21,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.2",
|
"@nteract/monaco-editor": "3.2.0",
|
||||||
"@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",
|
||||||
@@ -68,7 +66,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.18.1",
|
"monaco-editor": "0.15.6",
|
||||||
"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",
|
||||||
@@ -117,7 +115,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.56",
|
"@types/react": "16.9.49",
|
||||||
"@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",
|
||||||
@@ -196,8 +194,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,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
"format": "prettier --write \"{src,cypress,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
||||||
"format:check": "prettier --check \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
"format:check": "prettier --check \"{src,cypress,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
||||||
"lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"",
|
"lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"",
|
||||||
"build:contracts": "npm run compile:contracts",
|
"build:contracts": "npm run compile:contracts",
|
||||||
"strictEligibleFiles": "node ./strict-migration-tools/index.js",
|
"strictEligibleFiles": "node ./strict-migration-tools/index.js",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
"offerThroughput": 400,
|
"offerThroughput": 400,
|
||||||
"databaseLevelThroughput": false,
|
"databaseLevelThroughput": false,
|
||||||
"collectionId": "Persons",
|
"collectionId": "Persons",
|
||||||
"rupmEnabled": false,
|
|
||||||
"partitionKey": { "kind": "Hash", "paths": ["/name"] },
|
"partitionKey": { "kind": "Hash", "paths": ["/name"] },
|
||||||
"data": [
|
"data": [
|
||||||
"g.addV('person').property(id, '1').property('name', 'Eva').property('age', 44)",
|
"g.addV('person').property(id, '1').property('name', 'Eva').property('age', 44)",
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ export class CapabilityNames {
|
|||||||
export class Features {
|
export class Features {
|
||||||
public static readonly cosmosdb = "cosmosdb";
|
public static readonly cosmosdb = "cosmosdb";
|
||||||
public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy";
|
public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy";
|
||||||
public static readonly enableRupm = "enablerupm";
|
|
||||||
public static readonly executeSproc = "dataexplorerexecutesproc";
|
public static readonly executeSproc = "dataexplorerexecutesproc";
|
||||||
public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
|
public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
|
||||||
public static readonly enableTtl = "enablettl";
|
public static readonly enableTtl = "enablettl";
|
||||||
@@ -125,9 +124,7 @@ export class Features {
|
|||||||
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
|
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
|
||||||
public static readonly ttl90Days = "ttl90days";
|
public static readonly ttl90Days = "ttl90days";
|
||||||
public static readonly enableRightPanelV2 = "enablerightpanelv2";
|
public static readonly enableRightPanelV2 = "enablerightpanelv2";
|
||||||
public static readonly enableSchema = "enableschema";
|
|
||||||
public static readonly enableSDKoperations = "enablesdkoperations";
|
public static readonly enableSDKoperations = "enablesdkoperations";
|
||||||
public static readonly showMinRUSurvey = "showminrusurvey";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// flight names returned from the portal are always lowercase
|
// flight names returned from the portal are always lowercase
|
||||||
@@ -180,12 +177,6 @@ export class CassandraBackend {
|
|||||||
public static readonly schemaApi: string = "api/cassandra/schema";
|
public static readonly schemaApi: string = "api/cassandra/schema";
|
||||||
public static readonly guestSchemaApi: string = "api/guest/cassandra/schema";
|
public static readonly guestSchemaApi: string = "api/guest/cassandra/schema";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RUPMStates {
|
|
||||||
public static on: string = "on";
|
|
||||||
public static off: string = "off";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Queries {
|
export class Queries {
|
||||||
public static CustomPageOption: string = "custom";
|
public static CustomPageOption: string = "custom";
|
||||||
public static UnlimitedPageOption: string = "unlimited";
|
public static UnlimitedPageOption: string = "unlimited";
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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";
|
||||||
@@ -70,7 +69,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}: ${getErrorMessage(error)}`);
|
logConsoleError(`Failed to get authorization headers for ${resourceType}: ${error.message}`);
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
import { ConflictDefinition, FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import {
|
||||||
|
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,
|
||||||
"ExecuteStoredProcedure",
|
`Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`,
|
||||||
`Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
|
"ExecuteStoredProcedure"
|
||||||
);
|
);
|
||||||
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, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`);
|
handleError(error, `Failed to query ${entityName} for container ${resourceName}`, "QueryDocumentsPage");
|
||||||
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, "ReadDocument", `Failed to read ${entityName} ${documentId.id()}`);
|
handleError(error, `Failed to read ${entityName} ${documentId.id()}`, "ReadDocument");
|
||||||
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, "UpdateDocument", `Failed to update ${entityName} ${documentId.id()}`);
|
handleError(error, `Failed to update ${entityName} ${documentId.id()}`, "UpdateDocument");
|
||||||
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, "CreateDocument", `Error while creating new ${entityName} for container ${collection.id()}`);
|
handleError(error, `Error while creating new ${entityName} for container ${collection.id()}`, "CreateDocument");
|
||||||
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, "DeleteDocument", `Error while deleting ${entityName} ${documentId.id()}`);
|
handleError(error, `Error while deleting ${entityName} ${documentId.id()}`, "DeleteDocument");
|
||||||
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, "DeleteConflict", `Error while deleting conflict ${conflictId.id()}`);
|
handleError(error, `Error while deleting conflict ${conflictId.id()}`, "DeleteConflict");
|
||||||
deferred.reject(error);
|
deferred.reject(error);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,56 +1,11 @@
|
|||||||
import { ARMError } from "../Utils/arm/request";
|
import { CosmosError, sendNotificationForError } from "./dataAccess/sendNotificationForError";
|
||||||
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 { sendMessage } from "./MessageHandler";
|
import { replaceKnownError } from "./ErrorParserUtility";
|
||||||
|
|
||||||
export const handleError = (error: string | ARMError | Error, area: string, consoleErrorPrefix?: string): void => {
|
export const handleError = (error: CosmosError, consoleErrorPrefix: string, area: string): void => {
|
||||||
const errorMessage = getErrorMessage(error);
|
const sanitizedErrorMsg = replaceKnownError(error.message);
|
||||||
const errorCode = error instanceof ARMError ? error.code : undefined;
|
logConsoleError(`${consoleErrorPrefix}:\n ${sanitizedErrorMsg}`);
|
||||||
|
logError(sanitizedErrorMsg, area, error.code);
|
||||||
// logs error to data explorer console
|
sendNotificationForError(error);
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|||||||
24
src/Common/ErrorParserUtility.test.ts
Normal file
24
src/Common/ErrorParserUtility.test.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
69
src/Common/ErrorParserUtility.ts
Normal file
69
src/Common/ErrorParserUtility.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
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,8 +21,14 @@ export function logWarning(message: string, area: string, code?: number): void {
|
|||||||
return _logEntry(entry);
|
return _logEntry(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logError(errorMessage: string, area: string, code?: number | string): void {
|
export function logError(message: string | Error, area: string, code?: number): void {
|
||||||
const entry: Diagnostics.LogEntry = _generateLogEntry(Diagnostics.LogEntryLevel.Error, errorMessage, area, code);
|
let logMessage: string;
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +59,7 @@ function _generateLogEntry(
|
|||||||
level: Diagnostics.LogEntryLevel,
|
level: Diagnostics.LogEntryLevel,
|
||||||
message: string,
|
message: string,
|
||||||
area: string,
|
area: string,
|
||||||
code?: number | string
|
code?: number
|
||||||
): Diagnostics.LogEntry {
|
): Diagnostics.LogEntry {
|
||||||
return {
|
return {
|
||||||
timestamp: new Date().getUTCSeconds(),
|
timestamp: new Date().getUTCSeconds(),
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
import * as OfferUtility from "./OfferUtility";
|
|
||||||
import { SDKOfferDefinition, Offer } from "../Contracts/DataModels";
|
|
||||||
import { OfferResponse } from "@azure/cosmos";
|
|
||||||
|
|
||||||
describe("parseSDKOfferResponse", () => {
|
|
||||||
it("manual throughput", () => {
|
|
||||||
const mockOfferDefinition = {
|
|
||||||
content: {
|
|
||||||
offerThroughput: 500,
|
|
||||||
collectionThroughputInfo: {
|
|
||||||
minimumRUForCollection: 400,
|
|
||||||
numPhysicalPartitions: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
id: "test"
|
|
||||||
} as SDKOfferDefinition;
|
|
||||||
|
|
||||||
const mockResponse = {
|
|
||||||
resource: mockOfferDefinition
|
|
||||||
} as OfferResponse;
|
|
||||||
|
|
||||||
const expectedResult: Offer = {
|
|
||||||
manualThroughput: 500,
|
|
||||||
autoscaleMaxThroughput: undefined,
|
|
||||||
minimumThroughput: 400,
|
|
||||||
id: "test",
|
|
||||||
offerDefinition: mockOfferDefinition
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
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 || configContext.platform === Platform.Hosted) {
|
if (configContext.platform === Platform.Emulator) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ 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 { handleError } from "./ErrorHandlingUtils";
|
import * as ErrorParserUtility from "./ErrorParserUtility";
|
||||||
|
import * as Logger from "./Logger";
|
||||||
|
|
||||||
export class QueriesClient {
|
export class QueriesClient {
|
||||||
private static readonly PartitionKey: DataModels.PartitionKey = {
|
private static readonly PartitionKey: DataModels.PartitionKey = {
|
||||||
@@ -52,8 +53,13 @@ export class QueriesClient {
|
|||||||
return Promise.resolve(collection);
|
return Promise.resolve(collection);
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
handleError(error, "setupQueriesCollection", "Failed to set up account for saving queries");
|
const stringifiedError: string = error.message;
|
||||||
return Promise.reject(error);
|
NotificationConsoleUtils.logConsoleMessage(
|
||||||
|
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));
|
||||||
@@ -96,11 +102,19 @@ export class QueriesClient {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
if (error.code === HttpStatusCodes.Conflict.toString()) {
|
let errorMessage: string;
|
||||||
error = `Query ${query.queryName} already exists`;
|
const parsedError: DataModels.ErrorDataModel = ErrorParserUtility.parse(error)[0];
|
||||||
|
if (parsedError.code === HttpStatusCodes.Conflict.toString()) {
|
||||||
|
errorMessage = `Query ${query.queryName} already exists`;
|
||||||
|
} else {
|
||||||
|
errorMessage = parsedError.message;
|
||||||
}
|
}
|
||||||
handleError(error, "saveQuery", `Failed to save query ${query.queryName}`);
|
NotificationConsoleUtils.logConsoleMessage(
|
||||||
return Promise.reject(error);
|
ConsoleDataType.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));
|
||||||
@@ -149,15 +163,25 @@ export class QueriesClient {
|
|||||||
return Promise.resolve(queries);
|
return Promise.resolve(queries);
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
handleError(error, "getSavedQueries", "Failed to fetch saved queries");
|
const stringifiedError: string = error.message;
|
||||||
return Promise.reject(error);
|
NotificationConsoleUtils.logConsoleMessage(
|
||||||
|
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
|
||||||
handleError(error, "getSavedQueries", "Failed to fetch saved queries");
|
const stringifiedError: string = error.message;
|
||||||
return Promise.reject(error);
|
NotificationConsoleUtils.logConsoleMessage(
|
||||||
|
ConsoleDataType.Error,
|
||||||
|
`Failed to fetch saved queries: ${stringifiedError}`
|
||||||
|
);
|
||||||
|
Logger.logError(stringifiedError, "getSavedQueries");
|
||||||
|
return Promise.reject(stringifiedError);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
||||||
@@ -208,8 +232,13 @@ export class QueriesClient {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
handleError(error, "deleteQuery", `Failed to delete query ${query.queryName}`);
|
const stringifiedError: string = error.message;
|
||||||
return Promise.reject(error);
|
NotificationConsoleUtils.logConsoleMessage(
|
||||||
|
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, "CreateCollection", `Error while creating container ${params.collectionId}`);
|
handleError(error, `Error while creating container ${params.collectionId}`, "CreateCollection");
|
||||||
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, "CreateDatabase", `Error while creating database ${params.databaseId}`);
|
handleError(error, `Error while creating database ${params.databaseId}`, "CreateDatabase");
|
||||||
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, "CreateStoredProcedure", `Error while creating stored procedure ${storedProcedure.id}`);
|
handleError(error, `Error while creating stored procedure ${storedProcedure.id}`, "CreateStoredProcedure");
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ 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 { handleError } from "../ErrorHandlingUtils";
|
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logError } from "../Logger";
|
||||||
|
import { sendNotificationForError } from "./sendNotificationForError";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export async function createTrigger(
|
export async function createTrigger(
|
||||||
@@ -65,7 +66,9 @@ export async function createTrigger(
|
|||||||
.scripts.triggers.create(trigger);
|
.scripts.triggers.create(trigger);
|
||||||
return response.resource;
|
return response.resource;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "CreateTrigger", `Error while creating trigger ${trigger.id}`);
|
logConsoleError(`Error while creating trigger ${trigger.id}:\n ${error.message}`);
|
||||||
|
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,
|
||||||
"CreateUserupdateUserDefinedFunction",
|
`Error while creating user defined function ${userDefinedFunction.id}`,
|
||||||
`Error while creating user defined function ${userDefinedFunction.id}`
|
"CreateUserupdateUserDefinedFunction"
|
||||||
);
|
);
|
||||||
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, "DeleteCollection", `Error while deleting container ${collectionId}`);
|
handleError(error, `Error while deleting container ${collectionId}`, "DeleteCollection");
|
||||||
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, "DeleteDatabase", `Error while deleting database ${databaseId}`);
|
handleError(error, `Error while deleting database ${databaseId}`, "DeleteDatabase");
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export async function deleteStoredProcedure(
|
|||||||
.delete();
|
.delete();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "DeleteStoredProcedure", `Error while deleting stored procedure ${storedProcedureId}`);
|
handleError(error, `Error while deleting stored procedure ${storedProcedureId}`, "DeleteStoredProcedure");
|
||||||
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, "DeleteTrigger", `Error while deleting trigger ${triggerId}`);
|
handleError(error, `Error while deleting trigger ${triggerId}`, "DeleteTrigger");
|
||||||
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, "DeleteUserDefinedFunction", `Error while deleting user defined function ${id}`);
|
handleError(error, `Error while deleting user defined function ${id}`, "DeleteUserDefinedFunction");
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
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, "ReadCollection", `Error while querying container ${collectionId}`);
|
handleError(error, `Error while querying container ${collectionId}`, "ReadCollection");
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels";
|
import { HttpHeaders } from "../Constants";
|
||||||
|
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { 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";
|
||||||
@@ -8,114 +11,113 @@ 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 { readOfferWithSDK } from "./readOfferWithSDK";
|
import { readOffers } from "./readOffers";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise<Offer> => {
|
export const readCollectionOffer = async (
|
||||||
|
params: DataModels.ReadCollectionOfferParams
|
||||||
|
): Promise<DataModels.OfferWithHeaders> => {
|
||||||
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
|
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 {
|
||||||
if (
|
const response = await client()
|
||||||
window.authType === AuthType.AAD &&
|
.offer(offerId)
|
||||||
!userContext.useSDKOperations &&
|
.read(options);
|
||||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
return (
|
||||||
) {
|
response && {
|
||||||
return await readCollectionOfferWithARM(params.databaseId, params.collectionId);
|
...response.resource,
|
||||||
}
|
headers: response.headers
|
||||||
|
}
|
||||||
return await readOfferWithSDK(params.offerId, params.collectionResourceId);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "ReadCollectionOffer", `Error while querying offer for collection ${params.collectionId}`);
|
handleError(error, `Error while querying offer for collection ${params.collectionId}`, "ReadCollectionOffer");
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const readCollectionOfferWithARM = async (databaseId: string, collectionId: string): Promise<Offer> => {
|
const getCollectionOfferIdWithARM = async (databaseId: string, collectionId: string): Promise<string> => {
|
||||||
|
let rpResponse;
|
||||||
const subscriptionId = userContext.subscriptionId;
|
const 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) {
|
||||||
let rpResponse;
|
case DefaultAccountExperienceType.DocumentDB:
|
||||||
try {
|
rpResponse = await getSqlContainerThroughput(
|
||||||
switch (defaultExperience) {
|
subscriptionId,
|
||||||
case DefaultAccountExperienceType.DocumentDB:
|
resourceGroup,
|
||||||
rpResponse = await getSqlContainerThroughput(
|
accountName,
|
||||||
subscriptionId,
|
databaseId,
|
||||||
resourceGroup,
|
collectionId
|
||||||
accountName,
|
);
|
||||||
databaseId,
|
break;
|
||||||
collectionId
|
case DefaultAccountExperienceType.MongoDB:
|
||||||
);
|
rpResponse = await getMongoDBCollectionThroughput(
|
||||||
break;
|
subscriptionId,
|
||||||
case DefaultAccountExperienceType.MongoDB:
|
resourceGroup,
|
||||||
rpResponse = await getMongoDBCollectionThroughput(
|
accountName,
|
||||||
subscriptionId,
|
databaseId,
|
||||||
resourceGroup,
|
collectionId
|
||||||
accountName,
|
);
|
||||||
databaseId,
|
break;
|
||||||
collectionId
|
case DefaultAccountExperienceType.Cassandra:
|
||||||
);
|
rpResponse = await getCassandraTableThroughput(
|
||||||
break;
|
subscriptionId,
|
||||||
case DefaultAccountExperienceType.Cassandra:
|
resourceGroup,
|
||||||
rpResponse = await getCassandraTableThroughput(
|
accountName,
|
||||||
subscriptionId,
|
databaseId,
|
||||||
resourceGroup,
|
collectionId
|
||||||
accountName,
|
);
|
||||||
databaseId,
|
break;
|
||||||
collectionId
|
case DefaultAccountExperienceType.Graph:
|
||||||
);
|
rpResponse = await getGremlinGraphThroughput(
|
||||||
break;
|
subscriptionId,
|
||||||
case DefaultAccountExperienceType.Graph:
|
resourceGroup,
|
||||||
rpResponse = await getGremlinGraphThroughput(
|
accountName,
|
||||||
subscriptionId,
|
databaseId,
|
||||||
resourceGroup,
|
collectionId
|
||||||
accountName,
|
);
|
||||||
databaseId,
|
break;
|
||||||
collectionId
|
case DefaultAccountExperienceType.Table:
|
||||||
);
|
rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId);
|
||||||
break;
|
break;
|
||||||
case DefaultAccountExperienceType.Table:
|
default:
|
||||||
rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId);
|
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code !== "NotFound") {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resource = rpResponse?.properties?.resource;
|
return rpResponse?.name;
|
||||||
if (resource) {
|
};
|
||||||
const offerId: string = rpResponse.name;
|
|
||||||
const minimumThroughput: number =
|
const getCollectionOfferIdWithSDK = async (collectionResourceId: string): Promise<string> => {
|
||||||
typeof resource.minimumThroughput === "string"
|
const offers = await readOffers();
|
||||||
? parseInt(resource.minimumThroughput)
|
const offer = offers.find(offer => offer.resource === collectionResourceId);
|
||||||
: resource.minimumThroughput;
|
return offer?.id;
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const readCollectionQuotaInfo = async (
|
|||||||
|
|
||||||
return quota;
|
return quota;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "ReadCollectionQuotaInfo", `Error while querying quota info for container ${collection.id}`);
|
handleError(error, `Error while querying quota info for container ${collection.id}`, "ReadCollectionQuotaInfo");
|
||||||
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, "ReadCollections", `Error while querying containers for database ${databaseId}`);
|
handleError(error, `Error while querying containers for database ${databaseId}`, "ReadCollections");
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -1,43 +1,66 @@
|
|||||||
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
|
import { HttpHeaders } from "../Constants";
|
||||||
|
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
import { 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 { readOfferWithSDK } from "./readOfferWithSDK";
|
import { readOffers } from "./readOffers";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
|
export const readDatabaseOffer = async (
|
||||||
|
params: DataModels.ReadDatabaseOfferParams
|
||||||
|
): Promise<DataModels.OfferWithHeaders> => {
|
||||||
const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`);
|
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 {
|
||||||
if (
|
const response = await client()
|
||||||
window.authType === AuthType.AAD &&
|
.offer(offerId)
|
||||||
!userContext.useSDKOperations &&
|
.read(options);
|
||||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
return (
|
||||||
) {
|
response && {
|
||||||
return await readDatabaseOfferWithARM(params.databaseId);
|
...response.resource,
|
||||||
}
|
headers: response.headers
|
||||||
|
}
|
||||||
return await readOfferWithSDK(params.offerId, params.databaseResourceId);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "ReadDatabaseOffer", `Error while querying offer for database ${params.databaseId}`);
|
handleError(error, `Error while querying offer for database ${params.databaseId}`, "ReadDatabaseOffer");
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
|
const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> => {
|
||||||
|
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:
|
||||||
@@ -55,39 +78,18 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
|
|||||||
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 resource = rpResponse?.properties?.resource;
|
|
||||||
if (resource) {
|
const getDatabaseOfferIdWithSDK = async (databaseResourceId: string): Promise<string> => {
|
||||||
const offerId: string = rpResponse.name;
|
const offers = await readOffers();
|
||||||
const minimumThroughput: number =
|
const offer = offers.find(offer => offer.resource === databaseResourceId);
|
||||||
typeof resource.minimumThroughput === "string"
|
return offer?.id;
|
||||||
? 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, "ReadDatabases", `Error while querying databases`);
|
handleError(error, `Error while querying databases`, "ReadDatabases");
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ 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";
|
||||||
|
|
||||||
@@ -22,9 +24,35 @@ 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, "ReadMongoDBCollection", `Error while reading container ${collectionId}`);
|
handleError(error, `Error while reading container ${collectionId}`, "ReadMongoDBCollection");
|
||||||
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import { HttpHeaders } from "../Constants";
|
|
||||||
import { Offer } from "../../Contracts/DataModels";
|
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
|
||||||
import { client } from "../CosmosClient";
|
|
||||||
import { parseSDKOfferResponse } from "../OfferUtility";
|
|
||||||
import { readOffers } from "./readOffers";
|
|
||||||
|
|
||||||
export const readOfferWithSDK = async (offerId: string, resourceId: string): Promise<Offer> => {
|
|
||||||
if (!offerId) {
|
|
||||||
const offers = await readOffers();
|
|
||||||
const offer = offers.find(offer => offer.resource === resourceId);
|
|
||||||
|
|
||||||
if (!offer) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
offerId = offer.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: RequestOptions = {
|
|
||||||
initialHeaders: {
|
|
||||||
[HttpHeaders.populateCollectionThroughputInfo]: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const response = await client()
|
|
||||||
.offer(offerId)
|
|
||||||
.read(options);
|
|
||||||
|
|
||||||
return parseSDKOfferResponse(response);
|
|
||||||
};
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { SDKOfferDefinition } from "../../Contracts/DataModels";
|
import { Offer } from "../../Contracts/DataModels";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { handleError, getErrorMessage } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
|
|
||||||
export const readOffers = async (): Promise<SDKOfferDefinition[]> => {
|
export const readOffers = async (): Promise<Offer[]> => {
|
||||||
const clearMessage = logConsoleProgress(`Querying offers`);
|
const clearMessage = logConsoleProgress(`Querying offers`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -13,11 +13,11 @@ export const readOffers = async (): Promise<SDKOfferDefinition[]> => {
|
|||||||
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 (getErrorMessage(error)?.includes("Reading or replacing offers is not supported for serverless accounts")) {
|
if (error.message.includes("Reading or replacing offers is not supported for serverless accounts")) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
handleError(error, "ReadOffers", `Error while querying offers`);
|
handleError(error, `Error while querying offers`, "ReadOffers");
|
||||||
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, "ReadStoredProcedures", `Failed to query stored procedures for container ${collectionId}`);
|
handleError(error, `Failed to query stored procedures for container ${collectionId}`, "ReadStoredProcedures");
|
||||||
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, "ReadTriggers", `Failed to query triggers for container ${collectionId}`);
|
handleError(error, `Failed to query triggers for container ${collectionId}`, "ReadTriggers");
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ export async function readUserDefinedFunctions(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(
|
handleError(
|
||||||
error,
|
error,
|
||||||
"ReadUserDefinedFunctions",
|
`Failed to query user defined functions for container ${collectionId}`,
|
||||||
`Failed to query user defined functions for container ${collectionId}`
|
"ReadUserDefinedFunctions"
|
||||||
);
|
);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
20
src/Common/dataAccess/sendNotificationForError.ts
Normal file
20
src/Common/dataAccess/sendNotificationForError.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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, "UpdateCollection", `Failed to update container ${collectionId}`);
|
handleError(error, `Failed to update container ${collectionId}`, "UpdateCollection");
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
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, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
|
import { Offer, 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 {
|
||||||
@@ -73,7 +72,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, "UpdateCollection", `Error updating offer for ${offerResourceText}`);
|
handleError(error, `Error updating offer for ${offerResourceText}`, "UpdateCollection");
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
@@ -374,21 +373,20 @@ const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpd
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> => {
|
const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> => {
|
||||||
const sdkOfferDefinition = params.currentOffer.offerDefinition;
|
const currentOffer = params.currentOffer;
|
||||||
const newOffer: SDKOfferDefinition = {
|
const newOffer: Offer = {
|
||||||
content: {
|
content: {
|
||||||
offerThroughput: undefined,
|
offerThroughput: undefined
|
||||||
offerIsRUPerMinuteThroughputEnabled: false
|
|
||||||
},
|
},
|
||||||
_etag: undefined,
|
_etag: undefined,
|
||||||
_ts: undefined,
|
_ts: undefined,
|
||||||
_rid: sdkOfferDefinition._rid,
|
_rid: currentOffer._rid,
|
||||||
_self: sdkOfferDefinition._self,
|
_self: currentOffer._self,
|
||||||
id: sdkOfferDefinition.id,
|
id: currentOffer.id,
|
||||||
offerResourceId: sdkOfferDefinition.offerResourceId,
|
offerResourceId: currentOffer.offerResourceId,
|
||||||
offerVersion: sdkOfferDefinition.offerVersion,
|
offerVersion: currentOffer.offerVersion,
|
||||||
offerType: sdkOfferDefinition.offerType,
|
offerType: currentOffer.offerType,
|
||||||
resource: sdkOfferDefinition.resource
|
resource: currentOffer.resource
|
||||||
};
|
};
|
||||||
|
|
||||||
if (params.autopilotThroughput) {
|
if (params.autopilotThroughput) {
|
||||||
@@ -416,6 +414,5 @@ 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);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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
|
||||||
|
});
|
||||||
|
expect(window.fetch).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
50
src/Common/dataAccess/updateOfferThroughputBeyondLimit.ts
Normal file
50
src/Common/dataAccess/updateOfferThroughputBeyondLimit.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
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;
|
||||||
|
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, "UpdateStoredProcedure", `Error while updating stored procedure ${storedProcedure.id}`);
|
handleError(error, `Error while updating stored procedure ${storedProcedure.id}`, "UpdateStoredProcedure");
|
||||||
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, "UpdateTrigger", `Error while updating trigger ${trigger.id}`);
|
handleError(error, `Error while updating trigger ${trigger.id}`, "UpdateTrigger");
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -66,8 +66,8 @@ export async function updateUserDefinedFunction(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(
|
handleError(
|
||||||
error,
|
error,
|
||||||
"UpdateUserupdateUserDefinedFunction",
|
`Error while updating user defined function ${userDefinedFunction.id}`,
|
||||||
`Error while updating user defined function ${userDefinedFunction.id}`
|
"UpdateUserupdateUserDefinedFunction"
|
||||||
);
|
);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -88,38 +88,6 @@ 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;
|
||||||
@@ -130,8 +98,6 @@ 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 {
|
||||||
@@ -208,21 +174,11 @@ export interface QueryMetrics {
|
|||||||
vmExecutionTime: any;
|
vmExecutionTime: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Offer {
|
export interface Offer extends Resource {
|
||||||
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;
|
|
||||||
collectionThroughputInfo?: OfferThroughputInfo;
|
collectionThroughputInfo?: OfferThroughputInfo;
|
||||||
offerAutopilotSettings?: AutoPilotOfferSettings;
|
offerAutopilotSettings?: AutoPilotOfferSettings;
|
||||||
};
|
};
|
||||||
@@ -230,6 +186,10 @@ export interface SDKOfferDefinition 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;
|
||||||
@@ -255,12 +215,23 @@ 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;
|
||||||
offerThroughput: number;
|
offerThroughput: number;
|
||||||
databaseLevelThroughput: boolean;
|
databaseLevelThroughput: boolean;
|
||||||
rupmEnabled?: boolean;
|
|
||||||
partitionKey?: PartitionKey;
|
partitionKey?: PartitionKey;
|
||||||
indexingPolicy?: IndexingPolicy;
|
indexingPolicy?: IndexingPolicy;
|
||||||
uniqueKeyPolicy?: UniqueKeyPolicy;
|
uniqueKeyPolicy?: UniqueKeyPolicy;
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export interface LogEntry {
|
|||||||
/**
|
/**
|
||||||
* The message code.
|
* The message code.
|
||||||
*/
|
*/
|
||||||
code?: number | string;
|
code?: number;
|
||||||
/**
|
/**
|
||||||
* Any additional data to be logged.
|
* Any additional data to be logged.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -32,8 +32,7 @@ export enum MessageTypes {
|
|||||||
GetArcadiaToken,
|
GetArcadiaToken,
|
||||||
CreateWorkspace,
|
CreateWorkspace,
|
||||||
CreateSparkPool,
|
CreateSparkPool,
|
||||||
RefreshDatabaseAccount,
|
RefreshDatabaseAccount
|
||||||
InitTestExplorer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Versions, ActionContracts, Diagnostics };
|
export { Versions, ActionContracts, Diagnostics };
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
export enum SubscriptionType {
|
|
||||||
Benefits,
|
|
||||||
EA,
|
|
||||||
Free,
|
|
||||||
Internal,
|
|
||||||
PAYG
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,6 @@ 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>;
|
||||||
@@ -116,8 +115,6 @@ 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>;
|
||||||
@@ -361,7 +358,6 @@ export enum CollectionTabKind {
|
|||||||
SparkMasterTab = 16,
|
SparkMasterTab = 16,
|
||||||
Gallery = 17,
|
Gallery = 17,
|
||||||
NotebookViewer = 18,
|
NotebookViewer = 18,
|
||||||
Schema = 19,
|
|
||||||
SettingsV2 = 19
|
SettingsV2 = 19
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,6 +412,14 @@ 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,6 +44,10 @@ describe("Component Registerer", () => {
|
|||||||
expect(ko.components.isRegistered("user-defined-function-tab")).toBe(true);
|
expect(ko.components.isRegistered("user-defined-function-tab")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should register settings-tab component", () => {
|
||||||
|
expect(ko.components.isRegistered("settings-tab")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("should register settings-tab-v2 component", () => {
|
it("should register settings-tab-v2 component", () => {
|
||||||
expect(ko.components.isRegistered("settings-tab-v2")).toBe(true);
|
expect(ko.components.isRegistered("settings-tab-v2")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ 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,9 +1,12 @@
|
|||||||
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 { IContextualMenuItem, IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu";
|
import {
|
||||||
|
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;
|
||||||
@@ -44,7 +47,7 @@ export class ArcadiaMenuPicker extends React.Component<ArcadiaMenuPickerProps, A
|
|||||||
selectedSparkPool: item.text
|
selectedSparkPool: item.text
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.logError(getErrorMessage(error), "ArcadiaMenuPicker/_onSparkPoolClicked");
|
Logger.logError(error, "ArcadiaMenuPicker/_onSparkPoolClicked");
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
|||||||
onChange?: (_?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => void;
|
onChange?: (_?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => void;
|
||||||
}[] = [
|
}[] = [
|
||||||
{ key: "feature.enablechangefeedpolicy", label: "Enable change feed policy", value: "true" },
|
{ key: "feature.enablechangefeedpolicy", label: "Enable change feed policy", value: "true" },
|
||||||
{ key: "feature.enablerupm", label: "Enable RUPM", value: "true" },
|
|
||||||
{ key: "feature.dataexplorerexecutesproc", label: "Execute stored procedure", value: "true" },
|
{ key: "feature.dataexplorerexecutesproc", label: "Execute stored procedure", value: "true" },
|
||||||
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
|
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
|
||||||
{ key: "feature.enablettl", label: "Enable TTL", value: "true" },
|
{ key: "feature.enablettl", label: "Enable TTL", value: "true" },
|
||||||
|
|||||||
@@ -131,12 +131,6 @@ exports[`Feature panel renders all flags 1`] = `
|
|||||||
label="Enable change feed policy"
|
label="Enable change feed policy"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
/>
|
/>
|
||||||
<StyledCheckboxBase
|
|
||||||
checked={false}
|
|
||||||
key="feature.enablerupm"
|
|
||||||
label="Enable RUPM"
|
|
||||||
onChange={[Function]}
|
|
||||||
/>
|
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
checked={false}
|
checked={false}
|
||||||
key="feature.dataexplorerexecutesproc"
|
key="feature.dataexplorerexecutesproc"
|
||||||
|
|||||||
@@ -4,10 +4,12 @@
|
|||||||
|
|
||||||
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;
|
||||||
@@ -39,10 +41,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,10 +71,9 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
|||||||
params: Map<string, string>
|
params: Map<string, string>
|
||||||
): string {
|
): string {
|
||||||
if (!serverInfo.notebookServerEndpoint) {
|
if (!serverInfo.notebookServerEndpoint) {
|
||||||
handleError(
|
const error = "Notebook server endpoint not defined. Terminal will fail to connect to jupyter server.";
|
||||||
"Notebook server endpoint not defined. Terminal will fail to connect to jupyter server.",
|
Logger.logError(error, "NotebookTerminalComponent/createNotebookAppSrc");
|
||||||
"NotebookTerminalComponent/createNotebookAppSrc"
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||||
);
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
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;
|
||||||
@@ -44,7 +45,9 @@ export class CodeOfConductComponent extends React.Component<CodeOfConductCompone
|
|||||||
|
|
||||||
this.props.onAcceptCodeOfConduct(response.data);
|
this.props.onAcceptCodeOfConduct(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "CodeOfConductComponent/acceptCodeOfConduct", "Failed to accept code of conduct");
|
const message = `Failed to accept code of conduct: ${error}`;
|
||||||
|
Logger.logError(message, "CodeOfConductComponent/acceptCodeOfConduct");
|
||||||
|
logConsoleError(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,11 @@ 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";
|
||||||
@@ -25,7 +28,6 @@ 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;
|
||||||
@@ -352,7 +354,9 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
|
|
||||||
this.sampleNotebooks = response.data;
|
this.sampleNotebooks = response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "GalleryViewerComponent/loadSampleNotebooks", "Failed to load sample notebooks");
|
const message = `Failed to load sample notebooks: ${error}`;
|
||||||
|
Logger.logError(message, "GalleryViewerComponent/loadSampleNotebooks");
|
||||||
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,7 +382,9 @@ 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) {
|
||||||
handleError(error, "GalleryViewerComponent/loadPublicNotebooks", "Failed to load public notebooks");
|
const message = `Failed to load public notebooks: ${error}`;
|
||||||
|
Logger.logError(message, "GalleryViewerComponent/loadPublicNotebooks");
|
||||||
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,7 +404,9 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
|
|
||||||
this.favoriteNotebooks = response.data;
|
this.favoriteNotebooks = response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "GalleryViewerComponent/loadFavoriteNotebooks", "Failed to load favorite notebooks");
|
const message = `Failed to load favorite notebooks: ${error}`;
|
||||||
|
Logger.logError(message, "GalleryViewerComponent/loadFavoriteNotebooks");
|
||||||
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,7 +432,9 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
|
|
||||||
this.publishedNotebooks = response.data;
|
this.publishedNotebooks = response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "GalleryViewerComponent/loadPublishedNotebooks", "Failed to load published notebooks");
|
const message = `Failed to load published notebooks: ${error}`;
|
||||||
|
Logger.logError(message, "GalleryViewerComponent/loadPublishedNotebooks");
|
||||||
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ 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;
|
||||||
@@ -101,7 +100,9 @@ export class NotebookViewerComponent extends React.Component<NotebookViewerCompo
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.setState({ showProgressBar: false });
|
this.setState({ showProgressBar: false });
|
||||||
handleError(error, "NotebookViewerComponent/loadNotebookContent", "Failed to load notebook content");
|
const message = `Failed to load notebook content: ${error}`;
|
||||||
|
Logger.logError(message, "NotebookViewerComponent/loadNotebookContent");
|
||||||
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ 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;
|
||||||
@@ -245,9 +244,7 @@ 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/getIndexTransformationProgress", () => ({
|
jest.mock("../../../Common/dataAccess/readMongoDBCollection", () => ({
|
||||||
getIndexTransformationProgress: jest.fn().mockReturnValue(undefined)
|
getMongoDBCollectionIndexTransformationProgress: 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,11 +89,12 @@ describe("SettingsComponent", () => {
|
|||||||
it("auto pilot helper functions pass on correct value", () => {
|
it("auto pilot helper functions pass on correct value", () => {
|
||||||
const newCollection = { ...collection };
|
const newCollection = { ...collection };
|
||||||
newCollection.offer = ko.observable<DataModels.Offer>({
|
newCollection.offer = ko.observable<DataModels.Offer>({
|
||||||
autoscaleMaxThroughput: 10000,
|
content: {
|
||||||
manualThroughput: undefined,
|
offerAutopilotSettings: {
|
||||||
minimumThroughput: 400,
|
maxThroughput: 10000
|
||||||
id: "test"
|
}
|
||||||
});
|
}
|
||||||
|
} as DataModels.Offer);
|
||||||
|
|
||||||
const props = { ...baseProps };
|
const props = { ...baseProps };
|
||||||
props.settingsTab.collection = newCollection;
|
props.settingsTab.collection = newCollection;
|
||||||
@@ -186,6 +187,21 @@ 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,23 +2,28 @@ 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 { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
|
import { throughputUnit } 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,
|
||||||
@@ -41,10 +46,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 { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
|
import {
|
||||||
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
|
getMongoDBCollectionIndexTransformationProgress,
|
||||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
readMongoDBCollectionThroughRP
|
||||||
import { isEmpty } from "underscore";
|
} from "../../../Common/dataAccess/readMongoDBCollection";
|
||||||
|
|
||||||
interface SettingsV2TabInfo {
|
interface SettingsV2TabInfo {
|
||||||
tab: SettingsV2TabTypes;
|
tab: SettingsV2TabTypes;
|
||||||
@@ -206,7 +211,6 @@ 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();
|
||||||
@@ -223,10 +227,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
|
|
||||||
public loadMongoIndexes = async (): Promise<void> => {
|
public loadMongoIndexes = async (): Promise<void> => {
|
||||||
if (
|
if (
|
||||||
|
this.container.isMongoIndexEditorEnabled() &&
|
||||||
this.container.isPreferredApiMongoDB() &&
|
this.container.isPreferredApiMongoDB() &&
|
||||||
this.container.isEnableMongoCapabilityPresent() &&
|
this.container.isEnableMongoCapabilityPresent() &&
|
||||||
this.container.databaseAccount()
|
this.container.databaseAccount()
|
||||||
) {
|
) {
|
||||||
|
await this.refreshIndexTransformationProgress();
|
||||||
|
|
||||||
this.mongoDBCollectionResource = await readMongoDBCollectionThroughRP(
|
this.mongoDBCollectionResource = await readMongoDBCollectionThroughRP(
|
||||||
this.collection.databaseId,
|
this.collection.databaseId,
|
||||||
this.collection.id()
|
this.collection.id()
|
||||||
@@ -241,7 +248,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
};
|
};
|
||||||
|
|
||||||
public refreshIndexTransformationProgress = async (): Promise<void> => {
|
public refreshIndexTransformationProgress = async (): Promise<void> => {
|
||||||
const currentProgress = await getIndexTransformationProgress(this.collection.databaseId, this.collection.id());
|
const currentProgress = await getMongoDBCollectionIndexTransformationProgress(
|
||||||
|
this.collection.databaseId,
|
||||||
|
this.collection.id()
|
||||||
|
);
|
||||||
this.setState({ indexTransformationProgress: currentProgress });
|
this.setState({ indexTransformationProgress: currentProgress });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -270,14 +280,19 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
};
|
};
|
||||||
|
|
||||||
private setAutoPilotStates = (): void => {
|
private setAutoPilotStates = (): void => {
|
||||||
const autoscaleMaxThroughput = this.collection?.offer()?.autoscaleMaxThroughput;
|
const offer = this.collection?.offer && this.collection.offer();
|
||||||
|
const offerAutopilotSettings = offer?.content?.offerAutopilotSettings;
|
||||||
|
|
||||||
if (autoscaleMaxThroughput && AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
|
if (
|
||||||
|
offerAutopilotSettings &&
|
||||||
|
offerAutopilotSettings.maxThroughput &&
|
||||||
|
AutoPilotUtils.isValidAutoPilotThroughput(offerAutopilotSettings.maxThroughput)
|
||||||
|
) {
|
||||||
this.setState({
|
this.setState({
|
||||||
isAutoPilotSelected: true,
|
isAutoPilotSelected: true,
|
||||||
wasAutopilotOriginallySet: true,
|
wasAutopilotOriginallySet: true,
|
||||||
autoPilotThroughput: autoscaleMaxThroughput,
|
autoPilotThroughput: offerAutopilotSettings.maxThroughput,
|
||||||
autoPilotThroughputBaseline: autoscaleMaxThroughput
|
autoPilotThroughputBaseline: offerAutopilotSettings.maxThroughput
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -295,7 +310,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
!!this.collection.conflictResolutionPolicy();
|
!!this.collection.conflictResolutionPolicy();
|
||||||
|
|
||||||
public isOfferReplacePending = (): boolean => {
|
public isOfferReplacePending = (): boolean => {
|
||||||
return !!this.collection?.offer()?.headers?.[Constants.HttpHeaders.offerReplacePending];
|
const offer = this.collection?.offer && this.collection.offer();
|
||||||
|
return (
|
||||||
|
offer &&
|
||||||
|
Object.keys(offer).find(value => value === "headers") &&
|
||||||
|
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
public onSaveClick = async (): Promise<void> => {
|
public onSaveClick = async (): Promise<void> => {
|
||||||
@@ -331,7 +351,6 @@ 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;
|
||||||
@@ -367,11 +386,6 @@ 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,
|
||||||
@@ -423,8 +437,7 @@ 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: getErrorMessage(error),
|
error: error.message
|
||||||
errorStack: getErrorStack(error)
|
|
||||||
},
|
},
|
||||||
startKey
|
startKey
|
||||||
);
|
);
|
||||||
@@ -433,33 +446,100 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.isScaleSaveable) {
|
if (this.state.isScaleSaveable) {
|
||||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
const newThroughput = this.state.throughput;
|
||||||
databaseId: this.collection.databaseId,
|
const newOffer: DataModels.Offer = { ...this.collection.offer() };
|
||||||
collectionId: this.collection.id(),
|
const originalThroughputValue: number = this.state.throughput;
|
||||||
currentOffer: this.collection.offer(),
|
|
||||||
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
if (newOffer.content) {
|
||||||
manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput
|
newOffer.content.offerThroughput = newThroughput;
|
||||||
};
|
} else {
|
||||||
if (this.hasProvisioningTypeChanged()) {
|
newOffer.content = {
|
||||||
if (this.state.isAutoPilotSelected) {
|
offerThroughput: newThroughput
|
||||||
updateOfferParams.migrateToAutoPilot = true;
|
};
|
||||||
} else {
|
|
||||||
updateOfferParams.migrateToManual = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
|
||||||
this.collection.offer(updatedOffer);
|
const headerOptions: RequestOptions = { initialHeaders: {} };
|
||||||
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
|
||||||
if (this.state.isAutoPilotSelected) {
|
if (this.state.isAutoPilotSelected) {
|
||||||
this.setState({
|
newOffer.content.offerAutopilotSettings = {
|
||||||
autoPilotThroughput: updatedOffer.autoscaleMaxThroughput,
|
maxThroughput: this.state.autoPilotThroughput
|
||||||
autoPilotThroughputBaseline: updatedOffer.autoscaleMaxThroughput
|
};
|
||||||
});
|
|
||||||
|
// 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 {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
throughput: updatedOffer.manualThroughput,
|
isAutoPilotSelected: false
|
||||||
throughputBaseline: updatedOffer.manualThroughput
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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
|
||||||
|
};
|
||||||
|
|
||||||
|
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) {
|
||||||
|
this.setState({
|
||||||
|
autoPilotThroughput: updatedOffer.content.offerAutopilotSettings.maxThroughput,
|
||||||
|
autoPilotThroughputBaseline: updatedOffer.content.offerAutopilotSettings.maxThroughput
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
throughput: updatedOffer.content.offerThroughput,
|
||||||
|
throughputBaseline: updatedOffer.content.offerThroughput
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.container.isRefreshingExplorer(false);
|
this.container.isRefreshingExplorer(false);
|
||||||
@@ -477,10 +557,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
},
|
},
|
||||||
startKey
|
startKey
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (reason) {
|
||||||
this.container.isRefreshingExplorer(false);
|
this.container.isRefreshingExplorer(false);
|
||||||
this.props.settingsTab.isExecutionError(true);
|
this.props.settingsTab.isExecutionError(true);
|
||||||
console.error(error);
|
console.error(reason);
|
||||||
traceFailure(
|
traceFailure(
|
||||||
Action.SettingsV2Updated,
|
Action.SettingsV2Updated,
|
||||||
{
|
{
|
||||||
@@ -490,8 +570,7 @@ 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: getErrorMessage(error),
|
error: reason.message
|
||||||
errorStack: getErrorStack(error)
|
|
||||||
},
|
},
|
||||||
startKey
|
startKey
|
||||||
);
|
);
|
||||||
@@ -725,7 +804,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const offerThroughput = this.collection.offer()?.manualThroughput;
|
const offerThroughput = this.collection?.offer && this.collection.offer()?.content?.offerThroughput;
|
||||||
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
||||||
? ChangeFeedPolicyState.On
|
? ChangeFeedPolicyState.On
|
||||||
: ChangeFeedPolicyState.Off;
|
: ChangeFeedPolicyState.Off;
|
||||||
@@ -864,8 +943,6 @@ 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
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -916,18 +993,15 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||||
content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />
|
content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />
|
||||||
});
|
});
|
||||||
} else if (this.container.isPreferredApiMongoDB()) {
|
} else if (
|
||||||
if (isEmpty(this.container.features())) {
|
this.container.isMongoIndexEditorEnabled() &&
|
||||||
tabs.push({
|
this.container.isPreferredApiMongoDB() &&
|
||||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
this.container.isEnableMongoCapabilityPresent()
|
||||||
content: mongoIndexingPolicyAADError
|
) {
|
||||||
});
|
tabs.push({
|
||||||
} else if (this.container.isEnableMongoCapabilityPresent()) {
|
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||||
tabs.push({
|
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />
|
||||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
});
|
||||||
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.hasConflictResolution()) {
|
if (this.hasConflictResolution()) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
getEstimatedAutoscaleSpendElement,
|
getEstimatedAutoscaleSpendElement,
|
||||||
manualToAutoscaleDisclaimerElement,
|
manualToAutoscaleDisclaimerElement,
|
||||||
ttlWarning,
|
ttlWarning,
|
||||||
indexingPolicynUnsavedWarningMessage,
|
indexingPolicyTTLWarningMessage,
|
||||||
updateThroughputBeyondLimitWarningMessage,
|
updateThroughputBeyondLimitWarningMessage,
|
||||||
updateThroughputDelayedApplyWarningMessage,
|
updateThroughputDelayedApplyWarningMessage,
|
||||||
getThroughputApplyDelayedMessage,
|
getThroughputApplyDelayedMessage,
|
||||||
@@ -31,18 +31,18 @@ class SettingsRenderUtilsTestComponent extends React.Component {
|
|||||||
{getAutoPilotV3SpendElement(1000, true)}
|
{getAutoPilotV3SpendElement(1000, true)}
|
||||||
{getAutoPilotV3SpendElement(undefined, true)}
|
{getAutoPilotV3SpendElement(undefined, true)}
|
||||||
|
|
||||||
{getEstimatedSpendElement(1000, "mooncake", 2, false, true)}
|
{getEstimatedSpendElement(1000, "mooncake", 2, false)}
|
||||||
|
|
||||||
{getEstimatedAutoscaleSpendElement(1000, "mooncake", 2, false)}
|
{getEstimatedAutoscaleSpendElement(1000, "mooncake", 2, false)}
|
||||||
|
|
||||||
{manualToAutoscaleDisclaimerElement}
|
{manualToAutoscaleDisclaimerElement}
|
||||||
{ttlWarning}
|
{ttlWarning}
|
||||||
{indexingPolicynUnsavedWarningMessage}
|
{indexingPolicyTTLWarningMessage}
|
||||||
{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")}
|
{getThroughputApplyShortDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||||
{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";
|
||||||
|
|
||||||
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } };
|
const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } };
|
||||||
|
|
||||||
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
||||||
label: {
|
label: {
|
||||||
@@ -199,10 +199,9 @@ export const getEstimatedSpendElement = (
|
|||||||
throughput: number,
|
throughput: number,
|
||||||
serverId: string,
|
serverId: string,
|
||||||
regions: number,
|
regions: number,
|
||||||
multimaster: boolean,
|
multimaster: boolean
|
||||||
rupmEnabled: boolean
|
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
const hourlyPrice: number = computeRUUsagePriceHourly(serverId, rupmEnabled, throughput, regions, multimaster);
|
const hourlyPrice: number = computeRUUsagePriceHourly(serverId, throughput, regions, multimaster);
|
||||||
const dailyPrice: number = hourlyPrice * 24;
|
const dailyPrice: number = hourlyPrice * 24;
|
||||||
const monthlyPrice: number = hourlyPrice * hoursInAMonth;
|
const monthlyPrice: number = hourlyPrice * hoursInAMonth;
|
||||||
const currency: string = getPriceCurrency(serverId);
|
const currency: string = getPriceCurrency(serverId);
|
||||||
@@ -245,9 +244,15 @@ export const ttlWarning: JSX.Element = (
|
|||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const indexingPolicynUnsavedWarningMessage: JSX.Element = (
|
export const indexingPolicyTTLWarningMessage: JSX.Element = (
|
||||||
<Text styles={infoAndToolTipTextStyle}>
|
<Text styles={infoAndToolTipTextStyle}>
|
||||||
You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes.
|
Changing the Indexing Policy impacts query results while the index transformation occurs. When a change is made and
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -319,13 +324,14 @@ 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)}
|
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, targetThroughput)}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -403,8 +409,8 @@ export const mongoIndexingPolicyAADError: JSX.Element = (
|
|||||||
|
|
||||||
export const mongoIndexTransformationRefreshingMessage: JSX.Element = (
|
export const mongoIndexTransformationRefreshingMessage: JSX.Element = (
|
||||||
<Stack horizontal {...mongoWarningStackProps}>
|
<Stack horizontal {...mongoWarningStackProps}>
|
||||||
<Text styles={infoAndToolTipTextStyle}>Refreshing index transformation progress</Text>
|
<Text>Refreshing index transformation progress</Text>
|
||||||
<Spinner size={SpinnerSize.small} />
|
<Spinner size={SpinnerSize.medium} />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -414,14 +420,14 @@ export const renderMongoIndexTransformationRefreshMessage = (
|
|||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
if (progress === 0) {
|
if (progress === 0) {
|
||||||
return (
|
return (
|
||||||
<Text styles={infoAndToolTipTextStyle}>
|
<Text>
|
||||||
{"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 styles={infoAndToolTipTextStyle}>
|
<Text>
|
||||||
{`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,9 +25,7 @@ describe("IndexingPolicyComponent", () => {
|
|||||||
},
|
},
|
||||||
onIndexingPolicyDirtyChange: () => {
|
onIndexingPolicyDirtyChange: () => {
|
||||||
return;
|
return;
|
||||||
},
|
}
|
||||||
indexTransformationProgress: undefined,
|
|
||||||
refreshIndexTransformationProgress: () => new Promise(jest.fn())
|
|
||||||
};
|
};
|
||||||
|
|
||||||
it("renders", () => {
|
it("renders", () => {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
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, isIndexTransforming } from "../SettingsUtils";
|
import { isDirty } from "../SettingsUtils";
|
||||||
import { MessageBar, MessageBarType, Stack } from "office-ui-fabric-react";
|
import { MessageBar, MessageBarType, Stack } from "office-ui-fabric-react";
|
||||||
import { indexingPolicynUnsavedWarningMessage, titleAndInputStackProps } from "../SettingsRenderUtils";
|
import { indexingPolicyTTLWarningMessage, titleAndInputStackProps } from "../SettingsRenderUtils";
|
||||||
import { IndexingPolicyRefreshComponent } from "./IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
|
||||||
|
|
||||||
export interface IndexingPolicyComponentProps {
|
export interface IndexingPolicyComponentProps {
|
||||||
shouldDiscardIndexingPolicy: boolean;
|
shouldDiscardIndexingPolicy: boolean;
|
||||||
@@ -13,8 +12,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,9 +51,6 @@ 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);
|
||||||
@@ -90,7 +84,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: isIndexTransforming(this.props.indexTransformationProgress),
|
readOnly: false,
|
||||||
ariaLabel: "Indexing Policy"
|
ariaLabel: "Indexing Policy"
|
||||||
});
|
});
|
||||||
if (this.indexingPolicyEditor) {
|
if (this.indexingPolicyEditor) {
|
||||||
@@ -114,12 +108,8 @@ 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}>{indexingPolicynUnsavedWarningMessage}</MessageBar>
|
<MessageBar messageBarType={MessageBarType.warning}>{indexingPolicyTTLWarningMessage}</MessageBar>
|
||||||
)}
|
)}
|
||||||
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
|
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
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>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
// 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,7 +2,6 @@ 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 = {
|
||||||
@@ -22,7 +21,10 @@ describe("MongoIndexingPolicyComponent", () => {
|
|||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
indexTransformationProgress: undefined,
|
indexTransformationProgress: undefined,
|
||||||
refreshIndexTransformationProgress: () => new Promise(jest.fn()),
|
refreshIndexTransformationProgress: () =>
|
||||||
|
new Promise(() => {
|
||||||
|
return;
|
||||||
|
}),
|
||||||
onMongoIndexingPolicySaveableChange: () => {
|
onMongoIndexingPolicySaveableChange: () => {
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
@@ -36,6 +38,16 @@ 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;
|
||||||
@@ -43,7 +55,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()).toBeUndefined();
|
expect(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage()).toEqual(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
const sampleWarning = "sampleWarning";
|
const sampleWarning = "sampleWarning";
|
||||||
@@ -101,12 +113,9 @@ describe("MongoIndexingPolicyComponent", () => {
|
|||||||
expect(mongoIndexingPolicyComponent.isMongoIndexingPolicyDiscardable()).toEqual(
|
expect(mongoIndexingPolicyComponent.isMongoIndexingPolicyDiscardable()).toEqual(
|
||||||
isMongoIndexingPolicyDiscardable
|
isMongoIndexingPolicyDiscardable
|
||||||
);
|
);
|
||||||
if (mongoWarningNotificationMessage) {
|
expect(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage()).toEqual(
|
||||||
const elementAsString = renderToString(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage());
|
mongoWarningNotificationMessage
|
||||||
expect(elementAsString).toContain(mongoWarningNotificationMessage);
|
);
|
||||||
} else {
|
|
||||||
expect(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage()).toBeUndefined();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user