mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-23 11:44:03 +00:00
Compare commits
9 Commits
remove-ru-
...
user/swvis
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35b15a1e20 | ||
|
|
17fd2185dc | ||
|
|
a93c8509cd | ||
|
|
5c93c11bd9 | ||
|
|
85d2378d3a | ||
|
|
84b6075ee8 | ||
|
|
d880723be9 | ||
|
|
4ce9dcc024 | ||
|
|
addcfedd5e |
@@ -3,7 +3,11 @@ PORTAL_RUNNER_PASSWORD=
|
|||||||
PORTAL_RUNNER_SUBSCRIPTION=
|
PORTAL_RUNNER_SUBSCRIPTION=
|
||||||
PORTAL_RUNNER_RESOURCE_GROUP=
|
PORTAL_RUNNER_RESOURCE_GROUP=
|
||||||
PORTAL_RUNNER_DATABASE_ACCOUNT=
|
PORTAL_RUNNER_DATABASE_ACCOUNT=
|
||||||
|
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY=
|
||||||
PORTAL_RUNNER_CONNECTION_STRING=
|
PORTAL_RUNNER_CONNECTION_STRING=
|
||||||
|
NOTEBOOKS_TEST_RUNNER_TENANT_ID=
|
||||||
|
NOTEBOOKS_TEST_RUNNER_CLIENT_ID=
|
||||||
|
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET=
|
||||||
CASSANDRA_CONNECTION_STRING=
|
CASSANDRA_CONNECTION_STRING=
|
||||||
MONGO_CONNECTION_STRING=
|
MONGO_CONNECTION_STRING=
|
||||||
TABLES_CONNECTION_STRING=
|
TABLES_CONNECTION_STRING=
|
||||||
|
|||||||
@@ -202,8 +202,6 @@ src/Explorer/Tabs/QueryTab.test.ts
|
|||||||
src/Explorer/Tabs/QueryTab.ts
|
src/Explorer/Tabs/QueryTab.ts
|
||||||
src/Explorer/Tabs/QueryTablesTab.ts
|
src/Explorer/Tabs/QueryTablesTab.ts
|
||||||
src/Explorer/Tabs/ScriptTabBase.ts
|
src/Explorer/Tabs/ScriptTabBase.ts
|
||||||
src/Explorer/Tabs/SettingsTab.test.ts
|
|
||||||
src/Explorer/Tabs/SettingsTab.ts
|
|
||||||
src/Explorer/Tabs/SparkMasterTab.ts
|
src/Explorer/Tabs/SparkMasterTab.ts
|
||||||
src/Explorer/Tabs/StoredProcedureTab.ts
|
src/Explorer/Tabs/StoredProcedureTab.ts
|
||||||
src/Explorer/Tabs/TabComponents.ts
|
src/Explorer/Tabs/TabComponents.ts
|
||||||
@@ -290,8 +288,6 @@ src/Utils/DatabaseAccountUtils.ts
|
|||||||
src/Utils/JunoUtils.ts
|
src/Utils/JunoUtils.ts
|
||||||
src/Utils/MessageValidation.ts
|
src/Utils/MessageValidation.ts
|
||||||
src/Utils/NotebookConfigurationUtils.ts
|
src/Utils/NotebookConfigurationUtils.ts
|
||||||
src/Utils/OfferUtils.test.ts
|
|
||||||
src/Utils/OfferUtils.ts
|
|
||||||
src/Utils/PricingUtils.test.ts
|
src/Utils/PricingUtils.test.ts
|
||||||
src/Utils/QueryUtils.test.ts
|
src/Utils/QueryUtils.test.ts
|
||||||
src/Utils/QueryUtils.ts
|
src/Utils/QueryUtils.ts
|
||||||
@@ -396,19 +392,5 @@ src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx
|
|||||||
src/GalleryViewer/Cards/GalleryCardComponent.tsx
|
src/GalleryViewer/Cards/GalleryCardComponent.tsx
|
||||||
src/GalleryViewer/GalleryViewer.tsx
|
src/GalleryViewer/GalleryViewer.tsx
|
||||||
src/GalleryViewer/GalleryViewerComponent.tsx
|
src/GalleryViewer/GalleryViewerComponent.tsx
|
||||||
cypress/integration/dataexplorer/CASSANDRA/addCollection.spec.ts
|
|
||||||
cypress/integration/dataexplorer/GRAPH/addCollection.spec.ts
|
|
||||||
cypress/integration/dataexplorer/ci-tests/addCollectionPane.spec.ts
|
|
||||||
cypress/integration/dataexplorer/ci-tests/createDatabase.spec.ts
|
|
||||||
cypress/integration/dataexplorer/ci-tests/deleteCollection.spec.ts
|
|
||||||
cypress/integration/dataexplorer/ci-tests/deleteDatabase.spec.ts
|
|
||||||
cypress/integration/dataexplorer/MONGO/addCollection.spec.ts
|
|
||||||
cypress/integration/dataexplorer/MONGO/addCollectionAutopilot.spec.ts
|
|
||||||
cypress/integration/dataexplorer/MONGO/addCollectionExistingDatabase.spec.ts
|
|
||||||
cypress/integration/dataexplorer/MONGO/provisionDatabaseThroughput.spec.ts
|
|
||||||
cypress/integration/dataexplorer/SQL/addCollection.spec.ts
|
|
||||||
cypress/integration/dataexplorer/TABLE/addCollection.spec.ts
|
|
||||||
cypress/integration/notebook/newNotebook.spec.ts
|
|
||||||
cypress/integration/notebook/resourceTree.spec.ts
|
|
||||||
__mocks__/monaco-editor.ts
|
__mocks__/monaco-editor.ts
|
||||||
src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx
|
src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx
|
||||||
40
.github/workflows/ci.yml
vendored
40
.github/workflows/ci.yml
vendored
@@ -79,32 +79,31 @@ jobs:
|
|||||||
name: dist
|
name: dist
|
||||||
path: dist/
|
path: dist/
|
||||||
endtoendemulator:
|
endtoendemulator:
|
||||||
name: "End To End Tests | Emulator | SQL"
|
name: "End To End Emulator Tests"
|
||||||
needs: [lint, format, compile, unittest]
|
needs: [lint, format, compile, unittest]
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: southpolesteve/cosmos-emulator-github-action@v1
|
|
||||||
- name: Use Node.js 12.x
|
- name: Use Node.js 12.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: 12.x
|
||||||
- name: Restore Cypress Binary Cache
|
- uses: southpolesteve/cosmos-emulator-github-action@v1
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/.cache/Cypress
|
|
||||||
key: ${{ runner.os }}-cypress-binary-cache
|
|
||||||
- name: End to End Tests
|
- name: End to End Tests
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
npm start &
|
npm start &
|
||||||
npm ci --prefix ./cypress
|
npm run wait-for-server
|
||||||
npm run test:ci --prefix ./cypress -- --spec ./integration/dataexplorer/ci-tests/createDatabase.spec.ts
|
npx jest -c ./jest.config.e2e.js --detectOpenHandles sql
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
EMULATOR_ENDPOINT: https://0.0.0.0:8081/
|
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/explorer.html?platform=Emulator"
|
||||||
|
PLATFORM: "Emulator"
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: screenshots
|
||||||
|
path: failed-*
|
||||||
accessibility:
|
accessibility:
|
||||||
name: "Accessibility | Hosted"
|
name: "Accessibility | Hosted"
|
||||||
needs: [lint, format, compile, unittest]
|
needs: [lint, format, compile, unittest]
|
||||||
@@ -123,13 +122,13 @@ jobs:
|
|||||||
sudo sysctl -p
|
sudo sysctl -p
|
||||||
npm ci
|
npm ci
|
||||||
npm start &
|
npm start &
|
||||||
npx wait-on -i 5000 https-get://0.0.0.0:1234/
|
npx wait-on -i 5000 https-get://0.0.0.0:1234/
|
||||||
node utils/accesibilityCheck.js
|
node utils/accesibilityCheck.js
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
endtoendpuppeteer:
|
endtoendhosted:
|
||||||
name: "End to end puppeteer tests"
|
name: "End to End Hosted Tests"
|
||||||
needs: [lint, format, compile, unittest]
|
needs: [lint, format, compile, unittest]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -138,7 +137,7 @@ jobs:
|
|||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: 12.x
|
||||||
- name: End to End Puppeteer Tests
|
- name: End to End Hosted Tests
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
npm start &
|
npm start &
|
||||||
@@ -147,6 +146,13 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
|
PORTAL_RUNNER_SUBSCRIPTION: ${{ secrets.PORTAL_RUNNER_SUBSCRIPTION }}
|
||||||
|
PORTAL_RUNNER_RESOURCE_GROUP: ${{ secrets.PORTAL_RUNNER_RESOURCE_GROUP }}
|
||||||
|
PORTAL_RUNNER_DATABASE_ACCOUNT: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT }}
|
||||||
|
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY }}
|
||||||
|
NOTEBOOKS_TEST_RUNNER_TENANT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_TENANT_ID }}
|
||||||
|
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
|
||||||
|
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
|
||||||
PORTAL_RUNNER_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
|
PORTAL_RUNNER_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
|
||||||
MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
|
MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
|
||||||
CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }}
|
CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }}
|
||||||
@@ -159,7 +165,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, endtoendpuppeteer]
|
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||||
@@ -183,7 +189,7 @@ jobs:
|
|||||||
nugetmpac:
|
nugetmpac:
|
||||||
name: Publish Nuget MPAC
|
name: Publish Nuget MPAC
|
||||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendpuppeteer]
|
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,9 +9,6 @@ pkg/DataExplorer/*
|
|||||||
test/out/*
|
test/out/*
|
||||||
workers/**/*.js
|
workers/**/*.js
|
||||||
*.trx
|
*.trx
|
||||||
cypress/videos
|
|
||||||
cypress/screenshots
|
|
||||||
cypress/fixtures
|
|
||||||
notebookapp/*
|
notebookapp/*
|
||||||
Contracts/*
|
Contracts/*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -76,17 +76,7 @@ Unit tests are located adjacent to the code under test and run with [Jest](https
|
|||||||
|
|
||||||
#### End to End CI Tests
|
#### End to End CI Tests
|
||||||
|
|
||||||
[Cypress](https://www.cypress.io/) is used for end to end tests and are contained in `cypress/`. Currently, it operates as sub project with its own typescript config and dependencies. It also only operates against the emulator. To run cypress tests:
|
Jest and Puppeteer are used for end to end browser based tests and are contained in `test/`. To run these tests locally:
|
||||||
|
|
||||||
1. Ensure the emulator is running
|
|
||||||
2. Start cosmos explorer in emulator mode: `PLATFORM=Emulator npm run watch`
|
|
||||||
3. Move into `cypress/` folder: `cd cypress`
|
|
||||||
4. Install dependencies: `npm install`
|
|
||||||
5. Run cypress headless(`npm run test`) or in interactive mode(`npm run test:debug`)
|
|
||||||
|
|
||||||
#### End to End Production Tests
|
|
||||||
|
|
||||||
Jest and Puppeteer are used for end to end production runners and are contained in `test/`. To run these tests locally:
|
|
||||||
|
|
||||||
1. Copy .env.example to .env
|
1. Copy .env.example to .env
|
||||||
2. Update the values in .env including your local data explorer endpoint (ask a teammate/codeowner for help with .env values)
|
2. Update the values in .env including your local data explorer endpoint (ask a teammate/codeowner for help with .env values)
|
||||||
|
|||||||
4
cypress/.gitignore
vendored
4
cypress/.gitignore
vendored
@@ -1,4 +0,0 @@
|
|||||||
cypress.env.json
|
|
||||||
cypress/report
|
|
||||||
cypress/screenshots
|
|
||||||
cypress/videos
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
// Cleans up old databases from previous test runs
|
|
||||||
const { CosmosClient } = require("@azure/cosmos");
|
|
||||||
|
|
||||||
// TODO: Add support for other API connection strings
|
|
||||||
const mongoRegex = RegExp("mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com");
|
|
||||||
|
|
||||||
async function cleanup() {
|
|
||||||
const connectionString = process.env.CYPRESS_CONNECTION_STRING;
|
|
||||||
if (!connectionString) {
|
|
||||||
throw new Error("Connection string not provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
let client;
|
|
||||||
switch (true) {
|
|
||||||
case connectionString.includes("mongodb://"): {
|
|
||||||
const [, key, accountName] = connectionString.match(mongoRegex);
|
|
||||||
client = new CosmosClient({
|
|
||||||
key,
|
|
||||||
endpoint: `https://${accountName}.documents.azure.com:443/`
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// TODO: Add support for other API connection strings
|
|
||||||
default:
|
|
||||||
client = new CosmosClient(connectionString);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await client.databases.readAll().fetchAll();
|
|
||||||
return Promise.all(
|
|
||||||
response.resources.map(async db => {
|
|
||||||
const dbTimestamp = new Date(db._ts * 1000);
|
|
||||||
const twentyMinutesAgo = new Date(Date.now() - 1000 * 60 * 20);
|
|
||||||
if (dbTimestamp < twentyMinutesAgo) {
|
|
||||||
await client.database(db.id).delete();
|
|
||||||
console.log(`DELETED: ${db.id} | Timestamp: ${dbTimestamp}`);
|
|
||||||
} else {
|
|
||||||
console.log(`SKIPPED: ${db.id} | Timestamp: ${dbTimestamp}`);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup()
|
|
||||||
.then(() => {
|
|
||||||
process.exit(0);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"integrationFolder": "./integration",
|
|
||||||
"pluginsFile": false,
|
|
||||||
"fixturesFolder": false,
|
|
||||||
"supportFile": "./support/index.js",
|
|
||||||
"defaultCommandTimeout": 90000,
|
|
||||||
"chromeWebSecurity": false,
|
|
||||||
"reporter": "mochawesome",
|
|
||||||
"reporterOptions": {
|
|
||||||
"reportDir": "cypress/report",
|
|
||||||
"json": true,
|
|
||||||
"overwrite": false,
|
|
||||||
"html": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
// 1. Click on "New Container" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Cassandra API Test - createDatabase", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString(connectionString.constants.cassandra);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new table in Cassandra API", () => {
|
|
||||||
const keyspaceId = `KeyspaceId${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const tableId = `TableId112`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Table"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[id="keyspace-id"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.type(keyspaceId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[class="textfontclr"]')
|
|
||||||
.type(tableId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('data-test="addCollection-createCollection"')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", tableId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
// 1. Click on "New Graph" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Graph API Test", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString(connectionString.constants.graph);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new graph in Graph API", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const graphId = `TestGraph${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const partitionKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Graph"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(graphId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(partitionKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId)
|
|
||||||
.click()
|
|
||||||
.should("contain", graphId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
// 1. Click on "New Container" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// // 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Mongo API Test - createDatabase", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new collection in Mongo API", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Collection"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(sharedKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find("#submitBtnAddCollection")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId)
|
|
||||||
.click()
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
// 1. Click on "New Container" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Mongo API Test", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
});
|
|
||||||
|
|
||||||
it.skip("Create a new collection in Mongo API - Autopilot", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Collection"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="throughputModeContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.and(input => {
|
|
||||||
expect(input.get(0).textContent, "first item").contains("Autopilot (preview)");
|
|
||||||
expect(input.get(1).textContent, "second item").contains("Manual");
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[id="newContainer-databaseThroughput-autoPilotRadio"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('select[name="autoPilotTiers"]')
|
|
||||||
// .eq(1).should('contain', '4,000 RU/s');
|
|
||||||
// // .select('4,000 RU/s').should('have.value', '1');
|
|
||||||
|
|
||||||
.find('option[value="2"]')
|
|
||||||
.then($element => $element.get(1).setAttribute("selected", "selected"));
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(sharedKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId)
|
|
||||||
.click()
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Mongo API Test", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
});
|
|
||||||
|
|
||||||
it.skip("Create a new collection in existing database in Mongo API", () => {
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('span[class="nodeLabel"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.then($span => {
|
|
||||||
const dbId1 = $span.text();
|
|
||||||
cy.log("DBBB", dbId1);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Collection"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-existingDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-existingDatabase"]')
|
|
||||||
.type(dbId1);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(sharedKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.click()
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context.skip("Mongo API Test", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new collection in Mongo API - Provision database throughput", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Collection"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find(".createNewDatabaseOrUseExisting")
|
|
||||||
.should("have.length", 2)
|
|
||||||
.and(input => {
|
|
||||||
expect(input.get(0).textContent, "first item").contains("Create new");
|
|
||||||
expect(input.get(1).textContent, "second item").contains("Use existing");
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(sharedKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId)
|
|
||||||
.click()
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new collection - without provision database throughput", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionIdTitle = `Add Collection`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Collection"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.uncheck();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[id="tab2"]')
|
|
||||||
.check({ force: true });
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(sharedKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId)
|
|
||||||
.click()
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new collection - without provision database throughput Fixed Storage Capacity", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Collection"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.uncheck();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[id="tab1"]')
|
|
||||||
.check({ force: true });
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId)
|
|
||||||
.click()
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
// 1. Click on "New Container" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("SQL API Test", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new container in SQL API", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Container"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(sharedKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find("#submitBtnAddCollection")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
// 1. Click on "New Container" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Table API Test", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString(connectionString.constants.table);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new table in Table API", () => {
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Table"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// 1. Click on "New Container" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Emulator - createDatabase", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("http://localhost:1234/explorer.html");
|
|
||||||
});
|
|
||||||
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionIdTitle = `Add Collection`;
|
|
||||||
const partitionKey = `PartitionKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
it("Create a new collection", () => {
|
|
||||||
cy.contains("New Container").click();
|
|
||||||
|
|
||||||
// cy.contains(collectionIdTitle);
|
|
||||||
|
|
||||||
cy.get(".createNewDatabaseOrUseExisting")
|
|
||||||
.should("have.length", 2)
|
|
||||||
.and(input => {
|
|
||||||
expect(input.get(0).textContent, "first item").contains("Create new");
|
|
||||||
expect(input.get(1).textContent, "second item").contains("Use existing");
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get('input[data-test="addCollection-createNewDatabase"]').check();
|
|
||||||
|
|
||||||
cy.get('input[data-test="addCollection-newDatabaseId"]').type(dbId);
|
|
||||||
|
|
||||||
cy.get('input[data-test="addCollection-collectionId"]').type(collectionId);
|
|
||||||
|
|
||||||
cy.get('input[data-test="databaseThroughputValue"]').should("have.value", "400");
|
|
||||||
|
|
||||||
cy.get('input[data-test="addCollection-partitionKeyValue"]').type(partitionKey);
|
|
||||||
|
|
||||||
cy.get('input[data-test="addCollection-createCollection"]').click();
|
|
||||||
|
|
||||||
cy.get('div[data-test="resourceTreeId"]').should("exist");
|
|
||||||
|
|
||||||
cy.get('div[data-test="resourceTree-collectionsTree"]').should("contain", dbId);
|
|
||||||
|
|
||||||
cy.get('div[data-test="databaseList"]').should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
// 1. Click on "New Database" on the command bar
|
|
||||||
// 2. a Pane with the title "Add Database" should appear on the right side of the screen
|
|
||||||
// i. It includes an input box for the database Id.
|
|
||||||
// ii. It includes a checkbox called "Provision throughput".
|
|
||||||
// iii. Whe the checkbox is marked, a new input with a throughput control let's you customize RU at the database level
|
|
||||||
// 3. Create a database WITHOUT "Provision throughput" checked.
|
|
||||||
// 4. It should appear in the Data Explorer list.
|
|
||||||
// 5. Repeat steps 1-3 but create a database WITH "Provision throughput" with the default RU value.
|
|
||||||
// 6. It should appear in the Data Explorer list.
|
|
||||||
// 7. If expanded, it should have the list item called "Scale", that once clicked, it should show the "Scale" tab.
|
|
||||||
// 8. Inside that tab, a throughput control will let you change the RU value within the permited range.
|
|
||||||
// 9. If you change the value, it should enable the "Save" button.
|
|
||||||
// 10. Click "Save" and verify that the process completes without error.
|
|
||||||
// 11. Close the tab and reopen it and verify that the input contains the last saved value.%
|
|
||||||
|
|
||||||
const crypto = require("crypto");
|
|
||||||
const client = require("../../../utilities/cosmosClient");
|
|
||||||
const randomString = crypto.randomBytes(2).toString("hex");
|
|
||||||
const databaseId = `TestDB-${randomString}`;
|
|
||||||
const collectionId = `TestColl-${randomString}`;
|
|
||||||
|
|
||||||
context("Emulator - Create database -> container -> item", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
const { resources } = await client.databases.readAll().fetchAll();
|
|
||||||
for (const database of resources) {
|
|
||||||
await client.database(database.id).delete();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("creates a new database", () => {
|
|
||||||
cy.visit("https://0.0.0.0:1234/explorer.html?platform=Emulator");
|
|
||||||
cy.contains("New Container").click();
|
|
||||||
cy.get("[data-test=addCollection-newDatabaseId]").click();
|
|
||||||
cy.get("[data-test=addCollection-newDatabaseId]").type(databaseId);
|
|
||||||
cy.get("[data-test=addCollection-collectionId]").click();
|
|
||||||
cy.get("[data-test=addCollection-collectionId]").type(collectionId);
|
|
||||||
cy.get("[data-test=addCollection-partitionKeyValue]").click();
|
|
||||||
cy.get("[data-test=addCollection-partitionKeyValue]").type("/pk");
|
|
||||||
cy.get('input[name="createCollection"]').click();
|
|
||||||
cy.get(".dataResourceTree").should("contain", databaseId);
|
|
||||||
cy.get(".dataResourceTree")
|
|
||||||
.contains(databaseId)
|
|
||||||
.click();
|
|
||||||
cy.get(".dataResourceTree").should("contain", collectionId);
|
|
||||||
cy.get(".dataResourceTree")
|
|
||||||
.contains(collectionId)
|
|
||||||
.click();
|
|
||||||
cy.get(".dataResourceTree")
|
|
||||||
.contains("Items")
|
|
||||||
.click();
|
|
||||||
cy.get(".dataResourceTree")
|
|
||||||
.contains("Items")
|
|
||||||
.click();
|
|
||||||
cy.wait(1000); // React rendering inside KO causes some weird async rendering that makes this test flaky without waiting
|
|
||||||
cy.get(".commandBarContainer")
|
|
||||||
.contains("New Item")
|
|
||||||
.click();
|
|
||||||
cy.wait(1000); // React rendering inside KO causes some weird async rendering that makes this test flaky without waiting
|
|
||||||
cy.get(".commandBarContainer")
|
|
||||||
.contains("Save")
|
|
||||||
.click();
|
|
||||||
cy.wait(1000); // React rendering inside KO causes some weird async rendering that makes this test flaky without waiting
|
|
||||||
cy.get(".documentsGridHeaderContainer").should("contain", "replace_with_new_document_id");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
// 1. Click last database in the resource tree
|
|
||||||
// 2. Click the last collection within the database
|
|
||||||
// 3. Select the context menu within the collection
|
|
||||||
// 4. Select "Delete Container" option in the dropdown
|
|
||||||
// 5. On Selection, Delete Container pane opens on the right side
|
|
||||||
// 6. Enter the same collection id that is to be deleted and click ok
|
|
||||||
// 7. Now, the resource tree refreshes, the deleted collection should not appear under the database
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Emulator - deleteCollection", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("http://localhost:1234/explorer.html");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Delete a collection", () => {
|
|
||||||
cy.get(".databaseId")
|
|
||||||
.last()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get(".collectionList")
|
|
||||||
.last()
|
|
||||||
.then($id => {
|
|
||||||
const collectionId = $id.text();
|
|
||||||
|
|
||||||
cy.get('span[data-test="collectionEllipsisMenu"]').should("exist");
|
|
||||||
|
|
||||||
cy.get('span[data-test="collectionEllipsisMenu"]')
|
|
||||||
.invoke("show")
|
|
||||||
.last()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get('div[data-test="collectionContextMenu"]')
|
|
||||||
.contains("Delete Container")
|
|
||||||
.click({ force: true });
|
|
||||||
|
|
||||||
cy.get('input[data-test="confirmCollectionId"]').type(collectionId.trim());
|
|
||||||
|
|
||||||
cy.get('input[data-test="deleteCollection"]').click();
|
|
||||||
|
|
||||||
cy.get('div[data-test="databaseList"]').should("not.contain", collectionId);
|
|
||||||
|
|
||||||
cy.get('div[data-test="databaseMenu"]').should("not.contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
// 1. Click last database in the resource tree
|
|
||||||
// 2. Select the context menu within the database
|
|
||||||
// 4. Select "Delete Database" option in the dropdown
|
|
||||||
// 5. On Selection, Delete Database pane opens on the right side
|
|
||||||
// 6. Enter the same database id that is to be deleted and click ok
|
|
||||||
// 7. Now, the resource tree refreshes, the deleted database should not appear in the resource tree
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Emulator - deleteDatabase", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
let db_rid = "";
|
|
||||||
const date = new Date().toUTCString();
|
|
||||||
let authToken = "";
|
|
||||||
cy.visit("http://localhost:1234/explorer.html");
|
|
||||||
|
|
||||||
// Creating auth token for collection creation
|
|
||||||
cy.request({
|
|
||||||
method: "GET",
|
|
||||||
url: "https://localhost:8081/_explorer/authorization/post/dbs/",
|
|
||||||
headers: {
|
|
||||||
"x-ms-date": date,
|
|
||||||
authorization: "-"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
authToken = response.body.Token; // Getting auth token for collection creation
|
|
||||||
return new Cypress.Promise((resolve, reject) => {
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
cy.request({
|
|
||||||
method: "POST",
|
|
||||||
url: "https://localhost:8081/dbs",
|
|
||||||
headers: {
|
|
||||||
"x-ms-date": date,
|
|
||||||
authorization: authToken,
|
|
||||||
"x-ms-version": "2018-12-31"
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
id: dbId
|
|
||||||
}
|
|
||||||
}).then(response => {
|
|
||||||
cy.log("Response", response);
|
|
||||||
db_rid = response.body._rid;
|
|
||||||
return new Cypress.Promise((resolve, reject) => {
|
|
||||||
cy.log("Rid", db_rid);
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Delete a database", () => {
|
|
||||||
cy.get('span[data-test="refreshTree"]').click();
|
|
||||||
|
|
||||||
cy.get(".databaseId")
|
|
||||||
.last()
|
|
||||||
.then($id => {
|
|
||||||
const dbId = $id.text();
|
|
||||||
|
|
||||||
cy.get('span[data-test="databaseEllipsisMenu"]').should("exist");
|
|
||||||
|
|
||||||
cy.get('span[data-test="databaseEllipsisMenu"]')
|
|
||||||
.invoke("show")
|
|
||||||
.last()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get('div[data-test="databaseContextMenu"]')
|
|
||||||
.contains("Delete Database")
|
|
||||||
.click({ force: true });
|
|
||||||
|
|
||||||
cy.get('input[data-test="confirmDatabaseId"]').type(dbId.trim());
|
|
||||||
|
|
||||||
cy.get('input[data-test="deleteDatabase"]').click();
|
|
||||||
|
|
||||||
cy.get('div[data-test="databaseList"]').should("not.contain", dbId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# Notebook end-to-end tests
|
|
||||||
This describes how to run the tests locally
|
|
||||||
|
|
||||||
## Stand up a local notebook container instance:
|
|
||||||
Instructions on how to build and run the container [here](https://microsoft.sharepoint.com/teams/DocDB/_layouts/OneNote.aspx?id=%2Fteams%2FDocDB%2FSiteAssets%2FDocDB%20Team%20Notebook&wd=target%28Tools%20_%20SDK%2FPortal%2FDevelopment.one%7CF800BE8E-1E31-48FE-90D7-EF698EF88112%2FHow%20to%20build%20notebook%20service%7C4BAA153B-422C-41E2-B997-F3FCE02CD743%2F%29)
|
|
||||||
|
|
||||||
## Run a local data explorer
|
|
||||||
Instructions are in [`DataExplorer/README.md`](https://msdata.visualstudio.com/CosmosDB/_git/cosmosdb-dataexplorer?path=%2FProduct%2FPortal%2FDataExplorer%2FREADME.md&_a=preview).
|
|
||||||
|
|
||||||
Make sure you can run Data Explorer locally from the web browser.
|
|
||||||
|
|
||||||
## Run cypress tests
|
|
||||||
1. Edit the URL for your DataExplorer in the `.spec.ts` file
|
|
||||||
2. Run the test:
|
|
||||||
```bash
|
|
||||||
cd DataExplorer/cypress
|
|
||||||
npm i
|
|
||||||
npm t -- --spec 'integration/notebook/newNotebook.spec.ts'
|
|
||||||
```
|
|
||||||
|
|
||||||
To run in Debug mode:
|
|
||||||
```
|
|
||||||
npm run test:debug
|
|
||||||
```
|
|
||||||
This opens Cypress UI
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
* The tests are recorded in the `videos` folder.
|
|
||||||
* Cypress does not support hover: workarounds [here](https://docs.cypress.io/api/commands/hover.html#Workarounds).
|
|
||||||
|
|
||||||
|
|
||||||
## References
|
|
||||||
* [Cypress API](https://docs.cypress.io/api/api/table-of-contents.html)
|
|
||||||
* [Cypress cookbook](https://docs.cypress.io/faq/questions/using-cypress-faq.html#How-do-I-get-an-element%E2%80%99s-text-contents)
|
|
||||||
* [Cypress best practices](https://docs.cypress.io/guides/references/best-practices.html#Selecting-Elements)
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
// THIS ADDS A NEW NOTEBOOK TO YOUR NOTEBOOKS
|
|
||||||
context("New Notebook smoke test", () => {
|
|
||||||
const timeout = 15000; // in ms
|
|
||||||
const explorerUrl =
|
|
||||||
"https://localhost:1234/explorer.html?feature.notebookserverurl=https%3A%2F%2Flocalhost%3A10001%2F12345%2Fnotebook&feature.notebookServerToken=token&feature.enablenotebooks=true";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for UI to be ready
|
|
||||||
*/
|
|
||||||
const waitForReady = () => {
|
|
||||||
cy.get(".splashScreenContainer", { timeout }).should("be.visible");
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit(explorerUrl);
|
|
||||||
waitForReady();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new notebook and run some code", () => {
|
|
||||||
// Create new notebook
|
|
||||||
cy.contains("New Notebook").click();
|
|
||||||
|
|
||||||
// Check tab name
|
|
||||||
cy.get("li.tabList .tabNavText").should($span => {
|
|
||||||
const text = $span.text();
|
|
||||||
expect(text).to.match(/^Untitled.*\.ipynb$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for python3 | idle status
|
|
||||||
cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should($p => {
|
|
||||||
const text = $p.text();
|
|
||||||
expect(text).to.match(/^python3.*idle$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Click on a cell
|
|
||||||
cy.get(".cell-container")
|
|
||||||
.as("cellContainer")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Type in some code
|
|
||||||
cy.get("@cellContainer").type("2+4");
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
cy.get('[data-test="Run"]')
|
|
||||||
.first()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Verify results
|
|
||||||
cy.get("@cellContainer").within(() => {
|
|
||||||
cy.get("pre code span").should("contain", "6");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Restart kernel
|
|
||||||
cy.get('[data-test="Run"] button')
|
|
||||||
.eq(-1)
|
|
||||||
.click();
|
|
||||||
cy.get("li")
|
|
||||||
.contains("Restart Kernel")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Wait for python3 | restarting status
|
|
||||||
cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should($p => {
|
|
||||||
const text = $p.text();
|
|
||||||
expect(text).to.match(/^python3.*restarting$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for python3 | idle status
|
|
||||||
cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should($p => {
|
|
||||||
const text = $p.text();
|
|
||||||
expect(text).to.match(/^python3.*idle$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Click on a cell
|
|
||||||
cy.get(".cell-container")
|
|
||||||
.as("cellContainer")
|
|
||||||
.find(".input")
|
|
||||||
.as("codeInput")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Type in some code
|
|
||||||
cy.get("@codeInput").type("{backspace}{backspace}{backspace}4+5");
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
cy.get('[data-test="Run"]')
|
|
||||||
.first()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Verify results
|
|
||||||
cy.get("@cellContainer").within(() => {
|
|
||||||
cy.get("pre code span").should("contain", "9");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
context("Resource tree notebook file manipulation", () => {
|
|
||||||
const timeout = 15000; // in ms
|
|
||||||
const explorerUrl =
|
|
||||||
"https://localhost:1234/explorer.html?feature.notebookserverurl=https%3A%2F%2Flocalhost%3A10001%2F12345%2Fnotebook&feature.notebookServerToken=token&feature.enablenotebooks=true";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for UI to be ready
|
|
||||||
*/
|
|
||||||
const waitForReady = () => {
|
|
||||||
cy.get(".splashScreenContainer", { timeout }).should("be.visible");
|
|
||||||
};
|
|
||||||
|
|
||||||
const clickContextMenuAndSelectOption = (nodeLabel, option) => {
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${nodeLabel}"]`)
|
|
||||||
.find("button.treeMenuEllipsis")
|
|
||||||
.click();
|
|
||||||
cy.get('[data-test="treeComponentMenuItemContainer"]')
|
|
||||||
.contains(option)
|
|
||||||
.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
const createFolder = folder => {
|
|
||||||
clickContextMenuAndSelectOption("My Notebooks/", "New Directory");
|
|
||||||
|
|
||||||
cy.get("#stringInputPane").within(() => {
|
|
||||||
cy.get('input[name="collectionIdConfirmation"]').type(folder);
|
|
||||||
cy.get("form").submit();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteItem = nodeName => {
|
|
||||||
clickContextMenuAndSelectOption(`${nodeName}`, "Delete");
|
|
||||||
cy.get(".ms-Dialog-main")
|
|
||||||
.contains("Delete")
|
|
||||||
.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit(explorerUrl);
|
|
||||||
waitForReady();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create and remove a directory", () => {
|
|
||||||
const folder = "e2etest_folder1";
|
|
||||||
createFolder(folder);
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).should("exist");
|
|
||||||
deleteItem(`${folder}/`);
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).should("not.exist");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create and rename a directory", () => {
|
|
||||||
const folder = "e2etest_folder2";
|
|
||||||
const renamedFolder = "e2etest_folder2_renamed";
|
|
||||||
createFolder(folder);
|
|
||||||
|
|
||||||
// Rename
|
|
||||||
clickContextMenuAndSelectOption(`${folder}/`, "Rename");
|
|
||||||
cy.get("#stringInputPane").within(() => {
|
|
||||||
cy.get('input[name="collectionIdConfirmation"]')
|
|
||||||
.clear()
|
|
||||||
.type(renamedFolder);
|
|
||||||
cy.get("form").submit();
|
|
||||||
});
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${renamedFolder}/"]`).should("exist");
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).should("not.exist");
|
|
||||||
|
|
||||||
deleteItem(`${renamedFolder}/`);
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${renamedFolder}/"]`).should("not.exist");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a notebook inside a directory", () => {
|
|
||||||
const folder = "e2etest_folder3";
|
|
||||||
const newNotebookName = "Untitled.ipynb";
|
|
||||||
createFolder(folder);
|
|
||||||
clickContextMenuAndSelectOption(`${folder}/`, "New Notebook");
|
|
||||||
|
|
||||||
// Verify tab is open
|
|
||||||
cy.get(".tabList")
|
|
||||||
.contains(newNotebookName)
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
// Close tab
|
|
||||||
cy.get(`.tabList[title="notebooks/${folder}/${newNotebookName}"]`)
|
|
||||||
.find(".cancelButton")
|
|
||||||
.click();
|
|
||||||
// When running from command line, closing the tab is too fast
|
|
||||||
cy.get("body").then($body => {
|
|
||||||
if ($body.find(".ms-Dialog-main").length) {
|
|
||||||
// For some reason, this does not work
|
|
||||||
// cy.get(".ms-Dialog-main").contains("Close").click();
|
|
||||||
cy.get(".ms-Dialog-main .ms-Button--primary").click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Expand folder node
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).click();
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("exist");
|
|
||||||
|
|
||||||
// Delete notebook
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`)
|
|
||||||
.find("button.treeMenuEllipsis")
|
|
||||||
.click();
|
|
||||||
cy.get('[data-test="treeComponentMenuItemContainer"]')
|
|
||||||
.contains("Delete")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Confirm
|
|
||||||
cy.get(".ms-Dialog-main")
|
|
||||||
.contains("Delete")
|
|
||||||
.click();
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("not.exist");
|
|
||||||
|
|
||||||
deleteItem(`${folder}/`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create and rename a notebook inside a directory", () => {
|
|
||||||
const folder = "e2etest_folder4";
|
|
||||||
const newNotebookName = "Untitled.ipynb";
|
|
||||||
const renamedNotebookName = "mynotebook.ipynb";
|
|
||||||
createFolder(folder);
|
|
||||||
clickContextMenuAndSelectOption(`${folder}/`, "New Notebook");
|
|
||||||
|
|
||||||
// Close tab
|
|
||||||
cy.get(`.tabList[title="notebooks/${folder}/${newNotebookName}"]`)
|
|
||||||
.find(".cancelButton")
|
|
||||||
.click();
|
|
||||||
cy.get("body").then($body => {
|
|
||||||
if ($body.find(".ms-Dialog-main").length) {
|
|
||||||
// For some reason, this does not work
|
|
||||||
// cy.get(".ms-Dialog-main").contains("Close").click();
|
|
||||||
cy.get(".ms-Dialog-main .ms-Button--primary").click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Expand folder node
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).click();
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("exist");
|
|
||||||
|
|
||||||
// Rename notebook
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`)
|
|
||||||
.find("button.treeMenuEllipsis")
|
|
||||||
.click();
|
|
||||||
cy.get('[data-test="treeComponentMenuItemContainer"]')
|
|
||||||
.contains("Rename")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get("#stringInputPane").within(() => {
|
|
||||||
cy.get('input[name="collectionIdConfirmation"]')
|
|
||||||
.clear()
|
|
||||||
.type(renamedNotebookName);
|
|
||||||
cy.get("form").submit();
|
|
||||||
});
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("not.exist");
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${renamedNotebookName}"]`).should("exist");
|
|
||||||
|
|
||||||
// Delete notebook
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${renamedNotebookName}"]`)
|
|
||||||
.find("button.treeMenuEllipsis")
|
|
||||||
.click();
|
|
||||||
cy.get('[data-test="treeComponentMenuItemContainer"]')
|
|
||||||
.contains("Delete")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Confirm
|
|
||||||
cy.get(".ms-Dialog-main")
|
|
||||||
.contains("Delete")
|
|
||||||
.click();
|
|
||||||
// Give it time to settle
|
|
||||||
cy.wait(1000);
|
|
||||||
deleteItem(`${folder}/`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
3066
cypress/package-lock.json
generated
3066
cypress/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "cosmos-explorer-cypress",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "cypress run",
|
|
||||||
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
|
|
||||||
"test:sql": "cypress run --browser chrome --spec \"./integration/dataexplorer/SQL/*\"",
|
|
||||||
"test:ci": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/ https-get://0.0.0.0:8081/_explorer/index.html && cypress run --browser edge --headless",
|
|
||||||
"test:debug": "cypress open"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"cypress": "^4.8.0",
|
|
||||||
"mocha": "^7.0.1",
|
|
||||||
"mochawesome": "^4.1.0",
|
|
||||||
"mochawesome-merge": "^4.0.1",
|
|
||||||
"mochawesome-report-generator": "^4.1.0",
|
|
||||||
"typescript": "3.4.3",
|
|
||||||
"wait-on": "^4.0.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@microsoft/applicationinsights-web": "^2.5.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
let appInsightsLib = require("@microsoft/applicationinsights-web");
|
|
||||||
|
|
||||||
const appInsights = new appInsightsLib.ApplicationInsights({
|
|
||||||
config: {
|
|
||||||
instrumentationKey: "fe61c39f-7d32-4488-a191-b13621965315"
|
|
||||||
/* ...Other Configuration Options... */
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
appInsights.loadAppInsights();
|
|
||||||
|
|
||||||
Cypress.on("fail", (error, runnable) => {
|
|
||||||
// App Insights will record the fail tests for Create Collection
|
|
||||||
let message = JSON.stringify(runnable.title);
|
|
||||||
appInsights.trackTrace({
|
|
||||||
message: `${message}`,
|
|
||||||
properties: {
|
|
||||||
passed: false,
|
|
||||||
error: error
|
|
||||||
}
|
|
||||||
});
|
|
||||||
throw error; // throw error to have test still fail
|
|
||||||
});
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"strict": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"module": "commonjs",
|
|
||||||
"target": "es5",
|
|
||||||
"lib": ["es5", "dom", "es6"],
|
|
||||||
"types": ["cypress", "node"]
|
|
||||||
},
|
|
||||||
"include": ["**/*.ts", "**/*.spec.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
loginUsingConnectionString: function() {
|
|
||||||
const prodUrl = Cypress.env("TEST_ENDPOINT") || "https://localhost:1234/hostedExplorer.html";
|
|
||||||
const timeout = 15000;
|
|
||||||
|
|
||||||
cy.visit(prodUrl);
|
|
||||||
cy.get('iframe[id="explorerMenu"]').should("be.visible");
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find("#connectExplorer")
|
|
||||||
.should("exist")
|
|
||||||
.find("div[class='connectExplorer']")
|
|
||||||
.should("exist")
|
|
||||||
.find("p[class='welcomeText']")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
cy.wrap($body.find("div > p.switchConnectTypeText"))
|
|
||||||
.should("exist")
|
|
||||||
.last()
|
|
||||||
.click({ force: true });
|
|
||||||
|
|
||||||
const secret = Cypress.env("CONNECTION_STRING");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find("input[class='inputToken']")
|
|
||||||
.should("exist")
|
|
||||||
.type(secret, {
|
|
||||||
force: true
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.wrap($body.find("input[value='Connect']"), { timeout })
|
|
||||||
.first()
|
|
||||||
.click({ force: true });
|
|
||||||
|
|
||||||
cy.wait(15000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
const { CosmosClient } = require("@azure/cosmos");
|
|
||||||
|
|
||||||
module.exports = new CosmosClient({
|
|
||||||
endpoint: "https://0.0.0.0:8081",
|
|
||||||
key: "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
|
|
||||||
});
|
|
||||||
4628
package-lock.json
generated
4628
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -4,15 +4,17 @@
|
|||||||
"description": "Cosmos Explorer",
|
"description": "Cosmos Explorer",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@azure/arm-cosmosdb": "9.1.0",
|
||||||
"@azure/cosmos": "3.9.0",
|
"@azure/cosmos": "3.9.0",
|
||||||
"@azure/cosmos-language-service": "0.0.4",
|
"@azure/cosmos-language-service": "0.0.5",
|
||||||
|
"@azure/identity": "1.1.0",
|
||||||
"@jupyterlab/services": "6.0.0-rc.2",
|
"@jupyterlab/services": "6.0.0-rc.2",
|
||||||
"@jupyterlab/terminal": "3.0.0-rc.2",
|
"@jupyterlab/terminal": "3.0.0-rc.2",
|
||||||
"@microsoft/applicationinsights-web": "2.5.9",
|
"@microsoft/applicationinsights-web": "2.5.9",
|
||||||
"@nteract/commutable": "7.3.2",
|
"@nteract/commutable": "7.3.2",
|
||||||
"@nteract/connected-components": "6.8.2",
|
"@nteract/connected-components": "6.8.2",
|
||||||
"@nteract/core": "15.1.0",
|
"@nteract/core": "15.1.0",
|
||||||
"@nteract/data-explorer": "8.0.3",
|
"@nteract/data-explorer": "8.2.9",
|
||||||
"@nteract/directory-listing": "2.0.6",
|
"@nteract/directory-listing": "2.0.6",
|
||||||
"@nteract/dropdown-menu": "1.0.1",
|
"@nteract/dropdown-menu": "1.0.1",
|
||||||
"@nteract/editor": "10.1.2",
|
"@nteract/editor": "10.1.2",
|
||||||
@@ -66,7 +68,7 @@
|
|||||||
"jquery-ui-dist": "1.12.1",
|
"jquery-ui-dist": "1.12.1",
|
||||||
"knockout": "3.5.1",
|
"knockout": "3.5.1",
|
||||||
"mkdirp": "1.0.4",
|
"mkdirp": "1.0.4",
|
||||||
"monaco-editor": "0.15.6",
|
"monaco-editor": "0.18.1",
|
||||||
"object.entries": "1.1.0",
|
"object.entries": "1.1.0",
|
||||||
"office-ui-fabric-react": "7.134.1",
|
"office-ui-fabric-react": "7.134.1",
|
||||||
"p-retry": "4.2.0",
|
"p-retry": "4.2.0",
|
||||||
@@ -115,7 +117,7 @@
|
|||||||
"@types/prop-types": "15.5.8",
|
"@types/prop-types": "15.5.8",
|
||||||
"@types/puppeteer": "3.0.1",
|
"@types/puppeteer": "3.0.1",
|
||||||
"@types/q": "1.5.1",
|
"@types/q": "1.5.1",
|
||||||
"@types/react": "16.9.49",
|
"@types/react": "16.9.56",
|
||||||
"@types/react-dom": "16.0.7",
|
"@types/react-dom": "16.0.7",
|
||||||
"@types/react-notification-system": "0.2.39",
|
"@types/react-notification-system": "0.2.39",
|
||||||
"@types/react-redux": "7.1.7",
|
"@types/react-redux": "7.1.7",
|
||||||
@@ -194,8 +196,8 @@
|
|||||||
"compile": "tsc",
|
"compile": "tsc",
|
||||||
"compile:contracts": "tsc -p ./tsconfig.contracts.json",
|
"compile:contracts": "tsc -p ./tsconfig.contracts.json",
|
||||||
"compile:strict": "tsc -p ./tsconfig.strict.json",
|
"compile:strict": "tsc -p ./tsconfig.strict.json",
|
||||||
"format": "prettier --write \"{src,cypress,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
"format": "prettier --write \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
||||||
"format:check": "prettier --check \"{src,cypress,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
"format:check": "prettier --check \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
||||||
"lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"",
|
"lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"",
|
||||||
"build:contracts": "npm run compile:contracts",
|
"build:contracts": "npm run compile:contracts",
|
||||||
"strictEligibleFiles": "node ./strict-migration-tools/index.js",
|
"strictEligibleFiles": "node ./strict-migration-tools/index.js",
|
||||||
|
|||||||
@@ -125,7 +125,9 @@ 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,26 +1,13 @@
|
|||||||
import {
|
import { ConflictDefinition, FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
ConflictDefinition,
|
|
||||||
FeedOptions,
|
|
||||||
ItemDefinition,
|
|
||||||
OfferDefinition,
|
|
||||||
QueryIterator,
|
|
||||||
Resource
|
|
||||||
} from "@azure/cosmos";
|
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
|
||||||
import Q from "q";
|
import Q from "q";
|
||||||
import { configContext, Platform } from "../ConfigContext";
|
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import ConflictId from "../Explorer/Tree/ConflictId";
|
import ConflictId from "../Explorer/Tree/ConflictId";
|
||||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||||
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
||||||
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
|
||||||
import { OfferUtils } from "../Utils/OfferUtils";
|
|
||||||
import * as Constants from "./Constants";
|
import * as Constants from "./Constants";
|
||||||
import { client } from "./CosmosClient";
|
import { client } from "./CosmosClient";
|
||||||
import * as HeadersUtility from "./HeadersUtility";
|
|
||||||
import { sendCachedDataMessage } from "./MessageHandler";
|
|
||||||
|
|
||||||
export function getCommonQueryOptions(options: FeedOptions): any {
|
export function getCommonQueryOptions(options: FeedOptions): any {
|
||||||
const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage);
|
const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ARMError } from "../Utils/arm/request";
|
import { ARMError } from "../Utils/arm/request";
|
||||||
import { HttpStatusCodes } from "./Constants";
|
import { HttpStatusCodes } from "./Constants";
|
||||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||||
import { SubscriptionType } from "../Contracts/ViewModels";
|
import { SubscriptionType } from "../Contracts/SubscriptionType";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import { logError } from "./Logger";
|
import { logError } from "./Logger";
|
||||||
import { sendMessage } from "./MessageHandler";
|
import { sendMessage } from "./MessageHandler";
|
||||||
|
|||||||
62
src/Common/OfferUtility.test.ts
Normal file
62
src/Common/OfferUtility.test.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import * as OfferUtility from "./OfferUtility";
|
||||||
|
import { SDKOfferDefinition, Offer } from "../Contracts/DataModels";
|
||||||
|
import { OfferResponse } from "@azure/cosmos";
|
||||||
|
|
||||||
|
describe("parseSDKOfferResponse", () => {
|
||||||
|
it("manual throughput", () => {
|
||||||
|
const mockOfferDefinition = {
|
||||||
|
content: {
|
||||||
|
offerThroughput: 500,
|
||||||
|
collectionThroughputInfo: {
|
||||||
|
minimumRUForCollection: 400,
|
||||||
|
numPhysicalPartitions: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
id: "test"
|
||||||
|
} as SDKOfferDefinition;
|
||||||
|
|
||||||
|
const mockResponse = {
|
||||||
|
resource: mockOfferDefinition
|
||||||
|
} as OfferResponse;
|
||||||
|
|
||||||
|
const expectedResult: Offer = {
|
||||||
|
manualThroughput: 500,
|
||||||
|
autoscaleMaxThroughput: undefined,
|
||||||
|
minimumThroughput: 400,
|
||||||
|
id: "test",
|
||||||
|
offerDefinition: mockOfferDefinition
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("autoscale throughput", () => {
|
||||||
|
const mockOfferDefinition = {
|
||||||
|
content: {
|
||||||
|
offerThroughput: 400,
|
||||||
|
collectionThroughputInfo: {
|
||||||
|
minimumRUForCollection: 400,
|
||||||
|
numPhysicalPartitions: 1
|
||||||
|
},
|
||||||
|
offerAutopilotSettings: {
|
||||||
|
maxThroughput: 5000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
id: "test"
|
||||||
|
} as SDKOfferDefinition;
|
||||||
|
|
||||||
|
const mockResponse = {
|
||||||
|
resource: mockOfferDefinition
|
||||||
|
} as OfferResponse;
|
||||||
|
|
||||||
|
const expectedResult: Offer = {
|
||||||
|
manualThroughput: undefined,
|
||||||
|
autoscaleMaxThroughput: 5000,
|
||||||
|
minimumThroughput: 400,
|
||||||
|
id: "test",
|
||||||
|
offerDefinition: mockOfferDefinition
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
33
src/Common/OfferUtility.ts
Normal file
33
src/Common/OfferUtility.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Offer, SDKOfferDefinition } from "../Contracts/DataModels";
|
||||||
|
import { OfferResponse } from "@azure/cosmos";
|
||||||
|
|
||||||
|
export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => {
|
||||||
|
const offerDefinition: SDKOfferDefinition = offerResponse?.resource;
|
||||||
|
const offerContent = offerDefinition.content;
|
||||||
|
if (!offerContent) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const minimumThroughput = offerContent.collectionThroughputInfo?.minimumRUForCollection;
|
||||||
|
const autopilotSettings = offerContent.offerAutopilotSettings;
|
||||||
|
|
||||||
|
if (autopilotSettings) {
|
||||||
|
return {
|
||||||
|
id: offerDefinition.id,
|
||||||
|
autoscaleMaxThroughput: autopilotSettings.maxThroughput,
|
||||||
|
manualThroughput: undefined,
|
||||||
|
minimumThroughput,
|
||||||
|
offerDefinition,
|
||||||
|
headers: offerResponse.headers
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: offerDefinition.id,
|
||||||
|
autoscaleMaxThroughput: undefined,
|
||||||
|
manualThroughput: offerContent.offerThroughput,
|
||||||
|
minimumThroughput,
|
||||||
|
offerDefinition,
|
||||||
|
headers: offerResponse.headers
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -16,7 +16,7 @@ const notificationsPath = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const fetchPortalNotifications = async (): Promise<DataModels.Notification[]> => {
|
export const fetchPortalNotifications = async (): Promise<DataModels.Notification[]> => {
|
||||||
if (configContext.platform === Platform.Emulator) {
|
if (configContext.platform === Platform.Emulator || configContext.platform === Platform.Hosted) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import * as DataModels from "../../Contracts/DataModels";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
import { HttpHeaders } from "../Constants";
|
import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels";
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
|
||||||
import { client } from "../CosmosClient";
|
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
import { getSqlContainerThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
import { getSqlContainerThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||||
import { getMongoDBCollectionThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
import { getMongoDBCollectionThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||||
@@ -11,50 +8,22 @@ import { getCassandraTableThroughput } from "../../Utils/arm/generatedClients/20
|
|||||||
import { getGremlinGraphThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
import { getGremlinGraphThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||||
import { getTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
|
import { getTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { readOffers } from "./readOffers";
|
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export const readCollectionOffer = async (
|
export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise<Offer> => {
|
||||||
params: DataModels.ReadCollectionOfferParams
|
|
||||||
): Promise<DataModels.OfferWithHeaders> => {
|
|
||||||
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
|
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
|
||||||
let offerId = params.offerId;
|
|
||||||
if (!offerId) {
|
|
||||||
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
|
||||||
try {
|
|
||||||
offerId = await getCollectionOfferIdWithARM(params.databaseId, params.collectionId);
|
|
||||||
} catch (error) {
|
|
||||||
clearMessage();
|
|
||||||
if (error.code !== "NotFound") {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
offerId = await getCollectionOfferIdWithSDK(params.collectionResourceId);
|
|
||||||
if (!offerId) {
|
|
||||||
clearMessage();
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: RequestOptions = {
|
|
||||||
initialHeaders: {
|
|
||||||
[HttpHeaders.populateCollectionThroughputInfo]: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await client()
|
if (
|
||||||
.offer(offerId)
|
window.authType === AuthType.AAD &&
|
||||||
.read(options);
|
!userContext.useSDKOperations &&
|
||||||
return (
|
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||||
response && {
|
) {
|
||||||
...response.resource,
|
return await readCollectionOfferWithARM(params.databaseId, params.collectionId);
|
||||||
headers: response.headers
|
}
|
||||||
}
|
|
||||||
);
|
return await readOfferWithSDK(params.offerId, params.collectionResourceId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "ReadCollectionOffer", `Error while querying offer for collection ${params.collectionId}`);
|
handleError(error, "ReadCollectionOffer", `Error while querying offer for collection ${params.collectionId}`);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -63,61 +32,90 @@ export const readCollectionOffer = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCollectionOfferIdWithARM = async (databaseId: string, collectionId: string): Promise<string> => {
|
const readCollectionOfferWithARM = async (databaseId: string, collectionId: string): Promise<Offer> => {
|
||||||
let rpResponse;
|
|
||||||
const subscriptionId = userContext.subscriptionId;
|
const subscriptionId = userContext.subscriptionId;
|
||||||
const resourceGroup = userContext.resourceGroup;
|
const resourceGroup = userContext.resourceGroup;
|
||||||
const accountName = userContext.databaseAccount.name;
|
const accountName = userContext.databaseAccount.name;
|
||||||
const defaultExperience = userContext.defaultExperience;
|
const defaultExperience = userContext.defaultExperience;
|
||||||
switch (defaultExperience) {
|
|
||||||
case DefaultAccountExperienceType.DocumentDB:
|
let rpResponse;
|
||||||
rpResponse = await getSqlContainerThroughput(
|
try {
|
||||||
subscriptionId,
|
switch (defaultExperience) {
|
||||||
resourceGroup,
|
case DefaultAccountExperienceType.DocumentDB:
|
||||||
accountName,
|
rpResponse = await getSqlContainerThroughput(
|
||||||
databaseId,
|
subscriptionId,
|
||||||
collectionId
|
resourceGroup,
|
||||||
);
|
accountName,
|
||||||
break;
|
databaseId,
|
||||||
case DefaultAccountExperienceType.MongoDB:
|
collectionId
|
||||||
rpResponse = await getMongoDBCollectionThroughput(
|
);
|
||||||
subscriptionId,
|
break;
|
||||||
resourceGroup,
|
case DefaultAccountExperienceType.MongoDB:
|
||||||
accountName,
|
rpResponse = await getMongoDBCollectionThroughput(
|
||||||
databaseId,
|
subscriptionId,
|
||||||
collectionId
|
resourceGroup,
|
||||||
);
|
accountName,
|
||||||
break;
|
databaseId,
|
||||||
case DefaultAccountExperienceType.Cassandra:
|
collectionId
|
||||||
rpResponse = await getCassandraTableThroughput(
|
);
|
||||||
subscriptionId,
|
break;
|
||||||
resourceGroup,
|
case DefaultAccountExperienceType.Cassandra:
|
||||||
accountName,
|
rpResponse = await getCassandraTableThroughput(
|
||||||
databaseId,
|
subscriptionId,
|
||||||
collectionId
|
resourceGroup,
|
||||||
);
|
accountName,
|
||||||
break;
|
databaseId,
|
||||||
case DefaultAccountExperienceType.Graph:
|
collectionId
|
||||||
rpResponse = await getGremlinGraphThroughput(
|
);
|
||||||
subscriptionId,
|
break;
|
||||||
resourceGroup,
|
case DefaultAccountExperienceType.Graph:
|
||||||
accountName,
|
rpResponse = await getGremlinGraphThroughput(
|
||||||
databaseId,
|
subscriptionId,
|
||||||
collectionId
|
resourceGroup,
|
||||||
);
|
accountName,
|
||||||
break;
|
databaseId,
|
||||||
case DefaultAccountExperienceType.Table:
|
collectionId
|
||||||
rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
case DefaultAccountExperienceType.Table:
|
||||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== "NotFound") {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return rpResponse?.name;
|
const resource = rpResponse?.properties?.resource;
|
||||||
};
|
if (resource) {
|
||||||
|
const offerId: string = rpResponse.name;
|
||||||
|
const minimumThroughput: number =
|
||||||
|
typeof resource.minimumThroughput === "string"
|
||||||
|
? parseInt(resource.minimumThroughput)
|
||||||
|
: resource.minimumThroughput;
|
||||||
|
const autoscaleSettings = resource.autoscaleSettings;
|
||||||
|
|
||||||
const getCollectionOfferIdWithSDK = async (collectionResourceId: string): Promise<string> => {
|
if (autoscaleSettings) {
|
||||||
const offers = await readOffers();
|
return {
|
||||||
const offer = offers.find(offer => offer.resource === collectionResourceId);
|
id: offerId,
|
||||||
return offer?.id;
|
autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
|
||||||
|
manualThroughput: undefined,
|
||||||
|
minimumThroughput
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: offerId,
|
||||||
|
autoscaleMaxThroughput: undefined,
|
||||||
|
manualThroughput: resource.throughput,
|
||||||
|
minimumThroughput
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,51 +1,28 @@
|
|||||||
import * as DataModels from "../../Contracts/DataModels";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
import { HttpHeaders } from "../Constants";
|
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
|
||||||
import { client } from "../CosmosClient";
|
|
||||||
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||||
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||||
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
||||||
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { readOffers } from "./readOffers";
|
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export const readDatabaseOffer = async (
|
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
|
||||||
params: DataModels.ReadDatabaseOfferParams
|
|
||||||
): Promise<DataModels.OfferWithHeaders> => {
|
|
||||||
const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`);
|
const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`);
|
||||||
let offerId = params.offerId;
|
|
||||||
if (!offerId) {
|
|
||||||
offerId = await (window.authType === AuthType.AAD &&
|
|
||||||
!userContext.useSDKOperations &&
|
|
||||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
|
||||||
? getDatabaseOfferIdWithARM(params.databaseId)
|
|
||||||
: getDatabaseOfferIdWithSDK(params.databaseResourceId));
|
|
||||||
if (!offerId) {
|
|
||||||
clearMessage();
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: RequestOptions = {
|
|
||||||
initialHeaders: {
|
|
||||||
[HttpHeaders.populateCollectionThroughputInfo]: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await client()
|
if (
|
||||||
.offer(offerId)
|
window.authType === AuthType.AAD &&
|
||||||
.read(options);
|
!userContext.useSDKOperations &&
|
||||||
return (
|
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||||
response && {
|
) {
|
||||||
...response.resource,
|
return await readDatabaseOfferWithARM(params.databaseId);
|
||||||
headers: response.headers
|
}
|
||||||
}
|
|
||||||
);
|
return await readOfferWithSDK(params.offerId, params.databaseResourceId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "ReadDatabaseOffer", `Error while querying offer for database ${params.databaseId}`);
|
handleError(error, "ReadDatabaseOffer", `Error while querying offer for database ${params.databaseId}`);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -54,13 +31,13 @@ export const readDatabaseOffer = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> => {
|
const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
|
||||||
let rpResponse;
|
|
||||||
const subscriptionId = userContext.subscriptionId;
|
const subscriptionId = userContext.subscriptionId;
|
||||||
const resourceGroup = userContext.resourceGroup;
|
const resourceGroup = userContext.resourceGroup;
|
||||||
const accountName = userContext.databaseAccount.name;
|
const accountName = userContext.databaseAccount.name;
|
||||||
const defaultExperience = userContext.defaultExperience;
|
const defaultExperience = userContext.defaultExperience;
|
||||||
|
|
||||||
|
let rpResponse;
|
||||||
try {
|
try {
|
||||||
switch (defaultExperience) {
|
switch (defaultExperience) {
|
||||||
case DefaultAccountExperienceType.DocumentDB:
|
case DefaultAccountExperienceType.DocumentDB:
|
||||||
@@ -78,18 +55,39 @@ const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> =>
|
|||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return rpResponse?.name;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code !== "NotFound") {
|
if (error.code !== "NotFound") {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const getDatabaseOfferIdWithSDK = async (databaseResourceId: string): Promise<string> => {
|
const resource = rpResponse?.properties?.resource;
|
||||||
const offers = await readOffers();
|
if (resource) {
|
||||||
const offer = offers.find(offer => offer.resource === databaseResourceId);
|
const offerId: string = rpResponse.name;
|
||||||
return offer?.id;
|
const minimumThroughput: number =
|
||||||
|
typeof resource.minimumThroughput === "string"
|
||||||
|
? parseInt(resource.minimumThroughput)
|
||||||
|
: resource.minimumThroughput;
|
||||||
|
const autoscaleSettings = resource.autoscaleSettings;
|
||||||
|
|
||||||
|
if (autoscaleSettings) {
|
||||||
|
return {
|
||||||
|
id: offerId,
|
||||||
|
autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
|
||||||
|
manualThroughput: undefined,
|
||||||
|
minimumThroughput
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: offerId,
|
||||||
|
autoscaleMaxThroughput: undefined,
|
||||||
|
manualThroughput: resource.throughput,
|
||||||
|
minimumThroughput
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|||||||
29
src/Common/dataAccess/readOfferWithSDK.ts
Normal file
29
src/Common/dataAccess/readOfferWithSDK.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { HttpHeaders } from "../Constants";
|
||||||
|
import { Offer } from "../../Contracts/DataModels";
|
||||||
|
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { parseSDKOfferResponse } from "../OfferUtility";
|
||||||
|
import { readOffers } from "./readOffers";
|
||||||
|
|
||||||
|
export const readOfferWithSDK = async (offerId: string, resourceId: string): Promise<Offer> => {
|
||||||
|
if (!offerId) {
|
||||||
|
const offers = await readOffers();
|
||||||
|
const offer = offers.find(offer => offer.resource === resourceId);
|
||||||
|
|
||||||
|
if (!offer) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
offerId = offer.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: RequestOptions = {
|
||||||
|
initialHeaders: {
|
||||||
|
[HttpHeaders.populateCollectionThroughputInfo]: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const response = await client()
|
||||||
|
.offer(offerId)
|
||||||
|
.read(options);
|
||||||
|
|
||||||
|
return parseSDKOfferResponse(response);
|
||||||
|
};
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Offer } from "../../Contracts/DataModels";
|
import { SDKOfferDefinition } from "../../Contracts/DataModels";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { handleError, getErrorMessage } from "../ErrorHandlingUtils";
|
import { handleError, getErrorMessage } from "../ErrorHandlingUtils";
|
||||||
|
|
||||||
export const readOffers = async (): Promise<Offer[]> => {
|
export const readOffers = async (): Promise<SDKOfferDefinition[]> => {
|
||||||
const clearMessage = logConsoleProgress(`Querying offers`);
|
const clearMessage = logConsoleProgress(`Querying offers`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
import { HttpHeaders } from "../Constants";
|
import { HttpHeaders } from "../Constants";
|
||||||
import { Offer, UpdateOfferParams } from "../../Contracts/DataModels";
|
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||||
import { OfferDefinition } from "@azure/cosmos";
|
import { OfferDefinition } from "@azure/cosmos";
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||||
import { ThroughputSettingsUpdateParameters } from "../../Utils/arm/generatedClients/2020-04-01/types";
|
import { ThroughputSettingsUpdateParameters } from "../../Utils/arm/generatedClients/2020-04-01/types";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import { parseSDKOfferResponse } from "../OfferUtility";
|
||||||
import { readCollectionOffer } from "./readCollectionOffer";
|
import { readCollectionOffer } from "./readCollectionOffer";
|
||||||
import { readDatabaseOffer } from "./readDatabaseOffer";
|
import { readDatabaseOffer } from "./readDatabaseOffer";
|
||||||
import {
|
import {
|
||||||
@@ -373,21 +374,21 @@ const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpd
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> => {
|
const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> => {
|
||||||
const currentOffer = params.currentOffer;
|
const sdkOfferDefinition = params.currentOffer.offerDefinition;
|
||||||
const newOffer: Offer = {
|
const newOffer: SDKOfferDefinition = {
|
||||||
content: {
|
content: {
|
||||||
offerThroughput: undefined,
|
offerThroughput: undefined,
|
||||||
offerIsRUPerMinuteThroughputEnabled: false
|
offerIsRUPerMinuteThroughputEnabled: false
|
||||||
},
|
},
|
||||||
_etag: undefined,
|
_etag: undefined,
|
||||||
_ts: undefined,
|
_ts: undefined,
|
||||||
_rid: currentOffer._rid,
|
_rid: sdkOfferDefinition._rid,
|
||||||
_self: currentOffer._self,
|
_self: sdkOfferDefinition._self,
|
||||||
id: currentOffer.id,
|
id: sdkOfferDefinition.id,
|
||||||
offerResourceId: currentOffer.offerResourceId,
|
offerResourceId: sdkOfferDefinition.offerResourceId,
|
||||||
offerVersion: currentOffer.offerVersion,
|
offerVersion: sdkOfferDefinition.offerVersion,
|
||||||
offerType: currentOffer.offerType,
|
offerType: sdkOfferDefinition.offerType,
|
||||||
resource: currentOffer.resource
|
resource: sdkOfferDefinition.resource
|
||||||
};
|
};
|
||||||
|
|
||||||
if (params.autopilotThroughput) {
|
if (params.autopilotThroughput) {
|
||||||
@@ -415,5 +416,6 @@ const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> =>
|
|||||||
.offer(params.currentOffer.id)
|
.offer(params.currentOffer.id)
|
||||||
// TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660)
|
// TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660)
|
||||||
.replace((newOffer as unknown) as OfferDefinition, options);
|
.replace((newOffer as unknown) as OfferDefinition, options);
|
||||||
return sdkResponse?.resource;
|
|
||||||
|
return parseSDKOfferResponse(sdkResponse);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { updateOfferThroughputBeyondLimit } from "./updateOfferThroughputBeyondLimit";
|
|
||||||
|
|
||||||
describe("updateOfferThroughputBeyondLimit", () => {
|
|
||||||
it("should call fetch", async () => {
|
|
||||||
window.fetch = jest.fn(() => {
|
|
||||||
return {
|
|
||||||
ok: true
|
|
||||||
};
|
|
||||||
});
|
|
||||||
window.dataExplorer = {
|
|
||||||
logConsoleData: jest.fn(),
|
|
||||||
deleteInProgressConsoleDataWithId: jest.fn()
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
} as any;
|
|
||||||
await updateOfferThroughputBeyondLimit({
|
|
||||||
subscriptionId: "foo",
|
|
||||||
resourceGroup: "foo",
|
|
||||||
databaseAccountName: "foo",
|
|
||||||
databaseName: "foo",
|
|
||||||
throughput: 1000000000,
|
|
||||||
offerIsRUPerMinuteThroughputEnabled: false
|
|
||||||
});
|
|
||||||
expect(window.fetch).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import { Platform, configContext } from "../../ConfigContext";
|
|
||||||
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
|
||||||
import { AutoPilotOfferSettings } from "../../Contracts/DataModels";
|
|
||||||
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
|
||||||
import { HttpHeaders } from "../Constants";
|
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
|
||||||
|
|
||||||
interface UpdateOfferThroughputRequest {
|
|
||||||
subscriptionId: string;
|
|
||||||
resourceGroup: string;
|
|
||||||
databaseAccountName: string;
|
|
||||||
databaseName: string;
|
|
||||||
collectionName?: string;
|
|
||||||
throughput: number;
|
|
||||||
offerIsRUPerMinuteThroughputEnabled: boolean;
|
|
||||||
offerAutopilotSettings?: AutoPilotOfferSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateOfferThroughputBeyondLimit(request: UpdateOfferThroughputRequest): Promise<void> {
|
|
||||||
if (configContext.platform !== Platform.Portal) {
|
|
||||||
throw new Error("Updating throughput beyond specified limit is not supported on this platform");
|
|
||||||
}
|
|
||||||
|
|
||||||
const resourceDescriptionInfo = request.collectionName
|
|
||||||
? `database ${request.databaseName} and container ${request.collectionName}`
|
|
||||||
: `database ${request.databaseName}`;
|
|
||||||
|
|
||||||
const clearMessage = logConsoleProgress(
|
|
||||||
`Requesting increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const url = `${configContext.BACKEND_ENDPOINT}/api/offerthroughputrequest/updatebeyondspecifiedlimit`;
|
|
||||||
const authorizationHeader = getAuthorizationHeader();
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(request),
|
|
||||||
headers: { [authorizationHeader.header]: authorizationHeader.token, [HttpHeaders.contentType]: "application/json" }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
logConsoleInfo(
|
|
||||||
`Successfully requested an increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
|
|
||||||
);
|
|
||||||
clearMessage();
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const error = await response.json();
|
|
||||||
handleError(
|
|
||||||
error,
|
|
||||||
"updateOfferThroughputBeyondLimit",
|
|
||||||
`Failed to request an increase in throughput for ${request.throughput}`
|
|
||||||
);
|
|
||||||
clearMessage();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
@@ -88,6 +88,38 @@ export interface Resource {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IType {
|
||||||
|
name: string;
|
||||||
|
code: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDataField {
|
||||||
|
dataType: IType;
|
||||||
|
hasNulls: boolean;
|
||||||
|
isArray: boolean;
|
||||||
|
schemaType: IType;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
maxRepetitionLevel: number;
|
||||||
|
maxDefinitionLevel: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISchema {
|
||||||
|
id: string;
|
||||||
|
accountName: string;
|
||||||
|
resource: string;
|
||||||
|
fields: IDataField[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISchemaRequest {
|
||||||
|
id: string;
|
||||||
|
subscriptionId: string;
|
||||||
|
resourceGroup: string;
|
||||||
|
accountName: string;
|
||||||
|
resource: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Collection extends Resource {
|
export interface Collection extends Resource {
|
||||||
defaultTtl?: number;
|
defaultTtl?: number;
|
||||||
indexingPolicy?: IndexingPolicy;
|
indexingPolicy?: IndexingPolicy;
|
||||||
@@ -98,6 +130,8 @@ export interface Collection extends Resource {
|
|||||||
changeFeedPolicy?: ChangeFeedPolicy;
|
changeFeedPolicy?: ChangeFeedPolicy;
|
||||||
analyticalStorageTtl?: number;
|
analyticalStorageTtl?: number;
|
||||||
geospatialConfig?: GeospatialConfig;
|
geospatialConfig?: GeospatialConfig;
|
||||||
|
schema?: ISchema;
|
||||||
|
requestSchema?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Database extends Resource {
|
export interface Database extends Resource {
|
||||||
@@ -174,12 +208,21 @@ export interface QueryMetrics {
|
|||||||
vmExecutionTime: any;
|
vmExecutionTime: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Offer extends Resource {
|
export interface Offer {
|
||||||
|
id: string;
|
||||||
|
autoscaleMaxThroughput: number;
|
||||||
|
manualThroughput: number;
|
||||||
|
minimumThroughput: number;
|
||||||
|
offerDefinition?: SDKOfferDefinition;
|
||||||
|
headers?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SDKOfferDefinition extends Resource {
|
||||||
offerVersion?: string;
|
offerVersion?: string;
|
||||||
offerType?: string;
|
offerType?: string;
|
||||||
content?: {
|
content?: {
|
||||||
offerThroughput: number;
|
offerThroughput: number;
|
||||||
offerIsRUPerMinuteThroughputEnabled: boolean;
|
offerIsRUPerMinuteThroughputEnabled?: boolean;
|
||||||
collectionThroughputInfo?: OfferThroughputInfo;
|
collectionThroughputInfo?: OfferThroughputInfo;
|
||||||
offerAutopilotSettings?: AutoPilotOfferSettings;
|
offerAutopilotSettings?: AutoPilotOfferSettings;
|
||||||
};
|
};
|
||||||
@@ -187,10 +230,6 @@ export interface Offer extends Resource {
|
|||||||
offerResourceId?: string;
|
offerResourceId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OfferWithHeaders extends Offer {
|
|
||||||
headers: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CollectionQuotaInfo {
|
export interface CollectionQuotaInfo {
|
||||||
storedProcedures: number;
|
storedProcedures: number;
|
||||||
triggers: number;
|
triggers: number;
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export enum MessageTypes {
|
|||||||
GetArcadiaToken,
|
GetArcadiaToken,
|
||||||
CreateWorkspace,
|
CreateWorkspace,
|
||||||
CreateSparkPool,
|
CreateSparkPool,
|
||||||
RefreshDatabaseAccount
|
RefreshDatabaseAccount,
|
||||||
|
InitTestExplorer
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Versions, ActionContracts, Diagnostics };
|
export { Versions, ActionContracts, Diagnostics };
|
||||||
|
|||||||
7
src/Contracts/SubscriptionType.ts
Normal file
7
src/Contracts/SubscriptionType.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export enum SubscriptionType {
|
||||||
|
Benefits,
|
||||||
|
EA,
|
||||||
|
Free,
|
||||||
|
Internal,
|
||||||
|
PAYG
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import Trigger from "../Explorer/Tree/Trigger";
|
|||||||
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
|
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
|
||||||
import { UploadDetails } from "../workers/upload/definitions";
|
import { UploadDetails } from "../workers/upload/definitions";
|
||||||
import * as DataModels from "./DataModels";
|
import * as DataModels from "./DataModels";
|
||||||
|
import { SubscriptionType } from "./SubscriptionType";
|
||||||
|
|
||||||
export interface TokenProvider {
|
export interface TokenProvider {
|
||||||
getAuthHeader(): Promise<Headers>;
|
getAuthHeader(): Promise<Headers>;
|
||||||
@@ -115,6 +116,8 @@ export interface CollectionBase extends TreeNode {
|
|||||||
export interface Collection extends CollectionBase {
|
export interface Collection extends CollectionBase {
|
||||||
defaultTtl: ko.Observable<number>;
|
defaultTtl: ko.Observable<number>;
|
||||||
analyticalStorageTtl: ko.Observable<number>;
|
analyticalStorageTtl: ko.Observable<number>;
|
||||||
|
schema?: DataModels.ISchema;
|
||||||
|
requestSchema?: () => void;
|
||||||
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
||||||
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
||||||
quotaInfo: ko.Observable<DataModels.CollectionQuotaInfo>;
|
quotaInfo: ko.Observable<DataModels.CollectionQuotaInfo>;
|
||||||
@@ -358,6 +361,7 @@ export enum CollectionTabKind {
|
|||||||
SparkMasterTab = 16,
|
SparkMasterTab = 16,
|
||||||
Gallery = 17,
|
Gallery = 17,
|
||||||
NotebookViewer = 18,
|
NotebookViewer = 18,
|
||||||
|
Schema = 19,
|
||||||
SettingsV2 = 19
|
SettingsV2 = 19
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,14 +416,6 @@ export interface ThroughputDefaults {
|
|||||||
shared: number;
|
shared: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SubscriptionType {
|
|
||||||
Benefits,
|
|
||||||
EA,
|
|
||||||
Free,
|
|
||||||
Internal,
|
|
||||||
PAYG
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MonacoEditorSettings {
|
export class MonacoEditorSettings {
|
||||||
public readonly language: string;
|
public readonly language: string;
|
||||||
public readonly readOnly: boolean;
|
public readonly readOnly: boolean;
|
||||||
|
|||||||
@@ -44,10 +44,6 @@ describe("Component Registerer", () => {
|
|||||||
expect(ko.components.isRegistered("user-defined-function-tab")).toBe(true);
|
expect(ko.components.isRegistered("user-defined-function-tab")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should register settings-tab component", () => {
|
|
||||||
expect(ko.components.isRegistered("settings-tab")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should register settings-tab-v2 component", () => {
|
it("should register settings-tab-v2 component", () => {
|
||||||
expect(ko.components.isRegistered("settings-tab-v2")).toBe(true);
|
expect(ko.components.isRegistered("settings-tab-v2")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTa
|
|||||||
ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab());
|
ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab());
|
||||||
ko.components.register("trigger-tab", new TabComponents.TriggerTab());
|
ko.components.register("trigger-tab", new TabComponents.TriggerTab());
|
||||||
ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab());
|
ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab());
|
||||||
ko.components.register("settings-tab", new TabComponents.SettingsTab());
|
|
||||||
ko.components.register("settings-tab-v2", new TabComponents.SettingsTabV2());
|
ko.components.register("settings-tab-v2", new TabComponents.SettingsTabV2());
|
||||||
ko.components.register("query-tab", new TabComponents.QueryTab());
|
ko.components.register("query-tab", new TabComponents.QueryTab());
|
||||||
ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab());
|
ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab());
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
|||||||
params.set(TerminalQueryParams.TerminalEndpoint, terminalEndpoint);
|
params.set(TerminalQueryParams.TerminalEndpoint, terminalEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
params.set("account","contoso-retail-mongodb");
|
||||||
|
params.set("port","10255");
|
||||||
|
//tofill
|
||||||
|
params.set("token","");
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,12 +89,11 @@ describe("SettingsComponent", () => {
|
|||||||
it("auto pilot helper functions pass on correct value", () => {
|
it("auto pilot helper functions pass on correct value", () => {
|
||||||
const newCollection = { ...collection };
|
const newCollection = { ...collection };
|
||||||
newCollection.offer = ko.observable<DataModels.Offer>({
|
newCollection.offer = ko.observable<DataModels.Offer>({
|
||||||
content: {
|
autoscaleMaxThroughput: 10000,
|
||||||
offerAutopilotSettings: {
|
manualThroughput: undefined,
|
||||||
maxThroughput: 10000
|
minimumThroughput: 400,
|
||||||
}
|
id: "test"
|
||||||
}
|
});
|
||||||
} as DataModels.Offer);
|
|
||||||
|
|
||||||
const props = { ...baseProps };
|
const props = { ...baseProps };
|
||||||
props.settingsTab.collection = newCollection;
|
props.settingsTab.collection = newCollection;
|
||||||
@@ -187,21 +186,6 @@ describe("SettingsComponent", () => {
|
|||||||
expect(settingsComponentInstance.hasConflictResolution()).toEqual(true);
|
expect(settingsComponentInstance.hasConflictResolution()).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("isOfferReplacePending", () => {
|
|
||||||
let settingsComponentInstance = new SettingsComponent(baseProps);
|
|
||||||
expect(settingsComponentInstance.isOfferReplacePending()).toEqual(undefined);
|
|
||||||
|
|
||||||
const newCollection = { ...collection };
|
|
||||||
newCollection.offer = ko.observable({
|
|
||||||
headers: { "x-ms-offer-replace-pending": true }
|
|
||||||
} as DataModels.OfferWithHeaders);
|
|
||||||
const props = { ...baseProps };
|
|
||||||
props.settingsTab.collection = newCollection;
|
|
||||||
|
|
||||||
settingsComponentInstance = new SettingsComponent(props);
|
|
||||||
expect(settingsComponentInstance.isOfferReplacePending()).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("save calls updateCollection, updateMongoDBCollectionThroughRP and updateOffer", async () => {
|
it("save calls updateCollection, updateMongoDBCollectionThroughRP and updateOffer", async () => {
|
||||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
||||||
wrapper.setState({ isSubSettingsSaveable: true, isScaleSaveable: true, isMongoIndexingPolicySaveable: true });
|
wrapper.setState({ isSubSettingsSaveable: true, isScaleSaveable: true, isMongoIndexingPolicySaveable: true });
|
||||||
|
|||||||
@@ -2,28 +2,23 @@ import * as React from "react";
|
|||||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import * as SharedConstants from "../../../Shared/Constants";
|
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import DiscardIcon from "../../../../images/discard.svg";
|
import DiscardIcon from "../../../../images/discard.svg";
|
||||||
import SaveIcon from "../../../../images/save-cosmos.svg";
|
import SaveIcon from "../../../../images/save-cosmos.svg";
|
||||||
import { traceStart, traceFailure, traceSuccess, trace } from "../../../Shared/Telemetry/TelemetryProcessor";
|
import { traceStart, traceFailure, traceSuccess, trace } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||||
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
|
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { userContext } from "../../../UserContext";
|
|
||||||
import { updateOfferThroughputBeyondLimit } from "../../../Common/dataAccess/updateOfferThroughputBeyondLimit";
|
|
||||||
import SettingsTab from "../../Tabs/SettingsTabV2";
|
import SettingsTab from "../../Tabs/SettingsTabV2";
|
||||||
import { throughputUnit } from "./SettingsRenderUtils";
|
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
|
||||||
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
|
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
|
||||||
import {
|
import {
|
||||||
MongoIndexingPolicyComponent,
|
MongoIndexingPolicyComponent,
|
||||||
MongoIndexingPolicyComponentProps
|
MongoIndexingPolicyComponentProps
|
||||||
} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
|
} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
|
||||||
import {
|
import {
|
||||||
getMaxRUs,
|
|
||||||
hasDatabaseSharedThroughput,
|
hasDatabaseSharedThroughput,
|
||||||
GeospatialConfigType,
|
GeospatialConfigType,
|
||||||
TtlType,
|
TtlType,
|
||||||
@@ -49,6 +44,7 @@ import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/genera
|
|||||||
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
|
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
|
||||||
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
|
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
|
||||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||||
|
import { isEmpty } from "underscore";
|
||||||
|
|
||||||
interface SettingsV2TabInfo {
|
interface SettingsV2TabInfo {
|
||||||
tab: SettingsV2TabTypes;
|
tab: SettingsV2TabTypes;
|
||||||
@@ -227,7 +223,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
|
|
||||||
public loadMongoIndexes = async (): Promise<void> => {
|
public loadMongoIndexes = async (): Promise<void> => {
|
||||||
if (
|
if (
|
||||||
this.container.isMongoIndexEditorEnabled() &&
|
|
||||||
this.container.isPreferredApiMongoDB() &&
|
this.container.isPreferredApiMongoDB() &&
|
||||||
this.container.isEnableMongoCapabilityPresent() &&
|
this.container.isEnableMongoCapabilityPresent() &&
|
||||||
this.container.databaseAccount()
|
this.container.databaseAccount()
|
||||||
@@ -275,19 +270,14 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
};
|
};
|
||||||
|
|
||||||
private setAutoPilotStates = (): void => {
|
private setAutoPilotStates = (): void => {
|
||||||
const offer = this.collection?.offer && this.collection.offer();
|
const autoscaleMaxThroughput = this.collection?.offer()?.autoscaleMaxThroughput;
|
||||||
const offerAutopilotSettings = offer?.content?.offerAutopilotSettings;
|
|
||||||
|
|
||||||
if (
|
if (autoscaleMaxThroughput && AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
|
||||||
offerAutopilotSettings &&
|
|
||||||
offerAutopilotSettings.maxThroughput &&
|
|
||||||
AutoPilotUtils.isValidAutoPilotThroughput(offerAutopilotSettings.maxThroughput)
|
|
||||||
) {
|
|
||||||
this.setState({
|
this.setState({
|
||||||
isAutoPilotSelected: true,
|
isAutoPilotSelected: true,
|
||||||
wasAutopilotOriginallySet: true,
|
wasAutopilotOriginallySet: true,
|
||||||
autoPilotThroughput: offerAutopilotSettings.maxThroughput,
|
autoPilotThroughput: autoscaleMaxThroughput,
|
||||||
autoPilotThroughputBaseline: offerAutopilotSettings.maxThroughput
|
autoPilotThroughputBaseline: autoscaleMaxThroughput
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -305,12 +295,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
!!this.collection.conflictResolutionPolicy();
|
!!this.collection.conflictResolutionPolicy();
|
||||||
|
|
||||||
public isOfferReplacePending = (): boolean => {
|
public isOfferReplacePending = (): boolean => {
|
||||||
const offer = this.collection?.offer && this.collection.offer();
|
return !!this.collection?.offer()?.headers?.[Constants.HttpHeaders.offerReplacePending];
|
||||||
return (
|
|
||||||
offer &&
|
|
||||||
Object.keys(offer).find(value => value === "headers") &&
|
|
||||||
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public onSaveClick = async (): Promise<void> => {
|
public onSaveClick = async (): Promise<void> => {
|
||||||
@@ -448,103 +433,34 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.isScaleSaveable) {
|
if (this.state.isScaleSaveable) {
|
||||||
const newThroughput = this.state.throughput;
|
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||||
const newOffer: DataModels.Offer = { ...this.collection.offer() };
|
databaseId: this.collection.databaseId,
|
||||||
const originalThroughputValue: number = this.state.throughput;
|
collectionId: this.collection.id(),
|
||||||
|
currentOffer: this.collection.offer(),
|
||||||
if (newOffer.content) {
|
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
||||||
newOffer.content.offerThroughput = newThroughput;
|
manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput
|
||||||
} else {
|
};
|
||||||
newOffer.content = {
|
if (this.hasProvisioningTypeChanged()) {
|
||||||
offerThroughput: newThroughput,
|
|
||||||
offerIsRUPerMinuteThroughputEnabled: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const headerOptions: RequestOptions = { initialHeaders: {} };
|
|
||||||
|
|
||||||
if (this.state.isAutoPilotSelected) {
|
|
||||||
newOffer.content.offerAutopilotSettings = {
|
|
||||||
maxThroughput: this.state.autoPilotThroughput
|
|
||||||
};
|
|
||||||
|
|
||||||
// user has changed from provisioned --> autoscale
|
|
||||||
if (this.hasProvisioningTypeChanged()) {
|
|
||||||
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToAutopilot] = "true";
|
|
||||||
delete newOffer.content.offerAutopilotSettings;
|
|
||||||
} else {
|
|
||||||
delete newOffer.content.offerThroughput;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
isAutoPilotSelected: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// user has changed from autoscale --> provisioned
|
|
||||||
if (this.hasProvisioningTypeChanged()) {
|
|
||||||
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToManualThroughput] = "true";
|
|
||||||
} else {
|
|
||||||
delete newOffer.content.offerAutopilotSettings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
getMaxRUs(this.collection, this.container) <=
|
|
||||||
SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
|
||||||
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
|
||||||
this.container
|
|
||||||
) {
|
|
||||||
const requestPayload = {
|
|
||||||
subscriptionId: userContext.subscriptionId,
|
|
||||||
databaseAccountName: userContext.databaseAccount.name,
|
|
||||||
resourceGroup: userContext.resourceGroup,
|
|
||||||
databaseName: this.collection.databaseId,
|
|
||||||
collectionName: this.collection.id(),
|
|
||||||
throughput: newThroughput,
|
|
||||||
offerIsRUPerMinuteThroughputEnabled: false
|
|
||||||
};
|
|
||||||
|
|
||||||
await updateOfferThroughputBeyondLimit(requestPayload);
|
|
||||||
this.collection.offer().content.offerThroughput = originalThroughputValue;
|
|
||||||
this.setState({
|
|
||||||
isScaleSaveable: false,
|
|
||||||
isScaleDiscardable: false,
|
|
||||||
throughput: originalThroughputValue,
|
|
||||||
throughputBaseline: originalThroughputValue,
|
|
||||||
initialNotification: {
|
|
||||||
description: `Throughput update for ${newThroughput} ${throughputUnit}`
|
|
||||||
} as DataModels.Notification
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
|
||||||
databaseId: this.collection.databaseId,
|
|
||||||
collectionId: this.collection.id(),
|
|
||||||
currentOffer: this.collection.offer(),
|
|
||||||
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
|
||||||
manualThroughput: this.state.isAutoPilotSelected ? undefined : newThroughput
|
|
||||||
};
|
|
||||||
if (this.hasProvisioningTypeChanged()) {
|
|
||||||
if (this.state.isAutoPilotSelected) {
|
|
||||||
updateOfferParams.migrateToAutoPilot = true;
|
|
||||||
} else {
|
|
||||||
updateOfferParams.migrateToManual = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
|
||||||
this.collection.offer(updatedOffer);
|
|
||||||
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
|
||||||
if (this.state.isAutoPilotSelected) {
|
if (this.state.isAutoPilotSelected) {
|
||||||
this.setState({
|
updateOfferParams.migrateToAutoPilot = true;
|
||||||
autoPilotThroughput: updatedOffer.content.offerAutopilotSettings.maxThroughput,
|
|
||||||
autoPilotThroughputBaseline: updatedOffer.content.offerAutopilotSettings.maxThroughput
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
updateOfferParams.migrateToManual = true;
|
||||||
throughput: updatedOffer.content.offerThroughput,
|
|
||||||
throughputBaseline: updatedOffer.content.offerThroughput
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||||
|
this.collection.offer(updatedOffer);
|
||||||
|
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
||||||
|
if (this.state.isAutoPilotSelected) {
|
||||||
|
this.setState({
|
||||||
|
autoPilotThroughput: updatedOffer.autoscaleMaxThroughput,
|
||||||
|
autoPilotThroughputBaseline: updatedOffer.autoscaleMaxThroughput
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
throughput: updatedOffer.manualThroughput,
|
||||||
|
throughputBaseline: updatedOffer.manualThroughput
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.container.isRefreshingExplorer(false);
|
this.container.isRefreshingExplorer(false);
|
||||||
this.setBaseline();
|
this.setBaseline();
|
||||||
@@ -809,7 +725,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const offerThroughput = this.collection?.offer && this.collection.offer()?.content?.offerThroughput;
|
const offerThroughput = this.collection.offer()?.manualThroughput;
|
||||||
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
||||||
? ChangeFeedPolicyState.On
|
? ChangeFeedPolicyState.On
|
||||||
: ChangeFeedPolicyState.Off;
|
: ChangeFeedPolicyState.Off;
|
||||||
@@ -1000,15 +916,18 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||||
content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />
|
content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />
|
||||||
});
|
});
|
||||||
} else if (
|
} else if (this.container.isPreferredApiMongoDB()) {
|
||||||
this.container.isMongoIndexEditorEnabled() &&
|
if (isEmpty(this.container.features())) {
|
||||||
this.container.isPreferredApiMongoDB() &&
|
tabs.push({
|
||||||
this.container.isEnableMongoCapabilityPresent()
|
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||||
) {
|
content: mongoIndexingPolicyAADError
|
||||||
tabs.push({
|
});
|
||||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
} else if (this.container.isEnableMongoCapabilityPresent()) {
|
||||||
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />
|
tabs.push({
|
||||||
});
|
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||||
|
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.hasConflictResolution()) {
|
if (this.hasConflictResolution()) {
|
||||||
|
|||||||
@@ -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", 2000)}
|
{getThroughputApplyShortDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection")}
|
||||||
{getThroughputApplyLongDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
{getThroughputApplyLongDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||||
|
|
||||||
{getToolTipContainer(<span>Sample Text</span>)}
|
{getToolTipContainer(<span>Sample Text</span>)}
|
||||||
|
|||||||
@@ -319,14 +319,13 @@ export const getThroughputApplyShortDelayMessage = (
|
|||||||
throughput: number,
|
throughput: number,
|
||||||
throughputUnit: string,
|
throughputUnit: string,
|
||||||
databaseName: string,
|
databaseName: string,
|
||||||
collectionName: string,
|
collectionName: string
|
||||||
targetThroughput: number
|
|
||||||
): JSX.Element => (
|
): JSX.Element => (
|
||||||
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
|
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
|
||||||
A request to increase the throughput is currently in progress. This operation will take some time to complete.
|
A request to increase the throughput is currently in progress. This operation will take some time to complete.
|
||||||
<br />
|
<br />
|
||||||
Database: {databaseName}, Container: {collectionName}{" "}
|
Database: {databaseName}, Container: {collectionName}{" "}
|
||||||
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, targetThroughput)}
|
{getCurrentThroughput(isAutoscale, throughput, throughputUnit)}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
transparentDetailsRowStyles,
|
transparentDetailsRowStyles,
|
||||||
createAndAddMongoIndexStackProps,
|
createAndAddMongoIndexStackProps,
|
||||||
separatorStyles,
|
separatorStyles,
|
||||||
mongoIndexingPolicyAADError,
|
|
||||||
indexingPolicynUnsavedWarningMessage,
|
indexingPolicynUnsavedWarningMessage,
|
||||||
infoAndToolTipTextStyle
|
infoAndToolTipTextStyle
|
||||||
} from "../../SettingsRenderUtils";
|
} from "../../SettingsRenderUtils";
|
||||||
@@ -40,7 +39,6 @@ import {
|
|||||||
} from "../../SettingsUtils";
|
} from "../../SettingsUtils";
|
||||||
import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
|
import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
|
||||||
import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent";
|
import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent";
|
||||||
import { AuthType } from "../../../../../AuthType";
|
|
||||||
import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
||||||
|
|
||||||
export interface MongoIndexingPolicyComponentProps {
|
export interface MongoIndexingPolicyComponentProps {
|
||||||
@@ -321,7 +319,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
|||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return window.authType !== AuthType.AAD ? mongoIndexingPolicyAADError : <Spinner size={SpinnerSize.large} />;
|
return <Spinner size={SpinnerSize.large} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ describe("ScaleComponent", () => {
|
|||||||
} as DataModels.Notification
|
} as DataModels.Notification
|
||||||
};
|
};
|
||||||
|
|
||||||
it("renders with correct intiial notification", () => {
|
it("renders with correct initial notification", () => {
|
||||||
let wrapper = shallow(<ScaleComponent {...baseProps} />);
|
let wrapper = shallow(<ScaleComponent {...baseProps} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
|
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
|
||||||
@@ -54,16 +54,13 @@ describe("ScaleComponent", () => {
|
|||||||
|
|
||||||
const newCollection = { ...collection };
|
const newCollection = { ...collection };
|
||||||
const maxThroughput = 5000;
|
const maxThroughput = 5000;
|
||||||
const targetMaxThroughput = 50000;
|
|
||||||
newCollection.offer = ko.observable({
|
newCollection.offer = ko.observable({
|
||||||
content: {
|
manualThroughput: undefined,
|
||||||
offerAutopilotSettings: {
|
autoscaleMaxThroughput: maxThroughput,
|
||||||
maxThroughput: maxThroughput,
|
minimumThroughput: 400,
|
||||||
targetMaxThroughput: targetMaxThroughput
|
id: "offer",
|
||||||
}
|
|
||||||
},
|
|
||||||
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,
|
||||||
@@ -73,7 +70,6 @@ describe("ScaleComponent", () => {
|
|||||||
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true);
|
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true);
|
||||||
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(false);
|
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(false);
|
||||||
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(maxThroughput);
|
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(maxThroughput);
|
||||||
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(targetMaxThroughput);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("autoScale disabled", () => {
|
it("autoScale disabled", () => {
|
||||||
@@ -109,11 +105,6 @@ describe("ScaleComponent", () => {
|
|||||||
expect(scaleComponent.isAutoScaleEnabled()).toEqual(true);
|
expect(scaleComponent.isAutoScaleEnabled()).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getMaxRUThroughputInputLimit", () => {
|
|
||||||
const scaleComponent = new ScaleComponent(baseProps);
|
|
||||||
expect(scaleComponent.getMaxRUThroughputInputLimit()).toEqual(40000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getThroughputTitle", () => {
|
it("getThroughputTitle", () => {
|
||||||
let scaleComponent = new ScaleComponent(baseProps);
|
let scaleComponent = new ScaleComponent(baseProps);
|
||||||
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
|
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
|
||||||
@@ -138,14 +129,8 @@ describe("ScaleComponent", () => {
|
|||||||
|
|
||||||
it("getThroughputWarningMessage", () => {
|
it("getThroughputWarningMessage", () => {
|
||||||
const throughputBeyondLimit = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million + 1000;
|
const throughputBeyondLimit = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million + 1000;
|
||||||
const throughputBeyondMaxRus = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million - 1000;
|
|
||||||
|
|
||||||
const newProps = { ...baseProps, container: nonNationalCloudContainer, throughput: throughputBeyondLimit };
|
const newProps = { ...baseProps, container: nonNationalCloudContainer, throughput: throughputBeyondLimit };
|
||||||
let scaleComponent = new ScaleComponent(newProps);
|
const scaleComponent = new ScaleComponent(newProps);
|
||||||
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputBeyondLimitWarningMessage");
|
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputBeyondLimitWarningMessage");
|
||||||
|
|
||||||
newProps.throughput = throughputBeyondMaxRus;
|
|
||||||
scaleComponent = new ScaleComponent(newProps);
|
|
||||||
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputDelayedApplyWarningMessage");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ import {
|
|||||||
throughputUnit,
|
throughputUnit,
|
||||||
getThroughputApplyLongDelayMessage,
|
getThroughputApplyLongDelayMessage,
|
||||||
getThroughputApplyShortDelayMessage,
|
getThroughputApplyShortDelayMessage,
|
||||||
updateThroughputBeyondLimitWarningMessage,
|
updateThroughputBeyondLimitWarningMessage
|
||||||
updateThroughputDelayedApplyWarningMessage
|
|
||||||
} from "../SettingsRenderUtils";
|
} from "../SettingsRenderUtils";
|
||||||
import { getMaxRUs, getMinRUs, hasDatabaseSharedThroughput } from "../SettingsUtils";
|
import { hasDatabaseSharedThroughput } from "../SettingsUtils";
|
||||||
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
|
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
|
||||||
import { Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
|
import { Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
|
||||||
import { configContext, Platform } from "../../../../ConfigContext";
|
import { configContext, Platform } from "../../../../ConfigContext";
|
||||||
@@ -62,11 +61,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private getStorageCapacityTitle = (): JSX.Element => {
|
private getStorageCapacityTitle = (): JSX.Element => {
|
||||||
// Mongo container with system partition key still treat as "Fixed"
|
const capacity: string = this.props.isFixedContainer ? "Fixed" : "Unlimited";
|
||||||
const isFixed =
|
|
||||||
!this.props.collection.partitionKey ||
|
|
||||||
(this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey);
|
|
||||||
const capacity: string = isFixed ? "Fixed" : "Unlimited";
|
|
||||||
return (
|
return (
|
||||||
<Stack {...titleAndInputStackProps}>
|
<Stack {...titleAndInputStackProps}>
|
||||||
<Label>Storage capacity</Label>
|
<Label>Storage capacity</Label>
|
||||||
@@ -75,12 +70,26 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
public getMaxRUThroughputInputLimit = (): number => {
|
public getMaxRUs = (): number => {
|
||||||
if (configContext.platform === Platform.Hosted && this.props.collection.partitionKey) {
|
if (this.props.container?.isTryCosmosDBSubscription()) {
|
||||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
return Constants.TryCosmosExperience.maxRU;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getMaxRUs(this.props.collection, this.props.container);
|
if (this.props.isFixedContainer) {
|
||||||
|
return SharedConstants.CollectionCreation.MaxRUPerPartition;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||||
|
};
|
||||||
|
|
||||||
|
public getMinRUs = (): number => {
|
||||||
|
if (this.props.container?.isTryCosmosDBSubscription()) {
|
||||||
|
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
this.props.collection.offer()?.minimumThroughput || SharedConstants.CollectionCreation.DefaultCollectionRUs400
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
public getThroughputTitle = (): string => {
|
public getThroughputTitle = (): string => {
|
||||||
@@ -88,11 +97,8 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
return AutoPilotUtils.getAutoPilotHeaderText();
|
return AutoPilotUtils.getAutoPilotHeaderText();
|
||||||
}
|
}
|
||||||
|
|
||||||
const minThroughput: string = getMinRUs(this.props.collection, this.props.container).toLocaleString();
|
const minThroughput: string = this.getMinRUs().toLocaleString();
|
||||||
const maxThroughput: string =
|
const maxThroughput: string = !this.props.isFixedContainer ? "unlimited" : this.getMaxRUs().toLocaleString();
|
||||||
this.canThroughputExceedMaximumValue() && !this.props.isFixedContainer
|
|
||||||
? "unlimited"
|
|
||||||
: getMaxRUs(this.props.collection, this.props.container).toLocaleString();
|
|
||||||
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`;
|
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,26 +115,15 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
return this.getLongDelayMessage();
|
return this.getLongDelayMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
const offer = this.props.collection?.offer && this.props.collection.offer();
|
const offer = this.props.collection?.offer();
|
||||||
if (
|
if (offer?.headers?.[Constants.HttpHeaders.offerReplacePending]) {
|
||||||
offer &&
|
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
|
||||||
Object.keys(offer).find(value => {
|
|
||||||
return value === "headers";
|
|
||||||
}) &&
|
|
||||||
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
|
||||||
) {
|
|
||||||
const throughput = offer?.content?.offerAutopilotSettings?.maxThroughput;
|
|
||||||
|
|
||||||
const targetThroughput =
|
|
||||||
offer.content?.offerAutopilotSettings?.targetMaxThroughput || offer?.content?.offerThroughput;
|
|
||||||
|
|
||||||
return getThroughputApplyShortDelayMessage(
|
return getThroughputApplyShortDelayMessage(
|
||||||
this.props.isAutoPilotSelected,
|
this.props.isAutoPilotSelected,
|
||||||
throughput,
|
throughput,
|
||||||
throughputUnit,
|
throughputUnit,
|
||||||
this.props.collection.databaseId,
|
this.props.collection.databaseId,
|
||||||
this.props.collection.id(),
|
this.props.collection.id()
|
||||||
targetThroughput
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,21 +133,12 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
public getThroughputWarningMessage = (): JSX.Element => {
|
public getThroughputWarningMessage = (): JSX.Element => {
|
||||||
const throughputExceedsBackendLimits: boolean =
|
const throughputExceedsBackendLimits: boolean =
|
||||||
this.canThroughputExceedMaximumValue() &&
|
this.canThroughputExceedMaximumValue() &&
|
||||||
getMaxRUs(this.props.collection, this.props.container) <=
|
|
||||||
SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
|
||||||
this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||||
|
|
||||||
if (throughputExceedsBackendLimits && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
|
if (throughputExceedsBackendLimits && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
|
||||||
return updateThroughputBeyondLimitWarningMessage;
|
return updateThroughputBeyondLimitWarningMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
const throughputExceedsMaxValue: boolean =
|
|
||||||
!this.isEmulator && this.props.throughput > getMaxRUs(this.props.collection, this.props.container);
|
|
||||||
|
|
||||||
if (throughputExceedsMaxValue && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
|
|
||||||
return updateThroughputDelayedApplyWarningMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -183,8 +169,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={getMinRUs(this.props.collection, this.props.container)}
|
minimum={this.getMinRUs()}
|
||||||
maximum={this.getMaxRUThroughputInputLimit()}
|
maximum={this.getMaxRUs()}
|
||||||
isEnabled={!hasDatabaseSharedThroughput(this.props.collection)}
|
isEnabled={!hasDatabaseSharedThroughput(this.props.collection)}
|
||||||
canExceedMaximumValue={this.canThroughputExceedMaximumValue()}
|
canExceedMaximumValue={this.canThroughputExceedMaximumValue()}
|
||||||
label={this.getThroughputTitle()}
|
label={this.getThroughputTitle()}
|
||||||
@@ -200,6 +186,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
onScaleSaveableChange={this.props.onScaleSaveableChange}
|
onScaleSaveableChange={this.props.onScaleSaveableChange}
|
||||||
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
|
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
|
||||||
getThroughputWarningMessage={this.getThroughputWarningMessage}
|
getThroughputWarningMessage={this.getThroughputWarningMessage}
|
||||||
|
usageSizeInKB={this.props.collection.quotaInfo().usageSizeInKB}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ describe("ThroughputInputAutoPilotV3Component", () => {
|
|||||||
minimum: 10000,
|
minimum: 10000,
|
||||||
maximum: 400,
|
maximum: 400,
|
||||||
step: 100,
|
step: 100,
|
||||||
|
usageSizeInKB: 10000,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
isEmulator: false,
|
isEmulator: false,
|
||||||
spendAckChecked: false,
|
spendAckChecked: false,
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../S
|
|||||||
import * as SharedConstants from "../../../../../Shared/Constants";
|
import * as SharedConstants from "../../../../../Shared/Constants";
|
||||||
import * as DataModels from "../../../../../Contracts/DataModels";
|
import * as DataModels from "../../../../../Contracts/DataModels";
|
||||||
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||||
|
import { userContext } from "../../../../../UserContext";
|
||||||
|
import { SubscriptionType } from "../../../../../Contracts/SubscriptionType";
|
||||||
|
import { usageInGB } from "../../../../../Utils/PricingUtils";
|
||||||
|
import { Features } from "../../../../../Common/Constants";
|
||||||
|
|
||||||
export interface ThroughputInputAutoPilotV3Props {
|
export interface ThroughputInputAutoPilotV3Props {
|
||||||
databaseAccount: DataModels.DatabaseAccount;
|
databaseAccount: DataModels.DatabaseAccount;
|
||||||
@@ -60,6 +64,7 @@ export interface ThroughputInputAutoPilotV3Props {
|
|||||||
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
||||||
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
||||||
getThroughputWarningMessage: () => JSX.Element;
|
getThroughputWarningMessage: () => JSX.Element;
|
||||||
|
usageSizeInKB: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ThroughputInputAutoPilotV3State {
|
interface ThroughputInputAutoPilotV3State {
|
||||||
@@ -224,6 +229,29 @@ 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 (
|
||||||
@@ -275,6 +303,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
onChange={this.onAutoPilotThroughputChange}
|
onChange={this.onAutoPilotThroughputChange}
|
||||||
/>
|
/>
|
||||||
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
|
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
|
||||||
|
{this.minRUperGBSurvey()}
|
||||||
{this.props.spendAckVisible && (
|
{this.props.spendAckVisible && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="spendAckCheckBox"
|
id="spendAckCheckBox"
|
||||||
@@ -305,15 +334,13 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
}
|
}
|
||||||
onChange={this.onThroughputChange}
|
onChange={this.onThroughputChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{this.props.getThroughputWarningMessage() && (
|
{this.props.getThroughputWarningMessage() && (
|
||||||
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
|
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
|
||||||
{this.props.getThroughputWarningMessage()}
|
{this.props.getThroughputWarningMessage()}
|
||||||
</MessageBar>
|
</MessageBar>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!this.props.isEmulator && this.getRequestUnitsUsageCost()}
|
{!this.props.isEmulator && this.getRequestUnitsUsageCost()}
|
||||||
|
{this.minRUperGBSurvey()}
|
||||||
{this.props.spendAckVisible && (
|
{this.props.spendAckVisible && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="spendAckCheckBox"
|
id="spendAckCheckBox"
|
||||||
@@ -323,7 +350,6 @@ 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 intiial notification 1`] = `
|
exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||||
<Stack
|
<Stack
|
||||||
tokens={
|
tokens={
|
||||||
Object {
|
Object {
|
||||||
@@ -48,7 +48,7 @@ exports[`ScaleComponent renders with correct intiial notification 1`] = `
|
|||||||
label="Throughput (6,000 - unlimited RU/s)"
|
label="Throughput (6,000 - unlimited RU/s)"
|
||||||
maxAutoPilotThroughput={4000}
|
maxAutoPilotThroughput={4000}
|
||||||
maxAutoPilotThroughputBaseline={4000}
|
maxAutoPilotThroughputBaseline={4000}
|
||||||
maximum={40000}
|
maximum={1000000}
|
||||||
minimum={6000}
|
minimum={6000}
|
||||||
onAutoPilotSelected={[Function]}
|
onAutoPilotSelected={[Function]}
|
||||||
onMaxAutoPilotThroughputChange={[Function]}
|
onMaxAutoPilotThroughputChange={[Function]}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { collection, container } from "./TestUtils";
|
import { collection } from "./TestUtils";
|
||||||
import {
|
import {
|
||||||
getMaxRUs,
|
|
||||||
getMinRUs,
|
|
||||||
getMongoIndexType,
|
getMongoIndexType,
|
||||||
getMongoNotification,
|
getMongoNotification,
|
||||||
getSanitizedInputValue,
|
getSanitizedInputValue,
|
||||||
@@ -23,16 +21,6 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
|||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
|
|
||||||
describe("SettingsUtils", () => {
|
describe("SettingsUtils", () => {
|
||||||
it("getMaxRUs", () => {
|
|
||||||
expect(collection.offer().content.collectionThroughputInfo.numPhysicalPartitions).toEqual(4);
|
|
||||||
expect(getMaxRUs(collection, container)).toEqual(40000);
|
|
||||||
});
|
|
||||||
|
|
||||||
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,10 +1,6 @@
|
|||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import * as SharedConstants from "../../../Shared/Constants";
|
|
||||||
import * as PricingUtils from "../../../Utils/PricingUtils";
|
|
||||||
|
|
||||||
import Explorer from "../../Explorer";
|
|
||||||
import { MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
|
import { MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
|
||||||
|
|
||||||
const zeroValue = 0;
|
const zeroValue = 0;
|
||||||
@@ -71,57 +67,6 @@ export const hasDatabaseSharedThroughput = (collection: ViewModels.Collection):
|
|||||||
return database?.isDatabaseShared() && !collection.offer();
|
return database?.isDatabaseShared() && !collection.offer();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMaxRUs = (collection: ViewModels.Collection, container: Explorer): number => {
|
|
||||||
const isTryCosmosDBSubscription = container?.isTryCosmosDBSubscription() || false;
|
|
||||||
if (isTryCosmosDBSubscription) {
|
|
||||||
return Constants.TryCosmosExperience.maxRU;
|
|
||||||
}
|
|
||||||
|
|
||||||
const numPartitionsFromOffer: number =
|
|
||||||
collection?.offer && collection.offer()?.content?.collectionThroughputInfo?.numPhysicalPartitions;
|
|
||||||
|
|
||||||
const numPartitionsFromQuotaInfo: number = collection?.quotaInfo()?.numPartitions;
|
|
||||||
|
|
||||||
const numPartitions = numPartitionsFromOffer ?? numPartitionsFromQuotaInfo ?? 1;
|
|
||||||
|
|
||||||
return SharedConstants.CollectionCreation.MaxRUPerPartition * numPartitions;
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const numPartitions = collectionThroughputInfo?.numPhysicalPartitions ?? collection.quotaInfo()?.numPartitions;
|
|
||||||
|
|
||||||
if (!numPartitions || numPartitions === 1) {
|
|
||||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseRU = SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
|
||||||
|
|
||||||
const quotaInKb = collection.quotaInfo().collectionSize;
|
|
||||||
const quotaInGb = PricingUtils.usageInGB(quotaInKb);
|
|
||||||
|
|
||||||
const perPartitionGBQuota: number = Math.max(10, quotaInGb / numPartitions);
|
|
||||||
const baseRUbyPartitions: number = ((numPartitions * perPartitionGBQuota) / 10) * 100;
|
|
||||||
|
|
||||||
return Math.max(baseRU, baseRUbyPartitions);
|
|
||||||
};
|
|
||||||
|
|
||||||
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) {
|
||||||
|
|||||||
@@ -20,15 +20,11 @@ export const collection = ({
|
|||||||
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
|
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
|
||||||
quotaInfo: ko.observable<DataModels.CollectionQuotaInfo>({} as DataModels.CollectionQuotaInfo),
|
quotaInfo: ko.observable<DataModels.CollectionQuotaInfo>({} as DataModels.CollectionQuotaInfo),
|
||||||
offer: ko.observable<DataModels.Offer>({
|
offer: ko.observable<DataModels.Offer>({
|
||||||
content: {
|
autoscaleMaxThroughput: undefined,
|
||||||
offerThroughput: 10000,
|
manualThroughput: 10000,
|
||||||
offerIsRUPerMinuteThroughputEnabled: false,
|
minimumThroughput: 6000,
|
||||||
collectionThroughputInfo: {
|
id: "offer"
|
||||||
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
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -956,7 +956,6 @@ 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],
|
||||||
@@ -971,8 +970,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],
|
||||||
@@ -2236,7 +2235,6 @@ 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],
|
||||||
@@ -2251,8 +2249,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],
|
||||||
@@ -3529,7 +3527,6 @@ 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],
|
||||||
@@ -3544,8 +3541,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],
|
||||||
@@ -4809,7 +4806,6 @@ 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],
|
||||||
@@ -4824,8 +4820,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, Target manual throughput: 2000
|
, Current manual throughput: 1000 RU/s
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
id="throughputApplyLongDelayMessage"
|
id="throughputApplyLongDelayMessage"
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ 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
|
||||||
@@ -119,7 +120,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<ViewModels.SubscriptionType>;
|
public subscriptionType: ko.Observable<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>;
|
||||||
@@ -205,8 +206,6 @@ 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>;
|
||||||
@@ -225,6 +224,7 @@ 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,9 +278,7 @@ 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<ViewModels.SubscriptionType>(
|
this.subscriptionType = ko.observable<SubscriptionType>(SharedConstants.CollectionCreation.DefaultSubscriptionType);
|
||||||
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);
|
||||||
@@ -412,8 +410,6 @@ 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);
|
||||||
@@ -422,6 +418,7 @@ 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>();
|
||||||
@@ -1733,6 +1730,7 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1890,7 +1888,8 @@ 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,
|
||||||
@@ -1911,14 +1910,6 @@ 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,7 +350,9 @@ 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,6 +7,7 @@ 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";
|
||||||
@@ -648,10 +649,8 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getSharedThroughputDefault(): boolean {
|
public getSharedThroughputDefault(): boolean {
|
||||||
const subscriptionType: ViewModels.SubscriptionType =
|
const subscriptionType = this.container.subscriptionType && this.container.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -690,7 +689,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
databaseId: this.databaseId(),
|
databaseId: this.databaseId(),
|
||||||
rupm: this.rupm()
|
rupm: this.rupm()
|
||||||
}),
|
}),
|
||||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: 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",
|
||||||
@@ -793,7 +792,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
uniqueKeyPolicy,
|
uniqueKeyPolicy,
|
||||||
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
||||||
}),
|
}),
|
||||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: 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",
|
||||||
@@ -868,7 +867,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
uniqueKeyPolicy,
|
uniqueKeyPolicy,
|
||||||
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
||||||
}),
|
}),
|
||||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: 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",
|
||||||
@@ -903,7 +902,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
uniqueKeyPolicy,
|
uniqueKeyPolicy,
|
||||||
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
||||||
},
|
},
|
||||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: 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",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import { SubscriptionType } from "../../Contracts/SubscriptionType";
|
||||||
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(ViewModels.SubscriptionType.Benefits);
|
explorer.subscriptionType(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(ViewModels.SubscriptionType.EA);
|
explorer.subscriptionType(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(ViewModels.SubscriptionType.Free);
|
explorer.subscriptionType(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(ViewModels.SubscriptionType.Internal);
|
explorer.subscriptionType(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(ViewModels.SubscriptionType.PAYG);
|
explorer.subscriptionType(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,6 +12,7 @@ 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>;
|
||||||
@@ -256,7 +257,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: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
throughput: this.throughput(),
|
throughput: this.throughput(),
|
||||||
@@ -284,7 +285,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
shared: this.databaseCreateNewShared()
|
shared: this.databaseCreateNewShared()
|
||||||
}),
|
}),
|
||||||
offerThroughput,
|
offerThroughput,
|
||||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
flight: this.container.flight()
|
flight: this.container.flight()
|
||||||
@@ -327,10 +328,9 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getSharedThroughputDefault(): boolean {
|
public getSharedThroughputDefault(): boolean {
|
||||||
const subscriptionType: ViewModels.SubscriptionType =
|
const subscriptionType = this.container.subscriptionType && this.container.subscriptionType();
|
||||||
this.container.subscriptionType && this.container.subscriptionType();
|
|
||||||
|
|
||||||
if (subscriptionType === ViewModels.SubscriptionType.EA || this.container.isServerlessEnabled()) {
|
if (subscriptionType === SubscriptionType.EA || this.container.isServerlessEnabled()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,7 +349,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
shared: this.databaseCreateNewShared()
|
shared: this.databaseCreateNewShared()
|
||||||
}),
|
}),
|
||||||
offerThroughput: offerThroughput,
|
offerThroughput: offerThroughput,
|
||||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
flight: this.container.flight()
|
flight: this.container.flight()
|
||||||
@@ -373,7 +373,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
shared: this.databaseCreateNewShared()
|
shared: this.databaseCreateNewShared()
|
||||||
}),
|
}),
|
||||||
offerThroughput: offerThroughput,
|
offerThroughput: offerThroughput,
|
||||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
flight: this.container.flight()
|
flight: this.container.flight()
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ 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>;
|
||||||
@@ -314,7 +315,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
databaseId: this.keyspaceId(),
|
databaseId: this.keyspaceId(),
|
||||||
rupm: false
|
rupm: false
|
||||||
}),
|
}),
|
||||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: "u",
|
storage: "u",
|
||||||
@@ -369,7 +370,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
hasDedicatedThroughput: this.dedicateTableThroughput()
|
hasDedicatedThroughput: this.dedicateTableThroughput()
|
||||||
}),
|
}),
|
||||||
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
||||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: "u",
|
storage: "u",
|
||||||
@@ -416,7 +417,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
hasDedicatedThroughput: this.dedicateTableThroughput()
|
hasDedicatedThroughput: this.dedicateTableThroughput()
|
||||||
}),
|
}),
|
||||||
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
||||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: "u",
|
storage: "u",
|
||||||
@@ -447,7 +448,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
hasDedicatedThroughput: this.dedicateTableThroughput()
|
hasDedicatedThroughput: this.dedicateTableThroughput()
|
||||||
},
|
},
|
||||||
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
||||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: "u",
|
storage: "u",
|
||||||
|
|||||||
@@ -50,13 +50,24 @@
|
|||||||
id="fileImportLinkNotebook"
|
id="fileImportLinkNotebook"
|
||||||
data-bind="event: { click: onImportLinkClick, keypress: onImportLinkKeyPress }"
|
data-bind="event: { click: onImportLinkClick, keypress: onImportLinkKeyPress }"
|
||||||
>
|
>
|
||||||
<img class="fileImportImg" src="/folder_16x16.svg" alt="upload files" title="Upload files" />
|
<img
|
||||||
|
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 type="submit" data-bind="attr: { value: submitButtonLabel }" class="btncreatecoll1" />
|
<input
|
||||||
|
id="uploadFileButton"
|
||||||
|
type="submit"
|
||||||
|
data-bind="attr: { value: submitButtonLabel }"
|
||||||
|
class="btncreatecoll1"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Upload File inputs - End -->
|
<!-- Upload File inputs - End -->
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ 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 { updateOfferThroughputBeyondLimit } from "../../Common/dataAccess/updateOfferThroughputBeyondLimit";
|
|
||||||
import { configContext, Platform } from "../../ConfigContext";
|
import { configContext, Platform } from "../../ConfigContext";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
|
|
||||||
@@ -35,11 +33,6 @@ 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 />
|
||||||
@@ -66,8 +59,8 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
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.Computed<number>;
|
public minRUs: ko.Observable<number>;
|
||||||
public maxRUs: ko.Computed<number>;
|
public maxRUs: ko.Observable<number>;
|
||||||
public maxRUsText: ko.PureComputed<string>;
|
public maxRUsText: ko.PureComputed<string>;
|
||||||
public maxRUThroughputInputLimit: ko.Computed<number>;
|
public maxRUThroughputInputLimit: ko.Computed<number>;
|
||||||
public notificationStatusInfo: ko.Observable<string>;
|
public notificationStatusInfo: ko.Observable<string>;
|
||||||
@@ -92,7 +85,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.Computed<boolean>;
|
private _offerReplacePending: ko.Observable<boolean>;
|
||||||
private container: Explorer;
|
private container: Explorer;
|
||||||
|
|
||||||
constructor(options: ViewModels.TabOptions) {
|
constructor(options: ViewModels.TabOptions) {
|
||||||
@@ -111,15 +104,14 @@ 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);
|
||||||
|
|
||||||
if (offerAutopilotSettings && offerAutopilotSettings.maxThroughput) {
|
const autoscaleMaxThroughput = this.database?.offer()?.autoscaleMaxThroughput;
|
||||||
if (AutoPilotUtils.isValidAutoPilotThroughput(offerAutopilotSettings.maxThroughput)) {
|
if (autoscaleMaxThroughput) {
|
||||||
|
if (AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
|
||||||
this._wasAutopilotOriginallySet(true);
|
this._wasAutopilotOriginallySet(true);
|
||||||
this.isAutoPilotSelected(true);
|
this.isAutoPilotSelected(true);
|
||||||
this.autoPilotThroughput(offerAutopilotSettings.maxThroughput);
|
this.autoPilotThroughput(autoscaleMaxThroughput);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,45 +197,15 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
return this._hasProvisioningTypeChanged() && !this._wasAutopilotOriginallySet();
|
return this._hasProvisioningTypeChanged() && !this._wasAutopilotOriginallySet();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.minRUs = ko.computed<number>(() => {
|
this.minRUs = ko.observable<number>(
|
||||||
const offerContent =
|
this.database.offer()?.minimumThroughput || this.container.collectionCreationDefaults.throughput.unlimitedmin
|
||||||
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.computed<number>(() => {
|
this.maxRUs = ko.observable<number>(this.container.collectionCreationDefaults.throughput.unlimitedmax);
|
||||||
const collectionThroughputInfo: DataModels.OfferThroughputInfo =
|
|
||||||
this.database &&
|
|
||||||
this.database.offer &&
|
|
||||||
this.database.offer() &&
|
|
||||||
this.database.offer().content &&
|
|
||||||
this.database.offer().content.collectionThroughputInfo;
|
|
||||||
const numPartitions = collectionThroughputInfo && collectionThroughputInfo.numPhysicalPartitions;
|
|
||||||
if (!!numPartitions) {
|
|
||||||
return SharedConstants.CollectionCreation.MaxRUPerPartition * numPartitions;
|
|
||||||
}
|
|
||||||
|
|
||||||
const throughputDefaults = this.container.collectionCreationDefaults.throughput;
|
|
||||||
return throughputDefaults.unlimitedmax;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.maxRUThroughputInputLimit = ko.pureComputed<number>(() => {
|
this.maxRUThroughputInputLimit = ko.pureComputed<number>(() => {
|
||||||
if (configContext.platform === Platform.Hosted) {
|
if (configContext.platform === Platform.Hosted) {
|
||||||
@@ -269,37 +231,23 @@ 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.pureComputed<boolean>(() => {
|
this._offerReplacePending = ko.observable<boolean>(
|
||||||
const offer = this.database && this.database.offer && this.database.offer();
|
!!this.database.offer()?.headers?.[Constants.HttpHeaders.offerReplacePending]
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
const offer = this.database.offer();
|
||||||
offer &&
|
if (offer?.headers?.[Constants.HttpHeaders.offerReplacePending]) {
|
||||||
offer.hasOwnProperty("headers") &&
|
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
|
||||||
!!(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.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
|
||||||
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||||
this.canThroughputExceedMaximumValue()
|
this.canThroughputExceedMaximumValue()
|
||||||
) {
|
) {
|
||||||
@@ -432,60 +380,26 @@ 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()
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
if (this._hasProvisioningTypeChanged()) {
|
||||||
this.database.offer(updatedOffer);
|
if (this.isAutoPilotSelected()) {
|
||||||
this.database.offer.valueHasMutated();
|
updateOfferParams.migrateToAutoPilot = true;
|
||||||
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
} else {
|
||||||
} else {
|
updateOfferParams.migrateToManual = true;
|
||||||
if (this.throughput.editableIsDirty() || this.isAutoPilotSelected.editableIsDirty()) {
|
|
||||||
const originalThroughputValue = this.throughput.getEditableOriginalValue();
|
|
||||||
const newThroughput = this.throughput();
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.canThroughputExceedMaximumValue() &&
|
|
||||||
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
|
||||||
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million
|
|
||||||
) {
|
|
||||||
const requestPayload = {
|
|
||||||
subscriptionId: userContext.subscriptionId,
|
|
||||||
databaseAccountName: userContext.databaseAccount.name,
|
|
||||||
resourceGroup: userContext.resourceGroup,
|
|
||||||
databaseName: this.database.id(),
|
|
||||||
throughput: newThroughput,
|
|
||||||
offerIsRUPerMinuteThroughputEnabled: false
|
|
||||||
};
|
|
||||||
await updateOfferThroughputBeyondLimit(requestPayload);
|
|
||||||
this.database.offer().content.offerThroughput = originalThroughputValue;
|
|
||||||
this.throughput(originalThroughputValue);
|
|
||||||
this.notificationStatusInfo(
|
|
||||||
throughputApplyDelayedMessage(this.isAutoPilotSelected(), newThroughput, this.database.id())
|
|
||||||
);
|
|
||||||
this.throughput.valueHasMutated(); // force component re-render
|
|
||||||
} else {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||||
|
this.database.offer(updatedOffer);
|
||||||
|
this.database.offer.valueHasMutated();
|
||||||
|
this._setBaseline();
|
||||||
|
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.container.isRefreshingExplorer(false);
|
this.container.isRefreshingExplorer(false);
|
||||||
this.isExecutionError(true);
|
this.isExecutionError(true);
|
||||||
@@ -527,15 +441,10 @@ 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();
|
||||||
const offerThroughput = offer.content && offer.content.offerThroughput;
|
this.isAutoPilotSelected.setBaseline(AutoPilotUtils.isValidAutoPilotThroughput(offer.autoscaleMaxThroughput));
|
||||||
const offerAutopilotSettings = offer && offer.content && offer.content.offerAutopilotSettings;
|
this.autoPilotThroughput.setBaseline(offer.autoscaleMaxThroughput);
|
||||||
|
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[] {
|
||||||
|
|||||||
@@ -1,723 +0,0 @@
|
|||||||
<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,
|
|
||||||
maximum: maxRUThroughputInputLimit,
|
|
||||||
isEnabled: !hasDatabaseSharedThroughput(),
|
|
||||||
canExceedMaximumValue: canThroughputExceedMaximumValue,
|
|
||||||
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>
|
|
||||||
@@ -1,449 +0,0 @@
|
|||||||
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]
|
|
||||||
};
|
|
||||||
|
|
||||||
const quotaInfo: DataModels.CollectionQuotaInfo = {
|
|
||||||
storedProcedures: 0,
|
|
||||||
triggers: 0,
|
|
||||||
functions: 0,
|
|
||||||
documentsSize: 0,
|
|
||||||
documentsCount: 0,
|
|
||||||
collectionSize: 0,
|
|
||||||
usageSizeInKB: 0,
|
|
||||||
numPartitions: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
|
||||||
quotaInfo,
|
|
||||||
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, quotaInfo, 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, quotaInfo, 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, quotaInfo, 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, quotaInfo, 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, quotaInfo, 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, quotaInfo, 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"
|
|
||||||
},
|
|
||||||
quotaInfo,
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
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("Scale & Settings");
|
this.tabTitle(this.currentCollection.offer() ? "Settings" : "Scale & Settings");
|
||||||
this.offerRead(true);
|
this.offerRead(true);
|
||||||
this.options.getPendingNotification.then(
|
this.options.getPendingNotification.then(
|
||||||
(data: DataModels.Notification) => {
|
(data: DataModels.Notification) => {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ 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";
|
||||||
@@ -133,15 +132,6 @@ export class QueryTablesTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SettingsTab {
|
|
||||||
constructor() {
|
|
||||||
return {
|
|
||||||
viewModel: TabComponent,
|
|
||||||
template: SettingsTabTemplate
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SettingsTabV2 {
|
export class SettingsTabV2 {
|
||||||
constructor() {
|
constructor() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ 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";
|
||||||
@@ -63,6 +62,8 @@ 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
|
||||||
@@ -117,6 +118,8 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
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
|
||||||
@@ -552,11 +555,6 @@ 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 => {
|
||||||
@@ -583,68 +581,8 @@ 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 = (
|
||||||
@@ -1357,8 +1295,7 @@ 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
|
||||||
);
|
);
|
||||||
|
|||||||
82
src/Explorer/Tree/Database.test.ts
Normal file
82
src/Explorer/Tree/Database.test.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
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,6 +13,8 @@ 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";
|
||||||
@@ -29,6 +31,7 @@ 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";
|
||||||
@@ -43,6 +46,7 @@ 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 = () => {
|
||||||
@@ -184,6 +188,10 @@ 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, null);
|
||||||
collectionVMs.push(collectionVM);
|
collectionVMs.push(collectionVM);
|
||||||
@@ -308,4 +316,42 @@ 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
253
src/Explorer/Tree/ResourceTreeAdapter.test.tsx
Normal file
253
src/Explorer/Tree/ResourceTreeAdapter.test.tsx
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
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 } from "../Controls/TreeComponent/TreeComponent";
|
import { TreeComponent, TreeNode, TreeNodeMenuItem, TreeNodeComponent } 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,6 +32,7 @@ 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";
|
||||||
@@ -289,6 +290,11 @@ 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));
|
||||||
@@ -405,6 +411,75 @@ 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,
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
// 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,6 +7,7 @@ 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;
|
||||||
@@ -427,6 +428,51 @@ 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);
|
||||||
|
|
||||||
@@ -457,6 +503,10 @@ 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
117
src/Main.ts
@@ -1,117 +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";
|
|
||||||
|
|
||||||
// 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
Normal file
452
src/Main.tsx
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
// 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/ViewModels";
|
import { SubscriptionType } from "../Contracts/SubscriptionType";
|
||||||
|
|
||||||
export const hoursInAMonth = 730;
|
export const hoursInAMonth = 730;
|
||||||
export class AutoscalePricing {
|
export class AutoscalePricing {
|
||||||
|
|||||||
@@ -23,10 +23,14 @@ 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,4 +1,5 @@
|
|||||||
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 {
|
||||||
@@ -12,6 +13,7 @@ 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,15 +7,6 @@ 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;
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
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,329 +8,5 @@
|
|||||||
<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>
|
||||||
|
|||||||
20
src/koComment.tsx
Normal file
20
src/koComment.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/* 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} />;
|
||||||
|
};
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import "expect-puppeteer";
|
|
||||||
import { trackEvent, trackException } from "./utils";
|
|
||||||
|
|
||||||
jest.setTimeout(300000);
|
|
||||||
|
|
||||||
describe.skip("Collection CRUD", () => {
|
|
||||||
it("should complete collection crud", async () => {
|
|
||||||
try {
|
|
||||||
// Login to Azure Portal
|
|
||||||
await page.goto("https://portal.azure.com");
|
|
||||||
await page.waitFor("input[name=loginfmt]");
|
|
||||||
await page.type("input[name=loginfmt]", process.env.PORTAL_RUNNER_USERNAME);
|
|
||||||
await page.click("input[type=submit]");
|
|
||||||
await page.waitFor(3000);
|
|
||||||
await page.waitFor("input[name=loginfmt]");
|
|
||||||
await page.type("input[name=passwd]", process.env.PORTAL_RUNNER_PASSWORD);
|
|
||||||
await page.click("input[type=submit]");
|
|
||||||
await page.waitFor(3000);
|
|
||||||
await page.waitForNavigation();
|
|
||||||
await page.goto(
|
|
||||||
`https://ms.portal.azure.com/#@microsoft.onmicrosoft.com/resource/subscriptions/${process.env.PORTAL_RUNNER_SUBSCRIPTION}/resourceGroups/${process.env.PORTAL_RUNNER_RESOURCE_GROUP}/providers/Microsoft.DocumentDb/databaseAccounts/${process.env.PORTAL_RUNNER_DATABASE_ACCOUNT}/dataExplorer`
|
|
||||||
);
|
|
||||||
// Wait for page to settle
|
|
||||||
await page.waitFor(10000);
|
|
||||||
// Find Data Explorer iFrame
|
|
||||||
const frames = page.frames();
|
|
||||||
const dataExplorer = frames.find(frame => frame.url().includes("cosmos.azure.com"));
|
|
||||||
// Click "New Container"
|
|
||||||
const newContainerButton = await dataExplorer.$('button[data-test="New Container"]');
|
|
||||||
await newContainerButton.click();
|
|
||||||
// Wait for side pane to appear
|
|
||||||
await dataExplorer.waitFor(".contextual-pane-in");
|
|
||||||
// Fill out New Container form
|
|
||||||
const databaseIdInput = await dataExplorer.$("#databaseId");
|
|
||||||
await databaseIdInput.type("foo");
|
|
||||||
const collectionIdInput = await dataExplorer.$("#containerId");
|
|
||||||
await collectionIdInput.type("foo");
|
|
||||||
const partitionKeyInput = await dataExplorer.$('input[data-test="addCollection-partitionKeyValue"]');
|
|
||||||
await partitionKeyInput.type("/partitionKey");
|
|
||||||
trackEvent({ name: "ProductionRunnerSuccess" });
|
|
||||||
|
|
||||||
// TODO: Submit form and assert results
|
|
||||||
// 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);
|
|
||||||
} catch (error) {
|
|
||||||
await page.screenshot({ path: "failure.png" });
|
|
||||||
trackException(error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -82,10 +82,6 @@ describe("Collection Add and Delete Mongo spec", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (collections.length) {
|
if (collections.length) {
|
||||||
await frame.waitFor(`div[class="collectionHeader main2 nodeItem "] > div[class="treeNodeHeader "]`, {
|
|
||||||
visible: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const textId = await frame.evaluate(element => {
|
const textId = await frame.evaluate(element => {
|
||||||
return element.attributes["data-test"].textContent;
|
return element.attributes["data-test"].textContent;
|
||||||
}, collections[0]);
|
}, collections[0]);
|
||||||
|
|||||||
101
test/notebooks/notebookTestUtils.ts
Normal file
101
test/notebooks/notebookTestUtils.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { ElementHandle, Frame } from "puppeteer";
|
||||||
|
import { TestExplorerParams } from "./testExplorer/TestExplorerParams";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
export const NOTEBOOK_OPERATION_DELAY = 5000;
|
||||||
|
export const RENDER_DELAY = 2500;
|
||||||
|
|
||||||
|
let testExplorerFrame: Frame;
|
||||||
|
export const getTestExplorerFrame = async (): Promise<Frame> => {
|
||||||
|
if (testExplorerFrame) {
|
||||||
|
return testExplorerFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
const notebooksTestRunnerTenantId = process.env.NOTEBOOKS_TEST_RUNNER_TENANT_ID;
|
||||||
|
const notebooksTestRunnerClientId = process.env.NOTEBOOKS_TEST_RUNNER_CLIENT_ID;
|
||||||
|
const notebooksTestRunnerClientSecret = process.env.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET;
|
||||||
|
const portalRunnerDatabaseAccount = process.env.PORTAL_RUNNER_DATABASE_ACCOUNT;
|
||||||
|
const portalRunnerDatabaseAccountKey = process.env.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY;
|
||||||
|
const portalRunnerSubscripton = process.env.PORTAL_RUNNER_SUBSCRIPTION;
|
||||||
|
const portalRunnerResourceGroup = process.env.PORTAL_RUNNER_RESOURCE_GROUP;
|
||||||
|
|
||||||
|
const testExplorerUrl = new URL("testExplorer.html", "https://localhost:1234");
|
||||||
|
testExplorerUrl.searchParams.append(
|
||||||
|
TestExplorerParams.notebooksTestRunnerTenantId,
|
||||||
|
encodeURI(notebooksTestRunnerTenantId)
|
||||||
|
);
|
||||||
|
testExplorerUrl.searchParams.append(
|
||||||
|
TestExplorerParams.notebooksTestRunnerClientId,
|
||||||
|
encodeURI(notebooksTestRunnerClientId)
|
||||||
|
);
|
||||||
|
testExplorerUrl.searchParams.append(
|
||||||
|
TestExplorerParams.notebooksTestRunnerClientSecret,
|
||||||
|
encodeURI(notebooksTestRunnerClientSecret)
|
||||||
|
);
|
||||||
|
testExplorerUrl.searchParams.append(
|
||||||
|
TestExplorerParams.portalRunnerDatabaseAccount,
|
||||||
|
encodeURI(portalRunnerDatabaseAccount)
|
||||||
|
);
|
||||||
|
testExplorerUrl.searchParams.append(
|
||||||
|
TestExplorerParams.portalRunnerDatabaseAccountKey,
|
||||||
|
encodeURI(portalRunnerDatabaseAccountKey)
|
||||||
|
);
|
||||||
|
testExplorerUrl.searchParams.append(TestExplorerParams.portalRunnerSubscripton, encodeURI(portalRunnerSubscripton));
|
||||||
|
testExplorerUrl.searchParams.append(
|
||||||
|
TestExplorerParams.portalRunnerResourceGroup,
|
||||||
|
encodeURI(portalRunnerResourceGroup)
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.goto(testExplorerUrl.toString());
|
||||||
|
|
||||||
|
const handle = await page.waitForSelector("iframe");
|
||||||
|
testExplorerFrame = await handle.contentFrame();
|
||||||
|
await testExplorerFrame.waitForSelector(".galleryHeader");
|
||||||
|
return testExplorerFrame;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadNotebookIfNotExist = async (frame: Frame, notebookName: string): Promise<ElementHandle<Element>> => {
|
||||||
|
const notebookNode = await getNotebookNode(frame, notebookName);
|
||||||
|
if (notebookNode) {
|
||||||
|
return notebookNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadNotebookPath = path.join(__dirname, "testNotebooks", notebookName);
|
||||||
|
const notebookResourceTree = await frame.waitForSelector(".notebookResourceTree");
|
||||||
|
const treeNodeHeadersBeforeUpload = await notebookResourceTree.$$(".treeNodeHeader");
|
||||||
|
|
||||||
|
const ellipses = await treeNodeHeadersBeforeUpload[2].$("button");
|
||||||
|
await ellipses.click();
|
||||||
|
|
||||||
|
await frame.waitFor(RENDER_DELAY);
|
||||||
|
|
||||||
|
const menuItems = await frame.$$(".ms-ContextualMenu-item");
|
||||||
|
await menuItems[4].click();
|
||||||
|
|
||||||
|
const uploadFileButton = await frame.waitForSelector("#importFileButton");
|
||||||
|
uploadFileButton.click();
|
||||||
|
|
||||||
|
const fileChooser = await page.waitForFileChooser();
|
||||||
|
fileChooser.accept([uploadNotebookPath]);
|
||||||
|
|
||||||
|
const submitButton = await frame.waitForSelector("#uploadFileButton");
|
||||||
|
await submitButton.click();
|
||||||
|
|
||||||
|
await frame.waitFor(NOTEBOOK_OPERATION_DELAY);
|
||||||
|
return await getNotebookNode(frame, notebookName);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getNotebookNode = async (frame: Frame, uploadNotebookName: string): Promise<ElementHandle<Element>> => {
|
||||||
|
const notebookResourceTree = await frame.waitForSelector(".notebookResourceTree");
|
||||||
|
let currentNotebookNode: ElementHandle<Element>;
|
||||||
|
|
||||||
|
const treeNodeHeaders = await notebookResourceTree.$$(".treeNodeHeader");
|
||||||
|
for (let i = 1; i < treeNodeHeaders.length; i++) {
|
||||||
|
currentNotebookNode = treeNodeHeaders[i];
|
||||||
|
const nodeLabel = await currentNotebookNode.$eval(".nodeLabel", element => element.textContent);
|
||||||
|
if (nodeLabel === uploadNotebookName) {
|
||||||
|
return currentNotebookNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user