mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-08 20:17:03 +00:00
Compare commits
2 Commits
user/swvis
...
remove-ru-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
627c346559 | ||
|
|
415ebc505b |
@@ -3,11 +3,7 @@ PORTAL_RUNNER_PASSWORD=
|
|||||||
PORTAL_RUNNER_SUBSCRIPTION=
|
PORTAL_RUNNER_SUBSCRIPTION=
|
||||||
PORTAL_RUNNER_RESOURCE_GROUP=
|
PORTAL_RUNNER_RESOURCE_GROUP=
|
||||||
PORTAL_RUNNER_DATABASE_ACCOUNT=
|
PORTAL_RUNNER_DATABASE_ACCOUNT=
|
||||||
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY=
|
|
||||||
PORTAL_RUNNER_CONNECTION_STRING=
|
PORTAL_RUNNER_CONNECTION_STRING=
|
||||||
NOTEBOOKS_TEST_RUNNER_TENANT_ID=
|
|
||||||
NOTEBOOKS_TEST_RUNNER_CLIENT_ID=
|
|
||||||
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET=
|
|
||||||
CASSANDRA_CONNECTION_STRING=
|
CASSANDRA_CONNECTION_STRING=
|
||||||
MONGO_CONNECTION_STRING=
|
MONGO_CONNECTION_STRING=
|
||||||
TABLES_CONNECTION_STRING=
|
TABLES_CONNECTION_STRING=
|
||||||
|
|||||||
@@ -202,6 +202,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 +290,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 +396,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
|
||||||
38
.github/workflows/ci.yml
vendored
38
.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]
|
||||||
@@ -127,8 +128,8 @@ jobs:
|
|||||||
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,13 +147,6 @@ 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 }}
|
||||||
@@ -165,7 +159,7 @@ jobs:
|
|||||||
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 +183,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
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -76,7 +76,17 @@ Unit tests are located adjacent to the code under test and run with [Jest](https
|
|||||||
|
|
||||||
#### End to End CI Tests
|
#### End to End CI Tests
|
||||||
|
|
||||||
Jest and Puppeteer are used for end to end browser based tests and are contained in `test/`. To run these tests locally:
|
[Cypress](https://www.cypress.io/) is used for end to end tests and are contained in `cypress/`. Currently, it operates as sub project with its own typescript config and dependencies. It also only operates against the emulator. To run cypress tests:
|
||||||
|
|
||||||
|
1. Ensure the emulator is running
|
||||||
|
2. Start cosmos explorer in emulator mode: `PLATFORM=Emulator npm run watch`
|
||||||
|
3. Move into `cypress/` folder: `cd cypress`
|
||||||
|
4. Install dependencies: `npm install`
|
||||||
|
5. Run cypress headless(`npm run test`) or in interactive mode(`npm run test:debug`)
|
||||||
|
|
||||||
|
#### End to End Production Tests
|
||||||
|
|
||||||
|
Jest and Puppeteer are used for end to end production runners and are contained in `test/`. To run these tests locally:
|
||||||
|
|
||||||
1. Copy .env.example to .env
|
1. Copy .env.example to .env
|
||||||
2. Update the values in .env including your local data explorer endpoint (ask a teammate/codeowner for help with .env values)
|
2. Update the values in .env including your local data explorer endpoint (ask a teammate/codeowner for help with .env values)
|
||||||
|
|||||||
4
cypress/.gitignore
vendored
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=="
|
||||||
|
});
|
||||||
4624
package-lock.json
generated
4624
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
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",
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -125,9 +125,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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ARMError } from "../Utils/arm/request";
|
import { ARMError } from "../Utils/arm/request";
|
||||||
import { HttpStatusCodes } from "./Constants";
|
import { HttpStatusCodes } from "./Constants";
|
||||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||||
import { SubscriptionType } from "../Contracts/SubscriptionType";
|
import { SubscriptionType } from "../Contracts/ViewModels";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import { logError } from "./Logger";
|
import { logError } from "./Logger";
|
||||||
import { sendMessage } from "./MessageHandler";
|
import { sendMessage } from "./MessageHandler";
|
||||||
|
|||||||
@@ -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 [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,22 +11,50 @@ 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 {
|
try {
|
||||||
if (
|
offerId = await getCollectionOfferIdWithARM(params.databaseId, params.collectionId);
|
||||||
window.authType === AuthType.AAD &&
|
} catch (error) {
|
||||||
!userContext.useSDKOperations &&
|
clearMessage();
|
||||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
if (error.code !== "NotFound") {
|
||||||
) {
|
throw error;
|
||||||
return await readCollectionOfferWithARM(params.databaseId, params.collectionId);
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
offerId = await getCollectionOfferIdWithSDK(params.collectionResourceId);
|
||||||
|
if (!offerId) {
|
||||||
|
clearMessage();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await readOfferWithSDK(params.offerId, params.collectionResourceId);
|
const options: RequestOptions = {
|
||||||
|
initialHeaders: {
|
||||||
|
[HttpHeaders.populateCollectionThroughputInfo]: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await client()
|
||||||
|
.offer(offerId)
|
||||||
|
.read(options);
|
||||||
|
return (
|
||||||
|
response && {
|
||||||
|
...response.resource,
|
||||||
|
headers: response.headers
|
||||||
|
}
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "ReadCollectionOffer", `Error while querying offer for collection ${params.collectionId}`);
|
handleError(error, "ReadCollectionOffer", `Error while querying offer for collection ${params.collectionId}`);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -32,14 +63,12 @@ export const readCollectionOffer = async (params: ReadCollectionOfferParams): Pr
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const readCollectionOfferWithARM = async (databaseId: string, collectionId: string): Promise<Offer> => {
|
const getCollectionOfferIdWithARM = async (databaseId: string, collectionId: string): Promise<string> => {
|
||||||
|
let rpResponse;
|
||||||
const subscriptionId = userContext.subscriptionId;
|
const 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 {
|
|
||||||
switch (defaultExperience) {
|
switch (defaultExperience) {
|
||||||
case DefaultAccountExperienceType.DocumentDB:
|
case DefaultAccountExperienceType.DocumentDB:
|
||||||
rpResponse = await getSqlContainerThroughput(
|
rpResponse = await getSqlContainerThroughput(
|
||||||
@@ -83,39 +112,12 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
|||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
if (error.code !== "NotFound") {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
return rpResponse?.name;
|
||||||
}
|
|
||||||
|
|
||||||
const resource = rpResponse?.properties?.resource;
|
|
||||||
if (resource) {
|
|
||||||
const offerId: string = rpResponse.name;
|
|
||||||
const minimumThroughput: number =
|
|
||||||
typeof resource.minimumThroughput === "string"
|
|
||||||
? parseInt(resource.minimumThroughput)
|
|
||||||
: resource.minimumThroughput;
|
|
||||||
const autoscaleSettings = resource.autoscaleSettings;
|
|
||||||
|
|
||||||
if (autoscaleSettings) {
|
|
||||||
return {
|
|
||||||
id: offerId,
|
|
||||||
autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
|
|
||||||
manualThroughput: undefined,
|
|
||||||
minimumThroughput
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
const getCollectionOfferIdWithSDK = async (collectionResourceId: string): Promise<string> => {
|
||||||
id: offerId,
|
const offers = await readOffers();
|
||||||
autoscaleMaxThroughput: undefined,
|
const offer = offers.find(offer => offer.resource === collectionResourceId);
|
||||||
manualThroughput: resource.throughput,
|
return offer?.id;
|
||||||
minimumThroughput
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import * as DataModels from "../../Contracts/DataModels";
|
|
||||||
import * as HeadersUtility from "../HeadersUtility";
|
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
|
||||||
import { ContainerDefinition, Resource } from "@azure/cosmos";
|
|
||||||
import { HttpHeaders } from "../Constants";
|
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
|
||||||
import { client } from "../CosmosClient";
|
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
|
||||||
|
|
||||||
interface ResourceWithStatistics {
|
|
||||||
statistics: DataModels.Statistic[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const readCollectionQuotaInfo = async (
|
|
||||||
collection: ViewModels.Collection
|
|
||||||
): Promise<DataModels.CollectionQuotaInfo> => {
|
|
||||||
const clearMessage = logConsoleProgress(`Querying containers for database ${collection.id}`);
|
|
||||||
const options: RequestOptions = {};
|
|
||||||
options.populateQuotaInfo = true;
|
|
||||||
options.initialHeaders = options.initialHeaders || {};
|
|
||||||
options.initialHeaders[HttpHeaders.populatePartitionStatistics] = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await client()
|
|
||||||
.database(collection.databaseId)
|
|
||||||
.container(collection.id())
|
|
||||||
.read(options);
|
|
||||||
const quota: DataModels.CollectionQuotaInfo = HeadersUtility.getQuota(response.headers);
|
|
||||||
const resource = response.resource as ContainerDefinition & Resource & ResourceWithStatistics;
|
|
||||||
quota["usageSizeInKB"] = resource.statistics.reduce(
|
|
||||||
(previousValue: number, currentValue: DataModels.Statistic) => previousValue + currentValue.sizeInKB,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
quota["numPartitions"] = resource.statistics.length;
|
|
||||||
quota["uniqueKeyPolicy"] = collection.uniqueKeyPolicy; // TODO: Remove after refactoring (#119617)
|
|
||||||
|
|
||||||
return quota;
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, "ReadCollectionQuotaInfo", `Error while querying quota info for container ${collection.id}`);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
clearMessage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,28 +1,51 @@
|
|||||||
|
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;
|
||||||
try {
|
if (!offerId) {
|
||||||
if (
|
offerId = await (window.authType === AuthType.AAD &&
|
||||||
window.authType === AuthType.AAD &&
|
|
||||||
!userContext.useSDKOperations &&
|
!userContext.useSDKOperations &&
|
||||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||||
) {
|
? getDatabaseOfferIdWithARM(params.databaseId)
|
||||||
return await readDatabaseOfferWithARM(params.databaseId);
|
: getDatabaseOfferIdWithSDK(params.databaseResourceId));
|
||||||
|
if (!offerId) {
|
||||||
|
clearMessage();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await readOfferWithSDK(params.offerId, params.databaseResourceId);
|
const options: RequestOptions = {
|
||||||
|
initialHeaders: {
|
||||||
|
[HttpHeaders.populateCollectionThroughputInfo]: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await client()
|
||||||
|
.offer(offerId)
|
||||||
|
.read(options);
|
||||||
|
return (
|
||||||
|
response && {
|
||||||
|
...response.resource,
|
||||||
|
headers: response.headers
|
||||||
|
}
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "ReadDatabaseOffer", `Error while querying offer for database ${params.databaseId}`);
|
handleError(error, "ReadDatabaseOffer", `Error while querying offer for database ${params.databaseId}`);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -31,13 +54,13 @@ export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promis
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
|
const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> => {
|
||||||
|
let rpResponse;
|
||||||
const subscriptionId = userContext.subscriptionId;
|
const 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 offerId: string = rpResponse.name;
|
|
||||||
const minimumThroughput: number =
|
|
||||||
typeof resource.minimumThroughput === "string"
|
|
||||||
? parseInt(resource.minimumThroughput)
|
|
||||||
: resource.minimumThroughput;
|
|
||||||
const autoscaleSettings = resource.autoscaleSettings;
|
|
||||||
|
|
||||||
if (autoscaleSettings) {
|
|
||||||
return {
|
|
||||||
id: offerId,
|
|
||||||
autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
|
|
||||||
manualThroughput: undefined,
|
|
||||||
minimumThroughput
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
const getDatabaseOfferIdWithSDK = async (databaseResourceId: string): Promise<string> => {
|
||||||
id: offerId,
|
const offers = await readOffers();
|
||||||
autoscaleMaxThroughput: undefined,
|
const offer = offers.find(offer => offer.resource === databaseResourceId);
|
||||||
manualThroughput: resource.throughput,
|
return offer?.id;
|
||||||
minimumThroughput
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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, getErrorMessage } 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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -374,21 +373,21 @@ 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
|
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 +415,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);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,12 @@ 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;
|
offerIsRUPerMinuteThroughputEnabled: boolean;
|
||||||
collectionThroughputInfo?: OfferThroughputInfo;
|
collectionThroughputInfo?: OfferThroughputInfo;
|
||||||
offerAutopilotSettings?: AutoPilotOfferSettings;
|
offerAutopilotSettings?: AutoPilotOfferSettings;
|
||||||
};
|
};
|
||||||
@@ -230,16 +187,8 @@ export interface SDKOfferDefinition extends Resource {
|
|||||||
offerResourceId?: string;
|
offerResourceId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionQuotaInfo {
|
export interface OfferWithHeaders extends Offer {
|
||||||
storedProcedures: number;
|
headers: any;
|
||||||
triggers: number;
|
|
||||||
functions: number;
|
|
||||||
documentsSize: number;
|
|
||||||
collectionSize: number;
|
|
||||||
documentsCount: number;
|
|
||||||
usageSizeInKB: number;
|
|
||||||
numPartitions: number;
|
|
||||||
uniqueKeyPolicy?: UniqueKeyPolicy; // TODO: This should ideally not be a part of the collection quota. Remove after refactoring. (#119617)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OfferThroughputInfo {
|
export interface OfferThroughputInfo {
|
||||||
|
|||||||
@@ -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,11 +115,8 @@ export interface CollectionBase extends TreeNode {
|
|||||||
export interface Collection extends CollectionBase {
|
export interface Collection extends CollectionBase {
|
||||||
defaultTtl: ko.Observable<number>;
|
defaultTtl: ko.Observable<number>;
|
||||||
analyticalStorageTtl: ko.Observable<number>;
|
analyticalStorageTtl: ko.Observable<number>;
|
||||||
schema?: DataModels.ISchema;
|
|
||||||
requestSchema?: () => void;
|
|
||||||
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
||||||
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
||||||
quotaInfo: ko.Observable<DataModels.CollectionQuotaInfo>;
|
|
||||||
offer: ko.Observable<DataModels.Offer>;
|
offer: ko.Observable<DataModels.Offer>;
|
||||||
conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
||||||
changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
||||||
@@ -361,7 +357,6 @@ export enum CollectionTabKind {
|
|||||||
SparkMasterTab = 16,
|
SparkMasterTab = 16,
|
||||||
Gallery = 17,
|
Gallery = 17,
|
||||||
NotebookViewer = 18,
|
NotebookViewer = 18,
|
||||||
Schema = 19,
|
|
||||||
SettingsV2 = 19
|
SettingsV2 = 19
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,6 +411,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());
|
||||||
|
|||||||
@@ -39,10 +39,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -1,50 +1,49 @@
|
|||||||
|
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||||
|
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "office-ui-fabric-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
|
||||||
import * as Constants from "../../../Common/Constants";
|
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
|
||||||
import * as 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 * as Constants from "../../../Common/Constants";
|
||||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
|
||||||
import Explorer from "../../Explorer";
|
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
|
||||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
|
||||||
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
|
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
|
||||||
|
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||||
|
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||||
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
|
||||||
|
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
|
import Explorer from "../../Explorer";
|
||||||
import SettingsTab from "../../Tabs/SettingsTabV2";
|
import SettingsTab from "../../Tabs/SettingsTabV2";
|
||||||
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
|
import "./SettingsComponent.less";
|
||||||
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
|
|
||||||
import {
|
|
||||||
MongoIndexingPolicyComponent,
|
|
||||||
MongoIndexingPolicyComponentProps
|
|
||||||
} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
|
|
||||||
import {
|
|
||||||
hasDatabaseSharedThroughput,
|
|
||||||
GeospatialConfigType,
|
|
||||||
TtlType,
|
|
||||||
ChangeFeedPolicyState,
|
|
||||||
SettingsV2TabTypes,
|
|
||||||
getTabTitle,
|
|
||||||
isDirty,
|
|
||||||
AddMongoIndexProps,
|
|
||||||
MongoIndexTypes,
|
|
||||||
parseConflictResolutionMode,
|
|
||||||
parseConflictResolutionProcedure,
|
|
||||||
getMongoNotification
|
|
||||||
} from "./SettingsUtils";
|
|
||||||
import {
|
import {
|
||||||
ConflictResolutionComponent,
|
ConflictResolutionComponent,
|
||||||
ConflictResolutionComponentProps
|
ConflictResolutionComponentProps
|
||||||
} from "./SettingsSubComponents/ConflictResolutionComponent";
|
} from "./SettingsSubComponents/ConflictResolutionComponent";
|
||||||
import { SubSettingsComponent, SubSettingsComponentProps } from "./SettingsSubComponents/SubSettingsComponent";
|
|
||||||
import { Pivot, PivotItem, IPivotProps, IPivotItemProps } from "office-ui-fabric-react";
|
|
||||||
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 {
|
||||||
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
|
MongoIndexingPolicyComponent,
|
||||||
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
|
MongoIndexingPolicyComponentProps
|
||||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
|
||||||
import { isEmpty } from "underscore";
|
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
|
||||||
|
import { SubSettingsComponent, SubSettingsComponentProps } from "./SettingsSubComponents/SubSettingsComponent";
|
||||||
|
import {
|
||||||
|
AddMongoIndexProps,
|
||||||
|
ChangeFeedPolicyState,
|
||||||
|
GeospatialConfigType,
|
||||||
|
getMongoNotification,
|
||||||
|
getTabTitle,
|
||||||
|
hasDatabaseSharedThroughput,
|
||||||
|
isDirty,
|
||||||
|
MongoIndexTypes,
|
||||||
|
parseConflictResolutionMode,
|
||||||
|
parseConflictResolutionProcedure,
|
||||||
|
SettingsV2TabTypes,
|
||||||
|
TtlType
|
||||||
|
} from "./SettingsUtils";
|
||||||
|
|
||||||
interface SettingsV2TabInfo {
|
interface SettingsV2TabInfo {
|
||||||
tab: SettingsV2TabTypes;
|
tab: SettingsV2TabTypes;
|
||||||
@@ -223,6 +222,7 @@ 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()
|
||||||
@@ -270,14 +270,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 +300,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> => {
|
||||||
@@ -433,12 +443,51 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.isScaleSaveable) {
|
if (this.state.isScaleSaveable) {
|
||||||
|
const newThroughput = this.state.throughput;
|
||||||
|
const newOffer: DataModels.Offer = { ...this.collection.offer() };
|
||||||
|
|
||||||
|
if (newOffer.content) {
|
||||||
|
newOffer.content.offerThroughput = newThroughput;
|
||||||
|
} else {
|
||||||
|
newOffer.content = {
|
||||||
|
offerThroughput: newThroughput,
|
||||||
|
offerIsRUPerMinuteThroughputEnabled: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerOptions: RequestOptions = { initialHeaders: {} };
|
||||||
|
|
||||||
|
if (this.state.isAutoPilotSelected) {
|
||||||
|
newOffer.content.offerAutopilotSettings = {
|
||||||
|
maxThroughput: this.state.autoPilotThroughput
|
||||||
|
};
|
||||||
|
|
||||||
|
// user has changed from provisioned --> autoscale
|
||||||
|
if (this.hasProvisioningTypeChanged()) {
|
||||||
|
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToAutopilot] = "true";
|
||||||
|
delete newOffer.content.offerAutopilotSettings;
|
||||||
|
} else {
|
||||||
|
delete newOffer.content.offerThroughput;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
isAutoPilotSelected: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// user has changed from autoscale --> provisioned
|
||||||
|
if (this.hasProvisioningTypeChanged()) {
|
||||||
|
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToManualThroughput] = "true";
|
||||||
|
} else {
|
||||||
|
delete newOffer.content.offerAutopilotSettings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||||
databaseId: this.collection.databaseId,
|
databaseId: this.collection.databaseId,
|
||||||
collectionId: this.collection.id(),
|
collectionId: this.collection.id(),
|
||||||
currentOffer: this.collection.offer(),
|
currentOffer: this.collection.offer(),
|
||||||
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
||||||
manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput
|
manualThroughput: this.state.isAutoPilotSelected ? undefined : newThroughput
|
||||||
};
|
};
|
||||||
if (this.hasProvisioningTypeChanged()) {
|
if (this.hasProvisioningTypeChanged()) {
|
||||||
if (this.state.isAutoPilotSelected) {
|
if (this.state.isAutoPilotSelected) {
|
||||||
@@ -452,13 +501,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
||||||
if (this.state.isAutoPilotSelected) {
|
if (this.state.isAutoPilotSelected) {
|
||||||
this.setState({
|
this.setState({
|
||||||
autoPilotThroughput: updatedOffer.autoscaleMaxThroughput,
|
autoPilotThroughput: updatedOffer.content.offerAutopilotSettings.maxThroughput,
|
||||||
autoPilotThroughputBaseline: updatedOffer.autoscaleMaxThroughput
|
autoPilotThroughputBaseline: updatedOffer.content.offerAutopilotSettings.maxThroughput
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
throughput: updatedOffer.manualThroughput,
|
throughput: updatedOffer.content.offerThroughput,
|
||||||
throughputBaseline: updatedOffer.manualThroughput
|
throughputBaseline: updatedOffer.content.offerThroughput
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -725,7 +774,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;
|
||||||
@@ -916,19 +965,16 @@ 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
|
) {
|
||||||
});
|
|
||||||
} else if (this.container.isEnableMongoCapabilityPresent()) {
|
|
||||||
tabs.push({
|
tabs.push({
|
||||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||||
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />
|
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (this.hasConflictResolution()) {
|
if (this.hasConflictResolution()) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class SettingsRenderUtilsTestComponent extends React.Component {
|
|||||||
{updateThroughputDelayedApplyWarningMessage}
|
{updateThroughputDelayedApplyWarningMessage}
|
||||||
|
|
||||||
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||||
{getThroughputApplyShortDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection")}
|
{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>)}
|
||||||
|
|||||||
@@ -319,13 +319,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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
transparentDetailsRowStyles,
|
transparentDetailsRowStyles,
|
||||||
createAndAddMongoIndexStackProps,
|
createAndAddMongoIndexStackProps,
|
||||||
separatorStyles,
|
separatorStyles,
|
||||||
|
mongoIndexingPolicyAADError,
|
||||||
indexingPolicynUnsavedWarningMessage,
|
indexingPolicynUnsavedWarningMessage,
|
||||||
infoAndToolTipTextStyle
|
infoAndToolTipTextStyle
|
||||||
} from "../../SettingsRenderUtils";
|
} from "../../SettingsRenderUtils";
|
||||||
@@ -39,6 +40,7 @@ import {
|
|||||||
} from "../../SettingsUtils";
|
} from "../../SettingsUtils";
|
||||||
import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
|
import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
|
||||||
import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent";
|
import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent";
|
||||||
|
import { AuthType } from "../../../../../AuthType";
|
||||||
import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
||||||
|
|
||||||
export interface MongoIndexingPolicyComponentProps {
|
export interface MongoIndexingPolicyComponentProps {
|
||||||
@@ -319,7 +321,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
|||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return <Spinner size={SpinnerSize.large} />;
|
return window.authType !== AuthType.AAD ? mongoIndexingPolicyAADError : <Spinner size={SpinnerSize.large} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
|
import ko from "knockout";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent";
|
|
||||||
import { container, collection } from "../TestUtils";
|
|
||||||
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
|
|
||||||
import Explorer from "../../../Explorer";
|
|
||||||
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 Explorer from "../../../Explorer";
|
||||||
import { throughputUnit } from "../SettingsRenderUtils";
|
import { throughputUnit } from "../SettingsRenderUtils";
|
||||||
import * as SharedConstants from "../../../../Shared/Constants";
|
import { collection, container } from "../TestUtils";
|
||||||
import ko from "knockout";
|
import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent";
|
||||||
|
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
|
||||||
|
|
||||||
describe("ScaleComponent", () => {
|
describe("ScaleComponent", () => {
|
||||||
const nonNationalCloudContainer = new Explorer();
|
const nonNationalCloudContainer = new Explorer();
|
||||||
@@ -44,23 +43,24 @@ describe("ScaleComponent", () => {
|
|||||||
} as DataModels.Notification
|
} as DataModels.Notification
|
||||||
};
|
};
|
||||||
|
|
||||||
it("renders with correct initial notification", () => {
|
it("renders with correct intiial notification", () => {
|
||||||
let wrapper = shallow(<ScaleComponent {...baseProps} />);
|
let wrapper = shallow(<ScaleComponent {...baseProps} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
|
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
|
||||||
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(true);
|
|
||||||
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(false);
|
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(false);
|
||||||
expect(wrapper.find("#throughputApplyLongDelayMessage").html()).toContain(targetThroughput);
|
|
||||||
|
|
||||||
const newCollection = { ...collection };
|
const newCollection = { ...collection };
|
||||||
const maxThroughput = 5000;
|
const maxThroughput = 5000;
|
||||||
|
const targetMaxThroughput = 50000;
|
||||||
newCollection.offer = ko.observable({
|
newCollection.offer = ko.observable({
|
||||||
manualThroughput: undefined,
|
content: {
|
||||||
autoscaleMaxThroughput: maxThroughput,
|
offerAutopilotSettings: {
|
||||||
minimumThroughput: 400,
|
maxThroughput: maxThroughput,
|
||||||
id: "offer",
|
targetMaxThroughput: targetMaxThroughput
|
||||||
|
}
|
||||||
|
},
|
||||||
headers: { "x-ms-offer-replace-pending": true }
|
headers: { "x-ms-offer-replace-pending": true }
|
||||||
});
|
} as DataModels.OfferWithHeaders);
|
||||||
const newProps = {
|
const newProps = {
|
||||||
...baseProps,
|
...baseProps,
|
||||||
initialNotification: undefined as DataModels.Notification,
|
initialNotification: undefined as DataModels.Notification,
|
||||||
@@ -70,6 +70,7 @@ describe("ScaleComponent", () => {
|
|||||||
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true);
|
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true);
|
||||||
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(false);
|
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(false);
|
||||||
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(maxThroughput);
|
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(maxThroughput);
|
||||||
|
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(targetMaxThroughput);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("autoScale disabled", () => {
|
it("autoScale disabled", () => {
|
||||||
@@ -117,20 +118,4 @@ describe("ScaleComponent", () => {
|
|||||||
scaleComponent = new ScaleComponent(newProps);
|
scaleComponent = new ScaleComponent(newProps);
|
||||||
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (autoscale)");
|
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (autoscale)");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("canThroughputExceedMaximumValue", () => {
|
|
||||||
let scaleComponent = new ScaleComponent(baseProps);
|
|
||||||
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(true);
|
|
||||||
|
|
||||||
const newProps = { ...baseProps, container: nonNationalCloudContainer };
|
|
||||||
scaleComponent = new ScaleComponent(newProps);
|
|
||||||
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getThroughputWarningMessage", () => {
|
|
||||||
const throughputBeyondLimit = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million + 1000;
|
|
||||||
const newProps = { ...baseProps, container: nonNationalCloudContainer, throughput: throughputBeyondLimit };
|
|
||||||
const scaleComponent = new ScaleComponent(newProps);
|
|
||||||
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputBeyondLimitWarningMessage");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
|
import { Label, MessageBar, MessageBarType, Stack, Text, TextField } from "office-ui-fabric-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as Constants from "../../../../Common/Constants";
|
import * as Constants from "../../../../Common/Constants";
|
||||||
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
|
import { configContext, Platform } from "../../../../ConfigContext";
|
||||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
|
||||||
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 AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
|
||||||
import Explorer from "../../../Explorer";
|
import Explorer from "../../../Explorer";
|
||||||
import {
|
import {
|
||||||
getTextFieldStyles,
|
getTextFieldStyles,
|
||||||
subComponentStackProps,
|
|
||||||
titleAndInputStackProps,
|
|
||||||
throughputUnit,
|
|
||||||
getThroughputApplyLongDelayMessage,
|
|
||||||
getThroughputApplyShortDelayMessage,
|
getThroughputApplyShortDelayMessage,
|
||||||
updateThroughputBeyondLimitWarningMessage
|
subComponentStackProps,
|
||||||
|
throughputUnit,
|
||||||
|
titleAndInputStackProps
|
||||||
} from "../SettingsRenderUtils";
|
} from "../SettingsRenderUtils";
|
||||||
import { hasDatabaseSharedThroughput } from "../SettingsUtils";
|
import { getMinRUs, hasDatabaseSharedThroughput } from "../SettingsUtils";
|
||||||
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
|
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
|
||||||
import { Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
|
|
||||||
import { configContext, Platform } from "../../../../ConfigContext";
|
|
||||||
|
|
||||||
export interface ScaleComponentProps {
|
export interface ScaleComponentProps {
|
||||||
collection: ViewModels.Collection;
|
collection: ViewModels.Collection;
|
||||||
@@ -61,7 +58,11 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private getStorageCapacityTitle = (): JSX.Element => {
|
private getStorageCapacityTitle = (): JSX.Element => {
|
||||||
const capacity: string = this.props.isFixedContainer ? "Fixed" : "Unlimited";
|
// Mongo container with system partition key still treat as "Fixed"
|
||||||
|
const isFixed =
|
||||||
|
!this.props.collection.partitionKey ||
|
||||||
|
(this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey);
|
||||||
|
const capacity: string = isFixed ? "Fixed" : "Unlimited";
|
||||||
return (
|
return (
|
||||||
<Stack {...titleAndInputStackProps}>
|
<Stack {...titleAndInputStackProps}>
|
||||||
<Label>Storage capacity</Label>
|
<Label>Storage capacity</Label>
|
||||||
@@ -70,96 +71,41 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
public getMaxRUs = (): number => {
|
|
||||||
if (this.props.container?.isTryCosmosDBSubscription()) {
|
|
||||||
return Constants.TryCosmosExperience.maxRU;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.isFixedContainer) {
|
|
||||||
return SharedConstants.CollectionCreation.MaxRUPerPartition;
|
|
||||||
}
|
|
||||||
|
|
||||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
|
||||||
};
|
|
||||||
|
|
||||||
public getMinRUs = (): number => {
|
|
||||||
if (this.props.container?.isTryCosmosDBSubscription()) {
|
|
||||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
this.props.collection.offer()?.minimumThroughput || SharedConstants.CollectionCreation.DefaultCollectionRUs400
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
public getThroughputTitle = (): string => {
|
public getThroughputTitle = (): string => {
|
||||||
if (this.props.isAutoPilotSelected) {
|
if (this.props.isAutoPilotSelected) {
|
||||||
return AutoPilotUtils.getAutoPilotHeaderText();
|
return AutoPilotUtils.getAutoPilotHeaderText();
|
||||||
}
|
}
|
||||||
|
|
||||||
const minThroughput: string = this.getMinRUs().toLocaleString();
|
const minThroughput: string = getMinRUs(this.props.collection, this.props.container).toLocaleString();
|
||||||
const maxThroughput: string = !this.props.isFixedContainer ? "unlimited" : this.getMaxRUs().toLocaleString();
|
const maxThroughput: string = !this.props.isFixedContainer ? "unlimited" : "10000";
|
||||||
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`;
|
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`;
|
||||||
};
|
};
|
||||||
|
|
||||||
public canThroughputExceedMaximumValue = (): boolean => {
|
|
||||||
return (
|
|
||||||
!this.props.isFixedContainer &&
|
|
||||||
configContext.platform === Platform.Portal &&
|
|
||||||
!this.props.container.isRunningOnNationalCloud()
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
public getInitialNotificationElement = (): JSX.Element => {
|
public getInitialNotificationElement = (): JSX.Element => {
|
||||||
if (this.props.initialNotification) {
|
const offer = this.props.collection?.offer && this.props.collection.offer();
|
||||||
return this.getLongDelayMessage();
|
if (
|
||||||
}
|
offer &&
|
||||||
|
Object.keys(offer).find(value => {
|
||||||
|
return value === "headers";
|
||||||
|
}) &&
|
||||||
|
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
||||||
|
) {
|
||||||
|
const throughput = offer?.content?.offerAutopilotSettings?.maxThroughput;
|
||||||
|
|
||||||
|
const targetThroughput =
|
||||||
|
offer.content?.offerAutopilotSettings?.targetMaxThroughput || offer?.content?.offerThroughput;
|
||||||
|
|
||||||
const offer = this.props.collection?.offer();
|
|
||||||
if (offer?.headers?.[Constants.HttpHeaders.offerReplacePending]) {
|
|
||||||
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
|
|
||||||
return getThroughputApplyShortDelayMessage(
|
return getThroughputApplyShortDelayMessage(
|
||||||
this.props.isAutoPilotSelected,
|
this.props.isAutoPilotSelected,
|
||||||
throughput,
|
throughput,
|
||||||
throughputUnit,
|
throughputUnit,
|
||||||
this.props.collection.databaseId,
|
this.props.collection.databaseId,
|
||||||
this.props.collection.id()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
public getThroughputWarningMessage = (): JSX.Element => {
|
|
||||||
const throughputExceedsBackendLimits: boolean =
|
|
||||||
this.canThroughputExceedMaximumValue() &&
|
|
||||||
this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
|
||||||
|
|
||||||
if (throughputExceedsBackendLimits && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
|
|
||||||
return updateThroughputBeyondLimitWarningMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
public getLongDelayMessage = (): JSX.Element => {
|
|
||||||
const matches: string[] = this.props.initialNotification?.description.match(
|
|
||||||
`Throughput update for (.*) ${throughputUnit}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const throughput = this.props.throughputBaseline;
|
|
||||||
const targetThroughput: number = matches.length > 1 && Number(matches[1]);
|
|
||||||
if (targetThroughput) {
|
|
||||||
return getThroughputApplyLongDelayMessage(
|
|
||||||
this.props.wasAutopilotOriginallySet,
|
|
||||||
throughput,
|
|
||||||
throughputUnit,
|
|
||||||
this.props.collection.databaseId,
|
|
||||||
this.props.collection.id(),
|
this.props.collection.id(),
|
||||||
targetThroughput
|
targetThroughput
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <></>;
|
|
||||||
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
private getThroughputInputComponent = (): JSX.Element => (
|
private getThroughputInputComponent = (): JSX.Element => (
|
||||||
@@ -169,10 +115,8 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
throughput={this.props.throughput}
|
throughput={this.props.throughput}
|
||||||
throughputBaseline={this.props.throughputBaseline}
|
throughputBaseline={this.props.throughputBaseline}
|
||||||
onThroughputChange={this.props.onThroughputChange}
|
onThroughputChange={this.props.onThroughputChange}
|
||||||
minimum={this.getMinRUs()}
|
minimum={getMinRUs(this.props.collection, this.props.container)}
|
||||||
maximum={this.getMaxRUs()}
|
|
||||||
isEnabled={!hasDatabaseSharedThroughput(this.props.collection)}
|
isEnabled={!hasDatabaseSharedThroughput(this.props.collection)}
|
||||||
canExceedMaximumValue={this.canThroughputExceedMaximumValue()}
|
|
||||||
label={this.getThroughputTitle()}
|
label={this.getThroughputTitle()}
|
||||||
isEmulator={this.isEmulator}
|
isEmulator={this.isEmulator}
|
||||||
isFixed={this.props.isFixedContainer}
|
isFixed={this.props.isFixedContainer}
|
||||||
@@ -185,8 +129,6 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
spendAckChecked={false}
|
spendAckChecked={false}
|
||||||
onScaleSaveableChange={this.props.onScaleSaveableChange}
|
onScaleSaveableChange={this.props.onScaleSaveableChange}
|
||||||
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
|
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
|
||||||
getThroughputWarningMessage={this.getThroughputWarningMessage}
|
|
||||||
usageSizeInKB={this.props.collection.quotaInfo().usageSizeInKB}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ describe("ThroughputInputAutoPilotV3Component", () => {
|
|||||||
throughputBaseline: 100,
|
throughputBaseline: 100,
|
||||||
onThroughputChange: undefined,
|
onThroughputChange: undefined,
|
||||||
minimum: 10000,
|
minimum: 10000,
|
||||||
maximum: 400,
|
|
||||||
step: 100,
|
step: 100,
|
||||||
usageSizeInKB: 10000,
|
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
isEmulator: false,
|
isEmulator: false,
|
||||||
spendAckChecked: false,
|
spendAckChecked: false,
|
||||||
@@ -39,8 +37,7 @@ describe("ThroughputInputAutoPilotV3Component", () => {
|
|||||||
},
|
},
|
||||||
onScaleDiscardableChange: () => {
|
onScaleDiscardableChange: () => {
|
||||||
return;
|
return;
|
||||||
},
|
}
|
||||||
getThroughputWarningMessage: () => undefined
|
|
||||||
};
|
};
|
||||||
|
|
||||||
it("throughput input visible", () => {
|
it("throughput input visible", () => {
|
||||||
|
|||||||
@@ -1,39 +1,33 @@
|
|||||||
import React from "react";
|
|
||||||
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
|
|
||||||
import {
|
import {
|
||||||
getTextFieldStyles,
|
Checkbox,
|
||||||
getToolTipContainer,
|
|
||||||
noLeftPaddingCheckBoxStyle,
|
|
||||||
titleAndInputStackProps,
|
|
||||||
checkBoxAndInputStackProps,
|
|
||||||
getChoiceGroupStyles,
|
|
||||||
messageBarStyles,
|
|
||||||
getEstimatedSpendElement,
|
|
||||||
getEstimatedAutoscaleSpendElement,
|
|
||||||
getAutoPilotV3SpendElement,
|
|
||||||
manualToAutoscaleDisclaimerElement
|
|
||||||
} from "../../SettingsRenderUtils";
|
|
||||||
import {
|
|
||||||
Text,
|
|
||||||
TextField,
|
|
||||||
ChoiceGroup,
|
ChoiceGroup,
|
||||||
IChoiceGroupOption,
|
IChoiceGroupOption,
|
||||||
Checkbox,
|
|
||||||
Stack,
|
|
||||||
Label,
|
Label,
|
||||||
Link,
|
Link,
|
||||||
MessageBar,
|
MessageBar,
|
||||||
MessageBarType
|
MessageBarType,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextField
|
||||||
} from "office-ui-fabric-react";
|
} from "office-ui-fabric-react";
|
||||||
import { ToolTipLabelComponent } from "../ToolTipLabelComponent";
|
import React from "react";
|
||||||
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
|
|
||||||
import * as SharedConstants from "../../../../../Shared/Constants";
|
|
||||||
import * as DataModels from "../../../../../Contracts/DataModels";
|
import * as DataModels from "../../../../../Contracts/DataModels";
|
||||||
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
|
||||||
import { userContext } from "../../../../../UserContext";
|
import {
|
||||||
import { SubscriptionType } from "../../../../../Contracts/SubscriptionType";
|
checkBoxAndInputStackProps,
|
||||||
import { usageInGB } from "../../../../../Utils/PricingUtils";
|
getAutoPilotV3SpendElement,
|
||||||
import { Features } from "../../../../../Common/Constants";
|
getChoiceGroupStyles,
|
||||||
|
getEstimatedAutoscaleSpendElement,
|
||||||
|
getEstimatedSpendElement,
|
||||||
|
getTextFieldStyles,
|
||||||
|
getToolTipContainer,
|
||||||
|
manualToAutoscaleDisclaimerElement,
|
||||||
|
messageBarStyles,
|
||||||
|
noLeftPaddingCheckBoxStyle,
|
||||||
|
titleAndInputStackProps
|
||||||
|
} from "../../SettingsRenderUtils";
|
||||||
|
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
|
||||||
|
import { ToolTipLabelComponent } from "../ToolTipLabelComponent";
|
||||||
|
|
||||||
export interface ThroughputInputAutoPilotV3Props {
|
export interface ThroughputInputAutoPilotV3Props {
|
||||||
databaseAccount: DataModels.DatabaseAccount;
|
databaseAccount: DataModels.DatabaseAccount;
|
||||||
@@ -42,7 +36,6 @@ export interface ThroughputInputAutoPilotV3Props {
|
|||||||
throughputBaseline: number;
|
throughputBaseline: number;
|
||||||
onThroughputChange: (newThroughput: number) => void;
|
onThroughputChange: (newThroughput: number) => void;
|
||||||
minimum: number;
|
minimum: number;
|
||||||
maximum: number;
|
|
||||||
step?: number;
|
step?: number;
|
||||||
isEnabled?: boolean;
|
isEnabled?: boolean;
|
||||||
spendAckChecked?: boolean;
|
spendAckChecked?: boolean;
|
||||||
@@ -63,8 +56,6 @@ export interface ThroughputInputAutoPilotV3Props {
|
|||||||
onMaxAutoPilotThroughputChange: (newThroughput: number) => void;
|
onMaxAutoPilotThroughputChange: (newThroughput: number) => void;
|
||||||
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
||||||
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
||||||
getThroughputWarningMessage: () => JSX.Element;
|
|
||||||
usageSizeInKB: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ThroughputInputAutoPilotV3State {
|
interface ThroughputInputAutoPilotV3State {
|
||||||
@@ -124,13 +115,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
if (isDirty(this.props.throughput, this.props.throughputBaseline)) {
|
if (isDirty(this.props.throughput, this.props.throughputBaseline)) {
|
||||||
isDiscardable = true;
|
isDiscardable = true;
|
||||||
isSaveable = true;
|
isSaveable = true;
|
||||||
if (
|
if (!this.props.throughput || this.props.throughput < this.props.minimum) {
|
||||||
!this.props.throughput ||
|
|
||||||
this.props.throughput < this.props.minimum ||
|
|
||||||
(this.props.throughput > this.props.maximum && (this.props.isEmulator || this.props.isFixed)) ||
|
|
||||||
(this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
|
||||||
!this.props.canExceedMaximumValue)
|
|
||||||
) {
|
|
||||||
isSaveable = false;
|
isSaveable = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,8 +131,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.step = this.props.step ?? ThroughputInputAutoPilotV3Component.defaultStep;
|
this.step = this.props.step ?? ThroughputInputAutoPilotV3Component.defaultStep;
|
||||||
this.throughputInputMaxValue = this.props.canExceedMaximumValue ? Int32.Max : this.props.maximum;
|
this.throughputInputMaxValue = Number.MAX_SAFE_INTEGER;
|
||||||
this.autoPilotInputMaxValue = this.props.isFixed ? this.props.maximum : Int32.Max;
|
this.autoPilotInputMaxValue = Number.MAX_SAFE_INTEGER;
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasProvisioningTypeChanged = (): boolean =>
|
public hasProvisioningTypeChanged = (): boolean =>
|
||||||
@@ -229,29 +214,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
option?: IChoiceGroupOption
|
option?: IChoiceGroupOption
|
||||||
): void => this.props.onAutoPilotSelected(option.key === "true");
|
): void => this.props.onAutoPilotSelected(option.key === "true");
|
||||||
|
|
||||||
private minRUperGBSurvey = (): JSX.Element => {
|
|
||||||
const href = `https://ncv.microsoft.com/vRBTO37jmO?ctx={"AzureSubscriptionId":"${userContext.subscriptionId}","CosmosDBAccountName":"${userContext.databaseAccount?.name}"}`;
|
|
||||||
const oneTBinKB = 1000000000;
|
|
||||||
const minRUperGB = 10;
|
|
||||||
const featureFlagEnabled = window.dataExplorer?.isFeatureEnabled(Features.showMinRUSurvey);
|
|
||||||
const collectionIsEligible =
|
|
||||||
userContext.subscriptionType !== SubscriptionType.Internal &&
|
|
||||||
this.props.usageSizeInKB > oneTBinKB &&
|
|
||||||
this.props.minimum >= usageInGB(this.props.usageSizeInKB) * minRUperGB;
|
|
||||||
if (featureFlagEnabled || collectionIsEligible) {
|
|
||||||
return (
|
|
||||||
<Text>
|
|
||||||
Need to scale below {this.props.minimum} RU/s? Reach out by filling{" "}
|
|
||||||
<a target="_blank" rel="noreferrer" href={href}>
|
|
||||||
this questionnaire
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
private renderThroughputModeChoices = (): JSX.Element => {
|
private renderThroughputModeChoices = (): JSX.Element => {
|
||||||
const labelId = "settingsV2RadioButtonLabelId";
|
const labelId = "settingsV2RadioButtonLabelId";
|
||||||
return (
|
return (
|
||||||
@@ -303,7 +265,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
onChange={this.onAutoPilotThroughputChange}
|
onChange={this.onAutoPilotThroughputChange}
|
||||||
/>
|
/>
|
||||||
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
|
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
|
||||||
{this.minRUperGBSurvey()}
|
|
||||||
{this.props.spendAckVisible && (
|
{this.props.spendAckVisible && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="spendAckCheckBox"
|
id="spendAckCheckBox"
|
||||||
@@ -334,13 +295,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
}
|
}
|
||||||
onChange={this.onThroughputChange}
|
onChange={this.onThroughputChange}
|
||||||
/>
|
/>
|
||||||
{this.props.getThroughputWarningMessage() && (
|
|
||||||
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
|
|
||||||
{this.props.getThroughputWarningMessage()}
|
|
||||||
</MessageBar>
|
|
||||||
)}
|
|
||||||
{!this.props.isEmulator && this.getRequestUnitsUsageCost()}
|
{!this.props.isEmulator && this.getRequestUnitsUsageCost()}
|
||||||
{this.minRUperGBSurvey()}
|
|
||||||
{this.props.spendAckVisible && (
|
{this.props.spendAckVisible && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="spendAckCheckBox"
|
id="spendAckCheckBox"
|
||||||
@@ -350,6 +307,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
onChange={this.onSpendAckChecked}
|
onChange={this.onSpendAckChecked}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{this.props.isFixed && <p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>}
|
{this.props.isFixed && <p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`ScaleComponent renders with correct initial notification 1`] = `
|
exports[`ScaleComponent renders with correct intiial notification 1`] = `
|
||||||
<Stack
|
<Stack
|
||||||
tokens={
|
tokens={
|
||||||
Object {
|
Object {
|
||||||
@@ -8,29 +8,6 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StyledMessageBarBase
|
|
||||||
messageBarType={5}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
id="throughputApplyLongDelayMessage"
|
|
||||||
styles={
|
|
||||||
Object {
|
|
||||||
"root": Object {
|
|
||||||
"fontSize": 12,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to complete. View the latest status in Notifications.
|
|
||||||
<br />
|
|
||||||
Database:
|
|
||||||
test
|
|
||||||
, Container:
|
|
||||||
test
|
|
||||||
|
|
||||||
, Current autoscale throughput: 100 - 1000 RU/s, Target autoscale throughput: 600 - 6000 RU/s
|
|
||||||
</Text>
|
|
||||||
</StyledMessageBarBase>
|
|
||||||
<Stack
|
<Stack
|
||||||
tokens={
|
tokens={
|
||||||
Object {
|
Object {
|
||||||
@@ -39,8 +16,6 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ThroughputInputAutoPilotV3Component
|
<ThroughputInputAutoPilotV3Component
|
||||||
canExceedMaximumValue={true}
|
|
||||||
getThroughputWarningMessage={[Function]}
|
|
||||||
isAutoPilotSelected={false}
|
isAutoPilotSelected={false}
|
||||||
isEmulator={false}
|
isEmulator={false}
|
||||||
isEnabled={true}
|
isEnabled={true}
|
||||||
@@ -48,7 +23,6 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
|
|||||||
label="Throughput (6,000 - unlimited RU/s)"
|
label="Throughput (6,000 - unlimited RU/s)"
|
||||||
maxAutoPilotThroughput={4000}
|
maxAutoPilotThroughput={4000}
|
||||||
maxAutoPilotThroughputBaseline={4000}
|
maxAutoPilotThroughputBaseline={4000}
|
||||||
maximum={1000000}
|
|
||||||
minimum={6000}
|
minimum={6000}
|
||||||
onAutoPilotSelected={[Function]}
|
onAutoPilotSelected={[Function]}
|
||||||
onMaxAutoPilotThroughputChange={[Function]}
|
onMaxAutoPilotThroughputChange={[Function]}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { collection } from "./TestUtils";
|
import { collection, container } from "./TestUtils";
|
||||||
import {
|
import {
|
||||||
|
getMinRUs,
|
||||||
getMongoIndexType,
|
getMongoIndexType,
|
||||||
getMongoNotification,
|
getMongoNotification,
|
||||||
getSanitizedInputValue,
|
getSanitizedInputValue,
|
||||||
@@ -21,6 +22,11 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
|||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
|
|
||||||
describe("SettingsUtils", () => {
|
describe("SettingsUtils", () => {
|
||||||
|
it("getMinRUs", () => {
|
||||||
|
expect(collection.offer().content.collectionThroughputInfo.numPhysicalPartitions).toEqual(4);
|
||||||
|
expect(getMinRUs(collection, container)).toEqual(6000);
|
||||||
|
});
|
||||||
|
|
||||||
it("hasDatabaseSharedThroughput", () => {
|
it("hasDatabaseSharedThroughput", () => {
|
||||||
expect(hasDatabaseSharedThroughput(collection)).toEqual(undefined);
|
expect(hasDatabaseSharedThroughput(collection)).toEqual(undefined);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
import * as SharedConstants from "../../../Shared/Constants";
|
||||||
import { MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
|
import { MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
|
||||||
|
import Explorer from "../../Explorer";
|
||||||
|
|
||||||
const zeroValue = 0;
|
const zeroValue = 0;
|
||||||
export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy;
|
export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy;
|
||||||
@@ -67,6 +69,27 @@ export const hasDatabaseSharedThroughput = (collection: ViewModels.Collection):
|
|||||||
return database?.isDatabaseShared() && !collection.offer();
|
return database?.isDatabaseShared() && !collection.offer();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getMinRUs = (collection: ViewModels.Collection, container: Explorer): number => {
|
||||||
|
const isTryCosmosDBSubscription = container?.isTryCosmosDBSubscription();
|
||||||
|
if (isTryCosmosDBSubscription) {
|
||||||
|
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||||
|
}
|
||||||
|
|
||||||
|
const offerContent = collection?.offer && collection.offer()?.content;
|
||||||
|
|
||||||
|
if (offerContent?.offerAutopilotSettings) {
|
||||||
|
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectionThroughputInfo: DataModels.OfferThroughputInfo = offerContent?.collectionThroughputInfo;
|
||||||
|
|
||||||
|
if (collectionThroughputInfo?.minimumRUForCollection > 0) {
|
||||||
|
return collectionThroughputInfo.minimumRUForCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||||
|
};
|
||||||
|
|
||||||
export const parseConflictResolutionMode = (modeFromBackend: string): DataModels.ConflictResolutionMode => {
|
export const parseConflictResolutionMode = (modeFromBackend: string): DataModels.ConflictResolutionMode => {
|
||||||
// Backend can contain different casing as it does case-insensitive comparisson
|
// Backend can contain different casing as it does case-insensitive comparisson
|
||||||
if (!modeFromBackend) {
|
if (!modeFromBackend) {
|
||||||
|
|||||||
@@ -18,13 +18,16 @@ export const collection = ({
|
|||||||
excludedPaths: []
|
excludedPaths: []
|
||||||
}),
|
}),
|
||||||
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
|
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
|
||||||
quotaInfo: ko.observable<DataModels.CollectionQuotaInfo>({} as DataModels.CollectionQuotaInfo),
|
|
||||||
offer: ko.observable<DataModels.Offer>({
|
offer: ko.observable<DataModels.Offer>({
|
||||||
autoscaleMaxThroughput: undefined,
|
content: {
|
||||||
manualThroughput: 10000,
|
offerThroughput: 10000,
|
||||||
minimumThroughput: 6000,
|
offerIsRUPerMinuteThroughputEnabled: false,
|
||||||
id: "offer"
|
collectionThroughputInfo: {
|
||||||
}),
|
minimumRUForCollection: 6000,
|
||||||
|
numPhysicalPartitions: 4
|
||||||
|
} as DataModels.OfferThroughputInfo
|
||||||
|
}
|
||||||
|
} as DataModels.Offer),
|
||||||
conflictResolutionPolicy: ko.observable<DataModels.ConflictResolutionPolicy>(
|
conflictResolutionPolicy: ko.observable<DataModels.ConflictResolutionPolicy>(
|
||||||
{} as DataModels.ConflictResolutionPolicy
|
{} as DataModels.ConflictResolutionPolicy
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
"canRequestSupport": [Function],
|
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"databaseCreateNewShared": [Function],
|
"databaseCreateNewShared": [Function],
|
||||||
@@ -86,7 +85,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
"canRequestSupport": [Function],
|
|
||||||
"collectionId": [Function],
|
"collectionId": [Function],
|
||||||
"collectionIdTitle": [Function],
|
"collectionIdTitle": [Function],
|
||||||
"collectionWithThroughputInShared": [Function],
|
"collectionWithThroughputInShared": [Function],
|
||||||
@@ -349,7 +347,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
"canRequestSupport": [Function],
|
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"createTableQuery": [Function],
|
"createTableQuery": [Function],
|
||||||
@@ -575,7 +572,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
"canRequestSupport": [Function],
|
|
||||||
"collectionId": [Function],
|
"collectionId": [Function],
|
||||||
"collectionIdTitle": [Function],
|
"collectionIdTitle": [Function],
|
||||||
"collectionWithThroughputInShared": [Function],
|
"collectionWithThroughputInShared": [Function],
|
||||||
@@ -657,7 +653,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
"canRequestSupport": [Function],
|
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"databaseCreateNewShared": [Function],
|
"databaseCreateNewShared": [Function],
|
||||||
@@ -760,7 +755,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
"canRequestSupport": [Function],
|
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"createTableQuery": [Function],
|
"createTableQuery": [Function],
|
||||||
@@ -956,6 +950,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isHostedDataExplorerEnabled": [Function],
|
"isHostedDataExplorerEnabled": [Function],
|
||||||
"isLeftPaneExpanded": [Function],
|
"isLeftPaneExpanded": [Function],
|
||||||
"isLinkInjectionEnabled": [Function],
|
"isLinkInjectionEnabled": [Function],
|
||||||
|
"isMongoIndexEditorEnabled": [Function],
|
||||||
"isNotebookEnabled": [Function],
|
"isNotebookEnabled": [Function],
|
||||||
"isNotebooksEnabledForAccount": [Function],
|
"isNotebooksEnabledForAccount": [Function],
|
||||||
"isNotificationConsoleExpanded": [Function],
|
"isNotificationConsoleExpanded": [Function],
|
||||||
@@ -970,8 +965,8 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isRefreshingExplorer": [Function],
|
"isRefreshingExplorer": [Function],
|
||||||
"isResourceTokenCollectionNodeSelected": [Function],
|
"isResourceTokenCollectionNodeSelected": [Function],
|
||||||
"isRightPanelV2Enabled": [Function],
|
"isRightPanelV2Enabled": [Function],
|
||||||
"isSchemaEnabled": [Function],
|
|
||||||
"isServerlessEnabled": [Function],
|
"isServerlessEnabled": [Function],
|
||||||
|
"isSettingsV2Enabled": [Function],
|
||||||
"isSparkEnabled": [Function],
|
"isSparkEnabled": [Function],
|
||||||
"isSparkEnabledForAccount": [Function],
|
"isSparkEnabledForAccount": [Function],
|
||||||
"isSynapseLinkUpdating": [Function],
|
"isSynapseLinkUpdating": [Function],
|
||||||
@@ -1300,7 +1295,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"version": 2,
|
"version": 2,
|
||||||
},
|
},
|
||||||
"partitionKeyProperty": "partitionKey",
|
"partitionKeyProperty": "partitionKey",
|
||||||
"quotaInfo": [Function],
|
|
||||||
"readSettings": [Function],
|
"readSettings": [Function],
|
||||||
"uniqueKeyPolicy": Object {},
|
"uniqueKeyPolicy": Object {},
|
||||||
}
|
}
|
||||||
@@ -1322,7 +1316,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
"canRequestSupport": [Function],
|
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"databaseCreateNewShared": [Function],
|
"databaseCreateNewShared": [Function],
|
||||||
@@ -1365,7 +1358,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
"canRequestSupport": [Function],
|
|
||||||
"collectionId": [Function],
|
"collectionId": [Function],
|
||||||
"collectionIdTitle": [Function],
|
"collectionIdTitle": [Function],
|
||||||
"collectionWithThroughputInShared": [Function],
|
"collectionWithThroughputInShared": [Function],
|
||||||
@@ -1628,7 +1620,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
"canRequestSupport": [Function],
|
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"createTableQuery": [Function],
|
"createTableQuery": [Function],
|
||||||
@@ -1854,7 +1845,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
"canRequestSupport": [Function],
|
|
||||||
"collectionId": [Function],
|
"collectionId": [Function],
|
||||||
"collectionIdTitle": [Function],
|
"collectionIdTitle": [Function],
|
||||||
"collectionWithThroughputInShared": [Function],
|
"collectionWithThroughputInShared": [Function],
|
||||||
@@ -1936,7 +1926,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
"canRequestSupport": [Function],
|
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"databaseCreateNewShared": [Function],
|
"databaseCreateNewShared": [Function],
|
||||||
@@ -2039,7 +2028,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
"canRequestSupport": [Function],
|
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"createTableQuery": [Function],
|
"createTableQuery": [Function],
|
||||||
@@ -2235,6 +2223,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isHostedDataExplorerEnabled": [Function],
|
"isHostedDataExplorerEnabled": [Function],
|
||||||
"isLeftPaneExpanded": [Function],
|
"isLeftPaneExpanded": [Function],
|
||||||
"isLinkInjectionEnabled": [Function],
|
"isLinkInjectionEnabled": [Function],
|
||||||
|
"isMongoIndexEditorEnabled": [Function],
|
||||||
"isNotebookEnabled": [Function],
|
"isNotebookEnabled": [Function],
|
||||||
"isNotebooksEnabledForAccount": [Function],
|
"isNotebooksEnabledForAccount": [Function],
|
||||||
"isNotificationConsoleExpanded": [Function],
|
"isNotificationConsoleExpanded": [Function],
|
||||||
@@ -2249,8 +2238,8 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isRefreshingExplorer": [Function],
|
"isRefreshingExplorer": [Function],
|
||||||
"isResourceTokenCollectionNodeSelected": [Function],
|
"isResourceTokenCollectionNodeSelected": [Function],
|
||||||
"isRightPanelV2Enabled": [Function],
|
"isRightPanelV2Enabled": [Function],
|
||||||
"isSchemaEnabled": [Function],
|
|
||||||
"isServerlessEnabled": [Function],
|
"isServerlessEnabled": [Function],
|
||||||
|
"isSettingsV2Enabled": [Function],
|
||||||
"isSparkEnabled": [Function],
|
"isSparkEnabled": [Function],
|
||||||
"isSparkEnabledForAccount": [Function],
|
"isSparkEnabledForAccount": [Function],
|
||||||
"isSynapseLinkUpdating": [Function],
|
"isSynapseLinkUpdating": [Function],
|
||||||
@@ -2614,7 +2603,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
"canRequestSupport": [Function],
|
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"databaseCreateNewShared": [Function],
|
"databaseCreateNewShared": [Function],
|
||||||
@@ -2657,7 +2645,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
"canRequestSupport": [Function],
|
|
||||||
"collectionId": [Function],
|
"collectionId": [Function],
|
||||||
"collectionIdTitle": [Function],
|
"collectionIdTitle": [Function],
|
||||||
"collectionWithThroughputInShared": [Function],
|
"collectionWithThroughputInShared": [Function],
|
||||||
@@ -2920,7 +2907,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
"canRequestSupport": [Function],
|
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"createTableQuery": [Function],
|
"createTableQuery": [Function],
|
||||||
@@ -3146,7 +3132,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
"canRequestSupport": [Function],
|
|
||||||
"collectionId": [Function],
|
"collectionId": [Function],
|
||||||
"collectionIdTitle": [Function],
|
"collectionIdTitle": [Function],
|
||||||
"collectionWithThroughputInShared": [Function],
|
"collectionWithThroughputInShared": [Function],
|
||||||
@@ -3228,7 +3213,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
"canRequestSupport": [Function],
|
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"databaseCreateNewShared": [Function],
|
"databaseCreateNewShared": [Function],
|
||||||
@@ -3331,7 +3315,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
"canRequestSupport": [Function],
|
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"createTableQuery": [Function],
|
"createTableQuery": [Function],
|
||||||
@@ -3527,6 +3510,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isHostedDataExplorerEnabled": [Function],
|
"isHostedDataExplorerEnabled": [Function],
|
||||||
"isLeftPaneExpanded": [Function],
|
"isLeftPaneExpanded": [Function],
|
||||||
"isLinkInjectionEnabled": [Function],
|
"isLinkInjectionEnabled": [Function],
|
||||||
|
"isMongoIndexEditorEnabled": [Function],
|
||||||
"isNotebookEnabled": [Function],
|
"isNotebookEnabled": [Function],
|
||||||
"isNotebooksEnabledForAccount": [Function],
|
"isNotebooksEnabledForAccount": [Function],
|
||||||
"isNotificationConsoleExpanded": [Function],
|
"isNotificationConsoleExpanded": [Function],
|
||||||
@@ -3541,8 +3525,8 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isRefreshingExplorer": [Function],
|
"isRefreshingExplorer": [Function],
|
||||||
"isResourceTokenCollectionNodeSelected": [Function],
|
"isResourceTokenCollectionNodeSelected": [Function],
|
||||||
"isRightPanelV2Enabled": [Function],
|
"isRightPanelV2Enabled": [Function],
|
||||||
"isSchemaEnabled": [Function],
|
|
||||||
"isServerlessEnabled": [Function],
|
"isServerlessEnabled": [Function],
|
||||||
|
"isSettingsV2Enabled": [Function],
|
||||||
"isSparkEnabled": [Function],
|
"isSparkEnabled": [Function],
|
||||||
"isSparkEnabledForAccount": [Function],
|
"isSparkEnabledForAccount": [Function],
|
||||||
"isSynapseLinkUpdating": [Function],
|
"isSynapseLinkUpdating": [Function],
|
||||||
@@ -3871,7 +3855,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"version": 2,
|
"version": 2,
|
||||||
},
|
},
|
||||||
"partitionKeyProperty": "partitionKey",
|
"partitionKeyProperty": "partitionKey",
|
||||||
"quotaInfo": [Function],
|
|
||||||
"readSettings": [Function],
|
"readSettings": [Function],
|
||||||
"uniqueKeyPolicy": Object {},
|
"uniqueKeyPolicy": Object {},
|
||||||
}
|
}
|
||||||
@@ -3893,7 +3876,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
"canRequestSupport": [Function],
|
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"databaseCreateNewShared": [Function],
|
"databaseCreateNewShared": [Function],
|
||||||
@@ -3936,7 +3918,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
"canRequestSupport": [Function],
|
|
||||||
"collectionId": [Function],
|
"collectionId": [Function],
|
||||||
"collectionIdTitle": [Function],
|
"collectionIdTitle": [Function],
|
||||||
"collectionWithThroughputInShared": [Function],
|
"collectionWithThroughputInShared": [Function],
|
||||||
@@ -4199,7 +4180,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
"canRequestSupport": [Function],
|
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"createTableQuery": [Function],
|
"createTableQuery": [Function],
|
||||||
@@ -4425,7 +4405,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
"canRequestSupport": [Function],
|
|
||||||
"collectionId": [Function],
|
"collectionId": [Function],
|
||||||
"collectionIdTitle": [Function],
|
"collectionIdTitle": [Function],
|
||||||
"collectionWithThroughputInShared": [Function],
|
"collectionWithThroughputInShared": [Function],
|
||||||
@@ -4507,7 +4486,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
"canRequestSupport": [Function],
|
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"databaseCreateNewShared": [Function],
|
"databaseCreateNewShared": [Function],
|
||||||
@@ -4610,7 +4588,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
"canRequestSupport": [Function],
|
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"createTableQuery": [Function],
|
"createTableQuery": [Function],
|
||||||
@@ -4806,6 +4783,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isHostedDataExplorerEnabled": [Function],
|
"isHostedDataExplorerEnabled": [Function],
|
||||||
"isLeftPaneExpanded": [Function],
|
"isLeftPaneExpanded": [Function],
|
||||||
"isLinkInjectionEnabled": [Function],
|
"isLinkInjectionEnabled": [Function],
|
||||||
|
"isMongoIndexEditorEnabled": [Function],
|
||||||
"isNotebookEnabled": [Function],
|
"isNotebookEnabled": [Function],
|
||||||
"isNotebooksEnabledForAccount": [Function],
|
"isNotebooksEnabledForAccount": [Function],
|
||||||
"isNotificationConsoleExpanded": [Function],
|
"isNotificationConsoleExpanded": [Function],
|
||||||
@@ -4820,8 +4798,8 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isRefreshingExplorer": [Function],
|
"isRefreshingExplorer": [Function],
|
||||||
"isResourceTokenCollectionNodeSelected": [Function],
|
"isResourceTokenCollectionNodeSelected": [Function],
|
||||||
"isRightPanelV2Enabled": [Function],
|
"isRightPanelV2Enabled": [Function],
|
||||||
"isSchemaEnabled": [Function],
|
|
||||||
"isServerlessEnabled": [Function],
|
"isServerlessEnabled": [Function],
|
||||||
|
"isSettingsV2Enabled": [Function],
|
||||||
"isSparkEnabled": [Function],
|
"isSparkEnabled": [Function],
|
||||||
"isSparkEnabledForAccount": [Function],
|
"isSparkEnabledForAccount": [Function],
|
||||||
"isSynapseLinkUpdating": [Function],
|
"isSynapseLinkUpdating": [Function],
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
, Container:
|
, Container:
|
||||||
sampleCollection
|
sampleCollection
|
||||||
|
|
||||||
, Current manual throughput: 1000 RU/s
|
, Current manual throughput: 1000 RU/s, Target manual throughput: 2000
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
id="throughputApplyLongDelayMessage"
|
id="throughputApplyLongDelayMessage"
|
||||||
|
|||||||
@@ -87,7 +87,6 @@ import { updateUserContext, userContext } from "../UserContext";
|
|||||||
import { stringToBlob } from "../Utils/BlobUtils";
|
import { stringToBlob } from "../Utils/BlobUtils";
|
||||||
import { IChoiceGroupProps } from "office-ui-fabric-react";
|
import { IChoiceGroupProps } from "office-ui-fabric-react";
|
||||||
import { getErrorMessage, handleError, getErrorStack } from "../Common/ErrorHandlingUtils";
|
import { getErrorMessage, handleError, getErrorStack } from "../Common/ErrorHandlingUtils";
|
||||||
import { SubscriptionType } from "../Contracts/SubscriptionType";
|
|
||||||
|
|
||||||
BindingHandlersRegisterer.registerBindingHandlers();
|
BindingHandlersRegisterer.registerBindingHandlers();
|
||||||
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
||||||
@@ -120,7 +119,7 @@ export default class Explorer {
|
|||||||
|
|
||||||
public databaseAccount: ko.Observable<DataModels.DatabaseAccount>;
|
public databaseAccount: ko.Observable<DataModels.DatabaseAccount>;
|
||||||
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults;
|
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults;
|
||||||
public subscriptionType: ko.Observable<SubscriptionType>;
|
public subscriptionType: ko.Observable<ViewModels.SubscriptionType>;
|
||||||
public quotaId: ko.Observable<string>;
|
public quotaId: ko.Observable<string>;
|
||||||
public defaultExperience: ko.Observable<string>;
|
public defaultExperience: ko.Observable<string>;
|
||||||
public isPreferredApiDocumentDB: ko.Computed<boolean>;
|
public isPreferredApiDocumentDB: ko.Computed<boolean>;
|
||||||
@@ -206,6 +205,8 @@ export default class Explorer {
|
|||||||
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
||||||
public isCodeOfConductEnabled: ko.Computed<boolean>;
|
public isCodeOfConductEnabled: ko.Computed<boolean>;
|
||||||
public isLinkInjectionEnabled: ko.Computed<boolean>;
|
public isLinkInjectionEnabled: ko.Computed<boolean>;
|
||||||
|
public isSettingsV2Enabled: ko.Observable<boolean>;
|
||||||
|
public isMongoIndexEditorEnabled: ko.Observable<boolean>;
|
||||||
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||||
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
||||||
public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
|
public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
|
||||||
@@ -224,7 +225,6 @@ export default class Explorer {
|
|||||||
public shareTokenCopyHelperText: ko.Observable<string>;
|
public shareTokenCopyHelperText: ko.Observable<string>;
|
||||||
public shouldShowDataAccessExpiryDialog: ko.Observable<boolean>;
|
public shouldShowDataAccessExpiryDialog: ko.Observable<boolean>;
|
||||||
public shouldShowContextSwitchPrompt: ko.Observable<boolean>;
|
public shouldShowContextSwitchPrompt: ko.Observable<boolean>;
|
||||||
public isSchemaEnabled: ko.Computed<boolean>;
|
|
||||||
|
|
||||||
// Notebooks
|
// Notebooks
|
||||||
public isNotebookEnabled: ko.Observable<boolean>;
|
public isNotebookEnabled: ko.Observable<boolean>;
|
||||||
@@ -278,7 +278,9 @@ export default class Explorer {
|
|||||||
this.refreshTreeTitle = ko.observable<string>("Refresh collections");
|
this.refreshTreeTitle = ko.observable<string>("Refresh collections");
|
||||||
|
|
||||||
this.databaseAccount = ko.observable<DataModels.DatabaseAccount>();
|
this.databaseAccount = ko.observable<DataModels.DatabaseAccount>();
|
||||||
this.subscriptionType = ko.observable<SubscriptionType>(SharedConstants.CollectionCreation.DefaultSubscriptionType);
|
this.subscriptionType = ko.observable<ViewModels.SubscriptionType>(
|
||||||
|
SharedConstants.CollectionCreation.DefaultSubscriptionType
|
||||||
|
);
|
||||||
this.quotaId = ko.observable<string>("");
|
this.quotaId = ko.observable<string>("");
|
||||||
let firstInitialization = true;
|
let firstInitialization = true;
|
||||||
this.isRefreshingExplorer = ko.observable<boolean>(true);
|
this.isRefreshingExplorer = ko.observable<boolean>(true);
|
||||||
@@ -410,6 +412,8 @@ export default class Explorer {
|
|||||||
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
|
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
|
||||||
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
|
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
|
||||||
);
|
);
|
||||||
|
this.isSettingsV2Enabled = ko.observable(false);
|
||||||
|
this.isMongoIndexEditorEnabled = ko.observable(false);
|
||||||
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
|
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
|
||||||
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
|
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||||
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
|
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||||
@@ -418,7 +422,6 @@ export default class Explorer {
|
|||||||
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
|
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.isSchemaEnabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSchema));
|
|
||||||
this.isNotificationConsoleExpanded = ko.observable<boolean>(false);
|
this.isNotificationConsoleExpanded = ko.observable<boolean>(false);
|
||||||
|
|
||||||
this.databases = ko.observableArray<ViewModels.Database>();
|
this.databases = ko.observableArray<ViewModels.Database>();
|
||||||
@@ -1730,7 +1733,6 @@ export default class Explorer {
|
|||||||
case MessageTypes.SendNotification:
|
case MessageTypes.SendNotification:
|
||||||
case MessageTypes.ClearNotification:
|
case MessageTypes.ClearNotification:
|
||||||
case MessageTypes.LoadingStatus:
|
case MessageTypes.LoadingStatus:
|
||||||
case MessageTypes.InitTestExplorer:
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1888,8 +1890,7 @@ export default class Explorer {
|
|||||||
masterKey,
|
masterKey,
|
||||||
databaseAccount,
|
databaseAccount,
|
||||||
resourceGroup: inputs.resourceGroup,
|
resourceGroup: inputs.resourceGroup,
|
||||||
subscriptionId: inputs.subscriptionId,
|
subscriptionId: inputs.subscriptionId
|
||||||
subscriptionType: inputs.subscriptionType
|
|
||||||
});
|
});
|
||||||
TelemetryProcessor.traceSuccess(
|
TelemetryProcessor.traceSuccess(
|
||||||
Action.LoadDatabaseAccount,
|
Action.LoadDatabaseAccount,
|
||||||
@@ -1910,6 +1911,14 @@ export default class Explorer {
|
|||||||
if (!flights) {
|
if (!flights) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (flights.indexOf(Constants.Flights.SettingsV2) !== -1) {
|
||||||
|
this.isSettingsV2Enabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flights.indexOf(Constants.Flights.MongoIndexEditor) !== -1) {
|
||||||
|
this.isMongoIndexEditorEnabled(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public findSelectedCollection(): ViewModels.Collection {
|
public findSelectedCollection(): ViewModels.Collection {
|
||||||
|
|||||||
@@ -350,9 +350,7 @@ export const launchWebSocketKernelEpic = (
|
|||||||
} as any,
|
} as any,
|
||||||
name: "",
|
name: "",
|
||||||
path: content.filepath.replace(/^\/+/g, ""),
|
path: content.filepath.replace(/^\/+/g, ""),
|
||||||
type: "notebook",
|
type: "notebook"
|
||||||
endpoint:"https://dech-notebooks-demo-7.documents.azure.com:443/",
|
|
||||||
token:"" //to fill
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return sessions.create(serverConfig, sessionPayload).pipe(
|
return sessions.create(serverConfig, sessionPayload).pipe(
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import * as ko from "knockout";
|
|||||||
import * as PricingUtils from "../../Utils/PricingUtils";
|
import * as PricingUtils from "../../Utils/PricingUtils";
|
||||||
import * as SharedConstants from "../../Shared/Constants";
|
import * as SharedConstants from "../../Shared/Constants";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { SubscriptionType } from "../../Contracts/SubscriptionType";
|
|
||||||
import editable from "../../Common/EditableUtility";
|
import editable from "../../Common/EditableUtility";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
@@ -62,7 +61,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
public maxCollectionsReachedMessage: ko.Observable<string>;
|
public maxCollectionsReachedMessage: ko.Observable<string>;
|
||||||
public requestUnitsUsageCost: ko.Computed<string>;
|
public requestUnitsUsageCost: ko.Computed<string>;
|
||||||
public dedicatedRequestUnitsUsageCost: ko.Computed<string>;
|
public dedicatedRequestUnitsUsageCost: ko.Computed<string>;
|
||||||
public canRequestSupport: ko.PureComputed<boolean>;
|
|
||||||
public largePartitionKey: ko.Observable<boolean> = ko.observable<boolean>(false);
|
public largePartitionKey: ko.Observable<boolean> = ko.observable<boolean>(false);
|
||||||
public useIndexingForSharedThroughput: ko.Observable<boolean> = ko.observable<boolean>(true);
|
public useIndexingForSharedThroughput: ko.Observable<boolean> = ko.observable<boolean>(true);
|
||||||
public costsVisible: ko.PureComputed<boolean>;
|
public costsVisible: ko.PureComputed<boolean>;
|
||||||
@@ -315,19 +313,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.canRequestSupport = ko.pureComputed(() => {
|
|
||||||
if (
|
|
||||||
configContext.platform !== Platform.Emulator &&
|
|
||||||
!this.container.isTryCosmosDBSubscription() &&
|
|
||||||
configContext.platform !== Platform.Portal
|
|
||||||
) {
|
|
||||||
const offerThroughput: number = this._getThroughput();
|
|
||||||
return offerThroughput <= 100000;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.costsVisible = ko.pureComputed(() => {
|
this.costsVisible = ko.pureComputed(() => {
|
||||||
return configContext.platform !== Platform.Emulator;
|
return configContext.platform !== Platform.Emulator;
|
||||||
});
|
});
|
||||||
@@ -649,8 +634,10 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getSharedThroughputDefault(): boolean {
|
public getSharedThroughputDefault(): boolean {
|
||||||
const subscriptionType = this.container.subscriptionType && this.container.subscriptionType();
|
const subscriptionType: ViewModels.SubscriptionType =
|
||||||
if (subscriptionType === SubscriptionType.EA || this.container.isServerlessEnabled()) {
|
this.container.subscriptionType && this.container.subscriptionType();
|
||||||
|
|
||||||
|
if (subscriptionType === ViewModels.SubscriptionType.EA || this.container.isServerlessEnabled()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -689,7 +676,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
databaseId: this.databaseId(),
|
databaseId: this.databaseId(),
|
||||||
rupm: this.rupm()
|
rupm: this.rupm()
|
||||||
}),
|
}),
|
||||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
||||||
@@ -792,7 +779,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
uniqueKeyPolicy,
|
uniqueKeyPolicy,
|
||||||
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
||||||
}),
|
}),
|
||||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
||||||
@@ -867,7 +854,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
uniqueKeyPolicy,
|
uniqueKeyPolicy,
|
||||||
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
||||||
}),
|
}),
|
||||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
||||||
@@ -902,7 +889,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
uniqueKeyPolicy,
|
uniqueKeyPolicy,
|
||||||
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
||||||
},
|
},
|
||||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
||||||
|
|||||||
@@ -117,10 +117,6 @@
|
|||||||
showAutoPilot: !isFreeTierAccount()
|
showAutoPilot: !isFreeTierAccount()
|
||||||
}">
|
}">
|
||||||
</throughput-input-autopilot-v3>
|
</throughput-input-autopilot-v3>
|
||||||
<p data-bind="visible: canRequestSupport">
|
|
||||||
<!-- TODO: Replace link with call to the Azure Support blade --><a
|
|
||||||
href="https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20More%20Throughput%20Request">Contact
|
|
||||||
support</a> for more than <span data-bind="text: maxThroughputRUText"></span> RU/s.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- /ko -->
|
<!-- /ko -->
|
||||||
<!-- Database provisioned throughput - End -->
|
<!-- Database provisioned throughput - End -->
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { SubscriptionType } from "../../Contracts/SubscriptionType";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import AddDatabasePane from "./AddDatabasePane";
|
import AddDatabasePane from "./AddDatabasePane";
|
||||||
import { DatabaseAccount } from "../../Contracts/DataModels";
|
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||||
@@ -44,31 +44,31 @@ describe("Add Database Pane", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should be true if subscription type is Benefits", () => {
|
it("should be true if subscription type is Benefits", () => {
|
||||||
explorer.subscriptionType(SubscriptionType.Benefits);
|
explorer.subscriptionType(ViewModels.SubscriptionType.Benefits);
|
||||||
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
||||||
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
|
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be false if subscription type is EA", () => {
|
it("should be false if subscription type is EA", () => {
|
||||||
explorer.subscriptionType(SubscriptionType.EA);
|
explorer.subscriptionType(ViewModels.SubscriptionType.EA);
|
||||||
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
||||||
expect(addDatabasePane.getSharedThroughputDefault()).toBe(false);
|
expect(addDatabasePane.getSharedThroughputDefault()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be true if subscription type is Free", () => {
|
it("should be true if subscription type is Free", () => {
|
||||||
explorer.subscriptionType(SubscriptionType.Free);
|
explorer.subscriptionType(ViewModels.SubscriptionType.Free);
|
||||||
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
||||||
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
|
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be true if subscription type is Internal", () => {
|
it("should be true if subscription type is Internal", () => {
|
||||||
explorer.subscriptionType(SubscriptionType.Internal);
|
explorer.subscriptionType(ViewModels.SubscriptionType.Internal);
|
||||||
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
||||||
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
|
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be true if subscription type is PAYG", () => {
|
it("should be true if subscription type is PAYG", () => {
|
||||||
explorer.subscriptionType(SubscriptionType.PAYG);
|
explorer.subscriptionType(ViewModels.SubscriptionType.PAYG);
|
||||||
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
||||||
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
|
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { ContextualPaneBase } from "./ContextualPaneBase";
|
|||||||
import { createDatabase } from "../../Common/dataAccess/createDatabase";
|
import { createDatabase } from "../../Common/dataAccess/createDatabase";
|
||||||
import { configContext, Platform } from "../../ConfigContext";
|
import { configContext, Platform } from "../../ConfigContext";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
import { SubscriptionType } from "../../Contracts/SubscriptionType";
|
|
||||||
|
|
||||||
export default class AddDatabasePane extends ContextualPaneBase {
|
export default class AddDatabasePane extends ContextualPaneBase {
|
||||||
public defaultExperience: ko.Computed<string>;
|
public defaultExperience: ko.Computed<string>;
|
||||||
@@ -32,7 +31,6 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
public throughputSpendAck: ko.Observable<boolean>;
|
public throughputSpendAck: ko.Observable<boolean>;
|
||||||
public throughputSpendAckVisible: ko.Computed<boolean>;
|
public throughputSpendAckVisible: ko.Computed<boolean>;
|
||||||
public requestUnitsUsageCost: ko.Computed<string>;
|
public requestUnitsUsageCost: ko.Computed<string>;
|
||||||
public canRequestSupport: ko.PureComputed<boolean>;
|
|
||||||
public costsVisible: ko.PureComputed<boolean>;
|
public costsVisible: ko.PureComputed<boolean>;
|
||||||
public upsellMessage: ko.PureComputed<string>;
|
public upsellMessage: ko.PureComputed<string>;
|
||||||
public upsellMessageAriaLabel: ko.PureComputed<string>;
|
public upsellMessageAriaLabel: ko.PureComputed<string>;
|
||||||
@@ -169,19 +167,6 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
return estimatedSpend;
|
return estimatedSpend;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.canRequestSupport = ko.pureComputed(() => {
|
|
||||||
if (
|
|
||||||
configContext.platform !== Platform.Emulator &&
|
|
||||||
!this.container.isTryCosmosDBSubscription() &&
|
|
||||||
configContext.platform !== Platform.Portal
|
|
||||||
) {
|
|
||||||
const offerThroughput: number = this.throughput();
|
|
||||||
return offerThroughput <= 100000;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.isFreeTierAccount = ko.computed<boolean>(() => {
|
this.isFreeTierAccount = ko.computed<boolean>(() => {
|
||||||
const databaseAccount = this.container && this.container.databaseAccount && this.container.databaseAccount();
|
const databaseAccount = this.container && this.container.databaseAccount && this.container.databaseAccount();
|
||||||
const isFreeTierAccount =
|
const isFreeTierAccount =
|
||||||
@@ -257,7 +242,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
const addDatabasePaneOpenMessage = {
|
const addDatabasePaneOpenMessage = {
|
||||||
databaseAccountName: this.container.databaseAccount().name,
|
databaseAccountName: this.container.databaseAccount().name,
|
||||||
defaultExperience: this.container.defaultExperience(),
|
defaultExperience: this.container.defaultExperience(),
|
||||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
throughput: this.throughput(),
|
throughput: this.throughput(),
|
||||||
@@ -285,7 +270,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
shared: this.databaseCreateNewShared()
|
shared: this.databaseCreateNewShared()
|
||||||
}),
|
}),
|
||||||
offerThroughput,
|
offerThroughput,
|
||||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
flight: this.container.flight()
|
flight: this.container.flight()
|
||||||
@@ -328,9 +313,10 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getSharedThroughputDefault(): boolean {
|
public getSharedThroughputDefault(): boolean {
|
||||||
const subscriptionType = this.container.subscriptionType && this.container.subscriptionType();
|
const subscriptionType: ViewModels.SubscriptionType =
|
||||||
|
this.container.subscriptionType && this.container.subscriptionType();
|
||||||
|
|
||||||
if (subscriptionType === SubscriptionType.EA || this.container.isServerlessEnabled()) {
|
if (subscriptionType === ViewModels.SubscriptionType.EA || this.container.isServerlessEnabled()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,7 +335,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
shared: this.databaseCreateNewShared()
|
shared: this.databaseCreateNewShared()
|
||||||
}),
|
}),
|
||||||
offerThroughput: offerThroughput,
|
offerThroughput: offerThroughput,
|
||||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
flight: this.container.flight()
|
flight: this.container.flight()
|
||||||
@@ -373,7 +359,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
shared: this.databaseCreateNewShared()
|
shared: this.databaseCreateNewShared()
|
||||||
}),
|
}),
|
||||||
offerThroughput: offerThroughput,
|
offerThroughput: offerThroughput,
|
||||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
flight: this.container.flight()
|
flight: this.container.flight()
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { ContextualPaneBase } from "./ContextualPaneBase";
|
|||||||
import { HashMap } from "../../Common/HashMap";
|
import { HashMap } from "../../Common/HashMap";
|
||||||
import { configContext, Platform } from "../../ConfigContext";
|
import { configContext, Platform } from "../../ConfigContext";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
import { SubscriptionType } from "../../Contracts/SubscriptionType";
|
|
||||||
|
|
||||||
export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||||
public createTableQuery: ko.Observable<string>;
|
public createTableQuery: ko.Observable<string>;
|
||||||
@@ -34,7 +33,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
public keyspaceThroughput: ko.Observable<number>;
|
public keyspaceThroughput: ko.Observable<number>;
|
||||||
public keyspaceCreateNew: ko.Observable<boolean>;
|
public keyspaceCreateNew: ko.Observable<boolean>;
|
||||||
public dedicateTableThroughput: ko.Observable<boolean>;
|
public dedicateTableThroughput: ko.Observable<boolean>;
|
||||||
public canRequestSupport: ko.PureComputed<boolean>;
|
|
||||||
public throughputSpendAckText: ko.Observable<string>;
|
public throughputSpendAckText: ko.Observable<string>;
|
||||||
public throughputSpendAck: ko.Observable<boolean>;
|
public throughputSpendAck: ko.Observable<boolean>;
|
||||||
public sharedThroughputSpendAck: ko.Observable<boolean>;
|
public sharedThroughputSpendAck: ko.Observable<boolean>;
|
||||||
@@ -229,15 +227,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
return configContext.platform !== Platform.Emulator;
|
return configContext.platform !== Platform.Emulator;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.canRequestSupport = ko.pureComputed(() => {
|
|
||||||
if (configContext.platform !== Platform.Emulator && !this.container.isTryCosmosDBSubscription()) {
|
|
||||||
const offerThroughput: number = this.throughput();
|
|
||||||
return offerThroughput <= 100000;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.sharedThroughputSpendAckVisible = ko.computed<boolean>(() => {
|
this.sharedThroughputSpendAckVisible = ko.computed<boolean>(() => {
|
||||||
const autoscaleThroughput = this.sharedAutoPilotThroughput() * 1;
|
const autoscaleThroughput = this.sharedAutoPilotThroughput() * 1;
|
||||||
if (this.isSharedAutoPilotSelected()) {
|
if (this.isSharedAutoPilotSelected()) {
|
||||||
@@ -315,7 +304,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
databaseId: this.keyspaceId(),
|
databaseId: this.keyspaceId(),
|
||||||
rupm: false
|
rupm: false
|
||||||
}),
|
}),
|
||||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: "u",
|
storage: "u",
|
||||||
@@ -370,7 +359,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
hasDedicatedThroughput: this.dedicateTableThroughput()
|
hasDedicatedThroughput: this.dedicateTableThroughput()
|
||||||
}),
|
}),
|
||||||
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
||||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: "u",
|
storage: "u",
|
||||||
@@ -417,7 +406,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
hasDedicatedThroughput: this.dedicateTableThroughput()
|
hasDedicatedThroughput: this.dedicateTableThroughput()
|
||||||
}),
|
}),
|
||||||
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
||||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: "u",
|
storage: "u",
|
||||||
@@ -448,7 +437,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
hasDedicatedThroughput: this.dedicateTableThroughput()
|
hasDedicatedThroughput: this.dedicateTableThroughput()
|
||||||
},
|
},
|
||||||
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
||||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: "u",
|
storage: "u",
|
||||||
|
|||||||
@@ -3,11 +3,6 @@ export var Int32 = {
|
|||||||
Max: 2147483647
|
Max: 2147483647
|
||||||
};
|
};
|
||||||
|
|
||||||
export var Int64 = {
|
|
||||||
Min: -9223372036854775808,
|
|
||||||
Max: 9223372036854775807
|
|
||||||
};
|
|
||||||
|
|
||||||
var yearMonthDay = "\\d{4}[- ][01]\\d[- ][0-3]\\d";
|
var yearMonthDay = "\\d{4}[- ][01]\\d[- ][0-3]\\d";
|
||||||
var timeOfDay = "T[0-2]\\d:[0-5]\\d(:[0-5]\\d(\\.\\d+)?)?";
|
var timeOfDay = "T[0-2]\\d:[0-5]\\d(:[0-5]\\d(\\.\\d+)?)?";
|
||||||
var timeZone = "Z|[+-][0-2]\\d:[0-5]\\d";
|
var timeZone = "Z|[+-][0-2]\\d:[0-5]\\d";
|
||||||
|
|||||||
@@ -50,24 +50,13 @@
|
|||||||
id="fileImportLinkNotebook"
|
id="fileImportLinkNotebook"
|
||||||
data-bind="event: { click: onImportLinkClick, keypress: onImportLinkKeyPress }"
|
data-bind="event: { click: onImportLinkClick, keypress: onImportLinkKeyPress }"
|
||||||
>
|
>
|
||||||
<img
|
<img class="fileImportImg" src="/folder_16x16.svg" alt="upload files" title="Upload files" />
|
||||||
id="importFileButton"
|
|
||||||
class="fileImportImg"
|
|
||||||
src="/folder_16x16.svg"
|
|
||||||
alt="upload files"
|
|
||||||
title="Upload files"
|
|
||||||
/>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="paneFooter">
|
<div class="paneFooter">
|
||||||
<div class="leftpanel-okbut">
|
<div class="leftpanel-okbut">
|
||||||
<input
|
<input type="submit" data-bind="attr: { value: submitButtonLabel }" class="btncreatecoll1" />
|
||||||
id="uploadFileButton"
|
|
||||||
type="submit"
|
|
||||||
data-bind="attr: { value: submitButtonLabel }"
|
|
||||||
class="btncreatecoll1"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Upload File inputs - End -->
|
<!-- Upload File inputs - End -->
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
class: 'scaleForm dirty',
|
class: 'scaleForm dirty',
|
||||||
value: throughput,
|
value: throughput,
|
||||||
minimum: minRUs,
|
minimum: minRUs,
|
||||||
maximum: maxRUThroughputInputLimit,
|
|
||||||
canExceedMaximumValue: canThroughputExceedMaximumValue,
|
canExceedMaximumValue: canThroughputExceedMaximumValue,
|
||||||
step: throughputIncreaseFactor,
|
step: throughputIncreaseFactor,
|
||||||
label: throughputTitle,
|
label: throughputTitle,
|
||||||
@@ -56,16 +55,6 @@
|
|||||||
<span>Learn more about minimum throughput </span>
|
<span>Learn more about minimum throughput </span>
|
||||||
<a href="https://docs.microsoft.com/azure/cosmos-db/set-throughput" target="_blank">here.</a>
|
<a href="https://docs.microsoft.com/azure/cosmos-db/set-throughput" target="_blank">here.</a>
|
||||||
</p>
|
</p>
|
||||||
<p data-bind="visible: canRequestSupport">
|
|
||||||
<!-- TODO: Replace link with call to the Azure Support blade -->
|
|
||||||
<a href="https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20More%20Throughput%20Request"
|
|
||||||
>Contact support</a
|
|
||||||
>
|
|
||||||
for more than <span data-bind="text: maxRUsText"></span> RU/s
|
|
||||||
</p>
|
|
||||||
<p data-bind="visible: shouldDisplayPortalUsePrompt">
|
|
||||||
Use Data Explorer from Azure Portal to request more than <span data-bind="text: maxRUsText"></span> RU/s
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ 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 { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
import { configContext, Platform } from "../../ConfigContext";
|
import { configContext, Platform } from "../../ConfigContext";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
|
|
||||||
@@ -33,6 +34,11 @@ const currentThroughput: (isAutoscale: boolean, throughput: number) => string =
|
|||||||
? `Current autoscale throughput: ${Math.round(throughput / 10)} - ${throughput} RU/s`
|
? `Current autoscale throughput: ${Math.round(throughput / 10)} - ${throughput} RU/s`
|
||||||
: `Current manual throughput: ${throughput} RU/s`;
|
: `Current manual throughput: ${throughput} RU/s`;
|
||||||
|
|
||||||
|
const throughputApplyDelayedMessage = (isAutoscale: boolean, throughput: number, databaseName: string) =>
|
||||||
|
`The request to increase the throughput has successfully been submitted.
|
||||||
|
This operation will take 1-3 business days to complete. View the latest status in Notifications.<br />
|
||||||
|
Database: ${databaseName}, ${currentThroughput(isAutoscale, throughput)}`;
|
||||||
|
|
||||||
const throughputApplyShortDelayMessage = (isAutoscale: boolean, throughput: number, databaseName: string) =>
|
const throughputApplyShortDelayMessage = (isAutoscale: boolean, throughput: number, databaseName: string) =>
|
||||||
`A request to increase the throughput is currently in progress.
|
`A request to increase the throughput is currently in progress.
|
||||||
This operation will take some time to complete.<br />
|
This operation will take some time to complete.<br />
|
||||||
@@ -52,23 +58,16 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
|
|
||||||
public saveSettingsButton: ViewModels.Button;
|
public saveSettingsButton: ViewModels.Button;
|
||||||
public discardSettingsChangesButton: ViewModels.Button;
|
public discardSettingsChangesButton: ViewModels.Button;
|
||||||
|
|
||||||
public canRequestSupport: ko.PureComputed<boolean>;
|
|
||||||
public canThroughputExceedMaximumValue: ko.Computed<boolean>;
|
|
||||||
public costsVisible: ko.Computed<boolean>;
|
public costsVisible: ko.Computed<boolean>;
|
||||||
public displayedError: ko.Observable<string>;
|
public displayedError: ko.Observable<string>;
|
||||||
public isTemplateReady: ko.Observable<boolean>;
|
public isTemplateReady: ko.Observable<boolean>;
|
||||||
public minRUAnotationVisible: ko.Computed<boolean>;
|
public minRUAnotationVisible: ko.Computed<boolean>;
|
||||||
public minRUs: ko.Observable<number>;
|
public minRUs: ko.Computed<number>;
|
||||||
public maxRUs: ko.Observable<number>;
|
|
||||||
public maxRUsText: ko.PureComputed<string>;
|
|
||||||
public maxRUThroughputInputLimit: ko.Computed<number>;
|
|
||||||
public notificationStatusInfo: ko.Observable<string>;
|
public notificationStatusInfo: ko.Observable<string>;
|
||||||
public pendingNotification: ko.Observable<DataModels.Notification>;
|
public pendingNotification: ko.Observable<DataModels.Notification>;
|
||||||
public requestUnitsUsageCost: ko.PureComputed<string>;
|
public requestUnitsUsageCost: ko.PureComputed<string>;
|
||||||
public autoscaleCost: ko.PureComputed<string>;
|
public autoscaleCost: ko.PureComputed<string>;
|
||||||
public shouldShowNotificationStatusPrompt: ko.Computed<boolean>;
|
public shouldShowNotificationStatusPrompt: ko.Computed<boolean>;
|
||||||
public shouldDisplayPortalUsePrompt: ko.Computed<boolean>;
|
|
||||||
public shouldShowStatusBar: ko.Computed<boolean>;
|
public shouldShowStatusBar: ko.Computed<boolean>;
|
||||||
public throughputTitle: ko.PureComputed<string>;
|
public throughputTitle: ko.PureComputed<string>;
|
||||||
public throughputAriaLabel: ko.PureComputed<string>;
|
public throughputAriaLabel: ko.PureComputed<string>;
|
||||||
@@ -85,7 +84,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
|
|
||||||
private _hasProvisioningTypeChanged: ko.Computed<boolean>;
|
private _hasProvisioningTypeChanged: ko.Computed<boolean>;
|
||||||
private _wasAutopilotOriginallySet: ko.Observable<boolean>;
|
private _wasAutopilotOriginallySet: ko.Observable<boolean>;
|
||||||
private _offerReplacePending: ko.Observable<boolean>;
|
private _offerReplacePending: ko.Computed<boolean>;
|
||||||
private container: Explorer;
|
private container: Explorer;
|
||||||
|
|
||||||
constructor(options: ViewModels.TabOptions) {
|
constructor(options: ViewModels.TabOptions) {
|
||||||
@@ -104,14 +103,15 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
this._wasAutopilotOriginallySet = ko.observable(false);
|
this._wasAutopilotOriginallySet = ko.observable(false);
|
||||||
this.isAutoPilotSelected = editable.observable(false);
|
this.isAutoPilotSelected = editable.observable(false);
|
||||||
this.autoPilotThroughput = editable.observable<number>();
|
this.autoPilotThroughput = editable.observable<number>();
|
||||||
|
const offer = this.database && this.database.offer && this.database.offer();
|
||||||
|
const offerAutopilotSettings = offer && offer.content && offer.content.offerAutopilotSettings;
|
||||||
this.userCanChangeProvisioningTypes = ko.observable(true);
|
this.userCanChangeProvisioningTypes = ko.observable(true);
|
||||||
|
|
||||||
const autoscaleMaxThroughput = this.database?.offer()?.autoscaleMaxThroughput;
|
if (offerAutopilotSettings && offerAutopilotSettings.maxThroughput) {
|
||||||
if (autoscaleMaxThroughput) {
|
if (AutoPilotUtils.isValidAutoPilotThroughput(offerAutopilotSettings.maxThroughput)) {
|
||||||
if (AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
|
|
||||||
this._wasAutopilotOriginallySet(true);
|
this._wasAutopilotOriginallySet(true);
|
||||||
this.isAutoPilotSelected(true);
|
this.isAutoPilotSelected(true);
|
||||||
this.autoPilotThroughput(autoscaleMaxThroughput);
|
this.autoPilotThroughput(offerAutopilotSettings.maxThroughput);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,22 +173,6 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
return configContext.platform !== Platform.Emulator;
|
return configContext.platform !== Platform.Emulator;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.shouldDisplayPortalUsePrompt = ko.pureComputed<boolean>(() => configContext.platform === Platform.Hosted);
|
|
||||||
this.canThroughputExceedMaximumValue = ko.pureComputed<boolean>(
|
|
||||||
() => configContext.platform === Platform.Portal && !this.container.isRunningOnNationalCloud()
|
|
||||||
);
|
|
||||||
this.canRequestSupport = ko.pureComputed(() => {
|
|
||||||
if (
|
|
||||||
configContext.platform === Platform.Emulator ||
|
|
||||||
configContext.platform === Platform.Hosted ||
|
|
||||||
this.canThroughputExceedMaximumValue()
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.overrideWithAutoPilotSettings = ko.pureComputed(() => {
|
this.overrideWithAutoPilotSettings = ko.pureComputed(() => {
|
||||||
return this._hasProvisioningTypeChanged() && this._wasAutopilotOriginallySet();
|
return this._hasProvisioningTypeChanged() && this._wasAutopilotOriginallySet();
|
||||||
});
|
});
|
||||||
@@ -197,28 +181,30 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
return this._hasProvisioningTypeChanged() && !this._wasAutopilotOriginallySet();
|
return this._hasProvisioningTypeChanged() && !this._wasAutopilotOriginallySet();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.minRUs = ko.observable<number>(
|
this.minRUs = ko.computed<number>(() => {
|
||||||
this.database.offer()?.minimumThroughput || this.container.collectionCreationDefaults.throughput.unlimitedmin
|
const offerContent =
|
||||||
);
|
this.database && this.database.offer && this.database.offer() && this.database.offer().content;
|
||||||
|
|
||||||
|
// TODO: backend is returning 1,000,000 as min throughput which seems wrong
|
||||||
|
// Setting to min throughput to not block and let the backend pass or fail
|
||||||
|
if (offerContent && offerContent.offerAutopilotSettings) {
|
||||||
|
return 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectionThroughputInfo: DataModels.OfferThroughputInfo =
|
||||||
|
offerContent && offerContent.collectionThroughputInfo;
|
||||||
|
|
||||||
|
if (collectionThroughputInfo && !!collectionThroughputInfo.minimumRUForCollection) {
|
||||||
|
return collectionThroughputInfo.minimumRUForCollection;
|
||||||
|
}
|
||||||
|
const throughputDefaults = this.container.collectionCreationDefaults.throughput;
|
||||||
|
return throughputDefaults.unlimitedmin;
|
||||||
|
});
|
||||||
|
|
||||||
this.minRUAnotationVisible = ko.computed<boolean>(() => {
|
this.minRUAnotationVisible = ko.computed<boolean>(() => {
|
||||||
return PricingUtils.isLargerThanDefaultMinRU(this.minRUs());
|
return PricingUtils.isLargerThanDefaultMinRU(this.minRUs());
|
||||||
});
|
});
|
||||||
|
|
||||||
this.maxRUs = ko.observable<number>(this.container.collectionCreationDefaults.throughput.unlimitedmax);
|
|
||||||
|
|
||||||
this.maxRUThroughputInputLimit = ko.pureComputed<number>(() => {
|
|
||||||
if (configContext.platform === Platform.Hosted) {
|
|
||||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.maxRUs();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.maxRUsText = ko.pureComputed(() => {
|
|
||||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million.toLocaleString();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.throughputTitle = ko.pureComputed<string>(() => {
|
this.throughputTitle = ko.pureComputed<string>(() => {
|
||||||
if (this.isAutoPilotSelected()) {
|
if (this.isAutoPilotSelected()) {
|
||||||
return AutoPilotUtils.getAutoPilotHeaderText();
|
return AutoPilotUtils.getAutoPilotHeaderText();
|
||||||
@@ -231,33 +217,39 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
return this.throughputTitle() + this.requestUnitsUsageCost();
|
return this.throughputTitle() + this.requestUnitsUsageCost();
|
||||||
});
|
});
|
||||||
this.pendingNotification = ko.observable<DataModels.Notification>();
|
this.pendingNotification = ko.observable<DataModels.Notification>();
|
||||||
this._offerReplacePending = ko.observable<boolean>(
|
this._offerReplacePending = ko.pureComputed<boolean>(() => {
|
||||||
!!this.database.offer()?.headers?.[Constants.HttpHeaders.offerReplacePending]
|
const offer = this.database && this.database.offer && this.database.offer();
|
||||||
|
return (
|
||||||
|
offer &&
|
||||||
|
offer.hasOwnProperty("headers") &&
|
||||||
|
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
||||||
);
|
);
|
||||||
|
});
|
||||||
this.notificationStatusInfo = ko.observable<string>("");
|
this.notificationStatusInfo = ko.observable<string>("");
|
||||||
this.shouldShowNotificationStatusPrompt = ko.computed<boolean>(() => this.notificationStatusInfo().length > 0);
|
this.shouldShowNotificationStatusPrompt = ko.computed<boolean>(() => this.notificationStatusInfo().length > 0);
|
||||||
this.warningMessage = ko.computed<string>(() => {
|
this.warningMessage = ko.computed<string>(() => {
|
||||||
|
const offer = this.database && this.database.offer && this.database.offer();
|
||||||
|
|
||||||
if (this.overrideWithProvisionedThroughputSettings()) {
|
if (this.overrideWithProvisionedThroughputSettings()) {
|
||||||
return AutoPilotUtils.manualToAutoscaleDisclaimer;
|
return AutoPilotUtils.manualToAutoscaleDisclaimer;
|
||||||
}
|
}
|
||||||
|
|
||||||
const offer = this.database.offer();
|
if (
|
||||||
if (offer?.headers?.[Constants.HttpHeaders.offerReplacePending]) {
|
offer &&
|
||||||
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
|
offer.hasOwnProperty("headers") &&
|
||||||
|
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
||||||
|
) {
|
||||||
|
const throughput = offer.content.offerAutopilotSettings
|
||||||
|
? offer.content.offerAutopilotSettings.maxThroughput
|
||||||
|
: offer.content.offerThroughput;
|
||||||
|
|
||||||
return throughputApplyShortDelayMessage(this.isAutoPilotSelected(), throughput, this.database.id());
|
return throughputApplyShortDelayMessage(this.isAutoPilotSelected(), throughput, this.database.id());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million) {
|
||||||
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
|
||||||
this.canThroughputExceedMaximumValue()
|
|
||||||
) {
|
|
||||||
return updateThroughputBeyondLimitWarningMessage;
|
return updateThroughputBeyondLimitWarningMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.throughput() > this.maxRUs()) {
|
|
||||||
return updateThroughputDelayedApplyWarningMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.pendingNotification()) {
|
if (this.pendingNotification()) {
|
||||||
const matches: string[] = this.pendingNotification().description.match("Throughput update for (.*) RU/s");
|
const matches: string[] = this.pendingNotification().description.match("Throughput update for (.*) RU/s");
|
||||||
const throughput: number = matches.length > 1 && Number(matches[1]);
|
const throughput: number = matches.length > 1 && Number(matches[1]);
|
||||||
@@ -316,13 +308,6 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
!this.canThroughputExceedMaximumValue() &&
|
|
||||||
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.throughput.editableIsDirty()) {
|
if (this.throughput.editableIsDirty()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -380,26 +365,38 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
const headerOptions: RequestOptions = { initialHeaders: {} };
|
const headerOptions: RequestOptions = { initialHeaders: {} };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (this.isAutoPilotSelected()) {
|
||||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||||
databaseId: this.database.id(),
|
databaseId: this.database.id(),
|
||||||
currentOffer: this.database.offer(),
|
currentOffer: this.database.offer(),
|
||||||
autopilotThroughput: this.isAutoPilotSelected() ? this.autoPilotThroughput() : undefined,
|
autopilotThroughput: this.autoPilotThroughput(),
|
||||||
manualThroughput: this.isAutoPilotSelected() ? undefined : this.throughput()
|
manualThroughput: undefined,
|
||||||
|
migrateToAutoPilot: this._hasProvisioningTypeChanged()
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this._hasProvisioningTypeChanged()) {
|
|
||||||
if (this.isAutoPilotSelected()) {
|
|
||||||
updateOfferParams.migrateToAutoPilot = true;
|
|
||||||
} else {
|
|
||||||
updateOfferParams.migrateToManual = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||||
this.database.offer(updatedOffer);
|
this.database.offer(updatedOffer);
|
||||||
this.database.offer.valueHasMutated();
|
this.database.offer.valueHasMutated();
|
||||||
this._setBaseline();
|
|
||||||
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
||||||
|
} else {
|
||||||
|
if (this.throughput.editableIsDirty() || this.isAutoPilotSelected.editableIsDirty()) {
|
||||||
|
const originalThroughputValue = this.throughput.getEditableOriginalValue();
|
||||||
|
const newThroughput = this.throughput();
|
||||||
|
|
||||||
|
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||||
|
databaseId: this.database.id(),
|
||||||
|
currentOffer: this.database.offer(),
|
||||||
|
autopilotThroughput: undefined,
|
||||||
|
manualThroughput: newThroughput,
|
||||||
|
migrateToManual: this._hasProvisioningTypeChanged()
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedOffer = await updateOffer(updateOfferParams);
|
||||||
|
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
||||||
|
this.database.offer(updatedOffer);
|
||||||
|
this.database.offer.valueHasMutated();
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.container.isRefreshingExplorer(false);
|
this.container.isRefreshingExplorer(false);
|
||||||
this.isExecutionError(true);
|
this.isExecutionError(true);
|
||||||
@@ -441,10 +438,15 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
|
|
||||||
private _setBaseline() {
|
private _setBaseline() {
|
||||||
const offer = this.database && this.database.offer && this.database.offer();
|
const offer = this.database && this.database.offer && this.database.offer();
|
||||||
this.isAutoPilotSelected.setBaseline(AutoPilotUtils.isValidAutoPilotThroughput(offer.autoscaleMaxThroughput));
|
const offerThroughput = offer.content && offer.content.offerThroughput;
|
||||||
this.autoPilotThroughput.setBaseline(offer.autoscaleMaxThroughput);
|
const offerAutopilotSettings = offer && offer.content && offer.content.offerAutopilotSettings;
|
||||||
this.throughput.setBaseline(offer.manualThroughput);
|
|
||||||
|
this.throughput.setBaseline(offerThroughput);
|
||||||
this.userCanChangeProvisioningTypes(true);
|
this.userCanChangeProvisioningTypes(true);
|
||||||
|
|
||||||
|
const maxThroughputForAutoPilot = offerAutopilotSettings && offerAutopilotSettings.maxThroughput;
|
||||||
|
this.isAutoPilotSelected.setBaseline(AutoPilotUtils.isValidAutoPilotThroughput(maxThroughputForAutoPilot));
|
||||||
|
this.autoPilotThroughput.setBaseline(maxThroughputForAutoPilot || AutoPilotUtils.minAutoPilotThroughput);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||||
|
|||||||
721
src/Explorer/Tabs/SettingsTab.html
Normal file
721
src/Explorer/Tabs/SettingsTab.html
Normal file
@@ -0,0 +1,721 @@
|
|||||||
|
<div
|
||||||
|
class="tab-pane flexContainer"
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
id: tabId
|
||||||
|
},
|
||||||
|
visible: isActive"
|
||||||
|
role="tabpanel"
|
||||||
|
>
|
||||||
|
<div class="warningErrorContainer scaleWarningContainer" data-bind="visible: shouldShowStatusBar">
|
||||||
|
<div>
|
||||||
|
<div class="warningErrorContent" data-bind="visible: shouldShowNotificationStatusPrompt">
|
||||||
|
<span><img src="/info_color.svg" alt="Info"/></span>
|
||||||
|
<span class="warningErrorDetailsLinkContainer" data-bind="html: notificationStatusInfo"></span>
|
||||||
|
</div>
|
||||||
|
<div class="warningErrorContent" data-bind="visible: !shouldShowNotificationStatusPrompt()">
|
||||||
|
<span><img src="/warning.svg" alt="Warning"/></span>
|
||||||
|
<span class="warningErrorDetailsLinkContainer" data-bind="html: warningMessage"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tabForm scaleSettingScrollable">
|
||||||
|
<!-- ko if: shouldShowKeyspaceSharedThroughputMessage -->
|
||||||
|
<div>This table shared throughput is configured at the keyspace</div>
|
||||||
|
<!-- /ko -->
|
||||||
|
|
||||||
|
<!-- ko ifnot: hasDatabaseSharedThroughput -->
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="scaleDivison"
|
||||||
|
data-bind="click:toggleScale, event: { keypress: onScaleKeyPress }, attr:{ 'aria-expanded': scaleExpanded() ? 'true' : 'false' }"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
aria-label="Scale"
|
||||||
|
aria-controls="scaleRegion"
|
||||||
|
>
|
||||||
|
<span class="themed-images" type="text/html" id="ExpandChevronRightScale" data-bind="visible: !scaleExpanded()">
|
||||||
|
<img
|
||||||
|
class="imgiconwidth ssExpandCollapseIcon ssCollapseIcon "
|
||||||
|
src="/Triangle-right.svg"
|
||||||
|
alt="Show scale properties"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="themed-images" type="text/html" id="ExpandChevronDownScale" data-bind="visible: scaleExpanded">
|
||||||
|
<img class="imgiconwidth ssExpandCollapseIcon " src="/Triangle-down.svg" alt="Hide scale properties" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="scaleSettingTitle">Scale</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ssTextAllignment" data-bind="visible: scaleExpanded" id="scaleRegion">
|
||||||
|
<!-- ko ifnot: isAutoScaleEnabled -->
|
||||||
|
<throughput-input-autopilot-v3
|
||||||
|
params="{
|
||||||
|
testId: testId,
|
||||||
|
class: 'scaleForm dirty',
|
||||||
|
value: throughput,
|
||||||
|
minimum: minRUs,
|
||||||
|
isEnabled: !hasDatabaseSharedThroughput(),
|
||||||
|
label: throughputTitle,
|
||||||
|
ariaLabel: throughputAriaLabel,
|
||||||
|
costsVisible: costsVisible,
|
||||||
|
requestUnitsUsageCost: requestUnitsUsageCost,
|
||||||
|
throughputAutoPilotRadioId: throughputAutoPilotRadioId,
|
||||||
|
throughputProvisionedRadioId: throughputProvisionedRadioId,
|
||||||
|
throughputModeRadioName: throughputModeRadioName,
|
||||||
|
showAutoPilot: userCanChangeProvisioningTypes,
|
||||||
|
isAutoPilotSelected: isAutoPilotSelected,
|
||||||
|
maxAutoPilotThroughputSet: autoPilotThroughput,
|
||||||
|
autoPilotUsageCost: autoPilotUsageCost,
|
||||||
|
overrideWithAutoPilotSettings: overrideWithAutoPilotSettings,
|
||||||
|
overrideWithProvisionedThroughputSettings: overrideWithProvisionedThroughputSettings
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
</throughput-input-autopilot-v3>
|
||||||
|
|
||||||
|
<div class="storageCapacityTitle throughputStorageValue" data-bind="html: storageCapacityTitle"></div>
|
||||||
|
<!-- /ko -->
|
||||||
|
|
||||||
|
<div data-bind="visible: rupmVisible">
|
||||||
|
<div class="formTitle">RU/m</div>
|
||||||
|
<div class="tabs" aria-label="RU/m">
|
||||||
|
<div class="tab">
|
||||||
|
<label
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
for: rupmOnId
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
dirty: rupm.editableIsDirty,
|
||||||
|
selectedRadio: rupm() === 'on',
|
||||||
|
unselectedRadio: rupm() !== 'on'
|
||||||
|
}"
|
||||||
|
>On</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="rupm"
|
||||||
|
value="on"
|
||||||
|
class="radio"
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
id: rupmOnId
|
||||||
|
},
|
||||||
|
checked: rupm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="tab">
|
||||||
|
<label
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
for: rupmOffId
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
dirty: rupm.editableIsDirty,
|
||||||
|
selectedRadio: rupm() === 'off',
|
||||||
|
unselectedRadio: rupm() !== 'off'
|
||||||
|
}"
|
||||||
|
>Off</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="rupm"
|
||||||
|
value="off"
|
||||||
|
class="radio"
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
id: rupmOffId
|
||||||
|
},
|
||||||
|
checked: rupm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TODO: Replace link with call to the Azure Support blade -->
|
||||||
|
<div data-bind="visible: isAutoScaleEnabled">
|
||||||
|
<div class="autoScaleThroughputTitle">Throughput (RU/s)</div>
|
||||||
|
<input
|
||||||
|
class="formReadOnly collid-white"
|
||||||
|
readonly
|
||||||
|
aria-label="Throughput input"
|
||||||
|
data-bind="textInput: throughput"
|
||||||
|
/>
|
||||||
|
<div class="autoScaleDescription">
|
||||||
|
Your account has custom settings that prevents setting throughput at the container level. Please work with
|
||||||
|
your Cosmos DB engineering team point of contact to make changes.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- /ko -->
|
||||||
|
|
||||||
|
<div data-bind="visible: hasConflictResolution">
|
||||||
|
<div
|
||||||
|
class="formTitle"
|
||||||
|
data-bind="click:toggleConflictResolution, event: { keypress: onConflictResolutionKeyPress }, attr:{ 'aria-expanded': conflictResolutionExpanded() ? 'true' : 'false' }"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
aria-label="Conflict Resolution"
|
||||||
|
aria-controls="conflictResolutionRegion"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="themed-images"
|
||||||
|
type="text/html"
|
||||||
|
id="ExpandChevronRightConflictResolution"
|
||||||
|
data-bind="visible: !conflictResolutionExpanded()"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="imgiconwidth ssExpandCollapseIcon ssCollapseIcon"
|
||||||
|
src="/Triangle-right.svg"
|
||||||
|
alt="Show conflict resolution"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="themed-images"
|
||||||
|
type="text/html"
|
||||||
|
id="ExpandChevronDownConflictResolution"
|
||||||
|
data-bind="visible: conflictResolutionExpanded"
|
||||||
|
>
|
||||||
|
<img class="imgiconwidth ssExpandCollapseIcon" src="/Triangle-down.svg" alt="Show conflict resolution" />
|
||||||
|
</span>
|
||||||
|
<span class="scaleSettingTitle">Conflict resolution</span>
|
||||||
|
</div>
|
||||||
|
<div id="conflictResolutionRegion" class="ssTextAllignment" data-bind="visible: conflictResolutionExpanded">
|
||||||
|
<div class="formTitle">Mode</div>
|
||||||
|
<div class="tabs" aria-label="Mode" role="radiogroup">
|
||||||
|
<div class="tab">
|
||||||
|
<label
|
||||||
|
tabindex="0"
|
||||||
|
role="radio"
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
for: conflictResolutionPolicyModeLWW,
|
||||||
|
'aria-checked': conflictResolutionPolicyMode() !== 'Custom' ? 'true' : 'false'
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
dirty: conflictResolutionPolicyMode.editableIsDirty,
|
||||||
|
selectedRadio: conflictResolutionPolicyMode() === 'LastWriterWins',
|
||||||
|
unselectedRadio: conflictResolutionPolicyMode() !== 'LastWriterWins'
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
keypress: onConflictResolutionLWWKeyPress
|
||||||
|
}"
|
||||||
|
>Last Write Wins (default)</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="conflictresolution"
|
||||||
|
value="LastWriterWins"
|
||||||
|
class="radio"
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
id: conflictResolutionPolicyModeLWW
|
||||||
|
},
|
||||||
|
checked: conflictResolutionPolicyMode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab">
|
||||||
|
<label
|
||||||
|
tabindex="0"
|
||||||
|
role="radio"
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
for: conflictResolutionPolicyModeCustom,
|
||||||
|
'aria-checked': conflictResolutionPolicyMode() === 'Custom' ? 'true' : 'false'
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
dirty: conflictResolutionPolicyMode.editableIsDirty,
|
||||||
|
selectedRadio: conflictResolutionPolicyMode() === 'Custom',
|
||||||
|
unselectedRadio: conflictResolutionPolicyMode() !== 'Custom'
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
keypress: onConflictResolutionCustomKeyPress
|
||||||
|
}"
|
||||||
|
>Merge Procedure (custom)</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="conflictresolution"
|
||||||
|
value="Custom"
|
||||||
|
class="radio"
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
id: conflictResolutionPolicyModeCustom
|
||||||
|
},
|
||||||
|
checked: conflictResolutionPolicyMode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div data-bind="visible: conflictResolutionPolicyMode() === 'LastWriterWins'">
|
||||||
|
<p class="formTitle">
|
||||||
|
Conflict Resolver Property
|
||||||
|
<span class="infoTooltip" role="tooltip" tabindex="0">
|
||||||
|
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
|
||||||
|
<span class="tooltiptext infoTooltipWidth"
|
||||||
|
>Gets or sets the name of a integer property in your documents which is used for the Last Write Wins
|
||||||
|
(LWW) based conflict resolution scheme. By default, the system uses the system defined timestamp
|
||||||
|
property, _ts to decide the winner for the conflicting versions of the document. Specify your own
|
||||||
|
integer property if you want to override the default timestamp based conflict resolution.</span
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
aria-label="Document path for conflict resolution"
|
||||||
|
data-bind="
|
||||||
|
css: {
|
||||||
|
dirty: conflictResolutionPolicyPath.editableIsDirty
|
||||||
|
},
|
||||||
|
textInput: conflictResolutionPolicyPath,
|
||||||
|
enable: conflictResolutionPolicyMode() === 'LastWriterWins'"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div data-bind="visible: conflictResolutionPolicyMode() === 'Custom'">
|
||||||
|
<p class="formTitle">
|
||||||
|
Stored procedure
|
||||||
|
<span class="infoTooltip" role="tooltip" tabindex="0">
|
||||||
|
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
|
||||||
|
<span class="tooltiptext infoTooltipWidth"
|
||||||
|
>Gets or sets the name of a stored procedure (aka merge procedure) for resolving the conflicts. You can
|
||||||
|
write application defined logic to determine the winner of the conflicting versions of a document. The
|
||||||
|
stored procedure will get executed transactionally, exactly once, on the server side. If you do not
|
||||||
|
provide a stored procedure, the conflicts will be populated in the
|
||||||
|
<a class="linkDarkBackground" href="https://aka.ms/dataexplorerconflics" target="_blank"
|
||||||
|
>conflicts feed</a
|
||||||
|
>. You can update/re-register the stored procedure at any time.</span
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
aria-label="Stored procedure name for conflict resolution"
|
||||||
|
data-bind="
|
||||||
|
css: {
|
||||||
|
dirty: conflictResolutionPolicyProcedure.editableIsDirty
|
||||||
|
},
|
||||||
|
textInput: conflictResolutionPolicyProcedure,
|
||||||
|
enable: conflictResolutionPolicyMode() === 'Custom'"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="formTitle"
|
||||||
|
data-bind="click:toggleSettings, event: { keypress: onSettingsKeyPress }, attr:{ 'aria-expanded': settingsExpanded() ? 'true' : 'false' }, visible: shouldShowIndexingPolicyEditor() || ttlVisible()"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
aria-label="Settings"
|
||||||
|
aria-controls="settingsRegion"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="themed-images"
|
||||||
|
type="text/html"
|
||||||
|
id="ExpandChevronRightSettings"
|
||||||
|
data-bind="visible: !settingsExpanded() && !hasDatabaseSharedThroughput()"
|
||||||
|
>
|
||||||
|
<img class="imgiconwidth ssExpandCollapseIcon ssCollapseIcon" src="/Triangle-right.svg" alt="Show settings" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="themed-images"
|
||||||
|
type="text/html"
|
||||||
|
id="ExpandChevronDownSettings"
|
||||||
|
data-bind="visible: settingsExpanded() && !hasDatabaseSharedThroughput()"
|
||||||
|
>
|
||||||
|
<img class="imgiconwidth ssExpandCollapseIcon" src="/Triangle-down.svg" alt="Show settings" />
|
||||||
|
</span>
|
||||||
|
<span class="scaleSettingTitle">Settings</span>
|
||||||
|
</div>
|
||||||
|
<div class="ssTextAllignment" data-bind="visible: settingsExpanded" id="settingsRegion">
|
||||||
|
<div data-bind="visible: ttlVisible">
|
||||||
|
<div class="formTitle">Time to Live</div>
|
||||||
|
<div class="tabs disableFocusDefaults" aria-label="Time to Live" role="radiogroup">
|
||||||
|
<div class="tab">
|
||||||
|
<label
|
||||||
|
class="ttlIndexingPolicyFocusElement"
|
||||||
|
tabindex="0"
|
||||||
|
role="radio"
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
for: ttlOffId,
|
||||||
|
'aria-checked': timeToLive() === 'off' ? 'true' : 'false'
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
dirty: timeToLive.editableIsDirty,
|
||||||
|
selectedRadio: timeToLive() === 'off',
|
||||||
|
unselectedRadio: timeToLive() !== 'off'
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
keypress: onTtlOffKeyPress
|
||||||
|
},
|
||||||
|
hasFocus: ttlOffFocused"
|
||||||
|
>Off</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="ttl"
|
||||||
|
value="off"
|
||||||
|
class="radio"
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
id: ttlOffId
|
||||||
|
},
|
||||||
|
checked: timeToLive"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab">
|
||||||
|
<label
|
||||||
|
class="ttlIndexingPolicyFocusElement"
|
||||||
|
tabindex="0"
|
||||||
|
role="radio"
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
for: ttlOnNoDefaultId,
|
||||||
|
'aria-checked': timeToLive() === 'on-nodefault' ? 'true' : 'false'
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
dirty: timeToLive.editableIsDirty,
|
||||||
|
selectedRadio: timeToLive() === 'on-nodefault',
|
||||||
|
unselectedRadio: timeToLive() !== 'on-nodefault'
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
keypress: onTtlOnNoDefaultKeyPress
|
||||||
|
},
|
||||||
|
hasFocus: ttlOnDefaultFocused"
|
||||||
|
>On (no default)</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="ttl"
|
||||||
|
value="on-nodefault"
|
||||||
|
class="radio"
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
id: ttlOnNoDefaultId
|
||||||
|
},
|
||||||
|
checked: timeToLive"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab">
|
||||||
|
<label
|
||||||
|
class="ttlIndexingPolicyFocusElement"
|
||||||
|
tabindex="0"
|
||||||
|
role="radio"
|
||||||
|
for="ttl3"
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
for: ttlOnId,
|
||||||
|
'aria-checked': timeToLive() === 'on' ? 'true' : 'false'
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
dirty: timeToLive.editableIsDirty,
|
||||||
|
selectedRadio: timeToLive() === 'on',
|
||||||
|
unselectedRadio: timeToLive() !== 'on'
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
keypress: onTtlOnKeyPress
|
||||||
|
},
|
||||||
|
hasFocus: ttlOnFocused"
|
||||||
|
>On</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="ttl"
|
||||||
|
value="on"
|
||||||
|
class="radio"
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
id: ttlOnId
|
||||||
|
},
|
||||||
|
checked: timeToLive"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div data-bind="visible: timeToLive() === 'on'">
|
||||||
|
<input
|
||||||
|
class="dirtyTextbox"
|
||||||
|
type="number"
|
||||||
|
required
|
||||||
|
min="1"
|
||||||
|
max="2147483647"
|
||||||
|
aria-label="Time to live in seconds"
|
||||||
|
data-bind="
|
||||||
|
css: {
|
||||||
|
dirty: timeToLive.editableIsDirty
|
||||||
|
},
|
||||||
|
textInput: timeToLiveSeconds,
|
||||||
|
enable: timeToLive() === 'on'"
|
||||||
|
/>
|
||||||
|
second(s)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Geospatial - start -->
|
||||||
|
<div data-bind="visible: geospatialVisible">
|
||||||
|
<div class="formTitle">Geospatial Configuration</div>
|
||||||
|
|
||||||
|
<div class="tabs disableFocusDefaults" aria-label="Geospatial Configuration" role="radiogroup">
|
||||||
|
<div class="tab">
|
||||||
|
<label
|
||||||
|
for="geography"
|
||||||
|
tabindex="0"
|
||||||
|
role="radio"
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
'aria-checked': geospatialConfigType().toLowerCase() !== GEOMETRY.toLowerCase() ? 'true' : 'false'
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
dirty: geospatialConfigType.editableIsDirty,
|
||||||
|
selectedRadio: geospatialConfigType().toLowerCase() !== GEOMETRY.toLowerCase(),
|
||||||
|
unselectedRadio: geospatialConfigType().toLowerCase() === GEOMETRY.toLowerCase()
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
keypress: onGeographyKeyPress
|
||||||
|
}"
|
||||||
|
>Geography</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="geospatial"
|
||||||
|
id="geography"
|
||||||
|
class="radio"
|
||||||
|
data-bind="
|
||||||
|
attr: {
|
||||||
|
value: GEOGRAPHY
|
||||||
|
},
|
||||||
|
checked: geospatialConfigType"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab">
|
||||||
|
<label
|
||||||
|
for="geometry"
|
||||||
|
tabindex="0"
|
||||||
|
role="radio"
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
'aria-checked': geospatialConfigType().toLowerCase() === GEOMETRY.toLowerCase() ? 'true' : 'false'
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
dirty: geospatialConfigType.editableIsDirty,
|
||||||
|
selectedRadio: geospatialConfigType().toLowerCase() === GEOMETRY.toLowerCase(),
|
||||||
|
unselectedRadio: geospatialConfigType().toLowerCase() !== GEOMETRY.toLowerCase()
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
keypress: onGeometryKeyPress
|
||||||
|
}"
|
||||||
|
>Geometry</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="geospatial"
|
||||||
|
id="geometry"
|
||||||
|
class="radio"
|
||||||
|
data-bind="
|
||||||
|
attr: {
|
||||||
|
value: GEOMETRY
|
||||||
|
},
|
||||||
|
checked: geospatialConfigType"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Geospatial - end -->
|
||||||
|
|
||||||
|
<div data-bind="visible: isAnalyticalStorageEnabled">
|
||||||
|
<div class="formTitle">Analytical Storage Time to Live</div>
|
||||||
|
<div class="tabs disableFocusDefaults" aria-label="Analytical Storage Time to Live" role="radiogroup">
|
||||||
|
<div class="tab">
|
||||||
|
<label tabindex="0" role="radio" class="disabledRadio">Off </label>
|
||||||
|
</div>
|
||||||
|
<div class="tab">
|
||||||
|
<label
|
||||||
|
tabindex="0"
|
||||||
|
role="radio"
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
for: 'analyticalStorageTtlOnNoDefaultId',
|
||||||
|
'aria-checked': analyticalStorageTtlSelection() === 'on-nodefault' ? 'true' : 'false'
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
dirty: analyticalStorageTtlSelection.editableIsDirty,
|
||||||
|
selectedRadio: analyticalStorageTtlSelection() === 'on-nodefault',
|
||||||
|
unselectedRadio: analyticalStorageTtlSelection() !== 'on-nodefault'
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
keypress: onAnalyticalStorageTtlOnNoDefaultKeyPress
|
||||||
|
}"
|
||||||
|
>On (no default)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="analyticalStorageTtl"
|
||||||
|
value="on-nodefault"
|
||||||
|
class="radio"
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
id: 'analyticalStorageTtlOnNoDefaultId'
|
||||||
|
},
|
||||||
|
checked: analyticalStorageTtlSelection"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab">
|
||||||
|
<label
|
||||||
|
tabindex="0"
|
||||||
|
role="radio"
|
||||||
|
for="ttl3"
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
for: 'analyticalStorageTtlOnId',
|
||||||
|
'aria-checked': analyticalStorageTtlSelection() === 'on' ? 'true' : 'false'
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
dirty: analyticalStorageTtlSelection.editableIsDirty,
|
||||||
|
selectedRadio: analyticalStorageTtlSelection() === 'on',
|
||||||
|
unselectedRadio: analyticalStorageTtlSelection() !== 'on'
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
keypress: onAnalyticalStorageTtlOnKeyPress
|
||||||
|
}"
|
||||||
|
>On</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="analyticalStorageTtl"
|
||||||
|
value="on"
|
||||||
|
class="radio"
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
id: 'analyticalStorageTtlOnId'
|
||||||
|
},
|
||||||
|
checked: analyticalStorageTtlSelection"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div data-bind="visible: analyticalStorageTtlSelection() === 'on'">
|
||||||
|
<input
|
||||||
|
class="dirtyTextbox"
|
||||||
|
type="number"
|
||||||
|
required
|
||||||
|
min="1"
|
||||||
|
max="2147483647"
|
||||||
|
aria-label="Time to live in seconds"
|
||||||
|
data-bind="
|
||||||
|
css: {
|
||||||
|
dirty: analyticalStorageTtlSelection.editableIsDirty
|
||||||
|
},
|
||||||
|
textInput: analyticalStorageTtlSeconds,
|
||||||
|
enable: analyticalStorageTtlSelection() === 'on'"
|
||||||
|
/>
|
||||||
|
second(s)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div data-bind="visible: changeFeedPolicyVisible">
|
||||||
|
<div class="formTitle">
|
||||||
|
<span>Change feed log retention policy</span>
|
||||||
|
<span class="infoTooltip" role="tooltip" tabindex="0">
|
||||||
|
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
|
||||||
|
<span class="tooltiptext infoTooltipWidth"
|
||||||
|
>Enable change feed log retention policy to retain last 10 minutes of history for items in the container
|
||||||
|
by default. To support this, the request unit (RU) charge for this container will be multiplied by a
|
||||||
|
factor of two for writes. Reads are unaffected.</span
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="tabs disableFocusDefaults" aria-label="Change feed selection tabs">
|
||||||
|
<div class="tab">
|
||||||
|
<label
|
||||||
|
tabindex="0"
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
for: changeFeedPolicyOffId
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
dirty: changeFeedPolicyToggled.editableIsDirty,
|
||||||
|
selectedRadio: changeFeedPolicyToggled() === 'Off',
|
||||||
|
unselectedRadio: changeFeedPolicyToggled() === 'On'
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
keypress: onChangeFeedPolicyOffKeyPress
|
||||||
|
}"
|
||||||
|
>Off</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="changeFeedPolicy"
|
||||||
|
value="Off"
|
||||||
|
class="radio"
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
id: changeFeedPolicyOffId
|
||||||
|
},
|
||||||
|
checked: changeFeedPolicyToggled"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="tab">
|
||||||
|
<label
|
||||||
|
tabindex="0"
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
for: changeFeedPolicyOnId
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
dirty: changeFeedPolicyToggled.editableIsDirty,
|
||||||
|
selectedRadio: changeFeedPolicyToggled() === 'On',
|
||||||
|
unselectedRadio: changeFeedPolicyToggled() === 'Off'
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
keypress: onChangeFeedPolicyOnKeyPress
|
||||||
|
}"
|
||||||
|
>On</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="changeFeedPolicy"
|
||||||
|
value="On"
|
||||||
|
class="radio"
|
||||||
|
data-bind="
|
||||||
|
attr:{
|
||||||
|
id: changeFeedPolicyOnId
|
||||||
|
},
|
||||||
|
checked: changeFeedPolicyToggled"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div data-bind="visible: partitionKeyVisible">
|
||||||
|
<div class="formTitle" data-bind="text: partitionKeyName">Partition Key</div>
|
||||||
|
<input
|
||||||
|
class="formReadOnly collid-white"
|
||||||
|
data-bind="textInput: partitionKeyValue, attr: { 'aria-label':partitionKeyName }"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="largePartitionKeyEnabled" data-bind="visible: isLargePartitionKeyEnabled">
|
||||||
|
<p data-bind="visible: isLargePartitionKeyEnabled">
|
||||||
|
Large <span data-bind="text:lowerCasePartitionKeyName"></span> has been enabled
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div data-bind="visible: shouldShowIndexingPolicyEditor">
|
||||||
|
<div class="formTitle">Indexing Policy</div>
|
||||||
|
<div
|
||||||
|
class="indexingPolicyEditor ttlIndexingPolicyFocusElement"
|
||||||
|
tabindex="0"
|
||||||
|
data-bind="setTemplateReady: true, attr:{ id: indexingPolicyEditorId }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
436
src/Explorer/Tabs/SettingsTab.test.ts
Normal file
436
src/Explorer/Tabs/SettingsTab.test.ts
Normal file
@@ -0,0 +1,436 @@
|
|||||||
|
import * as Constants from "../../Common/Constants";
|
||||||
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
|
import * as ko from "knockout";
|
||||||
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
|
import Collection from "../Tree/Collection";
|
||||||
|
import Database from "../Tree/Database";
|
||||||
|
import Explorer from "../Explorer";
|
||||||
|
import SettingsTab from "./SettingsTab";
|
||||||
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
|
import { IndexingPolicies } from "../../Shared/Constants";
|
||||||
|
|
||||||
|
describe("Settings tab", () => {
|
||||||
|
const baseCollection: DataModels.Collection = {
|
||||||
|
defaultTtl: 200,
|
||||||
|
partitionKey: null,
|
||||||
|
conflictResolutionPolicy: {
|
||||||
|
mode: DataModels.ConflictResolutionMode.LastWriterWins,
|
||||||
|
conflictResolutionPath: "/_ts"
|
||||||
|
},
|
||||||
|
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
|
||||||
|
_rid: "",
|
||||||
|
_self: "",
|
||||||
|
_etag: "",
|
||||||
|
_ts: 0,
|
||||||
|
id: "mycoll"
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseDatabase: DataModels.Database = {
|
||||||
|
_rid: "",
|
||||||
|
_self: "",
|
||||||
|
_etag: "",
|
||||||
|
_ts: 0,
|
||||||
|
id: "mydb",
|
||||||
|
collections: [baseCollection]
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("Conflict Resolution", () => {
|
||||||
|
describe("should show conflict resolution", () => {
|
||||||
|
let explorer: Explorer;
|
||||||
|
const baseCollectionWithoutConflict: DataModels.Collection = {
|
||||||
|
defaultTtl: 200,
|
||||||
|
partitionKey: null,
|
||||||
|
conflictResolutionPolicy: null,
|
||||||
|
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
|
||||||
|
_rid: "",
|
||||||
|
_self: "",
|
||||||
|
_etag: "",
|
||||||
|
_ts: 0,
|
||||||
|
id: "mycoll"
|
||||||
|
};
|
||||||
|
const getSettingsTab = (conflictResolution: boolean = true) => {
|
||||||
|
return new SettingsTab({
|
||||||
|
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||||
|
title: "Scale & Settings",
|
||||||
|
tabPath: "",
|
||||||
|
hashLocation: "",
|
||||||
|
isActive: ko.observable(false),
|
||||||
|
collection: new Collection(
|
||||||
|
explorer,
|
||||||
|
"mydb",
|
||||||
|
conflictResolution ? baseCollection : baseCollectionWithoutConflict,
|
||||||
|
null
|
||||||
|
),
|
||||||
|
onUpdateTabsButtons: undefined
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
explorer = new Explorer();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("single master, should not show conflict resolution", () => {
|
||||||
|
const settingsTab = getSettingsTab();
|
||||||
|
expect(settingsTab.hasConflictResolution()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("multi master with resolution conflict, show conflict resolution", () => {
|
||||||
|
explorer.databaseAccount({
|
||||||
|
id: "test",
|
||||||
|
kind: "",
|
||||||
|
location: "",
|
||||||
|
name: "",
|
||||||
|
tags: "",
|
||||||
|
type: "",
|
||||||
|
properties: {
|
||||||
|
enableMultipleWriteLocations: true,
|
||||||
|
documentEndpoint: "",
|
||||||
|
cassandraEndpoint: "",
|
||||||
|
gremlinEndpoint: "",
|
||||||
|
tableEndpoint: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const settingsTab = getSettingsTab();
|
||||||
|
expect(settingsTab.hasConflictResolution()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("multi master without resolution conflict, show conflict resolution", () => {
|
||||||
|
explorer.databaseAccount({
|
||||||
|
id: "test",
|
||||||
|
kind: "",
|
||||||
|
location: "",
|
||||||
|
name: "",
|
||||||
|
tags: "",
|
||||||
|
type: "",
|
||||||
|
properties: {
|
||||||
|
enableMultipleWriteLocations: true,
|
||||||
|
documentEndpoint: "",
|
||||||
|
cassandraEndpoint: "",
|
||||||
|
gremlinEndpoint: "",
|
||||||
|
tableEndpoint: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const settingsTab = getSettingsTab(false /* no resolution conflict*/);
|
||||||
|
expect(settingsTab.hasConflictResolution()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Parse Conflict Resolution Mode from backend", () => {
|
||||||
|
it("should parse any casing", () => {
|
||||||
|
expect(SettingsTab.parseConflictResolutionMode("custom")).toBe(DataModels.ConflictResolutionMode.Custom);
|
||||||
|
expect(SettingsTab.parseConflictResolutionMode("Custom")).toBe(DataModels.ConflictResolutionMode.Custom);
|
||||||
|
expect(SettingsTab.parseConflictResolutionMode("lastWriterWins")).toBe(
|
||||||
|
DataModels.ConflictResolutionMode.LastWriterWins
|
||||||
|
);
|
||||||
|
expect(SettingsTab.parseConflictResolutionMode("LastWriterWins")).toBe(
|
||||||
|
DataModels.ConflictResolutionMode.LastWriterWins
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse empty as null", () => {
|
||||||
|
expect(SettingsTab.parseConflictResolutionMode("")).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse null as null", () => {
|
||||||
|
expect(SettingsTab.parseConflictResolutionMode(null)).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Parse Conflict Resolution procedure from backend", () => {
|
||||||
|
it("should parse path as name", () => {
|
||||||
|
expect(SettingsTab.parseConflictResolutionProcedure("/dbs/xxxx/colls/xxxx/sprocs/validsproc")).toBe(
|
||||||
|
"validsproc"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse name as name", () => {
|
||||||
|
expect(SettingsTab.parseConflictResolutionProcedure("validsproc")).toBe("validsproc");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse invalid path as null", () => {
|
||||||
|
expect(SettingsTab.parseConflictResolutionProcedure("/not/a/valid/path")).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse empty or null as null", () => {
|
||||||
|
expect(SettingsTab.parseConflictResolutionProcedure("")).toBe(null);
|
||||||
|
expect(SettingsTab.parseConflictResolutionProcedure(null)).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Should update collection", () => {
|
||||||
|
let explorer: Explorer;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
explorer = new Explorer();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("On TTL changed", () => {
|
||||||
|
const settingsTab = new SettingsTab({
|
||||||
|
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||||
|
title: "Scale & Settings",
|
||||||
|
tabPath: "",
|
||||||
|
hashLocation: "",
|
||||||
|
isActive: ko.observable(false),
|
||||||
|
collection: new Collection(explorer, "mydb", baseCollection, null),
|
||||||
|
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(settingsTab.shouldUpdateCollection()).toBe(false);
|
||||||
|
settingsTab.timeToLive("off");
|
||||||
|
expect(settingsTab.shouldUpdateCollection()).toBe(true);
|
||||||
|
|
||||||
|
settingsTab.onRevertClick();
|
||||||
|
expect(settingsTab.shouldUpdateCollection()).toBe(false);
|
||||||
|
settingsTab.timeToLiveSeconds(100);
|
||||||
|
expect(settingsTab.shouldUpdateCollection()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("On Index Policy changed", () => {
|
||||||
|
const settingsTab = new SettingsTab({
|
||||||
|
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||||
|
title: "Scale & Settings",
|
||||||
|
tabPath: "",
|
||||||
|
hashLocation: "",
|
||||||
|
isActive: ko.observable(false),
|
||||||
|
collection: new Collection(explorer, "mydb", baseCollection, null),
|
||||||
|
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(settingsTab.shouldUpdateCollection()).toBe(false);
|
||||||
|
settingsTab.indexingPolicyContent({ somethingDifferent: "" });
|
||||||
|
expect(settingsTab.shouldUpdateCollection()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("On Conflict Resolution Mode changed", () => {
|
||||||
|
const settingsTab = new SettingsTab({
|
||||||
|
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||||
|
title: "Scale & Settings",
|
||||||
|
tabPath: "",
|
||||||
|
hashLocation: "",
|
||||||
|
isActive: ko.observable(false),
|
||||||
|
collection: new Collection(explorer, "mydb", baseCollection, null),
|
||||||
|
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(settingsTab.shouldUpdateCollection()).toBe(false);
|
||||||
|
settingsTab.conflictResolutionPolicyMode(DataModels.ConflictResolutionMode.Custom);
|
||||||
|
expect(settingsTab.shouldUpdateCollection()).toBe(true);
|
||||||
|
|
||||||
|
settingsTab.onRevertClick();
|
||||||
|
expect(settingsTab.shouldUpdateCollection()).toBe(false);
|
||||||
|
settingsTab.conflictResolutionPolicyPath("/somethingElse");
|
||||||
|
expect(settingsTab.shouldUpdateCollection()).toBe(true);
|
||||||
|
|
||||||
|
settingsTab.onRevertClick();
|
||||||
|
expect(settingsTab.shouldUpdateCollection()).toBe(false);
|
||||||
|
settingsTab.conflictResolutionPolicyMode(DataModels.ConflictResolutionMode.Custom);
|
||||||
|
settingsTab.conflictResolutionPolicyProcedure("resolver");
|
||||||
|
expect(settingsTab.shouldUpdateCollection()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Get Conflict Resolution configuration from user", () => {
|
||||||
|
let explorer: Explorer;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
explorer = new Explorer();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("null if it didnt change", () => {
|
||||||
|
const settingsTab = new SettingsTab({
|
||||||
|
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||||
|
title: "Scale & Settings",
|
||||||
|
tabPath: "",
|
||||||
|
hashLocation: "",
|
||||||
|
isActive: ko.observable(false),
|
||||||
|
collection: new Collection(explorer, "mydb", baseCollection, null),
|
||||||
|
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(settingsTab.getUpdatedConflictResolutionPolicy()).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Custom contains valid backend path", () => {
|
||||||
|
const settingsTab = new SettingsTab({
|
||||||
|
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||||
|
title: "Scale & Settings",
|
||||||
|
tabPath: "",
|
||||||
|
hashLocation: "",
|
||||||
|
isActive: ko.observable(false),
|
||||||
|
collection: new Collection(explorer, "mydb", baseCollection, null),
|
||||||
|
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(settingsTab.getUpdatedConflictResolutionPolicy()).toBe(null);
|
||||||
|
settingsTab.conflictResolutionPolicyMode(DataModels.ConflictResolutionMode.Custom);
|
||||||
|
settingsTab.conflictResolutionPolicyProcedure("resolver");
|
||||||
|
let updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
|
||||||
|
expect(updatedPolicy.mode).toBe(DataModels.ConflictResolutionMode.Custom);
|
||||||
|
expect(updatedPolicy.conflictResolutionProcedure).toBe("/dbs/mydb/colls/mycoll/sprocs/resolver");
|
||||||
|
|
||||||
|
settingsTab.conflictResolutionPolicyProcedure("");
|
||||||
|
updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
|
||||||
|
expect(updatedPolicy.conflictResolutionProcedure).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("LWW contains valid property path", () => {
|
||||||
|
const settingsTab = new SettingsTab({
|
||||||
|
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||||
|
title: "Scale & Settings",
|
||||||
|
tabPath: "",
|
||||||
|
hashLocation: "",
|
||||||
|
isActive: ko.observable(false),
|
||||||
|
collection: new Collection(explorer, "mydb", baseCollection, null),
|
||||||
|
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(settingsTab.getUpdatedConflictResolutionPolicy()).toBe(null);
|
||||||
|
settingsTab.conflictResolutionPolicyPath("someAttr");
|
||||||
|
let updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
|
||||||
|
expect(updatedPolicy.conflictResolutionPath).toBe("/someAttr");
|
||||||
|
|
||||||
|
settingsTab.conflictResolutionPolicyPath("/someAttr");
|
||||||
|
updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
|
||||||
|
expect(updatedPolicy.conflictResolutionPath).toBe("/someAttr");
|
||||||
|
|
||||||
|
settingsTab.conflictResolutionPolicyPath("");
|
||||||
|
updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
|
||||||
|
expect(updatedPolicy.conflictResolutionPath).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("partitionKeyVisible", () => {
|
||||||
|
enum PartitionKeyOption {
|
||||||
|
None,
|
||||||
|
System,
|
||||||
|
NonSystem
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCollection(defaultApi: string, partitionKeyOption: PartitionKeyOption) {
|
||||||
|
const explorer = new Explorer();
|
||||||
|
explorer.defaultExperience(defaultApi);
|
||||||
|
|
||||||
|
const offer: DataModels.Offer = null;
|
||||||
|
const defaultTtl = 200;
|
||||||
|
const conflictResolutionPolicy = {
|
||||||
|
mode: DataModels.ConflictResolutionMode.LastWriterWins,
|
||||||
|
conflictResolutionPath: "/_ts"
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Collection(
|
||||||
|
explorer,
|
||||||
|
"mydb",
|
||||||
|
{
|
||||||
|
defaultTtl: defaultTtl,
|
||||||
|
partitionKey:
|
||||||
|
partitionKeyOption != PartitionKeyOption.None
|
||||||
|
? {
|
||||||
|
paths: ["/foo"],
|
||||||
|
kind: "Hash",
|
||||||
|
version: 2,
|
||||||
|
systemKey: partitionKeyOption === PartitionKeyOption.System
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
conflictResolutionPolicy: conflictResolutionPolicy,
|
||||||
|
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
|
||||||
|
_rid: "",
|
||||||
|
_self: "",
|
||||||
|
_etag: "",
|
||||||
|
_ts: 0,
|
||||||
|
id: "mycoll"
|
||||||
|
},
|
||||||
|
offer
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSettingsTab(defaultApi: string, partitionKeyOption: PartitionKeyOption): SettingsTab {
|
||||||
|
return new SettingsTab({
|
||||||
|
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||||
|
title: "Scale & Settings",
|
||||||
|
tabPath: "",
|
||||||
|
hashLocation: "",
|
||||||
|
isActive: ko.observable(false),
|
||||||
|
collection: getCollection(defaultApi, partitionKeyOption),
|
||||||
|
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it("on SQL container with no partition key should be false", () => {
|
||||||
|
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.DocumentDB, PartitionKeyOption.None);
|
||||||
|
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("on Mongo container with no partition key should be false", () => {
|
||||||
|
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.MongoDB, PartitionKeyOption.None);
|
||||||
|
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("on Gremlin container with no partition key should be false", () => {
|
||||||
|
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Graph, PartitionKeyOption.None);
|
||||||
|
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("on Cassandra container with no partition key should be false", () => {
|
||||||
|
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Cassandra, PartitionKeyOption.None);
|
||||||
|
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("on Table container with no partition key should be false", () => {
|
||||||
|
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Table, PartitionKeyOption.None);
|
||||||
|
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("on SQL container with system partition key should be true", () => {
|
||||||
|
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.DocumentDB, PartitionKeyOption.System);
|
||||||
|
expect(settingsTab.partitionKeyVisible()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("on Mongo container with system partition key should be false", () => {
|
||||||
|
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.MongoDB, PartitionKeyOption.System);
|
||||||
|
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("on Gremlin container with system partition key should be true", () => {
|
||||||
|
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Graph, PartitionKeyOption.System);
|
||||||
|
expect(settingsTab.partitionKeyVisible()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("on Cassandra container with system partition key should be false", () => {
|
||||||
|
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Cassandra, PartitionKeyOption.System);
|
||||||
|
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("on Table container with system partition key should be false", () => {
|
||||||
|
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Table, PartitionKeyOption.System);
|
||||||
|
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("on SQL container with non-system partition key should be true", () => {
|
||||||
|
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.DocumentDB, PartitionKeyOption.NonSystem);
|
||||||
|
expect(settingsTab.partitionKeyVisible()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("on Mongo container with non-system partition key should be true", () => {
|
||||||
|
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.MongoDB, PartitionKeyOption.NonSystem);
|
||||||
|
expect(settingsTab.partitionKeyVisible()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("on Gremlin container with non-system partition key should be true", () => {
|
||||||
|
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Graph, PartitionKeyOption.NonSystem);
|
||||||
|
expect(settingsTab.partitionKeyVisible()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("on Cassandra container with non-system partition key should be false", () => {
|
||||||
|
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Cassandra, PartitionKeyOption.NonSystem);
|
||||||
|
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("on Table container with non-system partition key should be false", () => {
|
||||||
|
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Table, PartitionKeyOption.NonSystem);
|
||||||
|
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
1502
src/Explorer/Tabs/SettingsTab.ts
Normal file
1502
src/Explorer/Tabs/SettingsTab.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -47,7 +47,7 @@ export default class SettingsTabV2 extends TabsBase {
|
|||||||
this.currentCollection.loadOffer().then(
|
this.currentCollection.loadOffer().then(
|
||||||
() => {
|
() => {
|
||||||
// passed in options and set by parent as "Settings" by default
|
// passed in options and set by parent as "Settings" by default
|
||||||
this.tabTitle(this.currentCollection.offer() ? "Settings" : "Scale & Settings");
|
this.tabTitle("Scale & Settings");
|
||||||
this.offerRead(true);
|
this.offerRead(true);
|
||||||
this.options.getPendingNotification.then(
|
this.options.getPendingNotification.then(
|
||||||
(data: DataModels.Notification) => {
|
(data: DataModels.Notification) => {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import MongoQueryTabTemplate from "./MongoQueryTab.html";
|
|||||||
import MongoShellTabTemplate from "./MongoShellTab.html";
|
import MongoShellTabTemplate from "./MongoShellTab.html";
|
||||||
import QueryTabTemplate from "./QueryTab.html";
|
import QueryTabTemplate from "./QueryTab.html";
|
||||||
import QueryTablesTabTemplate from "./QueryTablesTab.html";
|
import QueryTablesTabTemplate from "./QueryTablesTab.html";
|
||||||
|
import SettingsTabTemplate from "./SettingsTab.html";
|
||||||
import SettingsTabV2Template from "./SettingsTabV2.html";
|
import SettingsTabV2Template from "./SettingsTabV2.html";
|
||||||
import DatabaseSettingsTabTemplate from "./DatabaseSettingsTab.html";
|
import DatabaseSettingsTabTemplate from "./DatabaseSettingsTab.html";
|
||||||
import StoredProcedureTabTemplate from "./StoredProcedureTab.html";
|
import StoredProcedureTabTemplate from "./StoredProcedureTab.html";
|
||||||
@@ -132,6 +133,15 @@ export class QueryTablesTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SettingsTab {
|
||||||
|
constructor() {
|
||||||
|
return {
|
||||||
|
viewModel: TabComponent,
|
||||||
|
template: SettingsTabTemplate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class SettingsTabV2 {
|
export class SettingsTabV2 {
|
||||||
constructor() {
|
constructor() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -10,10 +10,9 @@ describe("Collection", () => {
|
|||||||
container: Explorer,
|
container: Explorer,
|
||||||
databaseId: string,
|
databaseId: string,
|
||||||
data: DataModels.Collection,
|
data: DataModels.Collection,
|
||||||
quotaInfo: DataModels.CollectionQuotaInfo,
|
|
||||||
offer: DataModels.Offer
|
offer: DataModels.Offer
|
||||||
): Collection {
|
): Collection {
|
||||||
return new Collection(container, databaseId, data, quotaInfo, offer);
|
return new Collection(container, databaseId, data, offer);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateMockCollectionsDataModelWithPartitionKey(
|
function generateMockCollectionsDataModelWithPartitionKey(
|
||||||
@@ -50,7 +49,7 @@ describe("Collection", () => {
|
|||||||
});
|
});
|
||||||
mockContainer.deleteCollectionText = ko.observable<string>("delete collection");
|
mockContainer.deleteCollectionText = ko.observable<string>("delete collection");
|
||||||
|
|
||||||
return generateCollection(mockContainer, "abc", data, {} as DataModels.CollectionQuotaInfo, {} as DataModels.Offer);
|
return generateCollection(mockContainer, "abc", data, {} as DataModels.Offer);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Partition key path parsing", () => {
|
describe("Partition key path parsing", () => {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { readTriggers } from "../../Common/dataAccess/readTriggers";
|
|||||||
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
|
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
|
||||||
import { createDocument } from "../../Common/DocumentClientUtilityBase";
|
import { createDocument } from "../../Common/DocumentClientUtilityBase";
|
||||||
import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer";
|
import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer";
|
||||||
import { readCollectionQuotaInfo } from "../../Common/dataAccess/readCollectionQuotaInfo";
|
|
||||||
import * as Logger from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
@@ -29,6 +28,7 @@ import MongoShellTab from "../Tabs/MongoShellTab";
|
|||||||
import QueryTab from "../Tabs/QueryTab";
|
import QueryTab from "../Tabs/QueryTab";
|
||||||
import QueryTablesTab from "../Tabs/QueryTablesTab";
|
import QueryTablesTab from "../Tabs/QueryTablesTab";
|
||||||
import SettingsTabV2 from "../Tabs/SettingsTabV2";
|
import SettingsTabV2 from "../Tabs/SettingsTabV2";
|
||||||
|
import SettingsTab from "../Tabs/SettingsTab";
|
||||||
import ConflictId from "./ConflictId";
|
import ConflictId from "./ConflictId";
|
||||||
import DocumentId from "./DocumentId";
|
import DocumentId from "./DocumentId";
|
||||||
import StoredProcedure from "./StoredProcedure";
|
import StoredProcedure from "./StoredProcedure";
|
||||||
@@ -54,7 +54,6 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
public defaultTtl: ko.Observable<number>;
|
public defaultTtl: ko.Observable<number>;
|
||||||
public indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
public indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
||||||
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
||||||
public quotaInfo: ko.Observable<DataModels.CollectionQuotaInfo>;
|
|
||||||
public offer: ko.Observable<DataModels.Offer>;
|
public offer: ko.Observable<DataModels.Offer>;
|
||||||
public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
||||||
public changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
public changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
||||||
@@ -62,8 +61,6 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
public throughput: ko.Computed<number>;
|
public throughput: ko.Computed<number>;
|
||||||
public rawDataModel: DataModels.Collection;
|
public rawDataModel: DataModels.Collection;
|
||||||
public analyticalStorageTtl: ko.Observable<number>;
|
public analyticalStorageTtl: ko.Observable<number>;
|
||||||
public schema: DataModels.ISchema;
|
|
||||||
public requestSchema: () => void;
|
|
||||||
public geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
|
public geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
|
||||||
|
|
||||||
// TODO move this to API customization class
|
// TODO move this to API customization class
|
||||||
@@ -95,13 +92,7 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
public userDefinedFunctionsFocused: ko.Observable<boolean>;
|
public userDefinedFunctionsFocused: ko.Observable<boolean>;
|
||||||
public triggersFocused: ko.Observable<boolean>;
|
public triggersFocused: ko.Observable<boolean>;
|
||||||
|
|
||||||
constructor(
|
constructor(container: Explorer, databaseId: string, data: DataModels.Collection, offer: DataModels.Offer) {
|
||||||
container: Explorer,
|
|
||||||
databaseId: string,
|
|
||||||
data: DataModels.Collection,
|
|
||||||
quotaInfo: DataModels.CollectionQuotaInfo,
|
|
||||||
offer: DataModels.Offer
|
|
||||||
) {
|
|
||||||
this.nodeKind = "Collection";
|
this.nodeKind = "Collection";
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.self = data._self;
|
this.self = data._self;
|
||||||
@@ -113,13 +104,10 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
this.id = ko.observable(data.id);
|
this.id = ko.observable(data.id);
|
||||||
this.defaultTtl = ko.observable(data.defaultTtl);
|
this.defaultTtl = ko.observable(data.defaultTtl);
|
||||||
this.indexingPolicy = ko.observable(data.indexingPolicy);
|
this.indexingPolicy = ko.observable(data.indexingPolicy);
|
||||||
this.quotaInfo = ko.observable(quotaInfo);
|
|
||||||
this.offer = ko.observable(offer);
|
this.offer = ko.observable(offer);
|
||||||
this.conflictResolutionPolicy = ko.observable(data.conflictResolutionPolicy);
|
this.conflictResolutionPolicy = ko.observable(data.conflictResolutionPolicy);
|
||||||
this.changeFeedPolicy = ko.observable<DataModels.ChangeFeedPolicy>(data.changeFeedPolicy);
|
this.changeFeedPolicy = ko.observable<DataModels.ChangeFeedPolicy>(data.changeFeedPolicy);
|
||||||
this.analyticalStorageTtl = ko.observable(data.analyticalStorageTtl);
|
this.analyticalStorageTtl = ko.observable(data.analyticalStorageTtl);
|
||||||
this.schema = data.schema;
|
|
||||||
this.requestSchema = data.requestSchema;
|
|
||||||
this.geospatialConfig = ko.observable(data.geospatialConfig);
|
this.geospatialConfig = ko.observable(data.geospatialConfig);
|
||||||
|
|
||||||
// TODO fix this to only replace non-excaped single quotes
|
// TODO fix this to only replace non-excaped single quotes
|
||||||
@@ -555,6 +543,11 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
dataExplorerArea: Constants.Areas.ResourceTree
|
dataExplorerArea: Constants.Areas.ResourceTree
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isSettingsV2Enabled = this.container.isSettingsV2Enabled();
|
||||||
|
if (!isSettingsV2Enabled) {
|
||||||
|
await this.loadOffer();
|
||||||
|
}
|
||||||
|
|
||||||
const tabTitle = !this.offer() ? "Settings" : "Scale & Settings";
|
const tabTitle = !this.offer() ? "Settings" : "Scale & Settings";
|
||||||
const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
|
const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
|
||||||
const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Settings, tab => {
|
const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Settings, tab => {
|
||||||
@@ -581,8 +574,68 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
onUpdateTabsButtons: this.container.onUpdateTabsButtons
|
onUpdateTabsButtons: this.container.onUpdateTabsButtons
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isSettingsV2Enabled) {
|
||||||
let settingsTabV2 = matchingTabs && (matchingTabs[0] as SettingsTabV2);
|
let settingsTabV2 = matchingTabs && (matchingTabs[0] as SettingsTabV2);
|
||||||
this.launchSettingsTabV2(settingsTabV2, traceStartData, settingsTabOptions, pendingNotificationsPromise);
|
this.launchSettingsTabV2(settingsTabV2, traceStartData, settingsTabOptions, pendingNotificationsPromise);
|
||||||
|
} else {
|
||||||
|
let settingsTab = matchingTabs && (matchingTabs[0] as SettingsTab);
|
||||||
|
this.launchSettingsTabV1(settingsTab, traceStartData, settingsTabOptions, pendingNotificationsPromise);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private launchSettingsTabV1 = (
|
||||||
|
settingsTab: SettingsTab,
|
||||||
|
traceStartData: any,
|
||||||
|
settingsTabOptions: ViewModels.TabOptions,
|
||||||
|
getPendingNotification: Q.Promise<DataModels.Notification>
|
||||||
|
): void => {
|
||||||
|
if (!settingsTab) {
|
||||||
|
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, traceStartData);
|
||||||
|
settingsTabOptions.onLoadStartKey = startKey;
|
||||||
|
|
||||||
|
getPendingNotification.then(
|
||||||
|
(data: any) => {
|
||||||
|
const pendingNotification: DataModels.Notification = data && data[0];
|
||||||
|
settingsTabOptions.tabKind = ViewModels.CollectionTabKind.Settings;
|
||||||
|
settingsTab = new SettingsTab(settingsTabOptions);
|
||||||
|
this.container.tabsManager.activateNewTab(settingsTab);
|
||||||
|
settingsTab.pendingNotification(pendingNotification);
|
||||||
|
},
|
||||||
|
(error: any) => {
|
||||||
|
const errorMessage = getErrorMessage(error);
|
||||||
|
TelemetryProcessor.traceFailure(
|
||||||
|
Action.Tab,
|
||||||
|
{
|
||||||
|
databaseAccountName: this.container.databaseAccount().name,
|
||||||
|
databaseName: this.databaseId,
|
||||||
|
collectionName: this.id(),
|
||||||
|
defaultExperience: this.container.defaultExperience(),
|
||||||
|
dataExplorerArea: Constants.Areas.Tab,
|
||||||
|
tabTitle: settingsTabOptions.title,
|
||||||
|
error: errorMessage,
|
||||||
|
errorStack: getErrorStack(error)
|
||||||
|
},
|
||||||
|
startKey
|
||||||
|
);
|
||||||
|
NotificationConsoleUtils.logConsoleMessage(
|
||||||
|
ConsoleDataType.Error,
|
||||||
|
`Error while fetching container settings for container ${this.id()}: ${errorMessage}`
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
getPendingNotification.then(
|
||||||
|
(pendingNotification: DataModels.Notification) => {
|
||||||
|
settingsTab.pendingNotification(pendingNotification);
|
||||||
|
this.container.tabsManager.activateTab(settingsTab);
|
||||||
|
},
|
||||||
|
(error: any) => {
|
||||||
|
settingsTab.pendingNotification(undefined);
|
||||||
|
this.container.tabsManager.activateTab(settingsTab);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private launchSettingsTabV2 = (
|
private launchSettingsTabV2 = (
|
||||||
@@ -607,14 +660,6 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private async loadCollectionQuotaInfo(): Promise<void> {
|
|
||||||
// TODO: Use the collection entity cache to get quota info
|
|
||||||
const quotaInfoWithUniqueKeyPolicy = await readCollectionQuotaInfo(this);
|
|
||||||
this.uniqueKeyPolicy = quotaInfoWithUniqueKeyPolicy.uniqueKeyPolicy;
|
|
||||||
const quotaInfo = _.omit(quotaInfoWithUniqueKeyPolicy, "uniqueKeyPolicy");
|
|
||||||
this.quotaInfo(quotaInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) {
|
public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) {
|
||||||
const collection: ViewModels.Collection = source.collection || source;
|
const collection: ViewModels.Collection = source.collection || source;
|
||||||
const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Query).length + 1;
|
const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Query).length + 1;
|
||||||
@@ -1287,7 +1332,6 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this.offer(await readCollectionOffer(params));
|
this.offer(await readCollectionOffer(params));
|
||||||
await this.loadCollectionQuotaInfo();
|
|
||||||
|
|
||||||
TelemetryProcessor.traceSuccess(
|
TelemetryProcessor.traceSuccess(
|
||||||
Action.LoadOffers,
|
Action.LoadOffers,
|
||||||
@@ -1295,7 +1339,8 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
databaseAccountName: this.container.databaseAccount().name,
|
databaseAccountName: this.container.databaseAccount().name,
|
||||||
databaseName: this.databaseId,
|
databaseName: this.databaseId,
|
||||||
collectionName: this.id(),
|
collectionName: this.id(),
|
||||||
defaultExperience: this.container.defaultExperience()
|
defaultExperience: this.container.defaultExperience(),
|
||||||
|
offerVersion: this.offer()?.offerVersion
|
||||||
},
|
},
|
||||||
startKey
|
startKey
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
import * as DataModels from "../../Contracts/DataModels";
|
|
||||||
import * as ko from "knockout";
|
|
||||||
import Database from "./Database";
|
|
||||||
import Explorer from "../Explorer";
|
|
||||||
import { HttpStatusCodes } from "../../Common/Constants";
|
|
||||||
import { JunoClient } from "../../Juno/JunoClient";
|
|
||||||
import { userContext, updateUserContext } from "../../UserContext";
|
|
||||||
|
|
||||||
const createMockContainer = (): Explorer => {
|
|
||||||
const mockContainer = new Explorer();
|
|
||||||
return mockContainer;
|
|
||||||
};
|
|
||||||
|
|
||||||
updateUserContext({
|
|
||||||
subscriptionId: "fakeSubscriptionId",
|
|
||||||
resourceGroup: "fakeResourceGroup",
|
|
||||||
databaseAccount: {
|
|
||||||
id: "id",
|
|
||||||
name: "fakeName",
|
|
||||||
location: "fakeLocation",
|
|
||||||
type: "fakeType",
|
|
||||||
tags: undefined,
|
|
||||||
kind: "fakeKind",
|
|
||||||
properties: {
|
|
||||||
documentEndpoint: "fakeEndpoint",
|
|
||||||
tableEndpoint: "fakeEndpoint",
|
|
||||||
gremlinEndpoint: "fakeEndpoint",
|
|
||||||
cassandraEndpoint: "fakeEndpoint"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Add Schema", () => {
|
|
||||||
it("should not call requestSchema or getSchema if analyticalStorageTtl is undefined", () => {
|
|
||||||
const collection: DataModels.Collection = {} as DataModels.Collection;
|
|
||||||
collection.analyticalStorageTtl = undefined;
|
|
||||||
const database = new Database(createMockContainer(), { id: "fakeId" });
|
|
||||||
database.container = createMockContainer();
|
|
||||||
database.container.isSchemaEnabled = ko.computed<boolean>(() => false);
|
|
||||||
|
|
||||||
database.junoClient = new JunoClient();
|
|
||||||
database.junoClient.requestSchema = jest.fn();
|
|
||||||
database.junoClient.getSchema = jest.fn();
|
|
||||||
|
|
||||||
database.addSchema(collection);
|
|
||||||
|
|
||||||
expect(database.junoClient.requestSchema).toBeCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call requestSchema or getSchema if analyticalStorageTtl is not undefined", () => {
|
|
||||||
const collection: DataModels.Collection = { id: "fakeId" } as DataModels.Collection;
|
|
||||||
collection.analyticalStorageTtl = 0;
|
|
||||||
|
|
||||||
const database = new Database(createMockContainer(), {});
|
|
||||||
database.container = createMockContainer();
|
|
||||||
database.container.isSchemaEnabled = ko.computed<boolean>(() => true);
|
|
||||||
|
|
||||||
database.junoClient = new JunoClient();
|
|
||||||
database.junoClient.requestSchema = jest.fn();
|
|
||||||
database.junoClient.getSchema = jest.fn().mockResolvedValue({ status: HttpStatusCodes.OK, data: {} });
|
|
||||||
|
|
||||||
jest.useFakeTimers();
|
|
||||||
const interval = 5000;
|
|
||||||
const checkForSchema: NodeJS.Timeout = database.addSchema(collection, interval);
|
|
||||||
jest.advanceTimersByTime(interval + 1000);
|
|
||||||
|
|
||||||
expect(database.junoClient.requestSchema).toBeCalledWith({
|
|
||||||
id: undefined,
|
|
||||||
subscriptionId: userContext.subscriptionId,
|
|
||||||
resourceGroup: userContext.resourceGroup,
|
|
||||||
accountName: userContext.databaseAccount.name,
|
|
||||||
resource: `dbs/${database.id}/colls/${collection.id}`,
|
|
||||||
status: "new"
|
|
||||||
});
|
|
||||||
expect(checkForSchema).not.toBeNull();
|
|
||||||
expect(database.junoClient.getSchema).toBeCalledWith(
|
|
||||||
userContext.databaseAccount.name,
|
|
||||||
database.id(),
|
|
||||||
collection.id
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -13,8 +13,6 @@ import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsol
|
|||||||
import * as Logger from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { readCollections } from "../../Common/dataAccess/readCollections";
|
import { readCollections } from "../../Common/dataAccess/readCollections";
|
||||||
import { JunoClient, IJunoResponse } from "../../Juno/JunoClient";
|
|
||||||
import { userContext } from "../../UserContext";
|
|
||||||
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
|
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
|
||||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
|
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
|
||||||
@@ -31,7 +29,6 @@ export default class Database implements ViewModels.Database {
|
|||||||
public isDatabaseExpanded: ko.Observable<boolean>;
|
public isDatabaseExpanded: ko.Observable<boolean>;
|
||||||
public isDatabaseShared: ko.Computed<boolean>;
|
public isDatabaseShared: ko.Computed<boolean>;
|
||||||
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
|
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
|
||||||
public junoClient: JunoClient;
|
|
||||||
|
|
||||||
constructor(container: Explorer, data: any) {
|
constructor(container: Explorer, data: any) {
|
||||||
this.nodeKind = "Database";
|
this.nodeKind = "Database";
|
||||||
@@ -46,7 +43,6 @@ export default class Database implements ViewModels.Database {
|
|||||||
this.isDatabaseShared = ko.pureComputed(() => {
|
this.isDatabaseShared = ko.pureComputed(() => {
|
||||||
return this.offer && !!this.offer();
|
return this.offer && !!this.offer();
|
||||||
});
|
});
|
||||||
this.junoClient = new JunoClient();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSettingsClick = () => {
|
public onSettingsClick = () => {
|
||||||
@@ -188,12 +184,8 @@ export default class Database implements ViewModels.Database {
|
|||||||
const collections: DataModels.Collection[] = await readCollections(this.id());
|
const collections: DataModels.Collection[] = await readCollections(this.id());
|
||||||
const deltaCollections = this.getDeltaCollections(collections);
|
const deltaCollections = this.getDeltaCollections(collections);
|
||||||
|
|
||||||
collections.forEach((collection: DataModels.Collection) => {
|
|
||||||
this.addSchema(collection);
|
|
||||||
});
|
|
||||||
|
|
||||||
deltaCollections.toAdd.forEach((collection: DataModels.Collection) => {
|
deltaCollections.toAdd.forEach((collection: DataModels.Collection) => {
|
||||||
const collectionVM: Collection = new Collection(this.container, this.id(), collection, null, null);
|
const collectionVM: Collection = new Collection(this.container, this.id(), collection, null);
|
||||||
collectionVMs.push(collectionVM);
|
collectionVMs.push(collectionVM);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -316,42 +308,4 @@ export default class Database implements ViewModels.Database {
|
|||||||
|
|
||||||
this.collections(collectionsToKeep);
|
this.collections(collectionsToKeep);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addSchema(collection: DataModels.Collection, interval?: number): NodeJS.Timeout {
|
|
||||||
let checkForSchema: NodeJS.Timeout = null;
|
|
||||||
interval = interval || 5000;
|
|
||||||
|
|
||||||
if (collection.analyticalStorageTtl !== undefined && this.container.isSchemaEnabled()) {
|
|
||||||
collection.requestSchema = () => {
|
|
||||||
this.junoClient.requestSchema({
|
|
||||||
id: undefined,
|
|
||||||
subscriptionId: userContext.subscriptionId,
|
|
||||||
resourceGroup: userContext.resourceGroup,
|
|
||||||
accountName: userContext.databaseAccount.name,
|
|
||||||
resource: `dbs/${this.id}/colls/${collection.id}`,
|
|
||||||
status: "new"
|
|
||||||
});
|
|
||||||
checkForSchema = setInterval(async () => {
|
|
||||||
const response: IJunoResponse<DataModels.ISchema> = await this.junoClient.getSchema(
|
|
||||||
userContext.databaseAccount.name,
|
|
||||||
this.id(),
|
|
||||||
collection.id
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.status >= 404) {
|
|
||||||
clearInterval(checkForSchema);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.data !== null) {
|
|
||||||
clearInterval(checkForSchema);
|
|
||||||
collection.schema = response.data;
|
|
||||||
}
|
|
||||||
}, interval);
|
|
||||||
};
|
|
||||||
|
|
||||||
collection.requestSchema();
|
|
||||||
}
|
|
||||||
|
|
||||||
return checkForSchema;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,253 +0,0 @@
|
|||||||
import * as ko from "knockout";
|
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
|
||||||
import React from "react";
|
|
||||||
import { ResourceTreeAdapter } from "./ResourceTreeAdapter";
|
|
||||||
import { shallow } from "enzyme";
|
|
||||||
import { TreeComponent, TreeNode, TreeComponentProps } from "../Controls/TreeComponent/TreeComponent";
|
|
||||||
import Explorer from "../Explorer";
|
|
||||||
import Collection from "./Collection";
|
|
||||||
|
|
||||||
const schema: DataModels.ISchema = {
|
|
||||||
id: "fakeSchemaId",
|
|
||||||
accountName: "fakeAccountName",
|
|
||||||
resource: "dbs/FakeDbName/colls/FakeCollectionName",
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
dataType: {
|
|
||||||
code: 15,
|
|
||||||
name: "String"
|
|
||||||
},
|
|
||||||
hasNulls: true,
|
|
||||||
isArray: false,
|
|
||||||
schemaType: {
|
|
||||||
code: 0,
|
|
||||||
name: "Data"
|
|
||||||
},
|
|
||||||
name: "_rid",
|
|
||||||
path: "_rid",
|
|
||||||
maxRepetitionLevel: 0,
|
|
||||||
maxDefinitionLevel: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dataType: {
|
|
||||||
code: 11,
|
|
||||||
name: "Int64"
|
|
||||||
},
|
|
||||||
hasNulls: true,
|
|
||||||
isArray: false,
|
|
||||||
schemaType: {
|
|
||||||
code: 0,
|
|
||||||
name: "Data"
|
|
||||||
},
|
|
||||||
name: "_ts",
|
|
||||||
path: "_ts",
|
|
||||||
maxRepetitionLevel: 0,
|
|
||||||
maxDefinitionLevel: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dataType: {
|
|
||||||
code: 15,
|
|
||||||
name: "String"
|
|
||||||
},
|
|
||||||
hasNulls: true,
|
|
||||||
isArray: false,
|
|
||||||
schemaType: {
|
|
||||||
code: 0,
|
|
||||||
name: "Data"
|
|
||||||
},
|
|
||||||
name: "id",
|
|
||||||
path: "id",
|
|
||||||
maxRepetitionLevel: 0,
|
|
||||||
maxDefinitionLevel: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dataType: {
|
|
||||||
code: 15,
|
|
||||||
name: "String"
|
|
||||||
},
|
|
||||||
hasNulls: true,
|
|
||||||
isArray: false,
|
|
||||||
schemaType: {
|
|
||||||
code: 0,
|
|
||||||
name: "Data"
|
|
||||||
},
|
|
||||||
name: "pk",
|
|
||||||
path: "pk",
|
|
||||||
maxRepetitionLevel: 0,
|
|
||||||
maxDefinitionLevel: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dataType: {
|
|
||||||
code: 15,
|
|
||||||
name: "String"
|
|
||||||
},
|
|
||||||
hasNulls: true,
|
|
||||||
isArray: false,
|
|
||||||
schemaType: {
|
|
||||||
code: 0,
|
|
||||||
name: "Data"
|
|
||||||
},
|
|
||||||
name: "other",
|
|
||||||
path: "other",
|
|
||||||
maxRepetitionLevel: 0,
|
|
||||||
maxDefinitionLevel: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dataType: {
|
|
||||||
code: 15,
|
|
||||||
name: "String"
|
|
||||||
},
|
|
||||||
hasNulls: true,
|
|
||||||
isArray: false,
|
|
||||||
schemaType: {
|
|
||||||
code: 0,
|
|
||||||
name: "Data"
|
|
||||||
},
|
|
||||||
name: "name",
|
|
||||||
path: "nested.name",
|
|
||||||
maxRepetitionLevel: 0,
|
|
||||||
maxDefinitionLevel: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dataType: {
|
|
||||||
code: 11,
|
|
||||||
name: "Int64"
|
|
||||||
},
|
|
||||||
hasNulls: true,
|
|
||||||
isArray: false,
|
|
||||||
schemaType: {
|
|
||||||
code: 0,
|
|
||||||
name: "Data"
|
|
||||||
},
|
|
||||||
name: "someNumber",
|
|
||||||
path: "nested.someNumber",
|
|
||||||
maxRepetitionLevel: 0,
|
|
||||||
maxDefinitionLevel: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dataType: {
|
|
||||||
code: 17,
|
|
||||||
name: "Double"
|
|
||||||
},
|
|
||||||
hasNulls: true,
|
|
||||||
isArray: false,
|
|
||||||
schemaType: {
|
|
||||||
code: 0,
|
|
||||||
name: "Data"
|
|
||||||
},
|
|
||||||
name: "anotherNumber",
|
|
||||||
path: "nested.anotherNumber",
|
|
||||||
maxRepetitionLevel: 0,
|
|
||||||
maxDefinitionLevel: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dataType: {
|
|
||||||
code: 15,
|
|
||||||
name: "String"
|
|
||||||
},
|
|
||||||
hasNulls: true,
|
|
||||||
isArray: false,
|
|
||||||
schemaType: {
|
|
||||||
code: 0,
|
|
||||||
name: "Data"
|
|
||||||
},
|
|
||||||
name: "name",
|
|
||||||
path: "items.list.items.name",
|
|
||||||
maxRepetitionLevel: 1,
|
|
||||||
maxDefinitionLevel: 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dataType: {
|
|
||||||
code: 11,
|
|
||||||
name: "Int64"
|
|
||||||
},
|
|
||||||
hasNulls: true,
|
|
||||||
isArray: false,
|
|
||||||
schemaType: {
|
|
||||||
code: 0,
|
|
||||||
name: "Data"
|
|
||||||
},
|
|
||||||
name: "someNumber",
|
|
||||||
path: "items.list.items.someNumber",
|
|
||||||
maxRepetitionLevel: 1,
|
|
||||||
maxDefinitionLevel: 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dataType: {
|
|
||||||
code: 17,
|
|
||||||
name: "Double"
|
|
||||||
},
|
|
||||||
hasNulls: true,
|
|
||||||
isArray: false,
|
|
||||||
schemaType: {
|
|
||||||
code: 0,
|
|
||||||
name: "Data"
|
|
||||||
},
|
|
||||||
name: "anotherNumber",
|
|
||||||
path: "items.list.items.anotherNumber",
|
|
||||||
maxRepetitionLevel: 1,
|
|
||||||
maxDefinitionLevel: 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dataType: {
|
|
||||||
code: 15,
|
|
||||||
name: "String"
|
|
||||||
},
|
|
||||||
hasNulls: true,
|
|
||||||
isArray: false,
|
|
||||||
schemaType: {
|
|
||||||
code: 0,
|
|
||||||
name: "Data"
|
|
||||||
},
|
|
||||||
name: "_etag",
|
|
||||||
path: "_etag",
|
|
||||||
maxRepetitionLevel: 0,
|
|
||||||
maxDefinitionLevel: 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const createMockContainer = (): Explorer => {
|
|
||||||
const mockContainer = new Explorer();
|
|
||||||
mockContainer.selectedNode = ko.observable<ViewModels.TreeNode>();
|
|
||||||
mockContainer.onUpdateTabsButtons = () => {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
return mockContainer;
|
|
||||||
};
|
|
||||||
|
|
||||||
const createMockCollection = (): ViewModels.Collection => {
|
|
||||||
const mockCollection = {} as DataModels.Collection;
|
|
||||||
mockCollection._rid = "fakeRid";
|
|
||||||
mockCollection._self = "fakeSelf";
|
|
||||||
mockCollection.id = "fakeId";
|
|
||||||
mockCollection.analyticalStorageTtl = 0;
|
|
||||||
mockCollection.schema = schema;
|
|
||||||
|
|
||||||
const mockCollectionVM: ViewModels.Collection = new Collection(
|
|
||||||
createMockContainer(),
|
|
||||||
"fakeDatabaseId",
|
|
||||||
mockCollection,
|
|
||||||
undefined,
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
return mockCollectionVM;
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("Resource tree for schema", () => {
|
|
||||||
const mockContainer: Explorer = createMockContainer();
|
|
||||||
const resourceTree = new ResourceTreeAdapter(mockContainer);
|
|
||||||
|
|
||||||
it("should render", () => {
|
|
||||||
const rootNode: TreeNode = resourceTree.buildSchemaNode(createMockCollection());
|
|
||||||
const props: TreeComponentProps = {
|
|
||||||
rootNode,
|
|
||||||
className: "dataResourceTree"
|
|
||||||
};
|
|
||||||
const wrapper = shallow(<TreeComponent {...props} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -2,7 +2,7 @@ import * as ko from "knockout";
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||||
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
|
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
|
||||||
import { TreeComponent, TreeNode, TreeNodeMenuItem, TreeNodeComponent } from "../Controls/TreeComponent/TreeComponent";
|
import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
||||||
import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory";
|
import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory";
|
||||||
@@ -32,7 +32,6 @@ import StoredProcedure from "./StoredProcedure";
|
|||||||
import Trigger from "./Trigger";
|
import Trigger from "./Trigger";
|
||||||
import TabsBase from "../Tabs/TabsBase";
|
import TabsBase from "../Tabs/TabsBase";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
|
||||||
|
|
||||||
export class ResourceTreeAdapter implements ReactAdapter {
|
export class ResourceTreeAdapter implements ReactAdapter {
|
||||||
public static readonly MyNotebooksTitle = "My Notebooks";
|
public static readonly MyNotebooksTitle = "My Notebooks";
|
||||||
@@ -290,11 +289,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const schemaNode: TreeNode = this.buildSchemaNode(collection);
|
|
||||||
if (schemaNode) {
|
|
||||||
children.push(schemaNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ResourceTreeAdapter.showScriptNodes(this.container)) {
|
if (ResourceTreeAdapter.showScriptNodes(this.container)) {
|
||||||
children.push(this.buildStoredProcedureNode(collection));
|
children.push(this.buildStoredProcedureNode(collection));
|
||||||
children.push(this.buildUserDefinedFunctionsNode(collection));
|
children.push(this.buildUserDefinedFunctionsNode(collection));
|
||||||
@@ -411,75 +405,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public buildSchemaNode(collection: ViewModels.Collection): TreeNode {
|
|
||||||
if (collection.analyticalStorageTtl() == undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!collection.schema || !collection.schema.fields) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
label: "Schema",
|
|
||||||
children: this.getSchemaNodes(collection.schema.fields),
|
|
||||||
onClick: () => {
|
|
||||||
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Schema);
|
|
||||||
this.container.tabsManager.refreshActiveTab(
|
|
||||||
(tab: TabsBase) => tab.collection && tab.collection.rid === collection.rid
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private getSchemaNodes(fields: DataModels.IDataField[]): TreeNode[] {
|
|
||||||
const schema: any = {};
|
|
||||||
|
|
||||||
//unflatten
|
|
||||||
fields.forEach((field: DataModels.IDataField, fieldIndex: number) => {
|
|
||||||
const path: string[] = field.path.split(".");
|
|
||||||
const fieldProperties = [field.dataType.name, `HasNulls: ${field.hasNulls}`];
|
|
||||||
let current: any = {};
|
|
||||||
path.forEach((name: string, pathIndex: number) => {
|
|
||||||
if (pathIndex === 0) {
|
|
||||||
if (schema[name] === undefined) {
|
|
||||||
if (pathIndex === path.length - 1) {
|
|
||||||
schema[name] = fieldProperties;
|
|
||||||
} else {
|
|
||||||
schema[name] = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
current = schema[name];
|
|
||||||
} else {
|
|
||||||
if (current[name] === undefined) {
|
|
||||||
if (pathIndex === path.length - 1) {
|
|
||||||
current[name] = fieldProperties;
|
|
||||||
} else {
|
|
||||||
current[name] = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
current = current[name];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const traverse = (obj: any): TreeNode[] => {
|
|
||||||
const children: TreeNode[] = [];
|
|
||||||
|
|
||||||
if (obj !== null && !Array.isArray(obj) && typeof obj === "object") {
|
|
||||||
Object.entries(obj).forEach(([key, value]) => {
|
|
||||||
children.push({ label: key, children: traverse(value) });
|
|
||||||
});
|
|
||||||
} else if (Array.isArray(obj)) {
|
|
||||||
return [{ label: obj[0] }, { label: obj[1] }];
|
|
||||||
}
|
|
||||||
|
|
||||||
return children;
|
|
||||||
};
|
|
||||||
|
|
||||||
return traverse(schema);
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildNotebooksTrees(): TreeNode {
|
private buildNotebooksTrees(): TreeNode {
|
||||||
let notebooksTree: TreeNode = {
|
let notebooksTree: TreeNode = {
|
||||||
label: undefined,
|
label: undefined,
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`Resource tree for schema should render 1`] = `
|
|
||||||
<div
|
|
||||||
className="treeComponent dataResourceTree"
|
|
||||||
>
|
|
||||||
<TreeNodeComponent
|
|
||||||
generation={0}
|
|
||||||
node={
|
|
||||||
Object {
|
|
||||||
"children": Array [
|
|
||||||
Object {
|
|
||||||
"children": Array [
|
|
||||||
Object {
|
|
||||||
"label": "String",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "_rid",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"children": Array [
|
|
||||||
Object {
|
|
||||||
"label": "Int64",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "_ts",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"children": Array [
|
|
||||||
Object {
|
|
||||||
"label": "String",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "id",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"children": Array [
|
|
||||||
Object {
|
|
||||||
"label": "String",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "pk",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"children": Array [
|
|
||||||
Object {
|
|
||||||
"label": "String",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "other",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"children": Array [
|
|
||||||
Object {
|
|
||||||
"children": Array [
|
|
||||||
Object {
|
|
||||||
"label": "String",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "name",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"children": Array [
|
|
||||||
Object {
|
|
||||||
"label": "Int64",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "someNumber",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"children": Array [
|
|
||||||
Object {
|
|
||||||
"label": "Double",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "anotherNumber",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "nested",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"children": Array [
|
|
||||||
Object {
|
|
||||||
"children": Array [
|
|
||||||
Object {
|
|
||||||
"children": Array [
|
|
||||||
Object {
|
|
||||||
"children": Array [
|
|
||||||
Object {
|
|
||||||
"label": "String",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "name",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"children": Array [
|
|
||||||
Object {
|
|
||||||
"label": "Int64",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "someNumber",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"children": Array [
|
|
||||||
Object {
|
|
||||||
"label": "Double",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "anotherNumber",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "items",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "list",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "items",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"children": Array [
|
|
||||||
Object {
|
|
||||||
"label": "String",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "_etag",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "Schema",
|
|
||||||
"onClick": [Function],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
paddingLeft={0}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
@@ -7,7 +7,6 @@ import { IGitHubResponse } from "../GitHub/GitHubClient";
|
|||||||
import { IGitHubOAuthToken } from "../GitHub/GitHubOAuthService";
|
import { IGitHubOAuthToken } from "../GitHub/GitHubOAuthService";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||||
import { number } from "prop-types";
|
|
||||||
|
|
||||||
export interface IJunoResponse<T> {
|
export interface IJunoResponse<T> {
|
||||||
status: number;
|
status: number;
|
||||||
@@ -428,51 +427,6 @@ export class JunoClient {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async requestSchema(
|
|
||||||
schemaRequest: DataModels.ISchemaRequest
|
|
||||||
): Promise<IJunoResponse<DataModels.ISchemaRequest>> {
|
|
||||||
const response = await window.fetch(`${this.getAnalyticsUrl()}/${schemaRequest.accountName}/schema/request`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(schemaRequest),
|
|
||||||
headers: JunoClient.getHeaders()
|
|
||||||
});
|
|
||||||
|
|
||||||
let data: DataModels.ISchemaRequest;
|
|
||||||
if (response.status === HttpStatusCodes.OK) {
|
|
||||||
data = await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: response.status,
|
|
||||||
data
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getSchema(
|
|
||||||
accountName: string,
|
|
||||||
databaseName: string,
|
|
||||||
containerName: string
|
|
||||||
): Promise<IJunoResponse<DataModels.ISchema>> {
|
|
||||||
const response = await window.fetch(
|
|
||||||
`${this.getAnalyticsUrl()}/${accountName}/schema/${databaseName}/${containerName}`,
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: JunoClient.getHeaders()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let data: DataModels.ISchema;
|
|
||||||
|
|
||||||
if (response.status === HttpStatusCodes.OK) {
|
|
||||||
data = await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: response.status,
|
|
||||||
data
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getNotebooks(input: RequestInfo, init?: RequestInit): Promise<IJunoResponse<IGalleryItem[]>> {
|
private async getNotebooks(input: RequestInfo, init?: RequestInit): Promise<IJunoResponse<IGalleryItem[]>> {
|
||||||
const response = await window.fetch(input, init);
|
const response = await window.fetch(input, init);
|
||||||
|
|
||||||
@@ -503,10 +457,6 @@ export class JunoClient {
|
|||||||
return `${this.getNotebooksUrl()}/${this.getAccount()}`;
|
return `${this.getNotebooksUrl()}/${this.getAccount()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAnalyticsUrl(): string {
|
|
||||||
return `${configContext.JUNO_ENDPOINT}/api/analytics`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static getHeaders(): HeadersInit {
|
private static getHeaders(): HeadersInit {
|
||||||
const authorizationHeader = getAuthorizationHeader();
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
return {
|
return {
|
||||||
|
|||||||
117
src/Main.ts
Normal file
117
src/Main.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
// CSS Dependencies
|
||||||
|
import "bootstrap/dist/css/bootstrap.css";
|
||||||
|
import "../less/documentDB.less";
|
||||||
|
import "../less/tree.less";
|
||||||
|
import "../less/forms.less";
|
||||||
|
import "../less/menus.less";
|
||||||
|
import "../less/infobox.less";
|
||||||
|
import "../less/messagebox.less";
|
||||||
|
import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less";
|
||||||
|
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
|
||||||
|
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
|
||||||
|
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
|
||||||
|
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
|
||||||
|
import "./Explorer/Controls/DynamicList/DynamicListComponent.less";
|
||||||
|
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
|
||||||
|
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
|
||||||
|
import "../less/TableStyles/queryBuilder.less";
|
||||||
|
import "../externals/jquery.dataTables.min.css";
|
||||||
|
import "../less/TableStyles/fulldatatables.less";
|
||||||
|
import "../less/TableStyles/EntityEditor.less";
|
||||||
|
import "../less/TableStyles/CustomizeColumns.less";
|
||||||
|
import "../less/resourceTree.less";
|
||||||
|
import "../externals/jquery.typeahead.min.css";
|
||||||
|
import "../externals/jquery-ui.min.css";
|
||||||
|
import "../externals/jquery-ui.structure.min.css";
|
||||||
|
import "../externals/jquery-ui.theme.min.css";
|
||||||
|
import "./Explorer/Graph/NewVertexComponent/newVertexComponent.less";
|
||||||
|
import "./Explorer/Panes/GraphNewVertexPane.less";
|
||||||
|
import "./Explorer/Tabs/QueryTab.less";
|
||||||
|
import "./Explorer/Controls/TreeComponent/treeComponent.less";
|
||||||
|
import "./Explorer/Controls/Accordion/AccordionComponent.less";
|
||||||
|
import "./Explorer/SplashScreen/SplashScreenComponent.less";
|
||||||
|
import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
|
||||||
|
|
||||||
|
// Image Dependencies
|
||||||
|
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
||||||
|
import "../images/favicon.ico";
|
||||||
|
|
||||||
|
import "./Shared/appInsights";
|
||||||
|
import "babel-polyfill";
|
||||||
|
import "es6-symbol/implement";
|
||||||
|
import "webcrypto-liner/build/webcrypto-liner.shim.min";
|
||||||
|
import "./Libs/jquery";
|
||||||
|
import "bootstrap/dist/js/npm";
|
||||||
|
import "../externals/jquery.typeahead.min.js";
|
||||||
|
import "../externals/jquery-ui.min.js";
|
||||||
|
import "../externals/adal.js";
|
||||||
|
import "promise-polyfill/src/polyfill";
|
||||||
|
import "abort-controller/polyfill";
|
||||||
|
import "whatwg-fetch";
|
||||||
|
import "es6-object-assign/auto";
|
||||||
|
import "promise.prototype.finally/auto";
|
||||||
|
import "object.entries/auto";
|
||||||
|
import "./Libs/is-integer-polyfill";
|
||||||
|
import "url-polyfill/url-polyfill.min";
|
||||||
|
|
||||||
|
// TODO: Enable ReactDevTools after fixing the portal CORS issue
|
||||||
|
// import "./ReactDevTools"
|
||||||
|
|
||||||
|
import * as ko from "knockout";
|
||||||
|
import * as TelemetryProcessor from "./Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import { Action, ActionModifiers } from "./Shared/Telemetry/TelemetryConstants";
|
||||||
|
|
||||||
|
import { BindingHandlersRegisterer } from "./Bindings/BindingHandlersRegisterer";
|
||||||
|
import * as Emulator from "./Platform/Emulator/Main";
|
||||||
|
import Hosted from "./Platform/Hosted/Main";
|
||||||
|
import * as Portal from "./Platform/Portal/Main";
|
||||||
|
import { AuthType } from "./AuthType";
|
||||||
|
|
||||||
|
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
||||||
|
import { applyExplorerBindings } from "./applyExplorerBindings";
|
||||||
|
import { initializeConfiguration, Platform } from "./ConfigContext";
|
||||||
|
import Explorer from "./Explorer/Explorer";
|
||||||
|
|
||||||
|
initializeIcons(/* optional base url */);
|
||||||
|
|
||||||
|
// TODO: Encapsulate and reuse all global variables as environment variables
|
||||||
|
window.authType = AuthType.AAD;
|
||||||
|
|
||||||
|
initializeConfiguration().then(config => {
|
||||||
|
if (config.platform === Platform.Hosted) {
|
||||||
|
try {
|
||||||
|
Hosted.initializeExplorer().then(
|
||||||
|
(explorer: Explorer) => {
|
||||||
|
applyExplorerBindings(explorer);
|
||||||
|
Hosted.configureTokenValidationDisplayPrompt(explorer);
|
||||||
|
},
|
||||||
|
(error: any) => {
|
||||||
|
try {
|
||||||
|
const uninitializedExplorer: Explorer = Hosted.getUninitializedExplorerForGuestAccess();
|
||||||
|
window.dataExplorer = uninitializedExplorer;
|
||||||
|
ko.applyBindings(uninitializedExplorer);
|
||||||
|
BindingHandlersRegisterer.registerBindingHandlers();
|
||||||
|
if (window.authType !== AuthType.AAD) {
|
||||||
|
uninitializedExplorer.isRefreshingExplorer(false);
|
||||||
|
uninitializedExplorer.displayConnectExplorerForm();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
} else if (config.platform === Platform.Emulator) {
|
||||||
|
window.authType = AuthType.MasterKey;
|
||||||
|
const explorer = Emulator.initializeExplorer();
|
||||||
|
applyExplorerBindings(explorer);
|
||||||
|
} else if (config.platform === Platform.Portal) {
|
||||||
|
TelemetryProcessor.trace(Action.InitializeDataExplorer, ActionModifiers.Open, {});
|
||||||
|
const explorer = Portal.initializeExplorer();
|
||||||
|
TelemetryProcessor.trace(Action.InitializeDataExplorer, ActionModifiers.IFrameReady, {});
|
||||||
|
applyExplorerBindings(explorer);
|
||||||
|
}
|
||||||
|
});
|
||||||
452
src/Main.tsx
452
src/Main.tsx
@@ -1,452 +0,0 @@
|
|||||||
// CSS Dependencies
|
|
||||||
import "bootstrap/dist/css/bootstrap.css";
|
|
||||||
import "../less/documentDB.less";
|
|
||||||
import "../less/tree.less";
|
|
||||||
import "../less/forms.less";
|
|
||||||
import "../less/menus.less";
|
|
||||||
import "../less/infobox.less";
|
|
||||||
import "../less/messagebox.less";
|
|
||||||
import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less";
|
|
||||||
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
|
|
||||||
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
|
|
||||||
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
|
|
||||||
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
|
|
||||||
import "./Explorer/Controls/DynamicList/DynamicListComponent.less";
|
|
||||||
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
|
|
||||||
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
|
|
||||||
import "../less/TableStyles/queryBuilder.less";
|
|
||||||
import "../externals/jquery.dataTables.min.css";
|
|
||||||
import "../less/TableStyles/fulldatatables.less";
|
|
||||||
import "../less/TableStyles/EntityEditor.less";
|
|
||||||
import "../less/TableStyles/CustomizeColumns.less";
|
|
||||||
import "../less/resourceTree.less";
|
|
||||||
import "../externals/jquery.typeahead.min.css";
|
|
||||||
import "../externals/jquery-ui.min.css";
|
|
||||||
import "../externals/jquery-ui.structure.min.css";
|
|
||||||
import "../externals/jquery-ui.theme.min.css";
|
|
||||||
import "./Explorer/Graph/NewVertexComponent/newVertexComponent.less";
|
|
||||||
import "./Explorer/Panes/GraphNewVertexPane.less";
|
|
||||||
import "./Explorer/Tabs/QueryTab.less";
|
|
||||||
import "./Explorer/Controls/TreeComponent/treeComponent.less";
|
|
||||||
import "./Explorer/Controls/Accordion/AccordionComponent.less";
|
|
||||||
import "./Explorer/SplashScreen/SplashScreenComponent.less";
|
|
||||||
import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
|
|
||||||
|
|
||||||
// Image Dependencies
|
|
||||||
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
|
||||||
import "../images/favicon.ico";
|
|
||||||
|
|
||||||
import "./Shared/appInsights";
|
|
||||||
import "babel-polyfill";
|
|
||||||
import "es6-symbol/implement";
|
|
||||||
import "webcrypto-liner/build/webcrypto-liner.shim.min";
|
|
||||||
import "./Libs/jquery";
|
|
||||||
import "bootstrap/dist/js/npm";
|
|
||||||
import "../externals/jquery.typeahead.min.js";
|
|
||||||
import "../externals/jquery-ui.min.js";
|
|
||||||
import "../externals/adal.js";
|
|
||||||
import "promise-polyfill/src/polyfill";
|
|
||||||
import "abort-controller/polyfill";
|
|
||||||
import "whatwg-fetch";
|
|
||||||
import "es6-object-assign/auto";
|
|
||||||
import "promise.prototype.finally/auto";
|
|
||||||
import "object.entries/auto";
|
|
||||||
import "./Libs/is-integer-polyfill";
|
|
||||||
import "url-polyfill/url-polyfill.min";
|
|
||||||
|
|
||||||
initializeIcons();
|
|
||||||
|
|
||||||
import * as ko from "knockout";
|
|
||||||
import * as TelemetryProcessor from "./Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import { Action, ActionModifiers } from "./Shared/Telemetry/TelemetryConstants";
|
|
||||||
|
|
||||||
import { BindingHandlersRegisterer } from "./Bindings/BindingHandlersRegisterer";
|
|
||||||
import * as Emulator from "./Platform/Emulator/Main";
|
|
||||||
import Hosted from "./Platform/Hosted/Main";
|
|
||||||
import * as Portal from "./Platform/Portal/Main";
|
|
||||||
import { AuthType } from "./AuthType";
|
|
||||||
|
|
||||||
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
|
||||||
import { applyExplorerBindings } from "./applyExplorerBindings";
|
|
||||||
import { initializeConfiguration, Platform } from "./ConfigContext";
|
|
||||||
import Explorer from "./Explorer/Explorer";
|
|
||||||
import React, { useEffect } from "react";
|
|
||||||
import ReactDOM from "react-dom";
|
|
||||||
import errorImage from "../images/error.svg";
|
|
||||||
import copyImage from "../images/Copy.svg";
|
|
||||||
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
|
||||||
import refreshImg from "../images/refresh-cosmos.svg";
|
|
||||||
import arrowLeftImg from "../images/imgarrowlefticon.svg";
|
|
||||||
import { KOCommentEnd, KOCommentIfStart } from "./koComment";
|
|
||||||
|
|
||||||
// TODO: Encapsulate and reuse all global variables as environment variables
|
|
||||||
window.authType = AuthType.AAD;
|
|
||||||
|
|
||||||
const App: React.FunctionComponent = () => {
|
|
||||||
useEffect(() => {
|
|
||||||
initializeConfiguration().then(config => {
|
|
||||||
if (config.platform === Platform.Hosted) {
|
|
||||||
try {
|
|
||||||
Hosted.initializeExplorer().then(
|
|
||||||
(explorer: Explorer) => {
|
|
||||||
applyExplorerBindings(explorer);
|
|
||||||
Hosted.configureTokenValidationDisplayPrompt(explorer);
|
|
||||||
},
|
|
||||||
(error: unknown) => {
|
|
||||||
try {
|
|
||||||
const uninitializedExplorer: Explorer = Hosted.getUninitializedExplorerForGuestAccess();
|
|
||||||
window.dataExplorer = uninitializedExplorer;
|
|
||||||
ko.applyBindings(uninitializedExplorer);
|
|
||||||
BindingHandlersRegisterer.registerBindingHandlers();
|
|
||||||
if (window.authType !== AuthType.AAD) {
|
|
||||||
uninitializedExplorer.isRefreshingExplorer(false);
|
|
||||||
uninitializedExplorer.displayConnectExplorerForm();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
} else if (config.platform === Platform.Emulator) {
|
|
||||||
window.authType = AuthType.MasterKey;
|
|
||||||
const explorer = Emulator.initializeExplorer();
|
|
||||||
applyExplorerBindings(explorer);
|
|
||||||
} else if (config.platform === Platform.Portal) {
|
|
||||||
TelemetryProcessor.trace(Action.InitializeDataExplorer, ActionModifiers.Open, {});
|
|
||||||
const explorer = Portal.initializeExplorer();
|
|
||||||
TelemetryProcessor.trace(Action.InitializeDataExplorer, ActionModifiers.IFrameReady, {});
|
|
||||||
applyExplorerBindings(explorer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flexContainer">
|
|
||||||
<div id="divExplorer" className="flexContainer hideOverflows" style={{ display: "none" }}>
|
|
||||||
{/* Main Command Bar - Start */}
|
|
||||||
<div data-bind="react: commandBarComponentAdapter" />
|
|
||||||
{/* Main Command Bar - End */}
|
|
||||||
{/* Share url flyout - Start */}
|
|
||||||
<div
|
|
||||||
id="shareDataAccessFlyout"
|
|
||||||
className="shareDataAccessFlyout"
|
|
||||||
data-bind="visible: shouldShowShareDialogContents"
|
|
||||||
>
|
|
||||||
<div className="shareDataAccessFlyoutContent">
|
|
||||||
<div className="urlContainer">
|
|
||||||
<span className="urlContentText">
|
|
||||||
Open this database account in a new browser tab with Cosmos DB Explorer. Or copy the read-write or read
|
|
||||||
only access urls below to share with others. For security purposes, the URLs grant time-bound access to
|
|
||||||
the account. When access expires, you can reconnect, using a valid connection string for the account.
|
|
||||||
</span>
|
|
||||||
<br />
|
|
||||||
<div
|
|
||||||
className="toggles"
|
|
||||||
data-bind="event: { keydown: onToggleKeyDown }, visible: shareAccessData().readWriteUrl != null"
|
|
||||||
tabIndex={0}
|
|
||||||
aria-label="Read-Write and Read toggle"
|
|
||||||
>
|
|
||||||
<div className="tab">
|
|
||||||
<input type="radio" className="radio" defaultValue="readwrite" />
|
|
||||||
<span
|
|
||||||
className="toggleSwitch"
|
|
||||||
role="presentation"
|
|
||||||
data-bind="click: toggleReadWrite, css:{ selectedToggle: isReadWriteToggled(), unselectedToggle: !isReadWriteToggled() }"
|
|
||||||
>
|
|
||||||
Read-Write
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="tab">
|
|
||||||
<input type="radio" className="radio" defaultValue="read" />
|
|
||||||
<span
|
|
||||||
className="toggleSwitch"
|
|
||||||
role="presentation"
|
|
||||||
data-bind="click: toggleRead, css:{ selectedToggle: isReadToggled(), unselectedToggle: !isReadToggled() }"
|
|
||||||
>
|
|
||||||
Read
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="urlSpace">
|
|
||||||
<input
|
|
||||||
id="shareUrlLink"
|
|
||||||
aria-label="Share url link"
|
|
||||||
className="shareLink"
|
|
||||||
type="text"
|
|
||||||
read-only
|
|
||||||
data-bind="value: shareAccessUrl"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className="urlTokenCopyInfoTooltip"
|
|
||||||
data-bind="click: copyUrlLink, event: { keypress: onCopyUrlLinkKeyPress }"
|
|
||||||
aria-label="Copy url link"
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
<img src={copyImage} alt="Copy link" />
|
|
||||||
<span className="urlTokenCopyTooltiptext" data-bind="text: shareUrlCopyHelperText" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Share url flyout - End */}
|
|
||||||
{/* Collections Tree and Tabs - Begin */}
|
|
||||||
<div className="resourceTreeAndTabs">
|
|
||||||
{/* Collections Tree - Start */}
|
|
||||||
<div id="resourcetree" data-test="resourceTreeId" className="resourceTree">
|
|
||||||
<div className="collectionsTreeWithSplitter">
|
|
||||||
{/* Collections Tree Expanded - Start */}
|
|
||||||
<div
|
|
||||||
id="main"
|
|
||||||
className="main"
|
|
||||||
data-bind="
|
|
||||||
visible: isLeftPaneExpanded()"
|
|
||||||
>
|
|
||||||
{/* Collections Window - - Start */}
|
|
||||||
<div id="mainslide" className="flexContainer">
|
|
||||||
{/* Collections Window Title/Command Bar - Start */}
|
|
||||||
<div className="collectiontitle">
|
|
||||||
<div className="coltitle">
|
|
||||||
<span className="titlepadcol" data-bind="text: collectionTitle" />
|
|
||||||
<div className="float-right">
|
|
||||||
<span
|
|
||||||
className="padimgcolrefresh"
|
|
||||||
data-test="refreshTree"
|
|
||||||
role="button"
|
|
||||||
data-bind="
|
|
||||||
click: onRefreshResourcesClick, clickBubble: false, event: { keypress: onRefreshDatabasesKeyPress }"
|
|
||||||
tabIndex={0}
|
|
||||||
aria-label="Refresh tree"
|
|
||||||
title="Refresh tree"
|
|
||||||
>
|
|
||||||
<img className="refreshcol" src={refreshImg} data-bind="attr: { alt: refreshTreeTitle }" />
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className="padimgcolrefresh1"
|
|
||||||
id="expandToggleLeftPaneButton"
|
|
||||||
role="button"
|
|
||||||
data-bind="
|
|
||||||
click: toggleLeftPaneExpanded, event: { keypress: toggleLeftPaneExpandedKeyPress }"
|
|
||||||
tabIndex={0}
|
|
||||||
aria-label="Collapse Tree"
|
|
||||||
title="Collapse Tree"
|
|
||||||
>
|
|
||||||
<img className="refreshcol1" src={arrowLeftImg} alt="Hide" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Collections Window Title/Command Bar - End */}
|
|
||||||
|
|
||||||
{!window.dataExplorer?.isAuthWithResourceToken() && (
|
|
||||||
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
|
|
||||||
)}
|
|
||||||
{window.dataExplorer?.isAuthWithResourceToken() && (
|
|
||||||
<div style={{ overflowY: "auto" }} data-bind="react:resourceTreeForResourceToken" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{/* Collections Window - End */}
|
|
||||||
</div>
|
|
||||||
{/* Collections Tree Expanded - End */}
|
|
||||||
{/* Collections Tree Collapsed - Start */}
|
|
||||||
<div
|
|
||||||
id="mini"
|
|
||||||
className="mini toggle-mini"
|
|
||||||
data-bind="visible: !isLeftPaneExpanded()
|
|
||||||
attr: { style: { width: collapsedResourceTreeWidth }}"
|
|
||||||
>
|
|
||||||
<div className="main-nav nav">
|
|
||||||
<ul className="nav">
|
|
||||||
<li
|
|
||||||
className="resourceTreeCollapse"
|
|
||||||
id="collapseToggleLeftPaneButton"
|
|
||||||
role="button"
|
|
||||||
data-bind="event: { keypress: toggleLeftPaneExpandedKeyPress }"
|
|
||||||
tabIndex={0}
|
|
||||||
aria-label="Expand Tree"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="leftarrowCollapsed"
|
|
||||||
data-bind="
|
|
||||||
click: toggleLeftPaneExpanded"
|
|
||||||
>
|
|
||||||
<img className="arrowCollapsed" src={arrowLeftImg} alt="Expand" />
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className="collectionCollapsed"
|
|
||||||
data-bind="
|
|
||||||
click: toggleLeftPaneExpanded"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-bind="
|
|
||||||
text: collectionTitle"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Collections Tree Collapsed - End */}
|
|
||||||
</div>
|
|
||||||
{/* Splitter - Start */}
|
|
||||||
<div className="splitter ui-resizable-handle ui-resizable-e" id="h_splitter1" />
|
|
||||||
{/* Splitter - End */}
|
|
||||||
</div>
|
|
||||||
{/* Collections Tree - End */}
|
|
||||||
<div
|
|
||||||
className="connectExplorerContainer"
|
|
||||||
data-bind="visible: !isRefreshingExplorer() && tabsManager.openedTabs().length === 0"
|
|
||||||
>
|
|
||||||
<form className="connectExplorerFormContainer">
|
|
||||||
<div className="connectExplorer" data-bind="react: splashScreenAdapter" />
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="tabsManagerContainer"
|
|
||||||
data-bind='component: { name: "tabs-manager", params: {data: tabsManager} }'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* Collections Tree and Tabs - End */}
|
|
||||||
<div
|
|
||||||
className="dataExplorerErrorConsoleContainer"
|
|
||||||
role="contentinfo"
|
|
||||||
aria-label="Notification console"
|
|
||||||
id="explorerNotificationConsole"
|
|
||||||
data-bind="react: notificationConsoleComponentAdapter"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* Explorer Connection - Encryption Token / AAD - Start */}
|
|
||||||
<div id="connectExplorer" className="connectExplorerContainer" style={{ display: "none" }}>
|
|
||||||
<div className="connectExplorerFormContainer">
|
|
||||||
<div className="connectExplorer">
|
|
||||||
<p className="connectExplorerContent">
|
|
||||||
<img src={hdeConnectImage} alt="Azure Cosmos DB" />
|
|
||||||
</p>
|
|
||||||
<p className="welcomeText">Welcome to Azure Cosmos DB</p>
|
|
||||||
<div id="connectWithAad">
|
|
||||||
<input
|
|
||||||
className="filterbtnstyle"
|
|
||||||
data-test="cosmosdb-signinBtn"
|
|
||||||
type="button"
|
|
||||||
defaultValue="Sign In"
|
|
||||||
data-bind="click: $data.signInAad"
|
|
||||||
/>
|
|
||||||
<p
|
|
||||||
className="switchConnectTypeText"
|
|
||||||
data-test="cosmosdb-connectionString"
|
|
||||||
data-bind="click: $data.onSwitchToConnectionString"
|
|
||||||
>
|
|
||||||
Connect to your account with connection string
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<form id="connectWithConnectionString" data-bind="submit: renewToken" style={{ display: "none" }}>
|
|
||||||
<p className="connectExplorerContent connectStringText">Connect to your account with connection string</p>
|
|
||||||
<p className="connectExplorerContent">
|
|
||||||
<input
|
|
||||||
className="inputToken"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
placeholder="Please enter a connection string"
|
|
||||||
data-bind="value: tokenForRenewal"
|
|
||||||
/>
|
|
||||||
<span className="errorDetailsInfoTooltip" data-bind="visible: renewTokenError().length > 0">
|
|
||||||
<img className="errorImg" src={errorImage} alt="Error notification" />
|
|
||||||
<span className="errorDetails" data-bind="text: renewTokenError" />
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<p className="connectExplorerContent">
|
|
||||||
<input className="filterbtnstyle" type="submit" value="Connect" />
|
|
||||||
</p>
|
|
||||||
<p className="switchConnectTypeText" data-bind="click: $data.signInAad">
|
|
||||||
Sign In with Azure Account
|
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Explorer Connection - Encryption Token / AAD - End */}
|
|
||||||
{/* Global loader - Start */}
|
|
||||||
<div className="splashLoaderContainer" data-bind="visible: isRefreshingExplorer">
|
|
||||||
<div className="splashLoaderContentContainer">
|
|
||||||
<p className="connectExplorerContent">
|
|
||||||
<img src={hdeConnectImage} alt="Azure Cosmos DB" />
|
|
||||||
</p>
|
|
||||||
<p className="splashLoaderTitle" id="explorerLoadingStatusTitle">
|
|
||||||
Welcome to Azure Cosmos DB
|
|
||||||
</p>
|
|
||||||
<p className="splashLoaderText" id="explorerLoadingStatusText" role="alert">
|
|
||||||
Connecting...
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Global loader - End */}
|
|
||||||
<div data-bind="react:uploadItemsPaneAdapter" />
|
|
||||||
<div data-bind='component: { name: "add-database-pane", params: {data: addDatabasePane} }' />
|
|
||||||
<div data-bind='component: { name: "add-collection-pane", params: { data: addCollectionPane} }' />
|
|
||||||
<div data-bind='component: { name: "delete-collection-confirmation-pane", params: { data: deleteCollectionConfirmationPane} }' />
|
|
||||||
<div data-bind='component: { name: "delete-database-confirmation-pane", params: { data: deleteDatabaseConfirmationPane} }' />
|
|
||||||
<div data-bind='component: { name: "graph-new-vertex-pane", params: { data: newVertexPane} }' />
|
|
||||||
<div data-bind='component: { name: "graph-styling-pane", params: { data: graphStylingPane} }' />
|
|
||||||
<div data-bind='component: { name: "table-add-entity-pane", params: { data: addTableEntityPane} }' />
|
|
||||||
<div data-bind='component: { name: "table-edit-entity-pane", params: { data: editTableEntityPane} }' />
|
|
||||||
<div data-bind='component: { name: "table-column-options-pane", params: { data: tableColumnOptionsPane} }' />
|
|
||||||
<div data-bind='component: { name: "table-query-select-pane", params: { data: querySelectPane} }' />
|
|
||||||
<div data-bind='component: { name: "cassandra-add-collection-pane", params: { data: cassandraAddCollectionPane} }' />
|
|
||||||
<div data-bind='component: { name: "settings-pane", params: { data: settingsPane} }' />
|
|
||||||
<div data-bind='component: { name: "upload-items-pane", params: { data: uploadItemsPane} }' />
|
|
||||||
<div data-bind='component: { name: "load-query-pane", params: { data: loadQueryPane} }' />
|
|
||||||
<div data-bind='component: { name: "execute-sproc-params-pane", params: { data: executeSprocParamsPane} }' />
|
|
||||||
<div data-bind='component: { name: "renew-adhoc-access-pane", params: { data: renewAdHocAccessPane} }' />
|
|
||||||
<div data-bind='component: { name: "save-query-pane", params: { data: saveQueryPane} }' />
|
|
||||||
<div data-bind='component: { name: "browse-queries-pane", params: { data: browseQueriesPane} }' />
|
|
||||||
<div data-bind='component: { name: "upload-file-pane", params: { data: uploadFilePane} }' />
|
|
||||||
<div data-bind='component: { name: "string-input-pane", params: { data: stringInputPane} }' />
|
|
||||||
<div data-bind='component: { name: "setup-notebooks-pane", params: { data: setupNotebooksPane} }' />
|
|
||||||
<KOCommentIfStart if="isGitHubPaneEnabled" />
|
|
||||||
<div data-bind='component: { name: "github-repos-pane", params: { data: gitHubReposPane } }' />
|
|
||||||
<KOCommentEnd />
|
|
||||||
<KOCommentIfStart if="isPublishNotebookPaneEnabled" />
|
|
||||||
<div data-bind="react: publishNotebookPaneAdapter" />
|
|
||||||
<KOCommentEnd />
|
|
||||||
<KOCommentIfStart if="isCopyNotebookPaneEnabled" />
|
|
||||||
<div data-bind="react: copyNotebookPaneAdapter" />
|
|
||||||
<KOCommentEnd />
|
|
||||||
{/* Global access token expiration dialog - Start */}
|
|
||||||
<div
|
|
||||||
id="dataAccessTokenModal"
|
|
||||||
className="dataAccessTokenModal"
|
|
||||||
style={{ display: "none" }}
|
|
||||||
data-bind="visible: shouldShowDataAccessExpiryDialog"
|
|
||||||
>
|
|
||||||
<div className="dataAccessTokenModalContent">
|
|
||||||
<p className="dataAccessTokenExpireText">Please reconnect to the account using the connection string.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Global access token expiration dialog - End */}
|
|
||||||
{/* Context switch prompt - Start */}
|
|
||||||
<div
|
|
||||||
id="contextSwitchPrompt"
|
|
||||||
className="dataAccessTokenModal"
|
|
||||||
style={{ display: "none" }}
|
|
||||||
data-bind="visible: shouldShowContextSwitchPrompt"
|
|
||||||
>
|
|
||||||
<div className="dataAccessTokenModalContent">
|
|
||||||
<p className="dataAccessTokenExpireText">
|
|
||||||
Please save your work before you switch! When you switch to a different Azure Cosmos DB account, current
|
|
||||||
Data Explorer tabs will be closed.
|
|
||||||
</p>
|
|
||||||
<p className="dataAccessTokenExpireText">Proceed anyway?</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div data-bind="react: dialogComponentAdapter" />
|
|
||||||
<div data-bind="react: addSynapseLinkDialog" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ReactDOM.render(<App />, document.body);
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SubscriptionType } from "../Contracts/SubscriptionType";
|
import { SubscriptionType } from "../Contracts/ViewModels";
|
||||||
|
|
||||||
export const hoursInAMonth = 730;
|
export const hoursInAMonth = 730;
|
||||||
export class AutoscalePricing {
|
export class AutoscalePricing {
|
||||||
|
|||||||
@@ -23,14 +23,10 @@ const createServerSettings = (urlVars: { [key: string]: string }): ServerConnect
|
|||||||
let headers: HeadersInit;
|
let headers: HeadersInit;
|
||||||
if (urlVars.hasOwnProperty(TerminalQueryParams.TerminalEndpoint)) {
|
if (urlVars.hasOwnProperty(TerminalQueryParams.TerminalEndpoint)) {
|
||||||
body = JSON.stringify({
|
body = JSON.stringify({
|
||||||
endpoint: urlVars[TerminalQueryParams.TerminalEndpoint],
|
endpoint: urlVars[TerminalQueryParams.TerminalEndpoint]
|
||||||
account:"contoso-retail-mongodb",
|
|
||||||
port: "10255",
|
|
||||||
token:"" //tofill
|
|
||||||
});
|
});
|
||||||
headers = {
|
headers = {
|
||||||
[HttpHeaders.contentType]: "application/json"
|
[HttpHeaders.contentType]: "application/json"
|
||||||
//"Access-Control-Allow-Origin": "https://localhost:5001"
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { DatabaseAccount } from "./Contracts/DataModels";
|
import { DatabaseAccount } from "./Contracts/DataModels";
|
||||||
import { SubscriptionType } from "./Contracts/SubscriptionType";
|
|
||||||
import { DefaultAccountExperienceType } from "./DefaultAccountExperienceType";
|
import { DefaultAccountExperienceType } from "./DefaultAccountExperienceType";
|
||||||
|
|
||||||
interface UserContext {
|
interface UserContext {
|
||||||
@@ -13,7 +12,6 @@ interface UserContext {
|
|||||||
resourceToken?: string;
|
resourceToken?: string;
|
||||||
defaultExperience?: DefaultAccountExperienceType;
|
defaultExperience?: DefaultAccountExperienceType;
|
||||||
useSDKOperations?: boolean;
|
useSDKOperations?: boolean;
|
||||||
subscriptionType?: SubscriptionType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const userContext: Readonly<UserContext> = {} as const;
|
const userContext: Readonly<UserContext> = {} as const;
|
||||||
|
|||||||
@@ -7,6 +7,15 @@ export const minAutoPilotThroughput = 4000;
|
|||||||
|
|
||||||
export const autoPilotIncrementStep = 1000;
|
export const autoPilotIncrementStep = 1000;
|
||||||
|
|
||||||
|
export function isValidV3AutoPilotOffer(offer: Offer): boolean {
|
||||||
|
const maxThroughput =
|
||||||
|
offer &&
|
||||||
|
offer.content &&
|
||||||
|
offer.content.offerAutopilotSettings &&
|
||||||
|
offer.content.offerAutopilotSettings.maxThroughput;
|
||||||
|
return isValidAutoPilotThroughput(maxThroughput);
|
||||||
|
}
|
||||||
|
|
||||||
export function isValidAutoPilotThroughput(maxThroughput: number): boolean {
|
export function isValidAutoPilotThroughput(maxThroughput: number): boolean {
|
||||||
if (!maxThroughput) {
|
if (!maxThroughput) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
51
src/Utils/OfferUtils.test.ts
Normal file
51
src/Utils/OfferUtils.test.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import * as Constants from "../../src/Common/Constants";
|
||||||
|
import * as DataModels from "../../src/Contracts/DataModels";
|
||||||
|
import { OfferUtils } from "../../src/Utils/OfferUtils";
|
||||||
|
|
||||||
|
describe("OfferUtils tests", () => {
|
||||||
|
const offerV1: DataModels.Offer = {
|
||||||
|
_rid: "",
|
||||||
|
_self: "",
|
||||||
|
_ts: 0,
|
||||||
|
_etag: "",
|
||||||
|
id: "v1",
|
||||||
|
offerVersion: Constants.OfferVersions.V1,
|
||||||
|
offerType: "Standard",
|
||||||
|
offerResourceId: "",
|
||||||
|
content: null,
|
||||||
|
resource: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
const offerV2: DataModels.Offer = {
|
||||||
|
_rid: "",
|
||||||
|
_self: "",
|
||||||
|
_ts: 0,
|
||||||
|
_etag: "",
|
||||||
|
id: "v1",
|
||||||
|
offerVersion: Constants.OfferVersions.V2,
|
||||||
|
offerType: "Standard",
|
||||||
|
offerResourceId: "",
|
||||||
|
content: null,
|
||||||
|
resource: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("isOfferV1()", () => {
|
||||||
|
it("should return true for V1", () => {
|
||||||
|
expect(OfferUtils.isOfferV1(offerV1)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for V2", () => {
|
||||||
|
expect(OfferUtils.isOfferV1(offerV2)).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isNotOfferV1()", () => {
|
||||||
|
it("should return true for V2", () => {
|
||||||
|
expect(OfferUtils.isNotOfferV1(offerV2)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for V1", () => {
|
||||||
|
expect(OfferUtils.isNotOfferV1(offerV1)).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
12
src/Utils/OfferUtils.ts
Normal file
12
src/Utils/OfferUtils.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import * as Constants from "../Common/Constants";
|
||||||
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
|
|
||||||
|
export class OfferUtils {
|
||||||
|
public static isOfferV1(offer: DataModels.Offer): boolean {
|
||||||
|
return !offer || offer.offerVersion !== Constants.OfferVersions.V2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static isNotOfferV1(offer: DataModels.Offer): boolean {
|
||||||
|
return !OfferUtils.isOfferV1(offer);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,12 @@ import Explorer from "./Explorer/Explorer";
|
|||||||
|
|
||||||
export const applyExplorerBindings = (explorer: Explorer) => {
|
export const applyExplorerBindings = (explorer: Explorer) => {
|
||||||
if (!!explorer) {
|
if (!!explorer) {
|
||||||
|
ko.applyBindings(explorer);
|
||||||
// This message should ideally be sent immediately after explorer has been initialized for optimal data explorer load times.
|
// This message should ideally be sent immediately after explorer has been initialized for optimal data explorer load times.
|
||||||
// TODO: Send another message to describe that the bindings have been applied, and handle message transfers accordingly in the portal
|
// TODO: Send another message to describe that the bindings have been applied, and handle message transfers accordingly in the portal
|
||||||
sendMessage("ready");
|
sendMessage("ready");
|
||||||
window.dataExplorer = explorer;
|
window.dataExplorer = explorer;
|
||||||
BindingHandlersRegisterer.registerBindingHandlers();
|
BindingHandlersRegisterer.registerBindingHandlers();
|
||||||
ko.applyBindings(explorer);
|
|
||||||
$("#divExplorer").show();
|
$("#divExplorer").show();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,5 +8,329 @@
|
|||||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body></body>
|
<body>
|
||||||
|
<div class="flexContainer">
|
||||||
|
<div id="divExplorer" class="flexContainer hideOverflows" style="display: none">
|
||||||
|
<!-- Main Command Bar - Start -->
|
||||||
|
<div data-bind="react: commandBarComponentAdapter"></div>
|
||||||
|
<!-- Main Command Bar - End -->
|
||||||
|
<!-- Share url flyout - Start -->
|
||||||
|
<div
|
||||||
|
id="shareDataAccessFlyout"
|
||||||
|
class="shareDataAccessFlyout"
|
||||||
|
data-bind="visible: shouldShowShareDialogContents"
|
||||||
|
>
|
||||||
|
<div class="shareDataAccessFlyoutContent">
|
||||||
|
<div class="urlContainer">
|
||||||
|
<span class="urlContentText"
|
||||||
|
>Open this database account in a new browser tab with Cosmos DB Explorer. Or copy the read-write or read
|
||||||
|
only access urls below to share with others. For security purposes, the URLs grant time-bound access to
|
||||||
|
the account. When access expires, you can reconnect, using a valid connection string for the
|
||||||
|
account.</span
|
||||||
|
>
|
||||||
|
<br />
|
||||||
|
<div
|
||||||
|
class="toggles"
|
||||||
|
data-bind="event: { keydown: onToggleKeyDown }, visible: shareAccessData().readWriteUrl != null"
|
||||||
|
tabindex="0"
|
||||||
|
aria-label="Read-Write and Read toggle"
|
||||||
|
>
|
||||||
|
<div class="tab">
|
||||||
|
<input type="radio" class="radio" value="readwrite" />
|
||||||
|
<span
|
||||||
|
class="toggleSwitch"
|
||||||
|
role="presentation"
|
||||||
|
data-bind="click: toggleReadWrite, css:{ selectedToggle: isReadWriteToggled(), unselectedToggle: !isReadWriteToggled() }"
|
||||||
|
>Read-Write</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="tab">
|
||||||
|
<input type="radio" class="radio" value="read" />
|
||||||
|
<span
|
||||||
|
class="toggleSwitch"
|
||||||
|
role="presentation"
|
||||||
|
data-bind="click: toggleRead, css:{ selectedToggle: isReadToggled(), unselectedToggle: !isReadToggled() }"
|
||||||
|
>Read</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="urlSpace">
|
||||||
|
<input
|
||||||
|
id="shareUrlLink"
|
||||||
|
aria-label="Share url link"
|
||||||
|
class="shareLink"
|
||||||
|
type="text"
|
||||||
|
read-only
|
||||||
|
data-bind="value: shareAccessUrl"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="urlTokenCopyInfoTooltip"
|
||||||
|
data-bind="click: copyUrlLink, event: { keypress: onCopyUrlLinkKeyPress }"
|
||||||
|
aria-label="Copy url link"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<img src="/Copy.svg" alt="Copy link" />
|
||||||
|
<span class="urlTokenCopyTooltiptext" data-bind="text: shareUrlCopyHelperText"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Share url flyout - End -->
|
||||||
|
|
||||||
|
<!-- Collections Tree and Tabs - Begin -->
|
||||||
|
<div class="resourceTreeAndTabs">
|
||||||
|
<!-- Collections Tree - Start -->
|
||||||
|
<div id="resourcetree" data-test="resourceTreeId" class="resourceTree">
|
||||||
|
<div class="collectionsTreeWithSplitter">
|
||||||
|
<!-- Collections Tree Expanded - Start -->
|
||||||
|
<div
|
||||||
|
id="main"
|
||||||
|
class="main"
|
||||||
|
data-bind="
|
||||||
|
visible: isLeftPaneExpanded()"
|
||||||
|
>
|
||||||
|
<!-- Collections Window - - Start -->
|
||||||
|
<div id="mainslide" class="flexContainer">
|
||||||
|
<!-- Collections Window Title/Command Bar - Start -->
|
||||||
|
<div class="collectiontitle">
|
||||||
|
<div class="coltitle">
|
||||||
|
<span class="titlepadcol" data-bind="text: collectionTitle"></span>
|
||||||
|
<div class="float-right">
|
||||||
|
<span
|
||||||
|
class="padimgcolrefresh"
|
||||||
|
data-test="refreshTree"
|
||||||
|
role="button"
|
||||||
|
data-bind="
|
||||||
|
click: onRefreshResourcesClick, clickBubble: false, event: { keypress: onRefreshDatabasesKeyPress }"
|
||||||
|
tabindex="0"
|
||||||
|
aria-label="Refresh tree"
|
||||||
|
title="Refresh tree"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="refreshcol"
|
||||||
|
src="/refresh-cosmos.svg"
|
||||||
|
data-bind="attr: { alt: refreshTreeTitle }"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="padimgcolrefresh1"
|
||||||
|
id="expandToggleLeftPaneButton"
|
||||||
|
role="button"
|
||||||
|
data-bind="
|
||||||
|
click: toggleLeftPaneExpanded, event: { keypress: toggleLeftPaneExpandedKeyPress }"
|
||||||
|
tabindex="0"
|
||||||
|
aria-label="Collapse Tree"
|
||||||
|
title="Collapse Tree"
|
||||||
|
>
|
||||||
|
<img class="refreshcol1" src="/imgarrowlefticon.svg" alt="Hide" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Collections Window Title/Command Bar - End -->
|
||||||
|
<!-- ko if: !isAuthWithResourceToken() -->
|
||||||
|
<div style="overflow-y: auto" data-bind="react:resourceTree"></div>
|
||||||
|
<!-- /ko -->
|
||||||
|
<!-- ko if: isAuthWithResourceToken() -->
|
||||||
|
<div style="overflow-y: auto" data-bind="react:resourceTreeForResourceToken"></div>
|
||||||
|
<!-- /ko -->
|
||||||
|
</div>
|
||||||
|
<!-- Collections Window - End -->
|
||||||
|
</div>
|
||||||
|
<!-- Collections Tree Expanded - End -->
|
||||||
|
|
||||||
|
<!-- Collections Tree Collapsed - Start -->
|
||||||
|
<div
|
||||||
|
id="mini"
|
||||||
|
class="mini toggle-mini"
|
||||||
|
data-bind="visible: !isLeftPaneExpanded()
|
||||||
|
attr: { style: { width: collapsedResourceTreeWidth }}"
|
||||||
|
>
|
||||||
|
<div class="main-nav nav">
|
||||||
|
<ul class="nav">
|
||||||
|
<li
|
||||||
|
class="resourceTreeCollapse"
|
||||||
|
id="collapseToggleLeftPaneButton"
|
||||||
|
role="button"
|
||||||
|
data-bind="event: { keypress: toggleLeftPaneExpandedKeyPress }"
|
||||||
|
tabindex="0"
|
||||||
|
aria-label="Expand Tree"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="leftarrowCollapsed"
|
||||||
|
data-bind="
|
||||||
|
click: toggleLeftPaneExpanded"
|
||||||
|
>
|
||||||
|
<img class="arrowCollapsed" src="/imgarrowlefticon.svg" alt="Expand" />
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="collectionCollapsed"
|
||||||
|
data-bind="
|
||||||
|
click: toggleLeftPaneExpanded"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
data-bind="
|
||||||
|
text: collectionTitle"
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Collections Tree Collapsed - End -->
|
||||||
|
</div>
|
||||||
|
<!-- Splitter - Start -->
|
||||||
|
<div class="splitter ui-resizable-handle ui-resizable-e" id="h_splitter1"></div>
|
||||||
|
<!-- Splitter - End -->
|
||||||
|
</div>
|
||||||
|
<!-- Collections Tree - End -->
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="connectExplorerContainer"
|
||||||
|
data-bind="visible: !isRefreshingExplorer() && tabsManager.openedTabs().length === 0"
|
||||||
|
>
|
||||||
|
<form class="connectExplorerFormContainer">
|
||||||
|
<div class="connectExplorer" data-bind="react: splashScreenAdapter"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<tabs-manager
|
||||||
|
class="tabsManagerContainer"
|
||||||
|
params="{data: tabsManager}"
|
||||||
|
data-bind="visible: tabsManager.openedTabs().length > 0"
|
||||||
|
></tabs-manager>
|
||||||
|
</div>
|
||||||
|
<!-- Collections Tree and Tabs - End -->
|
||||||
|
<div
|
||||||
|
class="dataExplorerErrorConsoleContainer"
|
||||||
|
role="contentinfo"
|
||||||
|
aria-label="Notification console"
|
||||||
|
id="explorerNotificationConsole"
|
||||||
|
data-bind="react: notificationConsoleComponentAdapter"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Explorer Connection - Encryption Token / AAD - Start -->
|
||||||
|
<div id="connectExplorer" class="connectExplorerContainer" style="display: none;">
|
||||||
|
<div class="connectExplorerFormContainer">
|
||||||
|
<div class="connectExplorer">
|
||||||
|
<p class="connectExplorerContent"><img src="/HdeConnectCosmosDB.svg" alt="Azure Cosmos DB" /></p>
|
||||||
|
<p class="welcomeText">Welcome to Azure Cosmos DB</p>
|
||||||
|
<div id="connectWithAad">
|
||||||
|
<input
|
||||||
|
class="filterbtnstyle"
|
||||||
|
data-test="cosmosdb-signinBtn"
|
||||||
|
type="button"
|
||||||
|
value="Sign In"
|
||||||
|
data-bind="click: $data.signInAad"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
class="switchConnectTypeText"
|
||||||
|
data-test="cosmosdb-connectionString"
|
||||||
|
data-bind="click: $data.onSwitchToConnectionString"
|
||||||
|
>
|
||||||
|
Connect to your account with connection string
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<form id="connectWithConnectionString" data-bind="submit: renewToken" style="display: none;">
|
||||||
|
<p class="connectExplorerContent connectStringText">Connect to your account with connection string</p>
|
||||||
|
<p class="connectExplorerContent">
|
||||||
|
<input
|
||||||
|
class="inputToken"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="Please enter a connection string"
|
||||||
|
data-bind="value: tokenForRenewal"
|
||||||
|
/>
|
||||||
|
<span class="errorDetailsInfoTooltip" data-bind="visible: renewTokenError().length > 0">
|
||||||
|
<img class="errorImg" src="/error.svg" alt="Error notification" />
|
||||||
|
<span class="errorDetails" data-bind="text: renewTokenError"></span>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p class="connectExplorerContent"><input class="filterbtnstyle" type="submit" value="Connect" /></p>
|
||||||
|
<p class="switchConnectTypeText" data-bind="click: $data.signInAad">Sign In with Azure Account</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Explorer Connection - Encryption Token / AAD - End -->
|
||||||
|
|
||||||
|
<!-- Global loader - Start -->
|
||||||
|
<div class="splashLoaderContainer" data-bind="visible: isRefreshingExplorer">
|
||||||
|
<div class="splashLoaderContentContainer">
|
||||||
|
<p class="connectExplorerContent"><img src="/HdeConnectCosmosDB.svg" alt="Azure Cosmos DB" /></p>
|
||||||
|
<p class="splashLoaderTitle" id="explorerLoadingStatusTitle">Welcome to Azure Cosmos DB</p>
|
||||||
|
<p class="splashLoaderText" id="explorerLoadingStatusText" role="alert">Connecting...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Global loader - End -->
|
||||||
|
<div data-bind="react:uploadItemsPaneAdapter"></div>
|
||||||
|
<add-database-pane params="{data: addDatabasePane}"></add-database-pane>
|
||||||
|
<add-collection-pane params="{data: addCollectionPane}"></add-collection-pane>
|
||||||
|
<delete-collection-confirmation-pane params="{data: deleteCollectionConfirmationPane}">
|
||||||
|
</delete-collection-confirmation-pane>
|
||||||
|
<delete-database-confirmation-pane params="{data: deleteDatabaseConfirmationPane}">
|
||||||
|
</delete-database-confirmation-pane>
|
||||||
|
<graph-new-vertex-pane params="{data: newVertexPane}"></graph-new-vertex-pane>
|
||||||
|
<graph-styling-pane params="{data: graphStylingPane}"></graph-styling-pane>
|
||||||
|
<table-add-entity-pane params="{data: addTableEntityPane}"></table-add-entity-pane>
|
||||||
|
<table-edit-entity-pane params="{data: editTableEntityPane}"></table-edit-entity-pane>
|
||||||
|
<table-column-options-pane params="{data: tableColumnOptionsPane}"></table-column-options-pane>
|
||||||
|
<table-query-select-pane params="{data: querySelectPane}"></table-query-select-pane>
|
||||||
|
<cassandra-add-collection-pane params="{data: cassandraAddCollectionPane}"></cassandra-add-collection-pane>
|
||||||
|
<settings-pane params="{data: settingsPane}"></settings-pane>
|
||||||
|
<upload-items-pane params="{data: uploadItemsPane}"></upload-items-pane>
|
||||||
|
<load-query-pane params="{data: loadQueryPane}"></load-query-pane>
|
||||||
|
<execute-sproc-params-pane params="{data: executeSprocParamsPane}"></execute-sproc-params-pane>
|
||||||
|
<renew-adhoc-access-pane params="{data: renewAdHocAccessPane}"></renew-adhoc-access-pane>
|
||||||
|
<save-query-pane params="{data: saveQueryPane}"></save-query-pane>
|
||||||
|
<browse-queries-pane params="{data: browseQueriesPane}"></browse-queries-pane>
|
||||||
|
<upload-file-pane params="{data: uploadFilePane}"></upload-file-pane>
|
||||||
|
<string-input-pane params="{data: stringInputPane}"></string-input-pane>
|
||||||
|
<setup-notebooks-pane params="{data: setupNotebooksPane}"></setup-notebooks-pane>
|
||||||
|
|
||||||
|
<!-- ko if: isGitHubPaneEnabled -->
|
||||||
|
<github-repos-pane params="{data: gitHubReposPane}"></github-repos-pane>
|
||||||
|
<!-- /ko -->
|
||||||
|
|
||||||
|
<!-- ko if: isPublishNotebookPaneEnabled -->
|
||||||
|
<div data-bind="react: publishNotebookPaneAdapter"></div>
|
||||||
|
<!-- /ko -->
|
||||||
|
|
||||||
|
<!-- ko if: isCopyNotebookPaneEnabled -->
|
||||||
|
<div data-bind="react: copyNotebookPaneAdapter"></div>
|
||||||
|
<!-- /ko -->
|
||||||
|
|
||||||
|
<!-- Global access token expiration dialog - Start -->
|
||||||
|
<div
|
||||||
|
id="dataAccessTokenModal"
|
||||||
|
class="dataAccessTokenModal"
|
||||||
|
style="display: none"
|
||||||
|
data-bind="visible: shouldShowDataAccessExpiryDialog"
|
||||||
|
>
|
||||||
|
<div class="dataAccessTokenModalContent">
|
||||||
|
<p class="dataAccessTokenExpireText">Please reconnect to the account using the connection string.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Global access token expiration dialog - End -->
|
||||||
|
<!-- Context switch prompt - Start -->
|
||||||
|
<div
|
||||||
|
id="contextSwitchPrompt"
|
||||||
|
class="dataAccessTokenModal"
|
||||||
|
style="display: none"
|
||||||
|
data-bind="visible: shouldShowContextSwitchPrompt"
|
||||||
|
>
|
||||||
|
<div class="dataAccessTokenModalContent">
|
||||||
|
<p class="dataAccessTokenExpireText">
|
||||||
|
Please save your work before you switch! When you switch to a different Azure Cosmos DB account, current
|
||||||
|
Data Explorer tabs will be closed.
|
||||||
|
</p>
|
||||||
|
<p class="dataAccessTokenExpireText">Proceed anyway?</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div data-bind="react: dialogComponentAdapter"></div>
|
||||||
|
<div data-bind="react: addSynapseLinkDialog"></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
/* eslint-disable react/prop-types */
|
|
||||||
import React, { useEffect, useRef } from "react";
|
|
||||||
|
|
||||||
export const KOCommentIfStart: React.FunctionComponent<{ if: string }> = props => {
|
|
||||||
const el = useRef();
|
|
||||||
useEffect(() => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
(el.current as any).outerHTML = `<!-- ko if: ${props.if} -->`;
|
|
||||||
}, []);
|
|
||||||
return <div ref={el} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const KOCommentEnd: React.FunctionComponent = () => {
|
|
||||||
const el = useRef();
|
|
||||||
useEffect(() => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
(el.current as any).outerHTML = `<!-- /ko -->`;
|
|
||||||
}, []);
|
|
||||||
return <div ref={el} />;
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user