Compare commits

..

2 Commits

Author SHA1 Message Date
Steve Faulkner
627c346559 More cleanup 2020-11-11 15:45:32 -06:00
Steve Faulkner
415ebc505b Remove RU Max Limits 2020-11-11 13:50:17 -06:00
112 changed files with 10654 additions and 5035 deletions

View File

@@ -3,11 +3,7 @@ PORTAL_RUNNER_PASSWORD=
PORTAL_RUNNER_SUBSCRIPTION=
PORTAL_RUNNER_RESOURCE_GROUP=
PORTAL_RUNNER_DATABASE_ACCOUNT=
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY=
PORTAL_RUNNER_CONNECTION_STRING=
NOTEBOOKS_TEST_RUNNER_TENANT_ID=
NOTEBOOKS_TEST_RUNNER_CLIENT_ID=
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET=
CASSANDRA_CONNECTION_STRING=
MONGO_CONNECTION_STRING=
TABLES_CONNECTION_STRING=

View File

@@ -202,6 +202,8 @@ src/Explorer/Tabs/QueryTab.test.ts
src/Explorer/Tabs/QueryTab.ts
src/Explorer/Tabs/QueryTablesTab.ts
src/Explorer/Tabs/ScriptTabBase.ts
src/Explorer/Tabs/SettingsTab.test.ts
src/Explorer/Tabs/SettingsTab.ts
src/Explorer/Tabs/SparkMasterTab.ts
src/Explorer/Tabs/StoredProcedureTab.ts
src/Explorer/Tabs/TabComponents.ts
@@ -288,6 +290,8 @@ src/Utils/DatabaseAccountUtils.ts
src/Utils/JunoUtils.ts
src/Utils/MessageValidation.ts
src/Utils/NotebookConfigurationUtils.ts
src/Utils/OfferUtils.test.ts
src/Utils/OfferUtils.ts
src/Utils/PricingUtils.test.ts
src/Utils/QueryUtils.test.ts
src/Utils/QueryUtils.ts
@@ -392,5 +396,19 @@ src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx
src/GalleryViewer/Cards/GalleryCardComponent.tsx
src/GalleryViewer/GalleryViewer.tsx
src/GalleryViewer/GalleryViewerComponent.tsx
cypress/integration/dataexplorer/CASSANDRA/addCollection.spec.ts
cypress/integration/dataexplorer/GRAPH/addCollection.spec.ts
cypress/integration/dataexplorer/ci-tests/addCollectionPane.spec.ts
cypress/integration/dataexplorer/ci-tests/createDatabase.spec.ts
cypress/integration/dataexplorer/ci-tests/deleteCollection.spec.ts
cypress/integration/dataexplorer/ci-tests/deleteDatabase.spec.ts
cypress/integration/dataexplorer/MONGO/addCollection.spec.ts
cypress/integration/dataexplorer/MONGO/addCollectionAutopilot.spec.ts
cypress/integration/dataexplorer/MONGO/addCollectionExistingDatabase.spec.ts
cypress/integration/dataexplorer/MONGO/provisionDatabaseThroughput.spec.ts
cypress/integration/dataexplorer/SQL/addCollection.spec.ts
cypress/integration/dataexplorer/TABLE/addCollection.spec.ts
cypress/integration/notebook/newNotebook.spec.ts
cypress/integration/notebook/resourceTree.spec.ts
__mocks__/monaco-editor.ts
src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx

View File

@@ -79,31 +79,32 @@ jobs:
name: dist
path: dist/
endtoendemulator:
name: "End To End Emulator Tests"
name: "End To End Tests | Emulator | SQL"
needs: [lint, format, compile, unittest]
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- uses: southpolesteve/cosmos-emulator-github-action@v1
- name: Use Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: 12.x
- uses: southpolesteve/cosmos-emulator-github-action@v1
- name: Restore Cypress Binary Cache
uses: actions/cache@v2
with:
path: ~/.cache/Cypress
key: ${{ runner.os }}-cypress-binary-cache
- name: End to End Tests
run: |
npm ci
npm start &
npm run wait-for-server
npx jest -c ./jest.config.e2e.js --detectOpenHandles sql
npm ci --prefix ./cypress
npm run test:ci --prefix ./cypress -- --spec ./integration/dataexplorer/ci-tests/createDatabase.spec.ts
shell: bash
env:
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/explorer.html?platform=Emulator"
PLATFORM: "Emulator"
EMULATOR_ENDPOINT: https://0.0.0.0:8081/
NODE_TLS_REJECT_UNAUTHORIZED: 0
- uses: actions/upload-artifact@v2
with:
name: screenshots
path: failed-*
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
accessibility:
name: "Accessibility | Hosted"
needs: [lint, format, compile, unittest]
@@ -122,13 +123,13 @@ jobs:
sudo sysctl -p
npm ci
npm start &
npx wait-on -i 5000 https-get://0.0.0.0:1234/
npx wait-on -i 5000 https-get://0.0.0.0:1234/
node utils/accesibilityCheck.js
shell: bash
env:
NODE_TLS_REJECT_UNAUTHORIZED: 0
endtoendhosted:
name: "End to End Hosted Tests"
endtoendpuppeteer:
name: "End to end puppeteer tests"
needs: [lint, format, compile, unittest]
runs-on: ubuntu-latest
steps:
@@ -137,7 +138,7 @@ jobs:
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: End to End Hosted Tests
- name: End to End Puppeteer Tests
run: |
npm ci
npm start &
@@ -146,13 +147,6 @@ jobs:
shell: bash
env:
NODE_TLS_REJECT_UNAUTHORIZED: 0
PORTAL_RUNNER_SUBSCRIPTION: ${{ secrets.PORTAL_RUNNER_SUBSCRIPTION }}
PORTAL_RUNNER_RESOURCE_GROUP: ${{ secrets.PORTAL_RUNNER_RESOURCE_GROUP }}
PORTAL_RUNNER_DATABASE_ACCOUNT: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT }}
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY }}
NOTEBOOKS_TEST_RUNNER_TENANT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_TENANT_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
PORTAL_RUNNER_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }}
@@ -165,7 +159,7 @@ jobs:
nuget:
name: Publish Nuget
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted]
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendpuppeteer]
runs-on: ubuntu-latest
env:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
@@ -189,7 +183,7 @@ jobs:
nugetmpac:
name: Publish Nuget MPAC
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted]
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendpuppeteer]
runs-on: ubuntu-latest
env:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}

3
.gitignore vendored
View File

@@ -9,6 +9,9 @@ pkg/DataExplorer/*
test/out/*
workers/**/*.js
*.trx
cypress/videos
cypress/screenshots
cypress/fixtures
notebookapp/*
Contracts/*
.DS_Store

View File

@@ -76,7 +76,17 @@ Unit tests are located adjacent to the code under test and run with [Jest](https
#### End to End CI Tests
Jest and Puppeteer are used for end to end browser based tests and are contained in `test/`. To run these tests locally:
[Cypress](https://www.cypress.io/) is used for end to end tests and are contained in `cypress/`. Currently, it operates as sub project with its own typescript config and dependencies. It also only operates against the emulator. To run cypress tests:
1. Ensure the emulator is running
2. Start cosmos explorer in emulator mode: `PLATFORM=Emulator npm run watch`
3. Move into `cypress/` folder: `cd cypress`
4. Install dependencies: `npm install`
5. Run cypress headless(`npm run test`) or in interactive mode(`npm run test:debug`)
#### End to End Production Tests
Jest and Puppeteer are used for end to end production runners and are contained in `test/`. To run these tests locally:
1. Copy .env.example to .env
2. Update the values in .env including your local data explorer endpoint (ask a teammate/codeowner for help with .env values)

4
cypress/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
cypress.env.json
cypress/report
cypress/screenshots
cypress/videos

51
cypress/cleanup.js Normal file
View File

@@ -0,0 +1,51 @@
// Cleans up old databases from previous test runs
const { CosmosClient } = require("@azure/cosmos");
// TODO: Add support for other API connection strings
const mongoRegex = RegExp("mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com");
async function cleanup() {
const connectionString = process.env.CYPRESS_CONNECTION_STRING;
if (!connectionString) {
throw new Error("Connection string not provided");
}
let client;
switch (true) {
case connectionString.includes("mongodb://"): {
const [, key, accountName] = connectionString.match(mongoRegex);
client = new CosmosClient({
key,
endpoint: `https://${accountName}.documents.azure.com:443/`
});
break;
}
// TODO: Add support for other API connection strings
default:
client = new CosmosClient(connectionString);
break;
}
const response = await client.databases.readAll().fetchAll();
return Promise.all(
response.resources.map(async db => {
const dbTimestamp = new Date(db._ts * 1000);
const twentyMinutesAgo = new Date(Date.now() - 1000 * 60 * 20);
if (dbTimestamp < twentyMinutesAgo) {
await client.database(db.id).delete();
console.log(`DELETED: ${db.id} | Timestamp: ${dbTimestamp}`);
} else {
console.log(`SKIPPED: ${db.id} | Timestamp: ${dbTimestamp}`);
}
})
);
}
cleanup()
.then(() => {
process.exit(0);
})
.catch(error => {
console.error(error);
process.exit(1);
});

15
cypress/cypress.json Normal file
View File

@@ -0,0 +1,15 @@
{
"integrationFolder": "./integration",
"pluginsFile": false,
"fixturesFolder": false,
"supportFile": "./support/index.js",
"defaultCommandTimeout": 90000,
"chromeWebSecurity": false,
"reporter": "mochawesome",
"reporterOptions": {
"reportDir": "cypress/report",
"json": true,
"overwrite": false,
"html": false
}
}

View File

@@ -0,0 +1,66 @@
// 1. Click on "New Container" on the command bar.
// 2. Pane with the title "Add Container" should appear on the right side of the screen
// 3. It includes an input box for the database Id.
// 4. It includes a checkbox called "Create now".
// 5. When the checkbox is marked, enter new database id.
// 3. Create a database WITH "Provision throughput" checked.
// 4. Enter minimum throughput value of 400.
// 5. Enter container id to the container id text box.
// 6. Enter partition key to the partition key text box.
// 7. Click "OK" to create a new container.
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
const connectionString = require("../../../utilities/connectionString");
let crypt = require("crypto");
context("Cassandra API Test - createDatabase", () => {
beforeEach(() => {
connectionString.loginUsingConnectionString(connectionString.constants.cassandra);
});
it("Create a new table in Cassandra API", () => {
const keyspaceId = `KeyspaceId${crypt.randomBytes(8).toString("hex")}`;
const tableId = `TableId112`;
cy.get("iframe").then($element => {
const $body = $element.contents().find("body");
cy.wrap($body)
.find('div[class="commandBarContainer"]')
.should("be.visible")
.find('button[data-test="New Table"]')
.should("be.visible")
.click();
cy.wrap($body)
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body)
.find('input[id="keyspace-id"]')
.should("be.visible")
.type(keyspaceId);
cy.wrap($body)
.find('input[class="textfontclr"]')
.type(tableId);
cy.wrap($body)
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body)
.find('data-test="addCollection-createCollection"')
.click();
cy.wait(10000);
cy.wrap($body)
.find('div[data-test="resourceTreeId"]')
.should("exist")
.find('div[class="treeComponent dataResourceTree"]')
.should("contain", tableId);
});
});
});

View File

@@ -0,0 +1,81 @@
// 1. Click on "New Graph" on the command bar.
// 2. Pane with the title "Add Container" should appear on the right side of the screen
// 3. It includes an input box for the database Id.
// 4. It includes a checkbox called "Create now".
// 5. When the checkbox is marked, enter new database id.
// 3. Create a database WITH "Provision throughput" checked.
// 4. Enter minimum throughput value of 400.
// 5. Enter container id to the container id text box.
// 6. Enter partition key to the partition key text box.
// 7. Click "OK" to create a new container.
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
const connectionString = require("../../../utilities/connectionString");
let crypt = require("crypto");
context("Graph API Test", () => {
beforeEach(() => {
connectionString.loginUsingConnectionString(connectionString.constants.graph);
});
it("Create a new graph in Graph API", () => {
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
const graphId = `TestGraph${crypt.randomBytes(8).toString("hex")}`;
const partitionKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
cy.get("iframe").then($element => {
const $body = $element.contents().find("body");
cy.wrap($body)
.find('div[class="commandBarContainer"]')
.should("be.visible")
.find('button[data-test="New Graph"]')
.should("be.visible")
.click();
cy.wrap($body)
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body)
.find('input[data-test="addCollection-createNewDatabase"]')
.check();
cy.wrap($body)
.find('input[data-test="addCollection-newDatabaseId"]')
.should("be.visible")
.type(dbId);
cy.wrap($body)
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
.check();
cy.wrap($body)
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body)
.find('input[data-test="addCollection-collectionId"]')
.type(graphId);
cy.wrap($body)
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(partitionKey);
cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000);
cy.wrap($body)
.find('div[data-test="resourceTreeId"]')
.should("exist")
.find('div[class="treeComponent dataResourceTree"]')
.should("contain", dbId)
.click()
.should("contain", graphId);
});
});
});

View File

@@ -0,0 +1,80 @@
// 1. Click on "New Container" on the command bar.
// 2. Pane with the title "Add Container" should appear on the right side of the screen
// 3. It includes an input box for the database Id.
// 4. It includes a checkbox called "Create now".
// 5. When the checkbox is marked, enter new database id.
// 3. Create a database WITH "Provision throughput" checked.
// 4. Enter minimum throughput value of 400.
// 5. Enter container id to the container id text box.
// 6. Enter partition key to the partition key text box.
// 7. Click "OK" to create a new container.
// // 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
const connectionString = require("../../../utilities/connectionString");
let crypt = require("crypto");
context("Mongo API Test - createDatabase", () => {
beforeEach(() => {
connectionString.loginUsingConnectionString();
});
it("Create a new collection in Mongo API", () => {
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
cy.get("iframe").then($element => {
const $body = $element.contents().find("body");
cy.wrap($body)
.find('div[class="commandBarContainer"]')
.should("be.visible")
.find('button[data-test="New Collection"]')
.should("be.visible")
.click();
cy.wrap($body)
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body)
.find('input[data-test="addCollection-createNewDatabase"]')
.check();
cy.wrap($body)
.find('input[data-test="addCollection-newDatabaseId"]')
.type(dbId);
cy.wrap($body)
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
.check();
cy.wrap($body)
.find('input[data-test="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body)
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body)
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(sharedKey);
cy.wrap($body)
.find("#submitBtnAddCollection")
.click();
cy.wait(10000);
cy.wrap($body)
.find('div[data-test="resourceTreeId"]')
.should("exist")
.find('div[class="treeComponent dataResourceTree"]')
.should("contain", dbId)
.click()
.should("contain", collectionId);
});
});
});

View File

@@ -0,0 +1,96 @@
// 1. Click on "New Container" on the command bar.
// 2. Pane with the title "Add Container" should appear on the right side of the screen
// 3. It includes an input box for the database Id.
// 4. It includes a checkbox called "Create now".
// 5. When the checkbox is marked, enter new database id.
// 3. Create a database WITH "Provision throughput" checked.
// 4. Enter minimum throughput value of 400.
// 5. Enter container id to the container id text box.
// 6. Enter partition key to the partition key text box.
// 7. Click "OK" to create a new container.
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
const connectionString = require("../../../utilities/connectionString");
let crypt = require("crypto");
context("Mongo API Test", () => {
beforeEach(() => {
connectionString.loginUsingConnectionString();
});
it.skip("Create a new collection in Mongo API - Autopilot", () => {
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
cy.get("iframe").then($element => {
const $body = $element.contents().find("body");
cy.wrap($body)
.find('div[class="commandBarContainer"]')
.should("be.visible")
.find('button[data-test="New Collection"]')
.should("be.visible")
.click();
cy.wrap($body)
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body)
.find('input[data-test="addCollection-createNewDatabase"]')
.check();
cy.wrap($body)
.find('input[data-test="addCollection-newDatabaseId"]')
.type(dbId);
cy.wrap($body)
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
.check();
cy.wrap($body)
.find('div[class="throughputModeContainer"]')
.should("be.visible")
.and(input => {
expect(input.get(0).textContent, "first item").contains("Autopilot (preview)");
expect(input.get(1).textContent, "second item").contains("Manual");
});
cy.wrap($body)
.find('input[id="newContainer-databaseThroughput-autoPilotRadio"]')
.check();
cy.wrap($body)
.find('select[name="autoPilotTiers"]')
// .eq(1).should('contain', '4,000 RU/s');
// // .select('4,000 RU/s').should('have.value', '1');
.find('option[value="2"]')
.then($element => $element.get(1).setAttribute("selected", "selected"));
cy.wrap($body)
.find('input[data-test="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body)
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(sharedKey);
cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000);
cy.wrap($body)
.find('div[data-test="resourceTreeId"]')
.should("exist")
.find('div[class="treeComponent dataResourceTree"]')
.should("contain", dbId)
.click()
.should("contain", collectionId);
});
});
});

View File

@@ -0,0 +1,67 @@
const connectionString = require("../../../utilities/connectionString");
let crypt = require("crypto");
context("Mongo API Test", () => {
beforeEach(() => {
connectionString.loginUsingConnectionString();
});
it.skip("Create a new collection in existing database in Mongo API", () => {
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
cy.get("iframe").then($element => {
const $body = $element.contents().find("body");
cy.wrap($body)
.find('span[class="nodeLabel"]')
.should("be.visible")
.then($span => {
const dbId1 = $span.text();
cy.log("DBBB", dbId1);
cy.wrap($body)
.find('div[class="commandBarContainer"]')
.should("be.visible")
.find('button[data-test="New Collection"]')
.should("be.visible")
.click();
cy.wrap($body)
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body)
.find('input[data-test="addCollection-existingDatabase"]')
.check();
cy.wrap($body)
.find('input[data-test="addCollection-existingDatabase"]')
.type(dbId1);
cy.wrap($body)
.find('input[data-test="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body)
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(sharedKey);
cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000);
cy.wrap($body)
.find('div[data-test="resourceTreeId"]')
.should("exist")
.find('div[class="treeComponent dataResourceTree"]')
.click()
.should("contain", collectionId);
});
});
});
});

View File

@@ -0,0 +1,203 @@
const connectionString = require("../../../utilities/connectionString");
let crypt = require("crypto");
context.skip("Mongo API Test", () => {
beforeEach(() => {
connectionString.loginUsingConnectionString();
});
it("Create a new collection in Mongo API - Provision database throughput", () => {
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
cy.get("iframe").then($element => {
const $body = $element.contents().find("body");
cy.wrap($body)
.find('div[class="commandBarContainer"]')
.should("be.visible")
.find('button[data-test="New Collection"]')
.should("be.visible")
.click();
cy.wrap($body)
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body)
.find(".createNewDatabaseOrUseExisting")
.should("have.length", 2)
.and(input => {
expect(input.get(0).textContent, "first item").contains("Create new");
expect(input.get(1).textContent, "second item").contains("Use existing");
});
cy.wrap($body)
.find('input[data-test="addCollection-createNewDatabase"]')
.check();
cy.wrap($body)
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
.check();
cy.wrap($body)
.find('input[data-test="addCollection-newDatabaseId"]')
.type(dbId);
cy.wrap($body)
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
.check();
cy.wrap($body)
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body)
.find('input[data-test="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body)
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(sharedKey);
cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000);
cy.wrap($body)
.find('div[data-test="resourceTreeId"]')
.should("exist")
.find('div[class="treeComponent dataResourceTree"]')
.should("contain", dbId)
.click()
.should("contain", collectionId);
});
});
it("Create a new collection - without provision database throughput", () => {
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
const collectionIdTitle = `Add Collection`;
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
cy.get("iframe").then($element => {
const $body = $element.contents().find("body");
cy.wrap($body)
.find('div[class="commandBarContainer"]')
.should("be.visible")
.find('button[data-test="New Collection"]')
.should("be.visible")
.click();
cy.wrap($body)
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body)
.find('input[data-test="addCollection-createNewDatabase"]')
.check();
cy.wrap($body)
.find('input[data-test="addCollection-newDatabaseId"]')
.type(dbId);
cy.wrap($body)
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
.uncheck();
cy.wrap($body)
.find('input[data-test="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body)
.find('input[id="tab2"]')
.check({ force: true });
cy.wrap($body)
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(sharedKey);
cy.wrap($body)
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000);
cy.wrap($body)
.find('div[data-test="resourceTreeId"]')
.should("exist")
.find('div[class="treeComponent dataResourceTree"]')
.should("contain", dbId)
.click()
.should("contain", collectionId);
});
});
it("Create a new collection - without provision database throughput Fixed Storage Capacity", () => {
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
cy.get("iframe").then($element => {
const $body = $element.contents().find("body");
cy.wrap($body)
.find('div[class="commandBarContainer"]')
.should("be.visible")
.find('button[data-test="New Collection"]')
.should("be.visible")
.click();
cy.wrap($body)
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body)
.find('input[data-test="addCollection-createNewDatabase"]')
.check();
cy.wrap($body)
.find('input[data-test="addCollection-newDatabaseId"]')
.type(dbId);
cy.wrap($body)
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
.uncheck();
cy.wrap($body)
.find('input[data-test="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body)
.find('input[id="tab1"]')
.check({ force: true });
cy.wrap($body)
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000);
cy.wrap($body)
.find('div[data-test="resourceTreeId"]')
.should("exist")
.find('div[class="treeComponent dataResourceTree"]')
.should("contain", dbId)
.click()
.should("contain", collectionId);
});
});
});

View File

@@ -0,0 +1,79 @@
// 1. Click on "New Container" on the command bar.
// 2. Pane with the title "Add Container" should appear on the right side of the screen
// 3. It includes an input box for the database Id.
// 4. It includes a checkbox called "Create now".
// 5. When the checkbox is marked, enter new database id.
// 3. Create a database WITH "Provision throughput" checked.
// 4. Enter minimum throughput value of 400.
// 5. Enter container id to the container id text box.
// 6. Enter partition key to the partition key text box.
// 7. Click "OK" to create a new container.
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
const connectionString = require("../../../utilities/connectionString");
let crypt = require("crypto");
context("SQL API Test", () => {
beforeEach(() => {
connectionString.loginUsingConnectionString();
});
it("Create a new container in SQL API", () => {
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
connectionString.loginUsingConnectionString();
cy.get("iframe").then($element => {
const $body = $element.contents().find("body");
cy.wrap($body)
.find('div[class="commandBarContainer"]')
.should("be.visible")
.find('button[data-test="New Container"]')
.should("be.visible")
.click();
cy.wrap($body)
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body)
.find('input[data-test="addCollection-createNewDatabase"]')
.check();
cy.wrap($body)
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
.check();
cy.wrap($body)
.find('input[data-test="addCollection-newDatabaseId"]')
.type(dbId);
cy.wrap($body)
.find('input[data-test="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body)
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body)
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(sharedKey);
cy.wrap($body)
.find("#submitBtnAddCollection")
.click();
cy.wait(10000);
cy.wrap($body)
.find('div[data-test="resourceTreeId"]')
.should("exist")
.find('div[class="treeComponent dataResourceTree"]')
.should("contain", dbId);
});
});
});

View File

@@ -0,0 +1,60 @@
// 1. Click on "New Container" on the command bar.
// 2. Pane with the title "Add Container" should appear on the right side of the screen
// 3. It includes an input box for the database Id.
// 4. It includes a checkbox called "Create now".
// 5. When the checkbox is marked, enter new database id.
// 3. Create a database WITH "Provision throughput" checked.
// 4. Enter minimum throughput value of 400.
// 5. Enter container id to the container id text box.
// 6. Enter partition key to the partition key text box.
// 7. Click "OK" to create a new container.
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
const connectionString = require("../../../utilities/connectionString");
let crypt = require("crypto");
context("Table API Test", () => {
beforeEach(() => {
connectionString.loginUsingConnectionString(connectionString.constants.table);
});
it("Create a new table in Table API", () => {
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
cy.get("iframe").then($element => {
const $body = $element.contents().find("body");
cy.wrap($body)
.find('div[class="commandBarContainer"]')
.should("be.visible")
.find('button[data-test="New Table"]')
.should("be.visible")
.click();
cy.wrap($body)
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body)
.find('input[data-test="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body)
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000);
cy.wrap($body)
.find('div[data-test="resourceTreeId"]')
.should("exist")
.find('div[class="treeComponent dataResourceTree"]')
.should("contain", collectionId);
});
});
});

View File

@@ -0,0 +1,55 @@
// 1. Click on "New Container" on the command bar.
// 2. Pane with the title "Add Container" should appear on the right side of the screen
// 3. It includes an input box for the database Id.
// 4. It includes a checkbox called "Create now".
// 5. When the checkbox is marked, enter new database id.
// 3. Create a database WITH "Provision throughput" checked.
// 4. Enter minimum throughput value of 400.
// 5. Enter container id to the container id text box.
// 6. Enter partition key to the partition key text box.
// 7. Click "OK" to create a new container.
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
let crypt = require("crypto");
context("Emulator - createDatabase", () => {
beforeEach(() => {
cy.visit("http://localhost:1234/explorer.html");
});
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
const collectionIdTitle = `Add Collection`;
const partitionKey = `PartitionKey${crypt.randomBytes(8).toString("hex")}`;
it("Create a new collection", () => {
cy.contains("New Container").click();
// cy.contains(collectionIdTitle);
cy.get(".createNewDatabaseOrUseExisting")
.should("have.length", 2)
.and(input => {
expect(input.get(0).textContent, "first item").contains("Create new");
expect(input.get(1).textContent, "second item").contains("Use existing");
});
cy.get('input[data-test="addCollection-createNewDatabase"]').check();
cy.get('input[data-test="addCollection-newDatabaseId"]').type(dbId);
cy.get('input[data-test="addCollection-collectionId"]').type(collectionId);
cy.get('input[data-test="databaseThroughputValue"]').should("have.value", "400");
cy.get('input[data-test="addCollection-partitionKeyValue"]').type(partitionKey);
cy.get('input[data-test="addCollection-createCollection"]').click();
cy.get('div[data-test="resourceTreeId"]').should("exist");
cy.get('div[data-test="resourceTree-collectionsTree"]').should("contain", dbId);
cy.get('div[data-test="databaseList"]').should("contain", collectionId);
});
});

View File

@@ -0,0 +1,65 @@
// 1. Click on "New Database" on the command bar
// 2. a Pane with the title "Add Database" should appear on the right side of the screen
// i. It includes an input box for the database Id.
// ii. It includes a checkbox called "Provision throughput".
// iii. Whe the checkbox is marked, a new input with a throughput control let's you customize RU at the database level
// 3. Create a database WITHOUT "Provision throughput" checked.
// 4. It should appear in the Data Explorer list.
// 5. Repeat steps 1-3 but create a database WITH "Provision throughput" with the default RU value.
// 6. It should appear in the Data Explorer list.
// 7. If expanded, it should have the list item called "Scale", that once clicked, it should show the "Scale" tab.
// 8. Inside that tab, a throughput control will let you change the RU value within the permited range.
// 9. If you change the value, it should enable the "Save" button.
// 10. Click "Save" and verify that the process completes without error.
// 11. Close the tab and reopen it and verify that the input contains the last saved value.%
const crypto = require("crypto");
const client = require("../../../utilities/cosmosClient");
const randomString = crypto.randomBytes(2).toString("hex");
const databaseId = `TestDB-${randomString}`;
const collectionId = `TestColl-${randomString}`;
context("Emulator - Create database -> container -> item", () => {
beforeEach(async () => {
const { resources } = await client.databases.readAll().fetchAll();
for (const database of resources) {
await client.database(database.id).delete();
}
});
it("creates a new database", () => {
cy.visit("https://0.0.0.0:1234/explorer.html?platform=Emulator");
cy.contains("New Container").click();
cy.get("[data-test=addCollection-newDatabaseId]").click();
cy.get("[data-test=addCollection-newDatabaseId]").type(databaseId);
cy.get("[data-test=addCollection-collectionId]").click();
cy.get("[data-test=addCollection-collectionId]").type(collectionId);
cy.get("[data-test=addCollection-partitionKeyValue]").click();
cy.get("[data-test=addCollection-partitionKeyValue]").type("/pk");
cy.get('input[name="createCollection"]').click();
cy.get(".dataResourceTree").should("contain", databaseId);
cy.get(".dataResourceTree")
.contains(databaseId)
.click();
cy.get(".dataResourceTree").should("contain", collectionId);
cy.get(".dataResourceTree")
.contains(collectionId)
.click();
cy.get(".dataResourceTree")
.contains("Items")
.click();
cy.get(".dataResourceTree")
.contains("Items")
.click();
cy.wait(1000); // React rendering inside KO causes some weird async rendering that makes this test flaky without waiting
cy.get(".commandBarContainer")
.contains("New Item")
.click();
cy.wait(1000); // React rendering inside KO causes some weird async rendering that makes this test flaky without waiting
cy.get(".commandBarContainer")
.contains("Save")
.click();
cy.wait(1000); // React rendering inside KO causes some weird async rendering that makes this test flaky without waiting
cy.get(".documentsGridHeaderContainer").should("contain", "replace_with_new_document_id");
});
});

View File

@@ -0,0 +1,46 @@
// 1. Click last database in the resource tree
// 2. Click the last collection within the database
// 3. Select the context menu within the collection
// 4. Select "Delete Container" option in the dropdown
// 5. On Selection, Delete Container pane opens on the right side
// 6. Enter the same collection id that is to be deleted and click ok
// 7. Now, the resource tree refreshes, the deleted collection should not appear under the database
let crypt = require("crypto");
context("Emulator - deleteCollection", () => {
beforeEach(() => {
cy.visit("http://localhost:1234/explorer.html");
});
it("Delete a collection", () => {
cy.get(".databaseId")
.last()
.click();
cy.get(".collectionList")
.last()
.then($id => {
const collectionId = $id.text();
cy.get('span[data-test="collectionEllipsisMenu"]').should("exist");
cy.get('span[data-test="collectionEllipsisMenu"]')
.invoke("show")
.last()
.click();
cy.get('div[data-test="collectionContextMenu"]')
.contains("Delete Container")
.click({ force: true });
cy.get('input[data-test="confirmCollectionId"]').type(collectionId.trim());
cy.get('input[data-test="deleteCollection"]').click();
cy.get('div[data-test="databaseList"]').should("not.contain", collectionId);
cy.get('div[data-test="databaseMenu"]').should("not.contain", collectionId);
});
});
});

View File

@@ -0,0 +1,83 @@
// 1. Click last database in the resource tree
// 2. Select the context menu within the database
// 4. Select "Delete Database" option in the dropdown
// 5. On Selection, Delete Database pane opens on the right side
// 6. Enter the same database id that is to be deleted and click ok
// 7. Now, the resource tree refreshes, the deleted database should not appear in the resource tree
let crypt = require("crypto");
context("Emulator - deleteDatabase", () => {
beforeEach(() => {
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
let db_rid = "";
const date = new Date().toUTCString();
let authToken = "";
cy.visit("http://localhost:1234/explorer.html");
// Creating auth token for collection creation
cy.request({
method: "GET",
url: "https://localhost:8081/_explorer/authorization/post/dbs/",
headers: {
"x-ms-date": date,
authorization: "-"
}
})
.then(response => {
authToken = response.body.Token; // Getting auth token for collection creation
return new Cypress.Promise((resolve, reject) => {
return resolve();
});
})
.then(() => {
cy.request({
method: "POST",
url: "https://localhost:8081/dbs",
headers: {
"x-ms-date": date,
authorization: authToken,
"x-ms-version": "2018-12-31"
},
body: {
id: dbId
}
}).then(response => {
cy.log("Response", response);
db_rid = response.body._rid;
return new Cypress.Promise((resolve, reject) => {
cy.log("Rid", db_rid);
return resolve();
});
});
});
});
it("Delete a database", () => {
cy.get('span[data-test="refreshTree"]').click();
cy.get(".databaseId")
.last()
.then($id => {
const dbId = $id.text();
cy.get('span[data-test="databaseEllipsisMenu"]').should("exist");
cy.get('span[data-test="databaseEllipsisMenu"]')
.invoke("show")
.last()
.click();
cy.get('div[data-test="databaseContextMenu"]')
.contains("Delete Database")
.click({ force: true });
cy.get('input[data-test="confirmDatabaseId"]').type(dbId.trim());
cy.get('input[data-test="deleteDatabase"]').click();
cy.get('div[data-test="databaseList"]').should("not.contain", dbId);
});
});
});

View File

@@ -0,0 +1,35 @@
# Notebook end-to-end tests
This describes how to run the tests locally
## Stand up a local notebook container instance:
Instructions on how to build and run the container [here](https://microsoft.sharepoint.com/teams/DocDB/_layouts/OneNote.aspx?id=%2Fteams%2FDocDB%2FSiteAssets%2FDocDB%20Team%20Notebook&wd=target%28Tools%20_%20SDK%2FPortal%2FDevelopment.one%7CF800BE8E-1E31-48FE-90D7-EF698EF88112%2FHow%20to%20build%20notebook%20service%7C4BAA153B-422C-41E2-B997-F3FCE02CD743%2F%29)
## Run a local data explorer
Instructions are in [`DataExplorer/README.md`](https://msdata.visualstudio.com/CosmosDB/_git/cosmosdb-dataexplorer?path=%2FProduct%2FPortal%2FDataExplorer%2FREADME.md&_a=preview).
Make sure you can run Data Explorer locally from the web browser.
## Run cypress tests
1. Edit the URL for your DataExplorer in the `.spec.ts` file
2. Run the test:
```bash
cd DataExplorer/cypress
npm i
npm t -- --spec 'integration/notebook/newNotebook.spec.ts'
```
To run in Debug mode:
```
npm run test:debug
```
This opens Cypress UI
## Troubleshooting
* The tests are recorded in the `videos` folder.
* Cypress does not support hover: workarounds [here](https://docs.cypress.io/api/commands/hover.html#Workarounds).
## References
* [Cypress API](https://docs.cypress.io/api/api/table-of-contents.html)
* [Cypress cookbook](https://docs.cypress.io/faq/questions/using-cypress-faq.html#How-do-I-get-an-element%E2%80%99s-text-contents)
* [Cypress best practices](https://docs.cypress.io/guides/references/best-practices.html#Selecting-Elements)

View File

@@ -0,0 +1,93 @@
// THIS ADDS A NEW NOTEBOOK TO YOUR NOTEBOOKS
context("New Notebook smoke test", () => {
const timeout = 15000; // in ms
const explorerUrl =
"https://localhost:1234/explorer.html?feature.notebookserverurl=https%3A%2F%2Flocalhost%3A10001%2F12345%2Fnotebook&feature.notebookServerToken=token&feature.enablenotebooks=true";
/**
* Wait for UI to be ready
*/
const waitForReady = () => {
cy.get(".splashScreenContainer", { timeout }).should("be.visible");
};
beforeEach(() => {
cy.visit(explorerUrl);
waitForReady();
});
it("Create a new notebook and run some code", () => {
// Create new notebook
cy.contains("New Notebook").click();
// Check tab name
cy.get("li.tabList .tabNavText").should($span => {
const text = $span.text();
expect(text).to.match(/^Untitled.*\.ipynb$/);
});
// Wait for python3 | idle status
cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should($p => {
const text = $p.text();
expect(text).to.match(/^python3.*idle$/);
});
// Click on a cell
cy.get(".cell-container")
.as("cellContainer")
.click();
// Type in some code
cy.get("@cellContainer").type("2+4");
// Execute
cy.get('[data-test="Run"]')
.first()
.click();
// Verify results
cy.get("@cellContainer").within(() => {
cy.get("pre code span").should("contain", "6");
});
// Restart kernel
cy.get('[data-test="Run"] button')
.eq(-1)
.click();
cy.get("li")
.contains("Restart Kernel")
.click();
// Wait for python3 | restarting status
cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should($p => {
const text = $p.text();
expect(text).to.match(/^python3.*restarting$/);
});
// Wait for python3 | idle status
cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should($p => {
const text = $p.text();
expect(text).to.match(/^python3.*idle$/);
});
// Click on a cell
cy.get(".cell-container")
.as("cellContainer")
.find(".input")
.as("codeInput")
.click();
// Type in some code
cy.get("@codeInput").type("{backspace}{backspace}{backspace}4+5");
// Execute
cy.get('[data-test="Run"]')
.first()
.click();
// Verify results
cy.get("@cellContainer").within(() => {
cy.get("pre code span").should("contain", "9");
});
});
});

View File

@@ -0,0 +1,172 @@
context("Resource tree notebook file manipulation", () => {
const timeout = 15000; // in ms
const explorerUrl =
"https://localhost:1234/explorer.html?feature.notebookserverurl=https%3A%2F%2Flocalhost%3A10001%2F12345%2Fnotebook&feature.notebookServerToken=token&feature.enablenotebooks=true";
/**
* Wait for UI to be ready
*/
const waitForReady = () => {
cy.get(".splashScreenContainer", { timeout }).should("be.visible");
};
const clickContextMenuAndSelectOption = (nodeLabel, option) => {
cy.get(`.treeNodeHeader[data-test="${nodeLabel}"]`)
.find("button.treeMenuEllipsis")
.click();
cy.get('[data-test="treeComponentMenuItemContainer"]')
.contains(option)
.click();
};
const createFolder = folder => {
clickContextMenuAndSelectOption("My Notebooks/", "New Directory");
cy.get("#stringInputPane").within(() => {
cy.get('input[name="collectionIdConfirmation"]').type(folder);
cy.get("form").submit();
});
};
const deleteItem = nodeName => {
clickContextMenuAndSelectOption(`${nodeName}`, "Delete");
cy.get(".ms-Dialog-main")
.contains("Delete")
.click();
};
beforeEach(() => {
cy.visit(explorerUrl);
waitForReady();
});
it("Create and remove a directory", () => {
const folder = "e2etest_folder1";
createFolder(folder);
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).should("exist");
deleteItem(`${folder}/`);
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).should("not.exist");
});
it("Create and rename a directory", () => {
const folder = "e2etest_folder2";
const renamedFolder = "e2etest_folder2_renamed";
createFolder(folder);
// Rename
clickContextMenuAndSelectOption(`${folder}/`, "Rename");
cy.get("#stringInputPane").within(() => {
cy.get('input[name="collectionIdConfirmation"]')
.clear()
.type(renamedFolder);
cy.get("form").submit();
});
cy.get(`.treeNodeHeader[data-test="${renamedFolder}/"]`).should("exist");
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).should("not.exist");
deleteItem(`${renamedFolder}/`);
cy.get(`.treeNodeHeader[data-test="${renamedFolder}/"]`).should("not.exist");
});
it("Create a notebook inside a directory", () => {
const folder = "e2etest_folder3";
const newNotebookName = "Untitled.ipynb";
createFolder(folder);
clickContextMenuAndSelectOption(`${folder}/`, "New Notebook");
// Verify tab is open
cy.get(".tabList")
.contains(newNotebookName)
.should("exist");
// Close tab
cy.get(`.tabList[title="notebooks/${folder}/${newNotebookName}"]`)
.find(".cancelButton")
.click();
// When running from command line, closing the tab is too fast
cy.get("body").then($body => {
if ($body.find(".ms-Dialog-main").length) {
// For some reason, this does not work
// cy.get(".ms-Dialog-main").contains("Close").click();
cy.get(".ms-Dialog-main .ms-Button--primary").click();
}
});
// Expand folder node
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).click();
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("exist");
// Delete notebook
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`)
.find("button.treeMenuEllipsis")
.click();
cy.get('[data-test="treeComponentMenuItemContainer"]')
.contains("Delete")
.click();
// Confirm
cy.get(".ms-Dialog-main")
.contains("Delete")
.click();
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("not.exist");
deleteItem(`${folder}/`);
});
it("Create and rename a notebook inside a directory", () => {
const folder = "e2etest_folder4";
const newNotebookName = "Untitled.ipynb";
const renamedNotebookName = "mynotebook.ipynb";
createFolder(folder);
clickContextMenuAndSelectOption(`${folder}/`, "New Notebook");
// Close tab
cy.get(`.tabList[title="notebooks/${folder}/${newNotebookName}"]`)
.find(".cancelButton")
.click();
cy.get("body").then($body => {
if ($body.find(".ms-Dialog-main").length) {
// For some reason, this does not work
// cy.get(".ms-Dialog-main").contains("Close").click();
cy.get(".ms-Dialog-main .ms-Button--primary").click();
}
});
// Expand folder node
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).click();
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("exist");
// Rename notebook
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`)
.find("button.treeMenuEllipsis")
.click();
cy.get('[data-test="treeComponentMenuItemContainer"]')
.contains("Rename")
.click();
cy.get("#stringInputPane").within(() => {
cy.get('input[name="collectionIdConfirmation"]')
.clear()
.type(renamedNotebookName);
cy.get("form").submit();
});
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("not.exist");
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${renamedNotebookName}"]`).should("exist");
// Delete notebook
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${renamedNotebookName}"]`)
.find("button.treeMenuEllipsis")
.click();
cy.get('[data-test="treeComponentMenuItemContainer"]')
.contains("Delete")
.click();
// Confirm
cy.get(".ms-Dialog-main")
.contains("Delete")
.click();
// Give it time to settle
cy.wait(1000);
deleteItem(`${folder}/`);
});
});

3066
cypress/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
cypress/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "cosmos-explorer-cypress",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "cypress run",
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
"test:sql": "cypress run --browser chrome --spec \"./integration/dataexplorer/SQL/*\"",
"test:ci": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/ https-get://0.0.0.0:8081/_explorer/index.html && cypress run --browser edge --headless",
"test:debug": "cypress open"
},
"devDependencies": {
"cypress": "^4.8.0",
"mocha": "^7.0.1",
"mochawesome": "^4.1.0",
"mochawesome-merge": "^4.0.1",
"mochawesome-report-generator": "^4.1.0",
"typescript": "3.4.3",
"wait-on": "^4.0.2"
},
"dependencies": {
"@microsoft/applicationinsights-web": "^2.5.2"
}
}

23
cypress/support/index.js Normal file
View File

@@ -0,0 +1,23 @@
let appInsightsLib = require("@microsoft/applicationinsights-web");
const appInsights = new appInsightsLib.ApplicationInsights({
config: {
instrumentationKey: "fe61c39f-7d32-4488-a191-b13621965315"
/* ...Other Configuration Options... */
}
});
appInsights.loadAppInsights();
Cypress.on("fail", (error, runnable) => {
// App Insights will record the fail tests for Create Collection
let message = JSON.stringify(runnable.title);
appInsights.trackTrace({
message: `${message}`,
properties: {
passed: false,
error: error
}
});
throw error; // throw error to have test still fail
});

11
cypress/tsconfig.json Normal file
View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"strict": true,
"noEmit": true,
"module": "commonjs",
"target": "es5",
"lib": ["es5", "dom", "es6"],
"types": ["cypress", "node"]
},
"include": ["**/*.ts", "**/*.spec.ts"]
}

View File

@@ -0,0 +1,41 @@
module.exports = {
loginUsingConnectionString: function() {
const prodUrl = Cypress.env("TEST_ENDPOINT") || "https://localhost:1234/hostedExplorer.html";
const timeout = 15000;
cy.visit(prodUrl);
cy.get('iframe[id="explorerMenu"]').should("be.visible");
cy.get("iframe").then($element => {
const $body = $element.contents().find("body");
cy.wrap($body)
.find("#connectExplorer")
.should("exist")
.find("div[class='connectExplorer']")
.should("exist")
.find("p[class='welcomeText']")
.should("exist");
cy.wrap($body.find("div > p.switchConnectTypeText"))
.should("exist")
.last()
.click({ force: true });
const secret = Cypress.env("CONNECTION_STRING");
cy.wrap($body)
.find("input[class='inputToken']")
.should("exist")
.type(secret, {
force: true
});
cy.wrap($body.find("input[value='Connect']"), { timeout })
.first()
.click({ force: true });
cy.wait(15000);
});
}
};

View File

@@ -0,0 +1,6 @@
const { CosmosClient } = require("@azure/cosmos");
module.exports = new CosmosClient({
endpoint: "https://0.0.0.0:8081",
key: "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
});

4624
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,17 +4,15 @@
"description": "Cosmos Explorer",
"main": "index.js",
"dependencies": {
"@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "3.9.0",
"@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "1.1.0",
"@azure/cosmos-language-service": "0.0.4",
"@jupyterlab/services": "6.0.0-rc.2",
"@jupyterlab/terminal": "3.0.0-rc.2",
"@microsoft/applicationinsights-web": "2.5.9",
"@nteract/commutable": "7.3.2",
"@nteract/connected-components": "6.8.2",
"@nteract/core": "15.1.0",
"@nteract/data-explorer": "8.2.9",
"@nteract/data-explorer": "8.0.3",
"@nteract/directory-listing": "2.0.6",
"@nteract/dropdown-menu": "1.0.1",
"@nteract/editor": "10.1.2",
@@ -68,7 +66,7 @@
"jquery-ui-dist": "1.12.1",
"knockout": "3.5.1",
"mkdirp": "1.0.4",
"monaco-editor": "0.18.1",
"monaco-editor": "0.15.6",
"object.entries": "1.1.0",
"office-ui-fabric-react": "7.134.1",
"p-retry": "4.2.0",
@@ -117,7 +115,7 @@
"@types/prop-types": "15.5.8",
"@types/puppeteer": "3.0.1",
"@types/q": "1.5.1",
"@types/react": "16.9.56",
"@types/react": "16.9.49",
"@types/react-dom": "16.0.7",
"@types/react-notification-system": "0.2.39",
"@types/react-redux": "7.1.7",
@@ -196,8 +194,8 @@
"compile": "tsc",
"compile:contracts": "tsc -p ./tsconfig.contracts.json",
"compile:strict": "tsc -p ./tsconfig.strict.json",
"format": "prettier --write \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
"format:check": "prettier --check \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
"format": "prettier --write \"{src,cypress,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
"format:check": "prettier --check \"{src,cypress,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
"lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"",
"build:contracts": "npm run compile:contracts",
"strictEligibleFiles": "node ./strict-migration-tools/index.js",

View File

@@ -125,9 +125,7 @@ export class Features {
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
public static readonly ttl90Days = "ttl90days";
public static readonly enableRightPanelV2 = "enablerightpanelv2";
public static readonly enableSchema = "enableschema";
public static readonly enableSDKoperations = "enablesdkoperations";
public static readonly showMinRUSurvey = "showminrusurvey";
}
// flight names returned from the portal are always lowercase

View File

@@ -1,13 +1,26 @@
import { ConflictDefinition, FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import {
ConflictDefinition,
FeedOptions,
ItemDefinition,
OfferDefinition,
QueryIterator,
Resource
} from "@azure/cosmos";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import Q from "q";
import { configContext, Platform } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import * as ViewModels from "../Contracts/ViewModels";
import ConflictId from "../Explorer/Tree/ConflictId";
import DocumentId from "../Explorer/Tree/DocumentId";
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
import { OfferUtils } from "../Utils/OfferUtils";
import * as Constants from "./Constants";
import { client } from "./CosmosClient";
import * as HeadersUtility from "./HeadersUtility";
import { sendCachedDataMessage } from "./MessageHandler";
export function getCommonQueryOptions(options: FeedOptions): any {
const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage);

View File

@@ -1,7 +1,7 @@
import { ARMError } from "../Utils/arm/request";
import { HttpStatusCodes } from "./Constants";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import { SubscriptionType } from "../Contracts/SubscriptionType";
import { SubscriptionType } from "../Contracts/ViewModels";
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { logError } from "./Logger";
import { sendMessage } from "./MessageHandler";

View File

@@ -1,62 +0,0 @@
import * as OfferUtility from "./OfferUtility";
import { SDKOfferDefinition, Offer } from "../Contracts/DataModels";
import { OfferResponse } from "@azure/cosmos";
describe("parseSDKOfferResponse", () => {
it("manual throughput", () => {
const mockOfferDefinition = {
content: {
offerThroughput: 500,
collectionThroughputInfo: {
minimumRUForCollection: 400,
numPhysicalPartitions: 1
}
},
id: "test"
} as SDKOfferDefinition;
const mockResponse = {
resource: mockOfferDefinition
} as OfferResponse;
const expectedResult: Offer = {
manualThroughput: 500,
autoscaleMaxThroughput: undefined,
minimumThroughput: 400,
id: "test",
offerDefinition: mockOfferDefinition
};
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
});
it("autoscale throughput", () => {
const mockOfferDefinition = {
content: {
offerThroughput: 400,
collectionThroughputInfo: {
minimumRUForCollection: 400,
numPhysicalPartitions: 1
},
offerAutopilotSettings: {
maxThroughput: 5000
}
},
id: "test"
} as SDKOfferDefinition;
const mockResponse = {
resource: mockOfferDefinition
} as OfferResponse;
const expectedResult: Offer = {
manualThroughput: undefined,
autoscaleMaxThroughput: 5000,
minimumThroughput: 400,
id: "test",
offerDefinition: mockOfferDefinition
};
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
});
});

View File

@@ -1,33 +0,0 @@
import { Offer, SDKOfferDefinition } from "../Contracts/DataModels";
import { OfferResponse } from "@azure/cosmos";
export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => {
const offerDefinition: SDKOfferDefinition = offerResponse?.resource;
const offerContent = offerDefinition.content;
if (!offerContent) {
return undefined;
}
const minimumThroughput = offerContent.collectionThroughputInfo?.minimumRUForCollection;
const autopilotSettings = offerContent.offerAutopilotSettings;
if (autopilotSettings) {
return {
id: offerDefinition.id,
autoscaleMaxThroughput: autopilotSettings.maxThroughput,
manualThroughput: undefined,
minimumThroughput,
offerDefinition,
headers: offerResponse.headers
};
}
return {
id: offerDefinition.id,
autoscaleMaxThroughput: undefined,
manualThroughput: offerContent.offerThroughput,
minimumThroughput,
offerDefinition,
headers: offerResponse.headers
};
};

View File

@@ -16,7 +16,7 @@ const notificationsPath = () => {
};
export const fetchPortalNotifications = async (): Promise<DataModels.Notification[]> => {
if (configContext.platform === Platform.Emulator || configContext.platform === Platform.Hosted) {
if (configContext.platform === Platform.Emulator) {
return [];
}

View File

@@ -1,6 +1,9 @@
import * as DataModels from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels";
import { HttpHeaders } from "../Constants";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { getSqlContainerThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { getMongoDBCollectionThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
@@ -8,22 +11,50 @@ import { getCassandraTableThroughput } from "../../Utils/arm/generatedClients/20
import { getGremlinGraphThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { getTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { readOfferWithSDK } from "./readOfferWithSDK";
import { readOffers } from "./readOffers";
import { userContext } from "../../UserContext";
export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise<Offer> => {
export const readCollectionOffer = async (
params: DataModels.ReadCollectionOfferParams
): Promise<DataModels.OfferWithHeaders> => {
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
let offerId = params.offerId;
if (!offerId) {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
try {
offerId = await getCollectionOfferIdWithARM(params.databaseId, params.collectionId);
} catch (error) {
clearMessage();
if (error.code !== "NotFound") {
throw error;
}
return undefined;
}
} else {
offerId = await getCollectionOfferIdWithSDK(params.collectionResourceId);
if (!offerId) {
clearMessage();
return undefined;
}
}
}
const options: RequestOptions = {
initialHeaders: {
[HttpHeaders.populateCollectionThroughputInfo]: true
}
};
try {
if (
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
) {
return await readCollectionOfferWithARM(params.databaseId, params.collectionId);
}
return await readOfferWithSDK(params.offerId, params.collectionResourceId);
const response = await client()
.offer(offerId)
.read(options);
return (
response && {
...response.resource,
headers: response.headers
}
);
} catch (error) {
handleError(error, "ReadCollectionOffer", `Error while querying offer for collection ${params.collectionId}`);
throw error;
@@ -32,90 +63,61 @@ export const readCollectionOffer = async (params: ReadCollectionOfferParams): Pr
}
};
const readCollectionOfferWithARM = async (databaseId: string, collectionId: string): Promise<Offer> => {
const getCollectionOfferIdWithARM = async (databaseId: string, collectionId: string): Promise<string> => {
let rpResponse;
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const defaultExperience = userContext.defaultExperience;
let rpResponse;
try {
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
rpResponse = await getSqlContainerThroughput(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId
);
break;
case DefaultAccountExperienceType.MongoDB:
rpResponse = await getMongoDBCollectionThroughput(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId
);
break;
case DefaultAccountExperienceType.Cassandra:
rpResponse = await getCassandraTableThroughput(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId
);
break;
case DefaultAccountExperienceType.Graph:
rpResponse = await getGremlinGraphThroughput(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId
);
break;
case DefaultAccountExperienceType.Table:
rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId);
break;
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
return undefined;
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
rpResponse = await getSqlContainerThroughput(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId
);
break;
case DefaultAccountExperienceType.MongoDB:
rpResponse = await getMongoDBCollectionThroughput(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId
);
break;
case DefaultAccountExperienceType.Cassandra:
rpResponse = await getCassandraTableThroughput(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId
);
break;
case DefaultAccountExperienceType.Graph:
rpResponse = await getGremlinGraphThroughput(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId
);
break;
case DefaultAccountExperienceType.Table:
rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId);
break;
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
const resource = rpResponse?.properties?.resource;
if (resource) {
const offerId: string = rpResponse.name;
const minimumThroughput: number =
typeof resource.minimumThroughput === "string"
? parseInt(resource.minimumThroughput)
: resource.minimumThroughput;
const autoscaleSettings = resource.autoscaleSettings;
if (autoscaleSettings) {
return {
id: offerId,
autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
manualThroughput: undefined,
minimumThroughput
};
}
return {
id: offerId,
autoscaleMaxThroughput: undefined,
manualThroughput: resource.throughput,
minimumThroughput
};
}
return undefined;
return rpResponse?.name;
};
const getCollectionOfferIdWithSDK = async (collectionResourceId: string): Promise<string> => {
const offers = await readOffers();
const offer = offers.find(offer => offer.resource === collectionResourceId);
return offer?.id;
};

View File

@@ -1,45 +0,0 @@
import * as DataModels from "../../Contracts/DataModels";
import * as HeadersUtility from "../HeadersUtility";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContainerDefinition, Resource } from "@azure/cosmos";
import { HttpHeaders } from "../Constants";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
interface ResourceWithStatistics {
statistics: DataModels.Statistic[];
}
export const readCollectionQuotaInfo = async (
collection: ViewModels.Collection
): Promise<DataModels.CollectionQuotaInfo> => {
const clearMessage = logConsoleProgress(`Querying containers for database ${collection.id}`);
const options: RequestOptions = {};
options.populateQuotaInfo = true;
options.initialHeaders = options.initialHeaders || {};
options.initialHeaders[HttpHeaders.populatePartitionStatistics] = true;
try {
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.read(options);
const quota: DataModels.CollectionQuotaInfo = HeadersUtility.getQuota(response.headers);
const resource = response.resource as ContainerDefinition & Resource & ResourceWithStatistics;
quota["usageSizeInKB"] = resource.statistics.reduce(
(previousValue: number, currentValue: DataModels.Statistic) => previousValue + currentValue.sizeInKB,
0
);
quota["numPartitions"] = resource.statistics.length;
quota["uniqueKeyPolicy"] = collection.uniqueKeyPolicy; // TODO: Remove after refactoring (#119617)
return quota;
} catch (error) {
handleError(error, "ReadCollectionQuotaInfo", `Error while querying quota info for container ${collection.id}`);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -1,28 +1,51 @@
import * as DataModels from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
import { HttpHeaders } from "../Constants";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { readOfferWithSDK } from "./readOfferWithSDK";
import { readOffers } from "./readOffers";
import { userContext } from "../../UserContext";
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
export const readDatabaseOffer = async (
params: DataModels.ReadDatabaseOfferParams
): Promise<DataModels.OfferWithHeaders> => {
const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`);
let offerId = params.offerId;
if (!offerId) {
offerId = await (window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
? getDatabaseOfferIdWithARM(params.databaseId)
: getDatabaseOfferIdWithSDK(params.databaseResourceId));
if (!offerId) {
clearMessage();
return undefined;
}
}
const options: RequestOptions = {
initialHeaders: {
[HttpHeaders.populateCollectionThroughputInfo]: true
}
};
try {
if (
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
) {
return await readDatabaseOfferWithARM(params.databaseId);
}
return await readOfferWithSDK(params.offerId, params.databaseResourceId);
const response = await client()
.offer(offerId)
.read(options);
return (
response && {
...response.resource,
headers: response.headers
}
);
} catch (error) {
handleError(error, "ReadDatabaseOffer", `Error while querying offer for database ${params.databaseId}`);
throw error;
@@ -31,13 +54,13 @@ export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promis
}
};
const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> => {
let rpResponse;
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const defaultExperience = userContext.defaultExperience;
let rpResponse;
try {
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
@@ -55,39 +78,18 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
return rpResponse?.name;
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
return undefined;
}
const resource = rpResponse?.properties?.resource;
if (resource) {
const offerId: string = rpResponse.name;
const minimumThroughput: number =
typeof resource.minimumThroughput === "string"
? parseInt(resource.minimumThroughput)
: resource.minimumThroughput;
const autoscaleSettings = resource.autoscaleSettings;
if (autoscaleSettings) {
return {
id: offerId,
autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
manualThroughput: undefined,
minimumThroughput
};
}
return {
id: offerId,
autoscaleMaxThroughput: undefined,
manualThroughput: resource.throughput,
minimumThroughput
};
}
return undefined;
};
const getDatabaseOfferIdWithSDK = async (databaseResourceId: string): Promise<string> => {
const offers = await readOffers();
const offer = offers.find(offer => offer.resource === databaseResourceId);
return offer?.id;
};

View File

@@ -1,29 +0,0 @@
import { HttpHeaders } from "../Constants";
import { Offer } from "../../Contracts/DataModels";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { parseSDKOfferResponse } from "../OfferUtility";
import { readOffers } from "./readOffers";
export const readOfferWithSDK = async (offerId: string, resourceId: string): Promise<Offer> => {
if (!offerId) {
const offers = await readOffers();
const offer = offers.find(offer => offer.resource === resourceId);
if (!offer) {
return undefined;
}
offerId = offer.id;
}
const options: RequestOptions = {
initialHeaders: {
[HttpHeaders.populateCollectionThroughputInfo]: true
}
};
const response = await client()
.offer(offerId)
.read(options);
return parseSDKOfferResponse(response);
};

View File

@@ -1,9 +1,9 @@
import { SDKOfferDefinition } from "../../Contracts/DataModels";
import { Offer } from "../../Contracts/DataModels";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { handleError, getErrorMessage } from "../ErrorHandlingUtils";
export const readOffers = async (): Promise<SDKOfferDefinition[]> => {
export const readOffers = async (): Promise<Offer[]> => {
const clearMessage = logConsoleProgress(`Querying offers`);
try {

View File

@@ -1,14 +1,13 @@
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { HttpHeaders } from "../Constants";
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
import { Offer, UpdateOfferParams } from "../../Contracts/DataModels";
import { OfferDefinition } from "@azure/cosmos";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { ThroughputSettingsUpdateParameters } from "../../Utils/arm/generatedClients/2020-04-01/types";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { parseSDKOfferResponse } from "../OfferUtility";
import { readCollectionOffer } from "./readCollectionOffer";
import { readDatabaseOffer } from "./readDatabaseOffer";
import {
@@ -374,21 +373,21 @@ const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpd
};
const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> => {
const sdkOfferDefinition = params.currentOffer.offerDefinition;
const newOffer: SDKOfferDefinition = {
const currentOffer = params.currentOffer;
const newOffer: Offer = {
content: {
offerThroughput: undefined,
offerIsRUPerMinuteThroughputEnabled: false
},
_etag: undefined,
_ts: undefined,
_rid: sdkOfferDefinition._rid,
_self: sdkOfferDefinition._self,
id: sdkOfferDefinition.id,
offerResourceId: sdkOfferDefinition.offerResourceId,
offerVersion: sdkOfferDefinition.offerVersion,
offerType: sdkOfferDefinition.offerType,
resource: sdkOfferDefinition.resource
_rid: currentOffer._rid,
_self: currentOffer._self,
id: currentOffer.id,
offerResourceId: currentOffer.offerResourceId,
offerVersion: currentOffer.offerVersion,
offerType: currentOffer.offerType,
resource: currentOffer.resource
};
if (params.autopilotThroughput) {
@@ -416,6 +415,5 @@ const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> =>
.offer(params.currentOffer.id)
// TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660)
.replace((newOffer as unknown) as OfferDefinition, options);
return parseSDKOfferResponse(sdkResponse);
return sdkResponse?.resource;
};

View File

@@ -88,38 +88,6 @@ export interface Resource {
id: string;
}
export interface IType {
name: string;
code: number;
}
export interface IDataField {
dataType: IType;
hasNulls: boolean;
isArray: boolean;
schemaType: IType;
name: string;
path: string;
maxRepetitionLevel: number;
maxDefinitionLevel: number;
}
export interface ISchema {
id: string;
accountName: string;
resource: string;
fields: IDataField[];
}
export interface ISchemaRequest {
id: string;
subscriptionId: string;
resourceGroup: string;
accountName: string;
resource: string;
status: string;
}
export interface Collection extends Resource {
defaultTtl?: number;
indexingPolicy?: IndexingPolicy;
@@ -130,8 +98,6 @@ export interface Collection extends Resource {
changeFeedPolicy?: ChangeFeedPolicy;
analyticalStorageTtl?: number;
geospatialConfig?: GeospatialConfig;
schema?: ISchema;
requestSchema?: () => void;
}
export interface Database extends Resource {
@@ -208,21 +174,12 @@ export interface QueryMetrics {
vmExecutionTime: any;
}
export interface Offer {
id: string;
autoscaleMaxThroughput: number;
manualThroughput: number;
minimumThroughput: number;
offerDefinition?: SDKOfferDefinition;
headers?: any;
}
export interface SDKOfferDefinition extends Resource {
export interface Offer extends Resource {
offerVersion?: string;
offerType?: string;
content?: {
offerThroughput: number;
offerIsRUPerMinuteThroughputEnabled?: boolean;
offerIsRUPerMinuteThroughputEnabled: boolean;
collectionThroughputInfo?: OfferThroughputInfo;
offerAutopilotSettings?: AutoPilotOfferSettings;
};
@@ -230,16 +187,8 @@ export interface SDKOfferDefinition extends Resource {
offerResourceId?: string;
}
export interface CollectionQuotaInfo {
storedProcedures: number;
triggers: number;
functions: number;
documentsSize: number;
collectionSize: number;
documentsCount: number;
usageSizeInKB: number;
numPartitions: number;
uniqueKeyPolicy?: UniqueKeyPolicy; // TODO: This should ideally not be a part of the collection quota. Remove after refactoring. (#119617)
export interface OfferWithHeaders extends Offer {
headers: any;
}
export interface OfferThroughputInfo {

View File

@@ -32,8 +32,7 @@ export enum MessageTypes {
GetArcadiaToken,
CreateWorkspace,
CreateSparkPool,
RefreshDatabaseAccount,
InitTestExplorer
RefreshDatabaseAccount
}
export { Versions, ActionContracts, Diagnostics };

View File

@@ -1,7 +0,0 @@
export enum SubscriptionType {
Benefits,
EA,
Free,
Internal,
PAYG
}

View File

@@ -17,7 +17,6 @@ import Trigger from "../Explorer/Tree/Trigger";
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
import { UploadDetails } from "../workers/upload/definitions";
import * as DataModels from "./DataModels";
import { SubscriptionType } from "./SubscriptionType";
export interface TokenProvider {
getAuthHeader(): Promise<Headers>;
@@ -116,11 +115,8 @@ export interface CollectionBase extends TreeNode {
export interface Collection extends CollectionBase {
defaultTtl: ko.Observable<number>;
analyticalStorageTtl: ko.Observable<number>;
schema?: DataModels.ISchema;
requestSchema?: () => void;
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
quotaInfo: ko.Observable<DataModels.CollectionQuotaInfo>;
offer: ko.Observable<DataModels.Offer>;
conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
@@ -361,7 +357,6 @@ export enum CollectionTabKind {
SparkMasterTab = 16,
Gallery = 17,
NotebookViewer = 18,
Schema = 19,
SettingsV2 = 19
}
@@ -416,6 +411,14 @@ export interface ThroughputDefaults {
shared: number;
}
export enum SubscriptionType {
Benefits,
EA,
Free,
Internal,
PAYG
}
export class MonacoEditorSettings {
public readonly language: string;
public readonly readOnly: boolean;

View File

@@ -44,6 +44,10 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("user-defined-function-tab")).toBe(true);
});
it("should register settings-tab component", () => {
expect(ko.components.isRegistered("settings-tab")).toBe(true);
});
it("should register settings-tab-v2 component", () => {
expect(ko.components.isRegistered("settings-tab-v2")).toBe(true);
});

View File

@@ -31,6 +31,7 @@ ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTa
ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab());
ko.components.register("trigger-tab", new TabComponents.TriggerTab());
ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab());
ko.components.register("settings-tab", new TabComponents.SettingsTab());
ko.components.register("settings-tab-v2", new TabComponents.SettingsTabV2());
ko.components.register("query-tab", new TabComponents.QueryTab());
ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab());

View File

@@ -39,10 +39,6 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
params.set(TerminalQueryParams.TerminalEndpoint, terminalEndpoint);
}
params.set("account","contoso-retail-mongodb");
params.set("port","10255");
//tofill
params.set("token","");
return params;
}

View File

@@ -89,11 +89,12 @@ describe("SettingsComponent", () => {
it("auto pilot helper functions pass on correct value", () => {
const newCollection = { ...collection };
newCollection.offer = ko.observable<DataModels.Offer>({
autoscaleMaxThroughput: 10000,
manualThroughput: undefined,
minimumThroughput: 400,
id: "test"
});
content: {
offerAutopilotSettings: {
maxThroughput: 10000
}
}
} as DataModels.Offer);
const props = { ...baseProps };
props.settingsTab.collection = newCollection;
@@ -186,6 +187,21 @@ describe("SettingsComponent", () => {
expect(settingsComponentInstance.hasConflictResolution()).toEqual(true);
});
it("isOfferReplacePending", () => {
let settingsComponentInstance = new SettingsComponent(baseProps);
expect(settingsComponentInstance.isOfferReplacePending()).toEqual(undefined);
const newCollection = { ...collection };
newCollection.offer = ko.observable({
headers: { "x-ms-offer-replace-pending": true }
} as DataModels.OfferWithHeaders);
const props = { ...baseProps };
props.settingsTab.collection = newCollection;
settingsComponentInstance = new SettingsComponent(props);
expect(settingsComponentInstance.isOfferReplacePending()).toEqual(true);
});
it("save calls updateCollection, updateMongoDBCollectionThroughRP and updateOffer", async () => {
const wrapper = shallow(<SettingsComponent {...baseProps} />);
wrapper.setState({ isSubSettingsSaveable: true, isScaleSaveable: true, isMongoIndexingPolicySaveable: true });

View File

@@ -1,50 +1,49 @@
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "office-ui-fabric-react";
import * as React from "react";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import * as Constants from "../../../Common/Constants";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import DiscardIcon from "../../../../images/discard.svg";
import SaveIcon from "../../../../images/save-cosmos.svg";
import { traceStart, traceFailure, traceSuccess, trace } from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import Explorer from "../../Explorer";
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import * as Constants from "../../../Common/Constants";
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import SettingsTab from "../../Tabs/SettingsTabV2";
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
import {
MongoIndexingPolicyComponent,
MongoIndexingPolicyComponentProps
} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
import {
hasDatabaseSharedThroughput,
GeospatialConfigType,
TtlType,
ChangeFeedPolicyState,
SettingsV2TabTypes,
getTabTitle,
isDirty,
AddMongoIndexProps,
MongoIndexTypes,
parseConflictResolutionMode,
parseConflictResolutionProcedure,
getMongoNotification
} from "./SettingsUtils";
import "./SettingsComponent.less";
import {
ConflictResolutionComponent,
ConflictResolutionComponentProps
} from "./SettingsSubComponents/ConflictResolutionComponent";
import { SubSettingsComponent, SubSettingsComponentProps } from "./SettingsSubComponents/SubSettingsComponent";
import { Pivot, PivotItem, IPivotProps, IPivotItemProps } from "office-ui-fabric-react";
import "./SettingsComponent.less";
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { isEmpty } from "underscore";
import {
MongoIndexingPolicyComponent,
MongoIndexingPolicyComponentProps
} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
import { SubSettingsComponent, SubSettingsComponentProps } from "./SettingsSubComponents/SubSettingsComponent";
import {
AddMongoIndexProps,
ChangeFeedPolicyState,
GeospatialConfigType,
getMongoNotification,
getTabTitle,
hasDatabaseSharedThroughput,
isDirty,
MongoIndexTypes,
parseConflictResolutionMode,
parseConflictResolutionProcedure,
SettingsV2TabTypes,
TtlType
} from "./SettingsUtils";
interface SettingsV2TabInfo {
tab: SettingsV2TabTypes;
@@ -223,6 +222,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
public loadMongoIndexes = async (): Promise<void> => {
if (
this.container.isMongoIndexEditorEnabled() &&
this.container.isPreferredApiMongoDB() &&
this.container.isEnableMongoCapabilityPresent() &&
this.container.databaseAccount()
@@ -270,14 +270,19 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
};
private setAutoPilotStates = (): void => {
const autoscaleMaxThroughput = this.collection?.offer()?.autoscaleMaxThroughput;
const offer = this.collection?.offer && this.collection.offer();
const offerAutopilotSettings = offer?.content?.offerAutopilotSettings;
if (autoscaleMaxThroughput && AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
if (
offerAutopilotSettings &&
offerAutopilotSettings.maxThroughput &&
AutoPilotUtils.isValidAutoPilotThroughput(offerAutopilotSettings.maxThroughput)
) {
this.setState({
isAutoPilotSelected: true,
wasAutopilotOriginallySet: true,
autoPilotThroughput: autoscaleMaxThroughput,
autoPilotThroughputBaseline: autoscaleMaxThroughput
autoPilotThroughput: offerAutopilotSettings.maxThroughput,
autoPilotThroughputBaseline: offerAutopilotSettings.maxThroughput
});
}
};
@@ -295,7 +300,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
!!this.collection.conflictResolutionPolicy();
public isOfferReplacePending = (): boolean => {
return !!this.collection?.offer()?.headers?.[Constants.HttpHeaders.offerReplacePending];
const offer = this.collection?.offer && this.collection.offer();
return (
offer &&
Object.keys(offer).find(value => value === "headers") &&
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
);
};
public onSaveClick = async (): Promise<void> => {
@@ -433,12 +443,51 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}
if (this.state.isScaleSaveable) {
const newThroughput = this.state.throughput;
const newOffer: DataModels.Offer = { ...this.collection.offer() };
if (newOffer.content) {
newOffer.content.offerThroughput = newThroughput;
} else {
newOffer.content = {
offerThroughput: newThroughput,
offerIsRUPerMinuteThroughputEnabled: false
};
}
const headerOptions: RequestOptions = { initialHeaders: {} };
if (this.state.isAutoPilotSelected) {
newOffer.content.offerAutopilotSettings = {
maxThroughput: this.state.autoPilotThroughput
};
// user has changed from provisioned --> autoscale
if (this.hasProvisioningTypeChanged()) {
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToAutopilot] = "true";
delete newOffer.content.offerAutopilotSettings;
} else {
delete newOffer.content.offerThroughput;
}
} else {
this.setState({
isAutoPilotSelected: false
});
// user has changed from autoscale --> provisioned
if (this.hasProvisioningTypeChanged()) {
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToManualThroughput] = "true";
} else {
delete newOffer.content.offerAutopilotSettings;
}
}
const updateOfferParams: DataModels.UpdateOfferParams = {
databaseId: this.collection.databaseId,
collectionId: this.collection.id(),
currentOffer: this.collection.offer(),
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput
manualThroughput: this.state.isAutoPilotSelected ? undefined : newThroughput
};
if (this.hasProvisioningTypeChanged()) {
if (this.state.isAutoPilotSelected) {
@@ -452,13 +501,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
if (this.state.isAutoPilotSelected) {
this.setState({
autoPilotThroughput: updatedOffer.autoscaleMaxThroughput,
autoPilotThroughputBaseline: updatedOffer.autoscaleMaxThroughput
autoPilotThroughput: updatedOffer.content.offerAutopilotSettings.maxThroughput,
autoPilotThroughputBaseline: updatedOffer.content.offerAutopilotSettings.maxThroughput
});
} else {
this.setState({
throughput: updatedOffer.manualThroughput,
throughputBaseline: updatedOffer.manualThroughput
throughput: updatedOffer.content.offerThroughput,
throughputBaseline: updatedOffer.content.offerThroughput
});
}
}
@@ -725,7 +774,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}
}
const offerThroughput = this.collection.offer()?.manualThroughput;
const offerThroughput = this.collection?.offer && this.collection.offer()?.content?.offerThroughput;
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
? ChangeFeedPolicyState.On
: ChangeFeedPolicyState.Off;
@@ -916,18 +965,15 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
tab: SettingsV2TabTypes.IndexingPolicyTab,
content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />
});
} else if (this.container.isPreferredApiMongoDB()) {
if (isEmpty(this.container.features())) {
tabs.push({
tab: SettingsV2TabTypes.IndexingPolicyTab,
content: mongoIndexingPolicyAADError
});
} else if (this.container.isEnableMongoCapabilityPresent()) {
tabs.push({
tab: SettingsV2TabTypes.IndexingPolicyTab,
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />
});
}
} else if (
this.container.isMongoIndexEditorEnabled() &&
this.container.isPreferredApiMongoDB() &&
this.container.isEnableMongoCapabilityPresent()
) {
tabs.push({
tab: SettingsV2TabTypes.IndexingPolicyTab,
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />
});
}
if (this.hasConflictResolution()) {

View File

@@ -42,7 +42,7 @@ class SettingsRenderUtilsTestComponent extends React.Component {
{updateThroughputDelayedApplyWarningMessage}
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
{getThroughputApplyShortDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection")}
{getThroughputApplyShortDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
{getThroughputApplyLongDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
{getToolTipContainer(<span>Sample Text</span>)}

View File

@@ -319,13 +319,14 @@ export const getThroughputApplyShortDelayMessage = (
throughput: number,
throughputUnit: string,
databaseName: string,
collectionName: string
collectionName: string,
targetThroughput: number
): JSX.Element => (
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
A request to increase the throughput is currently in progress. This operation will take some time to complete.
<br />
Database: {databaseName}, Container: {collectionName}{" "}
{getCurrentThroughput(isAutoscale, throughput, throughputUnit)}
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, targetThroughput)}
</Text>
);

View File

@@ -24,6 +24,7 @@ import {
transparentDetailsRowStyles,
createAndAddMongoIndexStackProps,
separatorStyles,
mongoIndexingPolicyAADError,
indexingPolicynUnsavedWarningMessage,
infoAndToolTipTextStyle
} from "../../SettingsRenderUtils";
@@ -39,6 +40,7 @@ import {
} from "../../SettingsUtils";
import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent";
import { AuthType } from "../../../../../AuthType";
import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
export interface MongoIndexingPolicyComponentProps {
@@ -319,7 +321,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
</Stack>
);
} else {
return <Spinner size={SpinnerSize.large} />;
return window.authType !== AuthType.AAD ? mongoIndexingPolicyAADError : <Spinner size={SpinnerSize.large} />;
}
}
}

View File

@@ -1,14 +1,13 @@
import { shallow } from "enzyme";
import ko from "knockout";
import React from "react";
import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent";
import { container, collection } from "../TestUtils";
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
import Explorer from "../../../Explorer";
import * as Constants from "../../../../Common/Constants";
import * as DataModels from "../../../../Contracts/DataModels";
import Explorer from "../../../Explorer";
import { throughputUnit } from "../SettingsRenderUtils";
import * as SharedConstants from "../../../../Shared/Constants";
import ko from "knockout";
import { collection, container } from "../TestUtils";
import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent";
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
describe("ScaleComponent", () => {
const nonNationalCloudContainer = new Explorer();
@@ -44,23 +43,24 @@ describe("ScaleComponent", () => {
} as DataModels.Notification
};
it("renders with correct initial notification", () => {
it("renders with correct intiial notification", () => {
let wrapper = shallow(<ScaleComponent {...baseProps} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(true);
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(false);
expect(wrapper.find("#throughputApplyLongDelayMessage").html()).toContain(targetThroughput);
const newCollection = { ...collection };
const maxThroughput = 5000;
const targetMaxThroughput = 50000;
newCollection.offer = ko.observable({
manualThroughput: undefined,
autoscaleMaxThroughput: maxThroughput,
minimumThroughput: 400,
id: "offer",
content: {
offerAutopilotSettings: {
maxThroughput: maxThroughput,
targetMaxThroughput: targetMaxThroughput
}
},
headers: { "x-ms-offer-replace-pending": true }
});
} as DataModels.OfferWithHeaders);
const newProps = {
...baseProps,
initialNotification: undefined as DataModels.Notification,
@@ -70,6 +70,7 @@ describe("ScaleComponent", () => {
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true);
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(false);
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(maxThroughput);
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(targetMaxThroughput);
});
it("autoScale disabled", () => {
@@ -117,20 +118,4 @@ describe("ScaleComponent", () => {
scaleComponent = new ScaleComponent(newProps);
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (autoscale)");
});
it("canThroughputExceedMaximumValue", () => {
let scaleComponent = new ScaleComponent(baseProps);
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(true);
const newProps = { ...baseProps, container: nonNationalCloudContainer };
scaleComponent = new ScaleComponent(newProps);
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(true);
});
it("getThroughputWarningMessage", () => {
const throughputBeyondLimit = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million + 1000;
const newProps = { ...baseProps, container: nonNationalCloudContainer, throughput: throughputBeyondLimit };
const scaleComponent = new ScaleComponent(newProps);
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputBeyondLimitWarningMessage");
});
});

View File

@@ -1,23 +1,20 @@
import { Label, MessageBar, MessageBarType, Stack, Text, TextField } from "office-ui-fabric-react";
import * as React from "react";
import * as Constants from "../../../../Common/Constants";
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
import * as ViewModels from "../../../../Contracts/ViewModels";
import { configContext, Platform } from "../../../../ConfigContext";
import * as DataModels from "../../../../Contracts/DataModels";
import * as SharedConstants from "../../../../Shared/Constants";
import * as ViewModels from "../../../../Contracts/ViewModels";
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
import Explorer from "../../../Explorer";
import {
getTextFieldStyles,
subComponentStackProps,
titleAndInputStackProps,
throughputUnit,
getThroughputApplyLongDelayMessage,
getThroughputApplyShortDelayMessage,
updateThroughputBeyondLimitWarningMessage
subComponentStackProps,
throughputUnit,
titleAndInputStackProps
} from "../SettingsRenderUtils";
import { hasDatabaseSharedThroughput } from "../SettingsUtils";
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
import { Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
import { configContext, Platform } from "../../../../ConfigContext";
import { getMinRUs, hasDatabaseSharedThroughput } from "../SettingsUtils";
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
export interface ScaleComponentProps {
collection: ViewModels.Collection;
@@ -61,7 +58,11 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
};
private getStorageCapacityTitle = (): JSX.Element => {
const capacity: string = this.props.isFixedContainer ? "Fixed" : "Unlimited";
// Mongo container with system partition key still treat as "Fixed"
const isFixed =
!this.props.collection.partitionKey ||
(this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey);
const capacity: string = isFixed ? "Fixed" : "Unlimited";
return (
<Stack {...titleAndInputStackProps}>
<Label>Storage capacity</Label>
@@ -70,96 +71,41 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
);
};
public getMaxRUs = (): number => {
if (this.props.container?.isTryCosmosDBSubscription()) {
return Constants.TryCosmosExperience.maxRU;
}
if (this.props.isFixedContainer) {
return SharedConstants.CollectionCreation.MaxRUPerPartition;
}
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
};
public getMinRUs = (): number => {
if (this.props.container?.isTryCosmosDBSubscription()) {
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
}
return (
this.props.collection.offer()?.minimumThroughput || SharedConstants.CollectionCreation.DefaultCollectionRUs400
);
};
public getThroughputTitle = (): string => {
if (this.props.isAutoPilotSelected) {
return AutoPilotUtils.getAutoPilotHeaderText();
}
const minThroughput: string = this.getMinRUs().toLocaleString();
const maxThroughput: string = !this.props.isFixedContainer ? "unlimited" : this.getMaxRUs().toLocaleString();
const minThroughput: string = getMinRUs(this.props.collection, this.props.container).toLocaleString();
const maxThroughput: string = !this.props.isFixedContainer ? "unlimited" : "10000";
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`;
};
public canThroughputExceedMaximumValue = (): boolean => {
return (
!this.props.isFixedContainer &&
configContext.platform === Platform.Portal &&
!this.props.container.isRunningOnNationalCloud()
);
};
public getInitialNotificationElement = (): JSX.Element => {
if (this.props.initialNotification) {
return this.getLongDelayMessage();
}
const offer = this.props.collection?.offer && this.props.collection.offer();
if (
offer &&
Object.keys(offer).find(value => {
return value === "headers";
}) &&
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
) {
const throughput = offer?.content?.offerAutopilotSettings?.maxThroughput;
const targetThroughput =
offer.content?.offerAutopilotSettings?.targetMaxThroughput || offer?.content?.offerThroughput;
const offer = this.props.collection?.offer();
if (offer?.headers?.[Constants.HttpHeaders.offerReplacePending]) {
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
return getThroughputApplyShortDelayMessage(
this.props.isAutoPilotSelected,
throughput,
throughputUnit,
this.props.collection.databaseId,
this.props.collection.id()
);
}
return undefined;
};
public getThroughputWarningMessage = (): JSX.Element => {
const throughputExceedsBackendLimits: boolean =
this.canThroughputExceedMaximumValue() &&
this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
if (throughputExceedsBackendLimits && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
return updateThroughputBeyondLimitWarningMessage;
}
return undefined;
};
public getLongDelayMessage = (): JSX.Element => {
const matches: string[] = this.props.initialNotification?.description.match(
`Throughput update for (.*) ${throughputUnit}`
);
const throughput = this.props.throughputBaseline;
const targetThroughput: number = matches.length > 1 && Number(matches[1]);
if (targetThroughput) {
return getThroughputApplyLongDelayMessage(
this.props.wasAutopilotOriginallySet,
throughput,
throughputUnit,
this.props.collection.databaseId,
this.props.collection.id(),
targetThroughput
);
}
return <></>;
return undefined;
};
private getThroughputInputComponent = (): JSX.Element => (
@@ -169,10 +115,8 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
throughput={this.props.throughput}
throughputBaseline={this.props.throughputBaseline}
onThroughputChange={this.props.onThroughputChange}
minimum={this.getMinRUs()}
maximum={this.getMaxRUs()}
minimum={getMinRUs(this.props.collection, this.props.container)}
isEnabled={!hasDatabaseSharedThroughput(this.props.collection)}
canExceedMaximumValue={this.canThroughputExceedMaximumValue()}
label={this.getThroughputTitle()}
isEmulator={this.isEmulator}
isFixed={this.props.isFixedContainer}
@@ -185,8 +129,6 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
spendAckChecked={false}
onScaleSaveableChange={this.props.onScaleSaveableChange}
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
getThroughputWarningMessage={this.getThroughputWarningMessage}
usageSizeInKB={this.props.collection.quotaInfo().usageSizeInKB}
/>
);

View File

@@ -15,9 +15,7 @@ describe("ThroughputInputAutoPilotV3Component", () => {
throughputBaseline: 100,
onThroughputChange: undefined,
minimum: 10000,
maximum: 400,
step: 100,
usageSizeInKB: 10000,
isEnabled: true,
isEmulator: false,
spendAckChecked: false,
@@ -39,8 +37,7 @@ describe("ThroughputInputAutoPilotV3Component", () => {
},
onScaleDiscardableChange: () => {
return;
},
getThroughputWarningMessage: () => undefined
}
};
it("throughput input visible", () => {

View File

@@ -1,39 +1,33 @@
import React from "react";
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
import {
getTextFieldStyles,
getToolTipContainer,
noLeftPaddingCheckBoxStyle,
titleAndInputStackProps,
checkBoxAndInputStackProps,
getChoiceGroupStyles,
messageBarStyles,
getEstimatedSpendElement,
getEstimatedAutoscaleSpendElement,
getAutoPilotV3SpendElement,
manualToAutoscaleDisclaimerElement
} from "../../SettingsRenderUtils";
import {
Text,
TextField,
Checkbox,
ChoiceGroup,
IChoiceGroupOption,
Checkbox,
Stack,
Label,
Link,
MessageBar,
MessageBarType
MessageBarType,
Stack,
Text,
TextField
} from "office-ui-fabric-react";
import { ToolTipLabelComponent } from "../ToolTipLabelComponent";
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
import * as SharedConstants from "../../../../../Shared/Constants";
import React from "react";
import * as DataModels from "../../../../../Contracts/DataModels";
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";
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
import {
checkBoxAndInputStackProps,
getAutoPilotV3SpendElement,
getChoiceGroupStyles,
getEstimatedAutoscaleSpendElement,
getEstimatedSpendElement,
getTextFieldStyles,
getToolTipContainer,
manualToAutoscaleDisclaimerElement,
messageBarStyles,
noLeftPaddingCheckBoxStyle,
titleAndInputStackProps
} from "../../SettingsRenderUtils";
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
import { ToolTipLabelComponent } from "../ToolTipLabelComponent";
export interface ThroughputInputAutoPilotV3Props {
databaseAccount: DataModels.DatabaseAccount;
@@ -42,7 +36,6 @@ export interface ThroughputInputAutoPilotV3Props {
throughputBaseline: number;
onThroughputChange: (newThroughput: number) => void;
minimum: number;
maximum: number;
step?: number;
isEnabled?: boolean;
spendAckChecked?: boolean;
@@ -63,8 +56,6 @@ export interface ThroughputInputAutoPilotV3Props {
onMaxAutoPilotThroughputChange: (newThroughput: number) => void;
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
getThroughputWarningMessage: () => JSX.Element;
usageSizeInKB: number;
}
interface ThroughputInputAutoPilotV3State {
@@ -124,13 +115,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
if (isDirty(this.props.throughput, this.props.throughputBaseline)) {
isDiscardable = true;
isSaveable = true;
if (
!this.props.throughput ||
this.props.throughput < this.props.minimum ||
(this.props.throughput > this.props.maximum && (this.props.isEmulator || this.props.isFixed)) ||
(this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
!this.props.canExceedMaximumValue)
) {
if (!this.props.throughput || this.props.throughput < this.props.minimum) {
isSaveable = false;
}
}
@@ -146,8 +131,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
};
this.step = this.props.step ?? ThroughputInputAutoPilotV3Component.defaultStep;
this.throughputInputMaxValue = this.props.canExceedMaximumValue ? Int32.Max : this.props.maximum;
this.autoPilotInputMaxValue = this.props.isFixed ? this.props.maximum : Int32.Max;
this.throughputInputMaxValue = Number.MAX_SAFE_INTEGER;
this.autoPilotInputMaxValue = Number.MAX_SAFE_INTEGER;
}
public hasProvisioningTypeChanged = (): boolean =>
@@ -229,29 +214,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
option?: IChoiceGroupOption
): 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 => {
const labelId = "settingsV2RadioButtonLabelId";
return (
@@ -303,7 +265,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
onChange={this.onAutoPilotThroughputChange}
/>
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
{this.minRUperGBSurvey()}
{this.props.spendAckVisible && (
<Checkbox
id="spendAckCheckBox"
@@ -334,13 +295,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
}
onChange={this.onThroughputChange}
/>
{this.props.getThroughputWarningMessage() && (
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
{this.props.getThroughputWarningMessage()}
</MessageBar>
)}
{!this.props.isEmulator && this.getRequestUnitsUsageCost()}
{this.minRUperGBSurvey()}
{this.props.spendAckVisible && (
<Checkbox
id="spendAckCheckBox"
@@ -350,6 +307,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
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>}
</Stack>
);

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ScaleComponent renders with correct initial notification 1`] = `
exports[`ScaleComponent renders with correct intiial notification 1`] = `
<Stack
tokens={
Object {
@@ -8,29 +8,6 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
}
}
>
<StyledMessageBarBase
messageBarType={5}
>
<Text
id="throughputApplyLongDelayMessage"
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to complete. View the latest status in Notifications.
<br />
Database:
test
, Container:
test
, Current autoscale throughput: 100 - 1000 RU/s, Target autoscale throughput: 600 - 6000 RU/s
</Text>
</StyledMessageBarBase>
<Stack
tokens={
Object {
@@ -39,8 +16,6 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
}
>
<ThroughputInputAutoPilotV3Component
canExceedMaximumValue={true}
getThroughputWarningMessage={[Function]}
isAutoPilotSelected={false}
isEmulator={false}
isEnabled={true}
@@ -48,7 +23,6 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
label="Throughput (6,000 - unlimited RU/s)"
maxAutoPilotThroughput={4000}
maxAutoPilotThroughputBaseline={4000}
maximum={1000000}
minimum={6000}
onAutoPilotSelected={[Function]}
onMaxAutoPilotThroughputChange={[Function]}

View File

@@ -1,5 +1,6 @@
import { collection } from "./TestUtils";
import { collection, container } from "./TestUtils";
import {
getMinRUs,
getMongoIndexType,
getMongoNotification,
getSanitizedInputValue,
@@ -21,6 +22,11 @@ import * as ViewModels from "../../../Contracts/ViewModels";
import ko from "knockout";
describe("SettingsUtils", () => {
it("getMinRUs", () => {
expect(collection.offer().content.collectionThroughputInfo.numPhysicalPartitions).toEqual(4);
expect(getMinRUs(collection, container)).toEqual(6000);
});
it("hasDatabaseSharedThroughput", () => {
expect(hasDatabaseSharedThroughput(collection)).toEqual(undefined);

View File

@@ -1,7 +1,9 @@
import * as ViewModels from "../../../Contracts/ViewModels";
import * as DataModels from "../../../Contracts/DataModels";
import * as Constants from "../../../Common/Constants";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import * as SharedConstants from "../../../Shared/Constants";
import { MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
import Explorer from "../../Explorer";
const zeroValue = 0;
export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy;
@@ -67,6 +69,27 @@ export const hasDatabaseSharedThroughput = (collection: ViewModels.Collection):
return database?.isDatabaseShared() && !collection.offer();
};
export const getMinRUs = (collection: ViewModels.Collection, container: Explorer): number => {
const isTryCosmosDBSubscription = container?.isTryCosmosDBSubscription();
if (isTryCosmosDBSubscription) {
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
}
const offerContent = collection?.offer && collection.offer()?.content;
if (offerContent?.offerAutopilotSettings) {
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
}
const collectionThroughputInfo: DataModels.OfferThroughputInfo = offerContent?.collectionThroughputInfo;
if (collectionThroughputInfo?.minimumRUForCollection > 0) {
return collectionThroughputInfo.minimumRUForCollection;
}
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
};
export const parseConflictResolutionMode = (modeFromBackend: string): DataModels.ConflictResolutionMode => {
// Backend can contain different casing as it does case-insensitive comparisson
if (!modeFromBackend) {

View File

@@ -18,13 +18,16 @@ export const collection = ({
excludedPaths: []
}),
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
quotaInfo: ko.observable<DataModels.CollectionQuotaInfo>({} as DataModels.CollectionQuotaInfo),
offer: ko.observable<DataModels.Offer>({
autoscaleMaxThroughput: undefined,
manualThroughput: 10000,
minimumThroughput: 6000,
id: "offer"
}),
content: {
offerThroughput: 10000,
offerIsRUPerMinuteThroughputEnabled: false,
collectionThroughputInfo: {
minimumRUForCollection: 6000,
numPhysicalPartitions: 4
} as DataModels.OfferThroughputInfo
}
} as DataModels.Offer),
conflictResolutionPolicy: ko.observable<DataModels.ConflictResolutionPolicy>(
{} as DataModels.ConflictResolutionPolicy
),

View File

@@ -43,7 +43,6 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
@@ -86,7 +85,6 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
@@ -349,7 +347,6 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
@@ -575,7 +572,6 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
@@ -657,7 +653,6 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
@@ -760,7 +755,6 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
@@ -956,6 +950,7 @@ exports[`SettingsComponent renders 1`] = `
"isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function],
"isLinkInjectionEnabled": [Function],
"isMongoIndexEditorEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isNotificationConsoleExpanded": [Function],
@@ -970,8 +965,8 @@ exports[`SettingsComponent renders 1`] = `
"isRefreshingExplorer": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function],
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isSettingsV2Enabled": [Function],
"isSparkEnabled": [Function],
"isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function],
@@ -1300,7 +1295,6 @@ exports[`SettingsComponent renders 1`] = `
"version": 2,
},
"partitionKeyProperty": "partitionKey",
"quotaInfo": [Function],
"readSettings": [Function],
"uniqueKeyPolicy": Object {},
}
@@ -1322,7 +1316,6 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
@@ -1365,7 +1358,6 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
@@ -1628,7 +1620,6 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
@@ -1854,7 +1845,6 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
@@ -1936,7 +1926,6 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
@@ -2039,7 +2028,6 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
@@ -2235,6 +2223,7 @@ exports[`SettingsComponent renders 1`] = `
"isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function],
"isLinkInjectionEnabled": [Function],
"isMongoIndexEditorEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isNotificationConsoleExpanded": [Function],
@@ -2249,8 +2238,8 @@ exports[`SettingsComponent renders 1`] = `
"isRefreshingExplorer": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function],
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isSettingsV2Enabled": [Function],
"isSparkEnabled": [Function],
"isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function],
@@ -2614,7 +2603,6 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
@@ -2657,7 +2645,6 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
@@ -2920,7 +2907,6 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
@@ -3146,7 +3132,6 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
@@ -3228,7 +3213,6 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
@@ -3331,7 +3315,6 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
@@ -3527,6 +3510,7 @@ exports[`SettingsComponent renders 1`] = `
"isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function],
"isLinkInjectionEnabled": [Function],
"isMongoIndexEditorEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isNotificationConsoleExpanded": [Function],
@@ -3541,8 +3525,8 @@ exports[`SettingsComponent renders 1`] = `
"isRefreshingExplorer": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function],
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isSettingsV2Enabled": [Function],
"isSparkEnabled": [Function],
"isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function],
@@ -3871,7 +3855,6 @@ exports[`SettingsComponent renders 1`] = `
"version": 2,
},
"partitionKeyProperty": "partitionKey",
"quotaInfo": [Function],
"readSettings": [Function],
"uniqueKeyPolicy": Object {},
}
@@ -3893,7 +3876,6 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
@@ -3936,7 +3918,6 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
@@ -4199,7 +4180,6 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
@@ -4425,7 +4405,6 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
@@ -4507,7 +4486,6 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
@@ -4610,7 +4588,6 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
@@ -4806,6 +4783,7 @@ exports[`SettingsComponent renders 1`] = `
"isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function],
"isLinkInjectionEnabled": [Function],
"isMongoIndexEditorEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isNotificationConsoleExpanded": [Function],
@@ -4820,8 +4798,8 @@ exports[`SettingsComponent renders 1`] = `
"isRefreshingExplorer": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function],
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isSettingsV2Enabled": [Function],
"isSparkEnabled": [Function],
"isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function],

View File

@@ -227,7 +227,7 @@ exports[`SettingsUtils functions render 1`] = `
, Container:
sampleCollection
, Current manual throughput: 1000 RU/s
, Current manual throughput: 1000 RU/s, Target manual throughput: 2000
</Text>
<Text
id="throughputApplyLongDelayMessage"

View File

@@ -87,7 +87,6 @@ import { updateUserContext, userContext } from "../UserContext";
import { stringToBlob } from "../Utils/BlobUtils";
import { IChoiceGroupProps } from "office-ui-fabric-react";
import { getErrorMessage, handleError, getErrorStack } from "../Common/ErrorHandlingUtils";
import { SubscriptionType } from "../Contracts/SubscriptionType";
BindingHandlersRegisterer.registerBindingHandlers();
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
@@ -120,7 +119,7 @@ export default class Explorer {
public databaseAccount: ko.Observable<DataModels.DatabaseAccount>;
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults;
public subscriptionType: ko.Observable<SubscriptionType>;
public subscriptionType: ko.Observable<ViewModels.SubscriptionType>;
public quotaId: ko.Observable<string>;
public defaultExperience: ko.Observable<string>;
public isPreferredApiDocumentDB: ko.Computed<boolean>;
@@ -206,6 +205,8 @@ export default class Explorer {
public isGalleryPublishEnabled: ko.Computed<boolean>;
public isCodeOfConductEnabled: ko.Computed<boolean>;
public isLinkInjectionEnabled: ko.Computed<boolean>;
public isSettingsV2Enabled: ko.Observable<boolean>;
public isMongoIndexEditorEnabled: ko.Observable<boolean>;
public isGitHubPaneEnabled: ko.Observable<boolean>;
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
@@ -224,7 +225,6 @@ export default class Explorer {
public shareTokenCopyHelperText: ko.Observable<string>;
public shouldShowDataAccessExpiryDialog: ko.Observable<boolean>;
public shouldShowContextSwitchPrompt: ko.Observable<boolean>;
public isSchemaEnabled: ko.Computed<boolean>;
// Notebooks
public isNotebookEnabled: ko.Observable<boolean>;
@@ -278,7 +278,9 @@ export default class Explorer {
this.refreshTreeTitle = ko.observable<string>("Refresh collections");
this.databaseAccount = ko.observable<DataModels.DatabaseAccount>();
this.subscriptionType = ko.observable<SubscriptionType>(SharedConstants.CollectionCreation.DefaultSubscriptionType);
this.subscriptionType = ko.observable<ViewModels.SubscriptionType>(
SharedConstants.CollectionCreation.DefaultSubscriptionType
);
this.quotaId = ko.observable<string>("");
let firstInitialization = true;
this.isRefreshingExplorer = ko.observable<boolean>(true);
@@ -410,6 +412,8 @@ export default class Explorer {
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
);
this.isSettingsV2Enabled = ko.observable(false);
this.isMongoIndexEditorEnabled = ko.observable(false);
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
@@ -418,7 +422,6 @@ export default class Explorer {
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
);
this.isSchemaEnabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSchema));
this.isNotificationConsoleExpanded = ko.observable<boolean>(false);
this.databases = ko.observableArray<ViewModels.Database>();
@@ -1730,7 +1733,6 @@ export default class Explorer {
case MessageTypes.SendNotification:
case MessageTypes.ClearNotification:
case MessageTypes.LoadingStatus:
case MessageTypes.InitTestExplorer:
return true;
}
}
@@ -1888,8 +1890,7 @@ export default class Explorer {
masterKey,
databaseAccount,
resourceGroup: inputs.resourceGroup,
subscriptionId: inputs.subscriptionId,
subscriptionType: inputs.subscriptionType
subscriptionId: inputs.subscriptionId
});
TelemetryProcessor.traceSuccess(
Action.LoadDatabaseAccount,
@@ -1910,6 +1911,14 @@ export default class Explorer {
if (!flights) {
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 {

View File

@@ -350,9 +350,7 @@ export const launchWebSocketKernelEpic = (
} as any,
name: "",
path: content.filepath.replace(/^\/+/g, ""),
type: "notebook",
endpoint:"https://dech-notebooks-demo-7.documents.azure.com:443/",
token:"" //to fill
type: "notebook"
};
return sessions.create(serverConfig, sessionPayload).pipe(

View File

@@ -7,7 +7,6 @@ import * as ko from "knockout";
import * as PricingUtils from "../../Utils/PricingUtils";
import * as SharedConstants from "../../Shared/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import { SubscriptionType } from "../../Contracts/SubscriptionType";
import editable from "../../Common/EditableUtility";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
@@ -62,7 +61,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
public maxCollectionsReachedMessage: ko.Observable<string>;
public requestUnitsUsageCost: ko.Computed<string>;
public dedicatedRequestUnitsUsageCost: ko.Computed<string>;
public canRequestSupport: ko.PureComputed<boolean>;
public largePartitionKey: ko.Observable<boolean> = ko.observable<boolean>(false);
public useIndexingForSharedThroughput: ko.Observable<boolean> = ko.observable<boolean>(true);
public costsVisible: ko.PureComputed<boolean>;
@@ -315,19 +313,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
}
});
this.canRequestSupport = ko.pureComputed(() => {
if (
configContext.platform !== Platform.Emulator &&
!this.container.isTryCosmosDBSubscription() &&
configContext.platform !== Platform.Portal
) {
const offerThroughput: number = this._getThroughput();
return offerThroughput <= 100000;
}
return false;
});
this.costsVisible = ko.pureComputed(() => {
return configContext.platform !== Platform.Emulator;
});
@@ -649,8 +634,10 @@ export default class AddCollectionPane extends ContextualPaneBase {
}
public getSharedThroughputDefault(): boolean {
const subscriptionType = this.container.subscriptionType && this.container.subscriptionType();
if (subscriptionType === SubscriptionType.EA || this.container.isServerlessEnabled()) {
const subscriptionType: ViewModels.SubscriptionType =
this.container.subscriptionType && this.container.subscriptionType();
if (subscriptionType === ViewModels.SubscriptionType.EA || this.container.isServerlessEnabled()) {
return false;
}
@@ -689,7 +676,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
databaseId: this.databaseId(),
rupm: this.rupm()
}),
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
@@ -792,7 +779,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
uniqueKeyPolicy,
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
}),
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
@@ -867,7 +854,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
uniqueKeyPolicy,
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
}),
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
@@ -902,7 +889,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
uniqueKeyPolicy,
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
},
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",

View File

@@ -117,10 +117,6 @@
showAutoPilot: !isFreeTierAccount()
}">
</throughput-input-autopilot-v3>
<p data-bind="visible: canRequestSupport">
<!-- TODO: Replace link with call to the Azure Support blade --><a
href="https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20More%20Throughput%20Request">Contact
support</a> for more than <span data-bind="text: maxThroughputRUText"></span> RU/s.</p>
</div>
<!-- /ko -->
<!-- Database provisioned throughput - End -->

View File

@@ -1,5 +1,5 @@
import * as Constants from "../../Common/Constants";
import { SubscriptionType } from "../../Contracts/SubscriptionType";
import * as ViewModels from "../../Contracts/ViewModels";
import Explorer from "../Explorer";
import AddDatabasePane from "./AddDatabasePane";
import { DatabaseAccount } from "../../Contracts/DataModels";
@@ -44,31 +44,31 @@ describe("Add Database Pane", () => {
});
it("should be true if subscription type is Benefits", () => {
explorer.subscriptionType(SubscriptionType.Benefits);
explorer.subscriptionType(ViewModels.SubscriptionType.Benefits);
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
});
it("should be false if subscription type is EA", () => {
explorer.subscriptionType(SubscriptionType.EA);
explorer.subscriptionType(ViewModels.SubscriptionType.EA);
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(false);
});
it("should be true if subscription type is Free", () => {
explorer.subscriptionType(SubscriptionType.Free);
explorer.subscriptionType(ViewModels.SubscriptionType.Free);
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
});
it("should be true if subscription type is Internal", () => {
explorer.subscriptionType(SubscriptionType.Internal);
explorer.subscriptionType(ViewModels.SubscriptionType.Internal);
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
});
it("should be true if subscription type is PAYG", () => {
explorer.subscriptionType(SubscriptionType.PAYG);
explorer.subscriptionType(ViewModels.SubscriptionType.PAYG);
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
});

View File

@@ -12,7 +12,6 @@ import { ContextualPaneBase } from "./ContextualPaneBase";
import { createDatabase } from "../../Common/dataAccess/createDatabase";
import { configContext, Platform } from "../../ConfigContext";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { SubscriptionType } from "../../Contracts/SubscriptionType";
export default class AddDatabasePane extends ContextualPaneBase {
public defaultExperience: ko.Computed<string>;
@@ -32,7 +31,6 @@ export default class AddDatabasePane extends ContextualPaneBase {
public throughputSpendAck: ko.Observable<boolean>;
public throughputSpendAckVisible: ko.Computed<boolean>;
public requestUnitsUsageCost: ko.Computed<string>;
public canRequestSupport: ko.PureComputed<boolean>;
public costsVisible: ko.PureComputed<boolean>;
public upsellMessage: ko.PureComputed<string>;
public upsellMessageAriaLabel: ko.PureComputed<string>;
@@ -169,19 +167,6 @@ export default class AddDatabasePane extends ContextualPaneBase {
return estimatedSpend;
});
this.canRequestSupport = ko.pureComputed(() => {
if (
configContext.platform !== Platform.Emulator &&
!this.container.isTryCosmosDBSubscription() &&
configContext.platform !== Platform.Portal
) {
const offerThroughput: number = this.throughput();
return offerThroughput <= 100000;
}
return false;
});
this.isFreeTierAccount = ko.computed<boolean>(() => {
const databaseAccount = this.container && this.container.databaseAccount && this.container.databaseAccount();
const isFreeTierAccount =
@@ -257,7 +242,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
const addDatabasePaneOpenMessage = {
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
throughput: this.throughput(),
@@ -285,7 +270,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
shared: this.databaseCreateNewShared()
}),
offerThroughput,
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
flight: this.container.flight()
@@ -328,9 +313,10 @@ export default class AddDatabasePane extends ContextualPaneBase {
}
public getSharedThroughputDefault(): boolean {
const subscriptionType = this.container.subscriptionType && this.container.subscriptionType();
const subscriptionType: ViewModels.SubscriptionType =
this.container.subscriptionType && this.container.subscriptionType();
if (subscriptionType === SubscriptionType.EA || this.container.isServerlessEnabled()) {
if (subscriptionType === ViewModels.SubscriptionType.EA || this.container.isServerlessEnabled()) {
return false;
}
@@ -349,7 +335,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
shared: this.databaseCreateNewShared()
}),
offerThroughput: offerThroughput,
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
flight: this.container.flight()
@@ -373,7 +359,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
shared: this.databaseCreateNewShared()
}),
offerThroughput: offerThroughput,
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
flight: this.container.flight()

View File

@@ -14,7 +14,6 @@ import { ContextualPaneBase } from "./ContextualPaneBase";
import { HashMap } from "../../Common/HashMap";
import { configContext, Platform } from "../../ConfigContext";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { SubscriptionType } from "../../Contracts/SubscriptionType";
export default class CassandraAddCollectionPane extends ContextualPaneBase {
public createTableQuery: ko.Observable<string>;
@@ -34,7 +33,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
public keyspaceThroughput: ko.Observable<number>;
public keyspaceCreateNew: ko.Observable<boolean>;
public dedicateTableThroughput: ko.Observable<boolean>;
public canRequestSupport: ko.PureComputed<boolean>;
public throughputSpendAckText: ko.Observable<string>;
public throughputSpendAck: ko.Observable<boolean>;
public sharedThroughputSpendAck: ko.Observable<boolean>;
@@ -229,15 +227,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
return configContext.platform !== Platform.Emulator;
});
this.canRequestSupport = ko.pureComputed(() => {
if (configContext.platform !== Platform.Emulator && !this.container.isTryCosmosDBSubscription()) {
const offerThroughput: number = this.throughput();
return offerThroughput <= 100000;
}
return false;
});
this.sharedThroughputSpendAckVisible = ko.computed<boolean>(() => {
const autoscaleThroughput = this.sharedAutoPilotThroughput() * 1;
if (this.isSharedAutoPilotSelected()) {
@@ -315,7 +304,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
databaseId: this.keyspaceId(),
rupm: false
}),
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
storage: "u",
@@ -370,7 +359,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
hasDedicatedThroughput: this.dedicateTableThroughput()
}),
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
storage: "u",
@@ -417,7 +406,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
hasDedicatedThroughput: this.dedicateTableThroughput()
}),
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
storage: "u",
@@ -448,7 +437,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
hasDedicatedThroughput: this.dedicateTableThroughput()
},
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
storage: "u",

View File

@@ -3,11 +3,6 @@ export var Int32 = {
Max: 2147483647
};
export var Int64 = {
Min: -9223372036854775808,
Max: 9223372036854775807
};
var yearMonthDay = "\\d{4}[- ][01]\\d[- ][0-3]\\d";
var timeOfDay = "T[0-2]\\d:[0-5]\\d(:[0-5]\\d(\\.\\d+)?)?";
var timeZone = "Z|[+-][0-2]\\d:[0-5]\\d";

View File

@@ -50,24 +50,13 @@
id="fileImportLinkNotebook"
data-bind="event: { click: onImportLinkClick, keypress: onImportLinkKeyPress }"
>
<img
id="importFileButton"
class="fileImportImg"
src="/folder_16x16.svg"
alt="upload files"
title="Upload files"
/>
<img class="fileImportImg" src="/folder_16x16.svg" alt="upload files" title="Upload files" />
</a>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input
id="uploadFileButton"
type="submit"
data-bind="attr: { value: submitButtonLabel }"
class="btncreatecoll1"
/>
<input type="submit" data-bind="attr: { value: submitButtonLabel }" class="btncreatecoll1" />
</div>
</div>
<!-- Upload File inputs - End -->

View File

@@ -30,7 +30,6 @@
class: 'scaleForm dirty',
value: throughput,
minimum: minRUs,
maximum: maxRUThroughputInputLimit,
canExceedMaximumValue: canThroughputExceedMaximumValue,
step: throughputIncreaseFactor,
label: throughputTitle,
@@ -56,16 +55,6 @@
<span>Learn more about minimum throughput </span>
<a href="https://docs.microsoft.com/azure/cosmos-db/set-throughput" target="_blank">here.</a>
</p>
<p data-bind="visible: canRequestSupport">
<!-- TODO: Replace link with call to the Azure Support blade -->
<a href="https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20More%20Throughput%20Request"
>Contact support</a
>
for more than <span data-bind="text: maxRUsText"></span> RU/s
</p>
<p data-bind="visible: shouldDisplayPortalUsePrompt">
Use Data Explorer from Azure Portal to request more than <span data-bind="text: maxRUsText"></span> RU/s
</p>
</div>
</div>
</div>

View File

@@ -16,6 +16,7 @@ import { RequestOptions } from "@azure/cosmos/dist-esm";
import Explorer from "../Explorer";
import { updateOffer } from "../../Common/dataAccess/updateOffer";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { userContext } from "../../UserContext";
import { configContext, Platform } from "../../ConfigContext";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
@@ -33,6 +34,11 @@ const currentThroughput: (isAutoscale: boolean, throughput: number) => string =
? `Current autoscale throughput: ${Math.round(throughput / 10)} - ${throughput} RU/s`
: `Current 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) =>
`A request to increase the throughput is currently in progress.
This operation will take some time to complete.<br />
@@ -52,23 +58,16 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
public saveSettingsButton: ViewModels.Button;
public discardSettingsChangesButton: ViewModels.Button;
public canRequestSupport: ko.PureComputed<boolean>;
public canThroughputExceedMaximumValue: ko.Computed<boolean>;
public costsVisible: ko.Computed<boolean>;
public displayedError: ko.Observable<string>;
public isTemplateReady: ko.Observable<boolean>;
public minRUAnotationVisible: ko.Computed<boolean>;
public minRUs: ko.Observable<number>;
public maxRUs: ko.Observable<number>;
public maxRUsText: ko.PureComputed<string>;
public maxRUThroughputInputLimit: ko.Computed<number>;
public minRUs: ko.Computed<number>;
public notificationStatusInfo: ko.Observable<string>;
public pendingNotification: ko.Observable<DataModels.Notification>;
public requestUnitsUsageCost: ko.PureComputed<string>;
public autoscaleCost: ko.PureComputed<string>;
public shouldShowNotificationStatusPrompt: ko.Computed<boolean>;
public shouldDisplayPortalUsePrompt: ko.Computed<boolean>;
public shouldShowStatusBar: ko.Computed<boolean>;
public throughputTitle: ko.PureComputed<string>;
public throughputAriaLabel: ko.PureComputed<string>;
@@ -85,7 +84,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
private _hasProvisioningTypeChanged: ko.Computed<boolean>;
private _wasAutopilotOriginallySet: ko.Observable<boolean>;
private _offerReplacePending: ko.Observable<boolean>;
private _offerReplacePending: ko.Computed<boolean>;
private container: Explorer;
constructor(options: ViewModels.TabOptions) {
@@ -104,14 +103,15 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
this._wasAutopilotOriginallySet = ko.observable(false);
this.isAutoPilotSelected = editable.observable(false);
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);
const autoscaleMaxThroughput = this.database?.offer()?.autoscaleMaxThroughput;
if (autoscaleMaxThroughput) {
if (AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
if (offerAutopilotSettings && offerAutopilotSettings.maxThroughput) {
if (AutoPilotUtils.isValidAutoPilotThroughput(offerAutopilotSettings.maxThroughput)) {
this._wasAutopilotOriginallySet(true);
this.isAutoPilotSelected(true);
this.autoPilotThroughput(autoscaleMaxThroughput);
this.autoPilotThroughput(offerAutopilotSettings.maxThroughput);
}
}
@@ -173,22 +173,6 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
return configContext.platform !== Platform.Emulator;
});
this.shouldDisplayPortalUsePrompt = ko.pureComputed<boolean>(() => configContext.platform === Platform.Hosted);
this.canThroughputExceedMaximumValue = ko.pureComputed<boolean>(
() => configContext.platform === Platform.Portal && !this.container.isRunningOnNationalCloud()
);
this.canRequestSupport = ko.pureComputed(() => {
if (
configContext.platform === Platform.Emulator ||
configContext.platform === Platform.Hosted ||
this.canThroughputExceedMaximumValue()
) {
return false;
}
return true;
});
this.overrideWithAutoPilotSettings = ko.pureComputed(() => {
return this._hasProvisioningTypeChanged() && this._wasAutopilotOriginallySet();
});
@@ -197,28 +181,30 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
return this._hasProvisioningTypeChanged() && !this._wasAutopilotOriginallySet();
});
this.minRUs = ko.observable<number>(
this.database.offer()?.minimumThroughput || this.container.collectionCreationDefaults.throughput.unlimitedmin
);
this.minRUs = ko.computed<number>(() => {
const offerContent =
this.database && this.database.offer && this.database.offer() && this.database.offer().content;
// TODO: backend is returning 1,000,000 as min throughput which seems wrong
// Setting to min throughput to not block and let the backend pass or fail
if (offerContent && offerContent.offerAutopilotSettings) {
return 400;
}
const collectionThroughputInfo: DataModels.OfferThroughputInfo =
offerContent && offerContent.collectionThroughputInfo;
if (collectionThroughputInfo && !!collectionThroughputInfo.minimumRUForCollection) {
return collectionThroughputInfo.minimumRUForCollection;
}
const throughputDefaults = this.container.collectionCreationDefaults.throughput;
return throughputDefaults.unlimitedmin;
});
this.minRUAnotationVisible = ko.computed<boolean>(() => {
return PricingUtils.isLargerThanDefaultMinRU(this.minRUs());
});
this.maxRUs = ko.observable<number>(this.container.collectionCreationDefaults.throughput.unlimitedmax);
this.maxRUThroughputInputLimit = ko.pureComputed<number>(() => {
if (configContext.platform === Platform.Hosted) {
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
}
return this.maxRUs();
});
this.maxRUsText = ko.pureComputed(() => {
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million.toLocaleString();
});
this.throughputTitle = ko.pureComputed<string>(() => {
if (this.isAutoPilotSelected()) {
return AutoPilotUtils.getAutoPilotHeaderText();
@@ -231,33 +217,39 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
return this.throughputTitle() + this.requestUnitsUsageCost();
});
this.pendingNotification = ko.observable<DataModels.Notification>();
this._offerReplacePending = ko.observable<boolean>(
!!this.database.offer()?.headers?.[Constants.HttpHeaders.offerReplacePending]
);
this._offerReplacePending = ko.pureComputed<boolean>(() => {
const offer = this.database && this.database.offer && this.database.offer();
return (
offer &&
offer.hasOwnProperty("headers") &&
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
);
});
this.notificationStatusInfo = ko.observable<string>("");
this.shouldShowNotificationStatusPrompt = ko.computed<boolean>(() => this.notificationStatusInfo().length > 0);
this.warningMessage = ko.computed<string>(() => {
const offer = this.database && this.database.offer && this.database.offer();
if (this.overrideWithProvisionedThroughputSettings()) {
return AutoPilotUtils.manualToAutoscaleDisclaimer;
}
const offer = this.database.offer();
if (offer?.headers?.[Constants.HttpHeaders.offerReplacePending]) {
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
if (
offer &&
offer.hasOwnProperty("headers") &&
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
) {
const throughput = offer.content.offerAutopilotSettings
? offer.content.offerAutopilotSettings.maxThroughput
: offer.content.offerThroughput;
return throughputApplyShortDelayMessage(this.isAutoPilotSelected(), throughput, this.database.id());
}
if (
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
this.canThroughputExceedMaximumValue()
) {
if (this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million) {
return updateThroughputBeyondLimitWarningMessage;
}
if (this.throughput() > this.maxRUs()) {
return updateThroughputDelayedApplyWarningMessage;
}
if (this.pendingNotification()) {
const matches: string[] = this.pendingNotification().description.match("Throughput update for (.*) RU/s");
const throughput: number = matches.length > 1 && Number(matches[1]);
@@ -316,13 +308,6 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
return false;
}
if (
!this.canThroughputExceedMaximumValue() &&
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million
) {
return false;
}
if (this.throughput.editableIsDirty()) {
return true;
}
@@ -380,26 +365,38 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
const headerOptions: RequestOptions = { initialHeaders: {} };
try {
const updateOfferParams: DataModels.UpdateOfferParams = {
databaseId: this.database.id(),
currentOffer: this.database.offer(),
autopilotThroughput: this.isAutoPilotSelected() ? this.autoPilotThroughput() : undefined,
manualThroughput: this.isAutoPilotSelected() ? undefined : this.throughput()
};
if (this.isAutoPilotSelected()) {
const updateOfferParams: DataModels.UpdateOfferParams = {
databaseId: this.database.id(),
currentOffer: this.database.offer(),
autopilotThroughput: this.autoPilotThroughput(),
manualThroughput: undefined,
migrateToAutoPilot: this._hasProvisioningTypeChanged()
};
if (this._hasProvisioningTypeChanged()) {
if (this.isAutoPilotSelected()) {
updateOfferParams.migrateToAutoPilot = true;
} else {
updateOfferParams.migrateToManual = true;
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
this.database.offer(updatedOffer);
this.database.offer.valueHasMutated();
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
} else {
if (this.throughput.editableIsDirty() || this.isAutoPilotSelected.editableIsDirty()) {
const originalThroughputValue = this.throughput.getEditableOriginalValue();
const newThroughput = this.throughput();
const updateOfferParams: DataModels.UpdateOfferParams = {
databaseId: this.database.id(),
currentOffer: this.database.offer(),
autopilotThroughput: undefined,
manualThroughput: newThroughput,
migrateToManual: this._hasProvisioningTypeChanged()
};
const updatedOffer = await updateOffer(updateOfferParams);
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
this.database.offer(updatedOffer);
this.database.offer.valueHasMutated();
}
}
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
this.database.offer(updatedOffer);
this.database.offer.valueHasMutated();
this._setBaseline();
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
} catch (error) {
this.container.isRefreshingExplorer(false);
this.isExecutionError(true);
@@ -441,10 +438,15 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
private _setBaseline() {
const offer = this.database && this.database.offer && this.database.offer();
this.isAutoPilotSelected.setBaseline(AutoPilotUtils.isValidAutoPilotThroughput(offer.autoscaleMaxThroughput));
this.autoPilotThroughput.setBaseline(offer.autoscaleMaxThroughput);
this.throughput.setBaseline(offer.manualThroughput);
const offerThroughput = offer.content && offer.content.offerThroughput;
const offerAutopilotSettings = offer && offer.content && offer.content.offerAutopilotSettings;
this.throughput.setBaseline(offerThroughput);
this.userCanChangeProvisioningTypes(true);
const maxThroughputForAutoPilot = offerAutopilotSettings && offerAutopilotSettings.maxThroughput;
this.isAutoPilotSelected.setBaseline(AutoPilotUtils.isValidAutoPilotThroughput(maxThroughputForAutoPilot));
this.autoPilotThroughput.setBaseline(maxThroughputForAutoPilot || AutoPilotUtils.minAutoPilotThroughput);
}
protected getTabsButtons(): CommandButtonComponentProps[] {

View File

@@ -0,0 +1,721 @@
<div
class="tab-pane flexContainer"
data-bind="
attr:{
id: tabId
},
visible: isActive"
role="tabpanel"
>
<div class="warningErrorContainer scaleWarningContainer" data-bind="visible: shouldShowStatusBar">
<div>
<div class="warningErrorContent" data-bind="visible: shouldShowNotificationStatusPrompt">
<span><img src="/info_color.svg" alt="Info"/></span>
<span class="warningErrorDetailsLinkContainer" data-bind="html: notificationStatusInfo"></span>
</div>
<div class="warningErrorContent" data-bind="visible: !shouldShowNotificationStatusPrompt()">
<span><img src="/warning.svg" alt="Warning"/></span>
<span class="warningErrorDetailsLinkContainer" data-bind="html: warningMessage"></span>
</div>
</div>
</div>
<div class="tabForm scaleSettingScrollable">
<!-- ko if: shouldShowKeyspaceSharedThroughputMessage -->
<div>This table shared throughput is configured at the keyspace</div>
<!-- /ko -->
<!-- ko ifnot: hasDatabaseSharedThroughput -->
<div>
<div
class="scaleDivison"
data-bind="click:toggleScale, event: { keypress: onScaleKeyPress }, attr:{ 'aria-expanded': scaleExpanded() ? 'true' : 'false' }"
role="button"
tabindex="0"
aria-label="Scale"
aria-controls="scaleRegion"
>
<span class="themed-images" type="text/html" id="ExpandChevronRightScale" data-bind="visible: !scaleExpanded()">
<img
class="imgiconwidth ssExpandCollapseIcon ssCollapseIcon "
src="/Triangle-right.svg"
alt="Show scale properties"
/>
</span>
<span class="themed-images" type="text/html" id="ExpandChevronDownScale" data-bind="visible: scaleExpanded">
<img class="imgiconwidth ssExpandCollapseIcon " src="/Triangle-down.svg" alt="Hide scale properties" />
</span>
<span class="scaleSettingTitle">Scale</span>
</div>
<div class="ssTextAllignment" data-bind="visible: scaleExpanded" id="scaleRegion">
<!-- ko ifnot: isAutoScaleEnabled -->
<throughput-input-autopilot-v3
params="{
testId: testId,
class: 'scaleForm dirty',
value: throughput,
minimum: minRUs,
isEnabled: !hasDatabaseSharedThroughput(),
label: throughputTitle,
ariaLabel: throughputAriaLabel,
costsVisible: costsVisible,
requestUnitsUsageCost: requestUnitsUsageCost,
throughputAutoPilotRadioId: throughputAutoPilotRadioId,
throughputProvisionedRadioId: throughputProvisionedRadioId,
throughputModeRadioName: throughputModeRadioName,
showAutoPilot: userCanChangeProvisioningTypes,
isAutoPilotSelected: isAutoPilotSelected,
maxAutoPilotThroughputSet: autoPilotThroughput,
autoPilotUsageCost: autoPilotUsageCost,
overrideWithAutoPilotSettings: overrideWithAutoPilotSettings,
overrideWithProvisionedThroughputSettings: overrideWithProvisionedThroughputSettings
}"
>
</throughput-input-autopilot-v3>
<div class="storageCapacityTitle throughputStorageValue" data-bind="html: storageCapacityTitle"></div>
<!-- /ko -->
<div data-bind="visible: rupmVisible">
<div class="formTitle">RU/m</div>
<div class="tabs" aria-label="RU/m">
<div class="tab">
<label
data-bind="
attr:{
for: rupmOnId
},
css: {
dirty: rupm.editableIsDirty,
selectedRadio: rupm() === 'on',
unselectedRadio: rupm() !== 'on'
}"
>On</label
>
<input
type="radio"
name="rupm"
value="on"
class="radio"
data-bind="
attr:{
id: rupmOnId
},
checked: rupm"
/>
</div>
<div class="tab">
<label
data-bind="
attr:{
for: rupmOffId
},
css: {
dirty: rupm.editableIsDirty,
selectedRadio: rupm() === 'off',
unselectedRadio: rupm() !== 'off'
}"
>Off</label
>
<input
type="radio"
name="rupm"
value="off"
class="radio"
data-bind="
attr:{
id: rupmOffId
},
checked: rupm"
/>
</div>
</div>
</div>
<!-- TODO: Replace link with call to the Azure Support blade -->
<div data-bind="visible: isAutoScaleEnabled">
<div class="autoScaleThroughputTitle">Throughput (RU/s)</div>
<input
class="formReadOnly collid-white"
readonly
aria-label="Throughput input"
data-bind="textInput: throughput"
/>
<div class="autoScaleDescription">
Your account has custom settings that prevents setting throughput at the container level. Please work with
your Cosmos DB engineering team point of contact to make changes.
</div>
</div>
</div>
</div>
<!-- /ko -->
<div data-bind="visible: hasConflictResolution">
<div
class="formTitle"
data-bind="click:toggleConflictResolution, event: { keypress: onConflictResolutionKeyPress }, attr:{ 'aria-expanded': conflictResolutionExpanded() ? 'true' : 'false' }"
role="button"
tabindex="0"
aria-label="Conflict Resolution"
aria-controls="conflictResolutionRegion"
>
<span
class="themed-images"
type="text/html"
id="ExpandChevronRightConflictResolution"
data-bind="visible: !conflictResolutionExpanded()"
>
<img
class="imgiconwidth ssExpandCollapseIcon ssCollapseIcon"
src="/Triangle-right.svg"
alt="Show conflict resolution"
/>
</span>
<span
class="themed-images"
type="text/html"
id="ExpandChevronDownConflictResolution"
data-bind="visible: conflictResolutionExpanded"
>
<img class="imgiconwidth ssExpandCollapseIcon" src="/Triangle-down.svg" alt="Show conflict resolution" />
</span>
<span class="scaleSettingTitle">Conflict resolution</span>
</div>
<div id="conflictResolutionRegion" class="ssTextAllignment" data-bind="visible: conflictResolutionExpanded">
<div class="formTitle">Mode</div>
<div class="tabs" aria-label="Mode" role="radiogroup">
<div class="tab">
<label
tabindex="0"
role="radio"
data-bind="
attr:{
for: conflictResolutionPolicyModeLWW,
'aria-checked': conflictResolutionPolicyMode() !== 'Custom' ? 'true' : 'false'
},
css: {
dirty: conflictResolutionPolicyMode.editableIsDirty,
selectedRadio: conflictResolutionPolicyMode() === 'LastWriterWins',
unselectedRadio: conflictResolutionPolicyMode() !== 'LastWriterWins'
},
event: {
keypress: onConflictResolutionLWWKeyPress
}"
>Last Write Wins (default)</label
>
<input
type="radio"
name="conflictresolution"
value="LastWriterWins"
class="radio"
data-bind="
attr:{
id: conflictResolutionPolicyModeLWW
},
checked: conflictResolutionPolicyMode"
/>
</div>
<div class="tab">
<label
tabindex="0"
role="radio"
data-bind="
attr:{
for: conflictResolutionPolicyModeCustom,
'aria-checked': conflictResolutionPolicyMode() === 'Custom' ? 'true' : 'false'
},
css: {
dirty: conflictResolutionPolicyMode.editableIsDirty,
selectedRadio: conflictResolutionPolicyMode() === 'Custom',
unselectedRadio: conflictResolutionPolicyMode() !== 'Custom'
},
event: {
keypress: onConflictResolutionCustomKeyPress
}"
>Merge Procedure (custom)</label
>
<input
type="radio"
name="conflictresolution"
value="Custom"
class="radio"
data-bind="
attr:{
id: conflictResolutionPolicyModeCustom
},
checked: conflictResolutionPolicyMode"
/>
</div>
</div>
<div data-bind="visible: conflictResolutionPolicyMode() === 'LastWriterWins'">
<p class="formTitle">
Conflict Resolver Property
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext infoTooltipWidth"
>Gets or sets the name of a integer property in your documents which is used for the Last Write Wins
(LWW) based conflict resolution scheme. By default, the system uses the system defined timestamp
property, _ts to decide the winner for the conflicting versions of the document. Specify your own
integer property if you want to override the default timestamp based conflict resolution.</span
>
</span>
</p>
<p>
<input
type="text"
aria-label="Document path for conflict resolution"
data-bind="
css: {
dirty: conflictResolutionPolicyPath.editableIsDirty
},
textInput: conflictResolutionPolicyPath,
enable: conflictResolutionPolicyMode() === 'LastWriterWins'"
/>
</p>
</div>
<div data-bind="visible: conflictResolutionPolicyMode() === 'Custom'">
<p class="formTitle">
Stored procedure
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext infoTooltipWidth"
>Gets or sets the name of a stored procedure (aka merge procedure) for resolving the conflicts. You can
write application defined logic to determine the winner of the conflicting versions of a document. The
stored procedure will get executed transactionally, exactly once, on the server side. If you do not
provide a stored procedure, the conflicts will be populated in the
<a class="linkDarkBackground" href="https://aka.ms/dataexplorerconflics" target="_blank"
>conflicts feed</a
>. You can update/re-register the stored procedure at any time.</span
>
</span>
</p>
<p>
<input
type="text"
aria-label="Stored procedure name for conflict resolution"
data-bind="
css: {
dirty: conflictResolutionPolicyProcedure.editableIsDirty
},
textInput: conflictResolutionPolicyProcedure,
enable: conflictResolutionPolicyMode() === 'Custom'"
/>
</p>
</div>
</div>
</div>
<div
class="formTitle"
data-bind="click:toggleSettings, event: { keypress: onSettingsKeyPress }, attr:{ 'aria-expanded': settingsExpanded() ? 'true' : 'false' }, visible: shouldShowIndexingPolicyEditor() || ttlVisible()"
role="button"
tabindex="0"
aria-label="Settings"
aria-controls="settingsRegion"
>
<span
class="themed-images"
type="text/html"
id="ExpandChevronRightSettings"
data-bind="visible: !settingsExpanded() && !hasDatabaseSharedThroughput()"
>
<img class="imgiconwidth ssExpandCollapseIcon ssCollapseIcon" src="/Triangle-right.svg" alt="Show settings" />
</span>
<span
class="themed-images"
type="text/html"
id="ExpandChevronDownSettings"
data-bind="visible: settingsExpanded() && !hasDatabaseSharedThroughput()"
>
<img class="imgiconwidth ssExpandCollapseIcon" src="/Triangle-down.svg" alt="Show settings" />
</span>
<span class="scaleSettingTitle">Settings</span>
</div>
<div class="ssTextAllignment" data-bind="visible: settingsExpanded" id="settingsRegion">
<div data-bind="visible: ttlVisible">
<div class="formTitle">Time to Live</div>
<div class="tabs disableFocusDefaults" aria-label="Time to Live" role="radiogroup">
<div class="tab">
<label
class="ttlIndexingPolicyFocusElement"
tabindex="0"
role="radio"
data-bind="
attr:{
for: ttlOffId,
'aria-checked': timeToLive() === 'off' ? 'true' : 'false'
},
css: {
dirty: timeToLive.editableIsDirty,
selectedRadio: timeToLive() === 'off',
unselectedRadio: timeToLive() !== 'off'
},
event: {
keypress: onTtlOffKeyPress
},
hasFocus: ttlOffFocused"
>Off</label
>
<input
type="radio"
name="ttl"
value="off"
class="radio"
data-bind="
attr:{
id: ttlOffId
},
checked: timeToLive"
/>
</div>
<div class="tab">
<label
class="ttlIndexingPolicyFocusElement"
tabindex="0"
role="radio"
data-bind="
attr:{
for: ttlOnNoDefaultId,
'aria-checked': timeToLive() === 'on-nodefault' ? 'true' : 'false'
},
css: {
dirty: timeToLive.editableIsDirty,
selectedRadio: timeToLive() === 'on-nodefault',
unselectedRadio: timeToLive() !== 'on-nodefault'
},
event: {
keypress: onTtlOnNoDefaultKeyPress
},
hasFocus: ttlOnDefaultFocused"
>On (no default)</label
>
<input
type="radio"
name="ttl"
value="on-nodefault"
class="radio"
data-bind="
attr:{
id: ttlOnNoDefaultId
},
checked: timeToLive"
/>
</div>
<div class="tab">
<label
class="ttlIndexingPolicyFocusElement"
tabindex="0"
role="radio"
for="ttl3"
data-bind="
attr:{
for: ttlOnId,
'aria-checked': timeToLive() === 'on' ? 'true' : 'false'
},
css: {
dirty: timeToLive.editableIsDirty,
selectedRadio: timeToLive() === 'on',
unselectedRadio: timeToLive() !== 'on'
},
event: {
keypress: onTtlOnKeyPress
},
hasFocus: ttlOnFocused"
>On</label
>
<input
type="radio"
name="ttl"
value="on"
class="radio"
data-bind="
attr:{
id: ttlOnId
},
checked: timeToLive"
/>
</div>
</div>
<div data-bind="visible: timeToLive() === 'on'">
<input
class="dirtyTextbox"
type="number"
required
min="1"
max="2147483647"
aria-label="Time to live in seconds"
data-bind="
css: {
dirty: timeToLive.editableIsDirty
},
textInput: timeToLiveSeconds,
enable: timeToLive() === 'on'"
/>
second(s)
</div>
</div>
<!-- Geospatial - start -->
<div data-bind="visible: geospatialVisible">
<div class="formTitle">Geospatial Configuration</div>
<div class="tabs disableFocusDefaults" aria-label="Geospatial Configuration" role="radiogroup">
<div class="tab">
<label
for="geography"
tabindex="0"
role="radio"
data-bind="
attr:{
'aria-checked': geospatialConfigType().toLowerCase() !== GEOMETRY.toLowerCase() ? 'true' : 'false'
},
css: {
dirty: geospatialConfigType.editableIsDirty,
selectedRadio: geospatialConfigType().toLowerCase() !== GEOMETRY.toLowerCase(),
unselectedRadio: geospatialConfigType().toLowerCase() === GEOMETRY.toLowerCase()
},
event: {
keypress: onGeographyKeyPress
}"
>Geography</label
>
<input
type="radio"
name="geospatial"
id="geography"
class="radio"
data-bind="
attr: {
value: GEOGRAPHY
},
checked: geospatialConfigType"
/>
</div>
<div class="tab">
<label
for="geometry"
tabindex="0"
role="radio"
data-bind="
attr:{
'aria-checked': geospatialConfigType().toLowerCase() === GEOMETRY.toLowerCase() ? 'true' : 'false'
},
css: {
dirty: geospatialConfigType.editableIsDirty,
selectedRadio: geospatialConfigType().toLowerCase() === GEOMETRY.toLowerCase(),
unselectedRadio: geospatialConfigType().toLowerCase() !== GEOMETRY.toLowerCase()
},
event: {
keypress: onGeometryKeyPress
}"
>Geometry</label
>
<input
type="radio"
name="geospatial"
id="geometry"
class="radio"
data-bind="
attr: {
value: GEOMETRY
},
checked: geospatialConfigType"
/>
</div>
</div>
</div>
<!-- Geospatial - end -->
<div data-bind="visible: isAnalyticalStorageEnabled">
<div class="formTitle">Analytical Storage Time to Live</div>
<div class="tabs disableFocusDefaults" aria-label="Analytical Storage Time to Live" role="radiogroup">
<div class="tab">
<label tabindex="0" role="radio" class="disabledRadio">Off </label>
</div>
<div class="tab">
<label
tabindex="0"
role="radio"
data-bind="
attr:{
for: 'analyticalStorageTtlOnNoDefaultId',
'aria-checked': analyticalStorageTtlSelection() === 'on-nodefault' ? 'true' : 'false'
},
css: {
dirty: analyticalStorageTtlSelection.editableIsDirty,
selectedRadio: analyticalStorageTtlSelection() === 'on-nodefault',
unselectedRadio: analyticalStorageTtlSelection() !== 'on-nodefault'
},
event: {
keypress: onAnalyticalStorageTtlOnNoDefaultKeyPress
}"
>On (no default)
</label>
<input
type="radio"
name="analyticalStorageTtl"
value="on-nodefault"
class="radio"
data-bind="
attr:{
id: 'analyticalStorageTtlOnNoDefaultId'
},
checked: analyticalStorageTtlSelection"
/>
</div>
<div class="tab">
<label
tabindex="0"
role="radio"
for="ttl3"
data-bind="
attr:{
for: 'analyticalStorageTtlOnId',
'aria-checked': analyticalStorageTtlSelection() === 'on' ? 'true' : 'false'
},
css: {
dirty: analyticalStorageTtlSelection.editableIsDirty,
selectedRadio: analyticalStorageTtlSelection() === 'on',
unselectedRadio: analyticalStorageTtlSelection() !== 'on'
},
event: {
keypress: onAnalyticalStorageTtlOnKeyPress
}"
>On</label
>
<input
type="radio"
name="analyticalStorageTtl"
value="on"
class="radio"
data-bind="
attr:{
id: 'analyticalStorageTtlOnId'
},
checked: analyticalStorageTtlSelection"
/>
</div>
</div>
<div data-bind="visible: analyticalStorageTtlSelection() === 'on'">
<input
class="dirtyTextbox"
type="number"
required
min="1"
max="2147483647"
aria-label="Time to live in seconds"
data-bind="
css: {
dirty: analyticalStorageTtlSelection.editableIsDirty
},
textInput: analyticalStorageTtlSeconds,
enable: analyticalStorageTtlSelection() === 'on'"
/>
second(s)
</div>
</div>
<div data-bind="visible: changeFeedPolicyVisible">
<div class="formTitle">
<span>Change feed log retention policy</span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext infoTooltipWidth"
>Enable change feed log retention policy to retain last 10 minutes of history for items in the container
by default. To support this, the request unit (RU) charge for this container will be multiplied by a
factor of two for writes. Reads are unaffected.</span
>
</span>
</div>
<div class="tabs disableFocusDefaults" aria-label="Change feed selection tabs">
<div class="tab">
<label
tabindex="0"
data-bind="
attr:{
for: changeFeedPolicyOffId
},
css: {
dirty: changeFeedPolicyToggled.editableIsDirty,
selectedRadio: changeFeedPolicyToggled() === 'Off',
unselectedRadio: changeFeedPolicyToggled() === 'On'
},
event: {
keypress: onChangeFeedPolicyOffKeyPress
}"
>Off</label
>
<input
type="radio"
name="changeFeedPolicy"
value="Off"
class="radio"
data-bind="
attr:{
id: changeFeedPolicyOffId
},
checked: changeFeedPolicyToggled"
/>
</div>
<div class="tab">
<label
tabindex="0"
data-bind="
attr:{
for: changeFeedPolicyOnId
},
css: {
dirty: changeFeedPolicyToggled.editableIsDirty,
selectedRadio: changeFeedPolicyToggled() === 'On',
unselectedRadio: changeFeedPolicyToggled() === 'Off'
},
event: {
keypress: onChangeFeedPolicyOnKeyPress
}"
>On</label
>
<input
type="radio"
name="changeFeedPolicy"
value="On"
class="radio"
data-bind="
attr:{
id: changeFeedPolicyOnId
},
checked: changeFeedPolicyToggled"
/>
</div>
</div>
</div>
<div data-bind="visible: partitionKeyVisible">
<div class="formTitle" data-bind="text: partitionKeyName">Partition Key</div>
<input
class="formReadOnly collid-white"
data-bind="textInput: partitionKeyValue, attr: { 'aria-label':partitionKeyName }"
readonly
/>
</div>
<div class="largePartitionKeyEnabled" data-bind="visible: isLargePartitionKeyEnabled">
<p data-bind="visible: isLargePartitionKeyEnabled">
Large <span data-bind="text:lowerCasePartitionKeyName"></span> has been enabled
</p>
</div>
<div data-bind="visible: shouldShowIndexingPolicyEditor">
<div class="formTitle">Indexing Policy</div>
<div
class="indexingPolicyEditor ttlIndexingPolicyFocusElement"
tabindex="0"
data-bind="setTemplateReady: true, attr:{ id: indexingPolicyEditorId }"
></div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,436 @@
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import Collection from "../Tree/Collection";
import Database from "../Tree/Database";
import Explorer from "../Explorer";
import SettingsTab from "./SettingsTab";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { IndexingPolicies } from "../../Shared/Constants";
describe("Settings tab", () => {
const baseCollection: DataModels.Collection = {
defaultTtl: 200,
partitionKey: null,
conflictResolutionPolicy: {
mode: DataModels.ConflictResolutionMode.LastWriterWins,
conflictResolutionPath: "/_ts"
},
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
_rid: "",
_self: "",
_etag: "",
_ts: 0,
id: "mycoll"
};
const baseDatabase: DataModels.Database = {
_rid: "",
_self: "",
_etag: "",
_ts: 0,
id: "mydb",
collections: [baseCollection]
};
describe("Conflict Resolution", () => {
describe("should show conflict resolution", () => {
let explorer: Explorer;
const baseCollectionWithoutConflict: DataModels.Collection = {
defaultTtl: 200,
partitionKey: null,
conflictResolutionPolicy: null,
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
_rid: "",
_self: "",
_etag: "",
_ts: 0,
id: "mycoll"
};
const getSettingsTab = (conflictResolution: boolean = true) => {
return new SettingsTab({
tabKind: ViewModels.CollectionTabKind.Settings,
title: "Scale & Settings",
tabPath: "",
hashLocation: "",
isActive: ko.observable(false),
collection: new Collection(
explorer,
"mydb",
conflictResolution ? baseCollection : baseCollectionWithoutConflict,
null
),
onUpdateTabsButtons: undefined
});
};
beforeEach(() => {
explorer = new Explorer();
});
it("single master, should not show conflict resolution", () => {
const settingsTab = getSettingsTab();
expect(settingsTab.hasConflictResolution()).toBe(false);
});
it("multi master with resolution conflict, show conflict resolution", () => {
explorer.databaseAccount({
id: "test",
kind: "",
location: "",
name: "",
tags: "",
type: "",
properties: {
enableMultipleWriteLocations: true,
documentEndpoint: "",
cassandraEndpoint: "",
gremlinEndpoint: "",
tableEndpoint: ""
}
});
const settingsTab = getSettingsTab();
expect(settingsTab.hasConflictResolution()).toBe(true);
});
it("multi master without resolution conflict, show conflict resolution", () => {
explorer.databaseAccount({
id: "test",
kind: "",
location: "",
name: "",
tags: "",
type: "",
properties: {
enableMultipleWriteLocations: true,
documentEndpoint: "",
cassandraEndpoint: "",
gremlinEndpoint: "",
tableEndpoint: ""
}
});
const settingsTab = getSettingsTab(false /* no resolution conflict*/);
expect(settingsTab.hasConflictResolution()).toBe(false);
});
});
describe("Parse Conflict Resolution Mode from backend", () => {
it("should parse any casing", () => {
expect(SettingsTab.parseConflictResolutionMode("custom")).toBe(DataModels.ConflictResolutionMode.Custom);
expect(SettingsTab.parseConflictResolutionMode("Custom")).toBe(DataModels.ConflictResolutionMode.Custom);
expect(SettingsTab.parseConflictResolutionMode("lastWriterWins")).toBe(
DataModels.ConflictResolutionMode.LastWriterWins
);
expect(SettingsTab.parseConflictResolutionMode("LastWriterWins")).toBe(
DataModels.ConflictResolutionMode.LastWriterWins
);
});
it("should parse empty as null", () => {
expect(SettingsTab.parseConflictResolutionMode("")).toBe(null);
});
it("should parse null as null", () => {
expect(SettingsTab.parseConflictResolutionMode(null)).toBe(null);
});
});
describe("Parse Conflict Resolution procedure from backend", () => {
it("should parse path as name", () => {
expect(SettingsTab.parseConflictResolutionProcedure("/dbs/xxxx/colls/xxxx/sprocs/validsproc")).toBe(
"validsproc"
);
});
it("should parse name as name", () => {
expect(SettingsTab.parseConflictResolutionProcedure("validsproc")).toBe("validsproc");
});
it("should parse invalid path as null", () => {
expect(SettingsTab.parseConflictResolutionProcedure("/not/a/valid/path")).toBe(null);
});
it("should parse empty or null as null", () => {
expect(SettingsTab.parseConflictResolutionProcedure("")).toBe(null);
expect(SettingsTab.parseConflictResolutionProcedure(null)).toBe(null);
});
});
});
describe("Should update collection", () => {
let explorer: Explorer;
beforeEach(() => {
explorer = new Explorer();
});
it("On TTL changed", () => {
const settingsTab = new SettingsTab({
tabKind: ViewModels.CollectionTabKind.Settings,
title: "Scale & Settings",
tabPath: "",
hashLocation: "",
isActive: ko.observable(false),
collection: new Collection(explorer, "mydb", baseCollection, null),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
});
expect(settingsTab.shouldUpdateCollection()).toBe(false);
settingsTab.timeToLive("off");
expect(settingsTab.shouldUpdateCollection()).toBe(true);
settingsTab.onRevertClick();
expect(settingsTab.shouldUpdateCollection()).toBe(false);
settingsTab.timeToLiveSeconds(100);
expect(settingsTab.shouldUpdateCollection()).toBe(true);
});
it("On Index Policy changed", () => {
const settingsTab = new SettingsTab({
tabKind: ViewModels.CollectionTabKind.Settings,
title: "Scale & Settings",
tabPath: "",
hashLocation: "",
isActive: ko.observable(false),
collection: new Collection(explorer, "mydb", baseCollection, null),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
});
expect(settingsTab.shouldUpdateCollection()).toBe(false);
settingsTab.indexingPolicyContent({ somethingDifferent: "" });
expect(settingsTab.shouldUpdateCollection()).toBe(true);
});
it("On Conflict Resolution Mode changed", () => {
const settingsTab = new SettingsTab({
tabKind: ViewModels.CollectionTabKind.Settings,
title: "Scale & Settings",
tabPath: "",
hashLocation: "",
isActive: ko.observable(false),
collection: new Collection(explorer, "mydb", baseCollection, null),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
});
expect(settingsTab.shouldUpdateCollection()).toBe(false);
settingsTab.conflictResolutionPolicyMode(DataModels.ConflictResolutionMode.Custom);
expect(settingsTab.shouldUpdateCollection()).toBe(true);
settingsTab.onRevertClick();
expect(settingsTab.shouldUpdateCollection()).toBe(false);
settingsTab.conflictResolutionPolicyPath("/somethingElse");
expect(settingsTab.shouldUpdateCollection()).toBe(true);
settingsTab.onRevertClick();
expect(settingsTab.shouldUpdateCollection()).toBe(false);
settingsTab.conflictResolutionPolicyMode(DataModels.ConflictResolutionMode.Custom);
settingsTab.conflictResolutionPolicyProcedure("resolver");
expect(settingsTab.shouldUpdateCollection()).toBe(true);
});
});
describe("Get Conflict Resolution configuration from user", () => {
let explorer: Explorer;
beforeEach(() => {
explorer = new Explorer();
});
it("null if it didnt change", () => {
const settingsTab = new SettingsTab({
tabKind: ViewModels.CollectionTabKind.Settings,
title: "Scale & Settings",
tabPath: "",
hashLocation: "",
isActive: ko.observable(false),
collection: new Collection(explorer, "mydb", baseCollection, null),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
});
expect(settingsTab.getUpdatedConflictResolutionPolicy()).toBe(null);
});
it("Custom contains valid backend path", () => {
const settingsTab = new SettingsTab({
tabKind: ViewModels.CollectionTabKind.Settings,
title: "Scale & Settings",
tabPath: "",
hashLocation: "",
isActive: ko.observable(false),
collection: new Collection(explorer, "mydb", baseCollection, null),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
});
expect(settingsTab.getUpdatedConflictResolutionPolicy()).toBe(null);
settingsTab.conflictResolutionPolicyMode(DataModels.ConflictResolutionMode.Custom);
settingsTab.conflictResolutionPolicyProcedure("resolver");
let updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
expect(updatedPolicy.mode).toBe(DataModels.ConflictResolutionMode.Custom);
expect(updatedPolicy.conflictResolutionProcedure).toBe("/dbs/mydb/colls/mycoll/sprocs/resolver");
settingsTab.conflictResolutionPolicyProcedure("");
updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
expect(updatedPolicy.conflictResolutionProcedure).toBe(undefined);
});
it("LWW contains valid property path", () => {
const settingsTab = new SettingsTab({
tabKind: ViewModels.CollectionTabKind.Settings,
title: "Scale & Settings",
tabPath: "",
hashLocation: "",
isActive: ko.observable(false),
collection: new Collection(explorer, "mydb", baseCollection, null),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
});
expect(settingsTab.getUpdatedConflictResolutionPolicy()).toBe(null);
settingsTab.conflictResolutionPolicyPath("someAttr");
let updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
expect(updatedPolicy.conflictResolutionPath).toBe("/someAttr");
settingsTab.conflictResolutionPolicyPath("/someAttr");
updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
expect(updatedPolicy.conflictResolutionPath).toBe("/someAttr");
settingsTab.conflictResolutionPolicyPath("");
updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
expect(updatedPolicy.conflictResolutionPath).toBe("");
});
});
describe("partitionKeyVisible", () => {
enum PartitionKeyOption {
None,
System,
NonSystem
}
function getCollection(defaultApi: string, partitionKeyOption: PartitionKeyOption) {
const explorer = new Explorer();
explorer.defaultExperience(defaultApi);
const offer: DataModels.Offer = null;
const defaultTtl = 200;
const conflictResolutionPolicy = {
mode: DataModels.ConflictResolutionMode.LastWriterWins,
conflictResolutionPath: "/_ts"
};
return new Collection(
explorer,
"mydb",
{
defaultTtl: defaultTtl,
partitionKey:
partitionKeyOption != PartitionKeyOption.None
? {
paths: ["/foo"],
kind: "Hash",
version: 2,
systemKey: partitionKeyOption === PartitionKeyOption.System
}
: null,
conflictResolutionPolicy: conflictResolutionPolicy,
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
_rid: "",
_self: "",
_etag: "",
_ts: 0,
id: "mycoll"
},
offer
);
}
function getSettingsTab(defaultApi: string, partitionKeyOption: PartitionKeyOption): SettingsTab {
return new SettingsTab({
tabKind: ViewModels.CollectionTabKind.Settings,
title: "Scale & Settings",
tabPath: "",
hashLocation: "",
isActive: ko.observable(false),
collection: getCollection(defaultApi, partitionKeyOption),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
});
}
it("on SQL container with no partition key should be false", () => {
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.DocumentDB, PartitionKeyOption.None);
expect(settingsTab.partitionKeyVisible()).toBe(false);
});
it("on Mongo container with no partition key should be false", () => {
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.MongoDB, PartitionKeyOption.None);
expect(settingsTab.partitionKeyVisible()).toBe(false);
});
it("on Gremlin container with no partition key should be false", () => {
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Graph, PartitionKeyOption.None);
expect(settingsTab.partitionKeyVisible()).toBe(false);
});
it("on Cassandra container with no partition key should be false", () => {
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Cassandra, PartitionKeyOption.None);
expect(settingsTab.partitionKeyVisible()).toBe(false);
});
it("on Table container with no partition key should be false", () => {
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Table, PartitionKeyOption.None);
expect(settingsTab.partitionKeyVisible()).toBe(false);
});
it("on SQL container with system partition key should be true", () => {
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.DocumentDB, PartitionKeyOption.System);
expect(settingsTab.partitionKeyVisible()).toBe(true);
});
it("on Mongo container with system partition key should be false", () => {
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.MongoDB, PartitionKeyOption.System);
expect(settingsTab.partitionKeyVisible()).toBe(false);
});
it("on Gremlin container with system partition key should be true", () => {
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Graph, PartitionKeyOption.System);
expect(settingsTab.partitionKeyVisible()).toBe(true);
});
it("on Cassandra container with system partition key should be false", () => {
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Cassandra, PartitionKeyOption.System);
expect(settingsTab.partitionKeyVisible()).toBe(false);
});
it("on Table container with system partition key should be false", () => {
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Table, PartitionKeyOption.System);
expect(settingsTab.partitionKeyVisible()).toBe(false);
});
it("on SQL container with non-system partition key should be true", () => {
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.DocumentDB, PartitionKeyOption.NonSystem);
expect(settingsTab.partitionKeyVisible()).toBe(true);
});
it("on Mongo container with non-system partition key should be true", () => {
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.MongoDB, PartitionKeyOption.NonSystem);
expect(settingsTab.partitionKeyVisible()).toBe(true);
});
it("on Gremlin container with non-system partition key should be true", () => {
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Graph, PartitionKeyOption.NonSystem);
expect(settingsTab.partitionKeyVisible()).toBe(true);
});
it("on Cassandra container with non-system partition key should be false", () => {
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Cassandra, PartitionKeyOption.NonSystem);
expect(settingsTab.partitionKeyVisible()).toBe(false);
});
it("on Table container with non-system partition key should be false", () => {
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Table, PartitionKeyOption.NonSystem);
expect(settingsTab.partitionKeyVisible()).toBe(false);
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -47,7 +47,7 @@ export default class SettingsTabV2 extends TabsBase {
this.currentCollection.loadOffer().then(
() => {
// passed in options and set by parent as "Settings" by default
this.tabTitle(this.currentCollection.offer() ? "Settings" : "Scale & Settings");
this.tabTitle("Scale & Settings");
this.offerRead(true);
this.options.getPendingNotification.then(
(data: DataModels.Notification) => {

View File

@@ -9,6 +9,7 @@ import MongoQueryTabTemplate from "./MongoQueryTab.html";
import MongoShellTabTemplate from "./MongoShellTab.html";
import QueryTabTemplate from "./QueryTab.html";
import QueryTablesTabTemplate from "./QueryTablesTab.html";
import SettingsTabTemplate from "./SettingsTab.html";
import SettingsTabV2Template from "./SettingsTabV2.html";
import DatabaseSettingsTabTemplate from "./DatabaseSettingsTab.html";
import StoredProcedureTabTemplate from "./StoredProcedureTab.html";
@@ -132,6 +133,15 @@ export class QueryTablesTab {
}
}
export class SettingsTab {
constructor() {
return {
viewModel: TabComponent,
template: SettingsTabTemplate
};
}
}
export class SettingsTabV2 {
constructor() {
return {

View File

@@ -10,10 +10,9 @@ describe("Collection", () => {
container: Explorer,
databaseId: string,
data: DataModels.Collection,
quotaInfo: DataModels.CollectionQuotaInfo,
offer: DataModels.Offer
): Collection {
return new Collection(container, databaseId, data, quotaInfo, offer);
return new Collection(container, databaseId, data, offer);
}
function generateMockCollectionsDataModelWithPartitionKey(
@@ -50,7 +49,7 @@ describe("Collection", () => {
});
mockContainer.deleteCollectionText = ko.observable<string>("delete collection");
return generateCollection(mockContainer, "abc", data, {} as DataModels.CollectionQuotaInfo, {} as DataModels.Offer);
return generateCollection(mockContainer, "abc", data, {} as DataModels.Offer);
}
describe("Partition key path parsing", () => {

View File

@@ -10,7 +10,6 @@ import { readTriggers } from "../../Common/dataAccess/readTriggers";
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
import { createDocument } from "../../Common/DocumentClientUtilityBase";
import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer";
import { readCollectionQuotaInfo } from "../../Common/dataAccess/readCollectionQuotaInfo";
import * as Logger from "../../Common/Logger";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
@@ -29,6 +28,7 @@ import MongoShellTab from "../Tabs/MongoShellTab";
import QueryTab from "../Tabs/QueryTab";
import QueryTablesTab from "../Tabs/QueryTablesTab";
import SettingsTabV2 from "../Tabs/SettingsTabV2";
import SettingsTab from "../Tabs/SettingsTab";
import ConflictId from "./ConflictId";
import DocumentId from "./DocumentId";
import StoredProcedure from "./StoredProcedure";
@@ -54,7 +54,6 @@ export default class Collection implements ViewModels.Collection {
public defaultTtl: ko.Observable<number>;
public indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
public quotaInfo: ko.Observable<DataModels.CollectionQuotaInfo>;
public offer: ko.Observable<DataModels.Offer>;
public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
public changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
@@ -62,8 +61,6 @@ export default class Collection implements ViewModels.Collection {
public throughput: ko.Computed<number>;
public rawDataModel: DataModels.Collection;
public analyticalStorageTtl: ko.Observable<number>;
public schema: DataModels.ISchema;
public requestSchema: () => void;
public geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
// TODO move this to API customization class
@@ -95,13 +92,7 @@ export default class Collection implements ViewModels.Collection {
public userDefinedFunctionsFocused: ko.Observable<boolean>;
public triggersFocused: ko.Observable<boolean>;
constructor(
container: Explorer,
databaseId: string,
data: DataModels.Collection,
quotaInfo: DataModels.CollectionQuotaInfo,
offer: DataModels.Offer
) {
constructor(container: Explorer, databaseId: string, data: DataModels.Collection, offer: DataModels.Offer) {
this.nodeKind = "Collection";
this.container = container;
this.self = data._self;
@@ -113,13 +104,10 @@ export default class Collection implements ViewModels.Collection {
this.id = ko.observable(data.id);
this.defaultTtl = ko.observable(data.defaultTtl);
this.indexingPolicy = ko.observable(data.indexingPolicy);
this.quotaInfo = ko.observable(quotaInfo);
this.offer = ko.observable(offer);
this.conflictResolutionPolicy = ko.observable(data.conflictResolutionPolicy);
this.changeFeedPolicy = ko.observable<DataModels.ChangeFeedPolicy>(data.changeFeedPolicy);
this.analyticalStorageTtl = ko.observable(data.analyticalStorageTtl);
this.schema = data.schema;
this.requestSchema = data.requestSchema;
this.geospatialConfig = ko.observable(data.geospatialConfig);
// TODO fix this to only replace non-excaped single quotes
@@ -555,6 +543,11 @@ export default class Collection implements ViewModels.Collection {
dataExplorerArea: Constants.Areas.ResourceTree
});
const isSettingsV2Enabled = this.container.isSettingsV2Enabled();
if (!isSettingsV2Enabled) {
await this.loadOffer();
}
const tabTitle = !this.offer() ? "Settings" : "Scale & Settings";
const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Settings, tab => {
@@ -581,8 +574,68 @@ export default class Collection implements ViewModels.Collection {
onUpdateTabsButtons: this.container.onUpdateTabsButtons
};
let settingsTabV2 = matchingTabs && (matchingTabs[0] as SettingsTabV2);
this.launchSettingsTabV2(settingsTabV2, traceStartData, settingsTabOptions, pendingNotificationsPromise);
if (isSettingsV2Enabled) {
let settingsTabV2 = matchingTabs && (matchingTabs[0] as SettingsTabV2);
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 = (
@@ -607,14 +660,6 @@ export default class Collection implements ViewModels.Collection {
}
};
private async loadCollectionQuotaInfo(): Promise<void> {
// TODO: Use the collection entity cache to get quota info
const quotaInfoWithUniqueKeyPolicy = await readCollectionQuotaInfo(this);
this.uniqueKeyPolicy = quotaInfoWithUniqueKeyPolicy.uniqueKeyPolicy;
const quotaInfo = _.omit(quotaInfoWithUniqueKeyPolicy, "uniqueKeyPolicy");
this.quotaInfo(quotaInfo);
}
public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) {
const collection: ViewModels.Collection = source.collection || source;
const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Query).length + 1;
@@ -1287,7 +1332,6 @@ export default class Collection implements ViewModels.Collection {
try {
this.offer(await readCollectionOffer(params));
await this.loadCollectionQuotaInfo();
TelemetryProcessor.traceSuccess(
Action.LoadOffers,
@@ -1295,7 +1339,8 @@ export default class Collection implements ViewModels.Collection {
databaseAccountName: this.container.databaseAccount().name,
databaseName: this.databaseId,
collectionName: this.id(),
defaultExperience: this.container.defaultExperience()
defaultExperience: this.container.defaultExperience(),
offerVersion: this.offer()?.offerVersion
},
startKey
);

View File

@@ -1,82 +0,0 @@
import * as DataModels from "../../Contracts/DataModels";
import * as ko from "knockout";
import Database from "./Database";
import Explorer from "../Explorer";
import { HttpStatusCodes } from "../../Common/Constants";
import { JunoClient } from "../../Juno/JunoClient";
import { userContext, updateUserContext } from "../../UserContext";
const createMockContainer = (): Explorer => {
const mockContainer = new Explorer();
return mockContainer;
};
updateUserContext({
subscriptionId: "fakeSubscriptionId",
resourceGroup: "fakeResourceGroup",
databaseAccount: {
id: "id",
name: "fakeName",
location: "fakeLocation",
type: "fakeType",
tags: undefined,
kind: "fakeKind",
properties: {
documentEndpoint: "fakeEndpoint",
tableEndpoint: "fakeEndpoint",
gremlinEndpoint: "fakeEndpoint",
cassandraEndpoint: "fakeEndpoint"
}
}
});
describe("Add Schema", () => {
it("should not call requestSchema or getSchema if analyticalStorageTtl is undefined", () => {
const collection: DataModels.Collection = {} as DataModels.Collection;
collection.analyticalStorageTtl = undefined;
const database = new Database(createMockContainer(), { id: "fakeId" });
database.container = createMockContainer();
database.container.isSchemaEnabled = ko.computed<boolean>(() => false);
database.junoClient = new JunoClient();
database.junoClient.requestSchema = jest.fn();
database.junoClient.getSchema = jest.fn();
database.addSchema(collection);
expect(database.junoClient.requestSchema).toBeCalledTimes(0);
});
it("should call requestSchema or getSchema if analyticalStorageTtl is not undefined", () => {
const collection: DataModels.Collection = { id: "fakeId" } as DataModels.Collection;
collection.analyticalStorageTtl = 0;
const database = new Database(createMockContainer(), {});
database.container = createMockContainer();
database.container.isSchemaEnabled = ko.computed<boolean>(() => true);
database.junoClient = new JunoClient();
database.junoClient.requestSchema = jest.fn();
database.junoClient.getSchema = jest.fn().mockResolvedValue({ status: HttpStatusCodes.OK, data: {} });
jest.useFakeTimers();
const interval = 5000;
const checkForSchema: NodeJS.Timeout = database.addSchema(collection, interval);
jest.advanceTimersByTime(interval + 1000);
expect(database.junoClient.requestSchema).toBeCalledWith({
id: undefined,
subscriptionId: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
accountName: userContext.databaseAccount.name,
resource: `dbs/${database.id}/colls/${collection.id}`,
status: "new"
});
expect(checkForSchema).not.toBeNull();
expect(database.junoClient.getSchema).toBeCalledWith(
userContext.databaseAccount.name,
database.id(),
collection.id
);
});
});

View File

@@ -13,8 +13,6 @@ import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsol
import * as Logger from "../../Common/Logger";
import Explorer from "../Explorer";
import { readCollections } from "../../Common/dataAccess/readCollections";
import { JunoClient, IJunoResponse } from "../../Juno/JunoClient";
import { userContext } from "../../UserContext";
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
@@ -31,7 +29,6 @@ export default class Database implements ViewModels.Database {
public isDatabaseExpanded: ko.Observable<boolean>;
public isDatabaseShared: ko.Computed<boolean>;
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
public junoClient: JunoClient;
constructor(container: Explorer, data: any) {
this.nodeKind = "Database";
@@ -46,7 +43,6 @@ export default class Database implements ViewModels.Database {
this.isDatabaseShared = ko.pureComputed(() => {
return this.offer && !!this.offer();
});
this.junoClient = new JunoClient();
}
public onSettingsClick = () => {
@@ -188,12 +184,8 @@ export default class Database implements ViewModels.Database {
const collections: DataModels.Collection[] = await readCollections(this.id());
const deltaCollections = this.getDeltaCollections(collections);
collections.forEach((collection: DataModels.Collection) => {
this.addSchema(collection);
});
deltaCollections.toAdd.forEach((collection: DataModels.Collection) => {
const collectionVM: Collection = new Collection(this.container, this.id(), collection, null, null);
const collectionVM: Collection = new Collection(this.container, this.id(), collection, null);
collectionVMs.push(collectionVM);
});
@@ -316,42 +308,4 @@ export default class Database implements ViewModels.Database {
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;
}
}

View File

@@ -1,253 +0,0 @@
import * as ko from "knockout";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import React from "react";
import { ResourceTreeAdapter } from "./ResourceTreeAdapter";
import { shallow } from "enzyme";
import { TreeComponent, TreeNode, TreeComponentProps } from "../Controls/TreeComponent/TreeComponent";
import Explorer from "../Explorer";
import Collection from "./Collection";
const schema: DataModels.ISchema = {
id: "fakeSchemaId",
accountName: "fakeAccountName",
resource: "dbs/FakeDbName/colls/FakeCollectionName",
fields: [
{
dataType: {
code: 15,
name: "String"
},
hasNulls: true,
isArray: false,
schemaType: {
code: 0,
name: "Data"
},
name: "_rid",
path: "_rid",
maxRepetitionLevel: 0,
maxDefinitionLevel: 1
},
{
dataType: {
code: 11,
name: "Int64"
},
hasNulls: true,
isArray: false,
schemaType: {
code: 0,
name: "Data"
},
name: "_ts",
path: "_ts",
maxRepetitionLevel: 0,
maxDefinitionLevel: 1
},
{
dataType: {
code: 15,
name: "String"
},
hasNulls: true,
isArray: false,
schemaType: {
code: 0,
name: "Data"
},
name: "id",
path: "id",
maxRepetitionLevel: 0,
maxDefinitionLevel: 1
},
{
dataType: {
code: 15,
name: "String"
},
hasNulls: true,
isArray: false,
schemaType: {
code: 0,
name: "Data"
},
name: "pk",
path: "pk",
maxRepetitionLevel: 0,
maxDefinitionLevel: 1
},
{
dataType: {
code: 15,
name: "String"
},
hasNulls: true,
isArray: false,
schemaType: {
code: 0,
name: "Data"
},
name: "other",
path: "other",
maxRepetitionLevel: 0,
maxDefinitionLevel: 1
},
{
dataType: {
code: 15,
name: "String"
},
hasNulls: true,
isArray: false,
schemaType: {
code: 0,
name: "Data"
},
name: "name",
path: "nested.name",
maxRepetitionLevel: 0,
maxDefinitionLevel: 1
},
{
dataType: {
code: 11,
name: "Int64"
},
hasNulls: true,
isArray: false,
schemaType: {
code: 0,
name: "Data"
},
name: "someNumber",
path: "nested.someNumber",
maxRepetitionLevel: 0,
maxDefinitionLevel: 1
},
{
dataType: {
code: 17,
name: "Double"
},
hasNulls: true,
isArray: false,
schemaType: {
code: 0,
name: "Data"
},
name: "anotherNumber",
path: "nested.anotherNumber",
maxRepetitionLevel: 0,
maxDefinitionLevel: 1
},
{
dataType: {
code: 15,
name: "String"
},
hasNulls: true,
isArray: false,
schemaType: {
code: 0,
name: "Data"
},
name: "name",
path: "items.list.items.name",
maxRepetitionLevel: 1,
maxDefinitionLevel: 3
},
{
dataType: {
code: 11,
name: "Int64"
},
hasNulls: true,
isArray: false,
schemaType: {
code: 0,
name: "Data"
},
name: "someNumber",
path: "items.list.items.someNumber",
maxRepetitionLevel: 1,
maxDefinitionLevel: 3
},
{
dataType: {
code: 17,
name: "Double"
},
hasNulls: true,
isArray: false,
schemaType: {
code: 0,
name: "Data"
},
name: "anotherNumber",
path: "items.list.items.anotherNumber",
maxRepetitionLevel: 1,
maxDefinitionLevel: 3
},
{
dataType: {
code: 15,
name: "String"
},
hasNulls: true,
isArray: false,
schemaType: {
code: 0,
name: "Data"
},
name: "_etag",
path: "_etag",
maxRepetitionLevel: 0,
maxDefinitionLevel: 1
}
]
};
const createMockContainer = (): Explorer => {
const mockContainer = new Explorer();
mockContainer.selectedNode = ko.observable<ViewModels.TreeNode>();
mockContainer.onUpdateTabsButtons = () => {
return;
};
return mockContainer;
};
const createMockCollection = (): ViewModels.Collection => {
const mockCollection = {} as DataModels.Collection;
mockCollection._rid = "fakeRid";
mockCollection._self = "fakeSelf";
mockCollection.id = "fakeId";
mockCollection.analyticalStorageTtl = 0;
mockCollection.schema = schema;
const mockCollectionVM: ViewModels.Collection = new Collection(
createMockContainer(),
"fakeDatabaseId",
mockCollection,
undefined,
undefined
);
return mockCollectionVM;
};
describe("Resource tree for schema", () => {
const mockContainer: Explorer = createMockContainer();
const resourceTree = new ResourceTreeAdapter(mockContainer);
it("should render", () => {
const rootNode: TreeNode = resourceTree.buildSchemaNode(createMockCollection());
const props: TreeComponentProps = {
rootNode,
className: "dataResourceTree"
};
const wrapper = shallow(<TreeComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -2,7 +2,7 @@ import * as ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
import { TreeComponent, TreeNode, TreeNodeMenuItem, TreeNodeComponent } from "../Controls/TreeComponent/TreeComponent";
import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
import * as ViewModels from "../../Contracts/ViewModels";
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory";
@@ -32,7 +32,6 @@ import StoredProcedure from "./StoredProcedure";
import Trigger from "./Trigger";
import TabsBase from "../Tabs/TabsBase";
import { userContext } from "../../UserContext";
import * as DataModels from "../../Contracts/DataModels";
export class ResourceTreeAdapter implements ReactAdapter {
public static readonly MyNotebooksTitle = "My Notebooks";
@@ -290,11 +289,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
});
}
const schemaNode: TreeNode = this.buildSchemaNode(collection);
if (schemaNode) {
children.push(schemaNode);
}
if (ResourceTreeAdapter.showScriptNodes(this.container)) {
children.push(this.buildStoredProcedureNode(collection));
children.push(this.buildUserDefinedFunctionsNode(collection));
@@ -411,75 +405,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
};
}
public buildSchemaNode(collection: ViewModels.Collection): TreeNode {
if (collection.analyticalStorageTtl() == undefined) {
return undefined;
}
if (!collection.schema || !collection.schema.fields) {
return undefined;
}
return {
label: "Schema",
children: this.getSchemaNodes(collection.schema.fields),
onClick: () => {
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Schema);
this.container.tabsManager.refreshActiveTab(
(tab: TabsBase) => tab.collection && tab.collection.rid === collection.rid
);
}
};
}
private getSchemaNodes(fields: DataModels.IDataField[]): TreeNode[] {
const schema: any = {};
//unflatten
fields.forEach((field: DataModels.IDataField, fieldIndex: number) => {
const path: string[] = field.path.split(".");
const fieldProperties = [field.dataType.name, `HasNulls: ${field.hasNulls}`];
let current: any = {};
path.forEach((name: string, pathIndex: number) => {
if (pathIndex === 0) {
if (schema[name] === undefined) {
if (pathIndex === path.length - 1) {
schema[name] = fieldProperties;
} else {
schema[name] = {};
}
}
current = schema[name];
} else {
if (current[name] === undefined) {
if (pathIndex === path.length - 1) {
current[name] = fieldProperties;
} else {
current[name] = {};
}
}
current = current[name];
}
});
});
const traverse = (obj: any): TreeNode[] => {
const children: TreeNode[] = [];
if (obj !== null && !Array.isArray(obj) && typeof obj === "object") {
Object.entries(obj).forEach(([key, value]) => {
children.push({ label: key, children: traverse(value) });
});
} else if (Array.isArray(obj)) {
return [{ label: obj[0] }, { label: obj[1] }];
}
return children;
};
return traverse(schema);
}
private buildNotebooksTrees(): TreeNode {
let notebooksTree: TreeNode = {
label: undefined,

View File

@@ -1,172 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Resource tree for schema should render 1`] = `
<div
className="treeComponent dataResourceTree"
>
<TreeNodeComponent
generation={0}
node={
Object {
"children": Array [
Object {
"children": Array [
Object {
"label": "String",
},
Object {
"label": "HasNulls: true",
},
],
"label": "_rid",
},
Object {
"children": Array [
Object {
"label": "Int64",
},
Object {
"label": "HasNulls: true",
},
],
"label": "_ts",
},
Object {
"children": Array [
Object {
"label": "String",
},
Object {
"label": "HasNulls: true",
},
],
"label": "id",
},
Object {
"children": Array [
Object {
"label": "String",
},
Object {
"label": "HasNulls: true",
},
],
"label": "pk",
},
Object {
"children": Array [
Object {
"label": "String",
},
Object {
"label": "HasNulls: true",
},
],
"label": "other",
},
Object {
"children": Array [
Object {
"children": Array [
Object {
"label": "String",
},
Object {
"label": "HasNulls: true",
},
],
"label": "name",
},
Object {
"children": Array [
Object {
"label": "Int64",
},
Object {
"label": "HasNulls: true",
},
],
"label": "someNumber",
},
Object {
"children": Array [
Object {
"label": "Double",
},
Object {
"label": "HasNulls: true",
},
],
"label": "anotherNumber",
},
],
"label": "nested",
},
Object {
"children": Array [
Object {
"children": Array [
Object {
"children": Array [
Object {
"children": Array [
Object {
"label": "String",
},
Object {
"label": "HasNulls: true",
},
],
"label": "name",
},
Object {
"children": Array [
Object {
"label": "Int64",
},
Object {
"label": "HasNulls: true",
},
],
"label": "someNumber",
},
Object {
"children": Array [
Object {
"label": "Double",
},
Object {
"label": "HasNulls: true",
},
],
"label": "anotherNumber",
},
],
"label": "items",
},
],
"label": "list",
},
],
"label": "items",
},
Object {
"children": Array [
Object {
"label": "String",
},
Object {
"label": "HasNulls: true",
},
],
"label": "_etag",
},
],
"label": "Schema",
"onClick": [Function],
}
}
paddingLeft={0}
/>
</div>
`;

View File

@@ -7,7 +7,6 @@ import { IGitHubResponse } from "../GitHub/GitHubClient";
import { IGitHubOAuthToken } from "../GitHub/GitHubOAuthService";
import { userContext } from "../UserContext";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { number } from "prop-types";
export interface IJunoResponse<T> {
status: number;
@@ -428,51 +427,6 @@ export class JunoClient {
};
}
public async requestSchema(
schemaRequest: DataModels.ISchemaRequest
): Promise<IJunoResponse<DataModels.ISchemaRequest>> {
const response = await window.fetch(`${this.getAnalyticsUrl()}/${schemaRequest.accountName}/schema/request`, {
method: "POST",
body: JSON.stringify(schemaRequest),
headers: JunoClient.getHeaders()
});
let data: DataModels.ISchemaRequest;
if (response.status === HttpStatusCodes.OK) {
data = await response.json();
}
return {
status: response.status,
data
};
}
public async getSchema(
accountName: string,
databaseName: string,
containerName: string
): Promise<IJunoResponse<DataModels.ISchema>> {
const response = await window.fetch(
`${this.getAnalyticsUrl()}/${accountName}/schema/${databaseName}/${containerName}`,
{
method: "GET",
headers: JunoClient.getHeaders()
}
);
let data: DataModels.ISchema;
if (response.status === HttpStatusCodes.OK) {
data = await response.json();
}
return {
status: response.status,
data
};
}
private async getNotebooks(input: RequestInfo, init?: RequestInit): Promise<IJunoResponse<IGalleryItem[]>> {
const response = await window.fetch(input, init);
@@ -503,10 +457,6 @@ export class JunoClient {
return `${this.getNotebooksUrl()}/${this.getAccount()}`;
}
private getAnalyticsUrl(): string {
return `${configContext.JUNO_ENDPOINT}/api/analytics`;
}
private static getHeaders(): HeadersInit {
const authorizationHeader = getAuthorizationHeader();
return {

117
src/Main.ts Normal file
View File

@@ -0,0 +1,117 @@
// CSS Dependencies
import "bootstrap/dist/css/bootstrap.css";
import "../less/documentDB.less";
import "../less/tree.less";
import "../less/forms.less";
import "../less/menus.less";
import "../less/infobox.less";
import "../less/messagebox.less";
import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less";
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
import "./Explorer/Controls/DynamicList/DynamicListComponent.less";
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
import "../less/TableStyles/queryBuilder.less";
import "../externals/jquery.dataTables.min.css";
import "../less/TableStyles/fulldatatables.less";
import "../less/TableStyles/EntityEditor.less";
import "../less/TableStyles/CustomizeColumns.less";
import "../less/resourceTree.less";
import "../externals/jquery.typeahead.min.css";
import "../externals/jquery-ui.min.css";
import "../externals/jquery-ui.structure.min.css";
import "../externals/jquery-ui.theme.min.css";
import "./Explorer/Graph/NewVertexComponent/newVertexComponent.less";
import "./Explorer/Panes/GraphNewVertexPane.less";
import "./Explorer/Tabs/QueryTab.less";
import "./Explorer/Controls/TreeComponent/treeComponent.less";
import "./Explorer/Controls/Accordion/AccordionComponent.less";
import "./Explorer/SplashScreen/SplashScreenComponent.less";
import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
// Image Dependencies
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
import "../images/favicon.ico";
import "./Shared/appInsights";
import "babel-polyfill";
import "es6-symbol/implement";
import "webcrypto-liner/build/webcrypto-liner.shim.min";
import "./Libs/jquery";
import "bootstrap/dist/js/npm";
import "../externals/jquery.typeahead.min.js";
import "../externals/jquery-ui.min.js";
import "../externals/adal.js";
import "promise-polyfill/src/polyfill";
import "abort-controller/polyfill";
import "whatwg-fetch";
import "es6-object-assign/auto";
import "promise.prototype.finally/auto";
import "object.entries/auto";
import "./Libs/is-integer-polyfill";
import "url-polyfill/url-polyfill.min";
// TODO: Enable ReactDevTools after fixing the portal CORS issue
// import "./ReactDevTools"
import * as ko from "knockout";
import * as TelemetryProcessor from "./Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "./Shared/Telemetry/TelemetryConstants";
import { BindingHandlersRegisterer } from "./Bindings/BindingHandlersRegisterer";
import * as Emulator from "./Platform/Emulator/Main";
import Hosted from "./Platform/Hosted/Main";
import * as Portal from "./Platform/Portal/Main";
import { AuthType } from "./AuthType";
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
import { applyExplorerBindings } from "./applyExplorerBindings";
import { initializeConfiguration, Platform } from "./ConfigContext";
import Explorer from "./Explorer/Explorer";
initializeIcons(/* optional base url */);
// TODO: Encapsulate and reuse all global variables as environment variables
window.authType = AuthType.AAD;
initializeConfiguration().then(config => {
if (config.platform === Platform.Hosted) {
try {
Hosted.initializeExplorer().then(
(explorer: Explorer) => {
applyExplorerBindings(explorer);
Hosted.configureTokenValidationDisplayPrompt(explorer);
},
(error: any) => {
try {
const uninitializedExplorer: Explorer = Hosted.getUninitializedExplorerForGuestAccess();
window.dataExplorer = uninitializedExplorer;
ko.applyBindings(uninitializedExplorer);
BindingHandlersRegisterer.registerBindingHandlers();
if (window.authType !== AuthType.AAD) {
uninitializedExplorer.isRefreshingExplorer(false);
uninitializedExplorer.displayConnectExplorerForm();
}
} catch (e) {
console.log(e);
}
console.error(error);
}
);
} catch (e) {
console.log(e);
}
} else if (config.platform === Platform.Emulator) {
window.authType = AuthType.MasterKey;
const explorer = Emulator.initializeExplorer();
applyExplorerBindings(explorer);
} else if (config.platform === Platform.Portal) {
TelemetryProcessor.trace(Action.InitializeDataExplorer, ActionModifiers.Open, {});
const explorer = Portal.initializeExplorer();
TelemetryProcessor.trace(Action.InitializeDataExplorer, ActionModifiers.IFrameReady, {});
applyExplorerBindings(explorer);
}
});

View File

@@ -1,452 +0,0 @@
// CSS Dependencies
import "bootstrap/dist/css/bootstrap.css";
import "../less/documentDB.less";
import "../less/tree.less";
import "../less/forms.less";
import "../less/menus.less";
import "../less/infobox.less";
import "../less/messagebox.less";
import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less";
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
import "./Explorer/Controls/DynamicList/DynamicListComponent.less";
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
import "../less/TableStyles/queryBuilder.less";
import "../externals/jquery.dataTables.min.css";
import "../less/TableStyles/fulldatatables.less";
import "../less/TableStyles/EntityEditor.less";
import "../less/TableStyles/CustomizeColumns.less";
import "../less/resourceTree.less";
import "../externals/jquery.typeahead.min.css";
import "../externals/jquery-ui.min.css";
import "../externals/jquery-ui.structure.min.css";
import "../externals/jquery-ui.theme.min.css";
import "./Explorer/Graph/NewVertexComponent/newVertexComponent.less";
import "./Explorer/Panes/GraphNewVertexPane.less";
import "./Explorer/Tabs/QueryTab.less";
import "./Explorer/Controls/TreeComponent/treeComponent.less";
import "./Explorer/Controls/Accordion/AccordionComponent.less";
import "./Explorer/SplashScreen/SplashScreenComponent.less";
import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
// Image Dependencies
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
import "../images/favicon.ico";
import "./Shared/appInsights";
import "babel-polyfill";
import "es6-symbol/implement";
import "webcrypto-liner/build/webcrypto-liner.shim.min";
import "./Libs/jquery";
import "bootstrap/dist/js/npm";
import "../externals/jquery.typeahead.min.js";
import "../externals/jquery-ui.min.js";
import "../externals/adal.js";
import "promise-polyfill/src/polyfill";
import "abort-controller/polyfill";
import "whatwg-fetch";
import "es6-object-assign/auto";
import "promise.prototype.finally/auto";
import "object.entries/auto";
import "./Libs/is-integer-polyfill";
import "url-polyfill/url-polyfill.min";
initializeIcons();
import * as ko from "knockout";
import * as TelemetryProcessor from "./Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "./Shared/Telemetry/TelemetryConstants";
import { BindingHandlersRegisterer } from "./Bindings/BindingHandlersRegisterer";
import * as Emulator from "./Platform/Emulator/Main";
import Hosted from "./Platform/Hosted/Main";
import * as Portal from "./Platform/Portal/Main";
import { AuthType } from "./AuthType";
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
import { applyExplorerBindings } from "./applyExplorerBindings";
import { initializeConfiguration, Platform } from "./ConfigContext";
import Explorer from "./Explorer/Explorer";
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
import errorImage from "../images/error.svg";
import copyImage from "../images/Copy.svg";
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
import refreshImg from "../images/refresh-cosmos.svg";
import arrowLeftImg from "../images/imgarrowlefticon.svg";
import { KOCommentEnd, KOCommentIfStart } from "./koComment";
// TODO: Encapsulate and reuse all global variables as environment variables
window.authType = AuthType.AAD;
const App: React.FunctionComponent = () => {
useEffect(() => {
initializeConfiguration().then(config => {
if (config.platform === Platform.Hosted) {
try {
Hosted.initializeExplorer().then(
(explorer: Explorer) => {
applyExplorerBindings(explorer);
Hosted.configureTokenValidationDisplayPrompt(explorer);
},
(error: unknown) => {
try {
const uninitializedExplorer: Explorer = Hosted.getUninitializedExplorerForGuestAccess();
window.dataExplorer = uninitializedExplorer;
ko.applyBindings(uninitializedExplorer);
BindingHandlersRegisterer.registerBindingHandlers();
if (window.authType !== AuthType.AAD) {
uninitializedExplorer.isRefreshingExplorer(false);
uninitializedExplorer.displayConnectExplorerForm();
}
} catch (e) {
console.log(e);
}
console.error(error);
}
);
} catch (e) {
console.log(e);
}
} else if (config.platform === Platform.Emulator) {
window.authType = AuthType.MasterKey;
const explorer = Emulator.initializeExplorer();
applyExplorerBindings(explorer);
} else if (config.platform === Platform.Portal) {
TelemetryProcessor.trace(Action.InitializeDataExplorer, ActionModifiers.Open, {});
const explorer = Portal.initializeExplorer();
TelemetryProcessor.trace(Action.InitializeDataExplorer, ActionModifiers.IFrameReady, {});
applyExplorerBindings(explorer);
}
});
}, []);
return (
<div className="flexContainer">
<div id="divExplorer" className="flexContainer hideOverflows" style={{ display: "none" }}>
{/* Main Command Bar - Start */}
<div data-bind="react: commandBarComponentAdapter" />
{/* Main Command Bar - End */}
{/* Share url flyout - Start */}
<div
id="shareDataAccessFlyout"
className="shareDataAccessFlyout"
data-bind="visible: shouldShowShareDialogContents"
>
<div className="shareDataAccessFlyoutContent">
<div className="urlContainer">
<span className="urlContentText">
Open this database account in a new browser tab with Cosmos DB Explorer. Or copy the read-write or read
only access urls below to share with others. For security purposes, the URLs grant time-bound access to
the account. When access expires, you can reconnect, using a valid connection string for the account.
</span>
<br />
<div
className="toggles"
data-bind="event: { keydown: onToggleKeyDown }, visible: shareAccessData().readWriteUrl != null"
tabIndex={0}
aria-label="Read-Write and Read toggle"
>
<div className="tab">
<input type="radio" className="radio" defaultValue="readwrite" />
<span
className="toggleSwitch"
role="presentation"
data-bind="click: toggleReadWrite, css:{ selectedToggle: isReadWriteToggled(), unselectedToggle: !isReadWriteToggled() }"
>
Read-Write
</span>
</div>
<div className="tab">
<input type="radio" className="radio" defaultValue="read" />
<span
className="toggleSwitch"
role="presentation"
data-bind="click: toggleRead, css:{ selectedToggle: isReadToggled(), unselectedToggle: !isReadToggled() }"
>
Read
</span>
</div>
</div>
<div className="urlSpace">
<input
id="shareUrlLink"
aria-label="Share url link"
className="shareLink"
type="text"
read-only
data-bind="value: shareAccessUrl"
/>
<span
className="urlTokenCopyInfoTooltip"
data-bind="click: copyUrlLink, event: { keypress: onCopyUrlLinkKeyPress }"
aria-label="Copy url link"
role="button"
tabIndex={0}
>
<img src={copyImage} alt="Copy link" />
<span className="urlTokenCopyTooltiptext" data-bind="text: shareUrlCopyHelperText" />
</span>
</div>
</div>
</div>
</div>
{/* Share url flyout - End */}
{/* Collections Tree and Tabs - Begin */}
<div className="resourceTreeAndTabs">
{/* Collections Tree - Start */}
<div id="resourcetree" data-test="resourceTreeId" className="resourceTree">
<div className="collectionsTreeWithSplitter">
{/* Collections Tree Expanded - Start */}
<div
id="main"
className="main"
data-bind="
visible: isLeftPaneExpanded()"
>
{/* Collections Window - - Start */}
<div id="mainslide" className="flexContainer">
{/* Collections Window Title/Command Bar - Start */}
<div className="collectiontitle">
<div className="coltitle">
<span className="titlepadcol" data-bind="text: collectionTitle" />
<div className="float-right">
<span
className="padimgcolrefresh"
data-test="refreshTree"
role="button"
data-bind="
click: onRefreshResourcesClick, clickBubble: false, event: { keypress: onRefreshDatabasesKeyPress }"
tabIndex={0}
aria-label="Refresh tree"
title="Refresh tree"
>
<img className="refreshcol" src={refreshImg} data-bind="attr: { alt: refreshTreeTitle }" />
</span>
<span
className="padimgcolrefresh1"
id="expandToggleLeftPaneButton"
role="button"
data-bind="
click: toggleLeftPaneExpanded, event: { keypress: toggleLeftPaneExpandedKeyPress }"
tabIndex={0}
aria-label="Collapse Tree"
title="Collapse Tree"
>
<img className="refreshcol1" src={arrowLeftImg} alt="Hide" />
</span>
</div>
</div>
</div>
{/* Collections Window Title/Command Bar - End */}
{!window.dataExplorer?.isAuthWithResourceToken() && (
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
)}
{window.dataExplorer?.isAuthWithResourceToken() && (
<div style={{ overflowY: "auto" }} data-bind="react:resourceTreeForResourceToken" />
)}
</div>
{/* Collections Window - End */}
</div>
{/* Collections Tree Expanded - End */}
{/* Collections Tree Collapsed - Start */}
<div
id="mini"
className="mini toggle-mini"
data-bind="visible: !isLeftPaneExpanded()
attr: { style: { width: collapsedResourceTreeWidth }}"
>
<div className="main-nav nav">
<ul className="nav">
<li
className="resourceTreeCollapse"
id="collapseToggleLeftPaneButton"
role="button"
data-bind="event: { keypress: toggleLeftPaneExpandedKeyPress }"
tabIndex={0}
aria-label="Expand Tree"
>
<span
className="leftarrowCollapsed"
data-bind="
click: toggleLeftPaneExpanded"
>
<img className="arrowCollapsed" src={arrowLeftImg} alt="Expand" />
</span>
<span
className="collectionCollapsed"
data-bind="
click: toggleLeftPaneExpanded"
>
<span
data-bind="
text: collectionTitle"
/>
</span>
</li>
</ul>
</div>
</div>
{/* Collections Tree Collapsed - End */}
</div>
{/* Splitter - Start */}
<div className="splitter ui-resizable-handle ui-resizable-e" id="h_splitter1" />
{/* Splitter - End */}
</div>
{/* Collections Tree - End */}
<div
className="connectExplorerContainer"
data-bind="visible: !isRefreshingExplorer() && tabsManager.openedTabs().length === 0"
>
<form className="connectExplorerFormContainer">
<div className="connectExplorer" data-bind="react: splashScreenAdapter" />
</form>
</div>
<div
className="tabsManagerContainer"
data-bind='component: { name: "tabs-manager", params: {data: tabsManager} }'
/>
</div>
{/* Collections Tree and Tabs - End */}
<div
className="dataExplorerErrorConsoleContainer"
role="contentinfo"
aria-label="Notification console"
id="explorerNotificationConsole"
data-bind="react: notificationConsoleComponentAdapter"
/>
</div>
{/* Explorer Connection - Encryption Token / AAD - Start */}
<div id="connectExplorer" className="connectExplorerContainer" style={{ display: "none" }}>
<div className="connectExplorerFormContainer">
<div className="connectExplorer">
<p className="connectExplorerContent">
<img src={hdeConnectImage} alt="Azure Cosmos DB" />
</p>
<p className="welcomeText">Welcome to Azure Cosmos DB</p>
<div id="connectWithAad">
<input
className="filterbtnstyle"
data-test="cosmosdb-signinBtn"
type="button"
defaultValue="Sign In"
data-bind="click: $data.signInAad"
/>
<p
className="switchConnectTypeText"
data-test="cosmosdb-connectionString"
data-bind="click: $data.onSwitchToConnectionString"
>
Connect to your account with connection string
</p>
</div>
<form id="connectWithConnectionString" data-bind="submit: renewToken" style={{ display: "none" }}>
<p className="connectExplorerContent connectStringText">Connect to your account with connection string</p>
<p className="connectExplorerContent">
<input
className="inputToken"
type="text"
required
placeholder="Please enter a connection string"
data-bind="value: tokenForRenewal"
/>
<span className="errorDetailsInfoTooltip" data-bind="visible: renewTokenError().length > 0">
<img className="errorImg" src={errorImage} alt="Error notification" />
<span className="errorDetails" data-bind="text: renewTokenError" />
</span>
</p>
<p className="connectExplorerContent">
<input className="filterbtnstyle" type="submit" value="Connect" />
</p>
<p className="switchConnectTypeText" data-bind="click: $data.signInAad">
Sign In with Azure Account
</p>
</form>
</div>
</div>
</div>
{/* Explorer Connection - Encryption Token / AAD - End */}
{/* Global loader - Start */}
<div className="splashLoaderContainer" data-bind="visible: isRefreshingExplorer">
<div className="splashLoaderContentContainer">
<p className="connectExplorerContent">
<img src={hdeConnectImage} alt="Azure Cosmos DB" />
</p>
<p className="splashLoaderTitle" id="explorerLoadingStatusTitle">
Welcome to Azure Cosmos DB
</p>
<p className="splashLoaderText" id="explorerLoadingStatusText" role="alert">
Connecting...
</p>
</div>
</div>
{/* Global loader - End */}
<div data-bind="react:uploadItemsPaneAdapter" />
<div data-bind='component: { name: "add-database-pane", params: {data: addDatabasePane} }' />
<div data-bind='component: { name: "add-collection-pane", params: { data: addCollectionPane} }' />
<div data-bind='component: { name: "delete-collection-confirmation-pane", params: { data: deleteCollectionConfirmationPane} }' />
<div data-bind='component: { name: "delete-database-confirmation-pane", params: { data: deleteDatabaseConfirmationPane} }' />
<div data-bind='component: { name: "graph-new-vertex-pane", params: { data: newVertexPane} }' />
<div data-bind='component: { name: "graph-styling-pane", params: { data: graphStylingPane} }' />
<div data-bind='component: { name: "table-add-entity-pane", params: { data: addTableEntityPane} }' />
<div data-bind='component: { name: "table-edit-entity-pane", params: { data: editTableEntityPane} }' />
<div data-bind='component: { name: "table-column-options-pane", params: { data: tableColumnOptionsPane} }' />
<div data-bind='component: { name: "table-query-select-pane", params: { data: querySelectPane} }' />
<div data-bind='component: { name: "cassandra-add-collection-pane", params: { data: cassandraAddCollectionPane} }' />
<div data-bind='component: { name: "settings-pane", params: { data: settingsPane} }' />
<div data-bind='component: { name: "upload-items-pane", params: { data: uploadItemsPane} }' />
<div data-bind='component: { name: "load-query-pane", params: { data: loadQueryPane} }' />
<div data-bind='component: { name: "execute-sproc-params-pane", params: { data: executeSprocParamsPane} }' />
<div data-bind='component: { name: "renew-adhoc-access-pane", params: { data: renewAdHocAccessPane} }' />
<div data-bind='component: { name: "save-query-pane", params: { data: saveQueryPane} }' />
<div data-bind='component: { name: "browse-queries-pane", params: { data: browseQueriesPane} }' />
<div data-bind='component: { name: "upload-file-pane", params: { data: uploadFilePane} }' />
<div data-bind='component: { name: "string-input-pane", params: { data: stringInputPane} }' />
<div data-bind='component: { name: "setup-notebooks-pane", params: { data: setupNotebooksPane} }' />
<KOCommentIfStart if="isGitHubPaneEnabled" />
<div data-bind='component: { name: "github-repos-pane", params: { data: gitHubReposPane } }' />
<KOCommentEnd />
<KOCommentIfStart if="isPublishNotebookPaneEnabled" />
<div data-bind="react: publishNotebookPaneAdapter" />
<KOCommentEnd />
<KOCommentIfStart if="isCopyNotebookPaneEnabled" />
<div data-bind="react: copyNotebookPaneAdapter" />
<KOCommentEnd />
{/* Global access token expiration dialog - Start */}
<div
id="dataAccessTokenModal"
className="dataAccessTokenModal"
style={{ display: "none" }}
data-bind="visible: shouldShowDataAccessExpiryDialog"
>
<div className="dataAccessTokenModalContent">
<p className="dataAccessTokenExpireText">Please reconnect to the account using the connection string.</p>
</div>
</div>
{/* Global access token expiration dialog - End */}
{/* Context switch prompt - Start */}
<div
id="contextSwitchPrompt"
className="dataAccessTokenModal"
style={{ display: "none" }}
data-bind="visible: shouldShowContextSwitchPrompt"
>
<div className="dataAccessTokenModalContent">
<p className="dataAccessTokenExpireText">
Please save your work before you switch! When you switch to a different Azure Cosmos DB account, current
Data Explorer tabs will be closed.
</p>
<p className="dataAccessTokenExpireText">Proceed anyway?</p>
</div>
</div>
<div data-bind="react: dialogComponentAdapter" />
<div data-bind="react: addSynapseLinkDialog" />
</div>
);
};
ReactDOM.render(<App />, document.body);

View File

@@ -1,4 +1,4 @@
import { SubscriptionType } from "../Contracts/SubscriptionType";
import { SubscriptionType } from "../Contracts/ViewModels";
export const hoursInAMonth = 730;
export class AutoscalePricing {

View File

@@ -23,14 +23,10 @@ const createServerSettings = (urlVars: { [key: string]: string }): ServerConnect
let headers: HeadersInit;
if (urlVars.hasOwnProperty(TerminalQueryParams.TerminalEndpoint)) {
body = JSON.stringify({
endpoint: urlVars[TerminalQueryParams.TerminalEndpoint],
account:"contoso-retail-mongodb",
port: "10255",
token:"" //tofill
endpoint: urlVars[TerminalQueryParams.TerminalEndpoint]
});
headers = {
[HttpHeaders.contentType]: "application/json"
//"Access-Control-Allow-Origin": "https://localhost:5001"
};
}

View File

@@ -1,5 +1,4 @@
import { DatabaseAccount } from "./Contracts/DataModels";
import { SubscriptionType } from "./Contracts/SubscriptionType";
import { DefaultAccountExperienceType } from "./DefaultAccountExperienceType";
interface UserContext {
@@ -13,7 +12,6 @@ interface UserContext {
resourceToken?: string;
defaultExperience?: DefaultAccountExperienceType;
useSDKOperations?: boolean;
subscriptionType?: SubscriptionType;
}
const userContext: Readonly<UserContext> = {} as const;

View File

@@ -7,6 +7,15 @@ export const minAutoPilotThroughput = 4000;
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 {
if (!maxThroughput) {
return false;

View File

@@ -0,0 +1,51 @@
import * as Constants from "../../src/Common/Constants";
import * as DataModels from "../../src/Contracts/DataModels";
import { OfferUtils } from "../../src/Utils/OfferUtils";
describe("OfferUtils tests", () => {
const offerV1: DataModels.Offer = {
_rid: "",
_self: "",
_ts: 0,
_etag: "",
id: "v1",
offerVersion: Constants.OfferVersions.V1,
offerType: "Standard",
offerResourceId: "",
content: null,
resource: ""
};
const offerV2: DataModels.Offer = {
_rid: "",
_self: "",
_ts: 0,
_etag: "",
id: "v1",
offerVersion: Constants.OfferVersions.V2,
offerType: "Standard",
offerResourceId: "",
content: null,
resource: ""
};
describe("isOfferV1()", () => {
it("should return true for V1", () => {
expect(OfferUtils.isOfferV1(offerV1)).toBeTruthy();
});
it("should return false for V2", () => {
expect(OfferUtils.isOfferV1(offerV2)).toBeFalsy();
});
});
describe("isNotOfferV1()", () => {
it("should return true for V2", () => {
expect(OfferUtils.isNotOfferV1(offerV2)).toBeTruthy();
});
it("should return false for V1", () => {
expect(OfferUtils.isNotOfferV1(offerV1)).toBeFalsy();
});
});
});

12
src/Utils/OfferUtils.ts Normal file
View File

@@ -0,0 +1,12 @@
import * as Constants from "../Common/Constants";
import * as DataModels from "../Contracts/DataModels";
export class OfferUtils {
public static isOfferV1(offer: DataModels.Offer): boolean {
return !offer || offer.offerVersion !== Constants.OfferVersions.V2;
}
public static isNotOfferV1(offer: DataModels.Offer): boolean {
return !OfferUtils.isOfferV1(offer);
}
}

View File

@@ -5,12 +5,12 @@ import Explorer from "./Explorer/Explorer";
export const applyExplorerBindings = (explorer: Explorer) => {
if (!!explorer) {
ko.applyBindings(explorer);
// 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
sendMessage("ready");
window.dataExplorer = explorer;
BindingHandlersRegisterer.registerBindingHandlers();
ko.applyBindings(explorer);
$("#divExplorer").show();
}
};

View File

@@ -8,5 +8,329 @@
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
</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>

View File

@@ -1,20 +0,0 @@
/* eslint-disable react/prop-types */
import React, { useEffect, useRef } from "react";
export const KOCommentIfStart: React.FunctionComponent<{ if: string }> = props => {
const el = useRef();
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(el.current as any).outerHTML = `<!-- ko if: ${props.if} -->`;
}, []);
return <div ref={el} />;
};
export const KOCommentEnd: React.FunctionComponent = () => {
const el = useRef();
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(el.current as any).outerHTML = `<!-- /ko -->`;
}, []);
return <div ref={el} />;
};

Some files were not shown because too many files have changed in this diff Show More