mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-05 18:47:41 +00:00
Compare commits
24 Commits
remove-ru-
...
explorer-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f8c36bbf0 | ||
|
|
661fb66f7b | ||
|
|
2eabc377b0 | ||
|
|
1aa3fb8e7b | ||
|
|
57aef782d8 | ||
|
|
ea39c1d092 | ||
|
|
c21f42159f | ||
|
|
31e4b49f11 | ||
|
|
40491ec9c5 | ||
|
|
e133df18dd | ||
|
|
0532ed26a2 | ||
|
|
fd60c9c15e | ||
|
|
04ab1f3918 | ||
|
|
b784ac0f86 | ||
|
|
28899f63d7 | ||
|
|
9cbf632577 | ||
|
|
17fd2185dc | ||
|
|
a93c8509cd | ||
|
|
5c93c11bd9 | ||
|
|
85d2378d3a | ||
|
|
84b6075ee8 | ||
|
|
d880723be9 | ||
|
|
4ce9dcc024 | ||
|
|
addcfedd5e |
@@ -3,7 +3,11 @@ PORTAL_RUNNER_PASSWORD=
|
||||
PORTAL_RUNNER_SUBSCRIPTION=
|
||||
PORTAL_RUNNER_RESOURCE_GROUP=
|
||||
PORTAL_RUNNER_DATABASE_ACCOUNT=
|
||||
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY=
|
||||
PORTAL_RUNNER_CONNECTION_STRING=
|
||||
NOTEBOOKS_TEST_RUNNER_TENANT_ID=
|
||||
NOTEBOOKS_TEST_RUNNER_CLIENT_ID=
|
||||
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET=
|
||||
CASSANDRA_CONNECTION_STRING=
|
||||
MONGO_CONNECTION_STRING=
|
||||
TABLES_CONNECTION_STRING=
|
||||
|
||||
@@ -14,7 +14,6 @@ src/Common/DataAccessUtilityBase.ts
|
||||
src/Common/DeleteFeedback.ts
|
||||
src/Common/DocumentClientUtilityBase.ts
|
||||
src/Common/EditableUtility.ts
|
||||
src/Common/EnvironmentUtility.ts
|
||||
src/Common/HashMap.test.ts
|
||||
src/Common/HashMap.ts
|
||||
src/Common/HeadersUtility.test.ts
|
||||
@@ -202,8 +201,6 @@ src/Explorer/Tabs/QueryTab.test.ts
|
||||
src/Explorer/Tabs/QueryTab.ts
|
||||
src/Explorer/Tabs/QueryTablesTab.ts
|
||||
src/Explorer/Tabs/ScriptTabBase.ts
|
||||
src/Explorer/Tabs/SettingsTab.test.ts
|
||||
src/Explorer/Tabs/SettingsTab.ts
|
||||
src/Explorer/Tabs/SparkMasterTab.ts
|
||||
src/Explorer/Tabs/StoredProcedureTab.ts
|
||||
src/Explorer/Tabs/TabComponents.ts
|
||||
@@ -290,8 +287,6 @@ src/Utils/DatabaseAccountUtils.ts
|
||||
src/Utils/JunoUtils.ts
|
||||
src/Utils/MessageValidation.ts
|
||||
src/Utils/NotebookConfigurationUtils.ts
|
||||
src/Utils/OfferUtils.test.ts
|
||||
src/Utils/OfferUtils.ts
|
||||
src/Utils/PricingUtils.test.ts
|
||||
src/Utils/QueryUtils.test.ts
|
||||
src/Utils/QueryUtils.ts
|
||||
@@ -396,19 +391,5 @@ src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx
|
||||
src/GalleryViewer/Cards/GalleryCardComponent.tsx
|
||||
src/GalleryViewer/GalleryViewer.tsx
|
||||
src/GalleryViewer/GalleryViewerComponent.tsx
|
||||
cypress/integration/dataexplorer/CASSANDRA/addCollection.spec.ts
|
||||
cypress/integration/dataexplorer/GRAPH/addCollection.spec.ts
|
||||
cypress/integration/dataexplorer/ci-tests/addCollectionPane.spec.ts
|
||||
cypress/integration/dataexplorer/ci-tests/createDatabase.spec.ts
|
||||
cypress/integration/dataexplorer/ci-tests/deleteCollection.spec.ts
|
||||
cypress/integration/dataexplorer/ci-tests/deleteDatabase.spec.ts
|
||||
cypress/integration/dataexplorer/MONGO/addCollection.spec.ts
|
||||
cypress/integration/dataexplorer/MONGO/addCollectionAutopilot.spec.ts
|
||||
cypress/integration/dataexplorer/MONGO/addCollectionExistingDatabase.spec.ts
|
||||
cypress/integration/dataexplorer/MONGO/provisionDatabaseThroughput.spec.ts
|
||||
cypress/integration/dataexplorer/SQL/addCollection.spec.ts
|
||||
cypress/integration/dataexplorer/TABLE/addCollection.spec.ts
|
||||
cypress/integration/notebook/newNotebook.spec.ts
|
||||
cypress/integration/notebook/resourceTree.spec.ts
|
||||
__mocks__/monaco-editor.ts
|
||||
src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx
|
||||
40
.github/workflows/ci.yml
vendored
40
.github/workflows/ci.yml
vendored
@@ -79,32 +79,31 @@ jobs:
|
||||
name: dist
|
||||
path: dist/
|
||||
endtoendemulator:
|
||||
name: "End To End Tests | Emulator | SQL"
|
||||
name: "End To End Emulator Tests"
|
||||
needs: [lint, format, compile, unittest]
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: southpolesteve/cosmos-emulator-github-action@v1
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: Restore Cypress Binary Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/Cypress
|
||||
key: ${{ runner.os }}-cypress-binary-cache
|
||||
- uses: southpolesteve/cosmos-emulator-github-action@v1
|
||||
- name: End to End Tests
|
||||
run: |
|
||||
npm ci
|
||||
npm start &
|
||||
npm ci --prefix ./cypress
|
||||
npm run test:ci --prefix ./cypress -- --spec ./integration/dataexplorer/ci-tests/createDatabase.spec.ts
|
||||
npm run wait-for-server
|
||||
npx jest -c ./jest.config.e2e.js --detectOpenHandles sql
|
||||
shell: bash
|
||||
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
|
||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: screenshots
|
||||
path: failed-*
|
||||
accessibility:
|
||||
name: "Accessibility | Hosted"
|
||||
needs: [lint, format, compile, unittest]
|
||||
@@ -123,13 +122,13 @@ jobs:
|
||||
sudo sysctl -p
|
||||
npm ci
|
||||
npm start &
|
||||
npx wait-on -i 5000 https-get://0.0.0.0:1234/
|
||||
npx wait-on -i 5000 https-get://0.0.0.0:1234/
|
||||
node utils/accesibilityCheck.js
|
||||
shell: bash
|
||||
env:
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
endtoendpuppeteer:
|
||||
name: "End to end puppeteer tests"
|
||||
endtoendhosted:
|
||||
name: "End to End Hosted Tests"
|
||||
needs: [lint, format, compile, unittest]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -138,7 +137,7 @@ jobs:
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: End to End Puppeteer Tests
|
||||
- name: End to End Hosted Tests
|
||||
run: |
|
||||
npm ci
|
||||
npm start &
|
||||
@@ -147,6 +146,13 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
PORTAL_RUNNER_SUBSCRIPTION: ${{ secrets.PORTAL_RUNNER_SUBSCRIPTION }}
|
||||
PORTAL_RUNNER_RESOURCE_GROUP: ${{ secrets.PORTAL_RUNNER_RESOURCE_GROUP }}
|
||||
PORTAL_RUNNER_DATABASE_ACCOUNT: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT }}
|
||||
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY }}
|
||||
NOTEBOOKS_TEST_RUNNER_TENANT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_TENANT_ID }}
|
||||
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
|
||||
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
|
||||
PORTAL_RUNNER_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
|
||||
MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
|
||||
CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }}
|
||||
@@ -159,7 +165,7 @@ jobs:
|
||||
nuget:
|
||||
name: Publish Nuget
|
||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendpuppeteer]
|
||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||
@@ -183,7 +189,7 @@ jobs:
|
||||
nugetmpac:
|
||||
name: Publish Nuget MPAC
|
||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendpuppeteer]
|
||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||
|
||||
25
.github/workflows/runners.yml
vendored
25
.github/workflows/runners.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: Runners
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 * 1 * *"
|
||||
jobs:
|
||||
sqlcreatecollection:
|
||||
runs-on: ubuntu-latest
|
||||
name: "SQL | Create Collection"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- run: npm ci
|
||||
- run: npm run test:e2e
|
||||
env:
|
||||
PORTAL_RUNNER_APP_INSIGHTS_KEY: ${{ secrets.PORTAL_RUNNER_APP_INSIGHTS_KEY }}
|
||||
PORTAL_RUNNER_USERNAME: ${{ secrets.PORTAL_RUNNER_USERNAME }}
|
||||
PORTAL_RUNNER_PASSWORD: ${{ secrets.PORTAL_RUNNER_PASSWORD }}
|
||||
PORTAL_RUNNER_SUBSCRIPTION: 69e02f2d-f059-4409-9eac-97e8a276ae2c
|
||||
PORTAL_RUNNER_RESOURCE_GROUP: runners
|
||||
PORTAL_RUNNER_DATABASE_ACCOUNT: portal-sql-runner
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: screenshots
|
||||
path: failure.png
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,9 +9,6 @@ pkg/DataExplorer/*
|
||||
test/out/*
|
||||
workers/**/*.js
|
||||
*.trx
|
||||
cypress/videos
|
||||
cypress/screenshots
|
||||
cypress/fixtures
|
||||
notebookapp/*
|
||||
Contracts/*
|
||||
.DS_Store
|
||||
|
||||
49
README.md
49
README.md
@@ -13,29 +13,18 @@ UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), ht
|
||||
|
||||
### Watch mode
|
||||
|
||||
Run `npm run watch` to start the development server and automatically rebuild on changes
|
||||
Run `npm start` to start the development server and automatically rebuild on changes
|
||||
|
||||
### Specifying Development Platform
|
||||
### Hosted Development (https://cosmos.azure.com)
|
||||
|
||||
Setting the environment variable `PLATFORM` during the build process will force the explorer to load the specified platform. By default in development it will run in `Hosted` mode. Valid options:
|
||||
|
||||
- Hosted
|
||||
- Emulator
|
||||
- Portal
|
||||
|
||||
`PLATFORM=Emulator npm run watch`
|
||||
|
||||
### Hosted Development
|
||||
|
||||
The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine.
|
||||
|
||||
To run pure hosted mode, in `webpack.config.js` change index HtmlWebpackPlugin to use hostedExplorer.html and change entry for index to use HostedExplorer.ts.
|
||||
- Visit: `https://localhost:1234/hostedExplorer.html`
|
||||
- Local sign in via AAD will NOT work. Connection string only in dev mode. Use the Portal if you need AAD auth.
|
||||
- The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine.
|
||||
|
||||
### Emulator Development
|
||||
|
||||
In a window environment, running `npm run build` will automatically copy the built files from `/dist` over to the default emulator install paths. In a non-windows environment you can specify an alternate endpoint using `EMULATOR_ENDPOINT` and webpack dev server will proxy requests for you.
|
||||
|
||||
`PLATFORM=Emulator EMULATOR_ENDPOINT=https://my-vm.azure.com:8081 npm run watch`
|
||||
- Start the Cosmos Emulator
|
||||
- Visit: https://localhost:1234/index.html
|
||||
|
||||
#### Setting up a Remote Emulator
|
||||
|
||||
@@ -55,16 +44,8 @@ The Cosmos emulator currently only runs in Windows environments. You can still d
|
||||
|
||||
### Portal Development
|
||||
|
||||
The Cosmos Portal that consumes this repo is not currently open source. If you have access to this project, `npm run build` will copy the built files over to the portal where they will be loaded by the portal development environment
|
||||
|
||||
You can however load a local running instance of data explorer in the production portal.
|
||||
|
||||
1. Turn off browser SSL validation for localhost: chrome://flags/#allow-insecure-localhost OR Install valid SSL certs for localhost (on IE, follow these [instructions](https://www.technipages.com/ie-bypass-problem-with-this-websites-security-certificate) to install the localhost certificate in the right place)
|
||||
2. Allowlist `https://localhost:1234` domain for CORS in the Azure Cosmos DB portal
|
||||
3. Start the project in portal mode: `PLATFORM=Portal npm run watch`
|
||||
4. Load the portal using the following link: https://ms.portal.azure.com/?dataExplorerSource=https%3A%2F%2Flocalhost%3A1234%2Fexplorer.html
|
||||
|
||||
Live reload will occur, but data explorer will not properly integrate again with the parent iframe. You will have to manually reload the page.
|
||||
- Visit: https://ms.portal.azure.com/?dataExplorerSource=https%3A%2F%2Flocalhost%3A1234%2Fexplorer.html
|
||||
- You may have to manually visit https://localhost:1234/explorer.html first and click through any SSL certificate warnings
|
||||
|
||||
### Testing
|
||||
|
||||
@@ -76,17 +57,7 @@ Unit tests are located adjacent to the code under test and run with [Jest](https
|
||||
|
||||
#### 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:
|
||||
|
||||
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:
|
||||
Jest and Puppeteer are used for end to end browser based tests and are contained in `test/`. To run these tests locally:
|
||||
|
||||
1. Copy .env.example to .env
|
||||
2. Update the values in .env including your local data explorer endpoint (ask a teammate/codeowner for help with .env values)
|
||||
|
||||
4
cypress/.gitignore
vendored
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=="
|
||||
});
|
||||
3660
package-lock.json
generated
3660
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -4,8 +4,10 @@
|
||||
"description": "Cosmos Explorer",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@azure/arm-cosmosdb": "9.1.0",
|
||||
"@azure/cosmos": "3.9.0",
|
||||
"@azure/cosmos-language-service": "0.0.4",
|
||||
"@azure/identity": "1.1.0",
|
||||
"@azure/cosmos-language-service": "0.0.5",
|
||||
"@jupyterlab/services": "6.0.0-rc.2",
|
||||
"@jupyterlab/terminal": "3.0.0-rc.2",
|
||||
"@microsoft/applicationinsights-web": "2.5.9",
|
||||
@@ -66,7 +68,7 @@
|
||||
"jquery-ui-dist": "1.12.1",
|
||||
"knockout": "3.5.1",
|
||||
"mkdirp": "1.0.4",
|
||||
"monaco-editor": "0.15.6",
|
||||
"monaco-editor": "0.18.1",
|
||||
"object.entries": "1.1.0",
|
||||
"office-ui-fabric-react": "7.134.1",
|
||||
"p-retry": "4.2.0",
|
||||
@@ -115,7 +117,7 @@
|
||||
"@types/prop-types": "15.5.8",
|
||||
"@types/puppeteer": "3.0.1",
|
||||
"@types/q": "1.5.1",
|
||||
"@types/react": "16.9.49",
|
||||
"@types/react": "16.9.56",
|
||||
"@types/react-dom": "16.0.7",
|
||||
"@types/react-notification-system": "0.2.39",
|
||||
"@types/react-redux": "7.1.7",
|
||||
@@ -194,8 +196,8 @@
|
||||
"compile": "tsc",
|
||||
"compile:contracts": "tsc -p ./tsconfig.contracts.json",
|
||||
"compile:strict": "tsc -p ./tsconfig.strict.json",
|
||||
"format": "prettier --write \"{src,cypress,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
||||
"format:check": "prettier --check \"{src,cypress,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
||||
"format": "prettier --write \"{src,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}\"",
|
||||
"build:contracts": "npm run compile:contracts",
|
||||
"strictEligibleFiles": "node ./strict-migration-tools/index.js",
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"offerThroughput": 400,
|
||||
"databaseLevelThroughput": false,
|
||||
"collectionId": "Persons",
|
||||
"rupmEnabled": false,
|
||||
"partitionKey": { "kind": "Hash", "paths": ["/name"] },
|
||||
"data": [
|
||||
"g.addV('person').property(id, '1').property('name', 'Eva').property('age', 44)",
|
||||
|
||||
@@ -108,13 +108,11 @@ export class CapabilityNames {
|
||||
export class Features {
|
||||
public static readonly cosmosdb = "cosmosdb";
|
||||
public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy";
|
||||
public static readonly enableRupm = "enablerupm";
|
||||
public static readonly executeSproc = "dataexplorerexecutesproc";
|
||||
public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
|
||||
public static readonly enableTtl = "enablettl";
|
||||
public static readonly enableNotebooks = "enablenotebooks";
|
||||
public static readonly enableGalleryPublish = "enablegallerypublish";
|
||||
public static readonly enableCodeOfConduct = "enablecodeofconduct";
|
||||
public static readonly enableLinkInjection = "enablelinkinjection";
|
||||
public static readonly enableSpark = "enablespark";
|
||||
public static readonly livyEndpoint = "livyendpoint";
|
||||
@@ -125,7 +123,9 @@ export class Features {
|
||||
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
|
||||
public static readonly ttl90Days = "ttl90days";
|
||||
public static readonly enableRightPanelV2 = "enablerightpanelv2";
|
||||
public static readonly enableSchema = "enableschema";
|
||||
public static readonly enableSDKoperations = "enablesdkoperations";
|
||||
public static readonly showMinRUSurvey = "showminrusurvey";
|
||||
}
|
||||
|
||||
// flight names returned from the portal are always lowercase
|
||||
@@ -179,11 +179,6 @@ export class CassandraBackend {
|
||||
public static readonly guestSchemaApi: string = "api/guest/cassandra/schema";
|
||||
}
|
||||
|
||||
export class RUPMStates {
|
||||
public static on: string = "on";
|
||||
public static off: string = "off";
|
||||
}
|
||||
|
||||
export class Queries {
|
||||
public static CustomPageOption: string = "custom";
|
||||
public static UnlimitedPageOption: string = "unlimited";
|
||||
|
||||
@@ -1,26 +1,13 @@
|
||||
import {
|
||||
ConflictDefinition,
|
||||
FeedOptions,
|
||||
ItemDefinition,
|
||||
OfferDefinition,
|
||||
QueryIterator,
|
||||
Resource
|
||||
} from "@azure/cosmos";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { ConflictDefinition, FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import Q from "q";
|
||||
import { configContext, Platform } from "../ConfigContext";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import ConflictId from "../Explorer/Tree/ConflictId";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
||||
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
|
||||
import { OfferUtils } from "../Utils/OfferUtils";
|
||||
import * as Constants from "./Constants";
|
||||
import { client } from "./CosmosClient";
|
||||
import * as HeadersUtility from "./HeadersUtility";
|
||||
import { sendCachedDataMessage } from "./MessageHandler";
|
||||
|
||||
export function getCommonQueryOptions(options: FeedOptions): any {
|
||||
const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
export default class EnvironmentUtility {
|
||||
public static normalizeArmEndpointUri(uri: string): string {
|
||||
if (uri && uri.slice(-1) !== "/") {
|
||||
return `${uri}/`;
|
||||
}
|
||||
return uri;
|
||||
export function normalizeArmEndpoint(uri: string): string {
|
||||
if (uri && uri.slice(-1) !== "/") {
|
||||
return `${uri}/`;
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ARMError } from "../Utils/arm/request";
|
||||
import { HttpStatusCodes } from "./Constants";
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import { SubscriptionType } from "../Contracts/ViewModels";
|
||||
import { SubscriptionType } from "../Contracts/SubscriptionType";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
import { logError } from "./Logger";
|
||||
import { sendMessage } from "./MessageHandler";
|
||||
|
||||
64
src/Common/OfferUtility.test.ts
Normal file
64
src/Common/OfferUtility.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import * as OfferUtility from "./OfferUtility";
|
||||
import { SDKOfferDefinition, Offer } from "../Contracts/DataModels";
|
||||
import { OfferResponse } from "@azure/cosmos";
|
||||
|
||||
describe("parseSDKOfferResponse", () => {
|
||||
it("manual throughput", () => {
|
||||
const mockOfferDefinition = {
|
||||
content: {
|
||||
offerThroughput: 500,
|
||||
collectionThroughputInfo: {
|
||||
minimumRUForCollection: 400,
|
||||
numPhysicalPartitions: 1
|
||||
}
|
||||
},
|
||||
id: "test"
|
||||
} as SDKOfferDefinition;
|
||||
|
||||
const mockResponse = {
|
||||
resource: mockOfferDefinition
|
||||
} as OfferResponse;
|
||||
|
||||
const expectedResult: Offer = {
|
||||
manualThroughput: 500,
|
||||
autoscaleMaxThroughput: undefined,
|
||||
minimumThroughput: 400,
|
||||
id: "test",
|
||||
offerDefinition: mockOfferDefinition,
|
||||
offerReplacePending: false
|
||||
};
|
||||
|
||||
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it("autoscale throughput", () => {
|
||||
const mockOfferDefinition = {
|
||||
content: {
|
||||
offerThroughput: 400,
|
||||
collectionThroughputInfo: {
|
||||
minimumRUForCollection: 400,
|
||||
numPhysicalPartitions: 1
|
||||
},
|
||||
offerAutopilotSettings: {
|
||||
maxThroughput: 5000
|
||||
}
|
||||
},
|
||||
id: "test"
|
||||
} as SDKOfferDefinition;
|
||||
|
||||
const mockResponse = {
|
||||
resource: mockOfferDefinition
|
||||
} as OfferResponse;
|
||||
|
||||
const expectedResult: Offer = {
|
||||
manualThroughput: undefined,
|
||||
autoscaleMaxThroughput: 5000,
|
||||
minimumThroughput: 400,
|
||||
id: "test",
|
||||
offerDefinition: mockOfferDefinition,
|
||||
offerReplacePending: false
|
||||
};
|
||||
|
||||
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
34
src/Common/OfferUtility.ts
Normal file
34
src/Common/OfferUtility.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Offer, SDKOfferDefinition } from "../Contracts/DataModels";
|
||||
import { OfferResponse } from "@azure/cosmos";
|
||||
import { HttpHeaders } from "./Constants";
|
||||
|
||||
export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => {
|
||||
const offerDefinition: SDKOfferDefinition = offerResponse?.resource;
|
||||
const offerContent = offerDefinition.content;
|
||||
if (!offerContent) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const minimumThroughput = offerContent.collectionThroughputInfo?.minimumRUForCollection;
|
||||
const autopilotSettings = offerContent.offerAutopilotSettings;
|
||||
|
||||
if (autopilotSettings) {
|
||||
return {
|
||||
id: offerDefinition.id,
|
||||
autoscaleMaxThroughput: autopilotSettings.maxThroughput,
|
||||
manualThroughput: undefined,
|
||||
minimumThroughput,
|
||||
offerDefinition,
|
||||
offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: offerDefinition.id,
|
||||
autoscaleMaxThroughput: undefined,
|
||||
manualThroughput: offerContent.offerThroughput,
|
||||
minimumThroughput,
|
||||
offerDefinition,
|
||||
offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true"
|
||||
};
|
||||
};
|
||||
@@ -16,7 +16,7 @@ const notificationsPath = () => {
|
||||
};
|
||||
|
||||
export const fetchPortalNotifications = async (): Promise<DataModels.Notification[]> => {
|
||||
if (configContext.platform === Platform.Emulator) {
|
||||
if (configContext.platform === Platform.Emulator || configContext.platform === Platform.Hosted) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
92
src/Common/dataAccess/getCollectionDataUsageSize.ts
Normal file
92
src/Common/dataAccess/getCollectionDataUsageSize.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { armRequest } from "../../Utils/arm/request";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
interface TimeSeriesData {
|
||||
data: {
|
||||
timeStamp: string;
|
||||
total: number;
|
||||
}[];
|
||||
metadatavalues: {
|
||||
name: {
|
||||
localizedValue: string;
|
||||
value: string;
|
||||
};
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface MetricsData {
|
||||
displayDescription: string;
|
||||
errorCode: string;
|
||||
id: string;
|
||||
name: {
|
||||
value: string;
|
||||
localizedValue: string;
|
||||
};
|
||||
timeseries: TimeSeriesData[];
|
||||
type: string;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
interface MetricsResponse {
|
||||
cost: number;
|
||||
interval: string;
|
||||
namespace: string;
|
||||
resourceregion: string;
|
||||
timespan: string;
|
||||
value: MetricsData[];
|
||||
}
|
||||
|
||||
export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => {
|
||||
if (window.authType !== AuthType.AAD) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const accountName = userContext.databaseAccount.name;
|
||||
const filter = `DatabaseName eq '${databaseName}' and CollectionName eq '${containerName}'`;
|
||||
const metricNames = "DataUsage,IndexUsage";
|
||||
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/providers/microsoft.insights/metrics`;
|
||||
|
||||
try {
|
||||
const metricsResponse: MetricsResponse = await armRequest({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path,
|
||||
method: "GET",
|
||||
apiVersion: "2018-01-01",
|
||||
queryParams: {
|
||||
filter,
|
||||
metricNames
|
||||
}
|
||||
});
|
||||
|
||||
if (metricsResponse?.value?.length !== 2) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const dataUsageData: MetricsData = metricsResponse.value[0];
|
||||
const indexUsagedata: MetricsData = metricsResponse.value[1];
|
||||
const dataUsageSizeInKb: number = getUsageSizeInKb(dataUsageData);
|
||||
const indexUsageSizeInKb: number = getUsageSizeInKb(indexUsagedata);
|
||||
|
||||
return dataUsageSizeInKb + indexUsageSizeInKb;
|
||||
} catch (error) {
|
||||
handleError(error, "getCollectionUsageSize");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const getUsageSizeInKb = (metricsData: MetricsData): number => {
|
||||
if (metricsData?.errorCode !== "Success") {
|
||||
throw Error(`Get collection usage size failed: ${metricsData.errorCode}`);
|
||||
}
|
||||
|
||||
const timeSeriesData: TimeSeriesData = metricsData?.timeseries?.[0];
|
||||
const usageSizeInBytes: number = timeSeriesData?.data?.[0]?.total;
|
||||
|
||||
return usageSizeInBytes ? usageSizeInBytes / 1024 : 0;
|
||||
};
|
||||
@@ -1,9 +1,6 @@
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { client } from "../CosmosClient";
|
||||
import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { getSqlContainerThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import { getMongoDBCollectionThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||
@@ -11,50 +8,22 @@ import { getCassandraTableThroughput } from "../../Utils/arm/generatedClients/20
|
||||
import { getGremlinGraphThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||
import { getTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { readOffers } from "./readOffers";
|
||||
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export const readCollectionOffer = async (
|
||||
params: DataModels.ReadCollectionOfferParams
|
||||
): Promise<DataModels.OfferWithHeaders> => {
|
||||
export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise<Offer> => {
|
||||
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
|
||||
let offerId = params.offerId;
|
||||
if (!offerId) {
|
||||
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
||||
try {
|
||||
offerId = await getCollectionOfferIdWithARM(params.databaseId, params.collectionId);
|
||||
} catch (error) {
|
||||
clearMessage();
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
offerId = await getCollectionOfferIdWithSDK(params.collectionResourceId);
|
||||
if (!offerId) {
|
||||
clearMessage();
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const options: RequestOptions = {
|
||||
initialHeaders: {
|
||||
[HttpHeaders.populateCollectionThroughputInfo]: true
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await client()
|
||||
.offer(offerId)
|
||||
.read(options);
|
||||
return (
|
||||
response && {
|
||||
...response.resource,
|
||||
headers: response.headers
|
||||
}
|
||||
);
|
||||
if (
|
||||
window.authType === AuthType.AAD &&
|
||||
!userContext.useSDKOperations &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||
) {
|
||||
return await readCollectionOfferWithARM(params.databaseId, params.collectionId);
|
||||
}
|
||||
|
||||
return await readOfferWithSDK(params.offerId, params.collectionResourceId);
|
||||
} catch (error) {
|
||||
handleError(error, "ReadCollectionOffer", `Error while querying offer for collection ${params.collectionId}`);
|
||||
throw error;
|
||||
@@ -63,61 +32,92 @@ export const readCollectionOffer = async (
|
||||
}
|
||||
};
|
||||
|
||||
const getCollectionOfferIdWithARM = async (databaseId: string, collectionId: string): Promise<string> => {
|
||||
let rpResponse;
|
||||
const readCollectionOfferWithARM = async (databaseId: string, collectionId: string): Promise<Offer> => {
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const accountName = userContext.databaseAccount.name;
|
||||
const defaultExperience = userContext.defaultExperience;
|
||||
switch (defaultExperience) {
|
||||
case DefaultAccountExperienceType.DocumentDB:
|
||||
rpResponse = await getSqlContainerThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.MongoDB:
|
||||
rpResponse = await getMongoDBCollectionThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Cassandra:
|
||||
rpResponse = await getCassandraTableThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Graph:
|
||||
rpResponse = await getGremlinGraphThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Table:
|
||||
rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||
|
||||
let rpResponse;
|
||||
try {
|
||||
switch (defaultExperience) {
|
||||
case DefaultAccountExperienceType.DocumentDB:
|
||||
rpResponse = await getSqlContainerThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.MongoDB:
|
||||
rpResponse = await getMongoDBCollectionThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Cassandra:
|
||||
rpResponse = await getCassandraTableThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Graph:
|
||||
rpResponse = await getGremlinGraphThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Table:
|
||||
rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return rpResponse?.name;
|
||||
};
|
||||
const resource = rpResponse?.properties?.resource;
|
||||
if (resource) {
|
||||
const offerId: string = rpResponse.name;
|
||||
const minimumThroughput: number =
|
||||
typeof resource.minimumThroughput === "string"
|
||||
? parseInt(resource.minimumThroughput)
|
||||
: resource.minimumThroughput;
|
||||
const autoscaleSettings = resource.autoscaleSettings;
|
||||
|
||||
const getCollectionOfferIdWithSDK = async (collectionResourceId: string): Promise<string> => {
|
||||
const offers = await readOffers();
|
||||
const offer = offers.find(offer => offer.resource === collectionResourceId);
|
||||
return offer?.id;
|
||||
if (autoscaleSettings) {
|
||||
return {
|
||||
id: offerId,
|
||||
autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
|
||||
manualThroughput: undefined,
|
||||
minimumThroughput,
|
||||
offerReplacePending: resource.offerReplacePending === "true"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: offerId,
|
||||
autoscaleMaxThroughput: undefined,
|
||||
manualThroughput: resource.throughput,
|
||||
minimumThroughput,
|
||||
offerReplacePending: resource.offerReplacePending === "true"
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as HeadersUtility from "../HeadersUtility";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { ContainerDefinition, Resource } from "@azure/cosmos";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
|
||||
interface ResourceWithStatistics {
|
||||
statistics: DataModels.Statistic[];
|
||||
}
|
||||
|
||||
export const readCollectionQuotaInfo = async (
|
||||
collection: ViewModels.Collection
|
||||
): Promise<DataModels.CollectionQuotaInfo> => {
|
||||
const clearMessage = logConsoleProgress(`Querying containers for database ${collection.id}`);
|
||||
const options: RequestOptions = {};
|
||||
options.populateQuotaInfo = true;
|
||||
options.initialHeaders = options.initialHeaders || {};
|
||||
options.initialHeaders[HttpHeaders.populatePartitionStatistics] = true;
|
||||
|
||||
try {
|
||||
const response = await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.read(options);
|
||||
const quota: DataModels.CollectionQuotaInfo = HeadersUtility.getQuota(response.headers);
|
||||
const resource = response.resource as ContainerDefinition & Resource & ResourceWithStatistics;
|
||||
quota["usageSizeInKB"] = resource.statistics.reduce(
|
||||
(previousValue: number, currentValue: DataModels.Statistic) => previousValue + currentValue.sizeInKB,
|
||||
0
|
||||
);
|
||||
quota["numPartitions"] = resource.statistics.length;
|
||||
quota["uniqueKeyPolicy"] = collection.uniqueKeyPolicy; // TODO: Remove after refactoring (#119617)
|
||||
|
||||
return quota;
|
||||
} catch (error) {
|
||||
handleError(error, "ReadCollectionQuotaInfo", `Error while querying quota info for container ${collection.id}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
};
|
||||
@@ -1,51 +1,28 @@
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { client } from "../CosmosClient";
|
||||
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
|
||||
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
||||
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { readOffers } from "./readOffers";
|
||||
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export const readDatabaseOffer = async (
|
||||
params: DataModels.ReadDatabaseOfferParams
|
||||
): Promise<DataModels.OfferWithHeaders> => {
|
||||
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
|
||||
const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`);
|
||||
let offerId = params.offerId;
|
||||
if (!offerId) {
|
||||
offerId = await (window.authType === AuthType.AAD &&
|
||||
!userContext.useSDKOperations &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||
? getDatabaseOfferIdWithARM(params.databaseId)
|
||||
: getDatabaseOfferIdWithSDK(params.databaseResourceId));
|
||||
if (!offerId) {
|
||||
clearMessage();
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const options: RequestOptions = {
|
||||
initialHeaders: {
|
||||
[HttpHeaders.populateCollectionThroughputInfo]: true
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await client()
|
||||
.offer(offerId)
|
||||
.read(options);
|
||||
return (
|
||||
response && {
|
||||
...response.resource,
|
||||
headers: response.headers
|
||||
}
|
||||
);
|
||||
if (
|
||||
window.authType === AuthType.AAD &&
|
||||
!userContext.useSDKOperations &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||
) {
|
||||
return await readDatabaseOfferWithARM(params.databaseId);
|
||||
}
|
||||
|
||||
return await readOfferWithSDK(params.offerId, params.databaseResourceId);
|
||||
} catch (error) {
|
||||
handleError(error, "ReadDatabaseOffer", `Error while querying offer for database ${params.databaseId}`);
|
||||
throw error;
|
||||
@@ -54,13 +31,13 @@ export const readDatabaseOffer = async (
|
||||
}
|
||||
};
|
||||
|
||||
const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> => {
|
||||
let rpResponse;
|
||||
const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const accountName = userContext.databaseAccount.name;
|
||||
const defaultExperience = userContext.defaultExperience;
|
||||
|
||||
let rpResponse;
|
||||
try {
|
||||
switch (defaultExperience) {
|
||||
case DefaultAccountExperienceType.DocumentDB:
|
||||
@@ -78,18 +55,41 @@ const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> =>
|
||||
default:
|
||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||
}
|
||||
|
||||
return rpResponse?.name;
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const getDatabaseOfferIdWithSDK = async (databaseResourceId: string): Promise<string> => {
|
||||
const offers = await readOffers();
|
||||
const offer = offers.find(offer => offer.resource === databaseResourceId);
|
||||
return offer?.id;
|
||||
const resource = rpResponse?.properties?.resource;
|
||||
if (resource) {
|
||||
const offerId: string = rpResponse.name;
|
||||
const minimumThroughput: number =
|
||||
typeof resource.minimumThroughput === "string"
|
||||
? parseInt(resource.minimumThroughput)
|
||||
: resource.minimumThroughput;
|
||||
const autoscaleSettings = resource.autoscaleSettings;
|
||||
|
||||
if (autoscaleSettings) {
|
||||
return {
|
||||
id: offerId,
|
||||
autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
|
||||
manualThroughput: undefined,
|
||||
minimumThroughput,
|
||||
offerReplacePending: resource.offerReplacePending === "true"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: offerId,
|
||||
autoscaleMaxThroughput: undefined,
|
||||
manualThroughput: resource.throughput,
|
||||
minimumThroughput,
|
||||
offerReplacePending: resource.offerReplacePending === "true"
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
29
src/Common/dataAccess/readOfferWithSDK.ts
Normal file
29
src/Common/dataAccess/readOfferWithSDK.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { Offer } from "../../Contracts/DataModels";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { client } from "../CosmosClient";
|
||||
import { parseSDKOfferResponse } from "../OfferUtility";
|
||||
import { readOffers } from "./readOffers";
|
||||
|
||||
export const readOfferWithSDK = async (offerId: string, resourceId: string): Promise<Offer> => {
|
||||
if (!offerId) {
|
||||
const offers = await readOffers();
|
||||
const offer = offers.find(offer => offer.resource === resourceId);
|
||||
|
||||
if (!offer) {
|
||||
return undefined;
|
||||
}
|
||||
offerId = offer.id;
|
||||
}
|
||||
|
||||
const options: RequestOptions = {
|
||||
initialHeaders: {
|
||||
[HttpHeaders.populateCollectionThroughputInfo]: true
|
||||
}
|
||||
};
|
||||
const response = await client()
|
||||
.offer(offerId)
|
||||
.read(options);
|
||||
|
||||
return parseSDKOfferResponse(response);
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Offer } from "../../Contracts/DataModels";
|
||||
import { SDKOfferDefinition } from "../../Contracts/DataModels";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError, getErrorMessage } from "../ErrorHandlingUtils";
|
||||
|
||||
export const readOffers = async (): Promise<Offer[]> => {
|
||||
export const readOffers = async (): Promise<SDKOfferDefinition[]> => {
|
||||
const clearMessage = logConsoleProgress(`Querying offers`);
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { Offer, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||
import { OfferDefinition } from "@azure/cosmos";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { ThroughputSettingsUpdateParameters } from "../../Utils/arm/generatedClients/2020-04-01/types";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { parseSDKOfferResponse } from "../OfferUtility";
|
||||
import { readCollectionOffer } from "./readCollectionOffer";
|
||||
import { readDatabaseOffer } from "./readDatabaseOffer";
|
||||
import {
|
||||
@@ -373,21 +374,21 @@ const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpd
|
||||
};
|
||||
|
||||
const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> => {
|
||||
const currentOffer = params.currentOffer;
|
||||
const newOffer: Offer = {
|
||||
const sdkOfferDefinition = params.currentOffer.offerDefinition;
|
||||
const newOffer: SDKOfferDefinition = {
|
||||
content: {
|
||||
offerThroughput: undefined,
|
||||
offerIsRUPerMinuteThroughputEnabled: false
|
||||
},
|
||||
_etag: undefined,
|
||||
_ts: undefined,
|
||||
_rid: currentOffer._rid,
|
||||
_self: currentOffer._self,
|
||||
id: currentOffer.id,
|
||||
offerResourceId: currentOffer.offerResourceId,
|
||||
offerVersion: currentOffer.offerVersion,
|
||||
offerType: currentOffer.offerType,
|
||||
resource: currentOffer.resource
|
||||
_rid: sdkOfferDefinition._rid,
|
||||
_self: sdkOfferDefinition._self,
|
||||
id: sdkOfferDefinition.id,
|
||||
offerResourceId: sdkOfferDefinition.offerResourceId,
|
||||
offerVersion: sdkOfferDefinition.offerVersion,
|
||||
offerType: sdkOfferDefinition.offerType,
|
||||
resource: sdkOfferDefinition.resource
|
||||
};
|
||||
|
||||
if (params.autopilotThroughput) {
|
||||
@@ -415,5 +416,6 @@ const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> =>
|
||||
.offer(params.currentOffer.id)
|
||||
// TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660)
|
||||
.replace((newOffer as unknown) as OfferDefinition, options);
|
||||
return sdkResponse?.resource;
|
||||
|
||||
return parseSDKOfferResponse(sdkResponse);
|
||||
};
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { updateOfferThroughputBeyondLimit } from "./updateOfferThroughputBeyondLimit";
|
||||
|
||||
describe("updateOfferThroughputBeyondLimit", () => {
|
||||
it("should call fetch", async () => {
|
||||
window.fetch = jest.fn(() => {
|
||||
return {
|
||||
ok: true
|
||||
};
|
||||
});
|
||||
window.dataExplorer = {
|
||||
logConsoleData: jest.fn(),
|
||||
deleteInProgressConsoleDataWithId: jest.fn()
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any;
|
||||
await updateOfferThroughputBeyondLimit({
|
||||
subscriptionId: "foo",
|
||||
resourceGroup: "foo",
|
||||
databaseAccountName: "foo",
|
||||
databaseName: "foo",
|
||||
throughput: 1000000000,
|
||||
offerIsRUPerMinuteThroughputEnabled: false
|
||||
});
|
||||
expect(window.fetch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Platform, configContext } from "../../ConfigContext";
|
||||
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||
import { AutoPilotOfferSettings } from "../../Contracts/DataModels";
|
||||
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
interface UpdateOfferThroughputRequest {
|
||||
subscriptionId: string;
|
||||
resourceGroup: string;
|
||||
databaseAccountName: string;
|
||||
databaseName: string;
|
||||
collectionName?: string;
|
||||
throughput: number;
|
||||
offerIsRUPerMinuteThroughputEnabled: boolean;
|
||||
offerAutopilotSettings?: AutoPilotOfferSettings;
|
||||
}
|
||||
|
||||
export async function updateOfferThroughputBeyondLimit(request: UpdateOfferThroughputRequest): Promise<void> {
|
||||
if (configContext.platform !== Platform.Portal) {
|
||||
throw new Error("Updating throughput beyond specified limit is not supported on this platform");
|
||||
}
|
||||
|
||||
const resourceDescriptionInfo = request.collectionName
|
||||
? `database ${request.databaseName} and container ${request.collectionName}`
|
||||
: `database ${request.databaseName}`;
|
||||
|
||||
const clearMessage = logConsoleProgress(
|
||||
`Requesting increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
|
||||
);
|
||||
|
||||
const url = `${configContext.BACKEND_ENDPOINT}/api/offerthroughputrequest/updatebeyondspecifiedlimit`;
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(request),
|
||||
headers: { [authorizationHeader.header]: authorizationHeader.token, [HttpHeaders.contentType]: "application/json" }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
logConsoleInfo(
|
||||
`Successfully requested an increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
|
||||
);
|
||||
clearMessage();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const error = await response.json();
|
||||
handleError(
|
||||
error,
|
||||
"updateOfferThroughputBeyondLimit",
|
||||
`Failed to request an increase in throughput for ${request.throughput}`
|
||||
);
|
||||
clearMessage();
|
||||
throw error;
|
||||
}
|
||||
@@ -26,6 +26,7 @@ interface ConfigContext {
|
||||
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
|
||||
hostedExplorerURL: string;
|
||||
armAPIVersion?: string;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
// Default configuration
|
||||
|
||||
@@ -88,6 +88,38 @@ export interface Resource {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface IType {
|
||||
name: string;
|
||||
code: number;
|
||||
}
|
||||
|
||||
export interface IDataField {
|
||||
dataType: IType;
|
||||
hasNulls: boolean;
|
||||
isArray: boolean;
|
||||
schemaType: IType;
|
||||
name: string;
|
||||
path: string;
|
||||
maxRepetitionLevel: number;
|
||||
maxDefinitionLevel: number;
|
||||
}
|
||||
|
||||
export interface ISchema {
|
||||
id: string;
|
||||
accountName: string;
|
||||
resource: string;
|
||||
fields: IDataField[];
|
||||
}
|
||||
|
||||
export interface ISchemaRequest {
|
||||
id: string;
|
||||
subscriptionId: string;
|
||||
resourceGroup: string;
|
||||
accountName: string;
|
||||
resource: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface Collection extends Resource {
|
||||
defaultTtl?: number;
|
||||
indexingPolicy?: IndexingPolicy;
|
||||
@@ -98,6 +130,8 @@ export interface Collection extends Resource {
|
||||
changeFeedPolicy?: ChangeFeedPolicy;
|
||||
analyticalStorageTtl?: number;
|
||||
geospatialConfig?: GeospatialConfig;
|
||||
schema?: ISchema;
|
||||
requestSchema?: () => void;
|
||||
}
|
||||
|
||||
export interface Database extends Resource {
|
||||
@@ -174,12 +208,21 @@ export interface QueryMetrics {
|
||||
vmExecutionTime: any;
|
||||
}
|
||||
|
||||
export interface Offer extends Resource {
|
||||
export interface Offer {
|
||||
id: string;
|
||||
autoscaleMaxThroughput: number;
|
||||
manualThroughput: number;
|
||||
minimumThroughput: number;
|
||||
offerDefinition?: SDKOfferDefinition;
|
||||
offerReplacePending: boolean;
|
||||
}
|
||||
|
||||
export interface SDKOfferDefinition extends Resource {
|
||||
offerVersion?: string;
|
||||
offerType?: string;
|
||||
content?: {
|
||||
offerThroughput: number;
|
||||
offerIsRUPerMinuteThroughputEnabled: boolean;
|
||||
offerIsRUPerMinuteThroughputEnabled?: boolean;
|
||||
collectionThroughputInfo?: OfferThroughputInfo;
|
||||
offerAutopilotSettings?: AutoPilotOfferSettings;
|
||||
};
|
||||
@@ -187,22 +230,6 @@ export interface Offer extends Resource {
|
||||
offerResourceId?: string;
|
||||
}
|
||||
|
||||
export interface OfferWithHeaders extends Offer {
|
||||
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 {
|
||||
minimumRUForCollection: number;
|
||||
numPhysicalPartitions: number;
|
||||
@@ -221,7 +248,6 @@ export interface CreateDatabaseAndCollectionRequest {
|
||||
collectionId: string;
|
||||
offerThroughput: number;
|
||||
databaseLevelThroughput: boolean;
|
||||
rupmEnabled?: boolean;
|
||||
partitionKey?: PartitionKey;
|
||||
indexingPolicy?: IndexingPolicy;
|
||||
uniqueKeyPolicy?: UniqueKeyPolicy;
|
||||
|
||||
@@ -32,7 +32,8 @@ export enum MessageTypes {
|
||||
GetArcadiaToken,
|
||||
CreateWorkspace,
|
||||
CreateSparkPool,
|
||||
RefreshDatabaseAccount
|
||||
RefreshDatabaseAccount,
|
||||
InitTestExplorer
|
||||
}
|
||||
|
||||
export { Versions, ActionContracts, Diagnostics };
|
||||
|
||||
7
src/Contracts/SubscriptionType.ts
Normal file
7
src/Contracts/SubscriptionType.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export enum SubscriptionType {
|
||||
Benefits,
|
||||
EA,
|
||||
Free,
|
||||
Internal,
|
||||
PAYG
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import Trigger from "../Explorer/Tree/Trigger";
|
||||
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
|
||||
import { UploadDetails } from "../workers/upload/definitions";
|
||||
import * as DataModels from "./DataModels";
|
||||
import { SubscriptionType } from "./SubscriptionType";
|
||||
|
||||
export interface TokenProvider {
|
||||
getAuthHeader(): Promise<Headers>;
|
||||
@@ -115,9 +116,11 @@ export interface CollectionBase extends TreeNode {
|
||||
export interface Collection extends CollectionBase {
|
||||
defaultTtl: ko.Observable<number>;
|
||||
analyticalStorageTtl: ko.Observable<number>;
|
||||
schema?: DataModels.ISchema;
|
||||
requestSchema?: () => void;
|
||||
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
||||
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
||||
quotaInfo: ko.Observable<DataModels.CollectionQuotaInfo>;
|
||||
usageSizeInKB: ko.Observable<number>;
|
||||
offer: ko.Observable<DataModels.Offer>;
|
||||
conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
||||
changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
||||
@@ -358,6 +361,7 @@ export enum CollectionTabKind {
|
||||
SparkMasterTab = 16,
|
||||
Gallery = 17,
|
||||
NotebookViewer = 18,
|
||||
Schema = 19,
|
||||
SettingsV2 = 19
|
||||
}
|
||||
|
||||
@@ -412,14 +416,6 @@ export interface ThroughputDefaults {
|
||||
shared: number;
|
||||
}
|
||||
|
||||
export enum SubscriptionType {
|
||||
Benefits,
|
||||
EA,
|
||||
Free,
|
||||
Internal,
|
||||
PAYG
|
||||
}
|
||||
|
||||
export class MonacoEditorSettings {
|
||||
public readonly language: string;
|
||||
public readonly readOnly: boolean;
|
||||
|
||||
@@ -44,10 +44,6 @@ describe("Component Registerer", () => {
|
||||
expect(ko.components.isRegistered("user-defined-function-tab")).toBe(true);
|
||||
});
|
||||
|
||||
it("should register settings-tab component", () => {
|
||||
expect(ko.components.isRegistered("settings-tab")).toBe(true);
|
||||
});
|
||||
|
||||
it("should register settings-tab-v2 component", () => {
|
||||
expect(ko.components.isRegistered("settings-tab-v2")).toBe(true);
|
||||
});
|
||||
|
||||
@@ -31,7 +31,6 @@ ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTa
|
||||
ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab());
|
||||
ko.components.register("trigger-tab", new TabComponents.TriggerTab());
|
||||
ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab());
|
||||
ko.components.register("settings-tab", new TabComponents.SettingsTab());
|
||||
ko.components.register("settings-tab-v2", new TabComponents.SettingsTabV2());
|
||||
ko.components.register("query-tab", new TabComponents.QueryTab());
|
||||
ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab());
|
||||
|
||||
@@ -44,12 +44,10 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
||||
onChange?: (_?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => void;
|
||||
}[] = [
|
||||
{ key: "feature.enablechangefeedpolicy", label: "Enable change feed policy", value: "true" },
|
||||
{ key: "feature.enablerupm", label: "Enable RUPM", value: "true" },
|
||||
{ key: "feature.dataexplorerexecutesproc", label: "Execute stored procedure", value: "true" },
|
||||
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
|
||||
{ key: "feature.enablettl", label: "Enable TTL", value: "true" },
|
||||
{ key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" },
|
||||
{ key: "feature.enablecodeofconduct", label: "Enable Code Of Conduct Acknowledgement", value: "true" },
|
||||
{
|
||||
key: "feature.enableLinkInjection",
|
||||
label: "Enable Injecting Notebook Viewer Link into the first cell",
|
||||
|
||||
@@ -131,12 +131,6 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
label="Enable change feed policy"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enablerupm"
|
||||
label="Enable RUPM"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.dataexplorerexecutesproc"
|
||||
@@ -163,14 +157,14 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enablecodeofconduct"
|
||||
label="Enable Code Of Conduct Acknowledgement"
|
||||
key="feature.enableLinkInjection"
|
||||
label="Enable Injecting Notebook Viewer Link into the first cell"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enableLinkInjection"
|
||||
label="Enable Injecting Notebook Viewer Link into the first cell"
|
||||
key="feature.canexceedmaximumvalue"
|
||||
label="Can exceed max value"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
@@ -178,12 +172,6 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
className="checkboxRow"
|
||||
horizontalAlign="space-between"
|
||||
>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.canexceedmaximumvalue"
|
||||
label="Can exceed max value"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enablefixedcollectionwithsharedthroughput"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Dropdown,
|
||||
FocusZone,
|
||||
FontIcon,
|
||||
FontWeights,
|
||||
IDropdownOption,
|
||||
IPageSpecification,
|
||||
@@ -16,7 +17,7 @@ import {
|
||||
Text
|
||||
} from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { IGalleryItem, JunoClient, IJunoResponse, IPublicGalleryData } from "../../../Juno/JunoClient";
|
||||
import { IGalleryItem, IJunoResponse, IPublicGalleryData, JunoClient } from "../../../Juno/JunoClient";
|
||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
|
||||
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
|
||||
@@ -136,7 +137,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
||||
const tabs: GalleryTabInfo[] = [this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
||||
|
||||
if (this.props.container?.isGalleryPublishEnabled()) {
|
||||
tabs.push(
|
||||
@@ -146,7 +147,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
this.state.isCodeOfConductAccepted
|
||||
)
|
||||
);
|
||||
tabs.push(this.createTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
||||
tabs.push(this.createFavoritesTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
||||
|
||||
// explicitly checking if isCodeOfConductAccepted is not false, as it is initially undefined.
|
||||
// Displaying code of conduct component on gallery load should not be the default behavior.
|
||||
@@ -183,6 +184,27 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
);
|
||||
}
|
||||
|
||||
private isEmptyData = (data: IGalleryItem[]): boolean => {
|
||||
return !data || data.length === 0;
|
||||
};
|
||||
|
||||
private createEmptyTabContent = (iconName: string, line1: string, line2: string): JSX.Element => {
|
||||
return (
|
||||
<Stack horizontalAlign="center" tokens={{ childrenGap: 10 }}>
|
||||
<FontIcon iconName={iconName} style={{ fontSize: 100, color: "lightgray", marginTop: 20 }} />
|
||||
<Text styles={{ root: { fontWeight: FontWeights.semibold } }}>{line1}</Text>
|
||||
<Text>{line2}</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
private createSamplesTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
|
||||
return {
|
||||
tab,
|
||||
content: this.createSearchBarHeader(this.createCardsTabContent(data))
|
||||
};
|
||||
};
|
||||
|
||||
private createPublicGalleryTab(
|
||||
tab: GalleryTab,
|
||||
data: IGalleryItem[],
|
||||
@@ -194,17 +216,29 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
};
|
||||
}
|
||||
|
||||
private createTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
|
||||
private createFavoritesTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
|
||||
return {
|
||||
tab,
|
||||
content: this.createSearchBarHeader(this.createCardsTabContent(data))
|
||||
content: this.isEmptyData(data)
|
||||
? this.createEmptyTabContent(
|
||||
"ContactHeart",
|
||||
"You have not liked anything",
|
||||
"Like any notebook from Official Samples or Public gallery"
|
||||
)
|
||||
: this.createSearchBarHeader(this.createCardsTabContent(data))
|
||||
};
|
||||
}
|
||||
|
||||
private createPublishedNotebooksTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
|
||||
return {
|
||||
tab,
|
||||
content: this.createPublishedNotebooksTabContent(data)
|
||||
content: this.isEmptyData(data)
|
||||
? this.createEmptyTabContent(
|
||||
"Contact",
|
||||
"You have not published anything",
|
||||
"Publish your sample notebooks to share your published work with others"
|
||||
)
|
||||
: this.createPublishedNotebooksTabContent(data)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -364,9 +398,9 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
||||
if (!offline) {
|
||||
try {
|
||||
let response: IJunoResponse<IPublicGalleryData> | IJunoResponse<IGalleryItem[]>;
|
||||
if (this.props.container.isCodeOfConductEnabled()) {
|
||||
response = await this.props.junoClient.fetchPublicNotebooks();
|
||||
let response: IJunoResponse<IGalleryItem[]> | IJunoResponse<IPublicGalleryData>;
|
||||
if (this.props.container) {
|
||||
response = await this.props.junoClient.getPublicGalleryData();
|
||||
this.isCodeOfConductAccepted = response.data?.metadata.acceptedCodeOfConduct;
|
||||
this.publicNotebooks = response.data?.notebooksData;
|
||||
} else {
|
||||
@@ -568,7 +602,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
|
||||
private deleteItem = async (data: IGalleryItem): Promise<void> => {
|
||||
GalleryUtils.deleteItem(this.props.container, this.props.junoClient, data, item => {
|
||||
this.publishedNotebooks = this.publishedNotebooks.filter(notebook => item.id !== notebook.id);
|
||||
this.publishedNotebooks = this.publishedNotebooks?.filter(notebook => item.id !== notebook.id);
|
||||
this.refreshSelectedTab(item);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -89,12 +89,12 @@ describe("SettingsComponent", () => {
|
||||
it("auto pilot helper functions pass on correct value", () => {
|
||||
const newCollection = { ...collection };
|
||||
newCollection.offer = ko.observable<DataModels.Offer>({
|
||||
content: {
|
||||
offerAutopilotSettings: {
|
||||
maxThroughput: 10000
|
||||
}
|
||||
}
|
||||
} as DataModels.Offer);
|
||||
autoscaleMaxThroughput: 10000,
|
||||
manualThroughput: undefined,
|
||||
minimumThroughput: 400,
|
||||
id: "test",
|
||||
offerReplacePending: false
|
||||
});
|
||||
|
||||
const props = { ...baseProps };
|
||||
props.settingsTab.collection = newCollection;
|
||||
@@ -187,21 +187,6 @@ describe("SettingsComponent", () => {
|
||||
expect(settingsComponentInstance.hasConflictResolution()).toEqual(true);
|
||||
});
|
||||
|
||||
it("isOfferReplacePending", () => {
|
||||
let settingsComponentInstance = new SettingsComponent(baseProps);
|
||||
expect(settingsComponentInstance.isOfferReplacePending()).toEqual(undefined);
|
||||
|
||||
const newCollection = { ...collection };
|
||||
newCollection.offer = ko.observable({
|
||||
headers: { "x-ms-offer-replace-pending": true }
|
||||
} as DataModels.OfferWithHeaders);
|
||||
const props = { ...baseProps };
|
||||
props.settingsTab.collection = newCollection;
|
||||
|
||||
settingsComponentInstance = new SettingsComponent(props);
|
||||
expect(settingsComponentInstance.isOfferReplacePending()).toEqual(true);
|
||||
});
|
||||
|
||||
it("save calls updateCollection, updateMongoDBCollectionThroughRP and updateOffer", async () => {
|
||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
||||
wrapper.setState({ isSubSettingsSaveable: true, isScaleSaveable: true, isMongoIndexingPolicySaveable: true });
|
||||
|
||||
@@ -2,28 +2,23 @@ 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 SaveIcon from "../../../../images/save-cosmos.svg";
|
||||
import { traceStart, traceFailure, traceSuccess, trace } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
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 { throughputUnit } from "./SettingsRenderUtils";
|
||||
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
|
||||
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
|
||||
import {
|
||||
MongoIndexingPolicyComponent,
|
||||
MongoIndexingPolicyComponentProps
|
||||
} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
|
||||
import {
|
||||
getMaxRUs,
|
||||
hasDatabaseSharedThroughput,
|
||||
GeospatialConfigType,
|
||||
TtlType,
|
||||
@@ -49,6 +44,7 @@ import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/genera
|
||||
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
|
||||
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
|
||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||
import { isEmpty } from "underscore";
|
||||
|
||||
interface SettingsV2TabInfo {
|
||||
tab: SettingsV2TabTypes;
|
||||
@@ -227,7 +223,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
|
||||
public loadMongoIndexes = async (): Promise<void> => {
|
||||
if (
|
||||
this.container.isMongoIndexEditorEnabled() &&
|
||||
this.container.isPreferredApiMongoDB() &&
|
||||
this.container.isEnableMongoCapabilityPresent() &&
|
||||
this.container.databaseAccount()
|
||||
@@ -275,19 +270,14 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
};
|
||||
|
||||
private setAutoPilotStates = (): void => {
|
||||
const offer = this.collection?.offer && this.collection.offer();
|
||||
const offerAutopilotSettings = offer?.content?.offerAutopilotSettings;
|
||||
const autoscaleMaxThroughput = this.collection?.offer()?.autoscaleMaxThroughput;
|
||||
|
||||
if (
|
||||
offerAutopilotSettings &&
|
||||
offerAutopilotSettings.maxThroughput &&
|
||||
AutoPilotUtils.isValidAutoPilotThroughput(offerAutopilotSettings.maxThroughput)
|
||||
) {
|
||||
if (autoscaleMaxThroughput && AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
|
||||
this.setState({
|
||||
isAutoPilotSelected: true,
|
||||
wasAutopilotOriginallySet: true,
|
||||
autoPilotThroughput: offerAutopilotSettings.maxThroughput,
|
||||
autoPilotThroughputBaseline: offerAutopilotSettings.maxThroughput
|
||||
autoPilotThroughput: autoscaleMaxThroughput,
|
||||
autoPilotThroughputBaseline: autoscaleMaxThroughput
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -305,12 +295,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
!!this.collection.conflictResolutionPolicy();
|
||||
|
||||
public isOfferReplacePending = (): boolean => {
|
||||
const offer = this.collection?.offer && this.collection.offer();
|
||||
return (
|
||||
offer &&
|
||||
Object.keys(offer).find(value => value === "headers") &&
|
||||
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
||||
);
|
||||
return this.collection?.offer()?.offerReplacePending;
|
||||
};
|
||||
|
||||
public onSaveClick = async (): Promise<void> => {
|
||||
@@ -448,103 +433,34 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
}
|
||||
|
||||
if (this.state.isScaleSaveable) {
|
||||
const newThroughput = this.state.throughput;
|
||||
const newOffer: DataModels.Offer = { ...this.collection.offer() };
|
||||
const originalThroughputValue: number = this.state.throughput;
|
||||
|
||||
if (newOffer.content) {
|
||||
newOffer.content.offerThroughput = newThroughput;
|
||||
} else {
|
||||
newOffer.content = {
|
||||
offerThroughput: newThroughput,
|
||||
offerIsRUPerMinuteThroughputEnabled: false
|
||||
};
|
||||
}
|
||||
|
||||
const headerOptions: RequestOptions = { initialHeaders: {} };
|
||||
|
||||
if (this.state.isAutoPilotSelected) {
|
||||
newOffer.content.offerAutopilotSettings = {
|
||||
maxThroughput: this.state.autoPilotThroughput
|
||||
};
|
||||
|
||||
// user has changed from provisioned --> autoscale
|
||||
if (this.hasProvisioningTypeChanged()) {
|
||||
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToAutopilot] = "true";
|
||||
delete newOffer.content.offerAutopilotSettings;
|
||||
} else {
|
||||
delete newOffer.content.offerThroughput;
|
||||
}
|
||||
} else {
|
||||
this.setState({
|
||||
isAutoPilotSelected: false
|
||||
});
|
||||
|
||||
// user has changed from autoscale --> provisioned
|
||||
if (this.hasProvisioningTypeChanged()) {
|
||||
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToManualThroughput] = "true";
|
||||
} else {
|
||||
delete newOffer.content.offerAutopilotSettings;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
getMaxRUs(this.collection, this.container) <=
|
||||
SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
this.container
|
||||
) {
|
||||
const requestPayload = {
|
||||
subscriptionId: userContext.subscriptionId,
|
||||
databaseAccountName: userContext.databaseAccount.name,
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
databaseName: this.collection.databaseId,
|
||||
collectionName: this.collection.id(),
|
||||
throughput: newThroughput,
|
||||
offerIsRUPerMinuteThroughputEnabled: false
|
||||
};
|
||||
|
||||
await updateOfferThroughputBeyondLimit(requestPayload);
|
||||
this.collection.offer().content.offerThroughput = originalThroughputValue;
|
||||
this.setState({
|
||||
isScaleSaveable: false,
|
||||
isScaleDiscardable: false,
|
||||
throughput: originalThroughputValue,
|
||||
throughputBaseline: originalThroughputValue,
|
||||
initialNotification: {
|
||||
description: `Throughput update for ${newThroughput} ${throughputUnit}`
|
||||
} as DataModels.Notification
|
||||
});
|
||||
} else {
|
||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||
databaseId: this.collection.databaseId,
|
||||
collectionId: this.collection.id(),
|
||||
currentOffer: this.collection.offer(),
|
||||
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
||||
manualThroughput: this.state.isAutoPilotSelected ? undefined : newThroughput
|
||||
};
|
||||
if (this.hasProvisioningTypeChanged()) {
|
||||
if (this.state.isAutoPilotSelected) {
|
||||
updateOfferParams.migrateToAutoPilot = true;
|
||||
} else {
|
||||
updateOfferParams.migrateToManual = true;
|
||||
}
|
||||
}
|
||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||
this.collection.offer(updatedOffer);
|
||||
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||
databaseId: this.collection.databaseId,
|
||||
collectionId: this.collection.id(),
|
||||
currentOffer: this.collection.offer(),
|
||||
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
||||
manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput
|
||||
};
|
||||
if (this.hasProvisioningTypeChanged()) {
|
||||
if (this.state.isAutoPilotSelected) {
|
||||
this.setState({
|
||||
autoPilotThroughput: updatedOffer.content.offerAutopilotSettings.maxThroughput,
|
||||
autoPilotThroughputBaseline: updatedOffer.content.offerAutopilotSettings.maxThroughput
|
||||
});
|
||||
updateOfferParams.migrateToAutoPilot = true;
|
||||
} else {
|
||||
this.setState({
|
||||
throughput: updatedOffer.content.offerThroughput,
|
||||
throughputBaseline: updatedOffer.content.offerThroughput
|
||||
});
|
||||
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.autoscaleMaxThroughput,
|
||||
autoPilotThroughputBaseline: updatedOffer.autoscaleMaxThroughput
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
throughput: updatedOffer.manualThroughput,
|
||||
throughputBaseline: updatedOffer.manualThroughput
|
||||
});
|
||||
}
|
||||
}
|
||||
this.container.isRefreshingExplorer(false);
|
||||
this.setBaseline();
|
||||
@@ -809,7 +725,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
}
|
||||
}
|
||||
|
||||
const offerThroughput = this.collection?.offer && this.collection.offer()?.content?.offerThroughput;
|
||||
const offerThroughput = this.collection.offer()?.manualThroughput;
|
||||
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
||||
? ChangeFeedPolicyState.On
|
||||
: ChangeFeedPolicyState.Off;
|
||||
@@ -1000,15 +916,18 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||
content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />
|
||||
});
|
||||
} else if (
|
||||
this.container.isMongoIndexEditorEnabled() &&
|
||||
this.container.isPreferredApiMongoDB() &&
|
||||
this.container.isEnableMongoCapabilityPresent()
|
||||
) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />
|
||||
});
|
||||
} else if (this.container.isPreferredApiMongoDB()) {
|
||||
if (isEmpty(this.container.features())) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||
content: mongoIndexingPolicyAADError
|
||||
});
|
||||
} else if (this.container.isEnableMongoCapabilityPresent()) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (this.hasConflictResolution()) {
|
||||
|
||||
@@ -31,7 +31,7 @@ class SettingsRenderUtilsTestComponent extends React.Component {
|
||||
{getAutoPilotV3SpendElement(1000, true)}
|
||||
{getAutoPilotV3SpendElement(undefined, true)}
|
||||
|
||||
{getEstimatedSpendElement(1000, "mooncake", 2, false, true)}
|
||||
{getEstimatedSpendElement(1000, "mooncake", 2, false)}
|
||||
|
||||
{getEstimatedAutoscaleSpendElement(1000, "mooncake", 2, false)}
|
||||
|
||||
@@ -42,7 +42,7 @@ class SettingsRenderUtilsTestComponent extends React.Component {
|
||||
{updateThroughputDelayedApplyWarningMessage}
|
||||
|
||||
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||
{getThroughputApplyShortDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||
{getThroughputApplyShortDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection")}
|
||||
{getThroughputApplyLongDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||
|
||||
{getToolTipContainer(<span>Sample Text</span>)}
|
||||
|
||||
@@ -199,10 +199,9 @@ export const getEstimatedSpendElement = (
|
||||
throughput: number,
|
||||
serverId: string,
|
||||
regions: number,
|
||||
multimaster: boolean,
|
||||
rupmEnabled: boolean
|
||||
multimaster: boolean
|
||||
): JSX.Element => {
|
||||
const hourlyPrice: number = computeRUUsagePriceHourly(serverId, rupmEnabled, throughput, regions, multimaster);
|
||||
const hourlyPrice: number = computeRUUsagePriceHourly(serverId, throughput, regions, multimaster);
|
||||
const dailyPrice: number = hourlyPrice * 24;
|
||||
const monthlyPrice: number = hourlyPrice * hoursInAMonth;
|
||||
const currency: string = getPriceCurrency(serverId);
|
||||
@@ -319,14 +318,13 @@ export const getThroughputApplyShortDelayMessage = (
|
||||
throughput: number,
|
||||
throughputUnit: string,
|
||||
databaseName: string,
|
||||
collectionName: string,
|
||||
targetThroughput: number
|
||||
collectionName: string
|
||||
): JSX.Element => (
|
||||
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
|
||||
A request to increase the throughput is currently in progress. This operation will take some time to complete.
|
||||
<br />
|
||||
Database: {databaseName}, Container: {collectionName}{" "}
|
||||
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, targetThroughput)}
|
||||
{getCurrentThroughput(isAutoscale, throughput, throughputUnit)}
|
||||
</Text>
|
||||
);
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
transparentDetailsRowStyles,
|
||||
createAndAddMongoIndexStackProps,
|
||||
separatorStyles,
|
||||
mongoIndexingPolicyAADError,
|
||||
indexingPolicynUnsavedWarningMessage,
|
||||
infoAndToolTipTextStyle
|
||||
} from "../../SettingsRenderUtils";
|
||||
@@ -40,7 +39,6 @@ import {
|
||||
} from "../../SettingsUtils";
|
||||
import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
|
||||
import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import { AuthType } from "../../../../../AuthType";
|
||||
import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
||||
|
||||
export interface MongoIndexingPolicyComponentProps {
|
||||
@@ -321,7 +319,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
</Stack>
|
||||
);
|
||||
} else {
|
||||
return window.authType !== AuthType.AAD ? mongoIndexingPolicyAADError : <Spinner size={SpinnerSize.large} />;
|
||||
return <Spinner size={SpinnerSize.large} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ describe("ScaleComponent", () => {
|
||||
} as DataModels.Notification
|
||||
};
|
||||
|
||||
it("renders with correct intiial notification", () => {
|
||||
it("renders with correct initial notification", () => {
|
||||
let wrapper = shallow(<ScaleComponent {...baseProps} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
|
||||
@@ -54,16 +54,13 @@ describe("ScaleComponent", () => {
|
||||
|
||||
const newCollection = { ...collection };
|
||||
const maxThroughput = 5000;
|
||||
const targetMaxThroughput = 50000;
|
||||
newCollection.offer = ko.observable({
|
||||
content: {
|
||||
offerAutopilotSettings: {
|
||||
maxThroughput: maxThroughput,
|
||||
targetMaxThroughput: targetMaxThroughput
|
||||
}
|
||||
},
|
||||
headers: { "x-ms-offer-replace-pending": true }
|
||||
} as DataModels.OfferWithHeaders);
|
||||
manualThroughput: undefined,
|
||||
autoscaleMaxThroughput: maxThroughput,
|
||||
minimumThroughput: 400,
|
||||
id: "offer",
|
||||
offerReplacePending: true
|
||||
});
|
||||
const newProps = {
|
||||
...baseProps,
|
||||
initialNotification: undefined as DataModels.Notification,
|
||||
@@ -73,7 +70,6 @@ describe("ScaleComponent", () => {
|
||||
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true);
|
||||
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(false);
|
||||
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(maxThroughput);
|
||||
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(targetMaxThroughput);
|
||||
});
|
||||
|
||||
it("autoScale disabled", () => {
|
||||
@@ -109,11 +105,6 @@ describe("ScaleComponent", () => {
|
||||
expect(scaleComponent.isAutoScaleEnabled()).toEqual(true);
|
||||
});
|
||||
|
||||
it("getMaxRUThroughputInputLimit", () => {
|
||||
const scaleComponent = new ScaleComponent(baseProps);
|
||||
expect(scaleComponent.getMaxRUThroughputInputLimit()).toEqual(40000);
|
||||
});
|
||||
|
||||
it("getThroughputTitle", () => {
|
||||
let scaleComponent = new ScaleComponent(baseProps);
|
||||
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
|
||||
@@ -138,14 +129,8 @@ describe("ScaleComponent", () => {
|
||||
|
||||
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);
|
||||
const 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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,10 +12,9 @@ import {
|
||||
throughputUnit,
|
||||
getThroughputApplyLongDelayMessage,
|
||||
getThroughputApplyShortDelayMessage,
|
||||
updateThroughputBeyondLimitWarningMessage,
|
||||
updateThroughputDelayedApplyWarningMessage
|
||||
updateThroughputBeyondLimitWarningMessage
|
||||
} from "../SettingsRenderUtils";
|
||||
import { getMaxRUs, getMinRUs, hasDatabaseSharedThroughput } from "../SettingsUtils";
|
||||
import { hasDatabaseSharedThroughput } from "../SettingsUtils";
|
||||
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
|
||||
import { Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
|
||||
import { configContext, Platform } from "../../../../ConfigContext";
|
||||
@@ -62,11 +61,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
};
|
||||
|
||||
private getStorageCapacityTitle = (): JSX.Element => {
|
||||
// Mongo container with system partition key still treat as "Fixed"
|
||||
const isFixed =
|
||||
!this.props.collection.partitionKey ||
|
||||
(this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey);
|
||||
const capacity: string = isFixed ? "Fixed" : "Unlimited";
|
||||
const capacity: string = this.props.isFixedContainer ? "Fixed" : "Unlimited";
|
||||
return (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<Label>Storage capacity</Label>
|
||||
@@ -75,12 +70,26 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
);
|
||||
};
|
||||
|
||||
public getMaxRUThroughputInputLimit = (): number => {
|
||||
if (configContext.platform === Platform.Hosted && this.props.collection.partitionKey) {
|
||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||
public getMaxRUs = (): number => {
|
||||
if (this.props.container?.isTryCosmosDBSubscription()) {
|
||||
return Constants.TryCosmosExperience.maxRU;
|
||||
}
|
||||
|
||||
return getMaxRUs(this.props.collection, this.props.container);
|
||||
if (this.props.isFixedContainer) {
|
||||
return SharedConstants.CollectionCreation.MaxRUPerPartition;
|
||||
}
|
||||
|
||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||
};
|
||||
|
||||
public getMinRUs = (): number => {
|
||||
if (this.props.container?.isTryCosmosDBSubscription()) {
|
||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||
}
|
||||
|
||||
return (
|
||||
this.props.collection.offer()?.minimumThroughput || SharedConstants.CollectionCreation.DefaultCollectionRUs400
|
||||
);
|
||||
};
|
||||
|
||||
public getThroughputTitle = (): string => {
|
||||
@@ -88,11 +97,8 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
return AutoPilotUtils.getAutoPilotHeaderText();
|
||||
}
|
||||
|
||||
const minThroughput: string = getMinRUs(this.props.collection, this.props.container).toLocaleString();
|
||||
const maxThroughput: string =
|
||||
this.canThroughputExceedMaximumValue() && !this.props.isFixedContainer
|
||||
? "unlimited"
|
||||
: getMaxRUs(this.props.collection, this.props.container).toLocaleString();
|
||||
const minThroughput: string = this.getMinRUs().toLocaleString();
|
||||
const maxThroughput: string = !this.props.isFixedContainer ? "unlimited" : this.getMaxRUs().toLocaleString();
|
||||
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`;
|
||||
};
|
||||
|
||||
@@ -109,26 +115,15 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
return this.getLongDelayMessage();
|
||||
}
|
||||
|
||||
const offer = this.props.collection?.offer && this.props.collection.offer();
|
||||
if (
|
||||
offer &&
|
||||
Object.keys(offer).find(value => {
|
||||
return value === "headers";
|
||||
}) &&
|
||||
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
||||
) {
|
||||
const throughput = offer?.content?.offerAutopilotSettings?.maxThroughput;
|
||||
|
||||
const targetThroughput =
|
||||
offer.content?.offerAutopilotSettings?.targetMaxThroughput || offer?.content?.offerThroughput;
|
||||
|
||||
const offer = this.props.collection?.offer();
|
||||
if (offer?.offerReplacePending) {
|
||||
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
|
||||
return getThroughputApplyShortDelayMessage(
|
||||
this.props.isAutoPilotSelected,
|
||||
throughput,
|
||||
throughputUnit,
|
||||
this.props.collection.databaseId,
|
||||
this.props.collection.id(),
|
||||
targetThroughput
|
||||
this.props.collection.id()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -138,21 +133,12 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -179,12 +165,12 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
private getThroughputInputComponent = (): JSX.Element => (
|
||||
<ThroughputInputAutoPilotV3Component
|
||||
databaseAccount={this.props.container.databaseAccount()}
|
||||
serverId={this.props.container.serverId()}
|
||||
serverId={configContext.serverId}
|
||||
throughput={this.props.throughput}
|
||||
throughputBaseline={this.props.throughputBaseline}
|
||||
onThroughputChange={this.props.onThroughputChange}
|
||||
minimum={getMinRUs(this.props.collection, this.props.container)}
|
||||
maximum={this.getMaxRUThroughputInputLimit()}
|
||||
minimum={this.getMinRUs()}
|
||||
maximum={this.getMaxRUs()}
|
||||
isEnabled={!hasDatabaseSharedThroughput(this.props.collection)}
|
||||
canExceedMaximumValue={this.canThroughputExceedMaximumValue()}
|
||||
label={this.getThroughputTitle()}
|
||||
@@ -200,6 +186,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
onScaleSaveableChange={this.props.onScaleSaveableChange}
|
||||
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
|
||||
getThroughputWarningMessage={this.getThroughputWarningMessage}
|
||||
usageSizeInKB={this.props.collection.usageSizeInKB()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ describe("ThroughputInputAutoPilotV3Component", () => {
|
||||
minimum: 10000,
|
||||
maximum: 400,
|
||||
step: 100,
|
||||
usageSizeInKB: 10000,
|
||||
isEnabled: true,
|
||||
isEmulator: false,
|
||||
spendAckChecked: false,
|
||||
|
||||
@@ -30,6 +30,10 @@ import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../S
|
||||
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 {
|
||||
databaseAccount: DataModels.DatabaseAccount;
|
||||
@@ -60,6 +64,7 @@ export interface ThroughputInputAutoPilotV3Props {
|
||||
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
||||
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
||||
getThroughputWarningMessage: () => JSX.Element;
|
||||
usageSizeInKB: number;
|
||||
}
|
||||
|
||||
interface ThroughputInputAutoPilotV3State {
|
||||
@@ -174,8 +179,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : offerThroughput,
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
false
|
||||
multimaster
|
||||
);
|
||||
} else {
|
||||
estimatedSpend = getEstimatedAutoscaleSpendElement(
|
||||
@@ -224,6 +228,29 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
option?: IChoiceGroupOption
|
||||
): void => this.props.onAutoPilotSelected(option.key === "true");
|
||||
|
||||
private minRUperGBSurvey = (): JSX.Element => {
|
||||
const href = `https://ncv.microsoft.com/vRBTO37jmO?ctx={"AzureSubscriptionId":"${userContext.subscriptionId}","CosmosDBAccountName":"${userContext.databaseAccount?.name}"}`;
|
||||
const oneTBinKB = 1000000000;
|
||||
const minRUperGB = 10;
|
||||
const featureFlagEnabled = window.dataExplorer?.isFeatureEnabled(Features.showMinRUSurvey);
|
||||
const collectionIsEligible =
|
||||
userContext.subscriptionType !== SubscriptionType.Internal &&
|
||||
this.props.usageSizeInKB > oneTBinKB &&
|
||||
this.props.minimum >= usageInGB(this.props.usageSizeInKB) * minRUperGB;
|
||||
if (featureFlagEnabled || collectionIsEligible) {
|
||||
return (
|
||||
<Text>
|
||||
Need to scale below {this.props.minimum} RU/s? Reach out by filling{" "}
|
||||
<a target="_blank" rel="noreferrer" href={href}>
|
||||
this questionnaire
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
private renderThroughputModeChoices = (): JSX.Element => {
|
||||
const labelId = "settingsV2RadioButtonLabelId";
|
||||
return (
|
||||
@@ -275,6 +302,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
onChange={this.onAutoPilotThroughputChange}
|
||||
/>
|
||||
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
|
||||
{this.minRUperGBSurvey()}
|
||||
{this.props.spendAckVisible && (
|
||||
<Checkbox
|
||||
id="spendAckCheckBox"
|
||||
@@ -305,15 +333,13 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
}
|
||||
onChange={this.onThroughputChange}
|
||||
/>
|
||||
|
||||
{this.props.getThroughputWarningMessage() && (
|
||||
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
|
||||
{this.props.getThroughputWarningMessage()}
|
||||
</MessageBar>
|
||||
)}
|
||||
|
||||
{!this.props.isEmulator && this.getRequestUnitsUsageCost()}
|
||||
|
||||
{this.minRUperGBSurvey()}
|
||||
{this.props.spendAckVisible && (
|
||||
<Checkbox
|
||||
id="spendAckCheckBox"
|
||||
@@ -323,7 +349,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
onChange={this.onSpendAckChecked}
|
||||
/>
|
||||
)}
|
||||
|
||||
{this.props.isFixed && <p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ScaleComponent renders with correct intiial notification 1`] = `
|
||||
exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
@@ -48,7 +48,7 @@ exports[`ScaleComponent renders with correct intiial notification 1`] = `
|
||||
label="Throughput (6,000 - unlimited RU/s)"
|
||||
maxAutoPilotThroughput={4000}
|
||||
maxAutoPilotThroughputBaseline={4000}
|
||||
maximum={40000}
|
||||
maximum={1000000}
|
||||
minimum={6000}
|
||||
onAutoPilotSelected={[Function]}
|
||||
onMaxAutoPilotThroughputChange={[Function]}
|
||||
@@ -58,6 +58,7 @@ exports[`ScaleComponent renders with correct intiial notification 1`] = `
|
||||
spendAckChecked={false}
|
||||
throughput={1000}
|
||||
throughputBaseline={1000}
|
||||
usageSizeInKB={100}
|
||||
wasAutopilotOriginallySet={true}
|
||||
/>
|
||||
<Stack
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { collection, container } from "./TestUtils";
|
||||
import { collection } from "./TestUtils";
|
||||
import {
|
||||
getMaxRUs,
|
||||
getMinRUs,
|
||||
getMongoIndexType,
|
||||
getMongoNotification,
|
||||
getSanitizedInputValue,
|
||||
@@ -23,16 +21,6 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import ko from "knockout";
|
||||
|
||||
describe("SettingsUtils", () => {
|
||||
it("getMaxRUs", () => {
|
||||
expect(collection.offer().content.collectionThroughputInfo.numPhysicalPartitions).toEqual(4);
|
||||
expect(getMaxRUs(collection, container)).toEqual(40000);
|
||||
});
|
||||
|
||||
it("getMinRUs", () => {
|
||||
expect(collection.offer().content.collectionThroughputInfo.numPhysicalPartitions).toEqual(4);
|
||||
expect(getMinRUs(collection, container)).toEqual(6000);
|
||||
});
|
||||
|
||||
it("hasDatabaseSharedThroughput", () => {
|
||||
expect(hasDatabaseSharedThroughput(collection)).toEqual(undefined);
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
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 PricingUtils from "../../../Utils/PricingUtils";
|
||||
|
||||
import Explorer from "../../Explorer";
|
||||
import { MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
|
||||
|
||||
const zeroValue = 0;
|
||||
@@ -71,57 +67,6 @@ export const hasDatabaseSharedThroughput = (collection: ViewModels.Collection):
|
||||
return database?.isDatabaseShared() && !collection.offer();
|
||||
};
|
||||
|
||||
export const getMaxRUs = (collection: ViewModels.Collection, container: Explorer): number => {
|
||||
const isTryCosmosDBSubscription = container?.isTryCosmosDBSubscription() || false;
|
||||
if (isTryCosmosDBSubscription) {
|
||||
return Constants.TryCosmosExperience.maxRU;
|
||||
}
|
||||
|
||||
const numPartitionsFromOffer: number =
|
||||
collection?.offer && collection.offer()?.content?.collectionThroughputInfo?.numPhysicalPartitions;
|
||||
|
||||
const numPartitionsFromQuotaInfo: number = collection?.quotaInfo()?.numPartitions;
|
||||
|
||||
const numPartitions = numPartitionsFromOffer ?? numPartitionsFromQuotaInfo ?? 1;
|
||||
|
||||
return SharedConstants.CollectionCreation.MaxRUPerPartition * numPartitions;
|
||||
};
|
||||
|
||||
export const getMinRUs = (collection: ViewModels.Collection, container: Explorer): number => {
|
||||
const isTryCosmosDBSubscription = container?.isTryCosmosDBSubscription();
|
||||
if (isTryCosmosDBSubscription) {
|
||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||
}
|
||||
|
||||
const offerContent = collection?.offer && collection.offer()?.content;
|
||||
|
||||
if (offerContent?.offerAutopilotSettings) {
|
||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||
}
|
||||
|
||||
const collectionThroughputInfo: DataModels.OfferThroughputInfo = offerContent?.collectionThroughputInfo;
|
||||
|
||||
if (collectionThroughputInfo?.minimumRUForCollection > 0) {
|
||||
return collectionThroughputInfo.minimumRUForCollection;
|
||||
}
|
||||
|
||||
const numPartitions = collectionThroughputInfo?.numPhysicalPartitions ?? collection.quotaInfo()?.numPartitions;
|
||||
|
||||
if (!numPartitions || numPartitions === 1) {
|
||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||
}
|
||||
|
||||
const baseRU = SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||
|
||||
const quotaInKb = collection.quotaInfo().collectionSize;
|
||||
const quotaInGb = PricingUtils.usageInGB(quotaInKb);
|
||||
|
||||
const perPartitionGBQuota: number = Math.max(10, quotaInGb / numPartitions);
|
||||
const baseRUbyPartitions: number = ((numPartitions * perPartitionGBQuota) / 10) * 100;
|
||||
|
||||
return Math.max(baseRU, baseRUbyPartitions);
|
||||
};
|
||||
|
||||
export const parseConflictResolutionMode = (modeFromBackend: string): DataModels.ConflictResolutionMode => {
|
||||
// Backend can contain different casing as it does case-insensitive comparisson
|
||||
if (!modeFromBackend) {
|
||||
|
||||
@@ -18,17 +18,14 @@ export const collection = ({
|
||||
excludedPaths: []
|
||||
}),
|
||||
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
|
||||
quotaInfo: ko.observable<DataModels.CollectionQuotaInfo>({} as DataModels.CollectionQuotaInfo),
|
||||
usageSizeInKB: ko.observable(100),
|
||||
offer: ko.observable<DataModels.Offer>({
|
||||
content: {
|
||||
offerThroughput: 10000,
|
||||
offerIsRUPerMinuteThroughputEnabled: false,
|
||||
collectionThroughputInfo: {
|
||||
minimumRUForCollection: 6000,
|
||||
numPhysicalPartitions: 4
|
||||
} as DataModels.OfferThroughputInfo
|
||||
}
|
||||
} as DataModels.Offer),
|
||||
autoscaleMaxThroughput: undefined,
|
||||
manualThroughput: 10000,
|
||||
minimumThroughput: 6000,
|
||||
id: "offer",
|
||||
offerReplacePending: false
|
||||
}),
|
||||
conflictResolutionPolicy: ko.observable<DataModels.ConflictResolutionPolicy>(
|
||||
{} as DataModels.ConflictResolutionPolicy
|
||||
),
|
||||
|
||||
@@ -133,8 +133,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"partitionKeyVisible": [Function],
|
||||
"requestUnitsUsageCost": [Function],
|
||||
"ruToolTipText": [Function],
|
||||
"rupm": [Function],
|
||||
"rupmVisible": [Function],
|
||||
"sharedAutoPilotThroughput": [Function],
|
||||
"sharedThroughputRangeText": [Function],
|
||||
"shouldCreateMongoWildcardIndex": [Function],
|
||||
@@ -622,8 +620,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"partitionKeyVisible": [Function],
|
||||
"requestUnitsUsageCost": [Function],
|
||||
"ruToolTipText": [Function],
|
||||
"rupm": [Function],
|
||||
"rupmVisible": [Function],
|
||||
"sharedAutoPilotThroughput": [Function],
|
||||
"sharedThroughputRangeText": [Function],
|
||||
"shouldCreateMongoWildcardIndex": [Function],
|
||||
@@ -735,7 +731,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"visible": [Function],
|
||||
},
|
||||
"arcadiaToken": [Function],
|
||||
"armEndpoint": [Function],
|
||||
"browseQueriesPane": BrowseQueriesPane {
|
||||
"canSaveQueries": [Function],
|
||||
"container": [Circular],
|
||||
@@ -947,7 +942,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"hasWriteAccess": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isAuthWithResourceToken": [Function],
|
||||
"isCodeOfConductEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
@@ -956,7 +950,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isHostedDataExplorerEnabled": [Function],
|
||||
"isLeftPaneExpanded": [Function],
|
||||
"isLinkInjectionEnabled": [Function],
|
||||
"isMongoIndexEditorEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isNotificationConsoleExpanded": [Function],
|
||||
@@ -971,8 +964,8 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isRefreshingExplorer": [Function],
|
||||
"isResourceTokenCollectionNodeSelected": [Function],
|
||||
"isRightPanelV2Enabled": [Function],
|
||||
"isSchemaEnabled": [Function],
|
||||
"isServerlessEnabled": [Function],
|
||||
"isSettingsV2Enabled": [Function],
|
||||
"isSparkEnabled": [Function],
|
||||
"isSparkEnabledForAccount": [Function],
|
||||
"isSynapseLinkUpdating": [Function],
|
||||
@@ -1029,7 +1022,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"onSwitchToConnectionString": [Function],
|
||||
"onToggleKeyDown": [Function],
|
||||
"parentFrameDataExplorerVersion": [Function],
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
@@ -1055,7 +1047,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"titleLabel": "Select Columns",
|
||||
"visible": [Function],
|
||||
},
|
||||
"quotaId": [Function],
|
||||
"refreshDatabaseAccount": [Function],
|
||||
"refreshNotebookList": [Function],
|
||||
"refreshTreeTitle": [Function],
|
||||
@@ -1301,9 +1292,9 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"version": 2,
|
||||
},
|
||||
"partitionKeyProperty": "partitionKey",
|
||||
"quotaInfo": [Function],
|
||||
"readSettings": [Function],
|
||||
"uniqueKeyPolicy": Object {},
|
||||
"usageSizeInKB": [Function],
|
||||
}
|
||||
}
|
||||
container={
|
||||
@@ -1413,8 +1404,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"partitionKeyVisible": [Function],
|
||||
"requestUnitsUsageCost": [Function],
|
||||
"ruToolTipText": [Function],
|
||||
"rupm": [Function],
|
||||
"rupmVisible": [Function],
|
||||
"sharedAutoPilotThroughput": [Function],
|
||||
"sharedThroughputRangeText": [Function],
|
||||
"shouldCreateMongoWildcardIndex": [Function],
|
||||
@@ -1902,8 +1891,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"partitionKeyVisible": [Function],
|
||||
"requestUnitsUsageCost": [Function],
|
||||
"ruToolTipText": [Function],
|
||||
"rupm": [Function],
|
||||
"rupmVisible": [Function],
|
||||
"sharedAutoPilotThroughput": [Function],
|
||||
"sharedThroughputRangeText": [Function],
|
||||
"shouldCreateMongoWildcardIndex": [Function],
|
||||
@@ -2015,7 +2002,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"visible": [Function],
|
||||
},
|
||||
"arcadiaToken": [Function],
|
||||
"armEndpoint": [Function],
|
||||
"browseQueriesPane": BrowseQueriesPane {
|
||||
"canSaveQueries": [Function],
|
||||
"container": [Circular],
|
||||
@@ -2227,7 +2213,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"hasWriteAccess": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isAuthWithResourceToken": [Function],
|
||||
"isCodeOfConductEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
@@ -2236,7 +2221,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isHostedDataExplorerEnabled": [Function],
|
||||
"isLeftPaneExpanded": [Function],
|
||||
"isLinkInjectionEnabled": [Function],
|
||||
"isMongoIndexEditorEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isNotificationConsoleExpanded": [Function],
|
||||
@@ -2251,8 +2235,8 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isRefreshingExplorer": [Function],
|
||||
"isResourceTokenCollectionNodeSelected": [Function],
|
||||
"isRightPanelV2Enabled": [Function],
|
||||
"isSchemaEnabled": [Function],
|
||||
"isServerlessEnabled": [Function],
|
||||
"isSettingsV2Enabled": [Function],
|
||||
"isSparkEnabled": [Function],
|
||||
"isSparkEnabledForAccount": [Function],
|
||||
"isSynapseLinkUpdating": [Function],
|
||||
@@ -2309,7 +2293,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"onSwitchToConnectionString": [Function],
|
||||
"onToggleKeyDown": [Function],
|
||||
"parentFrameDataExplorerVersion": [Function],
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
@@ -2335,7 +2318,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"titleLabel": "Select Columns",
|
||||
"visible": [Function],
|
||||
},
|
||||
"quotaId": [Function],
|
||||
"refreshDatabaseAccount": [Function],
|
||||
"refreshNotebookList": [Function],
|
||||
"refreshTreeTitle": [Function],
|
||||
@@ -2706,8 +2688,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"partitionKeyVisible": [Function],
|
||||
"requestUnitsUsageCost": [Function],
|
||||
"ruToolTipText": [Function],
|
||||
"rupm": [Function],
|
||||
"rupmVisible": [Function],
|
||||
"sharedAutoPilotThroughput": [Function],
|
||||
"sharedThroughputRangeText": [Function],
|
||||
"shouldCreateMongoWildcardIndex": [Function],
|
||||
@@ -3195,8 +3175,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"partitionKeyVisible": [Function],
|
||||
"requestUnitsUsageCost": [Function],
|
||||
"ruToolTipText": [Function],
|
||||
"rupm": [Function],
|
||||
"rupmVisible": [Function],
|
||||
"sharedAutoPilotThroughput": [Function],
|
||||
"sharedThroughputRangeText": [Function],
|
||||
"shouldCreateMongoWildcardIndex": [Function],
|
||||
@@ -3308,7 +3286,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"visible": [Function],
|
||||
},
|
||||
"arcadiaToken": [Function],
|
||||
"armEndpoint": [Function],
|
||||
"browseQueriesPane": BrowseQueriesPane {
|
||||
"canSaveQueries": [Function],
|
||||
"container": [Circular],
|
||||
@@ -3520,7 +3497,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"hasWriteAccess": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isAuthWithResourceToken": [Function],
|
||||
"isCodeOfConductEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
@@ -3529,7 +3505,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isHostedDataExplorerEnabled": [Function],
|
||||
"isLeftPaneExpanded": [Function],
|
||||
"isLinkInjectionEnabled": [Function],
|
||||
"isMongoIndexEditorEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isNotificationConsoleExpanded": [Function],
|
||||
@@ -3544,8 +3519,8 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isRefreshingExplorer": [Function],
|
||||
"isResourceTokenCollectionNodeSelected": [Function],
|
||||
"isRightPanelV2Enabled": [Function],
|
||||
"isSchemaEnabled": [Function],
|
||||
"isServerlessEnabled": [Function],
|
||||
"isSettingsV2Enabled": [Function],
|
||||
"isSparkEnabled": [Function],
|
||||
"isSparkEnabledForAccount": [Function],
|
||||
"isSynapseLinkUpdating": [Function],
|
||||
@@ -3602,7 +3577,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"onSwitchToConnectionString": [Function],
|
||||
"onToggleKeyDown": [Function],
|
||||
"parentFrameDataExplorerVersion": [Function],
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
@@ -3628,7 +3602,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"titleLabel": "Select Columns",
|
||||
"visible": [Function],
|
||||
},
|
||||
"quotaId": [Function],
|
||||
"refreshDatabaseAccount": [Function],
|
||||
"refreshNotebookList": [Function],
|
||||
"refreshTreeTitle": [Function],
|
||||
@@ -3874,9 +3847,9 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"version": 2,
|
||||
},
|
||||
"partitionKeyProperty": "partitionKey",
|
||||
"quotaInfo": [Function],
|
||||
"readSettings": [Function],
|
||||
"uniqueKeyPolicy": Object {},
|
||||
"usageSizeInKB": [Function],
|
||||
}
|
||||
}
|
||||
container={
|
||||
@@ -3986,8 +3959,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"partitionKeyVisible": [Function],
|
||||
"requestUnitsUsageCost": [Function],
|
||||
"ruToolTipText": [Function],
|
||||
"rupm": [Function],
|
||||
"rupmVisible": [Function],
|
||||
"sharedAutoPilotThroughput": [Function],
|
||||
"sharedThroughputRangeText": [Function],
|
||||
"shouldCreateMongoWildcardIndex": [Function],
|
||||
@@ -4475,8 +4446,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"partitionKeyVisible": [Function],
|
||||
"requestUnitsUsageCost": [Function],
|
||||
"ruToolTipText": [Function],
|
||||
"rupm": [Function],
|
||||
"rupmVisible": [Function],
|
||||
"sharedAutoPilotThroughput": [Function],
|
||||
"sharedThroughputRangeText": [Function],
|
||||
"shouldCreateMongoWildcardIndex": [Function],
|
||||
@@ -4588,7 +4557,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"visible": [Function],
|
||||
},
|
||||
"arcadiaToken": [Function],
|
||||
"armEndpoint": [Function],
|
||||
"browseQueriesPane": BrowseQueriesPane {
|
||||
"canSaveQueries": [Function],
|
||||
"container": [Circular],
|
||||
@@ -4800,7 +4768,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"hasWriteAccess": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isAuthWithResourceToken": [Function],
|
||||
"isCodeOfConductEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
@@ -4809,7 +4776,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isHostedDataExplorerEnabled": [Function],
|
||||
"isLeftPaneExpanded": [Function],
|
||||
"isLinkInjectionEnabled": [Function],
|
||||
"isMongoIndexEditorEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isNotificationConsoleExpanded": [Function],
|
||||
@@ -4824,8 +4790,8 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isRefreshingExplorer": [Function],
|
||||
"isResourceTokenCollectionNodeSelected": [Function],
|
||||
"isRightPanelV2Enabled": [Function],
|
||||
"isSchemaEnabled": [Function],
|
||||
"isServerlessEnabled": [Function],
|
||||
"isSettingsV2Enabled": [Function],
|
||||
"isSparkEnabled": [Function],
|
||||
"isSparkEnabledForAccount": [Function],
|
||||
"isSynapseLinkUpdating": [Function],
|
||||
@@ -4882,7 +4848,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"onSwitchToConnectionString": [Function],
|
||||
"onToggleKeyDown": [Function],
|
||||
"parentFrameDataExplorerVersion": [Function],
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
@@ -4908,7 +4873,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"titleLabel": "Select Columns",
|
||||
"visible": [Function],
|
||||
},
|
||||
"quotaId": [Function],
|
||||
"refreshDatabaseAccount": [Function],
|
||||
"refreshNotebookList": [Function],
|
||||
"refreshTreeTitle": [Function],
|
||||
|
||||
@@ -69,15 +69,15 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
|
||||
<b>
|
||||
¥
|
||||
1.29
|
||||
1.02
|
||||
hourly
|
||||
/
|
||||
¥
|
||||
31.06
|
||||
24.48
|
||||
daily
|
||||
/
|
||||
¥
|
||||
944.60
|
||||
744.60
|
||||
monthly
|
||||
|
||||
</b>
|
||||
@@ -227,7 +227,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
, Container:
|
||||
sampleCollection
|
||||
|
||||
, Current manual throughput: 1000 RU/s, Target manual throughput: 2000
|
||||
, Current manual throughput: 1000 RU/s
|
||||
</Text>
|
||||
<Text
|
||||
id="throughputApplyLongDelayMessage"
|
||||
|
||||
@@ -126,6 +126,12 @@
|
||||
</div>
|
||||
|
||||
<div data-bind="visible: !isAutoPilotSelected()">
|
||||
<p>
|
||||
<span
|
||||
>Estimate your required throughput with
|
||||
<a target="_blank" href="https://cosmos.azure.com/capacitycalculator/">capacity calculator</a></span
|
||||
>
|
||||
</p>
|
||||
<div data-bind="setTemplateReady: true">
|
||||
<input
|
||||
data-bind="
|
||||
|
||||
@@ -18,7 +18,7 @@ import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPa
|
||||
import { readCollection } from "../Common/dataAccess/readCollection";
|
||||
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
||||
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
|
||||
import EnvironmentUtility from "../Common/EnvironmentUtility";
|
||||
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
|
||||
import GraphStylingPane from "./Panes/GraphStylingPane";
|
||||
import hasher from "hasher";
|
||||
import NewVertexPane from "./Panes/NewVertexPane";
|
||||
@@ -87,6 +87,7 @@ import { updateUserContext, userContext } from "../UserContext";
|
||||
import { stringToBlob } from "../Utils/BlobUtils";
|
||||
import { IChoiceGroupProps } from "office-ui-fabric-react";
|
||||
import { getErrorMessage, handleError, getErrorStack } from "../Common/ErrorHandlingUtils";
|
||||
import { SubscriptionType } from "../Contracts/SubscriptionType";
|
||||
|
||||
BindingHandlersRegisterer.registerBindingHandlers();
|
||||
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
||||
@@ -119,8 +120,7 @@ export default class Explorer {
|
||||
|
||||
public databaseAccount: ko.Observable<DataModels.DatabaseAccount>;
|
||||
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults;
|
||||
public subscriptionType: ko.Observable<ViewModels.SubscriptionType>;
|
||||
public quotaId: ko.Observable<string>;
|
||||
public subscriptionType: ko.Observable<SubscriptionType>;
|
||||
public defaultExperience: ko.Observable<string>;
|
||||
public isPreferredApiDocumentDB: ko.Computed<boolean>;
|
||||
public isPreferredApiCassandra: ko.Computed<boolean>;
|
||||
@@ -133,13 +133,10 @@ export default class Explorer {
|
||||
public isAccountReady: ko.Observable<boolean>;
|
||||
public canSaveQueries: ko.Computed<boolean>;
|
||||
public features: ko.Observable<any>;
|
||||
public serverId: ko.Observable<string>;
|
||||
public armEndpoint: ko.Observable<string>;
|
||||
public isTryCosmosDBSubscription: ko.Observable<boolean>;
|
||||
public queriesClient: QueriesClient;
|
||||
public tableDataClient: TableDataClient;
|
||||
public splitter: Splitter;
|
||||
public parentFrameDataExplorerVersion: ko.Observable<string> = ko.observable<string>("");
|
||||
public mostRecentActivity: MostRecentActivity.MostRecentActivity;
|
||||
|
||||
// Notification Console
|
||||
@@ -203,10 +200,7 @@ export default class Explorer {
|
||||
|
||||
// features
|
||||
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
||||
public isCodeOfConductEnabled: ko.Computed<boolean>;
|
||||
public isLinkInjectionEnabled: ko.Computed<boolean>;
|
||||
public isSettingsV2Enabled: ko.Observable<boolean>;
|
||||
public isMongoIndexEditorEnabled: ko.Observable<boolean>;
|
||||
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
||||
public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
|
||||
@@ -225,6 +219,7 @@ export default class Explorer {
|
||||
public shareTokenCopyHelperText: ko.Observable<string>;
|
||||
public shouldShowDataAccessExpiryDialog: ko.Observable<boolean>;
|
||||
public shouldShowContextSwitchPrompt: ko.Observable<boolean>;
|
||||
public isSchemaEnabled: ko.Computed<boolean>;
|
||||
|
||||
// Notebooks
|
||||
public isNotebookEnabled: ko.Observable<boolean>;
|
||||
@@ -278,10 +273,7 @@ export default class Explorer {
|
||||
this.refreshTreeTitle = ko.observable<string>("Refresh collections");
|
||||
|
||||
this.databaseAccount = ko.observable<DataModels.DatabaseAccount>();
|
||||
this.subscriptionType = ko.observable<ViewModels.SubscriptionType>(
|
||||
SharedConstants.CollectionCreation.DefaultSubscriptionType
|
||||
);
|
||||
this.quotaId = ko.observable<string>("");
|
||||
this.subscriptionType = ko.observable<SubscriptionType>(SharedConstants.CollectionCreation.DefaultSubscriptionType);
|
||||
let firstInitialization = true;
|
||||
this.isRefreshingExplorer = ko.observable<boolean>(true);
|
||||
this.isRefreshingExplorer.subscribe((isRefreshing: boolean) => {
|
||||
@@ -321,9 +313,9 @@ export default class Explorer {
|
||||
if (isAccountReady) {
|
||||
this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(true);
|
||||
RouteHandler.getInstance().initHandler();
|
||||
this.notebookWorkspaceManager = new NotebookWorkspaceManager(this.armEndpoint());
|
||||
this.notebookWorkspaceManager = new NotebookWorkspaceManager();
|
||||
this.arcadiaWorkspaces = ko.observableArray();
|
||||
this._arcadiaManager = new ArcadiaResourceManager(this.armEndpoint());
|
||||
this._arcadiaManager = new ArcadiaResourceManager();
|
||||
this._isAfecFeatureRegistered(Constants.AfecFeatures.StorageAnalytics).then(isRegistered =>
|
||||
this.hasStorageAnalyticsAfecFeature(isRegistered)
|
||||
);
|
||||
@@ -372,8 +364,6 @@ export default class Explorer {
|
||||
this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>();
|
||||
|
||||
this.features = ko.observable();
|
||||
this.serverId = ko.observable<string>();
|
||||
this.armEndpoint = ko.observable<string>(undefined);
|
||||
this.queriesClient = new QueriesClient(this);
|
||||
this.isTryCosmosDBSubscription = ko.observable<boolean>(false);
|
||||
|
||||
@@ -406,14 +396,9 @@ export default class Explorer {
|
||||
this.isGalleryPublishEnabled = ko.computed<boolean>(() =>
|
||||
this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
|
||||
);
|
||||
this.isCodeOfConductEnabled = ko.computed<boolean>(() =>
|
||||
this.isFeatureEnabled(Constants.Features.enableCodeOfConduct)
|
||||
);
|
||||
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
|
||||
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
|
||||
);
|
||||
this.isSettingsV2Enabled = ko.observable(false);
|
||||
this.isMongoIndexEditorEnabled = ko.observable(false);
|
||||
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
|
||||
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||
@@ -422,6 +407,7 @@ export default class Explorer {
|
||||
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
|
||||
);
|
||||
|
||||
this.isSchemaEnabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSchema));
|
||||
this.isNotificationConsoleExpanded = ko.observable<boolean>(false);
|
||||
|
||||
this.databases = ko.observableArray<ViewModels.Database>();
|
||||
@@ -1019,9 +1005,7 @@ export default class Explorer {
|
||||
this.isSynapseLinkUpdating(true);
|
||||
this._closeSynapseLinkModalDialog();
|
||||
|
||||
const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate(
|
||||
this.databaseAccount().id
|
||||
);
|
||||
const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(this.databaseAccount().id);
|
||||
|
||||
try {
|
||||
const databaseAccount: DataModels.DatabaseAccount = await resourceProviderClient.patchAsync(
|
||||
@@ -1733,6 +1717,7 @@ export default class Explorer {
|
||||
case MessageTypes.SendNotification:
|
||||
case MessageTypes.ClearNotification:
|
||||
case MessageTypes.LoadingStatus:
|
||||
case MessageTypes.InitTestExplorer:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1760,61 +1745,59 @@ export default class Explorer {
|
||||
inputs.extensionEndpoint = configContext.PROXY_PATH;
|
||||
}
|
||||
|
||||
const initPromise: Q.Promise<void> = inputs ? this.initDataExplorerWithFrameInputs(inputs) : Q();
|
||||
this.initDataExplorerWithFrameInputs(inputs);
|
||||
|
||||
initPromise.then(() => {
|
||||
const openAction: ActionContracts.DataExplorerAction = message.openAction;
|
||||
if (!!openAction) {
|
||||
if (this.isRefreshingExplorer()) {
|
||||
const subscription = this.databases.subscribe((databases: ViewModels.Database[]) => {
|
||||
handleOpenAction(openAction, this.nonSystemDatabases(), this);
|
||||
subscription.dispose();
|
||||
});
|
||||
} else {
|
||||
const openAction: ActionContracts.DataExplorerAction = message.openAction;
|
||||
if (!!openAction) {
|
||||
if (this.isRefreshingExplorer()) {
|
||||
const subscription = this.databases.subscribe((databases: ViewModels.Database[]) => {
|
||||
handleOpenAction(openAction, this.nonSystemDatabases(), this);
|
||||
}
|
||||
subscription.dispose();
|
||||
});
|
||||
} else {
|
||||
handleOpenAction(openAction, this.nonSystemDatabases(), this);
|
||||
}
|
||||
if (message.actionType === ActionContracts.ActionType.TransmitCachedData) {
|
||||
handleCachedDataMessage(message);
|
||||
return;
|
||||
}
|
||||
if (message.type) {
|
||||
switch (message.type) {
|
||||
case MessageTypes.UpdateLocationHash:
|
||||
if (!message.locationHash) {
|
||||
break;
|
||||
}
|
||||
hasher.replaceHash(message.locationHash);
|
||||
RouteHandler.getInstance().parseHash(message.locationHash);
|
||||
break;
|
||||
case MessageTypes.SendNotification:
|
||||
if (!message.message) {
|
||||
break;
|
||||
}
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
message.consoleDataType || ConsoleDataType.Info,
|
||||
message.message,
|
||||
message.id
|
||||
);
|
||||
break;
|
||||
case MessageTypes.ClearNotification:
|
||||
if (!message.id) {
|
||||
break;
|
||||
}
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(message.id);
|
||||
break;
|
||||
case MessageTypes.LoadingStatus:
|
||||
if (!message.text) {
|
||||
break;
|
||||
}
|
||||
this._setLoadingStatusText(message.text, message.title);
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (message.actionType === ActionContracts.ActionType.TransmitCachedData) {
|
||||
handleCachedDataMessage(message);
|
||||
return;
|
||||
}
|
||||
if (message.type) {
|
||||
switch (message.type) {
|
||||
case MessageTypes.UpdateLocationHash:
|
||||
if (!message.locationHash) {
|
||||
break;
|
||||
}
|
||||
hasher.replaceHash(message.locationHash);
|
||||
RouteHandler.getInstance().parseHash(message.locationHash);
|
||||
break;
|
||||
case MessageTypes.SendNotification:
|
||||
if (!message.message) {
|
||||
break;
|
||||
}
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
message.consoleDataType || ConsoleDataType.Info,
|
||||
message.message,
|
||||
message.id
|
||||
);
|
||||
break;
|
||||
case MessageTypes.ClearNotification:
|
||||
if (!message.id) {
|
||||
break;
|
||||
}
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(message.id);
|
||||
break;
|
||||
case MessageTypes.LoadingStatus:
|
||||
if (!message.text) {
|
||||
break;
|
||||
}
|
||||
this._setLoadingStatusText(message.text, message.title);
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.splashScreenAdapter.forceRender();
|
||||
});
|
||||
this.splashScreenAdapter.forceRender();
|
||||
}
|
||||
|
||||
public findSelectedDatabase(): ViewModels.Database {
|
||||
@@ -1854,8 +1837,14 @@ export default class Explorer {
|
||||
return false;
|
||||
}
|
||||
|
||||
public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): Q.Promise<void> {
|
||||
public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): void {
|
||||
if (inputs != null) {
|
||||
// In development mode, save the iframe message from the portal in session storage.
|
||||
// This allows webpack hot reload to funciton properly
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs));
|
||||
}
|
||||
|
||||
const authorizationToken = inputs.authorizationToken || "";
|
||||
const masterKey = inputs.masterKey || "";
|
||||
const databaseAccount = inputs.databaseAccount || null;
|
||||
@@ -1863,26 +1852,19 @@ export default class Explorer {
|
||||
this.collectionCreationDefaults = inputs.defaultCollectionThroughput;
|
||||
}
|
||||
this.features(inputs.features);
|
||||
this.serverId(inputs.serverId);
|
||||
this.armEndpoint(EnvironmentUtility.normalizeArmEndpointUri(inputs.csmEndpoint || configContext.ARM_ENDPOINT));
|
||||
this.databaseAccount(databaseAccount);
|
||||
this.subscriptionType(inputs.subscriptionType);
|
||||
this.quotaId(inputs.quotaId);
|
||||
this.hasWriteAccess(inputs.hasWriteAccess);
|
||||
this.flight(inputs.addCollectionDefaultFlight);
|
||||
this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription);
|
||||
this.isAuthWithResourceToken(inputs.isAuthWithresourceToken);
|
||||
this.setFeatureFlagsFromFlights(inputs.flights);
|
||||
|
||||
if (!!inputs.dataExplorerVersion) {
|
||||
this.parentFrameDataExplorerVersion(inputs.dataExplorerVersion);
|
||||
}
|
||||
|
||||
this._importExplorerConfigComplete = true;
|
||||
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: inputs.extensionEndpoint || "",
|
||||
ARM_ENDPOINT: this.armEndpoint()
|
||||
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
|
||||
serverId: inputs.serverId
|
||||
});
|
||||
|
||||
updateUserContext({
|
||||
@@ -1890,7 +1872,9 @@ export default class Explorer {
|
||||
masterKey,
|
||||
databaseAccount,
|
||||
resourceGroup: inputs.resourceGroup,
|
||||
subscriptionId: inputs.subscriptionId
|
||||
subscriptionId: inputs.subscriptionId,
|
||||
subscriptionType: inputs.subscriptionType,
|
||||
quotaId: inputs.quotaId
|
||||
});
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.LoadDatabaseAccount,
|
||||
@@ -1904,21 +1888,12 @@ export default class Explorer {
|
||||
|
||||
this.isAccountReady(true);
|
||||
}
|
||||
return Q();
|
||||
}
|
||||
|
||||
public setFeatureFlagsFromFlights(flights: readonly string[]): void {
|
||||
if (!flights) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (flights.indexOf(Constants.Flights.SettingsV2) !== -1) {
|
||||
this.isSettingsV2Enabled(true);
|
||||
}
|
||||
|
||||
if (flights.indexOf(Constants.Flights.MongoIndexEditor) !== -1) {
|
||||
this.isMongoIndexEditorEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
public findSelectedCollection(): ViewModels.Collection {
|
||||
@@ -1976,9 +1951,9 @@ export default class Explorer {
|
||||
|
||||
public isRunningOnNationalCloud(): boolean {
|
||||
return (
|
||||
this.serverId() === Constants.ServerIds.blackforest ||
|
||||
this.serverId() === Constants.ServerIds.fairfax ||
|
||||
this.serverId() === Constants.ServerIds.mooncake
|
||||
userContext === Constants.ServerIds.blackforest ||
|
||||
userContext === Constants.ServerIds.fairfax ||
|
||||
userContext === Constants.ServerIds.mooncake
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2285,7 +2260,6 @@ export default class Explorer {
|
||||
name,
|
||||
content,
|
||||
parentDomElement,
|
||||
this.isCodeOfConductEnabled(),
|
||||
this.isLinkInjectionEnabled()
|
||||
);
|
||||
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
|
||||
@@ -2576,7 +2550,7 @@ export default class Explorer {
|
||||
|
||||
public _refreshSparkEnabledStateForAccount = async (): Promise<void> => {
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const armEndpoint = this.armEndpoint();
|
||||
const armEndpoint = configContext.ARM_ENDPOINT;
|
||||
const authType = window.authType as AuthType;
|
||||
if (!subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
|
||||
// explorer is not aware of the database account yet
|
||||
@@ -2585,7 +2559,7 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${Constants.AfecFeatures.Spark}`;
|
||||
const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate(featureUri);
|
||||
const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri);
|
||||
try {
|
||||
const sparkNotebooksFeature: DataModels.AfecFeature = await resourceProviderClient.getAsync(
|
||||
featureUri,
|
||||
@@ -2605,7 +2579,7 @@ export default class Explorer {
|
||||
|
||||
public _isAfecFeatureRegistered = async (featureName: string): Promise<boolean> => {
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const armEndpoint = this.armEndpoint();
|
||||
const armEndpoint = configContext.ARM_ENDPOINT;
|
||||
const authType = window.authType as AuthType;
|
||||
if (!featureName || !subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
|
||||
// explorer is not aware of the database account yet
|
||||
@@ -2613,7 +2587,7 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${featureName}`;
|
||||
const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate(featureUri);
|
||||
const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri);
|
||||
try {
|
||||
const featureStatus: DataModels.AfecFeature = await resourceProviderClient.getAsync(
|
||||
featureUri,
|
||||
|
||||
@@ -99,7 +99,22 @@
|
||||
.notificationConsoleControls {
|
||||
padding: @MediumSpace;
|
||||
margin-left:@DefaultSpace;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.ms-Dropdown-container {
|
||||
display: flex;
|
||||
.ms-Dropdown-title {
|
||||
height: 25px;
|
||||
line-height: 25px;
|
||||
}
|
||||
.ms-Dropdown {
|
||||
min-width: 110px;
|
||||
margin-left: 10px;
|
||||
height: 25px;
|
||||
line-height: 25px;
|
||||
}
|
||||
}
|
||||
#consoleFilterLabel {
|
||||
padding: 4px;
|
||||
}
|
||||
@@ -107,6 +122,7 @@
|
||||
.consoleSplitter {
|
||||
border-left: 1px solid @BaseMedium;
|
||||
margin: @MediumSpace;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.clearNotificationsButton {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import * as React from "react";
|
||||
import { ClientDefaults, KeyCodes } from "../../../Common/Constants";
|
||||
import AnimateHeight from "react-animate-height";
|
||||
|
||||
import { Dropdown, IDropdownOption } from "office-ui-fabric-react";
|
||||
import LoadingIcon from "../../../../images/loading.svg";
|
||||
import ErrorBlackIcon from "../../../../images/error_black.svg";
|
||||
import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg";
|
||||
@@ -53,7 +53,12 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
NotificationConsoleComponentState
|
||||
> {
|
||||
private static readonly transitionDurationMs = 200;
|
||||
private static readonly FilterOptions = ["All", "In Progress", "Info", "Error"];
|
||||
private static readonly FilterOptions = [
|
||||
{ key: "All", text: "All" },
|
||||
{ key: "In Progress", text: "In progress" },
|
||||
{ key: "Info", text: "Info" },
|
||||
{ key: "Error", text: "Error" }
|
||||
];
|
||||
private headerTimeoutId: number;
|
||||
private prevHeaderStatus: string;
|
||||
private consoleHeaderElement: HTMLElement;
|
||||
@@ -62,7 +67,7 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
super(props);
|
||||
this.state = {
|
||||
headerStatus: "",
|
||||
selectedFilter: NotificationConsoleComponent.FilterOptions[0],
|
||||
selectedFilter: NotificationConsoleComponent.FilterOptions[0].key || "",
|
||||
isExpanded: props.isConsoleExpanded
|
||||
};
|
||||
this.prevHeaderStatus = null;
|
||||
@@ -150,20 +155,15 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
>
|
||||
<div className="notificationConsoleContents">
|
||||
<div className="notificationConsoleControls">
|
||||
<label id="consoleFilterLabel">Filter</label>
|
||||
<select
|
||||
aria-labelledby="consoleFilterLabel"
|
||||
<Dropdown
|
||||
label="Filter:"
|
||||
role="combobox"
|
||||
aria-label={this.state.selectedFilter}
|
||||
value={this.state.selectedFilter}
|
||||
selectedKey={this.state.selectedFilter}
|
||||
options={NotificationConsoleComponent.FilterOptions}
|
||||
onChange={this.onFilterSelected.bind(this)}
|
||||
>
|
||||
{NotificationConsoleComponent.FilterOptions.map((value: string) => (
|
||||
<option value={value} key={value}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
aria-labelledby="consoleFilterLabel"
|
||||
aria-label={this.state.selectedFilter}
|
||||
/>
|
||||
<span className="consoleSplitter" />
|
||||
<span
|
||||
className="clearNotificationsButton"
|
||||
@@ -220,8 +220,8 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
));
|
||||
}
|
||||
|
||||
private onFilterSelected(event: React.ChangeEvent<HTMLSelectElement>): void {
|
||||
this.setState({ selectedFilter: event.target.value });
|
||||
private onFilterSelected(event: React.ChangeEvent<HTMLSelectElement>, option: IDropdownOption): void {
|
||||
this.setState({ selectedFilter: String(option.key) });
|
||||
}
|
||||
|
||||
private getFilteredConsoleData(): ConsoleData[] {
|
||||
|
||||
@@ -110,43 +110,34 @@ exports[`NotificationConsoleComponent renders the console (expanded) 1`] = `
|
||||
<div
|
||||
className="notificationConsoleControls"
|
||||
>
|
||||
<label
|
||||
id="consoleFilterLabel"
|
||||
>
|
||||
Filter
|
||||
</label>
|
||||
<select
|
||||
<StyledWithResponsiveMode
|
||||
aria-label="All"
|
||||
aria-labelledby="consoleFilterLabel"
|
||||
label="Filter:"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": "All",
|
||||
"text": "All",
|
||||
},
|
||||
Object {
|
||||
"key": "In Progress",
|
||||
"text": "In progress",
|
||||
},
|
||||
Object {
|
||||
"key": "Info",
|
||||
"text": "Info",
|
||||
},
|
||||
Object {
|
||||
"key": "Error",
|
||||
"text": "Error",
|
||||
},
|
||||
]
|
||||
}
|
||||
role="combobox"
|
||||
value="All"
|
||||
>
|
||||
<option
|
||||
key="All"
|
||||
value="All"
|
||||
>
|
||||
All
|
||||
</option>
|
||||
<option
|
||||
key="In Progress"
|
||||
value="In Progress"
|
||||
>
|
||||
In Progress
|
||||
</option>
|
||||
<option
|
||||
key="Info"
|
||||
value="Info"
|
||||
>
|
||||
Info
|
||||
</option>
|
||||
<option
|
||||
key="Error"
|
||||
value="Error"
|
||||
>
|
||||
Error
|
||||
</option>
|
||||
</select>
|
||||
selectedKey="All"
|
||||
/>
|
||||
<span
|
||||
className="consoleSplitter"
|
||||
/>
|
||||
|
||||
@@ -128,17 +128,9 @@ export default class NotebookManager {
|
||||
name: string,
|
||||
content: string | ImmutableNotebook,
|
||||
parentDomElement: HTMLElement,
|
||||
isCodeOfConductEnabled: boolean,
|
||||
isLinkInjectionEnabled: boolean
|
||||
): Promise<void> {
|
||||
await this.publishNotebookPaneAdapter.open(
|
||||
name,
|
||||
getFullName(),
|
||||
content,
|
||||
parentDomElement,
|
||||
isCodeOfConductEnabled,
|
||||
isLinkInjectionEnabled
|
||||
);
|
||||
await this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement, isLinkInjectionEnabled);
|
||||
}
|
||||
|
||||
public openCopyNotebookPane(name: string, content: string): void {
|
||||
|
||||
@@ -243,38 +243,6 @@
|
||||
</div>
|
||||
<!-- Unlimited Button Content - Start -->
|
||||
<div class="tabcontent" data-bind="visible: isUnlimitedStorageSelected() || databaseHasSharedOffer()">
|
||||
<div data-bind="visible: rupmVisible">
|
||||
<div class="tabs">
|
||||
<p>
|
||||
<span class="mandatoryStar">*</span>
|
||||
<span class="addCollectionLabel">RU/m</span>
|
||||
<span class="infoTooltip" role="tooltip" tabindex="0">
|
||||
<img class="infoImg" src="/info-bubble.svg" alt="More information">
|
||||
<span class="tooltiptext throughputRuInfo">
|
||||
For each 100 Request Units per second (RU/s) provisioned, 1,000 Request Units
|
||||
per
|
||||
minute
|
||||
(RU/m) can be provisioned. E.g.: for a container with 5,000 RU/s provisioned
|
||||
with
|
||||
RU/m
|
||||
enabled, the RU/m budget will be 50,000 RU/m.
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
<div tabindex="0" data-bind="event: { keydown: onRupmOptionsKeyDown }" aria-label="RU/m">
|
||||
<div class="tab">
|
||||
<input type="radio" id="rupmOn2" name="rupmcoll2" value="on" class="radio"
|
||||
data-bind="checked: rupm">
|
||||
<label for="rupmOn2">ON</label>
|
||||
</div>
|
||||
<div class="tab">
|
||||
<input type="radio" id="rupmOff2" name="rupmcoll2" value="off" class="radio"
|
||||
data-bind="checked: rupm">
|
||||
<label for="rupmOff2">OFF</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-bind="visible: partitionKeyVisible">
|
||||
<p>
|
||||
<span class="mandatoryStar">*</span>
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as ko from "knockout";
|
||||
import * as PricingUtils from "../../Utils/PricingUtils";
|
||||
import * as SharedConstants from "../../Shared/Constants";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { SubscriptionType } from "../../Contracts/SubscriptionType";
|
||||
import editable from "../../Common/EditableUtility";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
@@ -15,6 +16,7 @@ import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||
import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent";
|
||||
import { createCollection } from "../../Common/dataAccess/createCollection";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export interface AddCollectionPaneOptions extends ViewModels.PaneOptions {
|
||||
isPreferredApiTable: ko.Computed<boolean>;
|
||||
@@ -41,8 +43,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
public partitionKeyVisible: ko.Computed<boolean>;
|
||||
public partitionKeyPattern: ko.Computed<string>;
|
||||
public partitionKeyTitle: ko.Computed<string>;
|
||||
public rupm: ko.Observable<string>;
|
||||
public rupmVisible: ko.Observable<boolean>;
|
||||
public storage: ko.Observable<string>;
|
||||
public throughputSinglePartition: ViewModels.Editable<number>;
|
||||
public throughputMultiPartition: ViewModels.Editable<number>;
|
||||
@@ -142,12 +142,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
}
|
||||
return "";
|
||||
});
|
||||
this.rupm = ko.observable<string>(Constants.RUPMStates.off);
|
||||
this.rupmVisible = ko.observable<boolean>(false);
|
||||
const featureSubcription = this.container.features.subscribe(() => {
|
||||
this.rupmVisible(this.container.isFeatureEnabled(Constants.Features.enableRupm));
|
||||
featureSubcription.dispose();
|
||||
});
|
||||
|
||||
this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue());
|
||||
|
||||
@@ -192,7 +186,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
return "";
|
||||
}
|
||||
|
||||
const serverId: string = this.container.serverId();
|
||||
const serverId = configContext.serverId;
|
||||
const regions =
|
||||
(account &&
|
||||
account.properties &&
|
||||
@@ -200,33 +194,24 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
account.properties.readLocations.length) ||
|
||||
1;
|
||||
const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false;
|
||||
const rupmEnabled: boolean = this.rupm() === Constants.RUPMStates.on;
|
||||
|
||||
let throughputSpendAckText: string;
|
||||
let estimatedSpend: string;
|
||||
if (!this.isSharedAutoPilotSelected()) {
|
||||
throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString(
|
||||
offerThroughput,
|
||||
serverId,
|
||||
configContext.serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
rupmEnabled,
|
||||
this.isSharedAutoPilotSelected()
|
||||
);
|
||||
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
|
||||
offerThroughput,
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
rupmEnabled
|
||||
);
|
||||
estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster);
|
||||
} else {
|
||||
throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString(
|
||||
this.sharedAutoPilotThroughput(),
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
rupmEnabled,
|
||||
this.isSharedAutoPilotSelected()
|
||||
);
|
||||
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
|
||||
@@ -255,7 +240,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
return "";
|
||||
}
|
||||
|
||||
const serverId: string = this.container.serverId();
|
||||
const serverId: string = configContext.serverId;
|
||||
const regions =
|
||||
(account &&
|
||||
account.properties &&
|
||||
@@ -263,7 +248,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
account.properties.readLocations.length) ||
|
||||
1;
|
||||
const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false;
|
||||
const rupmEnabled: boolean = this.rupm() === Constants.RUPMStates.on;
|
||||
|
||||
let throughputSpendAckText: string;
|
||||
let estimatedSpend: string;
|
||||
@@ -273,15 +257,13 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
rupmEnabled,
|
||||
this.isAutoPilotSelected()
|
||||
);
|
||||
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
|
||||
this.throughputMultiPartition(),
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
rupmEnabled
|
||||
multimaster
|
||||
);
|
||||
} else {
|
||||
throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString(
|
||||
@@ -289,7 +271,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
rupmEnabled,
|
||||
this.isAutoPilotSelected()
|
||||
);
|
||||
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
|
||||
@@ -501,7 +482,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
});
|
||||
|
||||
this.upsellMessage = ko.pureComputed<string>(() => {
|
||||
return PricingUtils.getUpsellMessage(this.container.serverId(), this.isFreeTierAccount());
|
||||
return PricingUtils.getUpsellMessage(configContext.serverId, this.isFreeTierAccount());
|
||||
});
|
||||
|
||||
this.upsellMessageAriaLabel = ko.pureComputed<string>(() => {
|
||||
@@ -648,10 +629,8 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
}
|
||||
|
||||
public getSharedThroughputDefault(): boolean {
|
||||
const subscriptionType: ViewModels.SubscriptionType =
|
||||
this.container.subscriptionType && this.container.subscriptionType();
|
||||
|
||||
if (subscriptionType === ViewModels.SubscriptionType.EA || this.container.isServerlessEnabled()) {
|
||||
const subscriptionType = this.container.subscriptionType && this.container.subscriptionType();
|
||||
if (subscriptionType === SubscriptionType.EA || this.container.isServerlessEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -687,11 +666,10 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
storage: this.storage(),
|
||||
offerThroughput: this._getThroughput(),
|
||||
partitionKey: this.partitionKey(),
|
||||
databaseId: this.databaseId(),
|
||||
rupm: this.rupm()
|
||||
databaseId: this.databaseId()
|
||||
}),
|
||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
||||
throughput: this._getThroughput(),
|
||||
@@ -789,12 +767,11 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
id: this.collectionId(),
|
||||
storage: this.storage(),
|
||||
partitionKey,
|
||||
rupm: this.rupm(),
|
||||
uniqueKeyPolicy,
|
||||
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
||||
}),
|
||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
||||
throughput: offerThroughput,
|
||||
@@ -864,12 +841,11 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
id: this.collectionId(),
|
||||
storage: this.storage(),
|
||||
partitionKey,
|
||||
rupm: this.rupm(),
|
||||
uniqueKeyPolicy,
|
||||
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
||||
}),
|
||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
||||
throughput: offerThroughput,
|
||||
@@ -899,12 +875,11 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
id: this.collectionId(),
|
||||
storage: this.storage(),
|
||||
partitionKey,
|
||||
rupm: this.rupm(),
|
||||
uniqueKeyPolicy,
|
||||
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
||||
},
|
||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
||||
throughput: offerThroughput,
|
||||
@@ -982,20 +957,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
return true;
|
||||
}
|
||||
|
||||
public onRupmOptionsKeyDown(source: any, event: KeyboardEvent): boolean {
|
||||
if (event.key === "ArrowRight") {
|
||||
this.rupm("off");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowLeft") {
|
||||
this.rupm("on");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public onEnableSynapseLinkButtonClicked() {
|
||||
this.container.openEnableSynapseLinkDialog();
|
||||
}
|
||||
@@ -1019,16 +980,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
}
|
||||
|
||||
const throughput = this._getThroughput();
|
||||
const maxThroughputWithRUPM =
|
||||
SharedConstants.CollectionCreation.MaxRUPMPerPartition * this._calculateNumberOfPartitions();
|
||||
|
||||
if (this.rupm() === Constants.RUPMStates.on && throughput > maxThroughputWithRUPM) {
|
||||
this.formErrors(
|
||||
`The maximum supported provisioned throughput with RU/m enabled is ${maxThroughputWithRUPM} RU/s. Please turn off RU/m to incease thoughput above ${maxThroughputWithRUPM} RU/s.`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !this.throughputSpendAck()) {
|
||||
this.formErrors(`Please acknowledge the estimated daily spend.`);
|
||||
return false;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { SubscriptionType } from "../../Contracts/SubscriptionType";
|
||||
import Explorer from "../Explorer";
|
||||
import AddDatabasePane from "./AddDatabasePane";
|
||||
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||
@@ -44,31 +44,31 @@ describe("Add Database Pane", () => {
|
||||
});
|
||||
|
||||
it("should be true if subscription type is Benefits", () => {
|
||||
explorer.subscriptionType(ViewModels.SubscriptionType.Benefits);
|
||||
explorer.subscriptionType(SubscriptionType.Benefits);
|
||||
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
||||
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
|
||||
});
|
||||
|
||||
it("should be false if subscription type is EA", () => {
|
||||
explorer.subscriptionType(ViewModels.SubscriptionType.EA);
|
||||
explorer.subscriptionType(SubscriptionType.EA);
|
||||
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
||||
expect(addDatabasePane.getSharedThroughputDefault()).toBe(false);
|
||||
});
|
||||
|
||||
it("should be true if subscription type is Free", () => {
|
||||
explorer.subscriptionType(ViewModels.SubscriptionType.Free);
|
||||
explorer.subscriptionType(SubscriptionType.Free);
|
||||
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
||||
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
|
||||
});
|
||||
|
||||
it("should be true if subscription type is Internal", () => {
|
||||
explorer.subscriptionType(ViewModels.SubscriptionType.Internal);
|
||||
explorer.subscriptionType(SubscriptionType.Internal);
|
||||
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
||||
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
|
||||
});
|
||||
|
||||
it("should be true if subscription type is PAYG", () => {
|
||||
explorer.subscriptionType(ViewModels.SubscriptionType.PAYG);
|
||||
explorer.subscriptionType(SubscriptionType.PAYG);
|
||||
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
||||
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
|
||||
});
|
||||
|
||||
@@ -12,6 +12,8 @@ import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||
import { createDatabase } from "../../Common/dataAccess/createDatabase";
|
||||
import { configContext, Platform } from "../../ConfigContext";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import { SubscriptionType } from "../../Contracts/SubscriptionType";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export default class AddDatabasePane extends ContextualPaneBase {
|
||||
public defaultExperience: ko.Computed<string>;
|
||||
@@ -120,7 +122,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
return "";
|
||||
}
|
||||
|
||||
const serverId = this.container.serverId();
|
||||
const serverId = configContext.serverId;
|
||||
const regions =
|
||||
(account &&
|
||||
account.properties &&
|
||||
@@ -132,19 +134,12 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
let estimatedSpendAcknowledge: string;
|
||||
let estimatedSpend: string;
|
||||
if (!this.isAutoPilotSelected()) {
|
||||
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
|
||||
offerThroughput,
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
false /*rupmEnabled*/
|
||||
);
|
||||
estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster);
|
||||
estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
|
||||
offerThroughput,
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
false /*rupmEnabled*/,
|
||||
this.isAutoPilotSelected()
|
||||
);
|
||||
} else {
|
||||
@@ -159,7 +154,6 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
false /*rupmEnabled*/,
|
||||
this.isAutoPilotSelected()
|
||||
);
|
||||
}
|
||||
@@ -226,7 +220,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
});
|
||||
|
||||
this.upsellMessage = ko.pureComputed<string>(() => {
|
||||
return PricingUtils.getUpsellMessage(this.container.serverId(), this.isFreeTierAccount());
|
||||
return PricingUtils.getUpsellMessage(configContext.serverId, this.isFreeTierAccount());
|
||||
});
|
||||
|
||||
this.upsellMessageAriaLabel = ko.pureComputed<string>(() => {
|
||||
@@ -256,8 +250,8 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
const addDatabasePaneOpenMessage = {
|
||||
databaseAccountName: this.container.databaseAccount().name,
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
throughput: this.throughput(),
|
||||
flight: this.container.flight()
|
||||
@@ -284,8 +278,8 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
shared: this.databaseCreateNewShared()
|
||||
}),
|
||||
offerThroughput,
|
||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
flight: this.container.flight()
|
||||
},
|
||||
@@ -327,10 +321,9 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
}
|
||||
|
||||
public getSharedThroughputDefault(): boolean {
|
||||
const subscriptionType: ViewModels.SubscriptionType =
|
||||
this.container.subscriptionType && this.container.subscriptionType();
|
||||
const subscriptionType = this.container.subscriptionType && this.container.subscriptionType();
|
||||
|
||||
if (subscriptionType === ViewModels.SubscriptionType.EA || this.container.isServerlessEnabled()) {
|
||||
if (subscriptionType === SubscriptionType.EA || this.container.isServerlessEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -349,8 +342,8 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
shared: this.databaseCreateNewShared()
|
||||
}),
|
||||
offerThroughput: offerThroughput,
|
||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
flight: this.container.flight()
|
||||
},
|
||||
@@ -373,8 +366,8 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
shared: this.databaseCreateNewShared()
|
||||
}),
|
||||
offerThroughput: offerThroughput,
|
||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
flight: this.container.flight()
|
||||
},
|
||||
|
||||
@@ -14,6 +14,8 @@ import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||
import { HashMap } from "../../Common/HashMap";
|
||||
import { configContext, Platform } from "../../ConfigContext";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import { SubscriptionType } from "../../Contracts/SubscriptionType";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
public createTableQuery: ko.Observable<string>;
|
||||
@@ -125,7 +127,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
return "";
|
||||
}
|
||||
|
||||
const serverId = this.container.serverId();
|
||||
const serverId = configContext.serverId;
|
||||
const regions =
|
||||
(account &&
|
||||
account.properties &&
|
||||
@@ -137,19 +139,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
let estimatedSpend: string;
|
||||
let estimatedDedicatedSpendAcknowledge: string;
|
||||
if (!this.isAutoPilotSelected()) {
|
||||
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
|
||||
offerThroughput,
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
false /*rupmEnabled*/
|
||||
);
|
||||
estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster);
|
||||
estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
|
||||
offerThroughput,
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
false /*rupmEnabled*/,
|
||||
this.isAutoPilotSelected()
|
||||
);
|
||||
} else {
|
||||
@@ -164,7 +159,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
false /*rupmEnabled*/,
|
||||
this.isAutoPilotSelected()
|
||||
);
|
||||
}
|
||||
@@ -178,7 +172,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
return "";
|
||||
}
|
||||
|
||||
const serverId = this.container.serverId();
|
||||
const serverId = configContext.serverId;
|
||||
const regions =
|
||||
(account &&
|
||||
account.properties &&
|
||||
@@ -189,19 +183,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
let estimatedSpend: string;
|
||||
let estimatedSharedSpendAcknowledge: string;
|
||||
if (!this.isSharedAutoPilotSelected()) {
|
||||
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
|
||||
this.keyspaceThroughput(),
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
false /*rupmEnabled*/
|
||||
);
|
||||
estimatedSpend = PricingUtils.getEstimatedSpendHtml(this.keyspaceThroughput(), serverId, regions, multimaster);
|
||||
estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
|
||||
this.keyspaceThroughput(),
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
false /*rupmEnabled*/,
|
||||
this.isSharedAutoPilotSelected()
|
||||
);
|
||||
} else {
|
||||
@@ -216,7 +203,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
false /*rupmEnabled*/,
|
||||
this.isSharedAutoPilotSelected()
|
||||
);
|
||||
}
|
||||
@@ -311,11 +297,10 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
storage: Constants.BackendDefaults.multiPartitionStorageInGb,
|
||||
offerThroughput: this.throughput(),
|
||||
partitionKey: "",
|
||||
databaseId: this.keyspaceId(),
|
||||
rupm: false
|
||||
databaseId: this.keyspaceId()
|
||||
}),
|
||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
storage: "u",
|
||||
throughput: this.throughput(),
|
||||
@@ -365,12 +350,11 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
offerThroughput: this.throughput(),
|
||||
partitionKey: "",
|
||||
databaseId: this.keyspaceId(),
|
||||
rupm: false,
|
||||
hasDedicatedThroughput: this.dedicateTableThroughput()
|
||||
}),
|
||||
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
storage: "u",
|
||||
throughput: this.throughput(),
|
||||
@@ -412,12 +396,11 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
offerThroughput: this.throughput(),
|
||||
partitionKey: "",
|
||||
databaseId: this.keyspaceId(),
|
||||
rupm: false,
|
||||
hasDedicatedThroughput: this.dedicateTableThroughput()
|
||||
}),
|
||||
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
storage: "u",
|
||||
throughput: this.throughput(),
|
||||
@@ -443,12 +426,11 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
offerThroughput: this.throughput(),
|
||||
partitionKey: "",
|
||||
databaseId: this.keyspaceId(),
|
||||
rupm: false,
|
||||
hasDedicatedThroughput: this.dedicateTableThroughput()
|
||||
},
|
||||
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
||||
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
storage: "u",
|
||||
throughput: this.throughput(),
|
||||
|
||||
@@ -98,26 +98,21 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
||||
author: string,
|
||||
notebookContent: string | ImmutableNotebook,
|
||||
parentDomElement: HTMLElement,
|
||||
isCodeOfConductEnabled: boolean,
|
||||
isLinkInjectionEnabled: boolean
|
||||
): Promise<void> {
|
||||
if (isCodeOfConductEnabled) {
|
||||
try {
|
||||
const response = await this.junoClient.isCodeOfConductAccepted();
|
||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
|
||||
}
|
||||
|
||||
this.isCodeOfConductAccepted = response.data;
|
||||
} catch (error) {
|
||||
handleError(
|
||||
error,
|
||||
"PublishNotebookPaneAdapter/isCodeOfConductAccepted",
|
||||
"Failed to check if code of conduct was accepted"
|
||||
);
|
||||
try {
|
||||
const response = await this.junoClient.isCodeOfConductAccepted();
|
||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
|
||||
}
|
||||
} else {
|
||||
this.isCodeOfConductAccepted = true;
|
||||
|
||||
this.isCodeOfConductAccepted = response.data;
|
||||
} catch (error) {
|
||||
handleError(
|
||||
error,
|
||||
"PublishNotebookPaneAdapter/isCodeOfConductAccepted",
|
||||
"Failed to check if code of conduct was accepted"
|
||||
);
|
||||
}
|
||||
|
||||
this.name = name;
|
||||
|
||||
@@ -50,13 +50,24 @@
|
||||
id="fileImportLinkNotebook"
|
||||
data-bind="event: { click: onImportLinkClick, keypress: onImportLinkKeyPress }"
|
||||
>
|
||||
<img class="fileImportImg" src="/folder_16x16.svg" alt="upload files" title="Upload files" />
|
||||
<img
|
||||
id="importFileButton"
|
||||
class="fileImportImg"
|
||||
src="/folder_16x16.svg"
|
||||
alt="upload files"
|
||||
title="Upload files"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paneFooter">
|
||||
<div class="leftpanel-okbut">
|
||||
<input type="submit" data-bind="attr: { value: submitButtonLabel }" class="btncreatecoll1" />
|
||||
<input
|
||||
id="uploadFileButton"
|
||||
type="submit"
|
||||
data-bind="attr: { value: submitButtonLabel }"
|
||||
class="btncreatecoll1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Upload File inputs - End -->
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
padding: 32px 16px;
|
||||
display: flex;
|
||||
background-color: @BaseLight;
|
||||
border: 1px solid #E5E5E5;
|
||||
border: 1px solid #949494;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -16,8 +16,6 @@ import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import Explorer from "../Explorer";
|
||||
import { updateOffer } from "../../Common/dataAccess/updateOffer";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { updateOfferThroughputBeyondLimit } from "../../Common/dataAccess/updateOfferThroughputBeyondLimit";
|
||||
import { configContext, Platform } from "../../ConfigContext";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
|
||||
@@ -35,11 +33,6 @@ const currentThroughput: (isAutoscale: boolean, throughput: number) => string =
|
||||
? `Current autoscale throughput: ${Math.round(throughput / 10)} - ${throughput} RU/s`
|
||||
: `Current manual throughput: ${throughput} RU/s`;
|
||||
|
||||
const throughputApplyDelayedMessage = (isAutoscale: boolean, throughput: number, databaseName: string) =>
|
||||
`The request to increase the throughput has successfully been submitted.
|
||||
This operation will take 1-3 business days to complete. View the latest status in Notifications.<br />
|
||||
Database: ${databaseName}, ${currentThroughput(isAutoscale, throughput)}`;
|
||||
|
||||
const throughputApplyShortDelayMessage = (isAutoscale: boolean, throughput: number, databaseName: string) =>
|
||||
`A request to increase the throughput is currently in progress.
|
||||
This operation will take some time to complete.<br />
|
||||
@@ -66,8 +59,8 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
public displayedError: ko.Observable<string>;
|
||||
public isTemplateReady: ko.Observable<boolean>;
|
||||
public minRUAnotationVisible: ko.Computed<boolean>;
|
||||
public minRUs: ko.Computed<number>;
|
||||
public maxRUs: ko.Computed<number>;
|
||||
public minRUs: ko.Observable<number>;
|
||||
public maxRUs: ko.Observable<number>;
|
||||
public maxRUsText: ko.PureComputed<string>;
|
||||
public maxRUThroughputInputLimit: ko.Computed<number>;
|
||||
public notificationStatusInfo: ko.Observable<string>;
|
||||
@@ -92,7 +85,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
|
||||
private _hasProvisioningTypeChanged: ko.Computed<boolean>;
|
||||
private _wasAutopilotOriginallySet: ko.Observable<boolean>;
|
||||
private _offerReplacePending: ko.Computed<boolean>;
|
||||
private _offerReplacePending: ko.Observable<boolean>;
|
||||
private container: Explorer;
|
||||
|
||||
constructor(options: ViewModels.TabOptions) {
|
||||
@@ -111,15 +104,14 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
this._wasAutopilotOriginallySet = ko.observable(false);
|
||||
this.isAutoPilotSelected = editable.observable(false);
|
||||
this.autoPilotThroughput = editable.observable<number>();
|
||||
const offer = this.database && this.database.offer && this.database.offer();
|
||||
const offerAutopilotSettings = offer && offer.content && offer.content.offerAutopilotSettings;
|
||||
this.userCanChangeProvisioningTypes = ko.observable(true);
|
||||
|
||||
if (offerAutopilotSettings && offerAutopilotSettings.maxThroughput) {
|
||||
if (AutoPilotUtils.isValidAutoPilotThroughput(offerAutopilotSettings.maxThroughput)) {
|
||||
const autoscaleMaxThroughput = this.database?.offer()?.autoscaleMaxThroughput;
|
||||
if (autoscaleMaxThroughput) {
|
||||
if (AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
|
||||
this._wasAutopilotOriginallySet(true);
|
||||
this.isAutoPilotSelected(true);
|
||||
this.autoPilotThroughput(offerAutopilotSettings.maxThroughput);
|
||||
this.autoPilotThroughput(autoscaleMaxThroughput);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +139,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
return "";
|
||||
}
|
||||
|
||||
const serverId = this.container.serverId();
|
||||
const serverId = configContext.serverId;
|
||||
const regions =
|
||||
(account &&
|
||||
account.properties &&
|
||||
@@ -163,8 +155,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
this.overrideWithAutoPilotSettings() ? this.autoPilotThroughput() : this.throughput(),
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
false /*rupmEnabled*/
|
||||
multimaster
|
||||
);
|
||||
} else {
|
||||
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
|
||||
@@ -205,45 +196,15 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
return this._hasProvisioningTypeChanged() && !this._wasAutopilotOriginallySet();
|
||||
});
|
||||
|
||||
this.minRUs = ko.computed<number>(() => {
|
||||
const offerContent =
|
||||
this.database && this.database.offer && this.database.offer() && this.database.offer().content;
|
||||
|
||||
// TODO: backend is returning 1,000,000 as min throughput which seems wrong
|
||||
// Setting to min throughput to not block and let the backend pass or fail
|
||||
if (offerContent && offerContent.offerAutopilotSettings) {
|
||||
return 400;
|
||||
}
|
||||
|
||||
const collectionThroughputInfo: DataModels.OfferThroughputInfo =
|
||||
offerContent && offerContent.collectionThroughputInfo;
|
||||
|
||||
if (collectionThroughputInfo && !!collectionThroughputInfo.minimumRUForCollection) {
|
||||
return collectionThroughputInfo.minimumRUForCollection;
|
||||
}
|
||||
const throughputDefaults = this.container.collectionCreationDefaults.throughput;
|
||||
return throughputDefaults.unlimitedmin;
|
||||
});
|
||||
this.minRUs = ko.observable<number>(
|
||||
this.database.offer()?.minimumThroughput || this.container.collectionCreationDefaults.throughput.unlimitedmin
|
||||
);
|
||||
|
||||
this.minRUAnotationVisible = ko.computed<boolean>(() => {
|
||||
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.maxRUs = ko.observable<number>(this.container.collectionCreationDefaults.throughput.unlimitedmax);
|
||||
|
||||
this.maxRUThroughputInputLimit = ko.pureComputed<number>(() => {
|
||||
if (configContext.platform === Platform.Hosted) {
|
||||
@@ -269,37 +230,21 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
return this.throughputTitle() + this.requestUnitsUsageCost();
|
||||
});
|
||||
this.pendingNotification = ko.observable<DataModels.Notification>();
|
||||
this._offerReplacePending = ko.pureComputed<boolean>(() => {
|
||||
const offer = this.database && this.database.offer && this.database.offer();
|
||||
return (
|
||||
offer &&
|
||||
offer.hasOwnProperty("headers") &&
|
||||
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
||||
);
|
||||
});
|
||||
this._offerReplacePending = ko.observable<boolean>(!!this.database.offer()?.offerReplacePending);
|
||||
this.notificationStatusInfo = ko.observable<string>("");
|
||||
this.shouldShowNotificationStatusPrompt = ko.computed<boolean>(() => this.notificationStatusInfo().length > 0);
|
||||
this.warningMessage = ko.computed<string>(() => {
|
||||
const offer = this.database && this.database.offer && this.database.offer();
|
||||
|
||||
if (this.overrideWithProvisionedThroughputSettings()) {
|
||||
return AutoPilotUtils.manualToAutoscaleDisclaimer;
|
||||
}
|
||||
|
||||
if (
|
||||
offer &&
|
||||
offer.hasOwnProperty("headers") &&
|
||||
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
||||
) {
|
||||
const throughput = offer.content.offerAutopilotSettings
|
||||
? offer.content.offerAutopilotSettings.maxThroughput
|
||||
: offer.content.offerThroughput;
|
||||
|
||||
const offer = this.database.offer();
|
||||
if (offer?.offerReplacePending) {
|
||||
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
|
||||
return throughputApplyShortDelayMessage(this.isAutoPilotSelected(), throughput, this.database.id());
|
||||
}
|
||||
|
||||
if (
|
||||
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
this.canThroughputExceedMaximumValue()
|
||||
) {
|
||||
@@ -432,60 +377,26 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
const headerOptions: RequestOptions = { initialHeaders: {} };
|
||||
|
||||
try {
|
||||
if (this.isAutoPilotSelected()) {
|
||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||
databaseId: this.database.id(),
|
||||
currentOffer: this.database.offer(),
|
||||
autopilotThroughput: this.autoPilotThroughput(),
|
||||
manualThroughput: undefined,
|
||||
migrateToAutoPilot: this._hasProvisioningTypeChanged()
|
||||
};
|
||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||
databaseId: this.database.id(),
|
||||
currentOffer: this.database.offer(),
|
||||
autopilotThroughput: this.isAutoPilotSelected() ? this.autoPilotThroughput() : undefined,
|
||||
manualThroughput: this.isAutoPilotSelected() ? undefined : this.throughput()
|
||||
};
|
||||
|
||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||
this.database.offer(updatedOffer);
|
||||
this.database.offer.valueHasMutated();
|
||||
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
||||
} else {
|
||||
if (this.throughput.editableIsDirty() || this.isAutoPilotSelected.editableIsDirty()) {
|
||||
const originalThroughputValue = this.throughput.getEditableOriginalValue();
|
||||
const newThroughput = this.throughput();
|
||||
|
||||
if (
|
||||
this.canThroughputExceedMaximumValue() &&
|
||||
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million
|
||||
) {
|
||||
const requestPayload = {
|
||||
subscriptionId: userContext.subscriptionId,
|
||||
databaseAccountName: userContext.databaseAccount.name,
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
databaseName: this.database.id(),
|
||||
throughput: newThroughput,
|
||||
offerIsRUPerMinuteThroughputEnabled: false
|
||||
};
|
||||
await updateOfferThroughputBeyondLimit(requestPayload);
|
||||
this.database.offer().content.offerThroughput = originalThroughputValue;
|
||||
this.throughput(originalThroughputValue);
|
||||
this.notificationStatusInfo(
|
||||
throughputApplyDelayedMessage(this.isAutoPilotSelected(), newThroughput, this.database.id())
|
||||
);
|
||||
this.throughput.valueHasMutated(); // force component re-render
|
||||
} else {
|
||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||
databaseId: this.database.id(),
|
||||
currentOffer: this.database.offer(),
|
||||
autopilotThroughput: undefined,
|
||||
manualThroughput: newThroughput,
|
||||
migrateToManual: this._hasProvisioningTypeChanged()
|
||||
};
|
||||
|
||||
const updatedOffer = await updateOffer(updateOfferParams);
|
||||
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
||||
this.database.offer(updatedOffer);
|
||||
this.database.offer.valueHasMutated();
|
||||
}
|
||||
if (this._hasProvisioningTypeChanged()) {
|
||||
if (this.isAutoPilotSelected()) {
|
||||
updateOfferParams.migrateToAutoPilot = true;
|
||||
} else {
|
||||
updateOfferParams.migrateToManual = true;
|
||||
}
|
||||
}
|
||||
|
||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||
this.database.offer(updatedOffer);
|
||||
this.database.offer.valueHasMutated();
|
||||
this._setBaseline();
|
||||
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
||||
} catch (error) {
|
||||
this.container.isRefreshingExplorer(false);
|
||||
this.isExecutionError(true);
|
||||
@@ -527,15 +438,10 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
|
||||
private _setBaseline() {
|
||||
const offer = this.database && this.database.offer && this.database.offer();
|
||||
const offerThroughput = offer.content && offer.content.offerThroughput;
|
||||
const offerAutopilotSettings = offer && offer.content && offer.content.offerAutopilotSettings;
|
||||
|
||||
this.throughput.setBaseline(offerThroughput);
|
||||
this.isAutoPilotSelected.setBaseline(AutoPilotUtils.isValidAutoPilotThroughput(offer.autoscaleMaxThroughput));
|
||||
this.autoPilotThroughput.setBaseline(offer.autoscaleMaxThroughput);
|
||||
this.throughput.setBaseline(offer.manualThroughput);
|
||||
this.userCanChangeProvisioningTypes(true);
|
||||
|
||||
const maxThroughputForAutoPilot = offerAutopilotSettings && offerAutopilotSettings.maxThroughput;
|
||||
this.isAutoPilotSelected.setBaseline(AutoPilotUtils.isValidAutoPilotThroughput(maxThroughputForAutoPilot));
|
||||
this.autoPilotThroughput.setBaseline(maxThroughputForAutoPilot || AutoPilotUtils.minAutoPilotThroughput);
|
||||
}
|
||||
|
||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||
|
||||
@@ -33,7 +33,7 @@ export default class MongoShellTab extends TabsBase {
|
||||
this._runtimeEndpoint = configContext.platform === Platform.Hosted ? configContext.BACKEND_ENDPOINT : "";
|
||||
const extensionEndpoint: string = configContext.BACKEND_ENDPOINT || this._runtimeEndpoint || "";
|
||||
let baseUrl = "/content/mongoshell/dist/";
|
||||
if (this._container.serverId() === "localhost") {
|
||||
if (configContext.serverId === "localhost") {
|
||||
baseUrl = "/content/mongoshell/";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,723 +0,0 @@
|
||||
<div
|
||||
class="tab-pane flexContainer"
|
||||
data-bind="
|
||||
attr:{
|
||||
id: tabId
|
||||
},
|
||||
visible: isActive"
|
||||
role="tabpanel"
|
||||
>
|
||||
<div class="warningErrorContainer scaleWarningContainer" data-bind="visible: shouldShowStatusBar">
|
||||
<div>
|
||||
<div class="warningErrorContent" data-bind="visible: shouldShowNotificationStatusPrompt">
|
||||
<span><img src="/info_color.svg" alt="Info"/></span>
|
||||
<span class="warningErrorDetailsLinkContainer" data-bind="html: notificationStatusInfo"></span>
|
||||
</div>
|
||||
<div class="warningErrorContent" data-bind="visible: !shouldShowNotificationStatusPrompt()">
|
||||
<span><img src="/warning.svg" alt="Warning"/></span>
|
||||
<span class="warningErrorDetailsLinkContainer" data-bind="html: warningMessage"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tabForm scaleSettingScrollable">
|
||||
<!-- ko if: shouldShowKeyspaceSharedThroughputMessage -->
|
||||
<div>This table shared throughput is configured at the keyspace</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko ifnot: hasDatabaseSharedThroughput -->
|
||||
<div>
|
||||
<div
|
||||
class="scaleDivison"
|
||||
data-bind="click:toggleScale, event: { keypress: onScaleKeyPress }, attr:{ 'aria-expanded': scaleExpanded() ? 'true' : 'false' }"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Scale"
|
||||
aria-controls="scaleRegion"
|
||||
>
|
||||
<span class="themed-images" type="text/html" id="ExpandChevronRightScale" data-bind="visible: !scaleExpanded()">
|
||||
<img
|
||||
class="imgiconwidth ssExpandCollapseIcon ssCollapseIcon "
|
||||
src="/Triangle-right.svg"
|
||||
alt="Show scale properties"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span class="themed-images" type="text/html" id="ExpandChevronDownScale" data-bind="visible: scaleExpanded">
|
||||
<img class="imgiconwidth ssExpandCollapseIcon " src="/Triangle-down.svg" alt="Hide scale properties" />
|
||||
</span>
|
||||
|
||||
<span class="scaleSettingTitle">Scale</span>
|
||||
</div>
|
||||
|
||||
<div class="ssTextAllignment" data-bind="visible: scaleExpanded" id="scaleRegion">
|
||||
<!-- ko ifnot: isAutoScaleEnabled -->
|
||||
<throughput-input-autopilot-v3
|
||||
params="{
|
||||
testId: testId,
|
||||
class: 'scaleForm dirty',
|
||||
value: throughput,
|
||||
minimum: minRUs,
|
||||
maximum: maxRUThroughputInputLimit,
|
||||
isEnabled: !hasDatabaseSharedThroughput(),
|
||||
canExceedMaximumValue: canThroughputExceedMaximumValue,
|
||||
label: throughputTitle,
|
||||
ariaLabel: throughputAriaLabel,
|
||||
costsVisible: costsVisible,
|
||||
requestUnitsUsageCost: requestUnitsUsageCost,
|
||||
throughputAutoPilotRadioId: throughputAutoPilotRadioId,
|
||||
throughputProvisionedRadioId: throughputProvisionedRadioId,
|
||||
throughputModeRadioName: throughputModeRadioName,
|
||||
showAutoPilot: userCanChangeProvisioningTypes,
|
||||
isAutoPilotSelected: isAutoPilotSelected,
|
||||
maxAutoPilotThroughputSet: autoPilotThroughput,
|
||||
autoPilotUsageCost: autoPilotUsageCost,
|
||||
overrideWithAutoPilotSettings: overrideWithAutoPilotSettings,
|
||||
overrideWithProvisionedThroughputSettings: overrideWithProvisionedThroughputSettings
|
||||
}"
|
||||
>
|
||||
</throughput-input-autopilot-v3>
|
||||
|
||||
<div class="storageCapacityTitle throughputStorageValue" data-bind="html: storageCapacityTitle"></div>
|
||||
<!-- /ko -->
|
||||
|
||||
<div data-bind="visible: rupmVisible">
|
||||
<div class="formTitle">RU/m</div>
|
||||
<div class="tabs" aria-label="RU/m">
|
||||
<div class="tab">
|
||||
<label
|
||||
data-bind="
|
||||
attr:{
|
||||
for: rupmOnId
|
||||
},
|
||||
css: {
|
||||
dirty: rupm.editableIsDirty,
|
||||
selectedRadio: rupm() === 'on',
|
||||
unselectedRadio: rupm() !== 'on'
|
||||
}"
|
||||
>On</label
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="rupm"
|
||||
value="on"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
id: rupmOnId
|
||||
},
|
||||
checked: rupm"
|
||||
/>
|
||||
</div>
|
||||
<div class="tab">
|
||||
<label
|
||||
data-bind="
|
||||
attr:{
|
||||
for: rupmOffId
|
||||
},
|
||||
css: {
|
||||
dirty: rupm.editableIsDirty,
|
||||
selectedRadio: rupm() === 'off',
|
||||
unselectedRadio: rupm() !== 'off'
|
||||
}"
|
||||
>Off</label
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="rupm"
|
||||
value="off"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
id: rupmOffId
|
||||
},
|
||||
checked: rupm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TODO: Replace link with call to the Azure Support blade -->
|
||||
<div data-bind="visible: isAutoScaleEnabled">
|
||||
<div class="autoScaleThroughputTitle">Throughput (RU/s)</div>
|
||||
<input
|
||||
class="formReadOnly collid-white"
|
||||
readonly
|
||||
aria-label="Throughput input"
|
||||
data-bind="textInput: throughput"
|
||||
/>
|
||||
<div class="autoScaleDescription">
|
||||
Your account has custom settings that prevents setting throughput at the container level. Please work with
|
||||
your Cosmos DB engineering team point of contact to make changes.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<div data-bind="visible: hasConflictResolution">
|
||||
<div
|
||||
class="formTitle"
|
||||
data-bind="click:toggleConflictResolution, event: { keypress: onConflictResolutionKeyPress }, attr:{ 'aria-expanded': conflictResolutionExpanded() ? 'true' : 'false' }"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Conflict Resolution"
|
||||
aria-controls="conflictResolutionRegion"
|
||||
>
|
||||
<span
|
||||
class="themed-images"
|
||||
type="text/html"
|
||||
id="ExpandChevronRightConflictResolution"
|
||||
data-bind="visible: !conflictResolutionExpanded()"
|
||||
>
|
||||
<img
|
||||
class="imgiconwidth ssExpandCollapseIcon ssCollapseIcon"
|
||||
src="/Triangle-right.svg"
|
||||
alt="Show conflict resolution"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="themed-images"
|
||||
type="text/html"
|
||||
id="ExpandChevronDownConflictResolution"
|
||||
data-bind="visible: conflictResolutionExpanded"
|
||||
>
|
||||
<img class="imgiconwidth ssExpandCollapseIcon" src="/Triangle-down.svg" alt="Show conflict resolution" />
|
||||
</span>
|
||||
<span class="scaleSettingTitle">Conflict resolution</span>
|
||||
</div>
|
||||
<div id="conflictResolutionRegion" class="ssTextAllignment" data-bind="visible: conflictResolutionExpanded">
|
||||
<div class="formTitle">Mode</div>
|
||||
<div class="tabs" aria-label="Mode" role="radiogroup">
|
||||
<div class="tab">
|
||||
<label
|
||||
tabindex="0"
|
||||
role="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
for: conflictResolutionPolicyModeLWW,
|
||||
'aria-checked': conflictResolutionPolicyMode() !== 'Custom' ? 'true' : 'false'
|
||||
},
|
||||
css: {
|
||||
dirty: conflictResolutionPolicyMode.editableIsDirty,
|
||||
selectedRadio: conflictResolutionPolicyMode() === 'LastWriterWins',
|
||||
unselectedRadio: conflictResolutionPolicyMode() !== 'LastWriterWins'
|
||||
},
|
||||
event: {
|
||||
keypress: onConflictResolutionLWWKeyPress
|
||||
}"
|
||||
>Last Write Wins (default)</label
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="conflictresolution"
|
||||
value="LastWriterWins"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
id: conflictResolutionPolicyModeLWW
|
||||
},
|
||||
checked: conflictResolutionPolicyMode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="tab">
|
||||
<label
|
||||
tabindex="0"
|
||||
role="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
for: conflictResolutionPolicyModeCustom,
|
||||
'aria-checked': conflictResolutionPolicyMode() === 'Custom' ? 'true' : 'false'
|
||||
},
|
||||
css: {
|
||||
dirty: conflictResolutionPolicyMode.editableIsDirty,
|
||||
selectedRadio: conflictResolutionPolicyMode() === 'Custom',
|
||||
unselectedRadio: conflictResolutionPolicyMode() !== 'Custom'
|
||||
},
|
||||
event: {
|
||||
keypress: onConflictResolutionCustomKeyPress
|
||||
}"
|
||||
>Merge Procedure (custom)</label
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="conflictresolution"
|
||||
value="Custom"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
id: conflictResolutionPolicyModeCustom
|
||||
},
|
||||
checked: conflictResolutionPolicyMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div data-bind="visible: conflictResolutionPolicyMode() === 'LastWriterWins'">
|
||||
<p class="formTitle">
|
||||
Conflict Resolver Property
|
||||
<span class="infoTooltip" role="tooltip" tabindex="0">
|
||||
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
|
||||
<span class="tooltiptext infoTooltipWidth"
|
||||
>Gets or sets the name of a integer property in your documents which is used for the Last Write Wins
|
||||
(LWW) based conflict resolution scheme. By default, the system uses the system defined timestamp
|
||||
property, _ts to decide the winner for the conflicting versions of the document. Specify your own
|
||||
integer property if you want to override the default timestamp based conflict resolution.</span
|
||||
>
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<input
|
||||
type="text"
|
||||
aria-label="Document path for conflict resolution"
|
||||
data-bind="
|
||||
css: {
|
||||
dirty: conflictResolutionPolicyPath.editableIsDirty
|
||||
},
|
||||
textInput: conflictResolutionPolicyPath,
|
||||
enable: conflictResolutionPolicyMode() === 'LastWriterWins'"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div data-bind="visible: conflictResolutionPolicyMode() === 'Custom'">
|
||||
<p class="formTitle">
|
||||
Stored procedure
|
||||
<span class="infoTooltip" role="tooltip" tabindex="0">
|
||||
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
|
||||
<span class="tooltiptext infoTooltipWidth"
|
||||
>Gets or sets the name of a stored procedure (aka merge procedure) for resolving the conflicts. You can
|
||||
write application defined logic to determine the winner of the conflicting versions of a document. The
|
||||
stored procedure will get executed transactionally, exactly once, on the server side. If you do not
|
||||
provide a stored procedure, the conflicts will be populated in the
|
||||
<a class="linkDarkBackground" href="https://aka.ms/dataexplorerconflics" target="_blank"
|
||||
>conflicts feed</a
|
||||
>. You can update/re-register the stored procedure at any time.</span
|
||||
>
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<input
|
||||
type="text"
|
||||
aria-label="Stored procedure name for conflict resolution"
|
||||
data-bind="
|
||||
css: {
|
||||
dirty: conflictResolutionPolicyProcedure.editableIsDirty
|
||||
},
|
||||
textInput: conflictResolutionPolicyProcedure,
|
||||
enable: conflictResolutionPolicyMode() === 'Custom'"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="formTitle"
|
||||
data-bind="click:toggleSettings, event: { keypress: onSettingsKeyPress }, attr:{ 'aria-expanded': settingsExpanded() ? 'true' : 'false' }, visible: shouldShowIndexingPolicyEditor() || ttlVisible()"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Settings"
|
||||
aria-controls="settingsRegion"
|
||||
>
|
||||
<span
|
||||
class="themed-images"
|
||||
type="text/html"
|
||||
id="ExpandChevronRightSettings"
|
||||
data-bind="visible: !settingsExpanded() && !hasDatabaseSharedThroughput()"
|
||||
>
|
||||
<img class="imgiconwidth ssExpandCollapseIcon ssCollapseIcon" src="/Triangle-right.svg" alt="Show settings" />
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="themed-images"
|
||||
type="text/html"
|
||||
id="ExpandChevronDownSettings"
|
||||
data-bind="visible: settingsExpanded() && !hasDatabaseSharedThroughput()"
|
||||
>
|
||||
<img class="imgiconwidth ssExpandCollapseIcon" src="/Triangle-down.svg" alt="Show settings" />
|
||||
</span>
|
||||
<span class="scaleSettingTitle">Settings</span>
|
||||
</div>
|
||||
<div class="ssTextAllignment" data-bind="visible: settingsExpanded" id="settingsRegion">
|
||||
<div data-bind="visible: ttlVisible">
|
||||
<div class="formTitle">Time to Live</div>
|
||||
<div class="tabs disableFocusDefaults" aria-label="Time to Live" role="radiogroup">
|
||||
<div class="tab">
|
||||
<label
|
||||
class="ttlIndexingPolicyFocusElement"
|
||||
tabindex="0"
|
||||
role="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
for: ttlOffId,
|
||||
'aria-checked': timeToLive() === 'off' ? 'true' : 'false'
|
||||
},
|
||||
css: {
|
||||
dirty: timeToLive.editableIsDirty,
|
||||
selectedRadio: timeToLive() === 'off',
|
||||
unselectedRadio: timeToLive() !== 'off'
|
||||
},
|
||||
event: {
|
||||
keypress: onTtlOffKeyPress
|
||||
},
|
||||
hasFocus: ttlOffFocused"
|
||||
>Off</label
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="ttl"
|
||||
value="off"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
id: ttlOffId
|
||||
},
|
||||
checked: timeToLive"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="tab">
|
||||
<label
|
||||
class="ttlIndexingPolicyFocusElement"
|
||||
tabindex="0"
|
||||
role="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
for: ttlOnNoDefaultId,
|
||||
'aria-checked': timeToLive() === 'on-nodefault' ? 'true' : 'false'
|
||||
},
|
||||
css: {
|
||||
dirty: timeToLive.editableIsDirty,
|
||||
selectedRadio: timeToLive() === 'on-nodefault',
|
||||
unselectedRadio: timeToLive() !== 'on-nodefault'
|
||||
},
|
||||
event: {
|
||||
keypress: onTtlOnNoDefaultKeyPress
|
||||
},
|
||||
hasFocus: ttlOnDefaultFocused"
|
||||
>On (no default)</label
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="ttl"
|
||||
value="on-nodefault"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
id: ttlOnNoDefaultId
|
||||
},
|
||||
checked: timeToLive"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="tab">
|
||||
<label
|
||||
class="ttlIndexingPolicyFocusElement"
|
||||
tabindex="0"
|
||||
role="radio"
|
||||
for="ttl3"
|
||||
data-bind="
|
||||
attr:{
|
||||
for: ttlOnId,
|
||||
'aria-checked': timeToLive() === 'on' ? 'true' : 'false'
|
||||
},
|
||||
css: {
|
||||
dirty: timeToLive.editableIsDirty,
|
||||
selectedRadio: timeToLive() === 'on',
|
||||
unselectedRadio: timeToLive() !== 'on'
|
||||
},
|
||||
event: {
|
||||
keypress: onTtlOnKeyPress
|
||||
},
|
||||
hasFocus: ttlOnFocused"
|
||||
>On</label
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="ttl"
|
||||
value="on"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
id: ttlOnId
|
||||
},
|
||||
checked: timeToLive"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div data-bind="visible: timeToLive() === 'on'">
|
||||
<input
|
||||
class="dirtyTextbox"
|
||||
type="number"
|
||||
required
|
||||
min="1"
|
||||
max="2147483647"
|
||||
aria-label="Time to live in seconds"
|
||||
data-bind="
|
||||
css: {
|
||||
dirty: timeToLive.editableIsDirty
|
||||
},
|
||||
textInput: timeToLiveSeconds,
|
||||
enable: timeToLive() === 'on'"
|
||||
/>
|
||||
second(s)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Geospatial - start -->
|
||||
<div data-bind="visible: geospatialVisible">
|
||||
<div class="formTitle">Geospatial Configuration</div>
|
||||
|
||||
<div class="tabs disableFocusDefaults" aria-label="Geospatial Configuration" role="radiogroup">
|
||||
<div class="tab">
|
||||
<label
|
||||
for="geography"
|
||||
tabindex="0"
|
||||
role="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
'aria-checked': geospatialConfigType().toLowerCase() !== GEOMETRY.toLowerCase() ? 'true' : 'false'
|
||||
},
|
||||
css: {
|
||||
dirty: geospatialConfigType.editableIsDirty,
|
||||
selectedRadio: geospatialConfigType().toLowerCase() !== GEOMETRY.toLowerCase(),
|
||||
unselectedRadio: geospatialConfigType().toLowerCase() === GEOMETRY.toLowerCase()
|
||||
},
|
||||
event: {
|
||||
keypress: onGeographyKeyPress
|
||||
}"
|
||||
>Geography</label
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="geospatial"
|
||||
id="geography"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr: {
|
||||
value: GEOGRAPHY
|
||||
},
|
||||
checked: geospatialConfigType"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="tab">
|
||||
<label
|
||||
for="geometry"
|
||||
tabindex="0"
|
||||
role="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
'aria-checked': geospatialConfigType().toLowerCase() === GEOMETRY.toLowerCase() ? 'true' : 'false'
|
||||
},
|
||||
css: {
|
||||
dirty: geospatialConfigType.editableIsDirty,
|
||||
selectedRadio: geospatialConfigType().toLowerCase() === GEOMETRY.toLowerCase(),
|
||||
unselectedRadio: geospatialConfigType().toLowerCase() !== GEOMETRY.toLowerCase()
|
||||
},
|
||||
event: {
|
||||
keypress: onGeometryKeyPress
|
||||
}"
|
||||
>Geometry</label
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="geospatial"
|
||||
id="geometry"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr: {
|
||||
value: GEOMETRY
|
||||
},
|
||||
checked: geospatialConfigType"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Geospatial - end -->
|
||||
|
||||
<div data-bind="visible: isAnalyticalStorageEnabled">
|
||||
<div class="formTitle">Analytical Storage Time to Live</div>
|
||||
<div class="tabs disableFocusDefaults" aria-label="Analytical Storage Time to Live" role="radiogroup">
|
||||
<div class="tab">
|
||||
<label tabindex="0" role="radio" class="disabledRadio">Off </label>
|
||||
</div>
|
||||
<div class="tab">
|
||||
<label
|
||||
tabindex="0"
|
||||
role="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
for: 'analyticalStorageTtlOnNoDefaultId',
|
||||
'aria-checked': analyticalStorageTtlSelection() === 'on-nodefault' ? 'true' : 'false'
|
||||
},
|
||||
css: {
|
||||
dirty: analyticalStorageTtlSelection.editableIsDirty,
|
||||
selectedRadio: analyticalStorageTtlSelection() === 'on-nodefault',
|
||||
unselectedRadio: analyticalStorageTtlSelection() !== 'on-nodefault'
|
||||
},
|
||||
event: {
|
||||
keypress: onAnalyticalStorageTtlOnNoDefaultKeyPress
|
||||
}"
|
||||
>On (no default)
|
||||
</label>
|
||||
<input
|
||||
type="radio"
|
||||
name="analyticalStorageTtl"
|
||||
value="on-nodefault"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
id: 'analyticalStorageTtlOnNoDefaultId'
|
||||
},
|
||||
checked: analyticalStorageTtlSelection"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="tab">
|
||||
<label
|
||||
tabindex="0"
|
||||
role="radio"
|
||||
for="ttl3"
|
||||
data-bind="
|
||||
attr:{
|
||||
for: 'analyticalStorageTtlOnId',
|
||||
'aria-checked': analyticalStorageTtlSelection() === 'on' ? 'true' : 'false'
|
||||
},
|
||||
css: {
|
||||
dirty: analyticalStorageTtlSelection.editableIsDirty,
|
||||
selectedRadio: analyticalStorageTtlSelection() === 'on',
|
||||
unselectedRadio: analyticalStorageTtlSelection() !== 'on'
|
||||
},
|
||||
event: {
|
||||
keypress: onAnalyticalStorageTtlOnKeyPress
|
||||
}"
|
||||
>On</label
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="analyticalStorageTtl"
|
||||
value="on"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
id: 'analyticalStorageTtlOnId'
|
||||
},
|
||||
checked: analyticalStorageTtlSelection"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div data-bind="visible: analyticalStorageTtlSelection() === 'on'">
|
||||
<input
|
||||
class="dirtyTextbox"
|
||||
type="number"
|
||||
required
|
||||
min="1"
|
||||
max="2147483647"
|
||||
aria-label="Time to live in seconds"
|
||||
data-bind="
|
||||
css: {
|
||||
dirty: analyticalStorageTtlSelection.editableIsDirty
|
||||
},
|
||||
textInput: analyticalStorageTtlSeconds,
|
||||
enable: analyticalStorageTtlSelection() === 'on'"
|
||||
/>
|
||||
second(s)
|
||||
</div>
|
||||
</div>
|
||||
<div data-bind="visible: changeFeedPolicyVisible">
|
||||
<div class="formTitle">
|
||||
<span>Change feed log retention policy</span>
|
||||
<span class="infoTooltip" role="tooltip" tabindex="0">
|
||||
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
|
||||
<span class="tooltiptext infoTooltipWidth"
|
||||
>Enable change feed log retention policy to retain last 10 minutes of history for items in the container
|
||||
by default. To support this, the request unit (RU) charge for this container will be multiplied by a
|
||||
factor of two for writes. Reads are unaffected.</span
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<div class="tabs disableFocusDefaults" aria-label="Change feed selection tabs">
|
||||
<div class="tab">
|
||||
<label
|
||||
tabindex="0"
|
||||
data-bind="
|
||||
attr:{
|
||||
for: changeFeedPolicyOffId
|
||||
},
|
||||
css: {
|
||||
dirty: changeFeedPolicyToggled.editableIsDirty,
|
||||
selectedRadio: changeFeedPolicyToggled() === 'Off',
|
||||
unselectedRadio: changeFeedPolicyToggled() === 'On'
|
||||
},
|
||||
event: {
|
||||
keypress: onChangeFeedPolicyOffKeyPress
|
||||
}"
|
||||
>Off</label
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="changeFeedPolicy"
|
||||
value="Off"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
id: changeFeedPolicyOffId
|
||||
},
|
||||
checked: changeFeedPolicyToggled"
|
||||
/>
|
||||
</div>
|
||||
<div class="tab">
|
||||
<label
|
||||
tabindex="0"
|
||||
data-bind="
|
||||
attr:{
|
||||
for: changeFeedPolicyOnId
|
||||
},
|
||||
css: {
|
||||
dirty: changeFeedPolicyToggled.editableIsDirty,
|
||||
selectedRadio: changeFeedPolicyToggled() === 'On',
|
||||
unselectedRadio: changeFeedPolicyToggled() === 'Off'
|
||||
},
|
||||
event: {
|
||||
keypress: onChangeFeedPolicyOnKeyPress
|
||||
}"
|
||||
>On</label
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="changeFeedPolicy"
|
||||
value="On"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
id: changeFeedPolicyOnId
|
||||
},
|
||||
checked: changeFeedPolicyToggled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-bind="visible: partitionKeyVisible">
|
||||
<div class="formTitle" data-bind="text: partitionKeyName">Partition Key</div>
|
||||
<input
|
||||
class="formReadOnly collid-white"
|
||||
data-bind="textInput: partitionKeyValue, attr: { 'aria-label':partitionKeyName }"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
<div class="largePartitionKeyEnabled" data-bind="visible: isLargePartitionKeyEnabled">
|
||||
<p data-bind="visible: isLargePartitionKeyEnabled">
|
||||
Large <span data-bind="text:lowerCasePartitionKeyName"></span> has been enabled
|
||||
</p>
|
||||
</div>
|
||||
<div data-bind="visible: shouldShowIndexingPolicyEditor">
|
||||
<div class="formTitle">Indexing Policy</div>
|
||||
<div
|
||||
class="indexingPolicyEditor ttlIndexingPolicyFocusElement"
|
||||
tabindex="0"
|
||||
data-bind="setTemplateReady: true, attr:{ id: indexingPolicyEditorId }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,449 +0,0 @@
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ko from "knockout";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import Collection from "../Tree/Collection";
|
||||
import Database from "../Tree/Database";
|
||||
import Explorer from "../Explorer";
|
||||
import SettingsTab from "./SettingsTab";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import { IndexingPolicies } from "../../Shared/Constants";
|
||||
|
||||
describe("Settings tab", () => {
|
||||
const baseCollection: DataModels.Collection = {
|
||||
defaultTtl: 200,
|
||||
partitionKey: null,
|
||||
conflictResolutionPolicy: {
|
||||
mode: DataModels.ConflictResolutionMode.LastWriterWins,
|
||||
conflictResolutionPath: "/_ts"
|
||||
},
|
||||
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
|
||||
_rid: "",
|
||||
_self: "",
|
||||
_etag: "",
|
||||
_ts: 0,
|
||||
id: "mycoll"
|
||||
};
|
||||
|
||||
const baseDatabase: DataModels.Database = {
|
||||
_rid: "",
|
||||
_self: "",
|
||||
_etag: "",
|
||||
_ts: 0,
|
||||
id: "mydb",
|
||||
collections: [baseCollection]
|
||||
};
|
||||
|
||||
const quotaInfo: DataModels.CollectionQuotaInfo = {
|
||||
storedProcedures: 0,
|
||||
triggers: 0,
|
||||
functions: 0,
|
||||
documentsSize: 0,
|
||||
documentsCount: 0,
|
||||
collectionSize: 0,
|
||||
usageSizeInKB: 0,
|
||||
numPartitions: 0
|
||||
};
|
||||
|
||||
describe("Conflict Resolution", () => {
|
||||
describe("should show conflict resolution", () => {
|
||||
let explorer: Explorer;
|
||||
const baseCollectionWithoutConflict: DataModels.Collection = {
|
||||
defaultTtl: 200,
|
||||
partitionKey: null,
|
||||
conflictResolutionPolicy: null,
|
||||
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
|
||||
_rid: "",
|
||||
_self: "",
|
||||
_etag: "",
|
||||
_ts: 0,
|
||||
id: "mycoll"
|
||||
};
|
||||
const getSettingsTab = (conflictResolution: boolean = true) => {
|
||||
return new SettingsTab({
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: new Collection(
|
||||
explorer,
|
||||
"mydb",
|
||||
conflictResolution ? baseCollection : baseCollectionWithoutConflict,
|
||||
quotaInfo,
|
||||
null
|
||||
),
|
||||
onUpdateTabsButtons: undefined
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer();
|
||||
});
|
||||
|
||||
it("single master, should not show conflict resolution", () => {
|
||||
const settingsTab = getSettingsTab();
|
||||
expect(settingsTab.hasConflictResolution()).toBe(false);
|
||||
});
|
||||
|
||||
it("multi master with resolution conflict, show conflict resolution", () => {
|
||||
explorer.databaseAccount({
|
||||
id: "test",
|
||||
kind: "",
|
||||
location: "",
|
||||
name: "",
|
||||
tags: "",
|
||||
type: "",
|
||||
properties: {
|
||||
enableMultipleWriteLocations: true,
|
||||
documentEndpoint: "",
|
||||
cassandraEndpoint: "",
|
||||
gremlinEndpoint: "",
|
||||
tableEndpoint: ""
|
||||
}
|
||||
});
|
||||
|
||||
const settingsTab = getSettingsTab();
|
||||
expect(settingsTab.hasConflictResolution()).toBe(true);
|
||||
});
|
||||
|
||||
it("multi master without resolution conflict, show conflict resolution", () => {
|
||||
explorer.databaseAccount({
|
||||
id: "test",
|
||||
kind: "",
|
||||
location: "",
|
||||
name: "",
|
||||
tags: "",
|
||||
type: "",
|
||||
properties: {
|
||||
enableMultipleWriteLocations: true,
|
||||
documentEndpoint: "",
|
||||
cassandraEndpoint: "",
|
||||
gremlinEndpoint: "",
|
||||
tableEndpoint: ""
|
||||
}
|
||||
});
|
||||
|
||||
const settingsTab = getSettingsTab(false /* no resolution conflict*/);
|
||||
expect(settingsTab.hasConflictResolution()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Parse Conflict Resolution Mode from backend", () => {
|
||||
it("should parse any casing", () => {
|
||||
expect(SettingsTab.parseConflictResolutionMode("custom")).toBe(DataModels.ConflictResolutionMode.Custom);
|
||||
expect(SettingsTab.parseConflictResolutionMode("Custom")).toBe(DataModels.ConflictResolutionMode.Custom);
|
||||
expect(SettingsTab.parseConflictResolutionMode("lastWriterWins")).toBe(
|
||||
DataModels.ConflictResolutionMode.LastWriterWins
|
||||
);
|
||||
expect(SettingsTab.parseConflictResolutionMode("LastWriterWins")).toBe(
|
||||
DataModels.ConflictResolutionMode.LastWriterWins
|
||||
);
|
||||
});
|
||||
|
||||
it("should parse empty as null", () => {
|
||||
expect(SettingsTab.parseConflictResolutionMode("")).toBe(null);
|
||||
});
|
||||
|
||||
it("should parse null as null", () => {
|
||||
expect(SettingsTab.parseConflictResolutionMode(null)).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Parse Conflict Resolution procedure from backend", () => {
|
||||
it("should parse path as name", () => {
|
||||
expect(SettingsTab.parseConflictResolutionProcedure("/dbs/xxxx/colls/xxxx/sprocs/validsproc")).toBe(
|
||||
"validsproc"
|
||||
);
|
||||
});
|
||||
|
||||
it("should parse name as name", () => {
|
||||
expect(SettingsTab.parseConflictResolutionProcedure("validsproc")).toBe("validsproc");
|
||||
});
|
||||
|
||||
it("should parse invalid path as null", () => {
|
||||
expect(SettingsTab.parseConflictResolutionProcedure("/not/a/valid/path")).toBe(null);
|
||||
});
|
||||
|
||||
it("should parse empty or null as null", () => {
|
||||
expect(SettingsTab.parseConflictResolutionProcedure("")).toBe(null);
|
||||
expect(SettingsTab.parseConflictResolutionProcedure(null)).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Should update collection", () => {
|
||||
let explorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer();
|
||||
});
|
||||
|
||||
it("On TTL changed", () => {
|
||||
const settingsTab = new SettingsTab({
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||
});
|
||||
|
||||
expect(settingsTab.shouldUpdateCollection()).toBe(false);
|
||||
settingsTab.timeToLive("off");
|
||||
expect(settingsTab.shouldUpdateCollection()).toBe(true);
|
||||
|
||||
settingsTab.onRevertClick();
|
||||
expect(settingsTab.shouldUpdateCollection()).toBe(false);
|
||||
settingsTab.timeToLiveSeconds(100);
|
||||
expect(settingsTab.shouldUpdateCollection()).toBe(true);
|
||||
});
|
||||
|
||||
it("On Index Policy changed", () => {
|
||||
const settingsTab = new SettingsTab({
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||
});
|
||||
|
||||
expect(settingsTab.shouldUpdateCollection()).toBe(false);
|
||||
settingsTab.indexingPolicyContent({ somethingDifferent: "" });
|
||||
expect(settingsTab.shouldUpdateCollection()).toBe(true);
|
||||
});
|
||||
|
||||
it("On Conflict Resolution Mode changed", () => {
|
||||
const settingsTab = new SettingsTab({
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||
});
|
||||
|
||||
expect(settingsTab.shouldUpdateCollection()).toBe(false);
|
||||
settingsTab.conflictResolutionPolicyMode(DataModels.ConflictResolutionMode.Custom);
|
||||
expect(settingsTab.shouldUpdateCollection()).toBe(true);
|
||||
|
||||
settingsTab.onRevertClick();
|
||||
expect(settingsTab.shouldUpdateCollection()).toBe(false);
|
||||
settingsTab.conflictResolutionPolicyPath("/somethingElse");
|
||||
expect(settingsTab.shouldUpdateCollection()).toBe(true);
|
||||
|
||||
settingsTab.onRevertClick();
|
||||
expect(settingsTab.shouldUpdateCollection()).toBe(false);
|
||||
settingsTab.conflictResolutionPolicyMode(DataModels.ConflictResolutionMode.Custom);
|
||||
settingsTab.conflictResolutionPolicyProcedure("resolver");
|
||||
expect(settingsTab.shouldUpdateCollection()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Get Conflict Resolution configuration from user", () => {
|
||||
let explorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer();
|
||||
});
|
||||
|
||||
it("null if it didnt change", () => {
|
||||
const settingsTab = new SettingsTab({
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||
});
|
||||
|
||||
expect(settingsTab.getUpdatedConflictResolutionPolicy()).toBe(null);
|
||||
});
|
||||
|
||||
it("Custom contains valid backend path", () => {
|
||||
const settingsTab = new SettingsTab({
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||
});
|
||||
|
||||
expect(settingsTab.getUpdatedConflictResolutionPolicy()).toBe(null);
|
||||
settingsTab.conflictResolutionPolicyMode(DataModels.ConflictResolutionMode.Custom);
|
||||
settingsTab.conflictResolutionPolicyProcedure("resolver");
|
||||
let updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
|
||||
expect(updatedPolicy.mode).toBe(DataModels.ConflictResolutionMode.Custom);
|
||||
expect(updatedPolicy.conflictResolutionProcedure).toBe("/dbs/mydb/colls/mycoll/sprocs/resolver");
|
||||
|
||||
settingsTab.conflictResolutionPolicyProcedure("");
|
||||
updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
|
||||
expect(updatedPolicy.conflictResolutionProcedure).toBe(undefined);
|
||||
});
|
||||
|
||||
it("LWW contains valid property path", () => {
|
||||
const settingsTab = new SettingsTab({
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||
});
|
||||
|
||||
expect(settingsTab.getUpdatedConflictResolutionPolicy()).toBe(null);
|
||||
settingsTab.conflictResolutionPolicyPath("someAttr");
|
||||
let updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
|
||||
expect(updatedPolicy.conflictResolutionPath).toBe("/someAttr");
|
||||
|
||||
settingsTab.conflictResolutionPolicyPath("/someAttr");
|
||||
updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
|
||||
expect(updatedPolicy.conflictResolutionPath).toBe("/someAttr");
|
||||
|
||||
settingsTab.conflictResolutionPolicyPath("");
|
||||
updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
|
||||
expect(updatedPolicy.conflictResolutionPath).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("partitionKeyVisible", () => {
|
||||
enum PartitionKeyOption {
|
||||
None,
|
||||
System,
|
||||
NonSystem
|
||||
}
|
||||
|
||||
function getCollection(defaultApi: string, partitionKeyOption: PartitionKeyOption) {
|
||||
const explorer = new Explorer();
|
||||
explorer.defaultExperience(defaultApi);
|
||||
|
||||
const offer: DataModels.Offer = null;
|
||||
const defaultTtl = 200;
|
||||
const conflictResolutionPolicy = {
|
||||
mode: DataModels.ConflictResolutionMode.LastWriterWins,
|
||||
conflictResolutionPath: "/_ts"
|
||||
};
|
||||
|
||||
return new Collection(
|
||||
explorer,
|
||||
"mydb",
|
||||
{
|
||||
defaultTtl: defaultTtl,
|
||||
partitionKey:
|
||||
partitionKeyOption != PartitionKeyOption.None
|
||||
? {
|
||||
paths: ["/foo"],
|
||||
kind: "Hash",
|
||||
version: 2,
|
||||
systemKey: partitionKeyOption === PartitionKeyOption.System
|
||||
}
|
||||
: null,
|
||||
conflictResolutionPolicy: conflictResolutionPolicy,
|
||||
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
|
||||
_rid: "",
|
||||
_self: "",
|
||||
_etag: "",
|
||||
_ts: 0,
|
||||
id: "mycoll"
|
||||
},
|
||||
quotaInfo,
|
||||
offer
|
||||
);
|
||||
}
|
||||
|
||||
function getSettingsTab(defaultApi: string, partitionKeyOption: PartitionKeyOption): SettingsTab {
|
||||
return new SettingsTab({
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: getCollection(defaultApi, partitionKeyOption),
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||
});
|
||||
}
|
||||
|
||||
it("on SQL container with no partition key should be false", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.DocumentDB, PartitionKeyOption.None);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it("on Mongo container with no partition key should be false", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.MongoDB, PartitionKeyOption.None);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it("on Gremlin container with no partition key should be false", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Graph, PartitionKeyOption.None);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it("on Cassandra container with no partition key should be false", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Cassandra, PartitionKeyOption.None);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it("on Table container with no partition key should be false", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Table, PartitionKeyOption.None);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it("on SQL container with system partition key should be true", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.DocumentDB, PartitionKeyOption.System);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it("on Mongo container with system partition key should be false", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.MongoDB, PartitionKeyOption.System);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it("on Gremlin container with system partition key should be true", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Graph, PartitionKeyOption.System);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it("on Cassandra container with system partition key should be false", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Cassandra, PartitionKeyOption.System);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it("on Table container with system partition key should be false", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Table, PartitionKeyOption.System);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it("on SQL container with non-system partition key should be true", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.DocumentDB, PartitionKeyOption.NonSystem);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it("on Mongo container with non-system partition key should be true", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.MongoDB, PartitionKeyOption.NonSystem);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it("on Gremlin container with non-system partition key should be true", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Graph, PartitionKeyOption.NonSystem);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it("on Cassandra container with non-system partition key should be false", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Cassandra, PartitionKeyOption.NonSystem);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it("on Table container with non-system partition key should be false", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Table, PartitionKeyOption.NonSystem);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -47,7 +47,7 @@ export default class SettingsTabV2 extends TabsBase {
|
||||
this.currentCollection.loadOffer().then(
|
||||
() => {
|
||||
// passed in options and set by parent as "Settings" by default
|
||||
this.tabTitle("Scale & Settings");
|
||||
this.tabTitle(this.currentCollection.offer() ? "Settings" : "Scale & Settings");
|
||||
this.offerRead(true);
|
||||
this.options.getPendingNotification.then(
|
||||
(data: DataModels.Notification) => {
|
||||
|
||||
@@ -9,7 +9,6 @@ import MongoQueryTabTemplate from "./MongoQueryTab.html";
|
||||
import MongoShellTabTemplate from "./MongoShellTab.html";
|
||||
import QueryTabTemplate from "./QueryTab.html";
|
||||
import QueryTablesTabTemplate from "./QueryTablesTab.html";
|
||||
import SettingsTabTemplate from "./SettingsTab.html";
|
||||
import SettingsTabV2Template from "./SettingsTabV2.html";
|
||||
import DatabaseSettingsTabTemplate from "./DatabaseSettingsTab.html";
|
||||
import StoredProcedureTabTemplate from "./StoredProcedureTab.html";
|
||||
@@ -133,15 +132,6 @@ export class QueryTablesTab {
|
||||
}
|
||||
}
|
||||
|
||||
export class SettingsTab {
|
||||
constructor() {
|
||||
return {
|
||||
viewModel: TabComponent,
|
||||
template: SettingsTabTemplate
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class SettingsTabV2 {
|
||||
constructor() {
|
||||
return {
|
||||
|
||||
@@ -10,10 +10,9 @@ describe("Collection", () => {
|
||||
container: Explorer,
|
||||
databaseId: string,
|
||||
data: DataModels.Collection,
|
||||
quotaInfo: DataModels.CollectionQuotaInfo,
|
||||
offer: DataModels.Offer
|
||||
): Collection {
|
||||
return new Collection(container, databaseId, data, quotaInfo, offer);
|
||||
return new Collection(container, databaseId, data);
|
||||
}
|
||||
|
||||
function generateMockCollectionsDataModelWithPartitionKey(
|
||||
@@ -50,7 +49,7 @@ describe("Collection", () => {
|
||||
});
|
||||
mockContainer.deleteCollectionText = ko.observable<string>("delete collection");
|
||||
|
||||
return generateCollection(mockContainer, "abc", data, {} as DataModels.CollectionQuotaInfo, {} as DataModels.Offer);
|
||||
return generateCollection(mockContainer, "abc", data, {} as DataModels.Offer);
|
||||
}
|
||||
|
||||
describe("Partition key path parsing", () => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { readTriggers } from "../../Common/dataAccess/readTriggers";
|
||||
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
|
||||
import { createDocument } from "../../Common/DocumentClientUtilityBase";
|
||||
import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer";
|
||||
import { readCollectionQuotaInfo } from "../../Common/dataAccess/readCollectionQuotaInfo";
|
||||
import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
@@ -29,7 +29,6 @@ import MongoShellTab from "../Tabs/MongoShellTab";
|
||||
import QueryTab from "../Tabs/QueryTab";
|
||||
import QueryTablesTab from "../Tabs/QueryTablesTab";
|
||||
import SettingsTabV2 from "../Tabs/SettingsTabV2";
|
||||
import SettingsTab from "../Tabs/SettingsTab";
|
||||
import ConflictId from "./ConflictId";
|
||||
import DocumentId from "./DocumentId";
|
||||
import StoredProcedure from "./StoredProcedure";
|
||||
@@ -38,7 +37,6 @@ import UserDefinedFunction from "./UserDefinedFunction";
|
||||
import { configContext, Platform } from "../../ConfigContext";
|
||||
import Explorer from "../Explorer";
|
||||
import { userContext } from "../../UserContext";
|
||||
import TabsBase from "../Tabs/TabsBase";
|
||||
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
|
||||
@@ -55,7 +53,8 @@ export default class Collection implements ViewModels.Collection {
|
||||
public defaultTtl: ko.Observable<number>;
|
||||
public indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
||||
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
||||
public quotaInfo: ko.Observable<DataModels.CollectionQuotaInfo>;
|
||||
public usageSizeInKB: ko.Observable<number>;
|
||||
|
||||
public offer: ko.Observable<DataModels.Offer>;
|
||||
public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
||||
public changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
||||
@@ -63,6 +62,8 @@ export default class Collection implements ViewModels.Collection {
|
||||
public throughput: ko.Computed<number>;
|
||||
public rawDataModel: DataModels.Collection;
|
||||
public analyticalStorageTtl: ko.Observable<number>;
|
||||
public schema: DataModels.ISchema;
|
||||
public requestSchema: () => void;
|
||||
public geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
|
||||
|
||||
// TODO move this to API customization class
|
||||
@@ -94,13 +95,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
public userDefinedFunctionsFocused: ko.Observable<boolean>;
|
||||
public triggersFocused: ko.Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
container: Explorer,
|
||||
databaseId: string,
|
||||
data: DataModels.Collection,
|
||||
quotaInfo: DataModels.CollectionQuotaInfo,
|
||||
offer: DataModels.Offer
|
||||
) {
|
||||
constructor(container: Explorer, databaseId: string, data: DataModels.Collection) {
|
||||
this.nodeKind = "Collection";
|
||||
this.container = container;
|
||||
this.self = data._self;
|
||||
@@ -112,11 +107,13 @@ export default class Collection implements ViewModels.Collection {
|
||||
this.id = ko.observable(data.id);
|
||||
this.defaultTtl = ko.observable(data.defaultTtl);
|
||||
this.indexingPolicy = ko.observable(data.indexingPolicy);
|
||||
this.quotaInfo = ko.observable(quotaInfo);
|
||||
this.offer = ko.observable(offer);
|
||||
this.usageSizeInKB = ko.observable();
|
||||
this.offer = ko.observable();
|
||||
this.conflictResolutionPolicy = ko.observable(data.conflictResolutionPolicy);
|
||||
this.changeFeedPolicy = ko.observable<DataModels.ChangeFeedPolicy>(data.changeFeedPolicy);
|
||||
this.analyticalStorageTtl = ko.observable(data.analyticalStorageTtl);
|
||||
this.schema = data.schema;
|
||||
this.requestSchema = data.requestSchema;
|
||||
this.geospatialConfig = ko.observable(data.geospatialConfig);
|
||||
|
||||
// TODO fix this to only replace non-excaped single quotes
|
||||
@@ -552,11 +549,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
dataExplorerArea: Constants.Areas.ResourceTree
|
||||
});
|
||||
|
||||
const isSettingsV2Enabled = this.container.isSettingsV2Enabled();
|
||||
if (!isSettingsV2Enabled) {
|
||||
await this.loadOffer();
|
||||
}
|
||||
|
||||
const tabTitle = !this.offer() ? "Settings" : "Scale & Settings";
|
||||
const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
|
||||
const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Settings, tab => {
|
||||
@@ -583,68 +575,8 @@ export default class Collection implements ViewModels.Collection {
|
||||
onUpdateTabsButtons: this.container.onUpdateTabsButtons
|
||||
};
|
||||
|
||||
if (isSettingsV2Enabled) {
|
||||
let settingsTabV2 = matchingTabs && (matchingTabs[0] as SettingsTabV2);
|
||||
this.launchSettingsTabV2(settingsTabV2, traceStartData, settingsTabOptions, pendingNotificationsPromise);
|
||||
} else {
|
||||
let settingsTab = matchingTabs && (matchingTabs[0] as SettingsTab);
|
||||
this.launchSettingsTabV1(settingsTab, traceStartData, settingsTabOptions, pendingNotificationsPromise);
|
||||
}
|
||||
};
|
||||
|
||||
private launchSettingsTabV1 = (
|
||||
settingsTab: SettingsTab,
|
||||
traceStartData: any,
|
||||
settingsTabOptions: ViewModels.TabOptions,
|
||||
getPendingNotification: Q.Promise<DataModels.Notification>
|
||||
): void => {
|
||||
if (!settingsTab) {
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, traceStartData);
|
||||
settingsTabOptions.onLoadStartKey = startKey;
|
||||
|
||||
getPendingNotification.then(
|
||||
(data: any) => {
|
||||
const pendingNotification: DataModels.Notification = data && data[0];
|
||||
settingsTabOptions.tabKind = ViewModels.CollectionTabKind.Settings;
|
||||
settingsTab = new SettingsTab(settingsTabOptions);
|
||||
this.container.tabsManager.activateNewTab(settingsTab);
|
||||
settingsTab.pendingNotification(pendingNotification);
|
||||
},
|
||||
(error: any) => {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.Tab,
|
||||
{
|
||||
databaseAccountName: this.container.databaseAccount().name,
|
||||
databaseName: this.databaseId,
|
||||
collectionName: this.id(),
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: settingsTabOptions.title,
|
||||
error: errorMessage,
|
||||
errorStack: getErrorStack(error)
|
||||
},
|
||||
startKey
|
||||
);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while fetching container settings for container ${this.id()}: ${errorMessage}`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
getPendingNotification.then(
|
||||
(pendingNotification: DataModels.Notification) => {
|
||||
settingsTab.pendingNotification(pendingNotification);
|
||||
this.container.tabsManager.activateTab(settingsTab);
|
||||
},
|
||||
(error: any) => {
|
||||
settingsTab.pendingNotification(undefined);
|
||||
this.container.tabsManager.activateTab(settingsTab);
|
||||
}
|
||||
);
|
||||
}
|
||||
let settingsTabV2 = matchingTabs && (matchingTabs[0] as SettingsTabV2);
|
||||
this.launchSettingsTabV2(settingsTabV2, traceStartData, settingsTabOptions, pendingNotificationsPromise);
|
||||
};
|
||||
|
||||
private launchSettingsTabV2 = (
|
||||
@@ -669,14 +601,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
}
|
||||
};
|
||||
|
||||
private async loadCollectionQuotaInfo(): Promise<void> {
|
||||
// TODO: Use the collection entity cache to get quota info
|
||||
const quotaInfoWithUniqueKeyPolicy = await readCollectionQuotaInfo(this);
|
||||
this.uniqueKeyPolicy = quotaInfoWithUniqueKeyPolicy.uniqueKeyPolicy;
|
||||
const quotaInfo = _.omit(quotaInfoWithUniqueKeyPolicy, "uniqueKeyPolicy");
|
||||
this.quotaInfo(quotaInfo);
|
||||
}
|
||||
|
||||
public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) {
|
||||
const collection: ViewModels.Collection = source.collection || source;
|
||||
const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Query).length + 1;
|
||||
@@ -1349,7 +1273,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
|
||||
try {
|
||||
this.offer(await readCollectionOffer(params));
|
||||
await this.loadCollectionQuotaInfo();
|
||||
this.usageSizeInKB(await getCollectionUsageSizeInKB(this.databaseId, this.id()));
|
||||
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.LoadOffers,
|
||||
@@ -1357,8 +1281,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
databaseAccountName: this.container.databaseAccount().name,
|
||||
databaseName: this.databaseId,
|
||||
collectionName: this.id(),
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
offerVersion: this.offer()?.offerVersion
|
||||
defaultExperience: this.container.defaultExperience()
|
||||
},
|
||||
startKey
|
||||
);
|
||||
|
||||
82
src/Explorer/Tree/Database.test.ts
Normal file
82
src/Explorer/Tree/Database.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ko from "knockout";
|
||||
import Database from "./Database";
|
||||
import Explorer from "../Explorer";
|
||||
import { HttpStatusCodes } from "../../Common/Constants";
|
||||
import { JunoClient } from "../../Juno/JunoClient";
|
||||
import { userContext, updateUserContext } from "../../UserContext";
|
||||
|
||||
const createMockContainer = (): Explorer => {
|
||||
const mockContainer = new Explorer();
|
||||
return mockContainer;
|
||||
};
|
||||
|
||||
updateUserContext({
|
||||
subscriptionId: "fakeSubscriptionId",
|
||||
resourceGroup: "fakeResourceGroup",
|
||||
databaseAccount: {
|
||||
id: "id",
|
||||
name: "fakeName",
|
||||
location: "fakeLocation",
|
||||
type: "fakeType",
|
||||
tags: undefined,
|
||||
kind: "fakeKind",
|
||||
properties: {
|
||||
documentEndpoint: "fakeEndpoint",
|
||||
tableEndpoint: "fakeEndpoint",
|
||||
gremlinEndpoint: "fakeEndpoint",
|
||||
cassandraEndpoint: "fakeEndpoint"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe("Add Schema", () => {
|
||||
it("should not call requestSchema or getSchema if analyticalStorageTtl is undefined", () => {
|
||||
const collection: DataModels.Collection = {} as DataModels.Collection;
|
||||
collection.analyticalStorageTtl = undefined;
|
||||
const database = new Database(createMockContainer(), { id: "fakeId" });
|
||||
database.container = createMockContainer();
|
||||
database.container.isSchemaEnabled = ko.computed<boolean>(() => false);
|
||||
|
||||
database.junoClient = new JunoClient();
|
||||
database.junoClient.requestSchema = jest.fn();
|
||||
database.junoClient.getSchema = jest.fn();
|
||||
|
||||
database.addSchema(collection);
|
||||
|
||||
expect(database.junoClient.requestSchema).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it("should call requestSchema or getSchema if analyticalStorageTtl is not undefined", () => {
|
||||
const collection: DataModels.Collection = { id: "fakeId" } as DataModels.Collection;
|
||||
collection.analyticalStorageTtl = 0;
|
||||
|
||||
const database = new Database(createMockContainer(), {});
|
||||
database.container = createMockContainer();
|
||||
database.container.isSchemaEnabled = ko.computed<boolean>(() => true);
|
||||
|
||||
database.junoClient = new JunoClient();
|
||||
database.junoClient.requestSchema = jest.fn();
|
||||
database.junoClient.getSchema = jest.fn().mockResolvedValue({ status: HttpStatusCodes.OK, data: {} });
|
||||
|
||||
jest.useFakeTimers();
|
||||
const interval = 5000;
|
||||
const checkForSchema: NodeJS.Timeout = database.addSchema(collection, interval);
|
||||
jest.advanceTimersByTime(interval + 1000);
|
||||
|
||||
expect(database.junoClient.requestSchema).toBeCalledWith({
|
||||
id: undefined,
|
||||
subscriptionId: userContext.subscriptionId,
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
accountName: userContext.databaseAccount.name,
|
||||
resource: `dbs/${database.id}/colls/${collection.id}`,
|
||||
status: "new"
|
||||
});
|
||||
expect(checkForSchema).not.toBeNull();
|
||||
expect(database.junoClient.getSchema).toBeCalledWith(
|
||||
userContext.databaseAccount.name,
|
||||
database.id(),
|
||||
collection.id
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,8 @@ import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsol
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import Explorer from "../Explorer";
|
||||
import { readCollections } from "../../Common/dataAccess/readCollections";
|
||||
import { JunoClient, IJunoResponse } from "../../Juno/JunoClient";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
|
||||
@@ -29,6 +31,7 @@ export default class Database implements ViewModels.Database {
|
||||
public isDatabaseExpanded: ko.Observable<boolean>;
|
||||
public isDatabaseShared: ko.Computed<boolean>;
|
||||
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
|
||||
public junoClient: JunoClient;
|
||||
|
||||
constructor(container: Explorer, data: any) {
|
||||
this.nodeKind = "Database";
|
||||
@@ -43,6 +46,7 @@ export default class Database implements ViewModels.Database {
|
||||
this.isDatabaseShared = ko.pureComputed(() => {
|
||||
return this.offer && !!this.offer();
|
||||
});
|
||||
this.junoClient = new JunoClient();
|
||||
}
|
||||
|
||||
public onSettingsClick = () => {
|
||||
@@ -184,8 +188,12 @@ export default class Database implements ViewModels.Database {
|
||||
const collections: DataModels.Collection[] = await readCollections(this.id());
|
||||
const deltaCollections = this.getDeltaCollections(collections);
|
||||
|
||||
collections.forEach((collection: DataModels.Collection) => {
|
||||
this.addSchema(collection);
|
||||
});
|
||||
|
||||
deltaCollections.toAdd.forEach((collection: DataModels.Collection) => {
|
||||
const collectionVM: Collection = new Collection(this.container, this.id(), collection, null, null);
|
||||
const collectionVM: Collection = new Collection(this.container, this.id(), collection);
|
||||
collectionVMs.push(collectionVM);
|
||||
});
|
||||
|
||||
@@ -308,4 +316,42 @@ export default class Database implements ViewModels.Database {
|
||||
|
||||
this.collections(collectionsToKeep);
|
||||
}
|
||||
|
||||
public addSchema(collection: DataModels.Collection, interval?: number): NodeJS.Timeout {
|
||||
let checkForSchema: NodeJS.Timeout = null;
|
||||
interval = interval || 5000;
|
||||
|
||||
if (collection.analyticalStorageTtl !== undefined && this.container.isSchemaEnabled()) {
|
||||
collection.requestSchema = () => {
|
||||
this.junoClient.requestSchema({
|
||||
id: undefined,
|
||||
subscriptionId: userContext.subscriptionId,
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
accountName: userContext.databaseAccount.name,
|
||||
resource: `dbs/${this.id}/colls/${collection.id}`,
|
||||
status: "new"
|
||||
});
|
||||
checkForSchema = setInterval(async () => {
|
||||
const response: IJunoResponse<DataModels.ISchema> = await this.junoClient.getSchema(
|
||||
userContext.databaseAccount.name,
|
||||
this.id(),
|
||||
collection.id
|
||||
);
|
||||
|
||||
if (response.status >= 404) {
|
||||
clearInterval(checkForSchema);
|
||||
}
|
||||
|
||||
if (response.data !== null) {
|
||||
clearInterval(checkForSchema);
|
||||
collection.schema = response.data;
|
||||
}
|
||||
}, interval);
|
||||
};
|
||||
|
||||
collection.requestSchema();
|
||||
}
|
||||
|
||||
return checkForSchema;
|
||||
}
|
||||
}
|
||||
|
||||
251
src/Explorer/Tree/ResourceTreeAdapter.test.tsx
Normal file
251
src/Explorer/Tree/ResourceTreeAdapter.test.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
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
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user