mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-09 12:36:42 +00:00
Compare commits
10 Commits
remove-ru-
...
users/lang
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
572d573fdd | ||
|
|
37c64c4a4d | ||
|
|
fc5ffeb7ca | ||
|
|
f39b6accb1 | ||
|
|
64601693b7 | ||
|
|
0c80c45e22 | ||
|
|
84b6075ee8 | ||
|
|
d880723be9 | ||
|
|
4ce9dcc024 | ||
|
|
addcfedd5e |
@@ -396,19 +396,5 @@ src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx
|
|||||||
src/GalleryViewer/Cards/GalleryCardComponent.tsx
|
src/GalleryViewer/Cards/GalleryCardComponent.tsx
|
||||||
src/GalleryViewer/GalleryViewer.tsx
|
src/GalleryViewer/GalleryViewer.tsx
|
||||||
src/GalleryViewer/GalleryViewerComponent.tsx
|
src/GalleryViewer/GalleryViewerComponent.tsx
|
||||||
cypress/integration/dataexplorer/CASSANDRA/addCollection.spec.ts
|
|
||||||
cypress/integration/dataexplorer/GRAPH/addCollection.spec.ts
|
|
||||||
cypress/integration/dataexplorer/ci-tests/addCollectionPane.spec.ts
|
|
||||||
cypress/integration/dataexplorer/ci-tests/createDatabase.spec.ts
|
|
||||||
cypress/integration/dataexplorer/ci-tests/deleteCollection.spec.ts
|
|
||||||
cypress/integration/dataexplorer/ci-tests/deleteDatabase.spec.ts
|
|
||||||
cypress/integration/dataexplorer/MONGO/addCollection.spec.ts
|
|
||||||
cypress/integration/dataexplorer/MONGO/addCollectionAutopilot.spec.ts
|
|
||||||
cypress/integration/dataexplorer/MONGO/addCollectionExistingDatabase.spec.ts
|
|
||||||
cypress/integration/dataexplorer/MONGO/provisionDatabaseThroughput.spec.ts
|
|
||||||
cypress/integration/dataexplorer/SQL/addCollection.spec.ts
|
|
||||||
cypress/integration/dataexplorer/TABLE/addCollection.spec.ts
|
|
||||||
cypress/integration/notebook/newNotebook.spec.ts
|
|
||||||
cypress/integration/notebook/resourceTree.spec.ts
|
|
||||||
__mocks__/monaco-editor.ts
|
__mocks__/monaco-editor.ts
|
||||||
src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx
|
src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx
|
||||||
33
.github/workflows/ci.yml
vendored
33
.github/workflows/ci.yml
vendored
@@ -79,32 +79,31 @@ jobs:
|
|||||||
name: dist
|
name: dist
|
||||||
path: dist/
|
path: dist/
|
||||||
endtoendemulator:
|
endtoendemulator:
|
||||||
name: "End To End Tests | Emulator | SQL"
|
name: "End To End Emulator Tests"
|
||||||
needs: [lint, format, compile, unittest]
|
needs: [lint, format, compile, unittest]
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: southpolesteve/cosmos-emulator-github-action@v1
|
|
||||||
- name: Use Node.js 12.x
|
- name: Use Node.js 12.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: 12.x
|
||||||
- name: Restore Cypress Binary Cache
|
- uses: southpolesteve/cosmos-emulator-github-action@v1
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/.cache/Cypress
|
|
||||||
key: ${{ runner.os }}-cypress-binary-cache
|
|
||||||
- name: End to End Tests
|
- name: End to End Tests
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
npm start &
|
npm start &
|
||||||
npm ci --prefix ./cypress
|
npm run wait-for-server
|
||||||
npm run test:ci --prefix ./cypress -- --spec ./integration/dataexplorer/ci-tests/createDatabase.spec.ts
|
npx jest -c ./jest.config.e2e.js --detectOpenHandles sql
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
EMULATOR_ENDPOINT: https://0.0.0.0:8081/
|
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/explorer.html?platform=Emulator"
|
||||||
|
PLATFORM: "Emulator"
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: screenshots
|
||||||
|
path: failed-*
|
||||||
accessibility:
|
accessibility:
|
||||||
name: "Accessibility | Hosted"
|
name: "Accessibility | Hosted"
|
||||||
needs: [lint, format, compile, unittest]
|
needs: [lint, format, compile, unittest]
|
||||||
@@ -123,13 +122,13 @@ jobs:
|
|||||||
sudo sysctl -p
|
sudo sysctl -p
|
||||||
npm ci
|
npm ci
|
||||||
npm start &
|
npm start &
|
||||||
npx wait-on -i 5000 https-get://0.0.0.0:1234/
|
npx wait-on -i 5000 https-get://0.0.0.0:1234/
|
||||||
node utils/accesibilityCheck.js
|
node utils/accesibilityCheck.js
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
endtoendpuppeteer:
|
endtoendhosted:
|
||||||
name: "End to end puppeteer tests"
|
name: "End to End Hosted Tests"
|
||||||
needs: [lint, format, compile, unittest]
|
needs: [lint, format, compile, unittest]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -138,7 +137,7 @@ jobs:
|
|||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: 12.x
|
||||||
- name: End to End Puppeteer Tests
|
- name: End to End Hosted Tests
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
npm start &
|
npm start &
|
||||||
@@ -159,7 +158,7 @@ jobs:
|
|||||||
nuget:
|
nuget:
|
||||||
name: Publish Nuget
|
name: Publish Nuget
|
||||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendpuppeteer]
|
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||||
@@ -183,7 +182,7 @@ jobs:
|
|||||||
nugetmpac:
|
nugetmpac:
|
||||||
name: Publish Nuget MPAC
|
name: Publish Nuget MPAC
|
||||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendpuppeteer]
|
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,9 +9,6 @@ pkg/DataExplorer/*
|
|||||||
test/out/*
|
test/out/*
|
||||||
workers/**/*.js
|
workers/**/*.js
|
||||||
*.trx
|
*.trx
|
||||||
cypress/videos
|
|
||||||
cypress/screenshots
|
|
||||||
cypress/fixtures
|
|
||||||
notebookapp/*
|
notebookapp/*
|
||||||
Contracts/*
|
Contracts/*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -76,17 +76,7 @@ Unit tests are located adjacent to the code under test and run with [Jest](https
|
|||||||
|
|
||||||
#### End to End CI Tests
|
#### End to End CI Tests
|
||||||
|
|
||||||
[Cypress](https://www.cypress.io/) is used for end to end tests and are contained in `cypress/`. Currently, it operates as sub project with its own typescript config and dependencies. It also only operates against the emulator. To run cypress tests:
|
Jest and Puppeteer are used for end to end browser based tests and are contained in `test/`. To run these tests locally:
|
||||||
|
|
||||||
1. Ensure the emulator is running
|
|
||||||
2. Start cosmos explorer in emulator mode: `PLATFORM=Emulator npm run watch`
|
|
||||||
3. Move into `cypress/` folder: `cd cypress`
|
|
||||||
4. Install dependencies: `npm install`
|
|
||||||
5. Run cypress headless(`npm run test`) or in interactive mode(`npm run test:debug`)
|
|
||||||
|
|
||||||
#### End to End Production Tests
|
|
||||||
|
|
||||||
Jest and Puppeteer are used for end to end production runners and are contained in `test/`. To run these tests locally:
|
|
||||||
|
|
||||||
1. Copy .env.example to .env
|
1. Copy .env.example to .env
|
||||||
2. Update the values in .env including your local data explorer endpoint (ask a teammate/codeowner for help with .env values)
|
2. Update the values in .env including your local data explorer endpoint (ask a teammate/codeowner for help with .env values)
|
||||||
|
|||||||
4
cypress/.gitignore
vendored
4
cypress/.gitignore
vendored
@@ -1,4 +0,0 @@
|
|||||||
cypress.env.json
|
|
||||||
cypress/report
|
|
||||||
cypress/screenshots
|
|
||||||
cypress/videos
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
// Cleans up old databases from previous test runs
|
|
||||||
const { CosmosClient } = require("@azure/cosmos");
|
|
||||||
|
|
||||||
// TODO: Add support for other API connection strings
|
|
||||||
const mongoRegex = RegExp("mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com");
|
|
||||||
|
|
||||||
async function cleanup() {
|
|
||||||
const connectionString = process.env.CYPRESS_CONNECTION_STRING;
|
|
||||||
if (!connectionString) {
|
|
||||||
throw new Error("Connection string not provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
let client;
|
|
||||||
switch (true) {
|
|
||||||
case connectionString.includes("mongodb://"): {
|
|
||||||
const [, key, accountName] = connectionString.match(mongoRegex);
|
|
||||||
client = new CosmosClient({
|
|
||||||
key,
|
|
||||||
endpoint: `https://${accountName}.documents.azure.com:443/`
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// TODO: Add support for other API connection strings
|
|
||||||
default:
|
|
||||||
client = new CosmosClient(connectionString);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await client.databases.readAll().fetchAll();
|
|
||||||
return Promise.all(
|
|
||||||
response.resources.map(async db => {
|
|
||||||
const dbTimestamp = new Date(db._ts * 1000);
|
|
||||||
const twentyMinutesAgo = new Date(Date.now() - 1000 * 60 * 20);
|
|
||||||
if (dbTimestamp < twentyMinutesAgo) {
|
|
||||||
await client.database(db.id).delete();
|
|
||||||
console.log(`DELETED: ${db.id} | Timestamp: ${dbTimestamp}`);
|
|
||||||
} else {
|
|
||||||
console.log(`SKIPPED: ${db.id} | Timestamp: ${dbTimestamp}`);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup()
|
|
||||||
.then(() => {
|
|
||||||
process.exit(0);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"integrationFolder": "./integration",
|
|
||||||
"pluginsFile": false,
|
|
||||||
"fixturesFolder": false,
|
|
||||||
"supportFile": "./support/index.js",
|
|
||||||
"defaultCommandTimeout": 90000,
|
|
||||||
"chromeWebSecurity": false,
|
|
||||||
"reporter": "mochawesome",
|
|
||||||
"reporterOptions": {
|
|
||||||
"reportDir": "cypress/report",
|
|
||||||
"json": true,
|
|
||||||
"overwrite": false,
|
|
||||||
"html": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
// 1. Click on "New Container" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Cassandra API Test - createDatabase", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString(connectionString.constants.cassandra);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new table in Cassandra API", () => {
|
|
||||||
const keyspaceId = `KeyspaceId${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const tableId = `TableId112`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Table"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[id="keyspace-id"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.type(keyspaceId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[class="textfontclr"]')
|
|
||||||
.type(tableId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('data-test="addCollection-createCollection"')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", tableId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
// 1. Click on "New Graph" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Graph API Test", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString(connectionString.constants.graph);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new graph in Graph API", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const graphId = `TestGraph${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const partitionKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Graph"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(graphId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(partitionKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId)
|
|
||||||
.click()
|
|
||||||
.should("contain", graphId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
// 1. Click on "New Container" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// // 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Mongo API Test - createDatabase", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new collection in Mongo API", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Collection"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(sharedKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find("#submitBtnAddCollection")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId)
|
|
||||||
.click()
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
// 1. Click on "New Container" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Mongo API Test", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
});
|
|
||||||
|
|
||||||
it.skip("Create a new collection in Mongo API - Autopilot", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Collection"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="throughputModeContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.and(input => {
|
|
||||||
expect(input.get(0).textContent, "first item").contains("Autopilot (preview)");
|
|
||||||
expect(input.get(1).textContent, "second item").contains("Manual");
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[id="newContainer-databaseThroughput-autoPilotRadio"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('select[name="autoPilotTiers"]')
|
|
||||||
// .eq(1).should('contain', '4,000 RU/s');
|
|
||||||
// // .select('4,000 RU/s').should('have.value', '1');
|
|
||||||
|
|
||||||
.find('option[value="2"]')
|
|
||||||
.then($element => $element.get(1).setAttribute("selected", "selected"));
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(sharedKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId)
|
|
||||||
.click()
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Mongo API Test", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
});
|
|
||||||
|
|
||||||
it.skip("Create a new collection in existing database in Mongo API", () => {
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('span[class="nodeLabel"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.then($span => {
|
|
||||||
const dbId1 = $span.text();
|
|
||||||
cy.log("DBBB", dbId1);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Collection"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-existingDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-existingDatabase"]')
|
|
||||||
.type(dbId1);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(sharedKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.click()
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context.skip("Mongo API Test", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new collection in Mongo API - Provision database throughput", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Collection"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find(".createNewDatabaseOrUseExisting")
|
|
||||||
.should("have.length", 2)
|
|
||||||
.and(input => {
|
|
||||||
expect(input.get(0).textContent, "first item").contains("Create new");
|
|
||||||
expect(input.get(1).textContent, "second item").contains("Use existing");
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(sharedKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId)
|
|
||||||
.click()
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new collection - without provision database throughput", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionIdTitle = `Add Collection`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Collection"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.uncheck();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[id="tab2"]')
|
|
||||||
.check({ force: true });
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(sharedKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId)
|
|
||||||
.click()
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new collection - without provision database throughput Fixed Storage Capacity", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Collection"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.uncheck();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[id="tab1"]')
|
|
||||||
.check({ force: true });
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId)
|
|
||||||
.click()
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
// 1. Click on "New Container" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("SQL API Test", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new container in SQL API", () => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Container"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createNewDatabase"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
|
|
||||||
.check();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-newDatabaseId"]')
|
|
||||||
.type(dbId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-partitionKeyValue"]')
|
|
||||||
.type(sharedKey);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find("#submitBtnAddCollection")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", dbId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
// 1. Click on "New Container" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
const connectionString = require("../../../utilities/connectionString");
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Table API Test", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
connectionString.loginUsingConnectionString(connectionString.constants.table);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new table in Table API", () => {
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="commandBarContainer"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('button[data-test="New Table"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[class="contextual-pane-in"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.find('span[id="containerTitle"]');
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-collectionId"]')
|
|
||||||
.type(collectionId);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="databaseThroughputValue"]')
|
|
||||||
.should("have.value", "400");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('input[data-test="addCollection-createCollection"]')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.wait(10000);
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find('div[data-test="resourceTreeId"]')
|
|
||||||
.should("exist")
|
|
||||||
.find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
.should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// 1. Click on "New Container" on the command bar.
|
|
||||||
// 2. Pane with the title "Add Container" should appear on the right side of the screen
|
|
||||||
// 3. It includes an input box for the database Id.
|
|
||||||
// 4. It includes a checkbox called "Create now".
|
|
||||||
// 5. When the checkbox is marked, enter new database id.
|
|
||||||
// 3. Create a database WITH "Provision throughput" checked.
|
|
||||||
// 4. Enter minimum throughput value of 400.
|
|
||||||
// 5. Enter container id to the container id text box.
|
|
||||||
// 6. Enter partition key to the partition key text box.
|
|
||||||
// 7. Click "OK" to create a new container.
|
|
||||||
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Emulator - createDatabase", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("http://localhost:1234/explorer.html");
|
|
||||||
});
|
|
||||||
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionIdTitle = `Add Collection`;
|
|
||||||
const partitionKey = `PartitionKey${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
|
|
||||||
it("Create a new collection", () => {
|
|
||||||
cy.contains("New Container").click();
|
|
||||||
|
|
||||||
// cy.contains(collectionIdTitle);
|
|
||||||
|
|
||||||
cy.get(".createNewDatabaseOrUseExisting")
|
|
||||||
.should("have.length", 2)
|
|
||||||
.and(input => {
|
|
||||||
expect(input.get(0).textContent, "first item").contains("Create new");
|
|
||||||
expect(input.get(1).textContent, "second item").contains("Use existing");
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get('input[data-test="addCollection-createNewDatabase"]').check();
|
|
||||||
|
|
||||||
cy.get('input[data-test="addCollection-newDatabaseId"]').type(dbId);
|
|
||||||
|
|
||||||
cy.get('input[data-test="addCollection-collectionId"]').type(collectionId);
|
|
||||||
|
|
||||||
cy.get('input[data-test="databaseThroughputValue"]').should("have.value", "400");
|
|
||||||
|
|
||||||
cy.get('input[data-test="addCollection-partitionKeyValue"]').type(partitionKey);
|
|
||||||
|
|
||||||
cy.get('input[data-test="addCollection-createCollection"]').click();
|
|
||||||
|
|
||||||
cy.get('div[data-test="resourceTreeId"]').should("exist");
|
|
||||||
|
|
||||||
cy.get('div[data-test="resourceTree-collectionsTree"]').should("contain", dbId);
|
|
||||||
|
|
||||||
cy.get('div[data-test="databaseList"]').should("contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
// 1. Click on "New Database" on the command bar
|
|
||||||
// 2. a Pane with the title "Add Database" should appear on the right side of the screen
|
|
||||||
// i. It includes an input box for the database Id.
|
|
||||||
// ii. It includes a checkbox called "Provision throughput".
|
|
||||||
// iii. Whe the checkbox is marked, a new input with a throughput control let's you customize RU at the database level
|
|
||||||
// 3. Create a database WITHOUT "Provision throughput" checked.
|
|
||||||
// 4. It should appear in the Data Explorer list.
|
|
||||||
// 5. Repeat steps 1-3 but create a database WITH "Provision throughput" with the default RU value.
|
|
||||||
// 6. It should appear in the Data Explorer list.
|
|
||||||
// 7. If expanded, it should have the list item called "Scale", that once clicked, it should show the "Scale" tab.
|
|
||||||
// 8. Inside that tab, a throughput control will let you change the RU value within the permited range.
|
|
||||||
// 9. If you change the value, it should enable the "Save" button.
|
|
||||||
// 10. Click "Save" and verify that the process completes without error.
|
|
||||||
// 11. Close the tab and reopen it and verify that the input contains the last saved value.%
|
|
||||||
|
|
||||||
const crypto = require("crypto");
|
|
||||||
const client = require("../../../utilities/cosmosClient");
|
|
||||||
const randomString = crypto.randomBytes(2).toString("hex");
|
|
||||||
const databaseId = `TestDB-${randomString}`;
|
|
||||||
const collectionId = `TestColl-${randomString}`;
|
|
||||||
|
|
||||||
context("Emulator - Create database -> container -> item", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
const { resources } = await client.databases.readAll().fetchAll();
|
|
||||||
for (const database of resources) {
|
|
||||||
await client.database(database.id).delete();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("creates a new database", () => {
|
|
||||||
cy.visit("https://0.0.0.0:1234/explorer.html?platform=Emulator");
|
|
||||||
cy.contains("New Container").click();
|
|
||||||
cy.get("[data-test=addCollection-newDatabaseId]").click();
|
|
||||||
cy.get("[data-test=addCollection-newDatabaseId]").type(databaseId);
|
|
||||||
cy.get("[data-test=addCollection-collectionId]").click();
|
|
||||||
cy.get("[data-test=addCollection-collectionId]").type(collectionId);
|
|
||||||
cy.get("[data-test=addCollection-partitionKeyValue]").click();
|
|
||||||
cy.get("[data-test=addCollection-partitionKeyValue]").type("/pk");
|
|
||||||
cy.get('input[name="createCollection"]').click();
|
|
||||||
cy.get(".dataResourceTree").should("contain", databaseId);
|
|
||||||
cy.get(".dataResourceTree")
|
|
||||||
.contains(databaseId)
|
|
||||||
.click();
|
|
||||||
cy.get(".dataResourceTree").should("contain", collectionId);
|
|
||||||
cy.get(".dataResourceTree")
|
|
||||||
.contains(collectionId)
|
|
||||||
.click();
|
|
||||||
cy.get(".dataResourceTree")
|
|
||||||
.contains("Items")
|
|
||||||
.click();
|
|
||||||
cy.get(".dataResourceTree")
|
|
||||||
.contains("Items")
|
|
||||||
.click();
|
|
||||||
cy.wait(1000); // React rendering inside KO causes some weird async rendering that makes this test flaky without waiting
|
|
||||||
cy.get(".commandBarContainer")
|
|
||||||
.contains("New Item")
|
|
||||||
.click();
|
|
||||||
cy.wait(1000); // React rendering inside KO causes some weird async rendering that makes this test flaky without waiting
|
|
||||||
cy.get(".commandBarContainer")
|
|
||||||
.contains("Save")
|
|
||||||
.click();
|
|
||||||
cy.wait(1000); // React rendering inside KO causes some weird async rendering that makes this test flaky without waiting
|
|
||||||
cy.get(".documentsGridHeaderContainer").should("contain", "replace_with_new_document_id");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
// 1. Click last database in the resource tree
|
|
||||||
// 2. Click the last collection within the database
|
|
||||||
// 3. Select the context menu within the collection
|
|
||||||
// 4. Select "Delete Container" option in the dropdown
|
|
||||||
// 5. On Selection, Delete Container pane opens on the right side
|
|
||||||
// 6. Enter the same collection id that is to be deleted and click ok
|
|
||||||
// 7. Now, the resource tree refreshes, the deleted collection should not appear under the database
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Emulator - deleteCollection", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("http://localhost:1234/explorer.html");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Delete a collection", () => {
|
|
||||||
cy.get(".databaseId")
|
|
||||||
.last()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get(".collectionList")
|
|
||||||
.last()
|
|
||||||
.then($id => {
|
|
||||||
const collectionId = $id.text();
|
|
||||||
|
|
||||||
cy.get('span[data-test="collectionEllipsisMenu"]').should("exist");
|
|
||||||
|
|
||||||
cy.get('span[data-test="collectionEllipsisMenu"]')
|
|
||||||
.invoke("show")
|
|
||||||
.last()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get('div[data-test="collectionContextMenu"]')
|
|
||||||
.contains("Delete Container")
|
|
||||||
.click({ force: true });
|
|
||||||
|
|
||||||
cy.get('input[data-test="confirmCollectionId"]').type(collectionId.trim());
|
|
||||||
|
|
||||||
cy.get('input[data-test="deleteCollection"]').click();
|
|
||||||
|
|
||||||
cy.get('div[data-test="databaseList"]').should("not.contain", collectionId);
|
|
||||||
|
|
||||||
cy.get('div[data-test="databaseMenu"]').should("not.contain", collectionId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
// 1. Click last database in the resource tree
|
|
||||||
// 2. Select the context menu within the database
|
|
||||||
// 4. Select "Delete Database" option in the dropdown
|
|
||||||
// 5. On Selection, Delete Database pane opens on the right side
|
|
||||||
// 6. Enter the same database id that is to be deleted and click ok
|
|
||||||
// 7. Now, the resource tree refreshes, the deleted database should not appear in the resource tree
|
|
||||||
|
|
||||||
let crypt = require("crypto");
|
|
||||||
|
|
||||||
context("Emulator - deleteDatabase", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
|
||||||
let db_rid = "";
|
|
||||||
const date = new Date().toUTCString();
|
|
||||||
let authToken = "";
|
|
||||||
cy.visit("http://localhost:1234/explorer.html");
|
|
||||||
|
|
||||||
// Creating auth token for collection creation
|
|
||||||
cy.request({
|
|
||||||
method: "GET",
|
|
||||||
url: "https://localhost:8081/_explorer/authorization/post/dbs/",
|
|
||||||
headers: {
|
|
||||||
"x-ms-date": date,
|
|
||||||
authorization: "-"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
authToken = response.body.Token; // Getting auth token for collection creation
|
|
||||||
return new Cypress.Promise((resolve, reject) => {
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
cy.request({
|
|
||||||
method: "POST",
|
|
||||||
url: "https://localhost:8081/dbs",
|
|
||||||
headers: {
|
|
||||||
"x-ms-date": date,
|
|
||||||
authorization: authToken,
|
|
||||||
"x-ms-version": "2018-12-31"
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
id: dbId
|
|
||||||
}
|
|
||||||
}).then(response => {
|
|
||||||
cy.log("Response", response);
|
|
||||||
db_rid = response.body._rid;
|
|
||||||
return new Cypress.Promise((resolve, reject) => {
|
|
||||||
cy.log("Rid", db_rid);
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Delete a database", () => {
|
|
||||||
cy.get('span[data-test="refreshTree"]').click();
|
|
||||||
|
|
||||||
cy.get(".databaseId")
|
|
||||||
.last()
|
|
||||||
.then($id => {
|
|
||||||
const dbId = $id.text();
|
|
||||||
|
|
||||||
cy.get('span[data-test="databaseEllipsisMenu"]').should("exist");
|
|
||||||
|
|
||||||
cy.get('span[data-test="databaseEllipsisMenu"]')
|
|
||||||
.invoke("show")
|
|
||||||
.last()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get('div[data-test="databaseContextMenu"]')
|
|
||||||
.contains("Delete Database")
|
|
||||||
.click({ force: true });
|
|
||||||
|
|
||||||
cy.get('input[data-test="confirmDatabaseId"]').type(dbId.trim());
|
|
||||||
|
|
||||||
cy.get('input[data-test="deleteDatabase"]').click();
|
|
||||||
|
|
||||||
cy.get('div[data-test="databaseList"]').should("not.contain", dbId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# Notebook end-to-end tests
|
|
||||||
This describes how to run the tests locally
|
|
||||||
|
|
||||||
## Stand up a local notebook container instance:
|
|
||||||
Instructions on how to build and run the container [here](https://microsoft.sharepoint.com/teams/DocDB/_layouts/OneNote.aspx?id=%2Fteams%2FDocDB%2FSiteAssets%2FDocDB%20Team%20Notebook&wd=target%28Tools%20_%20SDK%2FPortal%2FDevelopment.one%7CF800BE8E-1E31-48FE-90D7-EF698EF88112%2FHow%20to%20build%20notebook%20service%7C4BAA153B-422C-41E2-B997-F3FCE02CD743%2F%29)
|
|
||||||
|
|
||||||
## Run a local data explorer
|
|
||||||
Instructions are in [`DataExplorer/README.md`](https://msdata.visualstudio.com/CosmosDB/_git/cosmosdb-dataexplorer?path=%2FProduct%2FPortal%2FDataExplorer%2FREADME.md&_a=preview).
|
|
||||||
|
|
||||||
Make sure you can run Data Explorer locally from the web browser.
|
|
||||||
|
|
||||||
## Run cypress tests
|
|
||||||
1. Edit the URL for your DataExplorer in the `.spec.ts` file
|
|
||||||
2. Run the test:
|
|
||||||
```bash
|
|
||||||
cd DataExplorer/cypress
|
|
||||||
npm i
|
|
||||||
npm t -- --spec 'integration/notebook/newNotebook.spec.ts'
|
|
||||||
```
|
|
||||||
|
|
||||||
To run in Debug mode:
|
|
||||||
```
|
|
||||||
npm run test:debug
|
|
||||||
```
|
|
||||||
This opens Cypress UI
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
* The tests are recorded in the `videos` folder.
|
|
||||||
* Cypress does not support hover: workarounds [here](https://docs.cypress.io/api/commands/hover.html#Workarounds).
|
|
||||||
|
|
||||||
|
|
||||||
## References
|
|
||||||
* [Cypress API](https://docs.cypress.io/api/api/table-of-contents.html)
|
|
||||||
* [Cypress cookbook](https://docs.cypress.io/faq/questions/using-cypress-faq.html#How-do-I-get-an-element%E2%80%99s-text-contents)
|
|
||||||
* [Cypress best practices](https://docs.cypress.io/guides/references/best-practices.html#Selecting-Elements)
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
// THIS ADDS A NEW NOTEBOOK TO YOUR NOTEBOOKS
|
|
||||||
context("New Notebook smoke test", () => {
|
|
||||||
const timeout = 15000; // in ms
|
|
||||||
const explorerUrl =
|
|
||||||
"https://localhost:1234/explorer.html?feature.notebookserverurl=https%3A%2F%2Flocalhost%3A10001%2F12345%2Fnotebook&feature.notebookServerToken=token&feature.enablenotebooks=true";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for UI to be ready
|
|
||||||
*/
|
|
||||||
const waitForReady = () => {
|
|
||||||
cy.get(".splashScreenContainer", { timeout }).should("be.visible");
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit(explorerUrl);
|
|
||||||
waitForReady();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a new notebook and run some code", () => {
|
|
||||||
// Create new notebook
|
|
||||||
cy.contains("New Notebook").click();
|
|
||||||
|
|
||||||
// Check tab name
|
|
||||||
cy.get("li.tabList .tabNavText").should($span => {
|
|
||||||
const text = $span.text();
|
|
||||||
expect(text).to.match(/^Untitled.*\.ipynb$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for python3 | idle status
|
|
||||||
cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should($p => {
|
|
||||||
const text = $p.text();
|
|
||||||
expect(text).to.match(/^python3.*idle$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Click on a cell
|
|
||||||
cy.get(".cell-container")
|
|
||||||
.as("cellContainer")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Type in some code
|
|
||||||
cy.get("@cellContainer").type("2+4");
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
cy.get('[data-test="Run"]')
|
|
||||||
.first()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Verify results
|
|
||||||
cy.get("@cellContainer").within(() => {
|
|
||||||
cy.get("pre code span").should("contain", "6");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Restart kernel
|
|
||||||
cy.get('[data-test="Run"] button')
|
|
||||||
.eq(-1)
|
|
||||||
.click();
|
|
||||||
cy.get("li")
|
|
||||||
.contains("Restart Kernel")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Wait for python3 | restarting status
|
|
||||||
cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should($p => {
|
|
||||||
const text = $p.text();
|
|
||||||
expect(text).to.match(/^python3.*restarting$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for python3 | idle status
|
|
||||||
cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should($p => {
|
|
||||||
const text = $p.text();
|
|
||||||
expect(text).to.match(/^python3.*idle$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Click on a cell
|
|
||||||
cy.get(".cell-container")
|
|
||||||
.as("cellContainer")
|
|
||||||
.find(".input")
|
|
||||||
.as("codeInput")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Type in some code
|
|
||||||
cy.get("@codeInput").type("{backspace}{backspace}{backspace}4+5");
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
cy.get('[data-test="Run"]')
|
|
||||||
.first()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Verify results
|
|
||||||
cy.get("@cellContainer").within(() => {
|
|
||||||
cy.get("pre code span").should("contain", "9");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
context("Resource tree notebook file manipulation", () => {
|
|
||||||
const timeout = 15000; // in ms
|
|
||||||
const explorerUrl =
|
|
||||||
"https://localhost:1234/explorer.html?feature.notebookserverurl=https%3A%2F%2Flocalhost%3A10001%2F12345%2Fnotebook&feature.notebookServerToken=token&feature.enablenotebooks=true";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for UI to be ready
|
|
||||||
*/
|
|
||||||
const waitForReady = () => {
|
|
||||||
cy.get(".splashScreenContainer", { timeout }).should("be.visible");
|
|
||||||
};
|
|
||||||
|
|
||||||
const clickContextMenuAndSelectOption = (nodeLabel, option) => {
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${nodeLabel}"]`)
|
|
||||||
.find("button.treeMenuEllipsis")
|
|
||||||
.click();
|
|
||||||
cy.get('[data-test="treeComponentMenuItemContainer"]')
|
|
||||||
.contains(option)
|
|
||||||
.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
const createFolder = folder => {
|
|
||||||
clickContextMenuAndSelectOption("My Notebooks/", "New Directory");
|
|
||||||
|
|
||||||
cy.get("#stringInputPane").within(() => {
|
|
||||||
cy.get('input[name="collectionIdConfirmation"]').type(folder);
|
|
||||||
cy.get("form").submit();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteItem = nodeName => {
|
|
||||||
clickContextMenuAndSelectOption(`${nodeName}`, "Delete");
|
|
||||||
cy.get(".ms-Dialog-main")
|
|
||||||
.contains("Delete")
|
|
||||||
.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit(explorerUrl);
|
|
||||||
waitForReady();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create and remove a directory", () => {
|
|
||||||
const folder = "e2etest_folder1";
|
|
||||||
createFolder(folder);
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).should("exist");
|
|
||||||
deleteItem(`${folder}/`);
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).should("not.exist");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create and rename a directory", () => {
|
|
||||||
const folder = "e2etest_folder2";
|
|
||||||
const renamedFolder = "e2etest_folder2_renamed";
|
|
||||||
createFolder(folder);
|
|
||||||
|
|
||||||
// Rename
|
|
||||||
clickContextMenuAndSelectOption(`${folder}/`, "Rename");
|
|
||||||
cy.get("#stringInputPane").within(() => {
|
|
||||||
cy.get('input[name="collectionIdConfirmation"]')
|
|
||||||
.clear()
|
|
||||||
.type(renamedFolder);
|
|
||||||
cy.get("form").submit();
|
|
||||||
});
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${renamedFolder}/"]`).should("exist");
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).should("not.exist");
|
|
||||||
|
|
||||||
deleteItem(`${renamedFolder}/`);
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${renamedFolder}/"]`).should("not.exist");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create a notebook inside a directory", () => {
|
|
||||||
const folder = "e2etest_folder3";
|
|
||||||
const newNotebookName = "Untitled.ipynb";
|
|
||||||
createFolder(folder);
|
|
||||||
clickContextMenuAndSelectOption(`${folder}/`, "New Notebook");
|
|
||||||
|
|
||||||
// Verify tab is open
|
|
||||||
cy.get(".tabList")
|
|
||||||
.contains(newNotebookName)
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
// Close tab
|
|
||||||
cy.get(`.tabList[title="notebooks/${folder}/${newNotebookName}"]`)
|
|
||||||
.find(".cancelButton")
|
|
||||||
.click();
|
|
||||||
// When running from command line, closing the tab is too fast
|
|
||||||
cy.get("body").then($body => {
|
|
||||||
if ($body.find(".ms-Dialog-main").length) {
|
|
||||||
// For some reason, this does not work
|
|
||||||
// cy.get(".ms-Dialog-main").contains("Close").click();
|
|
||||||
cy.get(".ms-Dialog-main .ms-Button--primary").click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Expand folder node
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).click();
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("exist");
|
|
||||||
|
|
||||||
// Delete notebook
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`)
|
|
||||||
.find("button.treeMenuEllipsis")
|
|
||||||
.click();
|
|
||||||
cy.get('[data-test="treeComponentMenuItemContainer"]')
|
|
||||||
.contains("Delete")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Confirm
|
|
||||||
cy.get(".ms-Dialog-main")
|
|
||||||
.contains("Delete")
|
|
||||||
.click();
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("not.exist");
|
|
||||||
|
|
||||||
deleteItem(`${folder}/`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Create and rename a notebook inside a directory", () => {
|
|
||||||
const folder = "e2etest_folder4";
|
|
||||||
const newNotebookName = "Untitled.ipynb";
|
|
||||||
const renamedNotebookName = "mynotebook.ipynb";
|
|
||||||
createFolder(folder);
|
|
||||||
clickContextMenuAndSelectOption(`${folder}/`, "New Notebook");
|
|
||||||
|
|
||||||
// Close tab
|
|
||||||
cy.get(`.tabList[title="notebooks/${folder}/${newNotebookName}"]`)
|
|
||||||
.find(".cancelButton")
|
|
||||||
.click();
|
|
||||||
cy.get("body").then($body => {
|
|
||||||
if ($body.find(".ms-Dialog-main").length) {
|
|
||||||
// For some reason, this does not work
|
|
||||||
// cy.get(".ms-Dialog-main").contains("Close").click();
|
|
||||||
cy.get(".ms-Dialog-main .ms-Button--primary").click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Expand folder node
|
|
||||||
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).click();
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("exist");
|
|
||||||
|
|
||||||
// Rename notebook
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`)
|
|
||||||
.find("button.treeMenuEllipsis")
|
|
||||||
.click();
|
|
||||||
cy.get('[data-test="treeComponentMenuItemContainer"]')
|
|
||||||
.contains("Rename")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get("#stringInputPane").within(() => {
|
|
||||||
cy.get('input[name="collectionIdConfirmation"]')
|
|
||||||
.clear()
|
|
||||||
.type(renamedNotebookName);
|
|
||||||
cy.get("form").submit();
|
|
||||||
});
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("not.exist");
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${renamedNotebookName}"]`).should("exist");
|
|
||||||
|
|
||||||
// Delete notebook
|
|
||||||
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${renamedNotebookName}"]`)
|
|
||||||
.find("button.treeMenuEllipsis")
|
|
||||||
.click();
|
|
||||||
cy.get('[data-test="treeComponentMenuItemContainer"]')
|
|
||||||
.contains("Delete")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Confirm
|
|
||||||
cy.get(".ms-Dialog-main")
|
|
||||||
.contains("Delete")
|
|
||||||
.click();
|
|
||||||
// Give it time to settle
|
|
||||||
cy.wait(1000);
|
|
||||||
deleteItem(`${folder}/`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
3066
cypress/package-lock.json
generated
3066
cypress/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "cosmos-explorer-cypress",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "cypress run",
|
|
||||||
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
|
|
||||||
"test:sql": "cypress run --browser chrome --spec \"./integration/dataexplorer/SQL/*\"",
|
|
||||||
"test:ci": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/ https-get://0.0.0.0:8081/_explorer/index.html && cypress run --browser edge --headless",
|
|
||||||
"test:debug": "cypress open"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"cypress": "^4.8.0",
|
|
||||||
"mocha": "^7.0.1",
|
|
||||||
"mochawesome": "^4.1.0",
|
|
||||||
"mochawesome-merge": "^4.0.1",
|
|
||||||
"mochawesome-report-generator": "^4.1.0",
|
|
||||||
"typescript": "3.4.3",
|
|
||||||
"wait-on": "^4.0.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@microsoft/applicationinsights-web": "^2.5.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
let appInsightsLib = require("@microsoft/applicationinsights-web");
|
|
||||||
|
|
||||||
const appInsights = new appInsightsLib.ApplicationInsights({
|
|
||||||
config: {
|
|
||||||
instrumentationKey: "fe61c39f-7d32-4488-a191-b13621965315"
|
|
||||||
/* ...Other Configuration Options... */
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
appInsights.loadAppInsights();
|
|
||||||
|
|
||||||
Cypress.on("fail", (error, runnable) => {
|
|
||||||
// App Insights will record the fail tests for Create Collection
|
|
||||||
let message = JSON.stringify(runnable.title);
|
|
||||||
appInsights.trackTrace({
|
|
||||||
message: `${message}`,
|
|
||||||
properties: {
|
|
||||||
passed: false,
|
|
||||||
error: error
|
|
||||||
}
|
|
||||||
});
|
|
||||||
throw error; // throw error to have test still fail
|
|
||||||
});
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"strict": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"module": "commonjs",
|
|
||||||
"target": "es5",
|
|
||||||
"lib": ["es5", "dom", "es6"],
|
|
||||||
"types": ["cypress", "node"]
|
|
||||||
},
|
|
||||||
"include": ["**/*.ts", "**/*.spec.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
loginUsingConnectionString: function() {
|
|
||||||
const prodUrl = Cypress.env("TEST_ENDPOINT") || "https://localhost:1234/hostedExplorer.html";
|
|
||||||
const timeout = 15000;
|
|
||||||
|
|
||||||
cy.visit(prodUrl);
|
|
||||||
cy.get('iframe[id="explorerMenu"]').should("be.visible");
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
|
||||||
const $body = $element.contents().find("body");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find("#connectExplorer")
|
|
||||||
.should("exist")
|
|
||||||
.find("div[class='connectExplorer']")
|
|
||||||
.should("exist")
|
|
||||||
.find("p[class='welcomeText']")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
cy.wrap($body.find("div > p.switchConnectTypeText"))
|
|
||||||
.should("exist")
|
|
||||||
.last()
|
|
||||||
.click({ force: true });
|
|
||||||
|
|
||||||
const secret = Cypress.env("CONNECTION_STRING");
|
|
||||||
|
|
||||||
cy.wrap($body)
|
|
||||||
.find("input[class='inputToken']")
|
|
||||||
.should("exist")
|
|
||||||
.type(secret, {
|
|
||||||
force: true
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.wrap($body.find("input[value='Connect']"), { timeout })
|
|
||||||
.first()
|
|
||||||
.click({ force: true });
|
|
||||||
|
|
||||||
cy.wait(15000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
const { CosmosClient } = require("@azure/cosmos");
|
|
||||||
|
|
||||||
module.exports = new CosmosClient({
|
|
||||||
endpoint: "https://0.0.0.0:8081",
|
|
||||||
key: "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
|
|
||||||
});
|
|
||||||
@@ -194,8 +194,8 @@
|
|||||||
"compile": "tsc",
|
"compile": "tsc",
|
||||||
"compile:contracts": "tsc -p ./tsconfig.contracts.json",
|
"compile:contracts": "tsc -p ./tsconfig.contracts.json",
|
||||||
"compile:strict": "tsc -p ./tsconfig.strict.json",
|
"compile:strict": "tsc -p ./tsconfig.strict.json",
|
||||||
"format": "prettier --write \"{src,cypress,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
"format": "prettier --write \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
||||||
"format:check": "prettier --check \"{src,cypress,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
"format:check": "prettier --check \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
||||||
"lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"",
|
"lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"",
|
||||||
"build:contracts": "npm run compile:contracts",
|
"build:contracts": "npm run compile:contracts",
|
||||||
"strictEligibleFiles": "node ./strict-migration-tools/index.js",
|
"strictEligibleFiles": "node ./strict-migration-tools/index.js",
|
||||||
|
|||||||
@@ -125,7 +125,9 @@ export class Features {
|
|||||||
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
|
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
|
||||||
public static readonly ttl90Days = "ttl90days";
|
public static readonly ttl90Days = "ttl90days";
|
||||||
public static readonly enableRightPanelV2 = "enablerightpanelv2";
|
public static readonly enableRightPanelV2 = "enablerightpanelv2";
|
||||||
|
public static readonly enableSchema = "enableschema";
|
||||||
public static readonly enableSDKoperations = "enablesdkoperations";
|
public static readonly enableSDKoperations = "enablesdkoperations";
|
||||||
|
public static readonly showMinRUSurvey = "showminrusurvey";
|
||||||
}
|
}
|
||||||
|
|
||||||
// flight names returned from the portal are always lowercase
|
// flight names returned from the portal are always lowercase
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ARMError } from "../Utils/arm/request";
|
import { ARMError } from "../Utils/arm/request";
|
||||||
import { HttpStatusCodes } from "./Constants";
|
import { HttpStatusCodes } from "./Constants";
|
||||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||||
import { SubscriptionType } from "../Contracts/ViewModels";
|
import { SubscriptionType } from "../Contracts/SubscriptionType";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import { logError } from "./Logger";
|
import { logError } from "./Logger";
|
||||||
import { sendMessage } from "./MessageHandler";
|
import { sendMessage } from "./MessageHandler";
|
||||||
|
|||||||
45
src/Common/dataAccess/readCollectionQuotaInfo.ts
Normal file
45
src/Common/dataAccess/readCollectionQuotaInfo.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { updateOfferThroughputBeyondLimit } from "./updateOfferThroughputBeyondLimit";
|
||||||
|
|
||||||
|
describe("updateOfferThroughputBeyondLimit", () => {
|
||||||
|
it("should call fetch", async () => {
|
||||||
|
window.fetch = jest.fn(() => {
|
||||||
|
return {
|
||||||
|
ok: true
|
||||||
|
};
|
||||||
|
});
|
||||||
|
window.dataExplorer = {
|
||||||
|
logConsoleData: jest.fn(),
|
||||||
|
deleteInProgressConsoleDataWithId: jest.fn()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} as any;
|
||||||
|
await updateOfferThroughputBeyondLimit({
|
||||||
|
subscriptionId: "foo",
|
||||||
|
resourceGroup: "foo",
|
||||||
|
databaseAccountName: "foo",
|
||||||
|
databaseName: "foo",
|
||||||
|
throughput: 1000000000,
|
||||||
|
offerIsRUPerMinuteThroughputEnabled: false
|
||||||
|
});
|
||||||
|
expect(window.fetch).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
57
src/Common/dataAccess/updateOfferThroughputBeyondLimit.ts
Normal file
57
src/Common/dataAccess/updateOfferThroughputBeyondLimit.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { Platform, configContext } from "../../ConfigContext";
|
||||||
|
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||||
|
import { AutoPilotOfferSettings } from "../../Contracts/DataModels";
|
||||||
|
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import { HttpHeaders } from "../Constants";
|
||||||
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
|
|
||||||
|
interface UpdateOfferThroughputRequest {
|
||||||
|
subscriptionId: string;
|
||||||
|
resourceGroup: string;
|
||||||
|
databaseAccountName: string;
|
||||||
|
databaseName: string;
|
||||||
|
collectionName?: string;
|
||||||
|
throughput: number;
|
||||||
|
offerIsRUPerMinuteThroughputEnabled: boolean;
|
||||||
|
offerAutopilotSettings?: AutoPilotOfferSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateOfferThroughputBeyondLimit(request: UpdateOfferThroughputRequest): Promise<void> {
|
||||||
|
if (configContext.platform !== Platform.Portal) {
|
||||||
|
throw new Error("Updating throughput beyond specified limit is not supported on this platform");
|
||||||
|
}
|
||||||
|
|
||||||
|
const resourceDescriptionInfo = request.collectionName
|
||||||
|
? `database ${request.databaseName} and container ${request.collectionName}`
|
||||||
|
: `database ${request.databaseName}`;
|
||||||
|
|
||||||
|
const clearMessage = logConsoleProgress(
|
||||||
|
`Requesting increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const url = `${configContext.BACKEND_ENDPOINT}/api/offerthroughputrequest/updatebeyondspecifiedlimit`;
|
||||||
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
headers: { [authorizationHeader.header]: authorizationHeader.token, [HttpHeaders.contentType]: "application/json" }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
logConsoleInfo(
|
||||||
|
`Successfully requested an increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
|
||||||
|
);
|
||||||
|
clearMessage();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = await response.json();
|
||||||
|
handleError(
|
||||||
|
error,
|
||||||
|
"updateOfferThroughputBeyondLimit",
|
||||||
|
`Failed to request an increase in throughput for ${request.throughput}`
|
||||||
|
);
|
||||||
|
clearMessage();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
@@ -88,6 +88,38 @@ export interface Resource {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IType {
|
||||||
|
name: string;
|
||||||
|
code: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDataField {
|
||||||
|
dataType: IType;
|
||||||
|
hasNulls: boolean;
|
||||||
|
isArray: boolean;
|
||||||
|
schemaType: IType;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
maxRepetitionLevel: number;
|
||||||
|
maxDefinitionLevel: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISchema {
|
||||||
|
id: string;
|
||||||
|
accountName: string;
|
||||||
|
resource: string;
|
||||||
|
fields: IDataField[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISchemaRequest {
|
||||||
|
id: string;
|
||||||
|
subscriptionId: string;
|
||||||
|
resourceGroup: string;
|
||||||
|
accountName: string;
|
||||||
|
resource: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Collection extends Resource {
|
export interface Collection extends Resource {
|
||||||
defaultTtl?: number;
|
defaultTtl?: number;
|
||||||
indexingPolicy?: IndexingPolicy;
|
indexingPolicy?: IndexingPolicy;
|
||||||
@@ -98,6 +130,8 @@ export interface Collection extends Resource {
|
|||||||
changeFeedPolicy?: ChangeFeedPolicy;
|
changeFeedPolicy?: ChangeFeedPolicy;
|
||||||
analyticalStorageTtl?: number;
|
analyticalStorageTtl?: number;
|
||||||
geospatialConfig?: GeospatialConfig;
|
geospatialConfig?: GeospatialConfig;
|
||||||
|
schema?: ISchema;
|
||||||
|
requestSchema?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Database extends Resource {
|
export interface Database extends Resource {
|
||||||
@@ -191,6 +225,18 @@ export interface OfferWithHeaders extends Offer {
|
|||||||
headers: any;
|
headers: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 OfferThroughputInfo {
|
export interface OfferThroughputInfo {
|
||||||
minimumRUForCollection: number;
|
minimumRUForCollection: number;
|
||||||
numPhysicalPartitions: number;
|
numPhysicalPartitions: number;
|
||||||
|
|||||||
7
src/Contracts/SubscriptionType.ts
Normal file
7
src/Contracts/SubscriptionType.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export enum SubscriptionType {
|
||||||
|
Benefits,
|
||||||
|
EA,
|
||||||
|
Free,
|
||||||
|
Internal,
|
||||||
|
PAYG
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import Trigger from "../Explorer/Tree/Trigger";
|
|||||||
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
|
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
|
||||||
import { UploadDetails } from "../workers/upload/definitions";
|
import { UploadDetails } from "../workers/upload/definitions";
|
||||||
import * as DataModels from "./DataModels";
|
import * as DataModels from "./DataModels";
|
||||||
|
import { SubscriptionType } from "./SubscriptionType";
|
||||||
|
|
||||||
export interface TokenProvider {
|
export interface TokenProvider {
|
||||||
getAuthHeader(): Promise<Headers>;
|
getAuthHeader(): Promise<Headers>;
|
||||||
@@ -115,8 +116,11 @@ export interface CollectionBase extends TreeNode {
|
|||||||
export interface Collection extends CollectionBase {
|
export interface Collection extends CollectionBase {
|
||||||
defaultTtl: ko.Observable<number>;
|
defaultTtl: ko.Observable<number>;
|
||||||
analyticalStorageTtl: ko.Observable<number>;
|
analyticalStorageTtl: ko.Observable<number>;
|
||||||
|
schema?: DataModels.ISchema;
|
||||||
|
requestSchema?: () => void;
|
||||||
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
||||||
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
||||||
|
quotaInfo: ko.Observable<DataModels.CollectionQuotaInfo>;
|
||||||
offer: ko.Observable<DataModels.Offer>;
|
offer: ko.Observable<DataModels.Offer>;
|
||||||
conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
||||||
changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
||||||
@@ -357,6 +361,7 @@ export enum CollectionTabKind {
|
|||||||
SparkMasterTab = 16,
|
SparkMasterTab = 16,
|
||||||
Gallery = 17,
|
Gallery = 17,
|
||||||
NotebookViewer = 18,
|
NotebookViewer = 18,
|
||||||
|
Schema = 19,
|
||||||
SettingsV2 = 19
|
SettingsV2 = 19
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,14 +416,6 @@ export interface ThroughputDefaults {
|
|||||||
shared: number;
|
shared: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SubscriptionType {
|
|
||||||
Benefits,
|
|
||||||
EA,
|
|
||||||
Free,
|
|
||||||
Internal,
|
|
||||||
PAYG
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MonacoEditorSettings {
|
export class MonacoEditorSettings {
|
||||||
public readonly language: string;
|
public readonly language: string;
|
||||||
public readonly readOnly: boolean;
|
public readonly readOnly: boolean;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponent
|
|||||||
ko.components.register("tabs-manager", TabsManagerKOComponent());
|
ko.components.register("tabs-manager", TabsManagerKOComponent());
|
||||||
|
|
||||||
// Collection Tabs
|
// Collection Tabs
|
||||||
ko.components.register("documents-tab", new TabComponents.DocumentsTab());
|
ko.components.register("documents-tab", new TabComponents.MongoDocumentsTabV2());
|
||||||
ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTab());
|
ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTab());
|
||||||
ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab());
|
ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab());
|
||||||
ko.components.register("trigger-tab", new TabComponents.TriggerTab());
|
ko.components.register("trigger-tab", new TabComponents.TriggerTab());
|
||||||
|
|||||||
@@ -1,49 +1,54 @@
|
|||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
|
||||||
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "office-ui-fabric-react";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||||
|
import * as Constants from "../../../Common/Constants";
|
||||||
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
|
import * as SharedConstants from "../../../Shared/Constants";
|
||||||
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import DiscardIcon from "../../../../images/discard.svg";
|
import DiscardIcon from "../../../../images/discard.svg";
|
||||||
import SaveIcon from "../../../../images/save-cosmos.svg";
|
import SaveIcon from "../../../../images/save-cosmos.svg";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import { traceStart, traceFailure, traceSuccess, trace } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
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 { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
|
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||||
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 Explorer from "../../Explorer";
|
||||||
|
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||||
|
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
|
||||||
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
|
import { userContext } from "../../../UserContext";
|
||||||
|
import { updateOfferThroughputBeyondLimit } from "../../../Common/dataAccess/updateOfferThroughputBeyondLimit";
|
||||||
import SettingsTab from "../../Tabs/SettingsTabV2";
|
import SettingsTab from "../../Tabs/SettingsTabV2";
|
||||||
import "./SettingsComponent.less";
|
import { throughputUnit } from "./SettingsRenderUtils";
|
||||||
import {
|
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
|
||||||
ConflictResolutionComponent,
|
|
||||||
ConflictResolutionComponentProps
|
|
||||||
} from "./SettingsSubComponents/ConflictResolutionComponent";
|
|
||||||
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
|
|
||||||
import {
|
import {
|
||||||
MongoIndexingPolicyComponent,
|
MongoIndexingPolicyComponent,
|
||||||
MongoIndexingPolicyComponentProps
|
MongoIndexingPolicyComponentProps
|
||||||
} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
|
} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
|
||||||
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
|
|
||||||
import { SubSettingsComponent, SubSettingsComponentProps } from "./SettingsSubComponents/SubSettingsComponent";
|
|
||||||
import {
|
import {
|
||||||
AddMongoIndexProps,
|
getMaxRUs,
|
||||||
ChangeFeedPolicyState,
|
|
||||||
GeospatialConfigType,
|
|
||||||
getMongoNotification,
|
|
||||||
getTabTitle,
|
|
||||||
hasDatabaseSharedThroughput,
|
hasDatabaseSharedThroughput,
|
||||||
|
GeospatialConfigType,
|
||||||
|
TtlType,
|
||||||
|
ChangeFeedPolicyState,
|
||||||
|
SettingsV2TabTypes,
|
||||||
|
getTabTitle,
|
||||||
isDirty,
|
isDirty,
|
||||||
|
AddMongoIndexProps,
|
||||||
MongoIndexTypes,
|
MongoIndexTypes,
|
||||||
parseConflictResolutionMode,
|
parseConflictResolutionMode,
|
||||||
parseConflictResolutionProcedure,
|
parseConflictResolutionProcedure,
|
||||||
SettingsV2TabTypes,
|
getMongoNotification
|
||||||
TtlType
|
|
||||||
} from "./SettingsUtils";
|
} from "./SettingsUtils";
|
||||||
|
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";
|
||||||
|
|
||||||
interface SettingsV2TabInfo {
|
interface SettingsV2TabInfo {
|
||||||
tab: SettingsV2TabTypes;
|
tab: SettingsV2TabTypes;
|
||||||
@@ -445,6 +450,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
if (this.state.isScaleSaveable) {
|
if (this.state.isScaleSaveable) {
|
||||||
const newThroughput = this.state.throughput;
|
const newThroughput = this.state.throughput;
|
||||||
const newOffer: DataModels.Offer = { ...this.collection.offer() };
|
const newOffer: DataModels.Offer = { ...this.collection.offer() };
|
||||||
|
const originalThroughputValue: number = this.state.throughput;
|
||||||
|
|
||||||
if (newOffer.content) {
|
if (newOffer.content) {
|
||||||
newOffer.content.offerThroughput = newThroughput;
|
newOffer.content.offerThroughput = newThroughput;
|
||||||
@@ -482,33 +488,62 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
if (
|
||||||
databaseId: this.collection.databaseId,
|
getMaxRUs(this.collection, this.container) <=
|
||||||
collectionId: this.collection.id(),
|
SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||||
currentOffer: this.collection.offer(),
|
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||||
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
this.container
|
||||||
manualThroughput: this.state.isAutoPilotSelected ? undefined : newThroughput
|
) {
|
||||||
};
|
const requestPayload = {
|
||||||
if (this.hasProvisioningTypeChanged()) {
|
subscriptionId: userContext.subscriptionId,
|
||||||
if (this.state.isAutoPilotSelected) {
|
databaseAccountName: userContext.databaseAccount.name,
|
||||||
updateOfferParams.migrateToAutoPilot = true;
|
resourceGroup: userContext.resourceGroup,
|
||||||
} else {
|
databaseName: this.collection.databaseId,
|
||||||
updateOfferParams.migrateToManual = true;
|
collectionName: this.collection.id(),
|
||||||
}
|
throughput: newThroughput,
|
||||||
}
|
offerIsRUPerMinuteThroughputEnabled: false
|
||||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
};
|
||||||
this.collection.offer(updatedOffer);
|
|
||||||
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
await updateOfferThroughputBeyondLimit(requestPayload);
|
||||||
if (this.state.isAutoPilotSelected) {
|
this.collection.offer().content.offerThroughput = originalThroughputValue;
|
||||||
this.setState({
|
this.setState({
|
||||||
autoPilotThroughput: updatedOffer.content.offerAutopilotSettings.maxThroughput,
|
isScaleSaveable: false,
|
||||||
autoPilotThroughputBaseline: updatedOffer.content.offerAutopilotSettings.maxThroughput
|
isScaleDiscardable: false,
|
||||||
|
throughput: originalThroughputValue,
|
||||||
|
throughputBaseline: originalThroughputValue,
|
||||||
|
initialNotification: {
|
||||||
|
description: `Throughput update for ${newThroughput} ${throughputUnit}`
|
||||||
|
} as DataModels.Notification
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||||
throughput: updatedOffer.content.offerThroughput,
|
databaseId: this.collection.databaseId,
|
||||||
throughputBaseline: updatedOffer.content.offerThroughput
|
collectionId: this.collection.id(),
|
||||||
});
|
currentOffer: this.collection.offer(),
|
||||||
|
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
||||||
|
manualThroughput: this.state.isAutoPilotSelected ? undefined : newThroughput
|
||||||
|
};
|
||||||
|
if (this.hasProvisioningTypeChanged()) {
|
||||||
|
if (this.state.isAutoPilotSelected) {
|
||||||
|
updateOfferParams.migrateToAutoPilot = true;
|
||||||
|
} else {
|
||||||
|
updateOfferParams.migrateToManual = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||||
|
this.collection.offer(updatedOffer);
|
||||||
|
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
||||||
|
if (this.state.isAutoPilotSelected) {
|
||||||
|
this.setState({
|
||||||
|
autoPilotThroughput: updatedOffer.content.offerAutopilotSettings.maxThroughput,
|
||||||
|
autoPilotThroughputBaseline: updatedOffer.content.offerAutopilotSettings.maxThroughput
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
throughput: updatedOffer.content.offerThroughput,
|
||||||
|
throughputBaseline: updatedOffer.content.offerThroughput
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.container.isRefreshingExplorer(false);
|
this.container.isRefreshingExplorer(false);
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import ko from "knockout";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent";
|
||||||
|
import { container, collection } from "../TestUtils";
|
||||||
|
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
|
||||||
|
import Explorer from "../../../Explorer";
|
||||||
import * as Constants from "../../../../Common/Constants";
|
import * as Constants from "../../../../Common/Constants";
|
||||||
import * as DataModels from "../../../../Contracts/DataModels";
|
import * as DataModels from "../../../../Contracts/DataModels";
|
||||||
import Explorer from "../../../Explorer";
|
|
||||||
import { throughputUnit } from "../SettingsRenderUtils";
|
import { throughputUnit } from "../SettingsRenderUtils";
|
||||||
import { collection, container } from "../TestUtils";
|
import * as SharedConstants from "../../../../Shared/Constants";
|
||||||
import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent";
|
import ko from "knockout";
|
||||||
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
|
|
||||||
|
|
||||||
describe("ScaleComponent", () => {
|
describe("ScaleComponent", () => {
|
||||||
const nonNationalCloudContainer = new Explorer();
|
const nonNationalCloudContainer = new Explorer();
|
||||||
@@ -47,7 +48,9 @@ describe("ScaleComponent", () => {
|
|||||||
let wrapper = shallow(<ScaleComponent {...baseProps} />);
|
let wrapper = shallow(<ScaleComponent {...baseProps} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
|
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
|
||||||
|
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(true);
|
||||||
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(false);
|
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(false);
|
||||||
|
expect(wrapper.find("#throughputApplyLongDelayMessage").html()).toContain(targetThroughput);
|
||||||
|
|
||||||
const newCollection = { ...collection };
|
const newCollection = { ...collection };
|
||||||
const maxThroughput = 5000;
|
const maxThroughput = 5000;
|
||||||
@@ -106,6 +109,11 @@ describe("ScaleComponent", () => {
|
|||||||
expect(scaleComponent.isAutoScaleEnabled()).toEqual(true);
|
expect(scaleComponent.isAutoScaleEnabled()).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("getMaxRUThroughputInputLimit", () => {
|
||||||
|
const scaleComponent = new ScaleComponent(baseProps);
|
||||||
|
expect(scaleComponent.getMaxRUThroughputInputLimit()).toEqual(40000);
|
||||||
|
});
|
||||||
|
|
||||||
it("getThroughputTitle", () => {
|
it("getThroughputTitle", () => {
|
||||||
let scaleComponent = new ScaleComponent(baseProps);
|
let scaleComponent = new ScaleComponent(baseProps);
|
||||||
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
|
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
|
||||||
@@ -118,4 +126,26 @@ describe("ScaleComponent", () => {
|
|||||||
scaleComponent = new ScaleComponent(newProps);
|
scaleComponent = new ScaleComponent(newProps);
|
||||||
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (autoscale)");
|
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (autoscale)");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("canThroughputExceedMaximumValue", () => {
|
||||||
|
let scaleComponent = new ScaleComponent(baseProps);
|
||||||
|
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(true);
|
||||||
|
|
||||||
|
const newProps = { ...baseProps, container: nonNationalCloudContainer };
|
||||||
|
scaleComponent = new ScaleComponent(newProps);
|
||||||
|
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getThroughputWarningMessage", () => {
|
||||||
|
const throughputBeyondLimit = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million + 1000;
|
||||||
|
const throughputBeyondMaxRus = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million - 1000;
|
||||||
|
|
||||||
|
const newProps = { ...baseProps, container: nonNationalCloudContainer, throughput: throughputBeyondLimit };
|
||||||
|
let scaleComponent = new ScaleComponent(newProps);
|
||||||
|
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputBeyondLimitWarningMessage");
|
||||||
|
|
||||||
|
newProps.throughput = throughputBeyondMaxRus;
|
||||||
|
scaleComponent = new ScaleComponent(newProps);
|
||||||
|
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputDelayedApplyWarningMessage");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
import { Label, MessageBar, MessageBarType, Stack, Text, TextField } from "office-ui-fabric-react";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as Constants from "../../../../Common/Constants";
|
import * as Constants from "../../../../Common/Constants";
|
||||||
import { configContext, Platform } from "../../../../ConfigContext";
|
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
|
||||||
import * as DataModels from "../../../../Contracts/DataModels";
|
|
||||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||||
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
|
import * as DataModels from "../../../../Contracts/DataModels";
|
||||||
|
import * as SharedConstants from "../../../../Shared/Constants";
|
||||||
import Explorer from "../../../Explorer";
|
import Explorer from "../../../Explorer";
|
||||||
import {
|
import {
|
||||||
getTextFieldStyles,
|
getTextFieldStyles,
|
||||||
getThroughputApplyShortDelayMessage,
|
|
||||||
subComponentStackProps,
|
subComponentStackProps,
|
||||||
|
titleAndInputStackProps,
|
||||||
throughputUnit,
|
throughputUnit,
|
||||||
titleAndInputStackProps
|
getThroughputApplyLongDelayMessage,
|
||||||
|
getThroughputApplyShortDelayMessage,
|
||||||
|
updateThroughputBeyondLimitWarningMessage,
|
||||||
|
updateThroughputDelayedApplyWarningMessage
|
||||||
} from "../SettingsRenderUtils";
|
} from "../SettingsRenderUtils";
|
||||||
import { getMinRUs, hasDatabaseSharedThroughput } from "../SettingsUtils";
|
import { getMaxRUs, getMinRUs, hasDatabaseSharedThroughput } from "../SettingsUtils";
|
||||||
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
|
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
|
||||||
|
import { Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
|
||||||
|
import { configContext, Platform } from "../../../../ConfigContext";
|
||||||
|
|
||||||
export interface ScaleComponentProps {
|
export interface ScaleComponentProps {
|
||||||
collection: ViewModels.Collection;
|
collection: ViewModels.Collection;
|
||||||
@@ -71,17 +75,40 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public getMaxRUThroughputInputLimit = (): number => {
|
||||||
|
if (configContext.platform === Platform.Hosted && this.props.collection.partitionKey) {
|
||||||
|
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getMaxRUs(this.props.collection, this.props.container);
|
||||||
|
};
|
||||||
|
|
||||||
public getThroughputTitle = (): string => {
|
public getThroughputTitle = (): string => {
|
||||||
if (this.props.isAutoPilotSelected) {
|
if (this.props.isAutoPilotSelected) {
|
||||||
return AutoPilotUtils.getAutoPilotHeaderText();
|
return AutoPilotUtils.getAutoPilotHeaderText();
|
||||||
}
|
}
|
||||||
|
|
||||||
const minThroughput: string = getMinRUs(this.props.collection, this.props.container).toLocaleString();
|
const minThroughput: string = getMinRUs(this.props.collection, this.props.container).toLocaleString();
|
||||||
const maxThroughput: string = !this.props.isFixedContainer ? "unlimited" : "10000";
|
const maxThroughput: string =
|
||||||
|
this.canThroughputExceedMaximumValue() && !this.props.isFixedContainer
|
||||||
|
? "unlimited"
|
||||||
|
: getMaxRUs(this.props.collection, this.props.container).toLocaleString();
|
||||||
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`;
|
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public canThroughputExceedMaximumValue = (): boolean => {
|
||||||
|
return (
|
||||||
|
!this.props.isFixedContainer &&
|
||||||
|
configContext.platform === Platform.Portal &&
|
||||||
|
!this.props.container.isRunningOnNationalCloud()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
public getInitialNotificationElement = (): JSX.Element => {
|
public getInitialNotificationElement = (): JSX.Element => {
|
||||||
|
if (this.props.initialNotification) {
|
||||||
|
return this.getLongDelayMessage();
|
||||||
|
}
|
||||||
|
|
||||||
const offer = this.props.collection?.offer && this.props.collection.offer();
|
const offer = this.props.collection?.offer && this.props.collection.offer();
|
||||||
if (
|
if (
|
||||||
offer &&
|
offer &&
|
||||||
@@ -108,6 +135,47 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public getThroughputWarningMessage = (): JSX.Element => {
|
||||||
|
const throughputExceedsBackendLimits: boolean =
|
||||||
|
this.canThroughputExceedMaximumValue() &&
|
||||||
|
getMaxRUs(this.props.collection, this.props.container) <=
|
||||||
|
SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||||
|
this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||||
|
|
||||||
|
if (throughputExceedsBackendLimits && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
|
||||||
|
return updateThroughputBeyondLimitWarningMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
const throughputExceedsMaxValue: boolean =
|
||||||
|
!this.isEmulator && this.props.throughput > getMaxRUs(this.props.collection, this.props.container);
|
||||||
|
|
||||||
|
if (throughputExceedsMaxValue && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
|
||||||
|
return updateThroughputDelayedApplyWarningMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 <></>;
|
||||||
|
};
|
||||||
|
|
||||||
private getThroughputInputComponent = (): JSX.Element => (
|
private getThroughputInputComponent = (): JSX.Element => (
|
||||||
<ThroughputInputAutoPilotV3Component
|
<ThroughputInputAutoPilotV3Component
|
||||||
databaseAccount={this.props.container.databaseAccount()}
|
databaseAccount={this.props.container.databaseAccount()}
|
||||||
@@ -116,7 +184,9 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
throughputBaseline={this.props.throughputBaseline}
|
throughputBaseline={this.props.throughputBaseline}
|
||||||
onThroughputChange={this.props.onThroughputChange}
|
onThroughputChange={this.props.onThroughputChange}
|
||||||
minimum={getMinRUs(this.props.collection, this.props.container)}
|
minimum={getMinRUs(this.props.collection, this.props.container)}
|
||||||
|
maximum={this.getMaxRUThroughputInputLimit()}
|
||||||
isEnabled={!hasDatabaseSharedThroughput(this.props.collection)}
|
isEnabled={!hasDatabaseSharedThroughput(this.props.collection)}
|
||||||
|
canExceedMaximumValue={this.canThroughputExceedMaximumValue()}
|
||||||
label={this.getThroughputTitle()}
|
label={this.getThroughputTitle()}
|
||||||
isEmulator={this.isEmulator}
|
isEmulator={this.isEmulator}
|
||||||
isFixed={this.props.isFixedContainer}
|
isFixed={this.props.isFixedContainer}
|
||||||
@@ -129,6 +199,8 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
spendAckChecked={false}
|
spendAckChecked={false}
|
||||||
onScaleSaveableChange={this.props.onScaleSaveableChange}
|
onScaleSaveableChange={this.props.onScaleSaveableChange}
|
||||||
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
|
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
|
||||||
|
getThroughputWarningMessage={this.getThroughputWarningMessage}
|
||||||
|
usageSizeInKB={this.props.collection.quotaInfo().usageSizeInKB}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ describe("ThroughputInputAutoPilotV3Component", () => {
|
|||||||
throughputBaseline: 100,
|
throughputBaseline: 100,
|
||||||
onThroughputChange: undefined,
|
onThroughputChange: undefined,
|
||||||
minimum: 10000,
|
minimum: 10000,
|
||||||
|
maximum: 400,
|
||||||
step: 100,
|
step: 100,
|
||||||
|
usageSizeInKB: 10000,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
isEmulator: false,
|
isEmulator: false,
|
||||||
spendAckChecked: false,
|
spendAckChecked: false,
|
||||||
@@ -37,7 +39,8 @@ describe("ThroughputInputAutoPilotV3Component", () => {
|
|||||||
},
|
},
|
||||||
onScaleDiscardableChange: () => {
|
onScaleDiscardableChange: () => {
|
||||||
return;
|
return;
|
||||||
}
|
},
|
||||||
|
getThroughputWarningMessage: () => undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
it("throughput input visible", () => {
|
it("throughput input visible", () => {
|
||||||
|
|||||||
@@ -1,33 +1,39 @@
|
|||||||
|
import React from "react";
|
||||||
|
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
|
||||||
import {
|
import {
|
||||||
Checkbox,
|
getTextFieldStyles,
|
||||||
|
getToolTipContainer,
|
||||||
|
noLeftPaddingCheckBoxStyle,
|
||||||
|
titleAndInputStackProps,
|
||||||
|
checkBoxAndInputStackProps,
|
||||||
|
getChoiceGroupStyles,
|
||||||
|
messageBarStyles,
|
||||||
|
getEstimatedSpendElement,
|
||||||
|
getEstimatedAutoscaleSpendElement,
|
||||||
|
getAutoPilotV3SpendElement,
|
||||||
|
manualToAutoscaleDisclaimerElement
|
||||||
|
} from "../../SettingsRenderUtils";
|
||||||
|
import {
|
||||||
|
Text,
|
||||||
|
TextField,
|
||||||
ChoiceGroup,
|
ChoiceGroup,
|
||||||
IChoiceGroupOption,
|
IChoiceGroupOption,
|
||||||
|
Checkbox,
|
||||||
|
Stack,
|
||||||
Label,
|
Label,
|
||||||
Link,
|
Link,
|
||||||
MessageBar,
|
MessageBar,
|
||||||
MessageBarType,
|
MessageBarType
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
TextField
|
|
||||||
} from "office-ui-fabric-react";
|
} from "office-ui-fabric-react";
|
||||||
import React from "react";
|
|
||||||
import * as DataModels from "../../../../../Contracts/DataModels";
|
|
||||||
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";
|
import { ToolTipLabelComponent } from "../ToolTipLabelComponent";
|
||||||
|
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
|
||||||
|
import * as SharedConstants from "../../../../../Shared/Constants";
|
||||||
|
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";
|
||||||
|
|
||||||
export interface ThroughputInputAutoPilotV3Props {
|
export interface ThroughputInputAutoPilotV3Props {
|
||||||
databaseAccount: DataModels.DatabaseAccount;
|
databaseAccount: DataModels.DatabaseAccount;
|
||||||
@@ -36,6 +42,7 @@ export interface ThroughputInputAutoPilotV3Props {
|
|||||||
throughputBaseline: number;
|
throughputBaseline: number;
|
||||||
onThroughputChange: (newThroughput: number) => void;
|
onThroughputChange: (newThroughput: number) => void;
|
||||||
minimum: number;
|
minimum: number;
|
||||||
|
maximum: number;
|
||||||
step?: number;
|
step?: number;
|
||||||
isEnabled?: boolean;
|
isEnabled?: boolean;
|
||||||
spendAckChecked?: boolean;
|
spendAckChecked?: boolean;
|
||||||
@@ -56,6 +63,8 @@ export interface ThroughputInputAutoPilotV3Props {
|
|||||||
onMaxAutoPilotThroughputChange: (newThroughput: number) => void;
|
onMaxAutoPilotThroughputChange: (newThroughput: number) => void;
|
||||||
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
||||||
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
||||||
|
getThroughputWarningMessage: () => JSX.Element;
|
||||||
|
usageSizeInKB: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ThroughputInputAutoPilotV3State {
|
interface ThroughputInputAutoPilotV3State {
|
||||||
@@ -115,7 +124,13 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
if (isDirty(this.props.throughput, this.props.throughputBaseline)) {
|
if (isDirty(this.props.throughput, this.props.throughputBaseline)) {
|
||||||
isDiscardable = true;
|
isDiscardable = true;
|
||||||
isSaveable = true;
|
isSaveable = true;
|
||||||
if (!this.props.throughput || this.props.throughput < this.props.minimum) {
|
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)
|
||||||
|
) {
|
||||||
isSaveable = false;
|
isSaveable = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,8 +146,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.step = this.props.step ?? ThroughputInputAutoPilotV3Component.defaultStep;
|
this.step = this.props.step ?? ThroughputInputAutoPilotV3Component.defaultStep;
|
||||||
this.throughputInputMaxValue = Number.MAX_SAFE_INTEGER;
|
this.throughputInputMaxValue = this.props.canExceedMaximumValue ? Int32.Max : this.props.maximum;
|
||||||
this.autoPilotInputMaxValue = Number.MAX_SAFE_INTEGER;
|
this.autoPilotInputMaxValue = this.props.isFixed ? this.props.maximum : Int32.Max;
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasProvisioningTypeChanged = (): boolean =>
|
public hasProvisioningTypeChanged = (): boolean =>
|
||||||
@@ -214,6 +229,29 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
option?: IChoiceGroupOption
|
option?: IChoiceGroupOption
|
||||||
): void => this.props.onAutoPilotSelected(option.key === "true");
|
): void => this.props.onAutoPilotSelected(option.key === "true");
|
||||||
|
|
||||||
|
private minRUperGBSurvey = (): JSX.Element => {
|
||||||
|
const href = `https://ncv.microsoft.com/vRBTO37jmO?ctx={"AzureSubscriptionId":"${userContext.subscriptionId}","CosmosDBAccountName":"${userContext.databaseAccount?.name}"}`;
|
||||||
|
const oneTBinKB = 1000000000;
|
||||||
|
const minRUperGB = 10;
|
||||||
|
const featureFlagEnabled = window.dataExplorer?.isFeatureEnabled(Features.showMinRUSurvey);
|
||||||
|
const collectionIsEligible =
|
||||||
|
userContext.subscriptionType !== SubscriptionType.Internal &&
|
||||||
|
this.props.usageSizeInKB > oneTBinKB &&
|
||||||
|
this.props.minimum >= usageInGB(this.props.usageSizeInKB) * minRUperGB;
|
||||||
|
if (featureFlagEnabled || collectionIsEligible) {
|
||||||
|
return (
|
||||||
|
<Text>
|
||||||
|
Need to scale below {this.props.minimum} RU/s? Reach out by filling{" "}
|
||||||
|
<a target="_blank" rel="noreferrer" href={href}>
|
||||||
|
this questionnaire
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
private renderThroughputModeChoices = (): JSX.Element => {
|
private renderThroughputModeChoices = (): JSX.Element => {
|
||||||
const labelId = "settingsV2RadioButtonLabelId";
|
const labelId = "settingsV2RadioButtonLabelId";
|
||||||
return (
|
return (
|
||||||
@@ -265,6 +303,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
onChange={this.onAutoPilotThroughputChange}
|
onChange={this.onAutoPilotThroughputChange}
|
||||||
/>
|
/>
|
||||||
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
|
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
|
||||||
|
{this.minRUperGBSurvey()}
|
||||||
{this.props.spendAckVisible && (
|
{this.props.spendAckVisible && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="spendAckCheckBox"
|
id="spendAckCheckBox"
|
||||||
@@ -295,9 +334,13 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
}
|
}
|
||||||
onChange={this.onThroughputChange}
|
onChange={this.onThroughputChange}
|
||||||
/>
|
/>
|
||||||
|
{this.props.getThroughputWarningMessage() && (
|
||||||
|
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
|
||||||
|
{this.props.getThroughputWarningMessage()}
|
||||||
|
</MessageBar>
|
||||||
|
)}
|
||||||
{!this.props.isEmulator && this.getRequestUnitsUsageCost()}
|
{!this.props.isEmulator && this.getRequestUnitsUsageCost()}
|
||||||
|
{this.minRUperGBSurvey()}
|
||||||
{this.props.spendAckVisible && (
|
{this.props.spendAckVisible && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="spendAckCheckBox"
|
id="spendAckCheckBox"
|
||||||
@@ -307,7 +350,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
onChange={this.onSpendAckChecked}
|
onChange={this.onSpendAckChecked}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{this.props.isFixed && <p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>}
|
{this.props.isFixed && <p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,29 @@ exports[`ScaleComponent renders with correct intiial notification 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<StyledMessageBarBase
|
||||||
|
messageBarType={5}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
id="throughputApplyLongDelayMessage"
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"fontSize": 12,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to complete. View the latest status in Notifications.
|
||||||
|
<br />
|
||||||
|
Database:
|
||||||
|
test
|
||||||
|
, Container:
|
||||||
|
test
|
||||||
|
|
||||||
|
, Current autoscale throughput: 100 - 1000 RU/s, Target autoscale throughput: 600 - 6000 RU/s
|
||||||
|
</Text>
|
||||||
|
</StyledMessageBarBase>
|
||||||
<Stack
|
<Stack
|
||||||
tokens={
|
tokens={
|
||||||
Object {
|
Object {
|
||||||
@@ -16,6 +39,8 @@ exports[`ScaleComponent renders with correct intiial notification 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ThroughputInputAutoPilotV3Component
|
<ThroughputInputAutoPilotV3Component
|
||||||
|
canExceedMaximumValue={true}
|
||||||
|
getThroughputWarningMessage={[Function]}
|
||||||
isAutoPilotSelected={false}
|
isAutoPilotSelected={false}
|
||||||
isEmulator={false}
|
isEmulator={false}
|
||||||
isEnabled={true}
|
isEnabled={true}
|
||||||
@@ -23,6 +48,7 @@ exports[`ScaleComponent renders with correct intiial notification 1`] = `
|
|||||||
label="Throughput (6,000 - unlimited RU/s)"
|
label="Throughput (6,000 - unlimited RU/s)"
|
||||||
maxAutoPilotThroughput={4000}
|
maxAutoPilotThroughput={4000}
|
||||||
maxAutoPilotThroughputBaseline={4000}
|
maxAutoPilotThroughputBaseline={4000}
|
||||||
|
maximum={40000}
|
||||||
minimum={6000}
|
minimum={6000}
|
||||||
onAutoPilotSelected={[Function]}
|
onAutoPilotSelected={[Function]}
|
||||||
onMaxAutoPilotThroughputChange={[Function]}
|
onMaxAutoPilotThroughputChange={[Function]}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { collection, container } from "./TestUtils";
|
import { collection, container } from "./TestUtils";
|
||||||
import {
|
import {
|
||||||
|
getMaxRUs,
|
||||||
getMinRUs,
|
getMinRUs,
|
||||||
getMongoIndexType,
|
getMongoIndexType,
|
||||||
getMongoNotification,
|
getMongoNotification,
|
||||||
@@ -22,6 +23,11 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
|||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
|
|
||||||
describe("SettingsUtils", () => {
|
describe("SettingsUtils", () => {
|
||||||
|
it("getMaxRUs", () => {
|
||||||
|
expect(collection.offer().content.collectionThroughputInfo.numPhysicalPartitions).toEqual(4);
|
||||||
|
expect(getMaxRUs(collection, container)).toEqual(40000);
|
||||||
|
});
|
||||||
|
|
||||||
it("getMinRUs", () => {
|
it("getMinRUs", () => {
|
||||||
expect(collection.offer().content.collectionThroughputInfo.numPhysicalPartitions).toEqual(4);
|
expect(collection.offer().content.collectionThroughputInfo.numPhysicalPartitions).toEqual(4);
|
||||||
expect(getMinRUs(collection, container)).toEqual(6000);
|
expect(getMinRUs(collection, container)).toEqual(6000);
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import * as Constants from "../../../Common/Constants";
|
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
|
import * as Constants from "../../../Common/Constants";
|
||||||
import * as SharedConstants from "../../../Shared/Constants";
|
import * as SharedConstants from "../../../Shared/Constants";
|
||||||
import { MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
|
import * as PricingUtils from "../../../Utils/PricingUtils";
|
||||||
|
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
|
import { MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
|
||||||
|
|
||||||
const zeroValue = 0;
|
const zeroValue = 0;
|
||||||
export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy;
|
export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy;
|
||||||
@@ -69,6 +71,22 @@ export const hasDatabaseSharedThroughput = (collection: ViewModels.Collection):
|
|||||||
return database?.isDatabaseShared() && !collection.offer();
|
return database?.isDatabaseShared() && !collection.offer();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getMaxRUs = (collection: ViewModels.Collection, container: Explorer): number => {
|
||||||
|
const isTryCosmosDBSubscription = container?.isTryCosmosDBSubscription() || false;
|
||||||
|
if (isTryCosmosDBSubscription) {
|
||||||
|
return Constants.TryCosmosExperience.maxRU;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numPartitionsFromOffer: number =
|
||||||
|
collection?.offer && collection.offer()?.content?.collectionThroughputInfo?.numPhysicalPartitions;
|
||||||
|
|
||||||
|
const numPartitionsFromQuotaInfo: number = collection?.quotaInfo()?.numPartitions;
|
||||||
|
|
||||||
|
const numPartitions = numPartitionsFromOffer ?? numPartitionsFromQuotaInfo ?? 1;
|
||||||
|
|
||||||
|
return SharedConstants.CollectionCreation.MaxRUPerPartition * numPartitions;
|
||||||
|
};
|
||||||
|
|
||||||
export const getMinRUs = (collection: ViewModels.Collection, container: Explorer): number => {
|
export const getMinRUs = (collection: ViewModels.Collection, container: Explorer): number => {
|
||||||
const isTryCosmosDBSubscription = container?.isTryCosmosDBSubscription();
|
const isTryCosmosDBSubscription = container?.isTryCosmosDBSubscription();
|
||||||
if (isTryCosmosDBSubscription) {
|
if (isTryCosmosDBSubscription) {
|
||||||
@@ -87,7 +105,21 @@ export const getMinRUs = (collection: ViewModels.Collection, container: Explorer
|
|||||||
return collectionThroughputInfo.minimumRUForCollection;
|
return collectionThroughputInfo.minimumRUForCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
const numPartitions = collectionThroughputInfo?.numPhysicalPartitions ?? collection.quotaInfo()?.numPartitions;
|
||||||
|
|
||||||
|
if (!numPartitions || numPartitions === 1) {
|
||||||
|
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseRU = SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||||
|
|
||||||
|
const quotaInKb = collection.quotaInfo().collectionSize;
|
||||||
|
const quotaInGb = PricingUtils.usageInGB(quotaInKb);
|
||||||
|
|
||||||
|
const perPartitionGBQuota: number = Math.max(10, quotaInGb / numPartitions);
|
||||||
|
const baseRUbyPartitions: number = ((numPartitions * perPartitionGBQuota) / 10) * 100;
|
||||||
|
|
||||||
|
return Math.max(baseRU, baseRUbyPartitions);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseConflictResolutionMode = (modeFromBackend: string): DataModels.ConflictResolutionMode => {
|
export const parseConflictResolutionMode = (modeFromBackend: string): DataModels.ConflictResolutionMode => {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const collection = ({
|
|||||||
excludedPaths: []
|
excludedPaths: []
|
||||||
}),
|
}),
|
||||||
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
|
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
|
||||||
|
quotaInfo: ko.observable<DataModels.CollectionQuotaInfo>({} as DataModels.CollectionQuotaInfo),
|
||||||
offer: ko.observable<DataModels.Offer>({
|
offer: ko.observable<DataModels.Offer>({
|
||||||
content: {
|
content: {
|
||||||
offerThroughput: 10000,
|
offerThroughput: 10000,
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
|
"canRequestSupport": [Function],
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"databaseCreateNewShared": [Function],
|
"databaseCreateNewShared": [Function],
|
||||||
@@ -85,6 +86,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
|
"canRequestSupport": [Function],
|
||||||
"collectionId": [Function],
|
"collectionId": [Function],
|
||||||
"collectionIdTitle": [Function],
|
"collectionIdTitle": [Function],
|
||||||
"collectionWithThroughputInShared": [Function],
|
"collectionWithThroughputInShared": [Function],
|
||||||
@@ -347,6 +349,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
|
"canRequestSupport": [Function],
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"createTableQuery": [Function],
|
"createTableQuery": [Function],
|
||||||
@@ -572,6 +575,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
|
"canRequestSupport": [Function],
|
||||||
"collectionId": [Function],
|
"collectionId": [Function],
|
||||||
"collectionIdTitle": [Function],
|
"collectionIdTitle": [Function],
|
||||||
"collectionWithThroughputInShared": [Function],
|
"collectionWithThroughputInShared": [Function],
|
||||||
@@ -653,6 +657,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
|
"canRequestSupport": [Function],
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"databaseCreateNewShared": [Function],
|
"databaseCreateNewShared": [Function],
|
||||||
@@ -755,6 +760,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
|
"canRequestSupport": [Function],
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"createTableQuery": [Function],
|
"createTableQuery": [Function],
|
||||||
@@ -965,6 +971,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isRefreshingExplorer": [Function],
|
"isRefreshingExplorer": [Function],
|
||||||
"isResourceTokenCollectionNodeSelected": [Function],
|
"isResourceTokenCollectionNodeSelected": [Function],
|
||||||
"isRightPanelV2Enabled": [Function],
|
"isRightPanelV2Enabled": [Function],
|
||||||
|
"isSchemaEnabled": [Function],
|
||||||
"isServerlessEnabled": [Function],
|
"isServerlessEnabled": [Function],
|
||||||
"isSettingsV2Enabled": [Function],
|
"isSettingsV2Enabled": [Function],
|
||||||
"isSparkEnabled": [Function],
|
"isSparkEnabled": [Function],
|
||||||
@@ -1295,6 +1302,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"version": 2,
|
"version": 2,
|
||||||
},
|
},
|
||||||
"partitionKeyProperty": "partitionKey",
|
"partitionKeyProperty": "partitionKey",
|
||||||
|
"quotaInfo": [Function],
|
||||||
"readSettings": [Function],
|
"readSettings": [Function],
|
||||||
"uniqueKeyPolicy": Object {},
|
"uniqueKeyPolicy": Object {},
|
||||||
}
|
}
|
||||||
@@ -1316,6 +1324,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
|
"canRequestSupport": [Function],
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"databaseCreateNewShared": [Function],
|
"databaseCreateNewShared": [Function],
|
||||||
@@ -1358,6 +1367,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
|
"canRequestSupport": [Function],
|
||||||
"collectionId": [Function],
|
"collectionId": [Function],
|
||||||
"collectionIdTitle": [Function],
|
"collectionIdTitle": [Function],
|
||||||
"collectionWithThroughputInShared": [Function],
|
"collectionWithThroughputInShared": [Function],
|
||||||
@@ -1620,6 +1630,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
|
"canRequestSupport": [Function],
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"createTableQuery": [Function],
|
"createTableQuery": [Function],
|
||||||
@@ -1845,6 +1856,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
|
"canRequestSupport": [Function],
|
||||||
"collectionId": [Function],
|
"collectionId": [Function],
|
||||||
"collectionIdTitle": [Function],
|
"collectionIdTitle": [Function],
|
||||||
"collectionWithThroughputInShared": [Function],
|
"collectionWithThroughputInShared": [Function],
|
||||||
@@ -1926,6 +1938,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
|
"canRequestSupport": [Function],
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"databaseCreateNewShared": [Function],
|
"databaseCreateNewShared": [Function],
|
||||||
@@ -2028,6 +2041,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
|
"canRequestSupport": [Function],
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"createTableQuery": [Function],
|
"createTableQuery": [Function],
|
||||||
@@ -2238,6 +2252,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isRefreshingExplorer": [Function],
|
"isRefreshingExplorer": [Function],
|
||||||
"isResourceTokenCollectionNodeSelected": [Function],
|
"isResourceTokenCollectionNodeSelected": [Function],
|
||||||
"isRightPanelV2Enabled": [Function],
|
"isRightPanelV2Enabled": [Function],
|
||||||
|
"isSchemaEnabled": [Function],
|
||||||
"isServerlessEnabled": [Function],
|
"isServerlessEnabled": [Function],
|
||||||
"isSettingsV2Enabled": [Function],
|
"isSettingsV2Enabled": [Function],
|
||||||
"isSparkEnabled": [Function],
|
"isSparkEnabled": [Function],
|
||||||
@@ -2603,6 +2618,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
|
"canRequestSupport": [Function],
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"databaseCreateNewShared": [Function],
|
"databaseCreateNewShared": [Function],
|
||||||
@@ -2645,6 +2661,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
|
"canRequestSupport": [Function],
|
||||||
"collectionId": [Function],
|
"collectionId": [Function],
|
||||||
"collectionIdTitle": [Function],
|
"collectionIdTitle": [Function],
|
||||||
"collectionWithThroughputInShared": [Function],
|
"collectionWithThroughputInShared": [Function],
|
||||||
@@ -2907,6 +2924,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
|
"canRequestSupport": [Function],
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"createTableQuery": [Function],
|
"createTableQuery": [Function],
|
||||||
@@ -3132,6 +3150,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
|
"canRequestSupport": [Function],
|
||||||
"collectionId": [Function],
|
"collectionId": [Function],
|
||||||
"collectionIdTitle": [Function],
|
"collectionIdTitle": [Function],
|
||||||
"collectionWithThroughputInShared": [Function],
|
"collectionWithThroughputInShared": [Function],
|
||||||
@@ -3213,6 +3232,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
|
"canRequestSupport": [Function],
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"databaseCreateNewShared": [Function],
|
"databaseCreateNewShared": [Function],
|
||||||
@@ -3315,6 +3335,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
|
"canRequestSupport": [Function],
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"createTableQuery": [Function],
|
"createTableQuery": [Function],
|
||||||
@@ -3525,6 +3546,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isRefreshingExplorer": [Function],
|
"isRefreshingExplorer": [Function],
|
||||||
"isResourceTokenCollectionNodeSelected": [Function],
|
"isResourceTokenCollectionNodeSelected": [Function],
|
||||||
"isRightPanelV2Enabled": [Function],
|
"isRightPanelV2Enabled": [Function],
|
||||||
|
"isSchemaEnabled": [Function],
|
||||||
"isServerlessEnabled": [Function],
|
"isServerlessEnabled": [Function],
|
||||||
"isSettingsV2Enabled": [Function],
|
"isSettingsV2Enabled": [Function],
|
||||||
"isSparkEnabled": [Function],
|
"isSparkEnabled": [Function],
|
||||||
@@ -3855,6 +3877,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"version": 2,
|
"version": 2,
|
||||||
},
|
},
|
||||||
"partitionKeyProperty": "partitionKey",
|
"partitionKeyProperty": "partitionKey",
|
||||||
|
"quotaInfo": [Function],
|
||||||
"readSettings": [Function],
|
"readSettings": [Function],
|
||||||
"uniqueKeyPolicy": Object {},
|
"uniqueKeyPolicy": Object {},
|
||||||
}
|
}
|
||||||
@@ -3876,6 +3899,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
|
"canRequestSupport": [Function],
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"databaseCreateNewShared": [Function],
|
"databaseCreateNewShared": [Function],
|
||||||
@@ -3918,6 +3942,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
|
"canRequestSupport": [Function],
|
||||||
"collectionId": [Function],
|
"collectionId": [Function],
|
||||||
"collectionIdTitle": [Function],
|
"collectionIdTitle": [Function],
|
||||||
"collectionWithThroughputInShared": [Function],
|
"collectionWithThroughputInShared": [Function],
|
||||||
@@ -4180,6 +4205,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
|
"canRequestSupport": [Function],
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"createTableQuery": [Function],
|
"createTableQuery": [Function],
|
||||||
@@ -4405,6 +4431,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
|
"canRequestSupport": [Function],
|
||||||
"collectionId": [Function],
|
"collectionId": [Function],
|
||||||
"collectionIdTitle": [Function],
|
"collectionIdTitle": [Function],
|
||||||
"collectionWithThroughputInShared": [Function],
|
"collectionWithThroughputInShared": [Function],
|
||||||
@@ -4486,6 +4513,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
|
"canRequestSupport": [Function],
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"databaseCreateNewShared": [Function],
|
"databaseCreateNewShared": [Function],
|
||||||
@@ -4588,6 +4616,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"autoPilotUsageCost": [Function],
|
"autoPilotUsageCost": [Function],
|
||||||
"canConfigureThroughput": [Function],
|
"canConfigureThroughput": [Function],
|
||||||
"canExceedMaximumValue": [Function],
|
"canExceedMaximumValue": [Function],
|
||||||
|
"canRequestSupport": [Function],
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"costsVisible": [Function],
|
"costsVisible": [Function],
|
||||||
"createTableQuery": [Function],
|
"createTableQuery": [Function],
|
||||||
@@ -4798,6 +4827,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isRefreshingExplorer": [Function],
|
"isRefreshingExplorer": [Function],
|
||||||
"isResourceTokenCollectionNodeSelected": [Function],
|
"isResourceTokenCollectionNodeSelected": [Function],
|
||||||
"isRightPanelV2Enabled": [Function],
|
"isRightPanelV2Enabled": [Function],
|
||||||
|
"isSchemaEnabled": [Function],
|
||||||
"isServerlessEnabled": [Function],
|
"isServerlessEnabled": [Function],
|
||||||
"isSettingsV2Enabled": [Function],
|
"isSettingsV2Enabled": [Function],
|
||||||
"isSparkEnabled": [Function],
|
"isSparkEnabled": [Function],
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ import { updateUserContext, userContext } from "../UserContext";
|
|||||||
import { stringToBlob } from "../Utils/BlobUtils";
|
import { stringToBlob } from "../Utils/BlobUtils";
|
||||||
import { IChoiceGroupProps } from "office-ui-fabric-react";
|
import { IChoiceGroupProps } from "office-ui-fabric-react";
|
||||||
import { getErrorMessage, handleError, getErrorStack } from "../Common/ErrorHandlingUtils";
|
import { getErrorMessage, handleError, getErrorStack } from "../Common/ErrorHandlingUtils";
|
||||||
|
import { SubscriptionType } from "../Contracts/SubscriptionType";
|
||||||
|
|
||||||
BindingHandlersRegisterer.registerBindingHandlers();
|
BindingHandlersRegisterer.registerBindingHandlers();
|
||||||
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
||||||
@@ -119,7 +120,7 @@ export default class Explorer {
|
|||||||
|
|
||||||
public databaseAccount: ko.Observable<DataModels.DatabaseAccount>;
|
public databaseAccount: ko.Observable<DataModels.DatabaseAccount>;
|
||||||
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults;
|
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults;
|
||||||
public subscriptionType: ko.Observable<ViewModels.SubscriptionType>;
|
public subscriptionType: ko.Observable<SubscriptionType>;
|
||||||
public quotaId: ko.Observable<string>;
|
public quotaId: ko.Observable<string>;
|
||||||
public defaultExperience: ko.Observable<string>;
|
public defaultExperience: ko.Observable<string>;
|
||||||
public isPreferredApiDocumentDB: ko.Computed<boolean>;
|
public isPreferredApiDocumentDB: ko.Computed<boolean>;
|
||||||
@@ -225,6 +226,7 @@ export default class Explorer {
|
|||||||
public shareTokenCopyHelperText: ko.Observable<string>;
|
public shareTokenCopyHelperText: ko.Observable<string>;
|
||||||
public shouldShowDataAccessExpiryDialog: ko.Observable<boolean>;
|
public shouldShowDataAccessExpiryDialog: ko.Observable<boolean>;
|
||||||
public shouldShowContextSwitchPrompt: ko.Observable<boolean>;
|
public shouldShowContextSwitchPrompt: ko.Observable<boolean>;
|
||||||
|
public isSchemaEnabled: ko.Computed<boolean>;
|
||||||
|
|
||||||
// Notebooks
|
// Notebooks
|
||||||
public isNotebookEnabled: ko.Observable<boolean>;
|
public isNotebookEnabled: ko.Observable<boolean>;
|
||||||
@@ -278,9 +280,7 @@ export default class Explorer {
|
|||||||
this.refreshTreeTitle = ko.observable<string>("Refresh collections");
|
this.refreshTreeTitle = ko.observable<string>("Refresh collections");
|
||||||
|
|
||||||
this.databaseAccount = ko.observable<DataModels.DatabaseAccount>();
|
this.databaseAccount = ko.observable<DataModels.DatabaseAccount>();
|
||||||
this.subscriptionType = ko.observable<ViewModels.SubscriptionType>(
|
this.subscriptionType = ko.observable<SubscriptionType>(SharedConstants.CollectionCreation.DefaultSubscriptionType);
|
||||||
SharedConstants.CollectionCreation.DefaultSubscriptionType
|
|
||||||
);
|
|
||||||
this.quotaId = ko.observable<string>("");
|
this.quotaId = ko.observable<string>("");
|
||||||
let firstInitialization = true;
|
let firstInitialization = true;
|
||||||
this.isRefreshingExplorer = ko.observable<boolean>(true);
|
this.isRefreshingExplorer = ko.observable<boolean>(true);
|
||||||
@@ -422,6 +422,7 @@ export default class Explorer {
|
|||||||
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
|
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.isSchemaEnabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSchema));
|
||||||
this.isNotificationConsoleExpanded = ko.observable<boolean>(false);
|
this.isNotificationConsoleExpanded = ko.observable<boolean>(false);
|
||||||
|
|
||||||
this.databases = ko.observableArray<ViewModels.Database>();
|
this.databases = ko.observableArray<ViewModels.Database>();
|
||||||
@@ -1890,7 +1891,8 @@ export default class Explorer {
|
|||||||
masterKey,
|
masterKey,
|
||||||
databaseAccount,
|
databaseAccount,
|
||||||
resourceGroup: inputs.resourceGroup,
|
resourceGroup: inputs.resourceGroup,
|
||||||
subscriptionId: inputs.subscriptionId
|
subscriptionId: inputs.subscriptionId,
|
||||||
|
subscriptionType: inputs.subscriptionType
|
||||||
});
|
});
|
||||||
TelemetryProcessor.traceSuccess(
|
TelemetryProcessor.traceSuccess(
|
||||||
Action.LoadDatabaseAccount,
|
Action.LoadDatabaseAccount,
|
||||||
@@ -2377,13 +2379,11 @@ export default class Explorer {
|
|||||||
this.tabsManager.activateTab(notebookTab);
|
this.tabsManager.activateTab(notebookTab);
|
||||||
} else {
|
} else {
|
||||||
const options: NotebookTabOptions = {
|
const options: NotebookTabOptions = {
|
||||||
account: userContext.databaseAccount,
|
|
||||||
tabKind: ViewModels.CollectionTabKind.NotebookV2,
|
tabKind: ViewModels.CollectionTabKind.NotebookV2,
|
||||||
node: null,
|
node: null,
|
||||||
title: notebookContentItem.name,
|
title: notebookContentItem.name,
|
||||||
tabPath: notebookContentItem.path,
|
tabPath: notebookContentItem.path,
|
||||||
collection: null,
|
collection: null,
|
||||||
masterKey: userContext.masterKey || "",
|
|
||||||
hashLocation: "notebooks",
|
hashLocation: "notebooks",
|
||||||
isActive: ko.observable(false),
|
isActive: ko.observable(false),
|
||||||
isTabsContentExpanded: ko.observable(true),
|
isTabsContentExpanded: ko.observable(true),
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
.mongoQueryComponent {
|
||||||
|
margin-left: 10px;
|
||||||
|
|
||||||
|
input {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label:before {
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queryInput {
|
||||||
|
border: 1px solid black;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Dispatch } from "redux";
|
||||||
|
import MonacoEditor from "@nteract/monaco-editor";
|
||||||
|
import { PrimaryButton } from "office-ui-fabric-react";
|
||||||
|
import { ChoiceGroup, IChoiceGroupOption } from "office-ui-fabric-react/lib/ChoiceGroup";
|
||||||
|
import Outputs from "@nteract/stateful-components/lib/outputs";
|
||||||
|
import { KernelOutputError, StreamText } from "@nteract/outputs";
|
||||||
|
import TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media";
|
||||||
|
import { actions, selectors, AppState, ContentRef, KernelRef } from "@nteract/core";
|
||||||
|
import loadTransform from "../NotebookComponent/loadTransform";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import Immutable from "immutable";
|
||||||
|
|
||||||
|
import "./MongoQueryComponent.less";
|
||||||
|
interface MongoQueryComponentPureProps {
|
||||||
|
contentRef: ContentRef;
|
||||||
|
kernelRef: KernelRef;
|
||||||
|
databaseId: string;
|
||||||
|
collectionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MongoQueryComponentDispatchProps {
|
||||||
|
runCell: (contentRef: ContentRef, cellId: string) => void;
|
||||||
|
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => void;
|
||||||
|
onChange: (text: string, id: string, contentRef: ContentRef) => void;
|
||||||
|
save: (contentRef: ContentRef) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutputType = "rich" | "json";
|
||||||
|
|
||||||
|
interface MongoQueryComponentState {
|
||||||
|
outputType: OutputType;
|
||||||
|
selectedId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: IChoiceGroupOption[] = [
|
||||||
|
{ key: "rich", text: "Rich Output" },
|
||||||
|
{ key: "json", text: "Json Output" }
|
||||||
|
];
|
||||||
|
|
||||||
|
interface MongoKernelJsonOutput {
|
||||||
|
results: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MongoDocument {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type MongoQueryComponentProps = MongoQueryComponentPureProps & StateProps & MongoQueryComponentDispatchProps;
|
||||||
|
export class MongoQueryComponent extends React.Component<MongoQueryComponentProps, MongoQueryComponentState> {
|
||||||
|
constructor(props: MongoQueryComponentProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
outputType: "json",
|
||||||
|
selectedId: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount(): void {
|
||||||
|
loadTransform(this.props);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onExecute = () => {
|
||||||
|
this.props.runCell(this.props.contentRef, this.props.firstCellId);
|
||||||
|
this.props.save(this.props.contentRef);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param databaseId
|
||||||
|
* @param collectionId
|
||||||
|
* @param query e.g. { "lastName": { $in: ["Andersen"] } }
|
||||||
|
*/
|
||||||
|
private createFilterQuery(databaseId: string, collectionId: string, query: string): string {
|
||||||
|
const newCommand = `{ "command": "filter", "database": "${databaseId}", "collection": "${collectionId}", "filter": ${JSON.stringify(query)}, "outputType": "${this.state.outputType}" }`;
|
||||||
|
return newCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onOutputTypeChange = (e: React.FormEvent<HTMLElement | HTMLInputElement>, option: IChoiceGroupOption): void => {
|
||||||
|
const outputType = option.key as OutputType;
|
||||||
|
this.setState({ outputType }, () => this.onInputChange(this.props.inputValue));
|
||||||
|
};
|
||||||
|
|
||||||
|
private onInputChange = (text: string) => {
|
||||||
|
this.props.onChange(this.createFilterQuery(this.props.databaseId, this.props.collectionId, text),
|
||||||
|
this.props.firstCellId, this.props.contentRef);
|
||||||
|
};
|
||||||
|
|
||||||
|
render(): JSX.Element {
|
||||||
|
const { firstCellId: id, contentRef, outputDocuments } = this.props;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mongoQueryComponent">
|
||||||
|
<div className="queryInput">
|
||||||
|
<MonacoEditor id={this.props.firstCellId} contentRef={this.props.contentRef} theme={""}
|
||||||
|
language="json" onChange={this.onInputChange}
|
||||||
|
value={this.props.inputValue} />
|
||||||
|
</div>
|
||||||
|
<PrimaryButton text="Apply" onClick={this.onExecute} disabled={!this.props.firstCellId} />
|
||||||
|
<ChoiceGroup
|
||||||
|
selectedKey={this.state.outputType}
|
||||||
|
options={options}
|
||||||
|
onChange={this.onOutputTypeChange}
|
||||||
|
label="Output Type"
|
||||||
|
styles={{ input: { marginTop: 0 }, root: { marginTop: 0 } }}
|
||||||
|
/>
|
||||||
|
<hr />
|
||||||
|
<div style={ { display: "flex" } }>
|
||||||
|
<ul>
|
||||||
|
{outputDocuments && outputDocuments.map(d => (
|
||||||
|
<li key={d.id}>
|
||||||
|
<a onClick={() => this.setState({ selectedId: id })}>{d.id}</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div style={{ width: "100%" }} >
|
||||||
|
<MonacoEditor id={""} contentRef={""} theme={""} language="json" onChange={() => {}}
|
||||||
|
value={JSON.stringify(outputDocuments.find(doc => doc.id ===this.state.selectedId)) ?? ""} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<Outputs id={id} contentRef={contentRef}>
|
||||||
|
<TransformMedia output_type={"display_data"} id={id} contentRef={contentRef} />
|
||||||
|
<TransformMedia output_type={"execute_result"} id={id} contentRef={contentRef} />
|
||||||
|
<KernelOutputError />
|
||||||
|
<StreamText />
|
||||||
|
</Outputs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StateProps {
|
||||||
|
firstCellId: string;
|
||||||
|
inputValue: string;
|
||||||
|
outputDocuments: MongoDocument[];
|
||||||
|
}
|
||||||
|
interface InitialProps {
|
||||||
|
contentRef: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redux
|
||||||
|
const makeMapStateToProps = (state: AppState, initialProps: InitialProps) => {
|
||||||
|
const { contentRef } = initialProps;
|
||||||
|
const mapStateToProps = (state: AppState) => {
|
||||||
|
let firstCellId;
|
||||||
|
let inputValue = "";
|
||||||
|
let outputDocuments = [];
|
||||||
|
const content = selectors.content(state, { contentRef });
|
||||||
|
if (content?.type === "notebook") {
|
||||||
|
const cellOrder = selectors.notebook.cellOrder(content.model);
|
||||||
|
if (cellOrder.size > 0) {
|
||||||
|
firstCellId = cellOrder.first() as string;
|
||||||
|
const cell = selectors.notebook.cellById(content.model, { id: firstCellId });
|
||||||
|
|
||||||
|
// Parse to extract filter and output type
|
||||||
|
const cellValue = cell.get("source", "");
|
||||||
|
if (cellValue) {
|
||||||
|
try {
|
||||||
|
const filterValue = JSON.parse(cellValue).filter;
|
||||||
|
if (filterValue) {
|
||||||
|
inputValue = filterValue;
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error("Could not parse", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const outputs = cell.get("outputs", Immutable.List());
|
||||||
|
// Extract "application/json" mime-type
|
||||||
|
let jsonOutput: MongoKernelJsonOutput;
|
||||||
|
for (const output of outputs) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(output.data, "application/json")) {
|
||||||
|
jsonOutput = output.data["application/json"];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outputDocuments = jsonOutput?.results ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
firstCellId,
|
||||||
|
inputValue,
|
||||||
|
outputDocuments
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return mapStateToProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: MongoQueryComponentProps) => {
|
||||||
|
const mapDispatchToProps = (dispatch: Dispatch) => {
|
||||||
|
return {
|
||||||
|
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => {
|
||||||
|
return dispatch(
|
||||||
|
actions.addTransform({
|
||||||
|
mediaType: transform.MIMETYPE,
|
||||||
|
component: transform
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
runCell: (contentRef: ContentRef, cellId: string) => {
|
||||||
|
return dispatch(
|
||||||
|
actions.executeCell({
|
||||||
|
contentRef,
|
||||||
|
id: cellId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onChange: (text: string, id: string, contentRef: ContentRef) => {
|
||||||
|
dispatch(actions.updateCellSource({ id, contentRef, value: text }));
|
||||||
|
},
|
||||||
|
save: (contentRef: ContentRef) => {
|
||||||
|
dispatch(actions.save({ contentRef }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return mapDispatchToProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(makeMapStateToProps, makeMapDispatchToProps)(MongoQueryComponent);
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||||
|
import {
|
||||||
|
NotebookComponentBootstrapper,
|
||||||
|
NotebookComponentBootstrapperOptions
|
||||||
|
} from "../NotebookComponent/NotebookComponentBootstrapper";
|
||||||
|
import MongoQueryComponent from "../MongoQueryComponent/MongoQueryComponent";
|
||||||
|
import { actions, createContentRef, createKernelRef, KernelRef } from "@nteract/core";
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
|
||||||
|
export class MongoQueryComponentAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
|
||||||
|
public parameters: unknown;
|
||||||
|
private kernelRef: KernelRef;
|
||||||
|
|
||||||
|
constructor(options: NotebookComponentBootstrapperOptions, private databaseId: string, private collectionId: string) {
|
||||||
|
super(options);
|
||||||
|
|
||||||
|
if (!this.contentRef) {
|
||||||
|
this.contentRef = createContentRef();
|
||||||
|
this.kernelRef = createKernelRef();
|
||||||
|
|
||||||
|
// Request fetching notebook content
|
||||||
|
this.getStore().dispatch(
|
||||||
|
actions.fetchContent({
|
||||||
|
filepath: "mongo.ipynb",
|
||||||
|
params: {},
|
||||||
|
kernelRef: this.kernelRef,
|
||||||
|
contentRef: this.contentRef
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public renderComponent(): JSX.Element {
|
||||||
|
const props = {
|
||||||
|
contentRef: this.contentRef,
|
||||||
|
kernelRef: this.kernelRef,
|
||||||
|
databaseId: this.databaseId,
|
||||||
|
collectionId: this.collectionId
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Provider store={this.getStore()}>
|
||||||
|
<MongoQueryComponent {...props} />;
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import * as ko from "knockout";
|
|||||||
import * as PricingUtils from "../../Utils/PricingUtils";
|
import * as PricingUtils from "../../Utils/PricingUtils";
|
||||||
import * as SharedConstants from "../../Shared/Constants";
|
import * as SharedConstants from "../../Shared/Constants";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
|
import { SubscriptionType } from "../../Contracts/SubscriptionType";
|
||||||
import editable from "../../Common/EditableUtility";
|
import editable from "../../Common/EditableUtility";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
@@ -61,6 +62,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
public maxCollectionsReachedMessage: ko.Observable<string>;
|
public maxCollectionsReachedMessage: ko.Observable<string>;
|
||||||
public requestUnitsUsageCost: ko.Computed<string>;
|
public requestUnitsUsageCost: ko.Computed<string>;
|
||||||
public dedicatedRequestUnitsUsageCost: ko.Computed<string>;
|
public dedicatedRequestUnitsUsageCost: ko.Computed<string>;
|
||||||
|
public canRequestSupport: ko.PureComputed<boolean>;
|
||||||
public largePartitionKey: ko.Observable<boolean> = ko.observable<boolean>(false);
|
public largePartitionKey: ko.Observable<boolean> = ko.observable<boolean>(false);
|
||||||
public useIndexingForSharedThroughput: ko.Observable<boolean> = ko.observable<boolean>(true);
|
public useIndexingForSharedThroughput: ko.Observable<boolean> = ko.observable<boolean>(true);
|
||||||
public costsVisible: ko.PureComputed<boolean>;
|
public costsVisible: ko.PureComputed<boolean>;
|
||||||
@@ -313,6 +315,19 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.canRequestSupport = ko.pureComputed(() => {
|
||||||
|
if (
|
||||||
|
configContext.platform !== Platform.Emulator &&
|
||||||
|
!this.container.isTryCosmosDBSubscription() &&
|
||||||
|
configContext.platform !== Platform.Portal
|
||||||
|
) {
|
||||||
|
const offerThroughput: number = this._getThroughput();
|
||||||
|
return offerThroughput <= 100000;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
this.costsVisible = ko.pureComputed(() => {
|
this.costsVisible = ko.pureComputed(() => {
|
||||||
return configContext.platform !== Platform.Emulator;
|
return configContext.platform !== Platform.Emulator;
|
||||||
});
|
});
|
||||||
@@ -634,10 +649,8 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getSharedThroughputDefault(): boolean {
|
public getSharedThroughputDefault(): boolean {
|
||||||
const subscriptionType: ViewModels.SubscriptionType =
|
const subscriptionType = this.container.subscriptionType && this.container.subscriptionType();
|
||||||
this.container.subscriptionType && this.container.subscriptionType();
|
if (subscriptionType === SubscriptionType.EA || this.container.isServerlessEnabled()) {
|
||||||
|
|
||||||
if (subscriptionType === ViewModels.SubscriptionType.EA || this.container.isServerlessEnabled()) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -676,7 +689,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
databaseId: this.databaseId(),
|
databaseId: this.databaseId(),
|
||||||
rupm: this.rupm()
|
rupm: this.rupm()
|
||||||
}),
|
}),
|
||||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
||||||
@@ -779,7 +792,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
uniqueKeyPolicy,
|
uniqueKeyPolicy,
|
||||||
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
||||||
}),
|
}),
|
||||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
||||||
@@ -854,7 +867,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
uniqueKeyPolicy,
|
uniqueKeyPolicy,
|
||||||
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
||||||
}),
|
}),
|
||||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
||||||
@@ -889,7 +902,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
uniqueKeyPolicy,
|
uniqueKeyPolicy,
|
||||||
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
||||||
},
|
},
|
||||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
||||||
|
|||||||
@@ -117,6 +117,10 @@
|
|||||||
showAutoPilot: !isFreeTierAccount()
|
showAutoPilot: !isFreeTierAccount()
|
||||||
}">
|
}">
|
||||||
</throughput-input-autopilot-v3>
|
</throughput-input-autopilot-v3>
|
||||||
|
<p data-bind="visible: canRequestSupport">
|
||||||
|
<!-- TODO: Replace link with call to the Azure Support blade --><a
|
||||||
|
href="https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20More%20Throughput%20Request">Contact
|
||||||
|
support</a> for more than <span data-bind="text: maxThroughputRUText"></span> RU/s.</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- /ko -->
|
<!-- /ko -->
|
||||||
<!-- Database provisioned throughput - End -->
|
<!-- Database provisioned throughput - End -->
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import { SubscriptionType } from "../../Contracts/SubscriptionType";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import AddDatabasePane from "./AddDatabasePane";
|
import AddDatabasePane from "./AddDatabasePane";
|
||||||
import { DatabaseAccount } from "../../Contracts/DataModels";
|
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||||
@@ -44,31 +44,31 @@ describe("Add Database Pane", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should be true if subscription type is Benefits", () => {
|
it("should be true if subscription type is Benefits", () => {
|
||||||
explorer.subscriptionType(ViewModels.SubscriptionType.Benefits);
|
explorer.subscriptionType(SubscriptionType.Benefits);
|
||||||
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
||||||
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
|
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be false if subscription type is EA", () => {
|
it("should be false if subscription type is EA", () => {
|
||||||
explorer.subscriptionType(ViewModels.SubscriptionType.EA);
|
explorer.subscriptionType(SubscriptionType.EA);
|
||||||
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
||||||
expect(addDatabasePane.getSharedThroughputDefault()).toBe(false);
|
expect(addDatabasePane.getSharedThroughputDefault()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be true if subscription type is Free", () => {
|
it("should be true if subscription type is Free", () => {
|
||||||
explorer.subscriptionType(ViewModels.SubscriptionType.Free);
|
explorer.subscriptionType(SubscriptionType.Free);
|
||||||
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
||||||
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
|
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be true if subscription type is Internal", () => {
|
it("should be true if subscription type is Internal", () => {
|
||||||
explorer.subscriptionType(ViewModels.SubscriptionType.Internal);
|
explorer.subscriptionType(SubscriptionType.Internal);
|
||||||
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
||||||
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
|
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be true if subscription type is PAYG", () => {
|
it("should be true if subscription type is PAYG", () => {
|
||||||
explorer.subscriptionType(ViewModels.SubscriptionType.PAYG);
|
explorer.subscriptionType(SubscriptionType.PAYG);
|
||||||
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
||||||
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
|
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { ContextualPaneBase } from "./ContextualPaneBase";
|
|||||||
import { createDatabase } from "../../Common/dataAccess/createDatabase";
|
import { createDatabase } from "../../Common/dataAccess/createDatabase";
|
||||||
import { configContext, Platform } from "../../ConfigContext";
|
import { configContext, Platform } from "../../ConfigContext";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
|
import { SubscriptionType } from "../../Contracts/SubscriptionType";
|
||||||
|
|
||||||
export default class AddDatabasePane extends ContextualPaneBase {
|
export default class AddDatabasePane extends ContextualPaneBase {
|
||||||
public defaultExperience: ko.Computed<string>;
|
public defaultExperience: ko.Computed<string>;
|
||||||
@@ -31,6 +32,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
public throughputSpendAck: ko.Observable<boolean>;
|
public throughputSpendAck: ko.Observable<boolean>;
|
||||||
public throughputSpendAckVisible: ko.Computed<boolean>;
|
public throughputSpendAckVisible: ko.Computed<boolean>;
|
||||||
public requestUnitsUsageCost: ko.Computed<string>;
|
public requestUnitsUsageCost: ko.Computed<string>;
|
||||||
|
public canRequestSupport: ko.PureComputed<boolean>;
|
||||||
public costsVisible: ko.PureComputed<boolean>;
|
public costsVisible: ko.PureComputed<boolean>;
|
||||||
public upsellMessage: ko.PureComputed<string>;
|
public upsellMessage: ko.PureComputed<string>;
|
||||||
public upsellMessageAriaLabel: ko.PureComputed<string>;
|
public upsellMessageAriaLabel: ko.PureComputed<string>;
|
||||||
@@ -167,6 +169,19 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
return estimatedSpend;
|
return estimatedSpend;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.canRequestSupport = ko.pureComputed(() => {
|
||||||
|
if (
|
||||||
|
configContext.platform !== Platform.Emulator &&
|
||||||
|
!this.container.isTryCosmosDBSubscription() &&
|
||||||
|
configContext.platform !== Platform.Portal
|
||||||
|
) {
|
||||||
|
const offerThroughput: number = this.throughput();
|
||||||
|
return offerThroughput <= 100000;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
this.isFreeTierAccount = ko.computed<boolean>(() => {
|
this.isFreeTierAccount = ko.computed<boolean>(() => {
|
||||||
const databaseAccount = this.container && this.container.databaseAccount && this.container.databaseAccount();
|
const databaseAccount = this.container && this.container.databaseAccount && this.container.databaseAccount();
|
||||||
const isFreeTierAccount =
|
const isFreeTierAccount =
|
||||||
@@ -242,7 +257,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
const addDatabasePaneOpenMessage = {
|
const addDatabasePaneOpenMessage = {
|
||||||
databaseAccountName: this.container.databaseAccount().name,
|
databaseAccountName: this.container.databaseAccount().name,
|
||||||
defaultExperience: this.container.defaultExperience(),
|
defaultExperience: this.container.defaultExperience(),
|
||||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
throughput: this.throughput(),
|
throughput: this.throughput(),
|
||||||
@@ -270,7 +285,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
shared: this.databaseCreateNewShared()
|
shared: this.databaseCreateNewShared()
|
||||||
}),
|
}),
|
||||||
offerThroughput,
|
offerThroughput,
|
||||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
flight: this.container.flight()
|
flight: this.container.flight()
|
||||||
@@ -313,10 +328,9 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getSharedThroughputDefault(): boolean {
|
public getSharedThroughputDefault(): boolean {
|
||||||
const subscriptionType: ViewModels.SubscriptionType =
|
const subscriptionType = this.container.subscriptionType && this.container.subscriptionType();
|
||||||
this.container.subscriptionType && this.container.subscriptionType();
|
|
||||||
|
|
||||||
if (subscriptionType === ViewModels.SubscriptionType.EA || this.container.isServerlessEnabled()) {
|
if (subscriptionType === SubscriptionType.EA || this.container.isServerlessEnabled()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,7 +349,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
shared: this.databaseCreateNewShared()
|
shared: this.databaseCreateNewShared()
|
||||||
}),
|
}),
|
||||||
offerThroughput: offerThroughput,
|
offerThroughput: offerThroughput,
|
||||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
flight: this.container.flight()
|
flight: this.container.flight()
|
||||||
@@ -359,7 +373,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
shared: this.databaseCreateNewShared()
|
shared: this.databaseCreateNewShared()
|
||||||
}),
|
}),
|
||||||
offerThroughput: offerThroughput,
|
offerThroughput: offerThroughput,
|
||||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
flight: this.container.flight()
|
flight: this.container.flight()
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { ContextualPaneBase } from "./ContextualPaneBase";
|
|||||||
import { HashMap } from "../../Common/HashMap";
|
import { HashMap } from "../../Common/HashMap";
|
||||||
import { configContext, Platform } from "../../ConfigContext";
|
import { configContext, Platform } from "../../ConfigContext";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
|
import { SubscriptionType } from "../../Contracts/SubscriptionType";
|
||||||
|
|
||||||
export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||||
public createTableQuery: ko.Observable<string>;
|
public createTableQuery: ko.Observable<string>;
|
||||||
@@ -33,6 +34,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
public keyspaceThroughput: ko.Observable<number>;
|
public keyspaceThroughput: ko.Observable<number>;
|
||||||
public keyspaceCreateNew: ko.Observable<boolean>;
|
public keyspaceCreateNew: ko.Observable<boolean>;
|
||||||
public dedicateTableThroughput: ko.Observable<boolean>;
|
public dedicateTableThroughput: ko.Observable<boolean>;
|
||||||
|
public canRequestSupport: ko.PureComputed<boolean>;
|
||||||
public throughputSpendAckText: ko.Observable<string>;
|
public throughputSpendAckText: ko.Observable<string>;
|
||||||
public throughputSpendAck: ko.Observable<boolean>;
|
public throughputSpendAck: ko.Observable<boolean>;
|
||||||
public sharedThroughputSpendAck: ko.Observable<boolean>;
|
public sharedThroughputSpendAck: ko.Observable<boolean>;
|
||||||
@@ -227,6 +229,15 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
return configContext.platform !== Platform.Emulator;
|
return configContext.platform !== Platform.Emulator;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.canRequestSupport = ko.pureComputed(() => {
|
||||||
|
if (configContext.platform !== Platform.Emulator && !this.container.isTryCosmosDBSubscription()) {
|
||||||
|
const offerThroughput: number = this.throughput();
|
||||||
|
return offerThroughput <= 100000;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
this.sharedThroughputSpendAckVisible = ko.computed<boolean>(() => {
|
this.sharedThroughputSpendAckVisible = ko.computed<boolean>(() => {
|
||||||
const autoscaleThroughput = this.sharedAutoPilotThroughput() * 1;
|
const autoscaleThroughput = this.sharedAutoPilotThroughput() * 1;
|
||||||
if (this.isSharedAutoPilotSelected()) {
|
if (this.isSharedAutoPilotSelected()) {
|
||||||
@@ -304,7 +315,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
databaseId: this.keyspaceId(),
|
databaseId: this.keyspaceId(),
|
||||||
rupm: false
|
rupm: false
|
||||||
}),
|
}),
|
||||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: "u",
|
storage: "u",
|
||||||
@@ -359,7 +370,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
hasDedicatedThroughput: this.dedicateTableThroughput()
|
hasDedicatedThroughput: this.dedicateTableThroughput()
|
||||||
}),
|
}),
|
||||||
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
||||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: "u",
|
storage: "u",
|
||||||
@@ -406,7 +417,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
hasDedicatedThroughput: this.dedicateTableThroughput()
|
hasDedicatedThroughput: this.dedicateTableThroughput()
|
||||||
}),
|
}),
|
||||||
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
||||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: "u",
|
storage: "u",
|
||||||
@@ -437,7 +448,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
hasDedicatedThroughput: this.dedicateTableThroughput()
|
hasDedicatedThroughput: this.dedicateTableThroughput()
|
||||||
},
|
},
|
||||||
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
||||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: this.container.quotaId(),
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: "u",
|
storage: "u",
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ export var Int32 = {
|
|||||||
Max: 2147483647
|
Max: 2147483647
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export var Int64 = {
|
||||||
|
Min: -9223372036854775808,
|
||||||
|
Max: 9223372036854775807
|
||||||
|
};
|
||||||
|
|
||||||
var yearMonthDay = "\\d{4}[- ][01]\\d[- ][0-3]\\d";
|
var yearMonthDay = "\\d{4}[- ][01]\\d[- ][0-3]\\d";
|
||||||
var timeOfDay = "T[0-2]\\d:[0-5]\\d(:[0-5]\\d(\\.\\d+)?)?";
|
var timeOfDay = "T[0-2]\\d:[0-5]\\d(:[0-5]\\d(\\.\\d+)?)?";
|
||||||
var timeZone = "Z|[+-][0-2]\\d:[0-5]\\d";
|
var timeZone = "Z|[+-][0-2]\\d:[0-5]\\d";
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
class: 'scaleForm dirty',
|
class: 'scaleForm dirty',
|
||||||
value: throughput,
|
value: throughput,
|
||||||
minimum: minRUs,
|
minimum: minRUs,
|
||||||
|
maximum: maxRUThroughputInputLimit,
|
||||||
canExceedMaximumValue: canThroughputExceedMaximumValue,
|
canExceedMaximumValue: canThroughputExceedMaximumValue,
|
||||||
step: throughputIncreaseFactor,
|
step: throughputIncreaseFactor,
|
||||||
label: throughputTitle,
|
label: throughputTitle,
|
||||||
@@ -55,6 +56,16 @@
|
|||||||
<span>Learn more about minimum throughput </span>
|
<span>Learn more about minimum throughput </span>
|
||||||
<a href="https://docs.microsoft.com/azure/cosmos-db/set-throughput" target="_blank">here.</a>
|
<a href="https://docs.microsoft.com/azure/cosmos-db/set-throughput" target="_blank">here.</a>
|
||||||
</p>
|
</p>
|
||||||
|
<p data-bind="visible: canRequestSupport">
|
||||||
|
<!-- TODO: Replace link with call to the Azure Support blade -->
|
||||||
|
<a href="https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20More%20Throughput%20Request"
|
||||||
|
>Contact support</a
|
||||||
|
>
|
||||||
|
for more than <span data-bind="text: maxRUsText"></span> RU/s
|
||||||
|
</p>
|
||||||
|
<p data-bind="visible: shouldDisplayPortalUsePrompt">
|
||||||
|
Use Data Explorer from Azure Portal to request more than <span data-bind="text: maxRUsText"></span> RU/s
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import Explorer from "../Explorer";
|
|||||||
import { updateOffer } from "../../Common/dataAccess/updateOffer";
|
import { updateOffer } from "../../Common/dataAccess/updateOffer";
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
import { updateOfferThroughputBeyondLimit } from "../../Common/dataAccess/updateOfferThroughputBeyondLimit";
|
||||||
import { configContext, Platform } from "../../ConfigContext";
|
import { configContext, Platform } from "../../ConfigContext";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
|
|
||||||
@@ -58,16 +59,23 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
|
|
||||||
public saveSettingsButton: ViewModels.Button;
|
public saveSettingsButton: ViewModels.Button;
|
||||||
public discardSettingsChangesButton: ViewModels.Button;
|
public discardSettingsChangesButton: ViewModels.Button;
|
||||||
|
|
||||||
|
public canRequestSupport: ko.PureComputed<boolean>;
|
||||||
|
public canThroughputExceedMaximumValue: ko.Computed<boolean>;
|
||||||
public costsVisible: ko.Computed<boolean>;
|
public costsVisible: ko.Computed<boolean>;
|
||||||
public displayedError: ko.Observable<string>;
|
public displayedError: ko.Observable<string>;
|
||||||
public isTemplateReady: ko.Observable<boolean>;
|
public isTemplateReady: ko.Observable<boolean>;
|
||||||
public minRUAnotationVisible: ko.Computed<boolean>;
|
public minRUAnotationVisible: ko.Computed<boolean>;
|
||||||
public minRUs: ko.Computed<number>;
|
public minRUs: ko.Computed<number>;
|
||||||
|
public maxRUs: ko.Computed<number>;
|
||||||
|
public maxRUsText: ko.PureComputed<string>;
|
||||||
|
public maxRUThroughputInputLimit: ko.Computed<number>;
|
||||||
public notificationStatusInfo: ko.Observable<string>;
|
public notificationStatusInfo: ko.Observable<string>;
|
||||||
public pendingNotification: ko.Observable<DataModels.Notification>;
|
public pendingNotification: ko.Observable<DataModels.Notification>;
|
||||||
public requestUnitsUsageCost: ko.PureComputed<string>;
|
public requestUnitsUsageCost: ko.PureComputed<string>;
|
||||||
public autoscaleCost: ko.PureComputed<string>;
|
public autoscaleCost: ko.PureComputed<string>;
|
||||||
public shouldShowNotificationStatusPrompt: ko.Computed<boolean>;
|
public shouldShowNotificationStatusPrompt: ko.Computed<boolean>;
|
||||||
|
public shouldDisplayPortalUsePrompt: ko.Computed<boolean>;
|
||||||
public shouldShowStatusBar: ko.Computed<boolean>;
|
public shouldShowStatusBar: ko.Computed<boolean>;
|
||||||
public throughputTitle: ko.PureComputed<string>;
|
public throughputTitle: ko.PureComputed<string>;
|
||||||
public throughputAriaLabel: ko.PureComputed<string>;
|
public throughputAriaLabel: ko.PureComputed<string>;
|
||||||
@@ -173,6 +181,22 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
return configContext.platform !== Platform.Emulator;
|
return configContext.platform !== Platform.Emulator;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.shouldDisplayPortalUsePrompt = ko.pureComputed<boolean>(() => configContext.platform === Platform.Hosted);
|
||||||
|
this.canThroughputExceedMaximumValue = ko.pureComputed<boolean>(
|
||||||
|
() => configContext.platform === Platform.Portal && !this.container.isRunningOnNationalCloud()
|
||||||
|
);
|
||||||
|
this.canRequestSupport = ko.pureComputed(() => {
|
||||||
|
if (
|
||||||
|
configContext.platform === Platform.Emulator ||
|
||||||
|
configContext.platform === Platform.Hosted ||
|
||||||
|
this.canThroughputExceedMaximumValue()
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
this.overrideWithAutoPilotSettings = ko.pureComputed(() => {
|
this.overrideWithAutoPilotSettings = ko.pureComputed(() => {
|
||||||
return this._hasProvisioningTypeChanged() && this._wasAutopilotOriginallySet();
|
return this._hasProvisioningTypeChanged() && this._wasAutopilotOriginallySet();
|
||||||
});
|
});
|
||||||
@@ -205,6 +229,34 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
return PricingUtils.isLargerThanDefaultMinRU(this.minRUs());
|
return PricingUtils.isLargerThanDefaultMinRU(this.minRUs());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.maxRUs = ko.computed<number>(() => {
|
||||||
|
const collectionThroughputInfo: DataModels.OfferThroughputInfo =
|
||||||
|
this.database &&
|
||||||
|
this.database.offer &&
|
||||||
|
this.database.offer() &&
|
||||||
|
this.database.offer().content &&
|
||||||
|
this.database.offer().content.collectionThroughputInfo;
|
||||||
|
const numPartitions = collectionThroughputInfo && collectionThroughputInfo.numPhysicalPartitions;
|
||||||
|
if (!!numPartitions) {
|
||||||
|
return SharedConstants.CollectionCreation.MaxRUPerPartition * numPartitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const throughputDefaults = this.container.collectionCreationDefaults.throughput;
|
||||||
|
return throughputDefaults.unlimitedmax;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.maxRUThroughputInputLimit = ko.pureComputed<number>(() => {
|
||||||
|
if (configContext.platform === Platform.Hosted) {
|
||||||
|
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.maxRUs();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.maxRUsText = ko.pureComputed(() => {
|
||||||
|
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million.toLocaleString();
|
||||||
|
});
|
||||||
|
|
||||||
this.throughputTitle = ko.pureComputed<string>(() => {
|
this.throughputTitle = ko.pureComputed<string>(() => {
|
||||||
if (this.isAutoPilotSelected()) {
|
if (this.isAutoPilotSelected()) {
|
||||||
return AutoPilotUtils.getAutoPilotHeaderText();
|
return AutoPilotUtils.getAutoPilotHeaderText();
|
||||||
@@ -246,10 +298,18 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
return throughputApplyShortDelayMessage(this.isAutoPilotSelected(), throughput, this.database.id());
|
return throughputApplyShortDelayMessage(this.isAutoPilotSelected(), throughput, this.database.id());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million) {
|
if (
|
||||||
|
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||||
|
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||||
|
this.canThroughputExceedMaximumValue()
|
||||||
|
) {
|
||||||
return updateThroughputBeyondLimitWarningMessage;
|
return updateThroughputBeyondLimitWarningMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.throughput() > this.maxRUs()) {
|
||||||
|
return updateThroughputDelayedApplyWarningMessage;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.pendingNotification()) {
|
if (this.pendingNotification()) {
|
||||||
const matches: string[] = this.pendingNotification().description.match("Throughput update for (.*) RU/s");
|
const matches: string[] = this.pendingNotification().description.match("Throughput update for (.*) RU/s");
|
||||||
const throughput: number = matches.length > 1 && Number(matches[1]);
|
const throughput: number = matches.length > 1 && Number(matches[1]);
|
||||||
@@ -308,6 +368,13 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!this.canThroughputExceedMaximumValue() &&
|
||||||
|
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.throughput.editableIsDirty()) {
|
if (this.throughput.editableIsDirty()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -383,18 +450,40 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
const originalThroughputValue = this.throughput.getEditableOriginalValue();
|
const originalThroughputValue = this.throughput.getEditableOriginalValue();
|
||||||
const newThroughput = this.throughput();
|
const newThroughput = this.throughput();
|
||||||
|
|
||||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
if (
|
||||||
databaseId: this.database.id(),
|
this.canThroughputExceedMaximumValue() &&
|
||||||
currentOffer: this.database.offer(),
|
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||||
autopilotThroughput: undefined,
|
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million
|
||||||
manualThroughput: newThroughput,
|
) {
|
||||||
migrateToManual: this._hasProvisioningTypeChanged()
|
const requestPayload = {
|
||||||
};
|
subscriptionId: userContext.subscriptionId,
|
||||||
|
databaseAccountName: userContext.databaseAccount.name,
|
||||||
|
resourceGroup: userContext.resourceGroup,
|
||||||
|
databaseName: this.database.id(),
|
||||||
|
throughput: newThroughput,
|
||||||
|
offerIsRUPerMinuteThroughputEnabled: false
|
||||||
|
};
|
||||||
|
await updateOfferThroughputBeyondLimit(requestPayload);
|
||||||
|
this.database.offer().content.offerThroughput = originalThroughputValue;
|
||||||
|
this.throughput(originalThroughputValue);
|
||||||
|
this.notificationStatusInfo(
|
||||||
|
throughputApplyDelayedMessage(this.isAutoPilotSelected(), newThroughput, this.database.id())
|
||||||
|
);
|
||||||
|
this.throughput.valueHasMutated(); // force component re-render
|
||||||
|
} else {
|
||||||
|
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||||
|
databaseId: this.database.id(),
|
||||||
|
currentOffer: this.database.offer(),
|
||||||
|
autopilotThroughput: undefined,
|
||||||
|
manualThroughput: newThroughput,
|
||||||
|
migrateToManual: this._hasProvisioningTypeChanged()
|
||||||
|
};
|
||||||
|
|
||||||
const updatedOffer = await updateOffer(updateOfferParams);
|
const updatedOffer = await updateOffer(updateOfferParams);
|
||||||
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
||||||
this.database.offer(updatedOffer);
|
this.database.offer(updatedOffer);
|
||||||
this.database.offer.valueHasMutated();
|
this.database.offer.valueHasMutated();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
1
src/Explorer/Tabs/MongoDocumentsTabV2.html
Normal file
1
src/Explorer/Tabs/MongoDocumentsTabV2.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<div data-bind="react:mongoQueryComponentAdapter" style="height: 100%"></div>
|
||||||
45
src/Explorer/Tabs/MongoDocumentsTabV2.ts
Normal file
45
src/Explorer/Tabs/MongoDocumentsTabV2.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import * as Q from "q";
|
||||||
|
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
|
||||||
|
import { MongoQueryComponentAdapter } from "../Notebook/MongoQueryComponent/MongoQueryComponentAdapter";
|
||||||
|
|
||||||
|
export default class MongoDocumentsTabV2 extends NotebookTabBase {
|
||||||
|
private mongoQueryComponentAdapter: MongoQueryComponentAdapter;
|
||||||
|
|
||||||
|
constructor(options: NotebookTabBaseOptions) {
|
||||||
|
super(options);
|
||||||
|
this.mongoQueryComponentAdapter = new MongoQueryComponentAdapter({
|
||||||
|
contentRef: undefined,
|
||||||
|
notebookClient: NotebookTabBase.clientManager
|
||||||
|
}, options.collection?.databaseId, options.collection?.id());
|
||||||
|
}
|
||||||
|
|
||||||
|
public onCloseTabButtonClick(): Q.Promise<void> {
|
||||||
|
super.onCloseTabButtonClick();
|
||||||
|
|
||||||
|
// const cleanup = () => {
|
||||||
|
// this.notebookComponentAdapter.notebookShutdown();
|
||||||
|
// this.isActive(false);
|
||||||
|
// super.onCloseTabButtonClick();
|
||||||
|
// };
|
||||||
|
|
||||||
|
// if (this.notebookComponentAdapter.isContentDirty()) {
|
||||||
|
// this.container.showOkCancelModalDialog(
|
||||||
|
// "Close without saving?",
|
||||||
|
// `File has unsaved changes, close without saving?`,
|
||||||
|
// "Close",
|
||||||
|
// cleanup,
|
||||||
|
// "Cancel",
|
||||||
|
// undefined
|
||||||
|
// );
|
||||||
|
// return Q.resolve(null);
|
||||||
|
// } else {
|
||||||
|
// cleanup();
|
||||||
|
// return Q.resolve(null);
|
||||||
|
// }
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected buildCommandBarOptions(): void {
|
||||||
|
this.updateNavbarWithTabsButtons();
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/Explorer/Tabs/NotebookTabBase.ts
Normal file
50
src/Explorer/Tabs/NotebookTabBase.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
|
import TabsBase from "./TabsBase";
|
||||||
|
|
||||||
|
import { NotebookClientV2 } from "../Notebook/NotebookClientV2";
|
||||||
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import Explorer from "../Explorer";
|
||||||
|
import { ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
import { Areas } from "../../Common/Constants";
|
||||||
|
|
||||||
|
export interface NotebookTabBaseOptions extends ViewModels.TabOptions {
|
||||||
|
container: Explorer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every notebook-based tab inherits from this class. It holds the static reference to a notebook client (singleton)
|
||||||
|
*/
|
||||||
|
export default class NotebookTabBase extends TabsBase {
|
||||||
|
protected static clientManager: NotebookClientV2;
|
||||||
|
protected container: Explorer;
|
||||||
|
|
||||||
|
constructor(options: NotebookTabBaseOptions) {
|
||||||
|
super(options);
|
||||||
|
|
||||||
|
this.container = options.container;
|
||||||
|
|
||||||
|
if (!NotebookTabBase.clientManager) {
|
||||||
|
NotebookTabBase.clientManager = new NotebookClientV2({
|
||||||
|
connectionInfo: this.container.notebookServerInfo(),
|
||||||
|
databaseAccountName: this.container.databaseAccount().name,
|
||||||
|
defaultExperience: this.container.defaultExperience(),
|
||||||
|
contentProvider: this.container.notebookManager?.notebookContentProvider
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override base behavior
|
||||||
|
*/
|
||||||
|
protected getContainer(): Explorer {
|
||||||
|
return this.container;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected traceTelemetry(actionType: number): void {
|
||||||
|
TelemetryProcessor.trace(actionType, ActionModifiers.Mark, {
|
||||||
|
databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name,
|
||||||
|
defaultExperience: this.container.defaultExperience && this.container.defaultExperience(),
|
||||||
|
dataExplorerArea: Areas.Notebook
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,7 @@ import * as _ from "underscore";
|
|||||||
import * as Q from "q";
|
import * as Q from "q";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
|
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import TabsBase from "./TabsBase";
|
|
||||||
|
|
||||||
import NewCellIcon from "../../../images/notebook/Notebook-insert-cell.svg";
|
import NewCellIcon from "../../../images/notebook/Notebook-insert-cell.svg";
|
||||||
import CutIcon from "../../../images/notebook/Notebook-cut.svg";
|
import CutIcon from "../../../images/notebook/Notebook-cut.svg";
|
||||||
@@ -17,31 +15,25 @@ import SaveIcon from "../../../images/save-cosmos.svg";
|
|||||||
import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg";
|
import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg";
|
||||||
import InterruptKernelIcon from "../../../images/notebook/Notebook-stop.svg";
|
import InterruptKernelIcon from "../../../images/notebook/Notebook-stop.svg";
|
||||||
import KillKernelIcon from "../../../images/notebook/Notebook-stop.svg";
|
import KillKernelIcon from "../../../images/notebook/Notebook-stop.svg";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { ArmApiVersions } from "../../Common/Constants";
|
||||||
import { Areas, ArmApiVersions } from "../../Common/Constants";
|
|
||||||
import { CommandBarComponentButtonFactory } from "../Menus/CommandBar/CommandBarComponentButtonFactory";
|
import { CommandBarComponentButtonFactory } from "../Menus/CommandBar/CommandBarComponentButtonFactory";
|
||||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||||
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
|
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
|
||||||
import { NotebookConfigurationUtils } from "../../Utils/NotebookConfigurationUtils";
|
import { NotebookConfigurationUtils } from "../../Utils/NotebookConfigurationUtils";
|
||||||
import { KernelSpecsDisplay, NotebookClientV2 } from "../Notebook/NotebookClientV2";
|
import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2";
|
||||||
import { configContext } from "../../ConfigContext";
|
import { configContext } from "../../ConfigContext";
|
||||||
import Explorer from "../Explorer";
|
|
||||||
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { toJS, stringifyNotebook } from "@nteract/commutable";
|
import { toJS, stringifyNotebook } from "@nteract/commutable";
|
||||||
|
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
|
||||||
|
|
||||||
export interface NotebookTabOptions extends ViewModels.TabOptions {
|
export interface NotebookTabOptions extends NotebookTabBaseOptions {
|
||||||
account: DataModels.DatabaseAccount;
|
|
||||||
masterKey: string;
|
|
||||||
container: Explorer;
|
|
||||||
notebookContentItem: NotebookContentItem;
|
notebookContentItem: NotebookContentItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class NotebookTabV2 extends TabsBase {
|
export default class NotebookTabV2 extends NotebookTabBase {
|
||||||
private static clientManager: NotebookClientV2;
|
|
||||||
private container: Explorer;
|
|
||||||
public notebookPath: ko.Observable<string>;
|
public notebookPath: ko.Observable<string>;
|
||||||
private selectedSparkPool: ko.Observable<string>;
|
private selectedSparkPool: ko.Observable<string>;
|
||||||
private notebookComponentAdapter: NotebookComponentAdapter;
|
private notebookComponentAdapter: NotebookComponentAdapter;
|
||||||
@@ -50,16 +42,6 @@ export default class NotebookTabV2 extends TabsBase {
|
|||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.container = options.container;
|
this.container = options.container;
|
||||||
|
|
||||||
if (!NotebookTabV2.clientManager) {
|
|
||||||
NotebookTabV2.clientManager = new NotebookClientV2({
|
|
||||||
connectionInfo: this.container.notebookServerInfo(),
|
|
||||||
databaseAccountName: this.container.databaseAccount().name,
|
|
||||||
defaultExperience: this.container.defaultExperience(),
|
|
||||||
contentProvider: this.container.notebookManager?.notebookContentProvider
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.notebookPath = ko.observable(options.notebookContentItem.path);
|
this.notebookPath = ko.observable(options.notebookContentItem.path);
|
||||||
|
|
||||||
this.container.notebookServerInfo.subscribe((newValue: DataModels.NotebookWorkspaceConnectionInfo) => {
|
this.container.notebookServerInfo.subscribe((newValue: DataModels.NotebookWorkspaceConnectionInfo) => {
|
||||||
@@ -69,7 +51,7 @@ export default class NotebookTabV2 extends TabsBase {
|
|||||||
this.notebookComponentAdapter = new NotebookComponentAdapter({
|
this.notebookComponentAdapter = new NotebookComponentAdapter({
|
||||||
contentItem: options.notebookContentItem,
|
contentItem: options.notebookContentItem,
|
||||||
notebooksBasePath: this.container.getNotebookBasePath(),
|
notebooksBasePath: this.container.getNotebookBasePath(),
|
||||||
notebookClient: NotebookTabV2.clientManager,
|
notebookClient: NotebookTabBase.clientManager,
|
||||||
onUpdateKernelInfo: this.onKernelUpdate
|
onUpdateKernelInfo: this.onKernelUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -115,10 +97,6 @@ export default class NotebookTabV2 extends TabsBase {
|
|||||||
return await this.configureServiceEndpoints(this.notebookComponentAdapter.getCurrentKernelName());
|
return await this.configureServiceEndpoints(this.notebookComponentAdapter.getCurrentKernelName());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getContainer(): Explorer {
|
|
||||||
return this.container;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||||
const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs();
|
const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs();
|
||||||
|
|
||||||
@@ -493,12 +471,4 @@ export default class NotebookTabV2 extends TabsBase {
|
|||||||
|
|
||||||
this.container.copyNotebook(notebookContent.name, content);
|
this.container.copyNotebook(notebookContent.name, content);
|
||||||
};
|
};
|
||||||
|
|
||||||
private traceTelemetry(actionType: number) {
|
|
||||||
TelemetryProcessor.trace(actionType, ActionModifiers.Mark, {
|
|
||||||
databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name,
|
|
||||||
defaultExperience: this.container.defaultExperience && this.container.defaultExperience(),
|
|
||||||
dataExplorerArea: Areas.Notebook
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,9 @@
|
|||||||
class: 'scaleForm dirty',
|
class: 'scaleForm dirty',
|
||||||
value: throughput,
|
value: throughput,
|
||||||
minimum: minRUs,
|
minimum: minRUs,
|
||||||
|
maximum: maxRUThroughputInputLimit,
|
||||||
isEnabled: !hasDatabaseSharedThroughput(),
|
isEnabled: !hasDatabaseSharedThroughput(),
|
||||||
|
canExceedMaximumValue: canThroughputExceedMaximumValue,
|
||||||
label: throughputTitle,
|
label: throughputTitle,
|
||||||
ariaLabel: throughputAriaLabel,
|
ariaLabel: throughputAriaLabel,
|
||||||
costsVisible: costsVisible,
|
costsVisible: costsVisible,
|
||||||
|
|||||||
@@ -34,6 +34,17 @@ describe("Settings tab", () => {
|
|||||||
collections: [baseCollection]
|
collections: [baseCollection]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const quotaInfo: DataModels.CollectionQuotaInfo = {
|
||||||
|
storedProcedures: 0,
|
||||||
|
triggers: 0,
|
||||||
|
functions: 0,
|
||||||
|
documentsSize: 0,
|
||||||
|
documentsCount: 0,
|
||||||
|
collectionSize: 0,
|
||||||
|
usageSizeInKB: 0,
|
||||||
|
numPartitions: 0
|
||||||
|
};
|
||||||
|
|
||||||
describe("Conflict Resolution", () => {
|
describe("Conflict Resolution", () => {
|
||||||
describe("should show conflict resolution", () => {
|
describe("should show conflict resolution", () => {
|
||||||
let explorer: Explorer;
|
let explorer: Explorer;
|
||||||
@@ -59,6 +70,7 @@ describe("Settings tab", () => {
|
|||||||
explorer,
|
explorer,
|
||||||
"mydb",
|
"mydb",
|
||||||
conflictResolution ? baseCollection : baseCollectionWithoutConflict,
|
conflictResolution ? baseCollection : baseCollectionWithoutConflict,
|
||||||
|
quotaInfo,
|
||||||
null
|
null
|
||||||
),
|
),
|
||||||
onUpdateTabsButtons: undefined
|
onUpdateTabsButtons: undefined
|
||||||
@@ -174,7 +186,7 @@ describe("Settings tab", () => {
|
|||||||
tabPath: "",
|
tabPath: "",
|
||||||
hashLocation: "",
|
hashLocation: "",
|
||||||
isActive: ko.observable(false),
|
isActive: ko.observable(false),
|
||||||
collection: new Collection(explorer, "mydb", baseCollection, null),
|
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
||||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -195,7 +207,7 @@ describe("Settings tab", () => {
|
|||||||
tabPath: "",
|
tabPath: "",
|
||||||
hashLocation: "",
|
hashLocation: "",
|
||||||
isActive: ko.observable(false),
|
isActive: ko.observable(false),
|
||||||
collection: new Collection(explorer, "mydb", baseCollection, null),
|
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
||||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -211,7 +223,7 @@ describe("Settings tab", () => {
|
|||||||
tabPath: "",
|
tabPath: "",
|
||||||
hashLocation: "",
|
hashLocation: "",
|
||||||
isActive: ko.observable(false),
|
isActive: ko.observable(false),
|
||||||
collection: new Collection(explorer, "mydb", baseCollection, null),
|
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
||||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -246,7 +258,7 @@ describe("Settings tab", () => {
|
|||||||
tabPath: "",
|
tabPath: "",
|
||||||
hashLocation: "",
|
hashLocation: "",
|
||||||
isActive: ko.observable(false),
|
isActive: ko.observable(false),
|
||||||
collection: new Collection(explorer, "mydb", baseCollection, null),
|
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
||||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -260,7 +272,7 @@ describe("Settings tab", () => {
|
|||||||
tabPath: "",
|
tabPath: "",
|
||||||
hashLocation: "",
|
hashLocation: "",
|
||||||
isActive: ko.observable(false),
|
isActive: ko.observable(false),
|
||||||
collection: new Collection(explorer, "mydb", baseCollection, null),
|
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
||||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -283,7 +295,7 @@ describe("Settings tab", () => {
|
|||||||
tabPath: "",
|
tabPath: "",
|
||||||
hashLocation: "",
|
hashLocation: "",
|
||||||
isActive: ko.observable(false),
|
isActive: ko.observable(false),
|
||||||
collection: new Collection(explorer, "mydb", baseCollection, null),
|
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
||||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -342,6 +354,7 @@ describe("Settings tab", () => {
|
|||||||
_ts: 0,
|
_ts: 0,
|
||||||
id: "mycoll"
|
id: "mycoll"
|
||||||
},
|
},
|
||||||
|
quotaInfo,
|
||||||
offer
|
offer
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { updateOffer } from "../../Common/dataAccess/updateOffer";
|
|||||||
import { updateCollection } from "../../Common/dataAccess/updateCollection";
|
import { updateCollection } from "../../Common/dataAccess/updateCollection";
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
import { updateOfferThroughputBeyondLimit } from "../../Common/dataAccess/updateOfferThroughputBeyondLimit";
|
||||||
import { configContext, Platform } from "../../ConfigContext";
|
import { configContext, Platform } from "../../ConfigContext";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
|
|
||||||
@@ -150,6 +151,9 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
|
|
||||||
public saveSettingsButton: ViewModels.Button;
|
public saveSettingsButton: ViewModels.Button;
|
||||||
public discardSettingsChangesButton: ViewModels.Button;
|
public discardSettingsChangesButton: ViewModels.Button;
|
||||||
|
|
||||||
|
public canRequestSupport: ko.Computed<boolean>;
|
||||||
|
public canThroughputExceedMaximumValue: ko.Computed<boolean>;
|
||||||
public changeFeedPolicyOffId: string;
|
public changeFeedPolicyOffId: string;
|
||||||
public changeFeedPolicyOnId: string;
|
public changeFeedPolicyOnId: string;
|
||||||
public changeFeedPolicyToggled: ViewModels.Editable<ChangeFeedPolicyToggledState>;
|
public changeFeedPolicyToggled: ViewModels.Editable<ChangeFeedPolicyToggledState>;
|
||||||
@@ -170,6 +174,9 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
public indexingPolicyElementFocused: ko.Observable<boolean>;
|
public indexingPolicyElementFocused: ko.Observable<boolean>;
|
||||||
public minRUs: ko.Computed<number>;
|
public minRUs: ko.Computed<number>;
|
||||||
public minRUAnotationVisible: ko.Computed<boolean>;
|
public minRUAnotationVisible: ko.Computed<boolean>;
|
||||||
|
public maxRUs: ko.Computed<number>;
|
||||||
|
public maxRUThroughputInputLimit: ko.Computed<number>;
|
||||||
|
public maxRUsText: ko.PureComputed<string>;
|
||||||
public notificationStatusInfo: ko.Observable<string>;
|
public notificationStatusInfo: ko.Observable<string>;
|
||||||
public partitionKeyName: ko.Computed<string>;
|
public partitionKeyName: ko.Computed<string>;
|
||||||
public partitionKeyVisible: ko.PureComputed<boolean>;
|
public partitionKeyVisible: ko.PureComputed<boolean>;
|
||||||
@@ -181,6 +188,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
public rupmVisible: ko.Computed<boolean>;
|
public rupmVisible: ko.Computed<boolean>;
|
||||||
public scaleExpanded: ko.Observable<boolean>;
|
public scaleExpanded: ko.Observable<boolean>;
|
||||||
public settingsExpanded: ko.Observable<boolean>;
|
public settingsExpanded: ko.Observable<boolean>;
|
||||||
|
public shouldDisplayPortalUsePrompt: ko.Computed<boolean>;
|
||||||
public shouldShowIndexingPolicyEditor: ko.Computed<boolean>;
|
public shouldShowIndexingPolicyEditor: ko.Computed<boolean>;
|
||||||
public shouldShowNotificationStatusPrompt: ko.Computed<boolean>;
|
public shouldShowNotificationStatusPrompt: ko.Computed<boolean>;
|
||||||
public shouldShowStatusBar: ko.Computed<boolean>;
|
public shouldShowStatusBar: ko.Computed<boolean>;
|
||||||
@@ -453,6 +461,43 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
return (this.container && this.container.isTryCosmosDBSubscription()) || false;
|
return (this.container && this.container.isTryCosmosDBSubscription()) || false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.canThroughputExceedMaximumValue = ko.pureComputed<boolean>(() => {
|
||||||
|
return (
|
||||||
|
this._isFixedContainer() &&
|
||||||
|
configContext.platform === Platform.Portal &&
|
||||||
|
!this.container.isRunningOnNationalCloud()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.canRequestSupport = ko.pureComputed(() => {
|
||||||
|
if (configContext.platform === Platform.Emulator) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isTryCosmosDBSubscription()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.canThroughputExceedMaximumValue()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configContext.platform === Platform.Hosted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.container.isServerlessEnabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numPartitions = this.collection.quotaInfo().numPartitions;
|
||||||
|
return !!this.collection.partitionKeyProperty || numPartitions > 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.shouldDisplayPortalUsePrompt = ko.pureComputed<boolean>(
|
||||||
|
() => configContext.platform === Platform.Hosted && !!this.collection.partitionKey
|
||||||
|
);
|
||||||
|
|
||||||
this.minRUs = ko.computed<number>(() => {
|
this.minRUs = ko.computed<number>(() => {
|
||||||
if (this.isTryCosmosDBSubscription() || this.container.isServerlessEnabled()) {
|
if (this.isTryCosmosDBSubscription() || this.container.isServerlessEnabled()) {
|
||||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||||
@@ -462,7 +507,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
this.collection && this.collection.offer && this.collection.offer() && this.collection.offer().content;
|
this.collection && this.collection.offer && this.collection.offer() && this.collection.offer().content;
|
||||||
|
|
||||||
if (offerContent && offerContent.offerAutopilotSettings) {
|
if (offerContent && offerContent.offerAutopilotSettings) {
|
||||||
SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
return 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
const collectionThroughputInfo: DataModels.OfferThroughputInfo =
|
const collectionThroughputInfo: DataModels.OfferThroughputInfo =
|
||||||
@@ -476,21 +521,72 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
return collectionThroughputInfo.minimumRUForCollection;
|
return collectionThroughputInfo.minimumRUForCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
// minimumRUForCollection should always be present, but just in case return a default
|
const numPartitions =
|
||||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
(collectionThroughputInfo && collectionThroughputInfo.numPhysicalPartitions) ||
|
||||||
|
this.collection.quotaInfo().numPartitions;
|
||||||
|
|
||||||
|
if (!numPartitions || numPartitions === 1) {
|
||||||
|
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseRU = SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||||
|
|
||||||
|
const quotaInKb = this.collection.quotaInfo().collectionSize;
|
||||||
|
const quotaInGb = PricingUtils.usageInGB(quotaInKb);
|
||||||
|
|
||||||
|
const perPartitionGBQuota: number = Math.max(10, quotaInGb / numPartitions);
|
||||||
|
const baseRUbyPartitions: number = ((numPartitions * perPartitionGBQuota) / 10) * 100;
|
||||||
|
|
||||||
|
return Math.max(baseRU, baseRUbyPartitions);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.minRUAnotationVisible = ko.computed<boolean>(() => {
|
this.minRUAnotationVisible = ko.computed<boolean>(() => {
|
||||||
return PricingUtils.isLargerThanDefaultMinRU(this.minRUs());
|
return PricingUtils.isLargerThanDefaultMinRU(this.minRUs());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.maxRUs = ko.computed<number>(() => {
|
||||||
|
const isTryCosmosDBSubscription = this.isTryCosmosDBSubscription();
|
||||||
|
if (isTryCosmosDBSubscription || this.container.isServerlessEnabled()) {
|
||||||
|
return Constants.TryCosmosExperience.maxRU;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numPartitionsFromOffer: number =
|
||||||
|
this.collection &&
|
||||||
|
this.collection.offer &&
|
||||||
|
this.collection.offer() &&
|
||||||
|
this.collection.offer().content &&
|
||||||
|
this.collection.offer().content.collectionThroughputInfo &&
|
||||||
|
this.collection.offer().content.collectionThroughputInfo.numPhysicalPartitions;
|
||||||
|
|
||||||
|
const numPartitionsFromQuotaInfo: number = this.collection && this.collection.quotaInfo().numPartitions;
|
||||||
|
|
||||||
|
const numPartitions = numPartitionsFromOffer || numPartitionsFromQuotaInfo || 1;
|
||||||
|
|
||||||
|
return SharedConstants.CollectionCreation.MaxRUPerPartition * numPartitions;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.maxRUThroughputInputLimit = ko.pureComputed<number>(() => {
|
||||||
|
if (configContext.platform === Platform.Hosted && this.collection.partitionKey) {
|
||||||
|
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.maxRUs();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.maxRUsText = ko.pureComputed(() => {
|
||||||
|
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million.toLocaleString();
|
||||||
|
});
|
||||||
|
|
||||||
this.throughputTitle = ko.pureComputed<string>(() => {
|
this.throughputTitle = ko.pureComputed<string>(() => {
|
||||||
if (this.isAutoPilotSelected()) {
|
if (this.isAutoPilotSelected()) {
|
||||||
return AutoPilotUtils.getAutoPilotHeaderText();
|
return AutoPilotUtils.getAutoPilotHeaderText();
|
||||||
}
|
}
|
||||||
|
|
||||||
const minThroughput: string = this.minRUs().toLocaleString();
|
const minThroughput: string = this.minRUs().toLocaleString();
|
||||||
const maxThroughput: string = !this._isFixedContainer() ? "unlimited" : "10000";
|
const maxThroughput: string =
|
||||||
|
this.canThroughputExceedMaximumValue() && !this._isFixedContainer()
|
||||||
|
? "unlimited"
|
||||||
|
: this.maxRUs().toLocaleString();
|
||||||
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`;
|
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -579,6 +675,22 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isThroughputGreaterThanMaxRus = this.throughput() > this.maxRUs();
|
||||||
|
const isEmulator = configContext.platform === Platform.Emulator;
|
||||||
|
if (isThroughputGreaterThanMaxRus && isEmulator) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isThroughputGreaterThanMaxRus && this._isFixedContainer()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isThroughputMoreThan1Million =
|
||||||
|
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||||
|
if (!this.canThroughputExceedMaximumValue() && isThroughputMoreThan1Million) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.throughput.editableIsDirty()) {
|
if (this.throughput.editableIsDirty()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -602,6 +714,14 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.rupm() === Constants.RUPMStates.on &&
|
||||||
|
this.throughput() >
|
||||||
|
SharedConstants.CollectionCreation.MaxRUPMPerPartition * this.collection.quotaInfo()?.numPartitions
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.timeToLive.editableIsDirty()) {
|
if (this.timeToLive.editableIsDirty()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -721,6 +841,14 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
this.shouldShowNotificationStatusPrompt = ko.computed<boolean>(() => this.notificationStatusInfo().length > 0);
|
this.shouldShowNotificationStatusPrompt = ko.computed<boolean>(() => this.notificationStatusInfo().length > 0);
|
||||||
|
|
||||||
this.warningMessage = ko.computed<string>(() => {
|
this.warningMessage = ko.computed<string>(() => {
|
||||||
|
const throughputExceedsBackendLimits: boolean =
|
||||||
|
this.canThroughputExceedMaximumValue() &&
|
||||||
|
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||||
|
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||||
|
|
||||||
|
const throughputExceedsMaxValue: boolean =
|
||||||
|
configContext.platform !== Platform.Emulator && this.throughput() > this.maxRUs();
|
||||||
|
|
||||||
const ttlOptionDirty: boolean = this.timeToLive.editableIsDirty();
|
const ttlOptionDirty: boolean = this.timeToLive.editableIsDirty();
|
||||||
const ttlOrIndexingPolicyFieldsDirty: boolean =
|
const ttlOrIndexingPolicyFieldsDirty: boolean =
|
||||||
this.timeToLive.editableIsDirty() ||
|
this.timeToLive.editableIsDirty() ||
|
||||||
@@ -762,6 +890,26 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
return AutoPilotUtils.manualToAutoscaleDisclaimer;
|
return AutoPilotUtils.manualToAutoscaleDisclaimer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
throughputExceedsBackendLimits &&
|
||||||
|
!!this.collection.partitionKey &&
|
||||||
|
!this._isFixedContainer() &&
|
||||||
|
!ttlFieldFocused &&
|
||||||
|
!this.indexingPolicyElementFocused()
|
||||||
|
) {
|
||||||
|
return updateThroughputBeyondLimitWarningMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
throughputExceedsMaxValue &&
|
||||||
|
!!this.collection.partitionKey &&
|
||||||
|
!this._isFixedContainer() &&
|
||||||
|
!ttlFieldFocused &&
|
||||||
|
!this.indexingPolicyElementFocused()
|
||||||
|
) {
|
||||||
|
return updateThroughputDelayedApplyWarningMessage;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.pendingNotification()) {
|
if (this.pendingNotification()) {
|
||||||
const throughputUnit: string = this._getThroughputUnit();
|
const throughputUnit: string = this._getThroughputUnit();
|
||||||
const matches: string[] = this.pendingNotification().description.match(
|
const matches: string[] = this.pendingNotification().description.match(
|
||||||
@@ -951,23 +1099,54 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
if (
|
||||||
databaseId: this.collection.databaseId,
|
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||||
collectionId: this.collection.id(),
|
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||||
currentOffer: this.collection.offer(),
|
this.container != null
|
||||||
autopilotThroughput: this.isAutoPilotSelected() ? this.autoPilotThroughput() : undefined,
|
) {
|
||||||
manualThroughput: this.isAutoPilotSelected() ? undefined : newThroughput
|
const requestPayload = {
|
||||||
};
|
subscriptionId: userContext.subscriptionId,
|
||||||
if (this._hasProvisioningTypeChanged()) {
|
databaseAccountName: userContext.databaseAccount.name,
|
||||||
if (this.isAutoPilotSelected()) {
|
resourceGroup: userContext.resourceGroup,
|
||||||
updateOfferParams.migrateToAutoPilot = true;
|
databaseName: this.collection.databaseId,
|
||||||
} else {
|
collectionName: this.collection.id(),
|
||||||
updateOfferParams.migrateToManual = true;
|
throughput: newThroughput,
|
||||||
|
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
|
||||||
|
};
|
||||||
|
|
||||||
|
await updateOfferThroughputBeyondLimit(requestPayload);
|
||||||
|
this.collection.offer().content.offerThroughput = originalThroughputValue;
|
||||||
|
this.throughput(originalThroughputValue);
|
||||||
|
this.notificationStatusInfo(
|
||||||
|
throughputApplyDelayedMessage(
|
||||||
|
this.isAutoPilotSelected(),
|
||||||
|
originalThroughputValue,
|
||||||
|
this._getThroughputUnit(),
|
||||||
|
this.collection.databaseId,
|
||||||
|
this.collection.id(),
|
||||||
|
newThroughput
|
||||||
|
)
|
||||||
|
);
|
||||||
|
this.throughput.valueHasMutated(); // force component re-render
|
||||||
|
} else {
|
||||||
|
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||||
|
databaseId: this.collection.databaseId,
|
||||||
|
collectionId: this.collection.id(),
|
||||||
|
currentOffer: this.collection.offer(),
|
||||||
|
autopilotThroughput: this.isAutoPilotSelected() ? this.autoPilotThroughput() : undefined,
|
||||||
|
manualThroughput: this.isAutoPilotSelected() ? undefined : newThroughput
|
||||||
|
};
|
||||||
|
if (this._hasProvisioningTypeChanged()) {
|
||||||
|
if (this.isAutoPilotSelected()) {
|
||||||
|
updateOfferParams.migrateToAutoPilot = true;
|
||||||
|
} else {
|
||||||
|
updateOfferParams.migrateToManual = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||||
|
this.collection.offer(updatedOffer);
|
||||||
|
this.collection.offer.valueHasMutated();
|
||||||
}
|
}
|
||||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
|
||||||
this.collection.offer(updatedOffer);
|
|
||||||
this.collection.offer.valueHasMutated();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.container.isRefreshingExplorer(false);
|
this.container.isRefreshingExplorer(false);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import SparkMasterTabTemplate from "./SparkMasterTab.html";
|
|||||||
import NotebookV2TabTemplate from "./NotebookV2Tab.html";
|
import NotebookV2TabTemplate from "./NotebookV2Tab.html";
|
||||||
import TerminalTabTemplate from "./TerminalTab.html";
|
import TerminalTabTemplate from "./TerminalTab.html";
|
||||||
import MongoDocumentsTabTemplate from "./MongoDocumentsTab.html";
|
import MongoDocumentsTabTemplate from "./MongoDocumentsTab.html";
|
||||||
|
import MongoDocumentsTabV2Template from "./MongoDocumentsTabV2.html";
|
||||||
import MongoQueryTabTemplate from "./MongoQueryTab.html";
|
import MongoQueryTabTemplate from "./MongoQueryTab.html";
|
||||||
import MongoShellTabTemplate from "./MongoShellTab.html";
|
import MongoShellTabTemplate from "./MongoShellTab.html";
|
||||||
import QueryTabTemplate from "./QueryTab.html";
|
import QueryTabTemplate from "./QueryTab.html";
|
||||||
@@ -106,6 +107,15 @@ export class MongoQueryTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class MongoDocumentsTabV2 {
|
||||||
|
constructor() {
|
||||||
|
return {
|
||||||
|
viewModel: TabComponent,
|
||||||
|
template: MongoDocumentsTabV2Template
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class MongoShellTab {
|
export class MongoShellTab {
|
||||||
constructor() {
|
constructor() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ describe("Collection", () => {
|
|||||||
container: Explorer,
|
container: Explorer,
|
||||||
databaseId: string,
|
databaseId: string,
|
||||||
data: DataModels.Collection,
|
data: DataModels.Collection,
|
||||||
|
quotaInfo: DataModels.CollectionQuotaInfo,
|
||||||
offer: DataModels.Offer
|
offer: DataModels.Offer
|
||||||
): Collection {
|
): Collection {
|
||||||
return new Collection(container, databaseId, data, offer);
|
return new Collection(container, databaseId, data, quotaInfo, offer);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateMockCollectionsDataModelWithPartitionKey(
|
function generateMockCollectionsDataModelWithPartitionKey(
|
||||||
@@ -49,7 +50,7 @@ describe("Collection", () => {
|
|||||||
});
|
});
|
||||||
mockContainer.deleteCollectionText = ko.observable<string>("delete collection");
|
mockContainer.deleteCollectionText = ko.observable<string>("delete collection");
|
||||||
|
|
||||||
return generateCollection(mockContainer, "abc", data, {} as DataModels.Offer);
|
return generateCollection(mockContainer, "abc", data, {} as DataModels.CollectionQuotaInfo, {} as DataModels.Offer);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Partition key path parsing", () => {
|
describe("Partition key path parsing", () => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { readTriggers } from "../../Common/dataAccess/readTriggers";
|
|||||||
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
|
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
|
||||||
import { createDocument } from "../../Common/DocumentClientUtilityBase";
|
import { createDocument } from "../../Common/DocumentClientUtilityBase";
|
||||||
import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer";
|
import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer";
|
||||||
|
import { readCollectionQuotaInfo } from "../../Common/dataAccess/readCollectionQuotaInfo";
|
||||||
import * as Logger from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
@@ -23,6 +24,7 @@ import ConflictsTab from "../Tabs/ConflictsTab";
|
|||||||
import DocumentsTab from "../Tabs/DocumentsTab";
|
import DocumentsTab from "../Tabs/DocumentsTab";
|
||||||
import GraphTab from "../Tabs/GraphTab";
|
import GraphTab from "../Tabs/GraphTab";
|
||||||
import MongoDocumentsTab from "../Tabs/MongoDocumentsTab";
|
import MongoDocumentsTab from "../Tabs/MongoDocumentsTab";
|
||||||
|
import MongoDocumentsTabV2 from "../Tabs/MongoDocumentsTabV2";
|
||||||
import MongoQueryTab from "../Tabs/MongoQueryTab";
|
import MongoQueryTab from "../Tabs/MongoQueryTab";
|
||||||
import MongoShellTab from "../Tabs/MongoShellTab";
|
import MongoShellTab from "../Tabs/MongoShellTab";
|
||||||
import QueryTab from "../Tabs/QueryTab";
|
import QueryTab from "../Tabs/QueryTab";
|
||||||
@@ -54,6 +56,7 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
public defaultTtl: ko.Observable<number>;
|
public defaultTtl: ko.Observable<number>;
|
||||||
public indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
public indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
||||||
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
||||||
|
public quotaInfo: ko.Observable<DataModels.CollectionQuotaInfo>;
|
||||||
public offer: ko.Observable<DataModels.Offer>;
|
public offer: ko.Observable<DataModels.Offer>;
|
||||||
public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
||||||
public changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
public changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
||||||
@@ -61,6 +64,8 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
public throughput: ko.Computed<number>;
|
public throughput: ko.Computed<number>;
|
||||||
public rawDataModel: DataModels.Collection;
|
public rawDataModel: DataModels.Collection;
|
||||||
public analyticalStorageTtl: ko.Observable<number>;
|
public analyticalStorageTtl: ko.Observable<number>;
|
||||||
|
public schema: DataModels.ISchema;
|
||||||
|
public requestSchema: () => void;
|
||||||
public geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
|
public geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
|
||||||
|
|
||||||
// TODO move this to API customization class
|
// TODO move this to API customization class
|
||||||
@@ -92,7 +97,13 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
public userDefinedFunctionsFocused: ko.Observable<boolean>;
|
public userDefinedFunctionsFocused: ko.Observable<boolean>;
|
||||||
public triggersFocused: ko.Observable<boolean>;
|
public triggersFocused: ko.Observable<boolean>;
|
||||||
|
|
||||||
constructor(container: Explorer, databaseId: string, data: DataModels.Collection, offer: DataModels.Offer) {
|
constructor(
|
||||||
|
container: Explorer,
|
||||||
|
databaseId: string,
|
||||||
|
data: DataModels.Collection,
|
||||||
|
quotaInfo: DataModels.CollectionQuotaInfo,
|
||||||
|
offer: DataModels.Offer
|
||||||
|
) {
|
||||||
this.nodeKind = "Collection";
|
this.nodeKind = "Collection";
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.self = data._self;
|
this.self = data._self;
|
||||||
@@ -104,10 +115,13 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
this.id = ko.observable(data.id);
|
this.id = ko.observable(data.id);
|
||||||
this.defaultTtl = ko.observable(data.defaultTtl);
|
this.defaultTtl = ko.observable(data.defaultTtl);
|
||||||
this.indexingPolicy = ko.observable(data.indexingPolicy);
|
this.indexingPolicy = ko.observable(data.indexingPolicy);
|
||||||
|
this.quotaInfo = ko.observable(quotaInfo);
|
||||||
this.offer = ko.observable(offer);
|
this.offer = ko.observable(offer);
|
||||||
this.conflictResolutionPolicy = ko.observable(data.conflictResolutionPolicy);
|
this.conflictResolutionPolicy = ko.observable(data.conflictResolutionPolicy);
|
||||||
this.changeFeedPolicy = ko.observable<DataModels.ChangeFeedPolicy>(data.changeFeedPolicy);
|
this.changeFeedPolicy = ko.observable<DataModels.ChangeFeedPolicy>(data.changeFeedPolicy);
|
||||||
this.analyticalStorageTtl = ko.observable(data.analyticalStorageTtl);
|
this.analyticalStorageTtl = ko.observable(data.analyticalStorageTtl);
|
||||||
|
this.schema = data.schema;
|
||||||
|
this.requestSchema = data.requestSchema;
|
||||||
this.geospatialConfig = ko.observable(data.geospatialConfig);
|
this.geospatialConfig = ko.observable(data.geospatialConfig);
|
||||||
|
|
||||||
// TODO fix this to only replace non-excaped single quotes
|
// TODO fix this to only replace non-excaped single quotes
|
||||||
@@ -493,11 +507,11 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
dataExplorerArea: Constants.Areas.ResourceTree
|
dataExplorerArea: Constants.Areas.ResourceTree
|
||||||
});
|
});
|
||||||
|
|
||||||
const mongoDocumentsTabs: MongoDocumentsTab[] = this.container.tabsManager.getTabs(
|
const mongoDocumentsTabs: MongoDocumentsTabV2[] = this.container.tabsManager.getTabs(
|
||||||
ViewModels.CollectionTabKind.Documents,
|
ViewModels.CollectionTabKind.Documents,
|
||||||
tab => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
|
tab => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
|
||||||
) as MongoDocumentsTab[];
|
) as MongoDocumentsTabV2[];
|
||||||
let mongoDocumentsTab: MongoDocumentsTab = mongoDocumentsTabs && mongoDocumentsTabs[0];
|
let mongoDocumentsTab: MongoDocumentsTabV2 = mongoDocumentsTabs && mongoDocumentsTabs[0];
|
||||||
|
|
||||||
if (mongoDocumentsTab) {
|
if (mongoDocumentsTab) {
|
||||||
this.container.tabsManager.activateTab(mongoDocumentsTab);
|
this.container.tabsManager.activateTab(mongoDocumentsTab);
|
||||||
@@ -512,9 +526,8 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
});
|
});
|
||||||
this.documentIds([]);
|
this.documentIds([]);
|
||||||
|
|
||||||
mongoDocumentsTab = new MongoDocumentsTab({
|
mongoDocumentsTab = new MongoDocumentsTabV2({
|
||||||
partitionKey: this.partitionKey,
|
container: this.container,
|
||||||
documentIds: this.documentIds,
|
|
||||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||||
title: "Documents",
|
title: "Documents",
|
||||||
tabPath: "",
|
tabPath: "",
|
||||||
@@ -660,6 +673,14 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private async loadCollectionQuotaInfo(): Promise<void> {
|
||||||
|
// TODO: Use the collection entity cache to get quota info
|
||||||
|
const quotaInfoWithUniqueKeyPolicy = await readCollectionQuotaInfo(this);
|
||||||
|
this.uniqueKeyPolicy = quotaInfoWithUniqueKeyPolicy.uniqueKeyPolicy;
|
||||||
|
const quotaInfo = _.omit(quotaInfoWithUniqueKeyPolicy, "uniqueKeyPolicy");
|
||||||
|
this.quotaInfo(quotaInfo);
|
||||||
|
}
|
||||||
|
|
||||||
public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) {
|
public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) {
|
||||||
const collection: ViewModels.Collection = source.collection || source;
|
const collection: ViewModels.Collection = source.collection || source;
|
||||||
const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Query).length + 1;
|
const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Query).length + 1;
|
||||||
@@ -1332,6 +1353,7 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this.offer(await readCollectionOffer(params));
|
this.offer(await readCollectionOffer(params));
|
||||||
|
await this.loadCollectionQuotaInfo();
|
||||||
|
|
||||||
TelemetryProcessor.traceSuccess(
|
TelemetryProcessor.traceSuccess(
|
||||||
Action.LoadOffers,
|
Action.LoadOffers,
|
||||||
|
|||||||
82
src/Explorer/Tree/Database.test.ts
Normal file
82
src/Explorer/Tree/Database.test.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
|
import * as ko from "knockout";
|
||||||
|
import Database from "./Database";
|
||||||
|
import Explorer from "../Explorer";
|
||||||
|
import { HttpStatusCodes } from "../../Common/Constants";
|
||||||
|
import { JunoClient } from "../../Juno/JunoClient";
|
||||||
|
import { userContext, updateUserContext } from "../../UserContext";
|
||||||
|
|
||||||
|
const createMockContainer = (): Explorer => {
|
||||||
|
const mockContainer = new Explorer();
|
||||||
|
return mockContainer;
|
||||||
|
};
|
||||||
|
|
||||||
|
updateUserContext({
|
||||||
|
subscriptionId: "fakeSubscriptionId",
|
||||||
|
resourceGroup: "fakeResourceGroup",
|
||||||
|
databaseAccount: {
|
||||||
|
id: "id",
|
||||||
|
name: "fakeName",
|
||||||
|
location: "fakeLocation",
|
||||||
|
type: "fakeType",
|
||||||
|
tags: undefined,
|
||||||
|
kind: "fakeKind",
|
||||||
|
properties: {
|
||||||
|
documentEndpoint: "fakeEndpoint",
|
||||||
|
tableEndpoint: "fakeEndpoint",
|
||||||
|
gremlinEndpoint: "fakeEndpoint",
|
||||||
|
cassandraEndpoint: "fakeEndpoint"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Add Schema", () => {
|
||||||
|
it("should not call requestSchema or getSchema if analyticalStorageTtl is undefined", () => {
|
||||||
|
const collection: DataModels.Collection = {} as DataModels.Collection;
|
||||||
|
collection.analyticalStorageTtl = undefined;
|
||||||
|
const database = new Database(createMockContainer(), { id: "fakeId" });
|
||||||
|
database.container = createMockContainer();
|
||||||
|
database.container.isSchemaEnabled = ko.computed<boolean>(() => false);
|
||||||
|
|
||||||
|
database.junoClient = new JunoClient();
|
||||||
|
database.junoClient.requestSchema = jest.fn();
|
||||||
|
database.junoClient.getSchema = jest.fn();
|
||||||
|
|
||||||
|
database.addSchema(collection);
|
||||||
|
|
||||||
|
expect(database.junoClient.requestSchema).toBeCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call requestSchema or getSchema if analyticalStorageTtl is not undefined", () => {
|
||||||
|
const collection: DataModels.Collection = { id: "fakeId" } as DataModels.Collection;
|
||||||
|
collection.analyticalStorageTtl = 0;
|
||||||
|
|
||||||
|
const database = new Database(createMockContainer(), {});
|
||||||
|
database.container = createMockContainer();
|
||||||
|
database.container.isSchemaEnabled = ko.computed<boolean>(() => true);
|
||||||
|
|
||||||
|
database.junoClient = new JunoClient();
|
||||||
|
database.junoClient.requestSchema = jest.fn();
|
||||||
|
database.junoClient.getSchema = jest.fn().mockResolvedValue({ status: HttpStatusCodes.OK, data: {} });
|
||||||
|
|
||||||
|
jest.useFakeTimers();
|
||||||
|
const interval = 5000;
|
||||||
|
const checkForSchema: NodeJS.Timeout = database.addSchema(collection, interval);
|
||||||
|
jest.advanceTimersByTime(interval + 1000);
|
||||||
|
|
||||||
|
expect(database.junoClient.requestSchema).toBeCalledWith({
|
||||||
|
id: undefined,
|
||||||
|
subscriptionId: userContext.subscriptionId,
|
||||||
|
resourceGroup: userContext.resourceGroup,
|
||||||
|
accountName: userContext.databaseAccount.name,
|
||||||
|
resource: `dbs/${database.id}/colls/${collection.id}`,
|
||||||
|
status: "new"
|
||||||
|
});
|
||||||
|
expect(checkForSchema).not.toBeNull();
|
||||||
|
expect(database.junoClient.getSchema).toBeCalledWith(
|
||||||
|
userContext.databaseAccount.name,
|
||||||
|
database.id(),
|
||||||
|
collection.id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,6 +13,8 @@ import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsol
|
|||||||
import * as Logger from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { readCollections } from "../../Common/dataAccess/readCollections";
|
import { readCollections } from "../../Common/dataAccess/readCollections";
|
||||||
|
import { JunoClient, IJunoResponse } from "../../Juno/JunoClient";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
|
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
|
||||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
|
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
|
||||||
@@ -29,6 +31,7 @@ export default class Database implements ViewModels.Database {
|
|||||||
public isDatabaseExpanded: ko.Observable<boolean>;
|
public isDatabaseExpanded: ko.Observable<boolean>;
|
||||||
public isDatabaseShared: ko.Computed<boolean>;
|
public isDatabaseShared: ko.Computed<boolean>;
|
||||||
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
|
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
|
||||||
|
public junoClient: JunoClient;
|
||||||
|
|
||||||
constructor(container: Explorer, data: any) {
|
constructor(container: Explorer, data: any) {
|
||||||
this.nodeKind = "Database";
|
this.nodeKind = "Database";
|
||||||
@@ -43,6 +46,7 @@ export default class Database implements ViewModels.Database {
|
|||||||
this.isDatabaseShared = ko.pureComputed(() => {
|
this.isDatabaseShared = ko.pureComputed(() => {
|
||||||
return this.offer && !!this.offer();
|
return this.offer && !!this.offer();
|
||||||
});
|
});
|
||||||
|
this.junoClient = new JunoClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSettingsClick = () => {
|
public onSettingsClick = () => {
|
||||||
@@ -184,8 +188,12 @@ export default class Database implements ViewModels.Database {
|
|||||||
const collections: DataModels.Collection[] = await readCollections(this.id());
|
const collections: DataModels.Collection[] = await readCollections(this.id());
|
||||||
const deltaCollections = this.getDeltaCollections(collections);
|
const deltaCollections = this.getDeltaCollections(collections);
|
||||||
|
|
||||||
|
collections.forEach((collection: DataModels.Collection) => {
|
||||||
|
this.addSchema(collection);
|
||||||
|
});
|
||||||
|
|
||||||
deltaCollections.toAdd.forEach((collection: DataModels.Collection) => {
|
deltaCollections.toAdd.forEach((collection: DataModels.Collection) => {
|
||||||
const collectionVM: Collection = new Collection(this.container, this.id(), collection, null);
|
const collectionVM: Collection = new Collection(this.container, this.id(), collection, null, null);
|
||||||
collectionVMs.push(collectionVM);
|
collectionVMs.push(collectionVM);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -308,4 +316,42 @@ export default class Database implements ViewModels.Database {
|
|||||||
|
|
||||||
this.collections(collectionsToKeep);
|
this.collections(collectionsToKeep);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public addSchema(collection: DataModels.Collection, interval?: number): NodeJS.Timeout {
|
||||||
|
let checkForSchema: NodeJS.Timeout = null;
|
||||||
|
interval = interval || 5000;
|
||||||
|
|
||||||
|
if (collection.analyticalStorageTtl !== undefined && this.container.isSchemaEnabled()) {
|
||||||
|
collection.requestSchema = () => {
|
||||||
|
this.junoClient.requestSchema({
|
||||||
|
id: undefined,
|
||||||
|
subscriptionId: userContext.subscriptionId,
|
||||||
|
resourceGroup: userContext.resourceGroup,
|
||||||
|
accountName: userContext.databaseAccount.name,
|
||||||
|
resource: `dbs/${this.id}/colls/${collection.id}`,
|
||||||
|
status: "new"
|
||||||
|
});
|
||||||
|
checkForSchema = setInterval(async () => {
|
||||||
|
const response: IJunoResponse<DataModels.ISchema> = await this.junoClient.getSchema(
|
||||||
|
userContext.databaseAccount.name,
|
||||||
|
this.id(),
|
||||||
|
collection.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status >= 404) {
|
||||||
|
clearInterval(checkForSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data !== null) {
|
||||||
|
clearInterval(checkForSchema);
|
||||||
|
collection.schema = response.data;
|
||||||
|
}
|
||||||
|
}, interval);
|
||||||
|
};
|
||||||
|
|
||||||
|
collection.requestSchema();
|
||||||
|
}
|
||||||
|
|
||||||
|
return checkForSchema;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
253
src/Explorer/Tree/ResourceTreeAdapter.test.tsx
Normal file
253
src/Explorer/Tree/ResourceTreeAdapter.test.tsx
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
import * as ko from "knockout";
|
||||||
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
|
import React from "react";
|
||||||
|
import { ResourceTreeAdapter } from "./ResourceTreeAdapter";
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import { TreeComponent, TreeNode, TreeComponentProps } from "../Controls/TreeComponent/TreeComponent";
|
||||||
|
import Explorer from "../Explorer";
|
||||||
|
import Collection from "./Collection";
|
||||||
|
|
||||||
|
const schema: DataModels.ISchema = {
|
||||||
|
id: "fakeSchemaId",
|
||||||
|
accountName: "fakeAccountName",
|
||||||
|
resource: "dbs/FakeDbName/colls/FakeCollectionName",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
dataType: {
|
||||||
|
code: 15,
|
||||||
|
name: "String"
|
||||||
|
},
|
||||||
|
hasNulls: true,
|
||||||
|
isArray: false,
|
||||||
|
schemaType: {
|
||||||
|
code: 0,
|
||||||
|
name: "Data"
|
||||||
|
},
|
||||||
|
name: "_rid",
|
||||||
|
path: "_rid",
|
||||||
|
maxRepetitionLevel: 0,
|
||||||
|
maxDefinitionLevel: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataType: {
|
||||||
|
code: 11,
|
||||||
|
name: "Int64"
|
||||||
|
},
|
||||||
|
hasNulls: true,
|
||||||
|
isArray: false,
|
||||||
|
schemaType: {
|
||||||
|
code: 0,
|
||||||
|
name: "Data"
|
||||||
|
},
|
||||||
|
name: "_ts",
|
||||||
|
path: "_ts",
|
||||||
|
maxRepetitionLevel: 0,
|
||||||
|
maxDefinitionLevel: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataType: {
|
||||||
|
code: 15,
|
||||||
|
name: "String"
|
||||||
|
},
|
||||||
|
hasNulls: true,
|
||||||
|
isArray: false,
|
||||||
|
schemaType: {
|
||||||
|
code: 0,
|
||||||
|
name: "Data"
|
||||||
|
},
|
||||||
|
name: "id",
|
||||||
|
path: "id",
|
||||||
|
maxRepetitionLevel: 0,
|
||||||
|
maxDefinitionLevel: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataType: {
|
||||||
|
code: 15,
|
||||||
|
name: "String"
|
||||||
|
},
|
||||||
|
hasNulls: true,
|
||||||
|
isArray: false,
|
||||||
|
schemaType: {
|
||||||
|
code: 0,
|
||||||
|
name: "Data"
|
||||||
|
},
|
||||||
|
name: "pk",
|
||||||
|
path: "pk",
|
||||||
|
maxRepetitionLevel: 0,
|
||||||
|
maxDefinitionLevel: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataType: {
|
||||||
|
code: 15,
|
||||||
|
name: "String"
|
||||||
|
},
|
||||||
|
hasNulls: true,
|
||||||
|
isArray: false,
|
||||||
|
schemaType: {
|
||||||
|
code: 0,
|
||||||
|
name: "Data"
|
||||||
|
},
|
||||||
|
name: "other",
|
||||||
|
path: "other",
|
||||||
|
maxRepetitionLevel: 0,
|
||||||
|
maxDefinitionLevel: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataType: {
|
||||||
|
code: 15,
|
||||||
|
name: "String"
|
||||||
|
},
|
||||||
|
hasNulls: true,
|
||||||
|
isArray: false,
|
||||||
|
schemaType: {
|
||||||
|
code: 0,
|
||||||
|
name: "Data"
|
||||||
|
},
|
||||||
|
name: "name",
|
||||||
|
path: "nested.name",
|
||||||
|
maxRepetitionLevel: 0,
|
||||||
|
maxDefinitionLevel: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataType: {
|
||||||
|
code: 11,
|
||||||
|
name: "Int64"
|
||||||
|
},
|
||||||
|
hasNulls: true,
|
||||||
|
isArray: false,
|
||||||
|
schemaType: {
|
||||||
|
code: 0,
|
||||||
|
name: "Data"
|
||||||
|
},
|
||||||
|
name: "someNumber",
|
||||||
|
path: "nested.someNumber",
|
||||||
|
maxRepetitionLevel: 0,
|
||||||
|
maxDefinitionLevel: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataType: {
|
||||||
|
code: 17,
|
||||||
|
name: "Double"
|
||||||
|
},
|
||||||
|
hasNulls: true,
|
||||||
|
isArray: false,
|
||||||
|
schemaType: {
|
||||||
|
code: 0,
|
||||||
|
name: "Data"
|
||||||
|
},
|
||||||
|
name: "anotherNumber",
|
||||||
|
path: "nested.anotherNumber",
|
||||||
|
maxRepetitionLevel: 0,
|
||||||
|
maxDefinitionLevel: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataType: {
|
||||||
|
code: 15,
|
||||||
|
name: "String"
|
||||||
|
},
|
||||||
|
hasNulls: true,
|
||||||
|
isArray: false,
|
||||||
|
schemaType: {
|
||||||
|
code: 0,
|
||||||
|
name: "Data"
|
||||||
|
},
|
||||||
|
name: "name",
|
||||||
|
path: "items.list.items.name",
|
||||||
|
maxRepetitionLevel: 1,
|
||||||
|
maxDefinitionLevel: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataType: {
|
||||||
|
code: 11,
|
||||||
|
name: "Int64"
|
||||||
|
},
|
||||||
|
hasNulls: true,
|
||||||
|
isArray: false,
|
||||||
|
schemaType: {
|
||||||
|
code: 0,
|
||||||
|
name: "Data"
|
||||||
|
},
|
||||||
|
name: "someNumber",
|
||||||
|
path: "items.list.items.someNumber",
|
||||||
|
maxRepetitionLevel: 1,
|
||||||
|
maxDefinitionLevel: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataType: {
|
||||||
|
code: 17,
|
||||||
|
name: "Double"
|
||||||
|
},
|
||||||
|
hasNulls: true,
|
||||||
|
isArray: false,
|
||||||
|
schemaType: {
|
||||||
|
code: 0,
|
||||||
|
name: "Data"
|
||||||
|
},
|
||||||
|
name: "anotherNumber",
|
||||||
|
path: "items.list.items.anotherNumber",
|
||||||
|
maxRepetitionLevel: 1,
|
||||||
|
maxDefinitionLevel: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataType: {
|
||||||
|
code: 15,
|
||||||
|
name: "String"
|
||||||
|
},
|
||||||
|
hasNulls: true,
|
||||||
|
isArray: false,
|
||||||
|
schemaType: {
|
||||||
|
code: 0,
|
||||||
|
name: "Data"
|
||||||
|
},
|
||||||
|
name: "_etag",
|
||||||
|
path: "_etag",
|
||||||
|
maxRepetitionLevel: 0,
|
||||||
|
maxDefinitionLevel: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const createMockContainer = (): Explorer => {
|
||||||
|
const mockContainer = new Explorer();
|
||||||
|
mockContainer.selectedNode = ko.observable<ViewModels.TreeNode>();
|
||||||
|
mockContainer.onUpdateTabsButtons = () => {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
return mockContainer;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createMockCollection = (): ViewModels.Collection => {
|
||||||
|
const mockCollection = {} as DataModels.Collection;
|
||||||
|
mockCollection._rid = "fakeRid";
|
||||||
|
mockCollection._self = "fakeSelf";
|
||||||
|
mockCollection.id = "fakeId";
|
||||||
|
mockCollection.analyticalStorageTtl = 0;
|
||||||
|
mockCollection.schema = schema;
|
||||||
|
|
||||||
|
const mockCollectionVM: ViewModels.Collection = new Collection(
|
||||||
|
createMockContainer(),
|
||||||
|
"fakeDatabaseId",
|
||||||
|
mockCollection,
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
return mockCollectionVM;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("Resource tree for schema", () => {
|
||||||
|
const mockContainer: Explorer = createMockContainer();
|
||||||
|
const resourceTree = new ResourceTreeAdapter(mockContainer);
|
||||||
|
|
||||||
|
it("should render", () => {
|
||||||
|
const rootNode: TreeNode = resourceTree.buildSchemaNode(createMockCollection());
|
||||||
|
const props: TreeComponentProps = {
|
||||||
|
rootNode,
|
||||||
|
className: "dataResourceTree"
|
||||||
|
};
|
||||||
|
const wrapper = shallow(<TreeComponent {...props} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,7 +2,7 @@ import * as ko from "knockout";
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||||
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
|
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
|
||||||
import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
|
import { TreeComponent, TreeNode, TreeNodeMenuItem, TreeNodeComponent } from "../Controls/TreeComponent/TreeComponent";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
||||||
import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory";
|
import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory";
|
||||||
@@ -32,6 +32,7 @@ import StoredProcedure from "./StoredProcedure";
|
|||||||
import Trigger from "./Trigger";
|
import Trigger from "./Trigger";
|
||||||
import TabsBase from "../Tabs/TabsBase";
|
import TabsBase from "../Tabs/TabsBase";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
|
|
||||||
export class ResourceTreeAdapter implements ReactAdapter {
|
export class ResourceTreeAdapter implements ReactAdapter {
|
||||||
public static readonly MyNotebooksTitle = "My Notebooks";
|
public static readonly MyNotebooksTitle = "My Notebooks";
|
||||||
@@ -289,6 +290,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const schemaNode: TreeNode = this.buildSchemaNode(collection);
|
||||||
|
if (schemaNode) {
|
||||||
|
children.push(schemaNode);
|
||||||
|
}
|
||||||
|
|
||||||
if (ResourceTreeAdapter.showScriptNodes(this.container)) {
|
if (ResourceTreeAdapter.showScriptNodes(this.container)) {
|
||||||
children.push(this.buildStoredProcedureNode(collection));
|
children.push(this.buildStoredProcedureNode(collection));
|
||||||
children.push(this.buildUserDefinedFunctionsNode(collection));
|
children.push(this.buildUserDefinedFunctionsNode(collection));
|
||||||
@@ -405,6 +411,75 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public buildSchemaNode(collection: ViewModels.Collection): TreeNode {
|
||||||
|
if (collection.analyticalStorageTtl() == undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!collection.schema || !collection.schema.fields) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: "Schema",
|
||||||
|
children: this.getSchemaNodes(collection.schema.fields),
|
||||||
|
onClick: () => {
|
||||||
|
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Schema);
|
||||||
|
this.container.tabsManager.refreshActiveTab(
|
||||||
|
(tab: TabsBase) => tab.collection && tab.collection.rid === collection.rid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSchemaNodes(fields: DataModels.IDataField[]): TreeNode[] {
|
||||||
|
const schema: any = {};
|
||||||
|
|
||||||
|
//unflatten
|
||||||
|
fields.forEach((field: DataModels.IDataField, fieldIndex: number) => {
|
||||||
|
const path: string[] = field.path.split(".");
|
||||||
|
const fieldProperties = [field.dataType.name, `HasNulls: ${field.hasNulls}`];
|
||||||
|
let current: any = {};
|
||||||
|
path.forEach((name: string, pathIndex: number) => {
|
||||||
|
if (pathIndex === 0) {
|
||||||
|
if (schema[name] === undefined) {
|
||||||
|
if (pathIndex === path.length - 1) {
|
||||||
|
schema[name] = fieldProperties;
|
||||||
|
} else {
|
||||||
|
schema[name] = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current = schema[name];
|
||||||
|
} else {
|
||||||
|
if (current[name] === undefined) {
|
||||||
|
if (pathIndex === path.length - 1) {
|
||||||
|
current[name] = fieldProperties;
|
||||||
|
} else {
|
||||||
|
current[name] = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current = current[name];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const traverse = (obj: any): TreeNode[] => {
|
||||||
|
const children: TreeNode[] = [];
|
||||||
|
|
||||||
|
if (obj !== null && !Array.isArray(obj) && typeof obj === "object") {
|
||||||
|
Object.entries(obj).forEach(([key, value]) => {
|
||||||
|
children.push({ label: key, children: traverse(value) });
|
||||||
|
});
|
||||||
|
} else if (Array.isArray(obj)) {
|
||||||
|
return [{ label: obj[0] }, { label: obj[1] }];
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
};
|
||||||
|
|
||||||
|
return traverse(schema);
|
||||||
|
}
|
||||||
|
|
||||||
private buildNotebooksTrees(): TreeNode {
|
private buildNotebooksTrees(): TreeNode {
|
||||||
let notebooksTree: TreeNode = {
|
let notebooksTree: TreeNode = {
|
||||||
label: undefined,
|
label: undefined,
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Resource tree for schema should render 1`] = `
|
||||||
|
<div
|
||||||
|
className="treeComponent dataResourceTree"
|
||||||
|
>
|
||||||
|
<TreeNodeComponent
|
||||||
|
generation={0}
|
||||||
|
node={
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"label": "String",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"label": "HasNulls: true",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "_rid",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"label": "Int64",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"label": "HasNulls: true",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "_ts",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"label": "String",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"label": "HasNulls: true",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "id",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"label": "String",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"label": "HasNulls: true",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "pk",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"label": "String",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"label": "HasNulls: true",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "other",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"label": "String",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"label": "HasNulls: true",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "name",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"label": "Int64",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"label": "HasNulls: true",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "someNumber",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"label": "Double",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"label": "HasNulls: true",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "anotherNumber",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "nested",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"label": "String",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"label": "HasNulls: true",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "name",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"label": "Int64",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"label": "HasNulls: true",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "someNumber",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"label": "Double",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"label": "HasNulls: true",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "anotherNumber",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "items",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "list",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "items",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"label": "String",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"label": "HasNulls: true",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "_etag",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "Schema",
|
||||||
|
"onClick": [Function],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
paddingLeft={0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
@@ -7,6 +7,7 @@ import { IGitHubResponse } from "../GitHub/GitHubClient";
|
|||||||
import { IGitHubOAuthToken } from "../GitHub/GitHubOAuthService";
|
import { IGitHubOAuthToken } from "../GitHub/GitHubOAuthService";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||||
|
import { number } from "prop-types";
|
||||||
|
|
||||||
export interface IJunoResponse<T> {
|
export interface IJunoResponse<T> {
|
||||||
status: number;
|
status: number;
|
||||||
@@ -427,6 +428,51 @@ export class JunoClient {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async requestSchema(
|
||||||
|
schemaRequest: DataModels.ISchemaRequest
|
||||||
|
): Promise<IJunoResponse<DataModels.ISchemaRequest>> {
|
||||||
|
const response = await window.fetch(`${this.getAnalyticsUrl()}/${schemaRequest.accountName}/schema/request`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(schemaRequest),
|
||||||
|
headers: JunoClient.getHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
|
let data: DataModels.ISchemaRequest;
|
||||||
|
if (response.status === HttpStatusCodes.OK) {
|
||||||
|
data = await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getSchema(
|
||||||
|
accountName: string,
|
||||||
|
databaseName: string,
|
||||||
|
containerName: string
|
||||||
|
): Promise<IJunoResponse<DataModels.ISchema>> {
|
||||||
|
const response = await window.fetch(
|
||||||
|
`${this.getAnalyticsUrl()}/${accountName}/schema/${databaseName}/${containerName}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: JunoClient.getHeaders()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let data: DataModels.ISchema;
|
||||||
|
|
||||||
|
if (response.status === HttpStatusCodes.OK) {
|
||||||
|
data = await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private async getNotebooks(input: RequestInfo, init?: RequestInit): Promise<IJunoResponse<IGalleryItem[]>> {
|
private async getNotebooks(input: RequestInfo, init?: RequestInit): Promise<IJunoResponse<IGalleryItem[]>> {
|
||||||
const response = await window.fetch(input, init);
|
const response = await window.fetch(input, init);
|
||||||
|
|
||||||
@@ -457,6 +503,10 @@ export class JunoClient {
|
|||||||
return `${this.getNotebooksUrl()}/${this.getAccount()}`;
|
return `${this.getNotebooksUrl()}/${this.getAccount()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getAnalyticsUrl(): string {
|
||||||
|
return `${configContext.JUNO_ENDPOINT}/api/analytics`;
|
||||||
|
}
|
||||||
|
|
||||||
private static getHeaders(): HeadersInit {
|
private static getHeaders(): HeadersInit {
|
||||||
const authorizationHeader = getAuthorizationHeader();
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
return {
|
return {
|
||||||
|
|||||||
117
src/Main.ts
117
src/Main.ts
@@ -1,117 +0,0 @@
|
|||||||
// CSS Dependencies
|
|
||||||
import "bootstrap/dist/css/bootstrap.css";
|
|
||||||
import "../less/documentDB.less";
|
|
||||||
import "../less/tree.less";
|
|
||||||
import "../less/forms.less";
|
|
||||||
import "../less/menus.less";
|
|
||||||
import "../less/infobox.less";
|
|
||||||
import "../less/messagebox.less";
|
|
||||||
import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less";
|
|
||||||
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
|
|
||||||
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
|
|
||||||
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
|
|
||||||
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
|
|
||||||
import "./Explorer/Controls/DynamicList/DynamicListComponent.less";
|
|
||||||
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
|
|
||||||
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
|
|
||||||
import "../less/TableStyles/queryBuilder.less";
|
|
||||||
import "../externals/jquery.dataTables.min.css";
|
|
||||||
import "../less/TableStyles/fulldatatables.less";
|
|
||||||
import "../less/TableStyles/EntityEditor.less";
|
|
||||||
import "../less/TableStyles/CustomizeColumns.less";
|
|
||||||
import "../less/resourceTree.less";
|
|
||||||
import "../externals/jquery.typeahead.min.css";
|
|
||||||
import "../externals/jquery-ui.min.css";
|
|
||||||
import "../externals/jquery-ui.structure.min.css";
|
|
||||||
import "../externals/jquery-ui.theme.min.css";
|
|
||||||
import "./Explorer/Graph/NewVertexComponent/newVertexComponent.less";
|
|
||||||
import "./Explorer/Panes/GraphNewVertexPane.less";
|
|
||||||
import "./Explorer/Tabs/QueryTab.less";
|
|
||||||
import "./Explorer/Controls/TreeComponent/treeComponent.less";
|
|
||||||
import "./Explorer/Controls/Accordion/AccordionComponent.less";
|
|
||||||
import "./Explorer/SplashScreen/SplashScreenComponent.less";
|
|
||||||
import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
|
|
||||||
|
|
||||||
// Image Dependencies
|
|
||||||
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
|
||||||
import "../images/favicon.ico";
|
|
||||||
|
|
||||||
import "./Shared/appInsights";
|
|
||||||
import "babel-polyfill";
|
|
||||||
import "es6-symbol/implement";
|
|
||||||
import "webcrypto-liner/build/webcrypto-liner.shim.min";
|
|
||||||
import "./Libs/jquery";
|
|
||||||
import "bootstrap/dist/js/npm";
|
|
||||||
import "../externals/jquery.typeahead.min.js";
|
|
||||||
import "../externals/jquery-ui.min.js";
|
|
||||||
import "../externals/adal.js";
|
|
||||||
import "promise-polyfill/src/polyfill";
|
|
||||||
import "abort-controller/polyfill";
|
|
||||||
import "whatwg-fetch";
|
|
||||||
import "es6-object-assign/auto";
|
|
||||||
import "promise.prototype.finally/auto";
|
|
||||||
import "object.entries/auto";
|
|
||||||
import "./Libs/is-integer-polyfill";
|
|
||||||
import "url-polyfill/url-polyfill.min";
|
|
||||||
|
|
||||||
// TODO: Enable ReactDevTools after fixing the portal CORS issue
|
|
||||||
// import "./ReactDevTools"
|
|
||||||
|
|
||||||
import * as ko from "knockout";
|
|
||||||
import * as TelemetryProcessor from "./Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import { Action, ActionModifiers } from "./Shared/Telemetry/TelemetryConstants";
|
|
||||||
|
|
||||||
import { BindingHandlersRegisterer } from "./Bindings/BindingHandlersRegisterer";
|
|
||||||
import * as Emulator from "./Platform/Emulator/Main";
|
|
||||||
import Hosted from "./Platform/Hosted/Main";
|
|
||||||
import * as Portal from "./Platform/Portal/Main";
|
|
||||||
import { AuthType } from "./AuthType";
|
|
||||||
|
|
||||||
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
|
||||||
import { applyExplorerBindings } from "./applyExplorerBindings";
|
|
||||||
import { initializeConfiguration, Platform } from "./ConfigContext";
|
|
||||||
import Explorer from "./Explorer/Explorer";
|
|
||||||
|
|
||||||
initializeIcons(/* optional base url */);
|
|
||||||
|
|
||||||
// TODO: Encapsulate and reuse all global variables as environment variables
|
|
||||||
window.authType = AuthType.AAD;
|
|
||||||
|
|
||||||
initializeConfiguration().then(config => {
|
|
||||||
if (config.platform === Platform.Hosted) {
|
|
||||||
try {
|
|
||||||
Hosted.initializeExplorer().then(
|
|
||||||
(explorer: Explorer) => {
|
|
||||||
applyExplorerBindings(explorer);
|
|
||||||
Hosted.configureTokenValidationDisplayPrompt(explorer);
|
|
||||||
},
|
|
||||||
(error: any) => {
|
|
||||||
try {
|
|
||||||
const uninitializedExplorer: Explorer = Hosted.getUninitializedExplorerForGuestAccess();
|
|
||||||
window.dataExplorer = uninitializedExplorer;
|
|
||||||
ko.applyBindings(uninitializedExplorer);
|
|
||||||
BindingHandlersRegisterer.registerBindingHandlers();
|
|
||||||
if (window.authType !== AuthType.AAD) {
|
|
||||||
uninitializedExplorer.isRefreshingExplorer(false);
|
|
||||||
uninitializedExplorer.displayConnectExplorerForm();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
} else if (config.platform === Platform.Emulator) {
|
|
||||||
window.authType = AuthType.MasterKey;
|
|
||||||
const explorer = Emulator.initializeExplorer();
|
|
||||||
applyExplorerBindings(explorer);
|
|
||||||
} else if (config.platform === Platform.Portal) {
|
|
||||||
TelemetryProcessor.trace(Action.InitializeDataExplorer, ActionModifiers.Open, {});
|
|
||||||
const explorer = Portal.initializeExplorer();
|
|
||||||
TelemetryProcessor.trace(Action.InitializeDataExplorer, ActionModifiers.IFrameReady, {});
|
|
||||||
applyExplorerBindings(explorer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
452
src/Main.tsx
Normal file
452
src/Main.tsx
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
// CSS Dependencies
|
||||||
|
import "bootstrap/dist/css/bootstrap.css";
|
||||||
|
import "../less/documentDB.less";
|
||||||
|
import "../less/tree.less";
|
||||||
|
import "../less/forms.less";
|
||||||
|
import "../less/menus.less";
|
||||||
|
import "../less/infobox.less";
|
||||||
|
import "../less/messagebox.less";
|
||||||
|
import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less";
|
||||||
|
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
|
||||||
|
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
|
||||||
|
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
|
||||||
|
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
|
||||||
|
import "./Explorer/Controls/DynamicList/DynamicListComponent.less";
|
||||||
|
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
|
||||||
|
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
|
||||||
|
import "../less/TableStyles/queryBuilder.less";
|
||||||
|
import "../externals/jquery.dataTables.min.css";
|
||||||
|
import "../less/TableStyles/fulldatatables.less";
|
||||||
|
import "../less/TableStyles/EntityEditor.less";
|
||||||
|
import "../less/TableStyles/CustomizeColumns.less";
|
||||||
|
import "../less/resourceTree.less";
|
||||||
|
import "../externals/jquery.typeahead.min.css";
|
||||||
|
import "../externals/jquery-ui.min.css";
|
||||||
|
import "../externals/jquery-ui.structure.min.css";
|
||||||
|
import "../externals/jquery-ui.theme.min.css";
|
||||||
|
import "./Explorer/Graph/NewVertexComponent/newVertexComponent.less";
|
||||||
|
import "./Explorer/Panes/GraphNewVertexPane.less";
|
||||||
|
import "./Explorer/Tabs/QueryTab.less";
|
||||||
|
import "./Explorer/Controls/TreeComponent/treeComponent.less";
|
||||||
|
import "./Explorer/Controls/Accordion/AccordionComponent.less";
|
||||||
|
import "./Explorer/SplashScreen/SplashScreenComponent.less";
|
||||||
|
import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
|
||||||
|
|
||||||
|
// Image Dependencies
|
||||||
|
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
||||||
|
import "../images/favicon.ico";
|
||||||
|
|
||||||
|
import "./Shared/appInsights";
|
||||||
|
import "babel-polyfill";
|
||||||
|
import "es6-symbol/implement";
|
||||||
|
import "webcrypto-liner/build/webcrypto-liner.shim.min";
|
||||||
|
import "./Libs/jquery";
|
||||||
|
import "bootstrap/dist/js/npm";
|
||||||
|
import "../externals/jquery.typeahead.min.js";
|
||||||
|
import "../externals/jquery-ui.min.js";
|
||||||
|
import "../externals/adal.js";
|
||||||
|
import "promise-polyfill/src/polyfill";
|
||||||
|
import "abort-controller/polyfill";
|
||||||
|
import "whatwg-fetch";
|
||||||
|
import "es6-object-assign/auto";
|
||||||
|
import "promise.prototype.finally/auto";
|
||||||
|
import "object.entries/auto";
|
||||||
|
import "./Libs/is-integer-polyfill";
|
||||||
|
import "url-polyfill/url-polyfill.min";
|
||||||
|
|
||||||
|
initializeIcons();
|
||||||
|
|
||||||
|
import * as ko from "knockout";
|
||||||
|
import * as TelemetryProcessor from "./Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import { Action, ActionModifiers } from "./Shared/Telemetry/TelemetryConstants";
|
||||||
|
|
||||||
|
import { BindingHandlersRegisterer } from "./Bindings/BindingHandlersRegisterer";
|
||||||
|
import * as Emulator from "./Platform/Emulator/Main";
|
||||||
|
import Hosted from "./Platform/Hosted/Main";
|
||||||
|
import * as Portal from "./Platform/Portal/Main";
|
||||||
|
import { AuthType } from "./AuthType";
|
||||||
|
|
||||||
|
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
||||||
|
import { applyExplorerBindings } from "./applyExplorerBindings";
|
||||||
|
import { initializeConfiguration, Platform } from "./ConfigContext";
|
||||||
|
import Explorer from "./Explorer/Explorer";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import errorImage from "../images/error.svg";
|
||||||
|
import copyImage from "../images/Copy.svg";
|
||||||
|
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
||||||
|
import refreshImg from "../images/refresh-cosmos.svg";
|
||||||
|
import arrowLeftImg from "../images/imgarrowlefticon.svg";
|
||||||
|
import { KOCommentEnd, KOCommentIfStart } from "./koComment";
|
||||||
|
|
||||||
|
// TODO: Encapsulate and reuse all global variables as environment variables
|
||||||
|
window.authType = AuthType.AAD;
|
||||||
|
|
||||||
|
const App: React.FunctionComponent = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
initializeConfiguration().then(config => {
|
||||||
|
if (config.platform === Platform.Hosted) {
|
||||||
|
try {
|
||||||
|
Hosted.initializeExplorer().then(
|
||||||
|
(explorer: Explorer) => {
|
||||||
|
applyExplorerBindings(explorer);
|
||||||
|
Hosted.configureTokenValidationDisplayPrompt(explorer);
|
||||||
|
},
|
||||||
|
(error: unknown) => {
|
||||||
|
try {
|
||||||
|
const uninitializedExplorer: Explorer = Hosted.getUninitializedExplorerForGuestAccess();
|
||||||
|
window.dataExplorer = uninitializedExplorer;
|
||||||
|
ko.applyBindings(uninitializedExplorer);
|
||||||
|
BindingHandlersRegisterer.registerBindingHandlers();
|
||||||
|
if (window.authType !== AuthType.AAD) {
|
||||||
|
uninitializedExplorer.isRefreshingExplorer(false);
|
||||||
|
uninitializedExplorer.displayConnectExplorerForm();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
} else if (config.platform === Platform.Emulator) {
|
||||||
|
window.authType = AuthType.MasterKey;
|
||||||
|
const explorer = Emulator.initializeExplorer();
|
||||||
|
applyExplorerBindings(explorer);
|
||||||
|
} else if (config.platform === Platform.Portal) {
|
||||||
|
TelemetryProcessor.trace(Action.InitializeDataExplorer, ActionModifiers.Open, {});
|
||||||
|
const explorer = Portal.initializeExplorer();
|
||||||
|
TelemetryProcessor.trace(Action.InitializeDataExplorer, ActionModifiers.IFrameReady, {});
|
||||||
|
applyExplorerBindings(explorer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flexContainer">
|
||||||
|
<div id="divExplorer" className="flexContainer hideOverflows" style={{ display: "none" }}>
|
||||||
|
{/* Main Command Bar - Start */}
|
||||||
|
<div data-bind="react: commandBarComponentAdapter" />
|
||||||
|
{/* Main Command Bar - End */}
|
||||||
|
{/* Share url flyout - Start */}
|
||||||
|
<div
|
||||||
|
id="shareDataAccessFlyout"
|
||||||
|
className="shareDataAccessFlyout"
|
||||||
|
data-bind="visible: shouldShowShareDialogContents"
|
||||||
|
>
|
||||||
|
<div className="shareDataAccessFlyoutContent">
|
||||||
|
<div className="urlContainer">
|
||||||
|
<span className="urlContentText">
|
||||||
|
Open this database account in a new browser tab with Cosmos DB Explorer. Or copy the read-write or read
|
||||||
|
only access urls below to share with others. For security purposes, the URLs grant time-bound access to
|
||||||
|
the account. When access expires, you can reconnect, using a valid connection string for the account.
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<div
|
||||||
|
className="toggles"
|
||||||
|
data-bind="event: { keydown: onToggleKeyDown }, visible: shareAccessData().readWriteUrl != null"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label="Read-Write and Read toggle"
|
||||||
|
>
|
||||||
|
<div className="tab">
|
||||||
|
<input type="radio" className="radio" defaultValue="readwrite" />
|
||||||
|
<span
|
||||||
|
className="toggleSwitch"
|
||||||
|
role="presentation"
|
||||||
|
data-bind="click: toggleReadWrite, css:{ selectedToggle: isReadWriteToggled(), unselectedToggle: !isReadWriteToggled() }"
|
||||||
|
>
|
||||||
|
Read-Write
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="tab">
|
||||||
|
<input type="radio" className="radio" defaultValue="read" />
|
||||||
|
<span
|
||||||
|
className="toggleSwitch"
|
||||||
|
role="presentation"
|
||||||
|
data-bind="click: toggleRead, css:{ selectedToggle: isReadToggled(), unselectedToggle: !isReadToggled() }"
|
||||||
|
>
|
||||||
|
Read
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="urlSpace">
|
||||||
|
<input
|
||||||
|
id="shareUrlLink"
|
||||||
|
aria-label="Share url link"
|
||||||
|
className="shareLink"
|
||||||
|
type="text"
|
||||||
|
read-only
|
||||||
|
data-bind="value: shareAccessUrl"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="urlTokenCopyInfoTooltip"
|
||||||
|
data-bind="click: copyUrlLink, event: { keypress: onCopyUrlLinkKeyPress }"
|
||||||
|
aria-label="Copy url link"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<img src={copyImage} alt="Copy link" />
|
||||||
|
<span className="urlTokenCopyTooltiptext" data-bind="text: shareUrlCopyHelperText" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Share url flyout - End */}
|
||||||
|
{/* Collections Tree and Tabs - Begin */}
|
||||||
|
<div className="resourceTreeAndTabs">
|
||||||
|
{/* Collections Tree - Start */}
|
||||||
|
<div id="resourcetree" data-test="resourceTreeId" className="resourceTree">
|
||||||
|
<div className="collectionsTreeWithSplitter">
|
||||||
|
{/* Collections Tree Expanded - Start */}
|
||||||
|
<div
|
||||||
|
id="main"
|
||||||
|
className="main"
|
||||||
|
data-bind="
|
||||||
|
visible: isLeftPaneExpanded()"
|
||||||
|
>
|
||||||
|
{/* Collections Window - - Start */}
|
||||||
|
<div id="mainslide" className="flexContainer">
|
||||||
|
{/* Collections Window Title/Command Bar - Start */}
|
||||||
|
<div className="collectiontitle">
|
||||||
|
<div className="coltitle">
|
||||||
|
<span className="titlepadcol" data-bind="text: collectionTitle" />
|
||||||
|
<div className="float-right">
|
||||||
|
<span
|
||||||
|
className="padimgcolrefresh"
|
||||||
|
data-test="refreshTree"
|
||||||
|
role="button"
|
||||||
|
data-bind="
|
||||||
|
click: onRefreshResourcesClick, clickBubble: false, event: { keypress: onRefreshDatabasesKeyPress }"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label="Refresh tree"
|
||||||
|
title="Refresh tree"
|
||||||
|
>
|
||||||
|
<img className="refreshcol" src={refreshImg} data-bind="attr: { alt: refreshTreeTitle }" />
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="padimgcolrefresh1"
|
||||||
|
id="expandToggleLeftPaneButton"
|
||||||
|
role="button"
|
||||||
|
data-bind="
|
||||||
|
click: toggleLeftPaneExpanded, event: { keypress: toggleLeftPaneExpandedKeyPress }"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label="Collapse Tree"
|
||||||
|
title="Collapse Tree"
|
||||||
|
>
|
||||||
|
<img className="refreshcol1" src={arrowLeftImg} alt="Hide" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Collections Window Title/Command Bar - End */}
|
||||||
|
|
||||||
|
{!window.dataExplorer?.isAuthWithResourceToken() && (
|
||||||
|
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
|
||||||
|
)}
|
||||||
|
{window.dataExplorer?.isAuthWithResourceToken() && (
|
||||||
|
<div style={{ overflowY: "auto" }} data-bind="react:resourceTreeForResourceToken" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Collections Window - End */}
|
||||||
|
</div>
|
||||||
|
{/* Collections Tree Expanded - End */}
|
||||||
|
{/* Collections Tree Collapsed - Start */}
|
||||||
|
<div
|
||||||
|
id="mini"
|
||||||
|
className="mini toggle-mini"
|
||||||
|
data-bind="visible: !isLeftPaneExpanded()
|
||||||
|
attr: { style: { width: collapsedResourceTreeWidth }}"
|
||||||
|
>
|
||||||
|
<div className="main-nav nav">
|
||||||
|
<ul className="nav">
|
||||||
|
<li
|
||||||
|
className="resourceTreeCollapse"
|
||||||
|
id="collapseToggleLeftPaneButton"
|
||||||
|
role="button"
|
||||||
|
data-bind="event: { keypress: toggleLeftPaneExpandedKeyPress }"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label="Expand Tree"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="leftarrowCollapsed"
|
||||||
|
data-bind="
|
||||||
|
click: toggleLeftPaneExpanded"
|
||||||
|
>
|
||||||
|
<img className="arrowCollapsed" src={arrowLeftImg} alt="Expand" />
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="collectionCollapsed"
|
||||||
|
data-bind="
|
||||||
|
click: toggleLeftPaneExpanded"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
data-bind="
|
||||||
|
text: collectionTitle"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Collections Tree Collapsed - End */}
|
||||||
|
</div>
|
||||||
|
{/* Splitter - Start */}
|
||||||
|
<div className="splitter ui-resizable-handle ui-resizable-e" id="h_splitter1" />
|
||||||
|
{/* Splitter - End */}
|
||||||
|
</div>
|
||||||
|
{/* Collections Tree - End */}
|
||||||
|
<div
|
||||||
|
className="connectExplorerContainer"
|
||||||
|
data-bind="visible: !isRefreshingExplorer() && tabsManager.openedTabs().length === 0"
|
||||||
|
>
|
||||||
|
<form className="connectExplorerFormContainer">
|
||||||
|
<div className="connectExplorer" data-bind="react: splashScreenAdapter" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="tabsManagerContainer"
|
||||||
|
data-bind='component: { name: "tabs-manager", params: {data: tabsManager} }'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Collections Tree and Tabs - End */}
|
||||||
|
<div
|
||||||
|
className="dataExplorerErrorConsoleContainer"
|
||||||
|
role="contentinfo"
|
||||||
|
aria-label="Notification console"
|
||||||
|
id="explorerNotificationConsole"
|
||||||
|
data-bind="react: notificationConsoleComponentAdapter"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Explorer Connection - Encryption Token / AAD - Start */}
|
||||||
|
<div id="connectExplorer" className="connectExplorerContainer" style={{ display: "none" }}>
|
||||||
|
<div className="connectExplorerFormContainer">
|
||||||
|
<div className="connectExplorer">
|
||||||
|
<p className="connectExplorerContent">
|
||||||
|
<img src={hdeConnectImage} alt="Azure Cosmos DB" />
|
||||||
|
</p>
|
||||||
|
<p className="welcomeText">Welcome to Azure Cosmos DB</p>
|
||||||
|
<div id="connectWithAad">
|
||||||
|
<input
|
||||||
|
className="filterbtnstyle"
|
||||||
|
data-test="cosmosdb-signinBtn"
|
||||||
|
type="button"
|
||||||
|
defaultValue="Sign In"
|
||||||
|
data-bind="click: $data.signInAad"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
className="switchConnectTypeText"
|
||||||
|
data-test="cosmosdb-connectionString"
|
||||||
|
data-bind="click: $data.onSwitchToConnectionString"
|
||||||
|
>
|
||||||
|
Connect to your account with connection string
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<form id="connectWithConnectionString" data-bind="submit: renewToken" style={{ display: "none" }}>
|
||||||
|
<p className="connectExplorerContent connectStringText">Connect to your account with connection string</p>
|
||||||
|
<p className="connectExplorerContent">
|
||||||
|
<input
|
||||||
|
className="inputToken"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="Please enter a connection string"
|
||||||
|
data-bind="value: tokenForRenewal"
|
||||||
|
/>
|
||||||
|
<span className="errorDetailsInfoTooltip" data-bind="visible: renewTokenError().length > 0">
|
||||||
|
<img className="errorImg" src={errorImage} alt="Error notification" />
|
||||||
|
<span className="errorDetails" data-bind="text: renewTokenError" />
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p className="connectExplorerContent">
|
||||||
|
<input className="filterbtnstyle" type="submit" value="Connect" />
|
||||||
|
</p>
|
||||||
|
<p className="switchConnectTypeText" data-bind="click: $data.signInAad">
|
||||||
|
Sign In with Azure Account
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Explorer Connection - Encryption Token / AAD - End */}
|
||||||
|
{/* Global loader - Start */}
|
||||||
|
<div className="splashLoaderContainer" data-bind="visible: isRefreshingExplorer">
|
||||||
|
<div className="splashLoaderContentContainer">
|
||||||
|
<p className="connectExplorerContent">
|
||||||
|
<img src={hdeConnectImage} alt="Azure Cosmos DB" />
|
||||||
|
</p>
|
||||||
|
<p className="splashLoaderTitle" id="explorerLoadingStatusTitle">
|
||||||
|
Welcome to Azure Cosmos DB
|
||||||
|
</p>
|
||||||
|
<p className="splashLoaderText" id="explorerLoadingStatusText" role="alert">
|
||||||
|
Connecting...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Global loader - End */}
|
||||||
|
<div data-bind="react:uploadItemsPaneAdapter" />
|
||||||
|
<div data-bind='component: { name: "add-database-pane", params: {data: addDatabasePane} }' />
|
||||||
|
<div data-bind='component: { name: "add-collection-pane", params: { data: addCollectionPane} }' />
|
||||||
|
<div data-bind='component: { name: "delete-collection-confirmation-pane", params: { data: deleteCollectionConfirmationPane} }' />
|
||||||
|
<div data-bind='component: { name: "delete-database-confirmation-pane", params: { data: deleteDatabaseConfirmationPane} }' />
|
||||||
|
<div data-bind='component: { name: "graph-new-vertex-pane", params: { data: newVertexPane} }' />
|
||||||
|
<div data-bind='component: { name: "graph-styling-pane", params: { data: graphStylingPane} }' />
|
||||||
|
<div data-bind='component: { name: "table-add-entity-pane", params: { data: addTableEntityPane} }' />
|
||||||
|
<div data-bind='component: { name: "table-edit-entity-pane", params: { data: editTableEntityPane} }' />
|
||||||
|
<div data-bind='component: { name: "table-column-options-pane", params: { data: tableColumnOptionsPane} }' />
|
||||||
|
<div data-bind='component: { name: "table-query-select-pane", params: { data: querySelectPane} }' />
|
||||||
|
<div data-bind='component: { name: "cassandra-add-collection-pane", params: { data: cassandraAddCollectionPane} }' />
|
||||||
|
<div data-bind='component: { name: "settings-pane", params: { data: settingsPane} }' />
|
||||||
|
<div data-bind='component: { name: "upload-items-pane", params: { data: uploadItemsPane} }' />
|
||||||
|
<div data-bind='component: { name: "load-query-pane", params: { data: loadQueryPane} }' />
|
||||||
|
<div data-bind='component: { name: "execute-sproc-params-pane", params: { data: executeSprocParamsPane} }' />
|
||||||
|
<div data-bind='component: { name: "renew-adhoc-access-pane", params: { data: renewAdHocAccessPane} }' />
|
||||||
|
<div data-bind='component: { name: "save-query-pane", params: { data: saveQueryPane} }' />
|
||||||
|
<div data-bind='component: { name: "browse-queries-pane", params: { data: browseQueriesPane} }' />
|
||||||
|
<div data-bind='component: { name: "upload-file-pane", params: { data: uploadFilePane} }' />
|
||||||
|
<div data-bind='component: { name: "string-input-pane", params: { data: stringInputPane} }' />
|
||||||
|
<div data-bind='component: { name: "setup-notebooks-pane", params: { data: setupNotebooksPane} }' />
|
||||||
|
<KOCommentIfStart if="isGitHubPaneEnabled" />
|
||||||
|
<div data-bind='component: { name: "github-repos-pane", params: { data: gitHubReposPane } }' />
|
||||||
|
<KOCommentEnd />
|
||||||
|
<KOCommentIfStart if="isPublishNotebookPaneEnabled" />
|
||||||
|
<div data-bind="react: publishNotebookPaneAdapter" />
|
||||||
|
<KOCommentEnd />
|
||||||
|
<KOCommentIfStart if="isCopyNotebookPaneEnabled" />
|
||||||
|
<div data-bind="react: copyNotebookPaneAdapter" />
|
||||||
|
<KOCommentEnd />
|
||||||
|
{/* Global access token expiration dialog - Start */}
|
||||||
|
<div
|
||||||
|
id="dataAccessTokenModal"
|
||||||
|
className="dataAccessTokenModal"
|
||||||
|
style={{ display: "none" }}
|
||||||
|
data-bind="visible: shouldShowDataAccessExpiryDialog"
|
||||||
|
>
|
||||||
|
<div className="dataAccessTokenModalContent">
|
||||||
|
<p className="dataAccessTokenExpireText">Please reconnect to the account using the connection string.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Global access token expiration dialog - End */}
|
||||||
|
{/* Context switch prompt - Start */}
|
||||||
|
<div
|
||||||
|
id="contextSwitchPrompt"
|
||||||
|
className="dataAccessTokenModal"
|
||||||
|
style={{ display: "none" }}
|
||||||
|
data-bind="visible: shouldShowContextSwitchPrompt"
|
||||||
|
>
|
||||||
|
<div className="dataAccessTokenModalContent">
|
||||||
|
<p className="dataAccessTokenExpireText">
|
||||||
|
Please save your work before you switch! When you switch to a different Azure Cosmos DB account, current
|
||||||
|
Data Explorer tabs will be closed.
|
||||||
|
</p>
|
||||||
|
<p className="dataAccessTokenExpireText">Proceed anyway?</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div data-bind="react: dialogComponentAdapter" />
|
||||||
|
<div data-bind="react: addSynapseLinkDialog" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ReactDOM.render(<App />, document.body);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SubscriptionType } from "../Contracts/ViewModels";
|
import { SubscriptionType } from "../Contracts/SubscriptionType";
|
||||||
|
|
||||||
export const hoursInAMonth = 730;
|
export const hoursInAMonth = 730;
|
||||||
export class AutoscalePricing {
|
export class AutoscalePricing {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { DatabaseAccount } from "./Contracts/DataModels";
|
import { DatabaseAccount } from "./Contracts/DataModels";
|
||||||
|
import { SubscriptionType } from "./Contracts/SubscriptionType";
|
||||||
import { DefaultAccountExperienceType } from "./DefaultAccountExperienceType";
|
import { DefaultAccountExperienceType } from "./DefaultAccountExperienceType";
|
||||||
|
|
||||||
interface UserContext {
|
interface UserContext {
|
||||||
@@ -12,6 +13,7 @@ interface UserContext {
|
|||||||
resourceToken?: string;
|
resourceToken?: string;
|
||||||
defaultExperience?: DefaultAccountExperienceType;
|
defaultExperience?: DefaultAccountExperienceType;
|
||||||
useSDKOperations?: boolean;
|
useSDKOperations?: boolean;
|
||||||
|
subscriptionType?: SubscriptionType;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userContext: Readonly<UserContext> = {} as const;
|
const userContext: Readonly<UserContext> = {} as const;
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import Explorer from "./Explorer/Explorer";
|
|||||||
|
|
||||||
export const applyExplorerBindings = (explorer: Explorer) => {
|
export const applyExplorerBindings = (explorer: Explorer) => {
|
||||||
if (!!explorer) {
|
if (!!explorer) {
|
||||||
ko.applyBindings(explorer);
|
|
||||||
// This message should ideally be sent immediately after explorer has been initialized for optimal data explorer load times.
|
// This message should ideally be sent immediately after explorer has been initialized for optimal data explorer load times.
|
||||||
// TODO: Send another message to describe that the bindings have been applied, and handle message transfers accordingly in the portal
|
// TODO: Send another message to describe that the bindings have been applied, and handle message transfers accordingly in the portal
|
||||||
sendMessage("ready");
|
sendMessage("ready");
|
||||||
window.dataExplorer = explorer;
|
window.dataExplorer = explorer;
|
||||||
BindingHandlersRegisterer.registerBindingHandlers();
|
BindingHandlersRegisterer.registerBindingHandlers();
|
||||||
|
ko.applyBindings(explorer);
|
||||||
$("#divExplorer").show();
|
$("#divExplorer").show();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,329 +8,5 @@
|
|||||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body></body>
|
||||||
<div class="flexContainer">
|
|
||||||
<div id="divExplorer" class="flexContainer hideOverflows" style="display: none">
|
|
||||||
<!-- Main Command Bar - Start -->
|
|
||||||
<div data-bind="react: commandBarComponentAdapter"></div>
|
|
||||||
<!-- Main Command Bar - End -->
|
|
||||||
<!-- Share url flyout - Start -->
|
|
||||||
<div
|
|
||||||
id="shareDataAccessFlyout"
|
|
||||||
class="shareDataAccessFlyout"
|
|
||||||
data-bind="visible: shouldShowShareDialogContents"
|
|
||||||
>
|
|
||||||
<div class="shareDataAccessFlyoutContent">
|
|
||||||
<div class="urlContainer">
|
|
||||||
<span class="urlContentText"
|
|
||||||
>Open this database account in a new browser tab with Cosmos DB Explorer. Or copy the read-write or read
|
|
||||||
only access urls below to share with others. For security purposes, the URLs grant time-bound access to
|
|
||||||
the account. When access expires, you can reconnect, using a valid connection string for the
|
|
||||||
account.</span
|
|
||||||
>
|
|
||||||
<br />
|
|
||||||
<div
|
|
||||||
class="toggles"
|
|
||||||
data-bind="event: { keydown: onToggleKeyDown }, visible: shareAccessData().readWriteUrl != null"
|
|
||||||
tabindex="0"
|
|
||||||
aria-label="Read-Write and Read toggle"
|
|
||||||
>
|
|
||||||
<div class="tab">
|
|
||||||
<input type="radio" class="radio" value="readwrite" />
|
|
||||||
<span
|
|
||||||
class="toggleSwitch"
|
|
||||||
role="presentation"
|
|
||||||
data-bind="click: toggleReadWrite, css:{ selectedToggle: isReadWriteToggled(), unselectedToggle: !isReadWriteToggled() }"
|
|
||||||
>Read-Write</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="tab">
|
|
||||||
<input type="radio" class="radio" value="read" />
|
|
||||||
<span
|
|
||||||
class="toggleSwitch"
|
|
||||||
role="presentation"
|
|
||||||
data-bind="click: toggleRead, css:{ selectedToggle: isReadToggled(), unselectedToggle: !isReadToggled() }"
|
|
||||||
>Read</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="urlSpace">
|
|
||||||
<input
|
|
||||||
id="shareUrlLink"
|
|
||||||
aria-label="Share url link"
|
|
||||||
class="shareLink"
|
|
||||||
type="text"
|
|
||||||
read-only
|
|
||||||
data-bind="value: shareAccessUrl"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="urlTokenCopyInfoTooltip"
|
|
||||||
data-bind="click: copyUrlLink, event: { keypress: onCopyUrlLinkKeyPress }"
|
|
||||||
aria-label="Copy url link"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<img src="/Copy.svg" alt="Copy link" />
|
|
||||||
<span class="urlTokenCopyTooltiptext" data-bind="text: shareUrlCopyHelperText"></span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Share url flyout - End -->
|
|
||||||
|
|
||||||
<!-- Collections Tree and Tabs - Begin -->
|
|
||||||
<div class="resourceTreeAndTabs">
|
|
||||||
<!-- Collections Tree - Start -->
|
|
||||||
<div id="resourcetree" data-test="resourceTreeId" class="resourceTree">
|
|
||||||
<div class="collectionsTreeWithSplitter">
|
|
||||||
<!-- Collections Tree Expanded - Start -->
|
|
||||||
<div
|
|
||||||
id="main"
|
|
||||||
class="main"
|
|
||||||
data-bind="
|
|
||||||
visible: isLeftPaneExpanded()"
|
|
||||||
>
|
|
||||||
<!-- Collections Window - - Start -->
|
|
||||||
<div id="mainslide" class="flexContainer">
|
|
||||||
<!-- Collections Window Title/Command Bar - Start -->
|
|
||||||
<div class="collectiontitle">
|
|
||||||
<div class="coltitle">
|
|
||||||
<span class="titlepadcol" data-bind="text: collectionTitle"></span>
|
|
||||||
<div class="float-right">
|
|
||||||
<span
|
|
||||||
class="padimgcolrefresh"
|
|
||||||
data-test="refreshTree"
|
|
||||||
role="button"
|
|
||||||
data-bind="
|
|
||||||
click: onRefreshResourcesClick, clickBubble: false, event: { keypress: onRefreshDatabasesKeyPress }"
|
|
||||||
tabindex="0"
|
|
||||||
aria-label="Refresh tree"
|
|
||||||
title="Refresh tree"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="refreshcol"
|
|
||||||
src="/refresh-cosmos.svg"
|
|
||||||
data-bind="attr: { alt: refreshTreeTitle }"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="padimgcolrefresh1"
|
|
||||||
id="expandToggleLeftPaneButton"
|
|
||||||
role="button"
|
|
||||||
data-bind="
|
|
||||||
click: toggleLeftPaneExpanded, event: { keypress: toggleLeftPaneExpandedKeyPress }"
|
|
||||||
tabindex="0"
|
|
||||||
aria-label="Collapse Tree"
|
|
||||||
title="Collapse Tree"
|
|
||||||
>
|
|
||||||
<img class="refreshcol1" src="/imgarrowlefticon.svg" alt="Hide" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Collections Window Title/Command Bar - End -->
|
|
||||||
<!-- ko if: !isAuthWithResourceToken() -->
|
|
||||||
<div style="overflow-y: auto" data-bind="react:resourceTree"></div>
|
|
||||||
<!-- /ko -->
|
|
||||||
<!-- ko if: isAuthWithResourceToken() -->
|
|
||||||
<div style="overflow-y: auto" data-bind="react:resourceTreeForResourceToken"></div>
|
|
||||||
<!-- /ko -->
|
|
||||||
</div>
|
|
||||||
<!-- Collections Window - End -->
|
|
||||||
</div>
|
|
||||||
<!-- Collections Tree Expanded - End -->
|
|
||||||
|
|
||||||
<!-- Collections Tree Collapsed - Start -->
|
|
||||||
<div
|
|
||||||
id="mini"
|
|
||||||
class="mini toggle-mini"
|
|
||||||
data-bind="visible: !isLeftPaneExpanded()
|
|
||||||
attr: { style: { width: collapsedResourceTreeWidth }}"
|
|
||||||
>
|
|
||||||
<div class="main-nav nav">
|
|
||||||
<ul class="nav">
|
|
||||||
<li
|
|
||||||
class="resourceTreeCollapse"
|
|
||||||
id="collapseToggleLeftPaneButton"
|
|
||||||
role="button"
|
|
||||||
data-bind="event: { keypress: toggleLeftPaneExpandedKeyPress }"
|
|
||||||
tabindex="0"
|
|
||||||
aria-label="Expand Tree"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="leftarrowCollapsed"
|
|
||||||
data-bind="
|
|
||||||
click: toggleLeftPaneExpanded"
|
|
||||||
>
|
|
||||||
<img class="arrowCollapsed" src="/imgarrowlefticon.svg" alt="Expand" />
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="collectionCollapsed"
|
|
||||||
data-bind="
|
|
||||||
click: toggleLeftPaneExpanded"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-bind="
|
|
||||||
text: collectionTitle"
|
|
||||||
></span>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Collections Tree Collapsed - End -->
|
|
||||||
</div>
|
|
||||||
<!-- Splitter - Start -->
|
|
||||||
<div class="splitter ui-resizable-handle ui-resizable-e" id="h_splitter1"></div>
|
|
||||||
<!-- Splitter - End -->
|
|
||||||
</div>
|
|
||||||
<!-- Collections Tree - End -->
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="connectExplorerContainer"
|
|
||||||
data-bind="visible: !isRefreshingExplorer() && tabsManager.openedTabs().length === 0"
|
|
||||||
>
|
|
||||||
<form class="connectExplorerFormContainer">
|
|
||||||
<div class="connectExplorer" data-bind="react: splashScreenAdapter"></div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<tabs-manager
|
|
||||||
class="tabsManagerContainer"
|
|
||||||
params="{data: tabsManager}"
|
|
||||||
data-bind="visible: tabsManager.openedTabs().length > 0"
|
|
||||||
></tabs-manager>
|
|
||||||
</div>
|
|
||||||
<!-- Collections Tree and Tabs - End -->
|
|
||||||
<div
|
|
||||||
class="dataExplorerErrorConsoleContainer"
|
|
||||||
role="contentinfo"
|
|
||||||
aria-label="Notification console"
|
|
||||||
id="explorerNotificationConsole"
|
|
||||||
data-bind="react: notificationConsoleComponentAdapter"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Explorer Connection - Encryption Token / AAD - Start -->
|
|
||||||
<div id="connectExplorer" class="connectExplorerContainer" style="display: none;">
|
|
||||||
<div class="connectExplorerFormContainer">
|
|
||||||
<div class="connectExplorer">
|
|
||||||
<p class="connectExplorerContent"><img src="/HdeConnectCosmosDB.svg" alt="Azure Cosmos DB" /></p>
|
|
||||||
<p class="welcomeText">Welcome to Azure Cosmos DB</p>
|
|
||||||
<div id="connectWithAad">
|
|
||||||
<input
|
|
||||||
class="filterbtnstyle"
|
|
||||||
data-test="cosmosdb-signinBtn"
|
|
||||||
type="button"
|
|
||||||
value="Sign In"
|
|
||||||
data-bind="click: $data.signInAad"
|
|
||||||
/>
|
|
||||||
<p
|
|
||||||
class="switchConnectTypeText"
|
|
||||||
data-test="cosmosdb-connectionString"
|
|
||||||
data-bind="click: $data.onSwitchToConnectionString"
|
|
||||||
>
|
|
||||||
Connect to your account with connection string
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<form id="connectWithConnectionString" data-bind="submit: renewToken" style="display: none;">
|
|
||||||
<p class="connectExplorerContent connectStringText">Connect to your account with connection string</p>
|
|
||||||
<p class="connectExplorerContent">
|
|
||||||
<input
|
|
||||||
class="inputToken"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
placeholder="Please enter a connection string"
|
|
||||||
data-bind="value: tokenForRenewal"
|
|
||||||
/>
|
|
||||||
<span class="errorDetailsInfoTooltip" data-bind="visible: renewTokenError().length > 0">
|
|
||||||
<img class="errorImg" src="/error.svg" alt="Error notification" />
|
|
||||||
<span class="errorDetails" data-bind="text: renewTokenError"></span>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<p class="connectExplorerContent"><input class="filterbtnstyle" type="submit" value="Connect" /></p>
|
|
||||||
<p class="switchConnectTypeText" data-bind="click: $data.signInAad">Sign In with Azure Account</p>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Explorer Connection - Encryption Token / AAD - End -->
|
|
||||||
|
|
||||||
<!-- Global loader - Start -->
|
|
||||||
<div class="splashLoaderContainer" data-bind="visible: isRefreshingExplorer">
|
|
||||||
<div class="splashLoaderContentContainer">
|
|
||||||
<p class="connectExplorerContent"><img src="/HdeConnectCosmosDB.svg" alt="Azure Cosmos DB" /></p>
|
|
||||||
<p class="splashLoaderTitle" id="explorerLoadingStatusTitle">Welcome to Azure Cosmos DB</p>
|
|
||||||
<p class="splashLoaderText" id="explorerLoadingStatusText" role="alert">Connecting...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Global loader - End -->
|
|
||||||
<div data-bind="react:uploadItemsPaneAdapter"></div>
|
|
||||||
<add-database-pane params="{data: addDatabasePane}"></add-database-pane>
|
|
||||||
<add-collection-pane params="{data: addCollectionPane}"></add-collection-pane>
|
|
||||||
<delete-collection-confirmation-pane params="{data: deleteCollectionConfirmationPane}">
|
|
||||||
</delete-collection-confirmation-pane>
|
|
||||||
<delete-database-confirmation-pane params="{data: deleteDatabaseConfirmationPane}">
|
|
||||||
</delete-database-confirmation-pane>
|
|
||||||
<graph-new-vertex-pane params="{data: newVertexPane}"></graph-new-vertex-pane>
|
|
||||||
<graph-styling-pane params="{data: graphStylingPane}"></graph-styling-pane>
|
|
||||||
<table-add-entity-pane params="{data: addTableEntityPane}"></table-add-entity-pane>
|
|
||||||
<table-edit-entity-pane params="{data: editTableEntityPane}"></table-edit-entity-pane>
|
|
||||||
<table-column-options-pane params="{data: tableColumnOptionsPane}"></table-column-options-pane>
|
|
||||||
<table-query-select-pane params="{data: querySelectPane}"></table-query-select-pane>
|
|
||||||
<cassandra-add-collection-pane params="{data: cassandraAddCollectionPane}"></cassandra-add-collection-pane>
|
|
||||||
<settings-pane params="{data: settingsPane}"></settings-pane>
|
|
||||||
<upload-items-pane params="{data: uploadItemsPane}"></upload-items-pane>
|
|
||||||
<load-query-pane params="{data: loadQueryPane}"></load-query-pane>
|
|
||||||
<execute-sproc-params-pane params="{data: executeSprocParamsPane}"></execute-sproc-params-pane>
|
|
||||||
<renew-adhoc-access-pane params="{data: renewAdHocAccessPane}"></renew-adhoc-access-pane>
|
|
||||||
<save-query-pane params="{data: saveQueryPane}"></save-query-pane>
|
|
||||||
<browse-queries-pane params="{data: browseQueriesPane}"></browse-queries-pane>
|
|
||||||
<upload-file-pane params="{data: uploadFilePane}"></upload-file-pane>
|
|
||||||
<string-input-pane params="{data: stringInputPane}"></string-input-pane>
|
|
||||||
<setup-notebooks-pane params="{data: setupNotebooksPane}"></setup-notebooks-pane>
|
|
||||||
|
|
||||||
<!-- ko if: isGitHubPaneEnabled -->
|
|
||||||
<github-repos-pane params="{data: gitHubReposPane}"></github-repos-pane>
|
|
||||||
<!-- /ko -->
|
|
||||||
|
|
||||||
<!-- ko if: isPublishNotebookPaneEnabled -->
|
|
||||||
<div data-bind="react: publishNotebookPaneAdapter"></div>
|
|
||||||
<!-- /ko -->
|
|
||||||
|
|
||||||
<!-- ko if: isCopyNotebookPaneEnabled -->
|
|
||||||
<div data-bind="react: copyNotebookPaneAdapter"></div>
|
|
||||||
<!-- /ko -->
|
|
||||||
|
|
||||||
<!-- Global access token expiration dialog - Start -->
|
|
||||||
<div
|
|
||||||
id="dataAccessTokenModal"
|
|
||||||
class="dataAccessTokenModal"
|
|
||||||
style="display: none"
|
|
||||||
data-bind="visible: shouldShowDataAccessExpiryDialog"
|
|
||||||
>
|
|
||||||
<div class="dataAccessTokenModalContent">
|
|
||||||
<p class="dataAccessTokenExpireText">Please reconnect to the account using the connection string.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Global access token expiration dialog - End -->
|
|
||||||
<!-- Context switch prompt - Start -->
|
|
||||||
<div
|
|
||||||
id="contextSwitchPrompt"
|
|
||||||
class="dataAccessTokenModal"
|
|
||||||
style="display: none"
|
|
||||||
data-bind="visible: shouldShowContextSwitchPrompt"
|
|
||||||
>
|
|
||||||
<div class="dataAccessTokenModalContent">
|
|
||||||
<p class="dataAccessTokenExpireText">
|
|
||||||
Please save your work before you switch! When you switch to a different Azure Cosmos DB account, current
|
|
||||||
Data Explorer tabs will be closed.
|
|
||||||
</p>
|
|
||||||
<p class="dataAccessTokenExpireText">Proceed anyway?</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div data-bind="react: dialogComponentAdapter"></div>
|
|
||||||
<div data-bind="react: addSynapseLinkDialog"></div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
20
src/koComment.tsx
Normal file
20
src/koComment.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/* eslint-disable react/prop-types */
|
||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
export const KOCommentIfStart: React.FunctionComponent<{ if: string }> = props => {
|
||||||
|
const el = useRef();
|
||||||
|
useEffect(() => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(el.current as any).outerHTML = `<!-- ko if: ${props.if} -->`;
|
||||||
|
}, []);
|
||||||
|
return <div ref={el} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const KOCommentEnd: React.FunctionComponent = () => {
|
||||||
|
const el = useRef();
|
||||||
|
useEffect(() => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(el.current as any).outerHTML = `<!-- /ko -->`;
|
||||||
|
}, []);
|
||||||
|
return <div ref={el} />;
|
||||||
|
};
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import "expect-puppeteer";
|
|
||||||
import { trackEvent, trackException } from "./utils";
|
|
||||||
|
|
||||||
jest.setTimeout(300000);
|
|
||||||
|
|
||||||
describe.skip("Collection CRUD", () => {
|
|
||||||
it("should complete collection crud", async () => {
|
|
||||||
try {
|
|
||||||
// Login to Azure Portal
|
|
||||||
await page.goto("https://portal.azure.com");
|
|
||||||
await page.waitFor("input[name=loginfmt]");
|
|
||||||
await page.type("input[name=loginfmt]", process.env.PORTAL_RUNNER_USERNAME);
|
|
||||||
await page.click("input[type=submit]");
|
|
||||||
await page.waitFor(3000);
|
|
||||||
await page.waitFor("input[name=loginfmt]");
|
|
||||||
await page.type("input[name=passwd]", process.env.PORTAL_RUNNER_PASSWORD);
|
|
||||||
await page.click("input[type=submit]");
|
|
||||||
await page.waitFor(3000);
|
|
||||||
await page.waitForNavigation();
|
|
||||||
await page.goto(
|
|
||||||
`https://ms.portal.azure.com/#@microsoft.onmicrosoft.com/resource/subscriptions/${process.env.PORTAL_RUNNER_SUBSCRIPTION}/resourceGroups/${process.env.PORTAL_RUNNER_RESOURCE_GROUP}/providers/Microsoft.DocumentDb/databaseAccounts/${process.env.PORTAL_RUNNER_DATABASE_ACCOUNT}/dataExplorer`
|
|
||||||
);
|
|
||||||
// Wait for page to settle
|
|
||||||
await page.waitFor(10000);
|
|
||||||
// Find Data Explorer iFrame
|
|
||||||
const frames = page.frames();
|
|
||||||
const dataExplorer = frames.find(frame => frame.url().includes("cosmos.azure.com"));
|
|
||||||
// Click "New Container"
|
|
||||||
const newContainerButton = await dataExplorer.$('button[data-test="New Container"]');
|
|
||||||
await newContainerButton.click();
|
|
||||||
// Wait for side pane to appear
|
|
||||||
await dataExplorer.waitFor(".contextual-pane-in");
|
|
||||||
// Fill out New Container form
|
|
||||||
const databaseIdInput = await dataExplorer.$("#databaseId");
|
|
||||||
await databaseIdInput.type("foo");
|
|
||||||
const collectionIdInput = await dataExplorer.$("#containerId");
|
|
||||||
await collectionIdInput.type("foo");
|
|
||||||
const partitionKeyInput = await dataExplorer.$('input[data-test="addCollection-partitionKeyValue"]');
|
|
||||||
await partitionKeyInput.type("/partitionKey");
|
|
||||||
trackEvent({ name: "ProductionRunnerSuccess" });
|
|
||||||
|
|
||||||
// TODO: Submit form and assert results
|
|
||||||
// cy.wrap($body)
|
|
||||||
// .find("#submitBtnAddCollection")
|
|
||||||
// .click();
|
|
||||||
// cy.wait(10000);
|
|
||||||
// cy.wrap($body)
|
|
||||||
// .find('div[data-test="resourceTreeId"]')
|
|
||||||
// .should("exist")
|
|
||||||
// .find('div[class="treeComponent dataResourceTree"]')
|
|
||||||
// .should("contain", dbId);
|
|
||||||
} catch (error) {
|
|
||||||
await page.screenshot({ path: "failure.png" });
|
|
||||||
trackException(error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -3,8 +3,11 @@ import { Frame } from "puppeteer";
|
|||||||
|
|
||||||
export async function login(connectionString: string): Promise<Frame> {
|
export async function login(connectionString: string): Promise<Frame> {
|
||||||
const prodUrl = process.env.DATA_EXPLORER_ENDPOINT;
|
const prodUrl = process.env.DATA_EXPLORER_ENDPOINT;
|
||||||
page.goto(prodUrl, { waitUntil: "networkidle2" });
|
await page.goto(prodUrl);
|
||||||
|
|
||||||
|
if (process.env.PLATFORM === "Emulator") {
|
||||||
|
return page.mainFrame();
|
||||||
|
}
|
||||||
// log in with connection string
|
// log in with connection string
|
||||||
const handle = await page.waitForSelector("iframe");
|
const handle = await page.waitForSelector("iframe");
|
||||||
const frame = await handle.contentFrame();
|
const frame = await handle.contentFrame();
|
||||||
|
|||||||
@@ -13,8 +13,10 @@
|
|||||||
"./src/Common/ArrayHashMap.ts",
|
"./src/Common/ArrayHashMap.ts",
|
||||||
"./src/Common/Constants.ts",
|
"./src/Common/Constants.ts",
|
||||||
"./src/Common/DeleteFeedback.ts",
|
"./src/Common/DeleteFeedback.ts",
|
||||||
|
"./src/Common/EnvironmentUtility.ts",
|
||||||
"./src/Common/HashMap.ts",
|
"./src/Common/HashMap.ts",
|
||||||
"./src/Common/HeadersUtility.ts",
|
"./src/Common/HeadersUtility.ts",
|
||||||
|
"./src/Common/Logger.ts",
|
||||||
"./src/Common/MessageHandler.ts",
|
"./src/Common/MessageHandler.ts",
|
||||||
"./src/Common/MongoUtility.ts",
|
"./src/Common/MongoUtility.ts",
|
||||||
"./src/Common/ObjectCache.ts",
|
"./src/Common/ObjectCache.ts",
|
||||||
@@ -25,9 +27,11 @@
|
|||||||
"./src/Contracts/DataModels.ts",
|
"./src/Contracts/DataModels.ts",
|
||||||
"./src/Contracts/Diagnostics.ts",
|
"./src/Contracts/Diagnostics.ts",
|
||||||
"./src/Contracts/ExplorerContracts.ts",
|
"./src/Contracts/ExplorerContracts.ts",
|
||||||
|
"./src/Contracts/SubscriptionType.ts",
|
||||||
"./src/Contracts/Versions.ts",
|
"./src/Contracts/Versions.ts",
|
||||||
"./src/Controls/Heatmap/Heatmap.ts",
|
"./src/Controls/Heatmap/Heatmap.ts",
|
||||||
"./src/Controls/Heatmap/HeatmapDatatypes.ts",
|
"./src/Controls/Heatmap/HeatmapDatatypes.ts",
|
||||||
|
"./src/DefaultAccountExperienceType.ts",
|
||||||
"./src/Definitions/globals.d.ts",
|
"./src/Definitions/globals.d.ts",
|
||||||
"./src/Definitions/html.d.ts",
|
"./src/Definitions/html.d.ts",
|
||||||
"./src/Definitions/jquery-ui.d.ts",
|
"./src/Definitions/jquery-ui.d.ts",
|
||||||
@@ -37,6 +41,7 @@
|
|||||||
"./src/Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.ts",
|
"./src/Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.ts",
|
||||||
"./src/Explorer/Controls/GitHub/GitHubStyleConstants.ts",
|
"./src/Explorer/Controls/GitHub/GitHubStyleConstants.ts",
|
||||||
"./src/Explorer/Controls/SmartUi/InputUtils.ts",
|
"./src/Explorer/Controls/SmartUi/InputUtils.ts",
|
||||||
|
"./src/Explorer/Graph/GraphExplorerComponent/__mocks__/GremlinClient.ts",
|
||||||
"./src/Explorer/Notebook/FileSystemUtil.ts",
|
"./src/Explorer/Notebook/FileSystemUtil.ts",
|
||||||
"./src/Explorer/Notebook/NTeractUtil.ts",
|
"./src/Explorer/Notebook/NTeractUtil.ts",
|
||||||
"./src/Explorer/Notebook/NotebookComponent/actions.ts",
|
"./src/Explorer/Notebook/NotebookComponent/actions.ts",
|
||||||
@@ -49,6 +54,7 @@
|
|||||||
"./src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts",
|
"./src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts",
|
||||||
"./src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts",
|
"./src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts",
|
||||||
"./src/Explorer/Tables/Constants.ts",
|
"./src/Explorer/Tables/Constants.ts",
|
||||||
|
"./src/Explorer/Tables/CqlUtilities.ts",
|
||||||
"./src/Explorer/Tables/QueryBuilder/DateTimeUtilities.ts",
|
"./src/Explorer/Tables/QueryBuilder/DateTimeUtilities.ts",
|
||||||
"./src/Explorer/Tabs/TabComponents.ts",
|
"./src/Explorer/Tabs/TabComponents.ts",
|
||||||
"./src/GitHub/GitHubConnector.ts",
|
"./src/GitHub/GitHubConnector.ts",
|
||||||
@@ -56,15 +62,23 @@
|
|||||||
"./src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts",
|
"./src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts",
|
||||||
"./src/ReactDevTools.ts",
|
"./src/ReactDevTools.ts",
|
||||||
"./src/ResourceProvider/IResourceProviderClient.ts",
|
"./src/ResourceProvider/IResourceProviderClient.ts",
|
||||||
|
"./src/Shared/Constants.ts",
|
||||||
"./src/Shared/ExplorerSettings.ts",
|
"./src/Shared/ExplorerSettings.ts",
|
||||||
|
"./src/Shared/PriceEstimateCalculator.ts",
|
||||||
"./src/Shared/StorageUtility.ts",
|
"./src/Shared/StorageUtility.ts",
|
||||||
"./src/Shared/StringUtility.ts",
|
"./src/Shared/StringUtility.ts",
|
||||||
|
"./src/Shared/Telemetry/TelemetryConstants.ts",
|
||||||
|
"./src/Shared/Telemetry/TelemetryProcessor.ts",
|
||||||
"./src/Shared/appInsights.ts",
|
"./src/Shared/appInsights.ts",
|
||||||
|
"./src/Terminal/JupyterLabAppFactory.ts",
|
||||||
"./src/UserContext.ts",
|
"./src/UserContext.ts",
|
||||||
|
"./src/Utils/Base64Utils.ts",
|
||||||
|
"./src/Utils/BlobUtils.ts",
|
||||||
"./src/Utils/GitHubUtils.ts",
|
"./src/Utils/GitHubUtils.ts",
|
||||||
"./src/Utils/MessageValidation.ts",
|
"./src/Utils/MessageValidation.ts",
|
||||||
"./src/Utils/OfferUtils.ts",
|
"./src/Utils/OfferUtils.ts",
|
||||||
"./src/Utils/StringUtils.ts",
|
"./src/Utils/StringUtils.ts",
|
||||||
|
"./src/Utils/WindowUtils.ts",
|
||||||
"./src/Utils/arm/generatedClients/2020-04-01/types.ts",
|
"./src/Utils/arm/generatedClients/2020-04-01/types.ts",
|
||||||
"./src/quickstart.ts",
|
"./src/quickstart.ts",
|
||||||
"./src/setupTests.ts",
|
"./src/setupTests.ts",
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ module.exports = function(env = {}, argv = {}) {
|
|||||||
return {
|
return {
|
||||||
mode: mode,
|
mode: mode,
|
||||||
entry: {
|
entry: {
|
||||||
main: "./src/Main.ts",
|
main: "./src/Main.tsx",
|
||||||
index: "./src/Index.ts",
|
index: "./src/Index.ts",
|
||||||
quickstart: "./src/quickstart.ts",
|
quickstart: "./src/quickstart.ts",
|
||||||
hostedExplorer: "./src/HostedExplorer.ts",
|
hostedExplorer: "./src/HostedExplorer.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user