Compare commits

..

4 Commits

Author SHA1 Message Date
Steve Faulkner
3951f01422 More robust fix 2020-06-09 16:29:41 -05:00
Steve Faulkner
b62cd98f67 Merge branch 'firefox-emulator-fix' into fix-emulator-upload 2020-06-09 15:15:10 -05:00
Steve Faulkner
0240eac920 Fix emulator upload by passing down config.platform 2020-06-09 14:51:30 -05:00
Steve Faulkner
5fb2fe2798 Update to latest JS SDK. Fixes Firefox emulator bug. Close #12 2020-06-08 12:25:10 -05:00
497 changed files with 51808 additions and 58560 deletions

View File

@@ -1,6 +0,0 @@
# These options are only needed when if running end to end tests locally
PORTAL_RUNNER_USERNAME=
PORTAL_RUNNER_PASSWORD=
PORTAL_RUNNER_SUBSCRIPTION=
PORTAL_RUNNER_RESOURCE_GROUP=
PORTAL_RUNNER_DATABASE_ACCOUNT=

View File

@@ -1,5 +1,4 @@
**/node_modules/ **/node_modules/
dist/
src/Api/Apis.ts src/Api/Apis.ts
src/AuthType.ts src/AuthType.ts
src/Bindings/BindingHandlersRegisterer.ts src/Bindings/BindingHandlersRegisterer.ts
@@ -26,6 +25,7 @@ src/Common/Logger.test.ts
src/Common/MessageHandler.test.ts src/Common/MessageHandler.test.ts
src/Common/MessageHandler.ts src/Common/MessageHandler.ts
src/Common/MongoProxyClient.test.ts src/Common/MongoProxyClient.test.ts
src/Common/MongoProxyClient.ts
src/Common/MongoUtility.ts src/Common/MongoUtility.ts
src/Common/NotificationsClientBase.ts src/Common/NotificationsClientBase.ts
src/Common/ObjectCache.test.ts src/Common/ObjectCache.test.ts
@@ -202,6 +202,7 @@ src/Explorer/Tabs/GraphTab.ts
src/Explorer/Tabs/MongoDocumentsTab.ts src/Explorer/Tabs/MongoDocumentsTab.ts
src/Explorer/Tabs/MongoQueryTab.ts src/Explorer/Tabs/MongoQueryTab.ts
src/Explorer/Tabs/MongoShellTab.ts src/Explorer/Tabs/MongoShellTab.ts
src/Explorer/Tabs/NotebookTab.ts
src/Explorer/Tabs/NotebookV2Tab.ts src/Explorer/Tabs/NotebookV2Tab.ts
src/Explorer/Tabs/QueryTab.test.ts src/Explorer/Tabs/QueryTab.test.ts
src/Explorer/Tabs/QueryTab.ts src/Explorer/Tabs/QueryTab.ts
@@ -215,6 +216,7 @@ src/Explorer/Tabs/TabComponents.ts
src/Explorer/Tabs/TabsBase.ts src/Explorer/Tabs/TabsBase.ts
src/Explorer/Tabs/TriggerTab.ts src/Explorer/Tabs/TriggerTab.ts
src/Explorer/Tabs/UserDefinedFunctionTab.ts src/Explorer/Tabs/UserDefinedFunctionTab.ts
src/Explorer/Tabs/__mocks__/NotebookTab.ts
src/Explorer/Tree/AccessibleVerticalList.ts src/Explorer/Tree/AccessibleVerticalList.ts
src/Explorer/Tree/Collection.test.ts src/Explorer/Tree/Collection.test.ts
src/Explorer/Tree/Collection.ts src/Explorer/Tree/Collection.ts

View File

@@ -1,44 +1,45 @@
module.exports = { module.exports = {
env: { env: {
browser: true, browser: true,
es6: true, es6: true
}, },
plugins: ["@typescript-eslint", "no-null"], plugins: ["@typescript-eslint"],
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], extends: [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
globals: { globals: {
Atomics: "readonly", Atomics: "readonly",
SharedArrayBuffer: "readonly", SharedArrayBuffer: "readonly"
}, },
parser: "@typescript-eslint/parser", parser: "@typescript-eslint/parser",
parserOptions: { parserOptions: {
ecmaFeatures: { ecmaFeatures: {
jsx: true, jsx: true
}, },
ecmaVersion: 2018, ecmaVersion: 2018,
sourceType: "module", sourceType: "module"
}, },
overrides: [ overrides: [
{ {
files: ["**/*.tsx"], files: ["**/*.tsx"],
env: { env: {
jest: true, jest: true
}, },
extends: ["plugin:react/recommended"], extends: ["plugin:react/recommended"],
plugins: ["react"], plugins: ["react"]
}, },
{ {
files: ["**/*.test.{ts,tsx}"], files: ["**/*.test.{ts,tsx}"],
env: { env: {
jest: true, jest: true
}, },
extends: ["plugin:jest/recommended"], extends: ["plugin:jest/recommended"],
plugins: ["jest"], plugins: ["jest"]
}, }
], ],
rules: { rules: {
curly: "error", curly: "error"
"@typescript-eslint/no-unused-vars": "error", }
"@typescript-eslint/no-extraneous-class": "error",
"no-null/no-null": "error",
},
}; };

View File

@@ -1,3 +1,6 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: CI name: CI
on: on:
push: push:
@@ -5,41 +8,7 @@ on:
pull_request: pull_request:
branches: [master] branches: [master]
jobs: jobs:
compile: test:
runs-on: ubuntu-latest
name: "Compile TypeScript"
steps:
- uses: actions/checkout@v2
- name: Use Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: 12.x
- run: npm ci
- run: npm run compile
- run: npm run compile:strict
format:
runs-on: ubuntu-latest
name: "Check Format"
steps:
- uses: actions/checkout@v2
- name: Use Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: 12.x
- run: npm ci
- run: npm run format:check
lint:
runs-on: ubuntu-latest
name: "Lint"
steps:
- uses: actions/checkout@v2
- name: Use Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: 12.x
- run: npm ci
- run: npm run lint
unittest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: "Unit Tests" name: "Unit Tests"
steps: steps:
@@ -67,14 +36,13 @@ jobs:
path: .cache path: .cache
key: ${{ runner.os }}-build-cache key: ${{ runner.os }}-build-cache
- run: npm run pack:prod - run: npm run pack:prod
- run: cp -r ./Contracts ./dist/contracts - run: npm run copyToConsumers
- run: cp -r ./configs ./dist/configs
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
with: with:
name: dist name: dist
path: dist/ path: dist/
endtoendemulator: endtoend:
name: "End To End Tests | Emulator | SQL" name: "End to End Tests"
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -99,65 +67,9 @@ jobs:
EMULATOR_ENDPOINT: https://0.0.0.0:8081/ EMULATOR_ENDPOINT: https://0.0.0.0:8081/
NODE_TLS_REJECT_UNAUTHORIZED: 0 NODE_TLS_REJECT_UNAUTHORIZED: 0
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
endtoendsql:
name: "End To End Tests | SQL"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- 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
- run: npm ci
- name: End to End Tests
run: |
npm start &
cd cypress
npm ci
node cleanup.js
npm run wait-for-server
npx cypress run --browser chrome --headless --spec "./integration/dataexplorer/SQL/*"
shell: bash
env:
NODE_TLS_REJECT_UNAUTHORIZED: 0
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
endtoendmongo:
name: "End To End Tests | Mongo"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- 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
- name: End to End Tests
run: |
npm ci
npm start &
cd cypress
npm ci
node cleanup.js
npm run wait-for-server
npx cypress run --browser chrome --headless --spec "./integration/dataexplorer/MONGO/*"
shell: bash
env:
NODE_TLS_REJECT_UNAUTHORIZED: 0
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
nuget: nuget:
name: Publish Nuget name: Publish Nuget
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendsql, endtoendmongo] needs: [build, test, endtoend]
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
@@ -170,7 +82,6 @@ jobs:
uses: actions/download-artifact@v2 uses: actions/download-artifact@v2
with: with:
name: dist name: dist
- run: cp ./configs/prod.json config.json
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "GitHub" -Password "$AZURE_DEVOPS_PAT" - run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "GitHub" -Password "$AZURE_DEVOPS_PAT"
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}" - run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
- run: nuget push -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg - run: nuget push -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
@@ -179,7 +90,7 @@ jobs:
path: "*.nupkg" path: "*.nupkg"
nugetmpac: nugetmpac:
name: Publish Nuget MPAC name: Publish Nuget MPAC
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendsql, endtoendmongo] needs: [build, test, endtoend]
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
@@ -192,7 +103,6 @@ jobs:
uses: actions/download-artifact@v2 uses: actions/download-artifact@v2
with: with:
name: dist name: dist
- run: cp ./configs/mpac.json config.json
- run: sed -i 's/Azure.Cosmos.DB.Data.Explorer/Azure.Cosmos.DB.Data.Explorer.MPAC/g' DataExplorer.nuspec - run: sed -i 's/Azure.Cosmos.DB.Data.Explorer/Azure.Cosmos.DB.Data.Explorer.MPAC/g' DataExplorer.nuspec
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "GitHub" -Password "$AZURE_DEVOPS_PAT" - run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "GitHub" -Password "$AZURE_DEVOPS_PAT"
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}" - run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"

View File

@@ -1,20 +0,0 @@
name: Runners
on:
schedule:
- cron: "*/10 * * * *"
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

3
.gitignore vendored
View File

@@ -15,5 +15,4 @@ cypress/fixtures
notebookapp/* notebookapp/*
Contracts/* Contracts/*
.DS_Store .DS_Store
.cache/ .cache/
.env

View File

@@ -1,51 +0,0 @@
# Contribution guidelines to Data Explorer
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.microsoft.com.
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
## Microsoft Open Source Code of Conduct
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
Resources:
- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
## Browser support
Please make sure to support all modern browsers as well as Internet Explorer 11.
For IE support, polyfill is preferred over new usage of lodash or underscore. We already polyfill almost everything by importing babel-polyfill at the top of entry points.
## Coding guidelines, conventions and recommendations
### Typescript
* Follow this [typescript style guide](https://github.com/excelmicro/typescript) which is based on [airbnb's style guide](https://github.com/airbnb/javascript).
* Conventions speficic to this project:
* Use double-quotes for string
* Don't use null, use undefined
* Pascal case for private static readonly fields
* Camel case for classnames in markup
* Don't use class unless necessary
* Code related to notebooks should be dynamically imported so that it is loaded from a separate bundle only if the account is notebook-enabled. There are already top-level notebook components which are dynamically imported and their dependencies can be statically imported from these files.
### React
* Prefer using React class components over function components and hooks unless you have a simple component and require no nested functions:
* Nested functions may be harder to test independently
* Switching from function component to class component later mayb be painful
## Testing
Any PR should not decrease testing coverage.
## Recommended Tools and VS Code extensions
* [Bookmarks](https://github.com/alefragnani/vscode-bookmarks)
* [Bracket pair colorizer](https://github.com/CoenraadS/Bracket-Pair-Colorizer-2)
* [GitHub Pull Requests and Issues](https://github.com/Microsoft/vscode-pull-request-github)

View File

@@ -70,7 +70,7 @@ Unit tests are located adjacent to the code under test and run with [Jest](https
`npm run test` `npm run test`
#### End to End CI Tests #### End to End 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: [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:
@@ -80,13 +80,16 @@ Unit tests are located adjacent to the code under test and run with [Jest](https
4. Install dependencies: `npm install` 4. Install dependencies: `npm install`
5. Run cypress headless(`npm run test`) or in interactive mode(`npm run test:debug`) 5. Run cypress headless(`npm run test`) or in interactive mode(`npm run test:debug`)
#### End to End Production Runners
Jest and Puppeteer are used for end to end production runners and are contained in `test/`. To run these tests locally:
1. Copy .env.example to .env and fill in all variables
2. Run `npm run test:e2e`
# Contributing # Contributing
Please read the [contribution guidelines](./CONTRIBUTING.md). This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.microsoft.com.
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

View File

@@ -1,3 +1,3 @@
module.exports = { module.exports = {
presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-react", "@babel/preset-typescript"], presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-react", "@babel/preset-typescript"]
}; };

View File

@@ -1,3 +0,0 @@
{
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com"
}

View File

@@ -1,3 +0,0 @@
{
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com"
}

5
cypress/.gitignore vendored
View File

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

View File

@@ -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);
});

View File

@@ -23,7 +23,7 @@ context("Cassandra API Test - createDatabase", () => {
const keyspaceId = `KeyspaceId${crypt.randomBytes(8).toString("hex")}`; const keyspaceId = `KeyspaceId${crypt.randomBytes(8).toString("hex")}`;
const tableId = `TableId112`; const tableId = `TableId112`;
cy.get("iframe").then(($element) => { cy.get("iframe").then($element => {
const $body = $element.contents().find("body"); const $body = $element.contents().find("body");
cy.wrap($body) cy.wrap($body)
.find('div[class="commandBarContainer"]') .find('div[class="commandBarContainer"]')
@@ -32,15 +32,27 @@ context("Cassandra API Test - createDatabase", () => {
.should("be.visible") .should("be.visible")
.click(); .click();
cy.wrap($body).find('div[class="contextual-pane-in"]').should("be.visible").find('span[id="containerTitle"]'); 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[id="keyspace-id"]')
.should("be.visible")
.type(keyspaceId);
cy.wrap($body).find('input[class="textfontclr"]').type(tableId); 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('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body).find('data-test="addCollection-createCollection"').click(); cy.wrap($body)
.find('data-test="addCollection-createCollection"')
.click();
cy.wait(10000); cy.wait(10000);

View File

@@ -24,7 +24,7 @@ context("Graph API Test", () => {
const graphId = `TestGraph${crypt.randomBytes(8).toString("hex")}`; const graphId = `TestGraph${crypt.randomBytes(8).toString("hex")}`;
const partitionKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`; const partitionKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
cy.get("iframe").then(($element) => { cy.get("iframe").then($element => {
const $body = $element.contents().find("body"); const $body = $element.contents().find("body");
cy.wrap($body) cy.wrap($body)
.find('div[class="commandBarContainer"]') .find('div[class="commandBarContainer"]')
@@ -33,21 +33,39 @@ context("Graph API Test", () => {
.should("be.visible") .should("be.visible")
.click(); .click();
cy.wrap($body).find('div[class="contextual-pane-in"]').should("be.visible").find('span[id="containerTitle"]'); 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-createNewDatabase"]')
.check();
cy.wrap($body).find('input[data-test="addCollection-newDatabaseId"]').should("be.visible").type(dbId); 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="addCollectionPane-databaseSharedThroughput"]')
.check();
cy.wrap($body).find('input[data-test="databaseThroughputValue"]').should("have.value", "400"); 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-collectionId"]')
.type(graphId);
cy.wrap($body).find('input[data-test="addCollection-partitionKeyValue"]').type(partitionKey); cy.wrap($body)
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(partitionKey);
cy.wrap($body).find('input[data-test="addCollection-createCollection"]').click(); cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000); cy.wait(10000);

View File

@@ -16,7 +16,7 @@ let crypt = require("crypto");
context("Mongo API Test - createDatabase", () => { context("Mongo API Test - createDatabase", () => {
beforeEach(() => { beforeEach(() => {
connectionString.loginUsingConnectionString(); connectionString.loginUsingConnectionString(connectionString.constants.mongo);
}); });
it("Create a new collection in Mongo API", () => { it("Create a new collection in Mongo API", () => {
@@ -24,7 +24,7 @@ context("Mongo API Test - createDatabase", () => {
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`; const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`; const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
cy.get("iframe").then(($element) => { cy.get("iframe").then($element => {
const $body = $element.contents().find("body"); const $body = $element.contents().find("body");
cy.wrap($body) cy.wrap($body)
.find('div[class="commandBarContainer"]') .find('div[class="commandBarContainer"]')
@@ -33,21 +33,38 @@ context("Mongo API Test - createDatabase", () => {
.should("be.visible") .should("be.visible")
.click(); .click();
cy.wrap($body).find('div[class="contextual-pane-in"]').should("be.visible").find('span[id="containerTitle"]'); 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-createNewDatabase"]')
.check();
cy.wrap($body).find('input[data-test="addCollection-newDatabaseId"]').type(dbId); 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="addCollectionPane-databaseSharedThroughput"]')
.check();
cy.wrap($body).find('input[data-test="addCollection-collectionId"]').type(collectionId); 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="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body).find('input[data-test="addCollection-partitionKeyValue"]').type(sharedKey); cy.wrap($body)
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(sharedKey);
cy.wrap($body).find("#submitBtnAddCollection").click(); cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000); cy.wait(10000);

View File

@@ -16,15 +16,15 @@ let crypt = require("crypto");
context("Mongo API Test", () => { context("Mongo API Test", () => {
beforeEach(() => { beforeEach(() => {
connectionString.loginUsingConnectionString(); connectionString.loginUsingConnectionString(connectionString.constants.mongo);
}); });
it.skip("Create a new collection in Mongo API - Autopilot", () => { it("Create a new collection in Mongo API - Autopilot", () => {
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`; const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`; const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`; const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
cy.get("iframe").then(($element) => { cy.get("iframe").then($element => {
const $body = $element.contents().find("body"); const $body = $element.contents().find("body");
cy.wrap($body) cy.wrap($body)
.find('div[class="commandBarContainer"]') .find('div[class="commandBarContainer"]')
@@ -33,23 +33,34 @@ context("Mongo API Test", () => {
.should("be.visible") .should("be.visible")
.click(); .click();
cy.wrap($body).find('div[class="contextual-pane-in"]').should("be.visible").find('span[id="containerTitle"]'); 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-createNewDatabase"]')
.check();
cy.wrap($body).find('input[data-test="addCollection-newDatabaseId"]').type(dbId); 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="addCollectionPane-databaseSharedThroughput"]')
.check();
cy.wrap($body) cy.wrap($body)
.find('div[class="throughputModeContainer"]') .find('div[class="throughputModeContainer"]')
.should("be.visible") .should("be.visible")
.and((input) => { .and(input => {
expect(input.get(0).textContent, "first item").contains("Autopilot (preview)"); expect(input.get(0).textContent, "first item").contains("Autopilot (preview)");
expect(input.get(1).textContent, "second item").contains("Manual"); expect(input.get(1).textContent, "second item").contains("Manual");
}); });
cy.wrap($body).find('input[id="newContainer-databaseThroughput-autoPilotRadio"]').check(); cy.wrap($body)
.find('input[id="newContainer-databaseThroughput-autoPilotRadio"]')
.check();
cy.wrap($body) cy.wrap($body)
.find('select[name="autoPilotTiers"]') .find('select[name="autoPilotTiers"]')
@@ -57,13 +68,19 @@ context("Mongo API Test", () => {
// // .select('4,000 RU/s').should('have.value', '1'); // // .select('4,000 RU/s').should('have.value', '1');
.find('option[value="2"]') .find('option[value="2"]')
.then(($element) => $element.get(1).setAttribute("selected", "selected")); .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-collectionId"]')
.type(collectionId);
cy.wrap($body).find('input[data-test="addCollection-partitionKeyValue"]').type(sharedKey); cy.wrap($body)
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(sharedKey);
cy.wrap($body).find('input[data-test="addCollection-createCollection"]').click(); cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000); cy.wait(10000);

View File

@@ -4,20 +4,20 @@ let crypt = require("crypto");
context("Mongo API Test", () => { context("Mongo API Test", () => {
beforeEach(() => { beforeEach(() => {
connectionString.loginUsingConnectionString(); connectionString.loginUsingConnectionString(connectionString.constants.mongo);
}); });
it.skip("Create a new collection in existing database in Mongo API", () => { it("Create a new collection in existing database in Mongo API", () => {
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`; const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`; const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
cy.get("iframe").then(($element) => { cy.get("iframe").then($element => {
const $body = $element.contents().find("body"); const $body = $element.contents().find("body");
cy.wrap($body) cy.wrap($body)
.find('span[class="nodeLabel"]') .find('span[class="nodeLabel"]')
.should("be.visible") .should("be.visible")
.then(($span) => { .then($span => {
const dbId1 = $span.text(); const dbId1 = $span.text();
cy.log("DBBB", dbId1); cy.log("DBBB", dbId1);
@@ -28,17 +28,30 @@ context("Mongo API Test", () => {
.should("be.visible") .should("be.visible")
.click(); .click();
cy.wrap($body).find('div[class="contextual-pane-in"]').should("be.visible").find('span[id="containerTitle"]'); 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"]')
.check();
cy.wrap($body).find('input[data-test="addCollection-existingDatabase"]').type(dbId1); 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-collectionId"]')
.type(collectionId);
cy.wrap($body).find('input[data-test="addCollection-partitionKeyValue"]').type(sharedKey); cy.wrap($body)
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(sharedKey);
cy.wrap($body).find('input[data-test="addCollection-createCollection"]').click(); cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000); cy.wait(10000);

View File

@@ -2,9 +2,9 @@ const connectionString = require("../../../utilities/connectionString");
let crypt = require("crypto"); let crypt = require("crypto");
context.skip("Mongo API Test", () => { context("Mongo API Test", () => {
beforeEach(() => { beforeEach(() => {
connectionString.loginUsingConnectionString(); connectionString.loginUsingConnectionString(connectionString.constants.mongo);
}); });
it("Create a new collection in Mongo API - Provision database throughput", () => { it("Create a new collection in Mongo API - Provision database throughput", () => {
@@ -12,7 +12,7 @@ context.skip("Mongo API Test", () => {
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`; const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`; const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
cy.get("iframe").then(($element) => { cy.get("iframe").then($element => {
const $body = $element.contents().find("body"); const $body = $element.contents().find("body");
cy.wrap($body) cy.wrap($body)
.find('div[class="commandBarContainer"]') .find('div[class="commandBarContainer"]')
@@ -21,31 +21,50 @@ context.skip("Mongo API Test", () => {
.should("be.visible") .should("be.visible")
.click(); .click();
cy.wrap($body).find('div[class="contextual-pane-in"]').should("be.visible").find('span[id="containerTitle"]'); cy.wrap($body)
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body) cy.wrap($body)
.find(".createNewDatabaseOrUseExisting") .find(".createNewDatabaseOrUseExisting")
.should("have.length", 2) .should("have.length", 2)
.and((input) => { .and(input => {
expect(input.get(0).textContent, "first item").contains("Create new"); expect(input.get(0).textContent, "first item").contains("Create new");
expect(input.get(1).textContent, "second item").contains("Use existing"); 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="addCollection-createNewDatabase"]')
.check();
cy.wrap($body).find('input[data-test="addCollectionPane-databaseSharedThroughput"]').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-newDatabaseId"]')
.type(dbId);
cy.wrap($body).find('input[data-test="addCollectionPane-databaseSharedThroughput"]').check(); 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="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body).find('input[data-test="addCollection-collectionId"]').type(collectionId); 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-partitionKeyValue"]')
.type(sharedKey);
cy.wrap($body).find('input[data-test="addCollection-createCollection"]').click(); cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000); cy.wait(10000);
@@ -65,7 +84,7 @@ context.skip("Mongo API Test", () => {
const collectionIdTitle = `Add Collection`; const collectionIdTitle = `Add Collection`;
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`; const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
cy.get("iframe").then(($element) => { cy.get("iframe").then($element => {
const $body = $element.contents().find("body"); const $body = $element.contents().find("body");
cy.wrap($body) cy.wrap($body)
.find('div[class="commandBarContainer"]') .find('div[class="commandBarContainer"]')
@@ -74,23 +93,42 @@ context.skip("Mongo API Test", () => {
.should("be.visible") .should("be.visible")
.click(); .click();
cy.wrap($body).find('div[class="contextual-pane-in"]').should("be.visible").find('span[id="containerTitle"]'); 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-createNewDatabase"]')
.check();
cy.wrap($body).find('input[data-test="addCollection-newDatabaseId"]').type(dbId); 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="addCollectionPane-databaseSharedThroughput"]')
.uncheck();
cy.wrap($body).find('input[data-test="addCollection-collectionId"]').type(collectionId); 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[id="tab2"]')
.check({ force: true });
cy.wrap($body).find('input[data-test="addCollection-partitionKeyValue"]').type(sharedKey); 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="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body).find('input[data-test="addCollection-createCollection"]').click(); cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000); cy.wait(10000);
@@ -109,7 +147,7 @@ context.skip("Mongo API Test", () => {
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`; const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`; const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
cy.get("iframe").then(($element) => { cy.get("iframe").then($element => {
const $body = $element.contents().find("body"); const $body = $element.contents().find("body");
cy.wrap($body) cy.wrap($body)
.find('div[class="commandBarContainer"]') .find('div[class="commandBarContainer"]')
@@ -118,21 +156,38 @@ context.skip("Mongo API Test", () => {
.should("be.visible") .should("be.visible")
.click(); .click();
cy.wrap($body).find('div[class="contextual-pane-in"]').should("be.visible").find('span[id="containerTitle"]'); 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-createNewDatabase"]')
.check();
cy.wrap($body).find('input[data-test="addCollection-newDatabaseId"]').type(dbId); 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="addCollectionPane-databaseSharedThroughput"]')
.uncheck();
cy.wrap($body).find('input[data-test="addCollection-collectionId"]').type(collectionId); 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[id="tab1"]')
.check({ force: true });
cy.wrap($body).find('input[data-test="databaseThroughputValue"]').should("have.value", "400"); cy.wrap($body)
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body).find('input[data-test="addCollection-createCollection"]').click(); cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000); cy.wait(10000);

View File

@@ -16,16 +16,15 @@ let crypt = require("crypto");
context("SQL API Test", () => { context("SQL API Test", () => {
beforeEach(() => { beforeEach(() => {
connectionString.loginUsingConnectionString(); connectionString.loginUsingConnectionString(connectionString.constants.sql);
}); });
it("Create a new container in SQL API", () => { it("Create a new container in SQL API", () => {
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`; const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`; const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`; const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
connectionString.loginUsingConnectionString();
cy.get("iframe").then(($element) => { cy.get("iframe").then($element => {
const $body = $element.contents().find("body"); const $body = $element.contents().find("body");
cy.wrap($body) cy.wrap($body)
.find('div[class="commandBarContainer"]') .find('div[class="commandBarContainer"]')
@@ -34,21 +33,38 @@ context("SQL API Test", () => {
.should("be.visible") .should("be.visible")
.click(); .click();
cy.wrap($body).find('div[class="contextual-pane-in"]').should("be.visible").find('span[id="containerTitle"]'); 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-createNewDatabase"]')
.check();
cy.wrap($body).find('input[data-test="addCollectionPane-databaseSharedThroughput"]').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-newDatabaseId"]')
.type(dbId);
cy.wrap($body).find('input[data-test="addCollection-collectionId"]').type(collectionId); 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="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body).find('input[data-test="addCollection-partitionKeyValue"]').type(sharedKey); cy.wrap($body)
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(sharedKey);
cy.wrap($body).find("#submitBtnAddCollection").click(); cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000); cy.wait(10000);

View File

@@ -22,7 +22,7 @@ context("Table API Test", () => {
it("Create a new table in Table API", () => { it("Create a new table in Table API", () => {
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`; const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
cy.get("iframe").then(($element) => { cy.get("iframe").then($element => {
const $body = $element.contents().find("body"); const $body = $element.contents().find("body");
cy.wrap($body) cy.wrap($body)
.find('div[class="commandBarContainer"]') .find('div[class="commandBarContainer"]')
@@ -31,13 +31,22 @@ context("Table API Test", () => {
.should("be.visible") .should("be.visible")
.click(); .click();
cy.wrap($body).find('div[class="contextual-pane-in"]').should("be.visible").find('span[id="containerTitle"]'); 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="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body).find('input[data-test="databaseThroughputValue"]').should("have.value", "400"); cy.wrap($body)
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body).find('input[data-test="addCollection-createCollection"]').click(); cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000); cy.wait(10000);

View File

@@ -29,7 +29,7 @@ context("Emulator - createDatabase", () => {
cy.get(".createNewDatabaseOrUseExisting") cy.get(".createNewDatabaseOrUseExisting")
.should("have.length", 2) .should("have.length", 2)
.and((input) => { .and(input => {
expect(input.get(0).textContent, "first item").contains("Create new"); expect(input.get(0).textContent, "first item").contains("Create new");
expect(input.get(1).textContent, "second item").contains("Use existing"); expect(input.get(1).textContent, "second item").contains("Use existing");
}); });

View File

@@ -38,15 +38,27 @@ context("Emulator - Create database -> container -> item", () => {
cy.get("[data-test=addCollection-partitionKeyValue]").type("/pk"); cy.get("[data-test=addCollection-partitionKeyValue]").type("/pk");
cy.get('input[name="createCollection"]').click(); cy.get('input[name="createCollection"]').click();
cy.get(".dataResourceTree").should("contain", databaseId); cy.get(".dataResourceTree").should("contain", databaseId);
cy.get(".dataResourceTree").contains(databaseId).click(); cy.get(".dataResourceTree")
.contains(databaseId)
.click();
cy.get(".dataResourceTree").should("contain", collectionId); cy.get(".dataResourceTree").should("contain", collectionId);
cy.get(".dataResourceTree").contains(collectionId).click(); cy.get(".dataResourceTree")
cy.get(".dataResourceTree").contains("Items").click(); .contains(collectionId)
cy.get(".dataResourceTree").contains("Items").click(); .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.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.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.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.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.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"); cy.get(".documentsGridHeaderContainer").should("contain", "replace_with_new_document_id");
}); });

View File

@@ -14,18 +14,25 @@ context("Emulator - deleteCollection", () => {
}); });
it("Delete a collection", () => { it("Delete a collection", () => {
cy.get(".databaseId").last().click(); cy.get(".databaseId")
.last()
.click();
cy.get(".collectionList") cy.get(".collectionList")
.last() .last()
.then(($id) => { .then($id => {
const collectionId = $id.text(); const collectionId = $id.text();
cy.get('span[data-test="collectionEllipsisMenu"]').should("exist"); cy.get('span[data-test="collectionEllipsisMenu"]').should("exist");
cy.get('span[data-test="collectionEllipsisMenu"]').invoke("show").last().click(); cy.get('span[data-test="collectionEllipsisMenu"]')
.invoke("show")
.last()
.click();
cy.get('div[data-test="collectionContextMenu"]').contains("Delete Container").click({ force: true }); 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="confirmCollectionId"]').type(collectionId.trim());

View File

@@ -22,10 +22,10 @@ context("Emulator - deleteDatabase", () => {
url: "https://localhost:8081/_explorer/authorization/post/dbs/", url: "https://localhost:8081/_explorer/authorization/post/dbs/",
headers: { headers: {
"x-ms-date": date, "x-ms-date": date,
authorization: "-", authorization: "-"
}, }
}) })
.then((response) => { .then(response => {
authToken = response.body.Token; // Getting auth token for collection creation authToken = response.body.Token; // Getting auth token for collection creation
return new Cypress.Promise((resolve, reject) => { return new Cypress.Promise((resolve, reject) => {
return resolve(); return resolve();
@@ -38,12 +38,12 @@ context("Emulator - deleteDatabase", () => {
headers: { headers: {
"x-ms-date": date, "x-ms-date": date,
authorization: authToken, authorization: authToken,
"x-ms-version": "2018-12-31", "x-ms-version": "2018-12-31"
}, },
body: { body: {
id: dbId, id: dbId
}, }
}).then((response) => { }).then(response => {
cy.log("Response", response); cy.log("Response", response);
db_rid = response.body._rid; db_rid = response.body._rid;
return new Cypress.Promise((resolve, reject) => { return new Cypress.Promise((resolve, reject) => {
@@ -59,14 +59,19 @@ context("Emulator - deleteDatabase", () => {
cy.get(".databaseId") cy.get(".databaseId")
.last() .last()
.then(($id) => { .then($id => {
const dbId = $id.text(); const dbId = $id.text();
cy.get('span[data-test="databaseEllipsisMenu"]').should("exist"); cy.get('span[data-test="databaseEllipsisMenu"]').should("exist");
cy.get('span[data-test="databaseEllipsisMenu"]').invoke("show").last().click(); cy.get('span[data-test="databaseEllipsisMenu"]')
.invoke("show")
.last()
.click();
cy.get('div[data-test="databaseContextMenu"]').contains("Delete Database").click({ force: true }); 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="confirmDatabaseId"]').type(dbId.trim());

View File

@@ -21,25 +21,29 @@ context("New Notebook smoke test", () => {
cy.contains("New Notebook").click(); cy.contains("New Notebook").click();
// Check tab name // Check tab name
cy.get("li.tabList .tabNavText").should(($span) => { cy.get("li.tabList .tabNavText").should($span => {
const text = $span.text(); const text = $span.text();
expect(text).to.match(/^Untitled.*\.ipynb$/); expect(text).to.match(/^Untitled.*\.ipynb$/);
}); });
// Wait for python3 | idle status // Wait for python3 | idle status
cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should(($p) => { cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should($p => {
const text = $p.text(); const text = $p.text();
expect(text).to.match(/^python3.*idle$/); expect(text).to.match(/^python3.*idle$/);
}); });
// Click on a cell // Click on a cell
cy.get(".cell-container").as("cellContainer").click(); cy.get(".cell-container")
.as("cellContainer")
.click();
// Type in some code // Type in some code
cy.get("@cellContainer").type("2+4"); cy.get("@cellContainer").type("2+4");
// Execute // Execute
cy.get('[data-test="Run"]').first().click(); cy.get('[data-test="Run"]')
.first()
.click();
// Verify results // Verify results
cy.get("@cellContainer").within(() => { cy.get("@cellContainer").within(() => {
@@ -47,29 +51,39 @@ context("New Notebook smoke test", () => {
}); });
// Restart kernel // Restart kernel
cy.get('[data-test="Run"] button').eq(-1).click(); cy.get('[data-test="Run"] button')
cy.get("li").contains("Restart Kernel").click(); .eq(-1)
.click();
cy.get("li")
.contains("Restart Kernel")
.click();
// Wait for python3 | restarting status // Wait for python3 | restarting status
cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should(($p) => { cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should($p => {
const text = $p.text(); const text = $p.text();
expect(text).to.match(/^python3.*restarting$/); expect(text).to.match(/^python3.*restarting$/);
}); });
// Wait for python3 | idle status // Wait for python3 | idle status
cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should(($p) => { cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should($p => {
const text = $p.text(); const text = $p.text();
expect(text).to.match(/^python3.*idle$/); expect(text).to.match(/^python3.*idle$/);
}); });
// Click on a cell // Click on a cell
cy.get(".cell-container").as("cellContainer").find(".input").as("codeInput").click(); cy.get(".cell-container")
.as("cellContainer")
.find(".input")
.as("codeInput")
.click();
// Type in some code // Type in some code
cy.get("@codeInput").type("{backspace}{backspace}{backspace}4+5"); cy.get("@codeInput").type("{backspace}{backspace}{backspace}4+5");
// Execute // Execute
cy.get('[data-test="Run"]').first().click(); cy.get('[data-test="Run"]')
.first()
.click();
// Verify results // Verify results
cy.get("@cellContainer").within(() => { cy.get("@cellContainer").within(() => {

View File

@@ -11,11 +11,15 @@ context("Resource tree notebook file manipulation", () => {
}; };
const clickContextMenuAndSelectOption = (nodeLabel, option) => { const clickContextMenuAndSelectOption = (nodeLabel, option) => {
cy.get(`.treeNodeHeader[data-test="${nodeLabel}"]`).find("button.treeMenuEllipsis").click(); cy.get(`.treeNodeHeader[data-test="${nodeLabel}"]`)
cy.get('[data-test="treeComponentMenuItemContainer"]').contains(option).click(); .find("button.treeMenuEllipsis")
.click();
cy.get('[data-test="treeComponentMenuItemContainer"]')
.contains(option)
.click();
}; };
const createFolder = (folder) => { const createFolder = folder => {
clickContextMenuAndSelectOption("My Notebooks/", "New Directory"); clickContextMenuAndSelectOption("My Notebooks/", "New Directory");
cy.get("#stringInputPane").within(() => { cy.get("#stringInputPane").within(() => {
@@ -24,9 +28,11 @@ context("Resource tree notebook file manipulation", () => {
}); });
}; };
const deleteItem = (nodeName) => { const deleteItem = nodeName => {
clickContextMenuAndSelectOption(`${nodeName}`, "Delete"); clickContextMenuAndSelectOption(`${nodeName}`, "Delete");
cy.get(".ms-Dialog-main").contains("Delete").click(); cy.get(".ms-Dialog-main")
.contains("Delete")
.click();
}; };
beforeEach(() => { beforeEach(() => {
@@ -50,7 +56,9 @@ context("Resource tree notebook file manipulation", () => {
// Rename // Rename
clickContextMenuAndSelectOption(`${folder}/`, "Rename"); clickContextMenuAndSelectOption(`${folder}/`, "Rename");
cy.get("#stringInputPane").within(() => { cy.get("#stringInputPane").within(() => {
cy.get('input[name="collectionIdConfirmation"]').clear().type(renamedFolder); cy.get('input[name="collectionIdConfirmation"]')
.clear()
.type(renamedFolder);
cy.get("form").submit(); cy.get("form").submit();
}); });
cy.get(`.treeNodeHeader[data-test="${renamedFolder}/"]`).should("exist"); cy.get(`.treeNodeHeader[data-test="${renamedFolder}/"]`).should("exist");
@@ -67,12 +75,16 @@ context("Resource tree notebook file manipulation", () => {
clickContextMenuAndSelectOption(`${folder}/`, "New Notebook"); clickContextMenuAndSelectOption(`${folder}/`, "New Notebook");
// Verify tab is open // Verify tab is open
cy.get(".tabList").contains(newNotebookName).should("exist"); cy.get(".tabList")
.contains(newNotebookName)
.should("exist");
// Close tab // Close tab
cy.get(`.tabList[title="notebooks/${folder}/${newNotebookName}"]`).find(".cancelButton").click(); cy.get(`.tabList[title="notebooks/${folder}/${newNotebookName}"]`)
.find(".cancelButton")
.click();
// When running from command line, closing the tab is too fast // When running from command line, closing the tab is too fast
cy.get("body").then(($body) => { cy.get("body").then($body => {
if ($body.find(".ms-Dialog-main").length) { if ($body.find(".ms-Dialog-main").length) {
// For some reason, this does not work // For some reason, this does not work
// cy.get(".ms-Dialog-main").contains("Close").click(); // cy.get(".ms-Dialog-main").contains("Close").click();
@@ -88,10 +100,14 @@ context("Resource tree notebook file manipulation", () => {
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`) cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`)
.find("button.treeMenuEllipsis") .find("button.treeMenuEllipsis")
.click(); .click();
cy.get('[data-test="treeComponentMenuItemContainer"]').contains("Delete").click(); cy.get('[data-test="treeComponentMenuItemContainer"]')
.contains("Delete")
.click();
// Confirm // Confirm
cy.get(".ms-Dialog-main").contains("Delete").click(); cy.get(".ms-Dialog-main")
.contains("Delete")
.click();
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("not.exist"); cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("not.exist");
deleteItem(`${folder}/`); deleteItem(`${folder}/`);
@@ -105,8 +121,10 @@ context("Resource tree notebook file manipulation", () => {
clickContextMenuAndSelectOption(`${folder}/`, "New Notebook"); clickContextMenuAndSelectOption(`${folder}/`, "New Notebook");
// Close tab // Close tab
cy.get(`.tabList[title="notebooks/${folder}/${newNotebookName}"]`).find(".cancelButton").click(); cy.get(`.tabList[title="notebooks/${folder}/${newNotebookName}"]`)
cy.get("body").then(($body) => { .find(".cancelButton")
.click();
cy.get("body").then($body => {
if ($body.find(".ms-Dialog-main").length) { if ($body.find(".ms-Dialog-main").length) {
// For some reason, this does not work // For some reason, this does not work
// cy.get(".ms-Dialog-main").contains("Close").click(); // cy.get(".ms-Dialog-main").contains("Close").click();
@@ -122,10 +140,14 @@ context("Resource tree notebook file manipulation", () => {
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`) cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`)
.find("button.treeMenuEllipsis") .find("button.treeMenuEllipsis")
.click(); .click();
cy.get('[data-test="treeComponentMenuItemContainer"]').contains("Rename").click(); cy.get('[data-test="treeComponentMenuItemContainer"]')
.contains("Rename")
.click();
cy.get("#stringInputPane").within(() => { cy.get("#stringInputPane").within(() => {
cy.get('input[name="collectionIdConfirmation"]').clear().type(renamedNotebookName); cy.get('input[name="collectionIdConfirmation"]')
.clear()
.type(renamedNotebookName);
cy.get("form").submit(); 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="${newNotebookName}"]`).should("not.exist");
@@ -135,10 +157,14 @@ context("Resource tree notebook file manipulation", () => {
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${renamedNotebookName}"]`) cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${renamedNotebookName}"]`)
.find("button.treeMenuEllipsis") .find("button.treeMenuEllipsis")
.click(); .click();
cy.get('[data-test="treeComponentMenuItemContainer"]').contains("Delete").click(); cy.get('[data-test="treeComponentMenuItemContainer"]')
.contains("Delete")
.click();
// Confirm // Confirm
cy.get(".ms-Dialog-main").contains("Delete").click(); cy.get(".ms-Dialog-main")
.contains("Delete")
.click();
// Give it time to settle // Give it time to settle
cy.wait(1000); cy.wait(1000);
deleteItem(`${folder}/`); deleteItem(`${folder}/`);

View File

@@ -273,12 +273,77 @@
"any-observable": "^0.3.0" "any-observable": "^0.3.0"
} }
}, },
"@types/sinonjs__fake-timers": { "@types/blob-util": {
"version": "6.0.1", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/blob-util/-/blob-util-1.3.3.tgz",
"integrity": "sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA==", "integrity": "sha512-4ahcL/QDnpjWA2Qs16ZMQif7HjGP2cw3AGjHabybjw7Vm1EKu+cfQN1D78BaZbS1WJNa1opSMF5HNMztx7lR0w==",
"dev": true "dev": true
}, },
"@types/bluebird": {
"version": "3.5.29",
"resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.29.tgz",
"integrity": "sha512-kmVtnxTuUuhCET669irqQmPAez4KFnFVKvpleVRyfC3g+SHD1hIkFZcWLim9BVcwUBLO59o8VZE4yGCmTif8Yw==",
"dev": true
},
"@types/chai": {
"version": "4.2.7",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.7.tgz",
"integrity": "sha512-luq8meHGYwvky0O7u0eQZdA7B4Wd9owUCqvbw2m3XCrCU8mplYOujMBbvyS547AxJkC+pGnd0Cm15eNxEUNU8g==",
"dev": true
},
"@types/chai-jquery": {
"version": "1.1.40",
"resolved": "https://registry.npmjs.org/@types/chai-jquery/-/chai-jquery-1.1.40.tgz",
"integrity": "sha512-mCNEZ3GKP7T7kftKeIs7QmfZZQM7hslGSpYzKbOlR2a2HCFf9ph4nlMRA9UnuOETeOQYJVhJQK7MwGqNZVyUtQ==",
"dev": true,
"requires": {
"@types/chai": "*",
"@types/jquery": "*"
}
},
"@types/jquery": {
"version": "3.3.31",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.31.tgz",
"integrity": "sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg==",
"dev": true,
"requires": {
"@types/sizzle": "*"
}
},
"@types/lodash": {
"version": "4.14.149",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz",
"integrity": "sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==",
"dev": true
},
"@types/minimatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
"dev": true
},
"@types/mocha": {
"version": "5.2.7",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz",
"integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==",
"dev": true
},
"@types/sinon": {
"version": "7.5.1",
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.5.1.tgz",
"integrity": "sha512-EZQUP3hSZQyTQRfiLqelC9NMWd1kqLcmQE0dMiklxBkgi84T+cHOhnKpgk4NnOWpGX863yE6+IaGnOXUNFqDnQ==",
"dev": true
},
"@types/sinon-chai": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.3.tgz",
"integrity": "sha512-TOUFS6vqS0PVL1I8NGVSNcFaNJtFoyZPXZ5zur+qlhDfOmQECZZM4H4kKgca6O8L+QceX/ymODZASfUfn+y4yQ==",
"dev": true,
"requires": {
"@types/chai": "*",
"@types/sinon": "*"
}
},
"@types/sizzle": { "@types/sizzle": {
"version": "2.3.2", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz", "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz",
@@ -762,15 +827,24 @@
} }
}, },
"cypress": { "cypress": {
"version": "4.8.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-4.8.0.tgz", "resolved": "https://registry.npmjs.org/cypress/-/cypress-4.5.0.tgz",
"integrity": "sha512-Lsff8lF8pq6k/ioNua783tCsxGSLp6gqGXecdIfqCkqjYiOA53XKuEf1CaQJLUVs1dHSf49eDUp/sb620oJjVQ==", "integrity": "sha512-2A4g5FW5d2fHzq8HKUGAMVTnW6P8nlWYQALiCoGN4bqBLvgwhYM/oG9oKc2CS6LnvgHFiKivKzpm9sfk3uU3zQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@cypress/listr-verbose-renderer": "0.4.1", "@cypress/listr-verbose-renderer": "0.4.1",
"@cypress/request": "2.88.5", "@cypress/request": "2.88.5",
"@cypress/xvfb": "1.2.4", "@cypress/xvfb": "1.2.4",
"@types/sinonjs__fake-timers": "6.0.1", "@types/blob-util": "1.3.3",
"@types/bluebird": "3.5.29",
"@types/chai": "4.2.7",
"@types/chai-jquery": "1.1.40",
"@types/jquery": "3.3.31",
"@types/lodash": "4.14.149",
"@types/minimatch": "3.0.3",
"@types/mocha": "5.2.7",
"@types/sinon": "7.5.1",
"@types/sinon-chai": "3.2.3",
"@types/sizzle": "2.3.2", "@types/sizzle": "2.3.2",
"arch": "2.1.1", "arch": "2.1.1",
"bluebird": "3.7.2", "bluebird": "3.7.2",
@@ -804,6 +878,14 @@
"untildify": "4.0.0", "untildify": "4.0.0",
"url": "0.11.0", "url": "0.11.0",
"yauzl": "2.10.0" "yauzl": "2.10.0"
},
"dependencies": {
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
}
} }
}, },
"dashdash": { "dashdash": {
@@ -999,6 +1081,12 @@
"ms": "2.0.0" "ms": "2.0.0"
} }
}, },
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
},
"mkdirp": { "mkdirp": {
"version": "0.5.5", "version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
@@ -1771,12 +1859,6 @@
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
}, },
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
},
"mkdirp": { "mkdirp": {
"version": "0.5.1", "version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",

View File

@@ -5,13 +5,11 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "cypress run", "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 --headless --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 chrome --headless", "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 chrome --headless",
"test:debug": "cypress open" "test:debug": "cypress open"
}, },
"devDependencies": { "devDependencies": {
"cypress": "^4.8.0", "cypress": "^4.5.0",
"mocha": "^7.0.1", "mocha": "^7.0.1",
"mochawesome": "^4.1.0", "mochawesome": "^4.1.0",
"mochawesome-merge": "^4.0.1", "mochawesome-merge": "^4.0.1",

View File

@@ -1,41 +1,56 @@
module.exports = { module.exports = {
loginUsingConnectionString: function() { loginUsingConnectionString: function (api) {
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 prodUrl = "https://cosmos.azure.com/";
const $body = $element.contents().find("body"); 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('connectionString')[api];
cy.wrap($body) cy.wrap($body)
.find("#connectExplorer") .find("input[class='inputToken']")
.should("exist") .should("exist")
.find("div[class='connectExplorer']") .type(secret, {
.should("exist") force: true
.find("p[class='welcomeText']") });
.should("exist");
cy.wrap($body.find("input[value='Connect']"), { timeout })
.first()
.click({ force: true });
cy.wait(15000);
cy.wrap($body)
.find(".connectExplorer > p:nth-child(3)")
.should("be.visible");
cy.wrap($body.find("div > p.switchConnectTypeText")) });
.should("exist") },
.last() constants:{
.click({ force: true }); sql: "sql",
mongo: "mongo",
table: "table",
graph: "graph",
cassandra: "cassandra"
}
const secret = Cypress.env("CONNECTION_STRING"); }
cy.wrap($body)
.find("input[class='inputToken']")
.should("exist")
.type(secret, {
force: true
});
cy.wrap($body.find("input[value='Connect']"), { timeout })
.first()
.click({ force: true });
cy.wait(15000);
});
}
};

View File

@@ -1,9 +0,0 @@
const isCI = require("is-ci");
module.exports = {
launch: {
headless: isCI,
slowMo: isCI ? null : 20,
defaultViewport: null,
},
};

View File

@@ -1,5 +0,0 @@
module.exports = {
preset: "jest-puppeteer",
testMatch: ["<rootDir>/test/**/*.spec.[jt]s?(x)"],
setupFiles: ["dotenv/config"],
};

View File

@@ -42,8 +42,8 @@ module.exports = {
branches: 18, branches: 18,
functions: 22, functions: 22,
lines: 28, lines: 28,
statements: 27, statements: 27
}, }
}, },
// Make calling deprecated APIs throw helpful error messages // Make calling deprecated APIs throw helpful error messages
@@ -76,7 +76,7 @@ module.exports = {
"office-ui-fabric-react/lib/(.*)$": "office-ui-fabric-react/lib-commonjs/$1", // https://github.com/OfficeDev/office-ui-fabric-react/wiki/Fabric-6-Release-Notes "office-ui-fabric-react/lib/(.*)$": "office-ui-fabric-react/lib-commonjs/$1", // https://github.com/OfficeDev/office-ui-fabric-react/wiki/Fabric-6-Release-Notes
"^dnd-core$": "dnd-core/dist/cjs", "^dnd-core$": "dnd-core/dist/cjs",
"^react-dnd$": "react-dnd/dist/cjs", "^react-dnd$": "react-dnd/dist/cjs",
"^react-dnd-html5-backend$": "react-dnd-html5-backend/dist/cjs", "^react-dnd-html5-backend$": "react-dnd-html5-backend/dist/cjs"
}, },
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
@@ -150,7 +150,7 @@ module.exports = {
// testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?|ts?)$", // testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?|ts?)$",
// This option allows the use of a custom results processor // This option allows the use of a custom results processor
// testResultsProcessor: "./trxProcessor.js", testResultsProcessor: "./trxProcessor.js",
// This option allows use of a custom test runner // This option allows use of a custom test runner
// testRunner: "jasmine2", // testRunner: "jasmine2",
@@ -164,11 +164,11 @@ module.exports = {
// A map from regular expressions to paths to transformers // A map from regular expressions to paths to transformers
transform: { transform: {
"^.+\\.html?$": "html-loader-jest", "^.+\\.html?$": "html-loader-jest",
"^.+\\.[t|j]sx?$": "babel-jest", "^.+\\.[t|j]sx?$": "babel-jest"
}, },
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
transformIgnorePatterns: ["/node_modules/", "/externals/"], transformIgnorePatterns: ["/node_modules/", "/externals/"]
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined, // unmockedModulePathPatterns: undefined,

View File

@@ -54,8 +54,6 @@
@SelectionColor: #3074B0; @SelectionColor: #3074B0;
@FocusColor: #00bcf2;
/****************************************************************************** /******************************************************************************
METRICS METRICS
/******************************************************************************/ /******************************************************************************/
@@ -200,7 +198,7 @@
} }
.focus() { .focus() {
outline: 1px dashed @FocusColor; outline: 1px dashed @AccentMedium;
} }
/************************************************************************************************ /************************************************************************************************

View File

@@ -14,10 +14,6 @@ body {
font-family: @DataExplorerFont; font-family: @DataExplorerFont;
font-size: 12px; font-size: 12px;
height: 100%; height: 100%;
:focus {
.focus()
}
} }
.float-right { .float-right {
@@ -178,7 +174,7 @@ body {
&:active { &:active {
.active(); .active();
} }
&:focus .urlTokenCopyTooltiptext, &:focus .urlTokenCopyTooltiptext { &:focus .urlTokenCopyTooltiptext, &:focus .urlTokenCopyTooltiptext {
.tooltipVisible(); .tooltipVisible();
} }
@@ -366,7 +362,7 @@ body {
} }
.splashLoaderContainer { .splashLoaderContainer {
z-index: 5; z-index: 5;
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
@@ -574,12 +570,6 @@ body {
} }
} }
.fileImportButton {
height: 24px;
border: @ButtonBorderWidth solid transparent;
vertical-align: top;
}
.fileUploadSummaryContainer { .fileUploadSummaryContainer {
margin-top: 40px; margin-top: 40px;
@@ -1026,18 +1016,6 @@ menuQuickStart {
background: #262626; background: #262626;
} }
.panelContent {
display: flex;
flex-direction: column;
flex: 1;
}
.panelContentWrapper {
display: flex;
flex-direction: column;
height: 100%;
}
.contextual-pane { .contextual-pane {
top: 0px; top: 0px;
right: 0 !important; right: 0 !important;
@@ -1254,25 +1232,23 @@ menuQuickStart {
padding: 2px 30px; padding: 2px 30px;
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 12px;
&:active {
border-color: #0072c6;
background-color: #0072c6;
}
} }
.leftpanel-okbut .genericPaneSubmitBtn { .btncreatecoll1:hover {
border: 1px solid @AccentMediumHigh; background: @AccentMediumHigh;
background-color: @AccentMediumHigh;
color: #fff; color: #fff;
border-color: @AccentMediumHigh;
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 12px;
height: 24px; }
&:active { .btncreatecoll1:active {
border-color: #0072c6; border: 1px solid #0072c6;
background-color: #0072c6; background-color: #0072c6;
} color: white;
padding: 2px 30px;
cursor: pointer;
font-size: 12px;
} }
.btncreatecoll1-off { .btncreatecoll1-off {
@@ -1385,15 +1361,6 @@ p {
color: #000; color: #000;
} }
.headerline .closePaneBtn {
float: right;
cursor: pointer;
width: 16px;
height: 100%;
margin-right: 4px;
color: #000;
}
.closeImg { .closeImg {
float: right; float: right;
cursor: pointer; cursor: pointer;
@@ -1453,7 +1420,7 @@ p {
.throughputModeRadio { .throughputModeRadio {
vertical-align: text-bottom; vertical-align: text-bottom;
} }
.nonFirstRadio { .nonFirstRadio {
margin-left: @LargeSpace; margin-left: @LargeSpace;
} }
@@ -1488,7 +1455,7 @@ p {
.largePartitionKeyDescription { .largePartitionKeyDescription {
margin: @DefaultSpace 0px 0px; margin: @DefaultSpace 0px 0px;
} }
} }
.enableAnalyticalStorage { .enableAnalyticalStorage {
@@ -1743,13 +1710,6 @@ input::-webkit-calendar-picker-indicator {
margin: (2 * @MediumSpace) 0px; margin: (2 * @MediumSpace) 0px;
} }
.contextual-pane .panelMainContent {
padding-left: 34px;
padding-right: 34px;
color: @BaseDark;
margin: (2 * @MediumSpace) 0px;
}
.contextual-pane .paneFooter { .contextual-pane .paneFooter {
width: 100%; width: 100%;
height: 60px; height: 60px;
@@ -2220,13 +2180,13 @@ a:link {
.documentsGridHeaderContainer table thead tr { .documentsGridHeaderContainer table thead tr {
position: sticky; position: sticky;
top: 0; top: 0;
th { th {
position: sticky; position: sticky;
top: 0; top: 0;
background-color: #fff !important; background-color: #fff !important;
border-bottom: 1px solid #CCCCCC !important; border-bottom: 1px solid #CCCCCC !important;
} }
} }
.documentsGridHeader { .documentsGridHeader {

9838
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
"description": "Cosmos Explorer", "description": "Cosmos Explorer",
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@azure/cosmos": "3.7.4", "@azure/cosmos": "3.7.0",
"@azure/cosmos-language-service": "0.0.4", "@azure/cosmos-language-service": "0.0.4",
"@jupyterlab/services": "4.2.0", "@jupyterlab/services": "4.2.0",
"@jupyterlab/terminal": "1.2.1", "@jupyterlab/terminal": "1.2.1",
@@ -37,25 +37,22 @@
"@uifabric/react-cards": "0.109.53", "@uifabric/react-cards": "0.109.53",
"@uifabric/styling": "7.11.2", "@uifabric/styling": "7.11.2",
"abort-controller": "3.0.0", "abort-controller": "3.0.0",
"applicationinsights": "1.8.0",
"babel-polyfill": "6.26.0", "babel-polyfill": "6.26.0",
"bootstrap": "3.3.7", "bootstrap": "3.3.7",
"canvas": "2.6.0", "canvas": "2.6.0",
"clean-webpack-plugin": "0.1.19", "clean-webpack-plugin": "0.1.19",
"copy-webpack-plugin": "6.0.2", "copy-webpack-plugin": "4.5.4",
"crossroads": "0.12.2", "crossroads": "0.12.2",
"css-element-queries": "1.1.1", "css-element-queries": "1.1.1",
"datatables.net-colreorder-dt": "1.5.1", "datatables.net-colreorder-dt": "1.5.1",
"datatables.net-dt": "1.10.19", "datatables.net-dt": "1.10.19",
"date-fns": "1.29.0", "date-fns": "1.29.0",
"dayjs": "1.8.19", "dayjs": "1.8.19",
"dotenv": "8.2.0",
"es6-object-assign": "1.1.0", "es6-object-assign": "1.1.0",
"es6-symbol": "3.1.3", "es6-symbol": "3.1.3",
"eslint-plugin-jest": "23.13.2", "eslint-plugin-jest": "23.8.2",
"hasher": "1.2.0", "hasher": "1.2.0",
"immutable": "4.0.0-rc.12", "immutable": "4.0.0-rc.12",
"is-ci": "2.0.0",
"jquery": "3.4.0", "jquery": "3.4.0",
"jquery-typeahead": "2.10.6", "jquery-typeahead": "2.10.6",
"jquery-ui-dist": "1.12.1", "jquery-ui-dist": "1.12.1",
@@ -113,8 +110,8 @@
"@types/text-encoding": "0.0.33", "@types/text-encoding": "0.0.33",
"@types/underscore": "1.7.36", "@types/underscore": "1.7.36",
"@types/webfontloader": "1.6.29", "@types/webfontloader": "1.6.29",
"@typescript-eslint/eslint-plugin": "3.2.0", "@typescript-eslint/eslint-plugin": "2.25.0",
"@typescript-eslint/parser": "3.2.0", "@typescript-eslint/parser": "2.25.0",
"adal-angular": "1.0.15", "adal-angular": "1.0.15",
"babel-jest": "24.9.0", "babel-jest": "24.9.0",
"babel-loader": "8.1.0", "babel-loader": "8.1.0",
@@ -126,10 +123,9 @@
"enzyme": "3.10.0", "enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.15.1", "enzyme-adapter-react-16": "1.15.1",
"enzyme-to-json": "3.4.3", "enzyme-to-json": "3.4.3",
"eslint": "7.3.1", "eslint": "6.8.0",
"eslint-cli": "1.1.1", "eslint-cli": "1.1.1",
"eslint-plugin-no-null": "1.0.2", "eslint-plugin-react": "7.19.0",
"eslint-plugin-react": "7.20.0",
"expose-loader": "0.7.5", "expose-loader": "0.7.5",
"file-loader": "2.0.0", "file-loader": "2.0.0",
"fs-extra": "7.0.0", "fs-extra": "7.0.0",
@@ -137,31 +133,29 @@
"html-loader-jest": "0.2.1", "html-loader-jest": "0.2.1",
"html-webpack-plugin": "3.2.0", "html-webpack-plugin": "3.2.0",
"inline-css": "2.2.5", "inline-css": "2.2.5",
"jest": "25.5.4", "jest": "24.9.0",
"jest-canvas-mock": "2.1.0", "jest-canvas-mock": "2.1.0",
"jest-puppeteer": "4.4.0",
"jest-trx-results-processor": "0.0.7", "jest-trx-results-processor": "0.0.7",
"less": "3.8.1", "less": "3.8.1",
"less-loader": "4.1.0", "less-loader": "4.1.0",
"less-vars-loader": "1.1.0", "less-vars-loader": "1.1.0",
"mini-css-extract-plugin": "0.4.3", "mini-css-extract-plugin": "0.4.3",
"monaco-editor-webpack-plugin": "1.7.0", "monaco-editor-webpack-plugin": "1.7.0",
"prettier": "2.0.5", "prettier": "1.19.1",
"puppeteer": "4.0.0",
"raw-loader": "0.5.1", "raw-loader": "0.5.1",
"rimraf": "3.0.0", "rimraf": "3.0.0",
"sinon": "3.2.1", "sinon": "3.2.1",
"style-loader": "0.23.0", "style-loader": "0.23.0",
"terser-webpack-plugin": "3.0.5", "terser-webpack-plugin": "2.3.5",
"ts-loader": "6.2.2", "ts-loader": "6.2.2",
"tslint": "5.11.0", "tslint": "5.11.0",
"tslint-microsoft-contrib": "6.0.0", "tslint-microsoft-contrib": "6.0.0",
"typescript": "3.8.3", "typescript": "3.8.3",
"url-loader": "1.1.1", "url-loader": "1.1.1",
"webpack": "4.43.0", "webpack": "4.41.2",
"webpack-bundle-analyzer": "3.6.1", "webpack-bundle-analyzer": "3.6.1",
"webpack-cli": "3.3.10", "webpack-cli": "3.3.10",
"webpack-dev-server": "3.11.0", "webpack-dev-server": "3.9.0",
"worker-loader": "2.0.0" "worker-loader": "2.0.0"
}, },
"scripts": { "scripts": {
@@ -174,8 +168,8 @@
"pack:fast": "node --max_old_space_size=10196 ./node_modules/webpack/bin/webpack.js --mode development --progress", "pack:fast": "node --max_old_space_size=10196 ./node_modules/webpack/bin/webpack.js --mode development --progress",
"copyToConsumers": "node copyToConsumers", "copyToConsumers": "node copyToConsumers",
"test": "rimraf coverage && jest", "test": "rimraf coverage && jest",
"test:e2e": "jest -c ./jest.config.e2e.js --detectOpenHandles",
"watch": "npm run start", "watch": "npm run start",
"integrationTest": "runIntegrationTests.cmd",
"build:ase": "gulp build:ase", "build:ase": "gulp build:ase",
"compile": "tsc", "compile": "tsc",
"compile:contracts": "tsc -p ./tsconfig.contracts.json", "compile:contracts": "tsc -p ./tsconfig.contracts.json",

24
runIntegrationTests.cmd Normal file
View File

@@ -0,0 +1,24 @@
@echo off
@for /f "delims=" %%P in ('npm prefix -g') do set "NPM_PREFIX=%%P"
@echo npm prefix = %NPM_PREFIX%
@echo Compiling TypeScript Test Sources ...
call %NPM_PREFIX%\tsc -p ./test
if %errorlevel% neq 0 goto end
copy .\test\Integration\TestRunner.html .\test\out\test\Integration /y >nul 2>&1
@echo Copying files for test simulation against Emulator ...
rmdir "%ProgramFiles%\Azure Cosmos DB Emulator\Packages\DataExplorer\test" >nul 2>&1
mkdir "%ProgramFiles%\Azure Cosmos DB Emulator\Packages\DataExplorer\test" >nul 2>&1
xcopy .\node_modules\jasmine-core\lib .\test\out\lib /s /c /i /r /y >nul 2>&1
xcopy .\node_modules\jasmine-core\images .\test\out\lib\images /s /c /i /r /y >nul 2>&1
xcopy .\test\out "%ProgramFiles%\Azure Cosmos DB Emulator\Packages\DataExplorer\test" /s /c /i /r /y >nul 2>&1
@echo Initiating test runner ...
start https://localhost:8081/_explorer/test/test/Integration/TestRunner.html
@echo Done!
:end
@echo on

View File

@@ -1,13 +1,13 @@
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
export class DefaultApi implements ViewModels.CosmosDbApi { export class DefaultApi implements ViewModels.CosmosDbApi {
public isSystemDatabasePredicate = (database: ViewModels.Database): boolean => { public isSystemDatabasePredicate = (database: ViewModels.Database): boolean => {
return false; return false;
}; };
} }
export class CassandraApi implements ViewModels.CosmosDbApi { export class CassandraApi implements ViewModels.CosmosDbApi {
public isSystemDatabasePredicate = (database: ViewModels.Database): boolean => { public isSystemDatabasePredicate = (database: ViewModels.Database): boolean => {
return database.id() === "system"; return database.id() === "system";
}; };
} }

View File

@@ -1,6 +1,6 @@
export enum AuthType { export enum AuthType {
AAD = "aad", AAD = "aad",
EncryptedToken = "encryptedtoken", EncryptedToken = "encryptedtoken",
MasterKey = "masterkey", MasterKey = "masterkey",
ResourceToken = "resourcetoken", ResourceToken = "resourcetoken"
} }

View File

@@ -1,26 +1,26 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as ReactBindingHandler from "./ReactBindingHandler"; import * as ReactBindingHandler from "./ReactBindingHandler";
interface RestorePoint { interface RestorePoint {
readonly element: JQuery; readonly element: JQuery;
readonly width: number; readonly width: number;
} }
export class BindingHandlersRegisterer { export class BindingHandlersRegisterer {
public static registerBindingHandlers() { public static registerBindingHandlers() {
ko.bindingHandlers.setTemplateReady = { ko.bindingHandlers.setTemplateReady = {
init( init(
element: any, element: any,
wrappedValueAccessor: () => any, wrappedValueAccessor: () => any,
allBindings?: ko.AllBindings, allBindings?: ko.AllBindings,
viewModel?: any, viewModel?: any,
bindingContext?: ko.BindingContext bindingContext?: ko.BindingContext
) { ) {
const value = ko.unwrap(wrappedValueAccessor()); const value = ko.unwrap(wrappedValueAccessor());
bindingContext.$data.isTemplateReady(value); bindingContext.$data.isTemplateReady(value);
}, }
} as ko.BindingHandler; } as ko.BindingHandler;
ReactBindingHandler.Registerer.register(); ReactBindingHandler.Registerer.register();
} }
} }

View File

@@ -42,7 +42,7 @@ export class Registerer {
// Initial rendering at mount point // Initial rendering at mount point
ReactDOM.render(adapter.renderComponent(), element); ReactDOM.render(adapter.renderComponent(), element);
}, }
} as ko.BindingHandler; } as ko.BindingHandler;
} }
} }

View File

@@ -40,7 +40,7 @@ export class ArrayHashMap<T> {
public forEach(key: string, iteratorFct: (value: T) => void) { public forEach(key: string, iteratorFct: (value: T) => void) {
const values = this.store.get(key); const values = this.store.get(key);
if (values) { if (values) {
values.forEach((value) => iteratorFct(value)); values.forEach(value => iteratorFct(value));
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@ describe("tokenProvider", () => {
resourceId: "", resourceId: "",
resourceType: "dbs" as ResourceType, resourceType: "dbs" as ResourceType,
headers: {}, headers: {},
getAuthorizationTokenUsingMasterKey: () => "", getAuthorizationTokenUsingMasterKey: () => ""
}; };
beforeEach(() => { beforeEach(() => {
@@ -17,7 +17,7 @@ describe("tokenProvider", () => {
window.fetch = jest.fn().mockImplementation(() => { window.fetch = jest.fn().mockImplementation(() => {
return { return {
json: () => "{}", json: () => "{}",
headers: new Map(), headers: new Map()
}; };
}); });
}); });
@@ -45,7 +45,7 @@ describe("getTokenFromAuthService", () => {
window.fetch = jest.fn().mockImplementation(() => { window.fetch = jest.fn().mockImplementation(() => {
return { return {
json: () => "{}", json: () => "{}",
headers: new Map(), headers: new Map()
}; };
}); });
}); });
@@ -86,8 +86,8 @@ describe("endpoint", () => {
documentEndpoint: "bar", documentEndpoint: "bar",
gremlinEndpoint: "foo", gremlinEndpoint: "foo",
tableEndpoint: "foo", tableEndpoint: "foo",
cassandraEndpoint: "foo", cassandraEndpoint: "foo"
}, }
}); });
expect(endpoint()).toEqual("bar"); expect(endpoint()).toEqual("bar");
}); });

View File

@@ -63,13 +63,13 @@ export async function getTokenFromAuthService(verb: string, resourceType: string
method: "POST", method: "POST",
headers: { headers: {
"content-type": "application/json", "content-type": "application/json",
"x-ms-encrypted-auth-token": _accessToken, "x-ms-encrypted-auth-token": _accessToken
}, },
body: JSON.stringify({ body: JSON.stringify({
verb, verb,
resourceType, resourceType,
resourceId, resourceId
}), })
}); });
//TODO I am not sure why we have to parse the JSON again here. fetch should do it for us when we call .json() //TODO I am not sure why we have to parse the JSON again here. fetch should do it for us when we call .json()
const result = JSON.parse(await response.json()); const result = JSON.parse(await response.json());
@@ -93,9 +93,9 @@ export const CosmosClient = {
key: _masterKey, key: _masterKey,
tokenProvider, tokenProvider,
connectionPolicy: { connectionPolicy: {
enableEndpointDiscovery: false, enableEndpointDiscovery: false
}, },
userAgentSuffix: "Azure Portal", userAgentSuffix: "Azure Portal"
}; };
// In development we proxy requests to the backend via webpack. This is removed in production bundles. // In development we proxy requests to the backend via webpack. This is removed in production bundles.
@@ -176,5 +176,5 @@ export const CosmosClient = {
_client = null; _client = null;
_resourceToken = value; _resourceToken = value;
return value; return value;
}, }
}; };

View File

@@ -1,13 +1,13 @@
import { getCommonQueryOptions } from "./DataAccessUtilityBase"; import { getCommonQueryOptions } from "./DataAccessUtilityBase";
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
describe("getCommonQueryOptions", () => { describe("getCommonQueryOptions", () => {
it("builds the correct default options objects", () => { it("builds the correct default options objects", () => {
expect(getCommonQueryOptions({})).toMatchSnapshot(); expect(getCommonQueryOptions({})).toMatchSnapshot();
}); });
it("reads from localStorage", () => { it("reads from localStorage", () => {
LocalStorageUtility.setEntryNumber(StorageKey.ActualItemPerPage, 37); LocalStorageUtility.setEntryNumber(StorageKey.ActualItemPerPage, 37);
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, 17); LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, 17);
expect(getCommonQueryOptions({})).toMatchSnapshot(); expect(getCommonQueryOptions({})).toMatchSnapshot();
}); });
}); });

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,94 +1,94 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
export default class EditableUtility { export default class EditableUtility {
public static observable<T>(initialValue?: T): ViewModels.Editable<T> { public static observable<T>(initialValue?: T): ViewModels.Editable<T> {
var observable: ViewModels.Editable<T> = <ViewModels.Editable<T>>ko.observable<T>(initialValue); var observable: ViewModels.Editable<T> = <ViewModels.Editable<T>>ko.observable<T>(initialValue);
observable.edits = ko.observableArray<T>([initialValue]); observable.edits = ko.observableArray<T>([initialValue]);
observable.validations = ko.observableArray<(value: T) => boolean>([]); observable.validations = ko.observableArray<(value: T) => boolean>([]);
observable.setBaseline = (baseline: T) => { observable.setBaseline = (baseline: T) => {
observable(baseline); observable(baseline);
observable.edits([baseline]); observable.edits([baseline]);
}; };
observable.getEditableCurrentValue = ko.computed<T>(() => { observable.getEditableCurrentValue = ko.computed<T>(() => {
const edits = (observable.edits && observable.edits()) || []; const edits = (observable.edits && observable.edits()) || [];
if (edits.length === 0) { if (edits.length === 0) {
return undefined; return undefined;
} }
return edits[edits.length - 1]; return edits[edits.length - 1];
}); });
observable.getEditableOriginalValue = ko.computed<T>(() => { observable.getEditableOriginalValue = ko.computed<T>(() => {
const edits = (observable.edits && observable.edits()) || []; const edits = (observable.edits && observable.edits()) || [];
if (edits.length === 0) { if (edits.length === 0) {
return undefined; return undefined;
} }
return edits[0]; return edits[0];
}); });
observable.editableIsDirty = ko.computed<boolean>(() => { observable.editableIsDirty = ko.computed<boolean>(() => {
const edits = (observable.edits && observable.edits()) || []; const edits = (observable.edits && observable.edits()) || [];
if (edits.length <= 1) { if (edits.length <= 1) {
return false; return false;
} }
let current: any = observable.getEditableCurrentValue(); let current: any = observable.getEditableCurrentValue();
let original: any = observable.getEditableOriginalValue(); let original: any = observable.getEditableOriginalValue();
switch (typeof current) { switch (typeof current) {
case "string": case "string":
case "undefined": case "undefined":
case "number": case "number":
case "boolean": case "boolean":
current = current && current.toString(); current = current && current.toString();
break; break;
default: default:
current = JSON.stringify(current); current = JSON.stringify(current);
break; break;
} }
switch (typeof original) { switch (typeof original) {
case "string": case "string":
case "undefined": case "undefined":
case "number": case "number":
case "boolean": case "boolean":
original = original && original.toString(); original = original && original.toString();
break; break;
default: default:
original = JSON.stringify(original); original = JSON.stringify(original);
break; break;
} }
if (current !== original) { if (current !== original) {
return true; return true;
} }
return false; return false;
}); });
observable.subscribe((edit) => { observable.subscribe(edit => {
var edits = observable.edits && observable.edits(); var edits = observable.edits && observable.edits();
if (!edits) { if (!edits) {
return; return;
} }
edits.push(edit); edits.push(edit);
observable.edits(edits); observable.edits(edits);
}); });
observable.editableIsValid = ko.observable<boolean>(true); observable.editableIsValid = ko.observable<boolean>(true);
observable.subscribe((value) => { observable.subscribe(value => {
const validations: ((value: T) => boolean)[] = (observable.validations && observable.validations()) || []; const validations: ((value: T) => boolean)[] = (observable.validations && observable.validations()) || [];
const isValid = validations.every((validate) => validate(value)); const isValid = validations.every(validate => validate(value));
observable.editableIsValid(isValid); observable.editableIsValid(isValid);
}); });
return observable; return observable;
} }
} }

View File

@@ -1,48 +1,48 @@
import * as Constants from "../Common/Constants"; import * as Constants from "../Common/Constants";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { StringUtils } from "../Utils/StringUtils"; import { StringUtils } from "../Utils/StringUtils";
export default class EnvironmentUtility { export default class EnvironmentUtility {
public static getMongoBackendEndpoint(serverId: string, location: string, extensionEndpoint: string = ""): string { public static getMongoBackendEndpoint(serverId: string, location: string, extensionEndpoint: string = ""): string {
const defaultEnvironment: string = "default"; const defaultEnvironment: string = "default";
const defaultLocation: string = "default"; const defaultLocation: string = "default";
let environment: string = serverId; let environment: string = serverId;
const endpointType: Constants.MongoBackendEndpointType = const endpointType: Constants.MongoBackendEndpointType =
Constants.MongoBackend.endpointsByEnvironment[environment] || Constants.MongoBackend.endpointsByEnvironment[environment] ||
Constants.MongoBackend.endpointsByEnvironment[defaultEnvironment]; Constants.MongoBackend.endpointsByEnvironment[defaultEnvironment];
if (endpointType === Constants.MongoBackendEndpointType.local) { if (endpointType === Constants.MongoBackendEndpointType.local) {
return `${extensionEndpoint}${Constants.MongoBackend.localhostEndpoint}`; return `${extensionEndpoint}${Constants.MongoBackend.localhostEndpoint}`;
} }
const normalizedLocation = EnvironmentUtility.normalizeRegionName(location); const normalizedLocation = EnvironmentUtility.normalizeRegionName(location);
return ( return (
Constants.MongoBackend.endpointsByRegion[normalizedLocation] || Constants.MongoBackend.endpointsByRegion[normalizedLocation] ||
Constants.MongoBackend.endpointsByRegion[defaultLocation] Constants.MongoBackend.endpointsByRegion[defaultLocation]
); );
} }
public static isAadUser(): boolean { public static isAadUser(): boolean {
return window.authType === AuthType.AAD; return window.authType === AuthType.AAD;
} }
public static getCassandraBackendEndpoint(explorer: ViewModels.Explorer): string { public static getCassandraBackendEndpoint(explorer: ViewModels.Explorer): string {
const defaultLocation: string = "default"; const defaultLocation: string = "default";
const location: string = EnvironmentUtility.normalizeRegionName(explorer.databaseAccount().location); const location: string = EnvironmentUtility.normalizeRegionName(explorer.databaseAccount().location);
return ( return (
Constants.CassandraBackend.endpointsByRegion[location] || Constants.CassandraBackend.endpointsByRegion[location] ||
Constants.CassandraBackend.endpointsByRegion[defaultLocation] Constants.CassandraBackend.endpointsByRegion[defaultLocation]
); );
} }
public static normalizeArmEndpointUri(uri: string): string { public static normalizeArmEndpointUri(uri: string): string {
if (uri && uri.slice(-1) !== "/") { if (uri && uri.slice(-1) !== "/") {
return `${uri}/`; return `${uri}/`;
} }
return uri; return uri;
} }
private static normalizeRegionName(region: string): string { private static normalizeRegionName(region: string): string {
return region && StringUtils.stripSpacesFromString(region.toLocaleLowerCase()); return region && StringUtils.stripSpacesFromString(region.toLocaleLowerCase());
} }
} }

View File

@@ -11,7 +11,7 @@ describe("Error Parser Utility", () => {
err.code = 400; err.code = 400;
err.body = { err.body = {
code: "BadRequest", code: "BadRequest",
message, message
}; };
err.headers = {}; err.headers = {};
err.activityId = "97b2e684-7505-4921-85f6-2513b9b28220"; err.activityId = "97b2e684-7505-4921-85f6-2513b9b28220";

View File

@@ -26,7 +26,7 @@ function _parse(err: any): DataModels.ErrorDataModel[] {
normalizedErrors.push(err); normalizedErrors.push(err);
} else { } else {
const innerErrors: any[] = _getInnerErrors(err.message); const innerErrors: any[] = _getInnerErrors(err.message);
normalizedErrors = innerErrors.map((innerError) => normalizedErrors = innerErrors.map(innerError =>
typeof innerError === "string" ? { message: innerError } : innerError typeof innerError === "string" ? { message: innerError } : innerError
); );
} }

View File

@@ -1,25 +1,25 @@
import * as HeadersUtility from "./HeadersUtility"; import * as HeadersUtility from "./HeadersUtility";
import { ExplorerSettings } from "../Shared/ExplorerSettings"; import { ExplorerSettings } from "../Shared/ExplorerSettings";
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
describe("Headers Utility", () => { describe("Headers Utility", () => {
describe("shouldEnableCrossPartitionKeyForResourceWithPartitionKey()", () => { describe("shouldEnableCrossPartitionKeyForResourceWithPartitionKey()", () => {
beforeEach(() => { beforeEach(() => {
ExplorerSettings.createDefaultSettings(); ExplorerSettings.createDefaultSettings();
}); });
it("should return true by default", () => { it("should return true by default", () => {
expect(HeadersUtility.shouldEnableCrossPartitionKey()).toBe(true); expect(HeadersUtility.shouldEnableCrossPartitionKey()).toBe(true);
}); });
it("should return false if the enable cross partition key feed option is false", () => { it("should return false if the enable cross partition key feed option is false", () => {
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, "false"); LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, "false");
expect(HeadersUtility.shouldEnableCrossPartitionKey()).toBe(false); expect(HeadersUtility.shouldEnableCrossPartitionKey()).toBe(false);
}); });
it("should return true if the enable cross partition key feed option is true", () => { it("should return true if the enable cross partition key feed option is true", () => {
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, "true"); LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, "true");
expect(HeadersUtility.shouldEnableCrossPartitionKey()).toBe(true); expect(HeadersUtility.shouldEnableCrossPartitionKey()).toBe(true);
}); });
}); });
}); });

View File

@@ -1,28 +1,28 @@
import * as Constants from "./Constants"; import * as Constants from "./Constants";
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
// x-ms-resource-quota: databases = 100; collections = 5000; users = 500000; permissions = 2000000; // x-ms-resource-quota: databases = 100; collections = 5000; users = 500000; permissions = 2000000;
export function getQuota(responseHeaders: any): any { export function getQuota(responseHeaders: any): any {
return responseHeaders && responseHeaders[Constants.HttpHeaders.resourceQuota] return responseHeaders && responseHeaders[Constants.HttpHeaders.resourceQuota]
? parseStringIntoObject(responseHeaders[Constants.HttpHeaders.resourceQuota]) ? parseStringIntoObject(responseHeaders[Constants.HttpHeaders.resourceQuota])
: null; : null;
} }
export function shouldEnableCrossPartitionKey(): boolean { export function shouldEnableCrossPartitionKey(): boolean {
return LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"; return LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true";
} }
function parseStringIntoObject(resourceString: string) { function parseStringIntoObject(resourceString: string) {
var entityObject: any = {}; var entityObject: any = {};
if (resourceString) { if (resourceString) {
var entitiesArray: string[] = resourceString.split(";"); var entitiesArray: string[] = resourceString.split(";");
for (var i: any = 0; i < entitiesArray.length; i++) { for (var i: any = 0; i < entitiesArray.length; i++) {
var entity: string[] = entitiesArray[i].split("="); var entity: string[] = entitiesArray[i].split("=");
entityObject[entity[0]] = entity[1]; entityObject[entity[0]] = entity[1];
} }
} }
return entityObject; return entityObject;
} }

View File

@@ -11,8 +11,8 @@ describe("nextPage", () => {
queryMetrics: {}, queryMetrics: {},
requestCharge: 1, requestCharge: 1,
headers: {}, headers: {},
activityId: "foo", activityId: "foo"
}), })
}; };
expect(await nextPage(fakeIterator, 10)).toMatchSnapshot(); expect(await nextPage(fakeIterator, 10)).toMatchSnapshot();

View File

@@ -14,7 +14,7 @@ export interface MinimalQueryIterator {
// Pick<QueryIterator<any>, "fetchNext">; // Pick<QueryIterator<any>, "fetchNext">;
export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> { export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> {
return documentsIterator.fetchNext().then((response) => { return documentsIterator.fetchNext().then(response => {
const documents = response.resources; const documents = response.resources;
const headers = (response as any).headers || {}; // TODO this is a private key. Remove any const headers = (response as any).headers || {}; // TODO this is a private key. Remove any
const itemCount = (documents && documents.length) || 0; const itemCount = (documents && documents.length) || 0;
@@ -26,7 +26,7 @@ export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex
lastItemIndex: Number(firstItemIndex) + Number(itemCount), lastItemIndex: Number(firstItemIndex) + Number(itemCount),
headers, headers,
activityId: response.activityId, activityId: response.activityId,
requestCharge: response.requestCharge, requestCharge: response.requestCharge
}; };
}); });
} }

View File

@@ -1,46 +1,46 @@
import { LogEntryLevel } from "../Contracts/Diagnostics"; import { LogEntryLevel } from "../Contracts/Diagnostics";
import * as Logger from "./Logger"; import { Logger } from "./Logger";
import { MessageHandler } from "./MessageHandler"; import { MessageHandler } from "./MessageHandler";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
describe("Logger", () => { describe("Logger", () => {
let sendMessageSpy: jasmine.Spy; let sendMessageSpy: jasmine.Spy;
beforeEach(() => { beforeEach(() => {
sendMessageSpy = spyOn(MessageHandler, "sendMessage"); sendMessageSpy = spyOn(MessageHandler, "sendMessage");
}); });
afterEach(() => { afterEach(() => {
sendMessageSpy = null; sendMessageSpy = null;
}); });
it("should log info messages", () => { it("should log info messages", () => {
Logger.logInfo("Test info", "DocDB"); Logger.logInfo("Test info", "DocDB");
const spyArgs = sendMessageSpy.calls.mostRecent().args[0]; const spyArgs = sendMessageSpy.calls.mostRecent().args[0];
expect(spyArgs.type).toBe(MessageTypes.LogInfo); expect(spyArgs.type).toBe(MessageTypes.LogInfo);
expect(spyArgs.data).toContain(LogEntryLevel.Verbose); expect(spyArgs.data).toContain(LogEntryLevel.Verbose);
expect(spyArgs.data).toContain("DocDB"); expect(spyArgs.data).toContain("DocDB");
expect(spyArgs.data).toContain("Test info"); expect(spyArgs.data).toContain("Test info");
}); });
it("should log error messages", () => { it("should log error messages", () => {
Logger.logError("Test error", "DocDB"); Logger.logError("Test error", "DocDB");
const spyArgs = sendMessageSpy.calls.mostRecent().args[0]; const spyArgs = sendMessageSpy.calls.mostRecent().args[0];
expect(spyArgs.type).toBe(MessageTypes.LogInfo); expect(spyArgs.type).toBe(MessageTypes.LogInfo);
expect(spyArgs.data).toContain(LogEntryLevel.Error); expect(spyArgs.data).toContain(LogEntryLevel.Error);
expect(spyArgs.data).toContain("DocDB"); expect(spyArgs.data).toContain("DocDB");
expect(spyArgs.data).toContain("Test error"); expect(spyArgs.data).toContain("Test error");
}); });
it("should log warnings", () => { it("should log warnings", () => {
Logger.logWarning("Test warning", "DocDB"); Logger.logWarning("Test warning", "DocDB");
const spyArgs = sendMessageSpy.calls.mostRecent().args[0]; const spyArgs = sendMessageSpy.calls.mostRecent().args[0];
expect(spyArgs.type).toBe(MessageTypes.LogInfo); expect(spyArgs.type).toBe(MessageTypes.LogInfo);
expect(spyArgs.data).toContain(LogEntryLevel.Warning); expect(spyArgs.data).toContain(LogEntryLevel.Warning);
expect(spyArgs.data).toContain("DocDB"); expect(spyArgs.data).toContain("DocDB");
expect(spyArgs.data).toContain("Test warning"); expect(spyArgs.data).toContain("Test warning");
}); });
}); });

View File

@@ -4,68 +4,84 @@ import { appInsights } from "../Shared/appInsights";
import { SeverityLevel } from "@microsoft/applicationinsights-web"; import { SeverityLevel } from "@microsoft/applicationinsights-web";
// TODO: Move to a separate Diagnostics folder // TODO: Move to a separate Diagnostics folder
// eslint-disable-next-line @typescript-eslint/no-explicit-any export class Logger {
export function logInfo(message: string | Record<string, any>, area: string, code?: number): void { public static logInfo(message: string | Record<string, any>, area: string, code?: number): void {
let logMessage: string; let logMessage: string;
if (typeof message === "string") { if (typeof message === "string") {
logMessage = message; logMessage = message;
} else { } else {
logMessage = JSON.stringify(message, Object.getOwnPropertyNames(message)); logMessage = JSON.stringify(message, Object.getOwnPropertyNames(message));
}
const entry: Diagnostics.LogEntry = _generateLogEntry(Diagnostics.LogEntryLevel.Verbose, logMessage, area, code);
return _logEntry(entry);
}
export function logWarning(message: string, area: string, code?: number): void {
const entry: Diagnostics.LogEntry = _generateLogEntry(Diagnostics.LogEntryLevel.Warning, message, area, code);
return _logEntry(entry);
}
export function logError(message: string | Error, area: string, code?: number): void {
let logMessage: string;
if (typeof message === "string") {
logMessage = message;
} else {
logMessage = JSON.stringify(message, Object.getOwnPropertyNames(message));
}
const entry: Diagnostics.LogEntry = _generateLogEntry(Diagnostics.LogEntryLevel.Error, logMessage, area, code);
return _logEntry(entry);
}
function _logEntry(entry: Diagnostics.LogEntry): void {
MessageHandler.sendMessage({
type: MessageTypes.LogInfo,
data: JSON.stringify(entry),
});
const severityLevel = ((level: Diagnostics.LogEntryLevel): SeverityLevel => {
switch (level) {
case Diagnostics.LogEntryLevel.Custom:
case Diagnostics.LogEntryLevel.Debug:
case Diagnostics.LogEntryLevel.Verbose:
return SeverityLevel.Verbose;
case Diagnostics.LogEntryLevel.Warning:
return SeverityLevel.Warning;
case Diagnostics.LogEntryLevel.Error:
return SeverityLevel.Error;
default:
return SeverityLevel.Information;
} }
})(entry.level); const entry: Diagnostics.LogEntry = Logger._generateLogEntry(
appInsights.trackTrace({ message: entry.message, severityLevel }, { area: entry.area }); Diagnostics.LogEntryLevel.Verbose,
} logMessage,
area,
code
);
return Logger._logEntry(entry);
}
function _generateLogEntry( public static logWarning(message: string, area: string, code?: number): void {
level: Diagnostics.LogEntryLevel, const entry: Diagnostics.LogEntry = Logger._generateLogEntry(
message: string, Diagnostics.LogEntryLevel.Warning,
area: string, message,
code: number area,
): Diagnostics.LogEntry { code
return { );
timestamp: new Date().getUTCSeconds(), return Logger._logEntry(entry);
level: level, }
message: message,
area: area, public static logError(message: string | Error, area: string, code?: number): void {
code: code, let logMessage: string;
}; if (typeof message === "string") {
logMessage = message;
} else {
logMessage = JSON.stringify(message, Object.getOwnPropertyNames(message));
}
const entry: Diagnostics.LogEntry = Logger._generateLogEntry(
Diagnostics.LogEntryLevel.Error,
logMessage,
area,
code
);
return Logger._logEntry(entry);
}
private static _logEntry(entry: Diagnostics.LogEntry): void {
MessageHandler.sendMessage({
type: MessageTypes.LogInfo,
data: JSON.stringify(entry)
});
const severityLevel = ((level: Diagnostics.LogEntryLevel): SeverityLevel => {
switch (level) {
case Diagnostics.LogEntryLevel.Custom:
case Diagnostics.LogEntryLevel.Debug:
case Diagnostics.LogEntryLevel.Verbose:
return SeverityLevel.Verbose;
case Diagnostics.LogEntryLevel.Warning:
return SeverityLevel.Warning;
case Diagnostics.LogEntryLevel.Error:
return SeverityLevel.Error;
default:
return SeverityLevel.Information;
}
})(entry.level);
appInsights.trackTrace({ message: entry.message, severityLevel }, { area: entry.area });
}
private static _generateLogEntry(
level: Diagnostics.LogEntryLevel,
message: string,
area: string,
code: number
): Diagnostics.LogEntry {
return {
timestamp: new Date().getUTCSeconds(),
level: level,
message: message,
area: area,
code: code
};
}
} }

View File

@@ -1,65 +1,65 @@
import Q from "q"; import Q from "q";
import { CachedDataPromise, MessageHandler } from "./MessageHandler"; import { CachedDataPromise, MessageHandler } from "./MessageHandler";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
class MockMessageHandler extends MessageHandler { class MockMessageHandler extends MessageHandler {
public static addToMap(key: string, value: CachedDataPromise<any>): void { public static addToMap(key: string, value: CachedDataPromise<any>): void {
MessageHandler.RequestMap[key] = value; MessageHandler.RequestMap[key] = value;
} }
public static mapContainsKey(key: string): boolean { public static mapContainsKey(key: string): boolean {
return MessageHandler.RequestMap[key] != null; return MessageHandler.RequestMap[key] != null;
} }
public static clearAllEntries(): void { public static clearAllEntries(): void {
MessageHandler.RequestMap = {}; MessageHandler.RequestMap = {};
} }
public static runGarbageCollector(): void { public static runGarbageCollector(): void {
MessageHandler.runGarbageCollector(); MessageHandler.runGarbageCollector();
} }
} }
describe("Message Handler", () => { describe("Message Handler", () => {
beforeEach(() => { beforeEach(() => {
MockMessageHandler.clearAllEntries(); MockMessageHandler.clearAllEntries();
}); });
xit("should send cached data message", (done: any) => { xit("should send cached data message", (done: any) => {
const testValidationCallback = (e: MessageEvent) => { const testValidationCallback = (e: MessageEvent) => {
expect(e.data.data).toEqual( expect(e.data.data).toEqual(
jasmine.objectContaining({ type: MessageTypes.AllDatabases, params: ["some param"] }) jasmine.objectContaining({ type: MessageTypes.AllDatabases, params: ["some param"] })
); );
e.currentTarget.removeEventListener(e.type, testValidationCallback); e.currentTarget.removeEventListener(e.type, testValidationCallback);
done(); done();
}; };
window.parent.addEventListener("message", testValidationCallback); window.parent.addEventListener("message", testValidationCallback);
MockMessageHandler.sendCachedDataMessage(MessageTypes.AllDatabases, ["some param"]); MockMessageHandler.sendCachedDataMessage(MessageTypes.AllDatabases, ["some param"]);
}); });
it("should handle cached message", () => { it("should handle cached message", () => {
let mockPromise: CachedDataPromise<any> = { let mockPromise: CachedDataPromise<any> = {
id: "123", id: "123",
startTime: new Date(), startTime: new Date(),
deferred: Q.defer<any>(), deferred: Q.defer<any>()
}; };
let mockMessage = { message: { id: "123", data: "{}" } }; let mockMessage = { message: { id: "123", data: "{}" } };
MockMessageHandler.addToMap(mockPromise.id, mockPromise); MockMessageHandler.addToMap(mockPromise.id, mockPromise);
MockMessageHandler.handleCachedDataMessage(mockMessage); MockMessageHandler.handleCachedDataMessage(mockMessage);
expect(mockPromise.deferred.promise.isFulfilled()).toBe(true); expect(mockPromise.deferred.promise.isFulfilled()).toBe(true);
}); });
it("should delete fulfilled promises on running the garbage collector", () => { it("should delete fulfilled promises on running the garbage collector", () => {
let mockPromise: CachedDataPromise<any> = { let mockPromise: CachedDataPromise<any> = {
id: "123", id: "123",
startTime: new Date(), startTime: new Date(),
deferred: Q.defer<any>(), deferred: Q.defer<any>()
}; };
MockMessageHandler.addToMap(mockPromise.id, mockPromise); MockMessageHandler.addToMap(mockPromise.id, mockPromise);
mockPromise.deferred.reject("some error"); mockPromise.deferred.reject("some error");
MockMessageHandler.runGarbageCollector(); MockMessageHandler.runGarbageCollector();
expect(MockMessageHandler.mapContainsKey(mockPromise.id)).toBe(false); expect(MockMessageHandler.mapContainsKey(mockPromise.id)).toBe(false);
}); });
}); });

View File

@@ -1,85 +1,85 @@
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import Q from "q"; import Q from "q";
import * as _ from "underscore"; import * as _ from "underscore";
import * as Constants from "./Constants"; import * as Constants from "./Constants";
export interface CachedDataPromise<T> { export interface CachedDataPromise<T> {
deferred: Q.Deferred<T>; deferred: Q.Deferred<T>;
startTime: Date; startTime: Date;
id: string; id: string;
} }
/** /**
* For some reason, typescript emits a Map() in the compiled js output(despite the target being set to ES5) forcing us to define our own polyfill, * For some reason, typescript emits a Map() in the compiled js output(despite the target being set to ES5) forcing us to define our own polyfill,
* so we define our own custom implementation of the ES6 Map to work around it. * so we define our own custom implementation of the ES6 Map to work around it.
*/ */
type Map = { [key: string]: CachedDataPromise<any> }; type Map = { [key: string]: CachedDataPromise<any> };
export class MessageHandler { export class MessageHandler {
protected static RequestMap: Map = {}; protected static RequestMap: Map = {};
public static handleCachedDataMessage(message: any): void { public static handleCachedDataMessage(message: any): void {
const messageContent = message && message.message; const messageContent = message && message.message;
if ( if (
message == null || message == null ||
messageContent == null || messageContent == null ||
messageContent.id == null || messageContent.id == null ||
!MessageHandler.RequestMap[messageContent.id] !MessageHandler.RequestMap[messageContent.id]
) { ) {
return; return;
} }
const cachedDataPromise = MessageHandler.RequestMap[messageContent.id]; const cachedDataPromise = MessageHandler.RequestMap[messageContent.id];
if (messageContent.error != null) { if (messageContent.error != null) {
cachedDataPromise.deferred.reject(messageContent.error); cachedDataPromise.deferred.reject(messageContent.error);
} else { } else {
cachedDataPromise.deferred.resolve(JSON.parse(messageContent.data)); cachedDataPromise.deferred.resolve(JSON.parse(messageContent.data));
} }
MessageHandler.runGarbageCollector(); MessageHandler.runGarbageCollector();
} }
public static sendCachedDataMessage<TResponseDataModel>( public static sendCachedDataMessage<TResponseDataModel>(
messageType: MessageTypes, messageType: MessageTypes,
params: Object[], params: Object[],
timeoutInMs?: number timeoutInMs?: number
): Q.Promise<TResponseDataModel> { ): Q.Promise<TResponseDataModel> {
let cachedDataPromise: CachedDataPromise<TResponseDataModel> = { let cachedDataPromise: CachedDataPromise<TResponseDataModel> = {
deferred: Q.defer<TResponseDataModel>(), deferred: Q.defer<TResponseDataModel>(),
startTime: new Date(), startTime: new Date(),
id: _.uniqueId(), id: _.uniqueId()
}; };
MessageHandler.RequestMap[cachedDataPromise.id] = cachedDataPromise; MessageHandler.RequestMap[cachedDataPromise.id] = cachedDataPromise;
MessageHandler.sendMessage({ type: messageType, params: params, id: cachedDataPromise.id }); MessageHandler.sendMessage({ type: messageType, params: params, id: cachedDataPromise.id });
//TODO: Use telemetry to measure optimal time to resolve/reject promises //TODO: Use telemetry to measure optimal time to resolve/reject promises
return cachedDataPromise.deferred.promise.timeout( return cachedDataPromise.deferred.promise.timeout(
timeoutInMs || Constants.ClientDefaults.requestTimeoutMs, timeoutInMs || Constants.ClientDefaults.requestTimeoutMs,
"Timed out while waiting for response from portal" "Timed out while waiting for response from portal"
); );
} }
public static sendMessage(data: any): void { public static sendMessage(data: any): void {
if (MessageHandler.canSendMessage()) { if (MessageHandler.canSendMessage()) {
window.parent.postMessage( window.parent.postMessage(
{ {
signature: "pcIframe", signature: "pcIframe",
data: data, data: data
}, },
window.document.referrer window.document.referrer
); );
} }
} }
public static canSendMessage(): boolean { public static canSendMessage(): boolean {
return window.parent !== window; return window.parent !== window;
} }
protected static runGarbageCollector() { protected static runGarbageCollector() {
Object.keys(MessageHandler.RequestMap).forEach((key: string) => { Object.keys(MessageHandler.RequestMap).forEach((key: string) => {
const promise: Q.Promise<any> = MessageHandler.RequestMap[key].deferred.promise; const promise: Q.Promise<any> = MessageHandler.RequestMap[key].deferred.promise;
if (promise.isFulfilled() || promise.isRejected()) { if (promise.isFulfilled() || promise.isRejected()) {
delete MessageHandler.RequestMap[key]; delete MessageHandler.RequestMap[key];
} }
}); });
} }
} }

View File

@@ -4,7 +4,7 @@ import {
getEndpoint, getEndpoint,
queryDocuments, queryDocuments,
readDocument, readDocument,
updateDocument, updateDocument
} from "./MongoProxyClient"; } from "./MongoProxyClient";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { Collection, DatabaseAccount, DocumentId } from "../Contracts/ViewModels"; import { Collection, DatabaseAccount, DocumentId } from "../Contracts/ViewModels";
@@ -20,7 +20,7 @@ const fetchMock = () => {
ok: true, ok: true,
text: () => "{}", text: () => "{}",
json: () => "{}", json: () => "{}",
headers: new Map(), headers: new Map()
}); });
}; };
@@ -33,8 +33,8 @@ const collection = {
partitionKey: { partitionKey: {
paths: ["/pk"], paths: ["/pk"],
kind: "Hash", kind: "Hash",
version: 1, version: 1
}, }
} as Collection; } as Collection;
const documentId = ({ const documentId = ({
@@ -44,8 +44,8 @@ const documentId = ({
partitionKey: { partitionKey: {
paths: ["/pk"], paths: ["/pk"],
kind: "Hash", kind: "Hash",
version: 1, version: 1
}, }
} as unknown) as DocumentId; } as unknown) as DocumentId;
const databaseAccount = { const databaseAccount = {
@@ -58,8 +58,8 @@ const databaseAccount = {
documentEndpoint: "bar", documentEndpoint: "bar",
gremlinEndpoint: "foo", gremlinEndpoint: "foo",
tableEndpoint: "foo", tableEndpoint: "foo",
cassandraEndpoint: "foo", cassandraEndpoint: "foo"
}, }
}; };
describe("MongoProxyClient", () => { describe("MongoProxyClient", () => {
@@ -69,7 +69,7 @@ describe("MongoProxyClient", () => {
CosmosClient.databaseAccount(databaseAccount as any); CosmosClient.databaseAccount(databaseAccount as any);
window.dataExplorer = { window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com", extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => "", serverId: () => ""
} as any; } as any;
window.fetch = jest.fn().mockImplementation(fetchMock); window.fetch = jest.fn().mockImplementation(fetchMock);
}); });
@@ -100,7 +100,7 @@ describe("MongoProxyClient", () => {
CosmosClient.databaseAccount(databaseAccount as any); CosmosClient.databaseAccount(databaseAccount as any);
window.dataExplorer = { window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com", extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => "", serverId: () => ""
} as any; } as any;
window.fetch = jest.fn().mockImplementation(fetchMock); window.fetch = jest.fn().mockImplementation(fetchMock);
}); });
@@ -131,7 +131,7 @@ describe("MongoProxyClient", () => {
CosmosClient.databaseAccount(databaseAccount as any); CosmosClient.databaseAccount(databaseAccount as any);
window.dataExplorer = { window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com", extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => "", serverId: () => ""
} as any; } as any;
window.fetch = jest.fn().mockImplementation(fetchMock); window.fetch = jest.fn().mockImplementation(fetchMock);
}); });
@@ -162,7 +162,7 @@ describe("MongoProxyClient", () => {
CosmosClient.databaseAccount(databaseAccount as any); CosmosClient.databaseAccount(databaseAccount as any);
window.dataExplorer = { window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com", extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => "", serverId: () => ""
} as any; } as any;
window.fetch = jest.fn().mockImplementation(fetchMock); window.fetch = jest.fn().mockImplementation(fetchMock);
}); });
@@ -193,7 +193,7 @@ describe("MongoProxyClient", () => {
CosmosClient.databaseAccount(databaseAccount as any); CosmosClient.databaseAccount(databaseAccount as any);
window.dataExplorer = { window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com", extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => "", serverId: () => ""
} as any; } as any;
window.fetch = jest.fn().mockImplementation(fetchMock); window.fetch = jest.fn().mockImplementation(fetchMock);
}); });
@@ -225,7 +225,7 @@ describe("MongoProxyClient", () => {
CosmosClient.databaseAccount(databaseAccount as any); CosmosClient.databaseAccount(databaseAccount as any);
window.dataExplorer = { window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com", extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => "", serverId: () => ""
} as any; } as any;
}); });
@@ -259,7 +259,7 @@ describe("MongoProxyClient", () => {
sid: "a2", sid: "a2",
rg: "c1", rg: "c1",
dba: "main", dba: "main",
is: false, is: false
}; };
_createMongoCollectionWithARM("management.azure.com", properties, { "x-ms-cosmos-offer-autopilot-tier": "1" }); _createMongoCollectionWithARM("management.azure.com", properties, { "x-ms-cosmos-offer-autopilot-tier": "1" });
expect(resourceProviderClientPutAsyncSpy).toHaveBeenCalledWith( expect(resourceProviderClientPutAsyncSpy).toHaveBeenCalledWith(
@@ -268,8 +268,8 @@ describe("MongoProxyClient", () => {
{ {
properties: { properties: {
options: { "x-ms-cosmos-offer-autopilot-tier": "1" }, options: { "x-ms-cosmos-offer-autopilot-tier": "1" },
resource: { id: "abc-collection" }, resource: { id: "abc-collection" }
}, }
} }
); );
}); });
@@ -285,7 +285,7 @@ describe("MongoProxyClient", () => {
rg: "c1", rg: "c1",
dba: "main", dba: "main",
is: false, is: false,
offerThroughput: 400, offerThroughput: 400
}; };
_createMongoCollectionWithARM("management.azure.com", properties, undefined); _createMongoCollectionWithARM("management.azure.com", properties, undefined);
expect(resourceProviderClientPutAsyncSpy).toHaveBeenCalledWith( expect(resourceProviderClientPutAsyncSpy).toHaveBeenCalledWith(
@@ -294,8 +294,8 @@ describe("MongoProxyClient", () => {
{ {
properties: { properties: {
options: { throughput: "400" }, options: { throughput: "400" },
resource: { id: "abc-collection" }, resource: { id: "abc-collection" }
}, }
} }
); );
}); });

View File

@@ -20,7 +20,7 @@ import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClie
const defaultHeaders = { const defaultHeaders = {
[HttpHeaders.apiType]: ApiType.MongoDB.toString(), [HttpHeaders.apiType]: ApiType.MongoDB.toString(),
[CosmosSDKConstants.HttpHeaders.MaxEntityCount]: "100", [CosmosSDKConstants.HttpHeaders.MaxEntityCount]: "100",
[CosmosSDKConstants.HttpHeaders.Version]: "2017-11-15", [CosmosSDKConstants.HttpHeaders.Version]: "2017-11-15"
}; };
function authHeaders(): any { function authHeaders(): any {
@@ -31,13 +31,13 @@ function authHeaders(): any {
} }
} }
export function queryIterator(databaseId: string, collection: Collection, query: string): any { export function queryIterator(databaseId: string, collection: Collection, query: string) {
let continuationToken: string; let continuationToken: string = null;
return { return {
fetchNext: () => { fetchNext: () => {
return queryDocuments(databaseId, collection, false, query).then((response) => { return queryDocuments(databaseId, collection, false, query).then(response => {
continuationToken = response.continuationToken; continuationToken = response.continuationToken;
const headers = {} as any; let headers = {} as any;
response.headers.forEach((value: any, key: any) => { response.headers.forEach((value: any, key: any) => {
headers[key] = value; headers[key] = value;
}); });
@@ -46,10 +46,10 @@ export function queryIterator(databaseId: string, collection: Collection, query:
headers, headers,
requestCharge: headers[CosmosSDKConstants.HttpHeaders.RequestCharge], requestCharge: headers[CosmosSDKConstants.HttpHeaders.RequestCharge],
activityId: headers[CosmosSDKConstants.HttpHeaders.ActivityId], activityId: headers[CosmosSDKConstants.HttpHeaders.ActivityId],
hasMoreResults: !!continuationToken, hasMoreResults: !!continuationToken
}; };
}); });
}, }
}; };
} }
@@ -78,9 +78,7 @@ export function queryDocuments(
rg: CosmosClient.resourceGroup(), rg: CosmosClient.resourceGroup(),
dba: databaseAccount.name, dba: databaseAccount.name,
pk: pk:
collection && collection.partitionKey && !collection.partitionKey.systemKey collection && collection.partitionKey && !collection.partitionKey.systemKey ? collection.partitionKeyProperty : ""
? collection.partitionKeyProperty
: "",
}; };
const endpoint = getEndpoint(databaseAccount) || ""; const endpoint = getEndpoint(databaseAccount) || "";
@@ -93,7 +91,7 @@ export function queryDocuments(
[CosmosSDKConstants.HttpHeaders.EnableScanInQuery]: "true", [CosmosSDKConstants.HttpHeaders.EnableScanInQuery]: "true",
[CosmosSDKConstants.HttpHeaders.EnableCrossPartitionQuery]: "true", [CosmosSDKConstants.HttpHeaders.EnableCrossPartitionQuery]: "true",
[CosmosSDKConstants.HttpHeaders.ParallelizeCrossPartitionQuery]: "true", [CosmosSDKConstants.HttpHeaders.ParallelizeCrossPartitionQuery]: "true",
[HttpHeaders.contentType]: "application/query+json", [HttpHeaders.contentType]: "application/query+json"
}; };
if (continuationToken) { if (continuationToken) {
@@ -106,17 +104,24 @@ export function queryDocuments(
.fetch(`${endpoint}${path}?${queryString.stringify(params)}`, { .fetch(`${endpoint}${path}?${queryString.stringify(params)}`, {
method: "POST", method: "POST",
body: JSON.stringify({ query }), body: JSON.stringify({ query }),
headers, headers
}) })
.then(async (response) => { .then(async response => {
if (response.ok) { if (response.ok) {
return { return {
continuationToken: response.headers.get(CosmosSDKConstants.HttpHeaders.Continuation), continuationToken: response.headers.get(CosmosSDKConstants.HttpHeaders.Continuation),
documents: (await response.json()).Documents as DataModels.DocumentId[], documents: (await response.json()).Documents as DataModels.DocumentId[],
headers: response.headers, headers: response.headers
}; };
} }
return errorHandling(response, "querying documents", params); const errorMessage = await response.text();
if (response.status === HttpStatusCodes.Forbidden) {
MessageHandler.sendMessage({
type: MessageTypes.ForbiddenError,
reason: errorMessage
});
}
throw new Error(errorMessage);
}); });
} }
@@ -140,9 +145,7 @@ export function readDocument(
rg: CosmosClient.resourceGroup(), rg: CosmosClient.resourceGroup(),
dba: databaseAccount.name, dba: databaseAccount.name,
pk: pk:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
? documentId.partitionKeyProperty
: "",
}; };
const endpoint = getEndpoint(databaseAccount); const endpoint = getEndpoint(databaseAccount);
@@ -154,14 +157,14 @@ export function readDocument(
...authHeaders(), ...authHeaders(),
[CosmosSDKConstants.HttpHeaders.PartitionKey]: encodeURIComponent( [CosmosSDKConstants.HttpHeaders.PartitionKey]: encodeURIComponent(
JSON.stringify(documentId.partitionKeyHeader()) JSON.stringify(documentId.partitionKeyHeader())
), )
}, }
}) })
.then((response) => { .then(async response => {
if (response.ok) { if (response.ok) {
return response.json(); return response.json();
} }
return errorHandling(response, "reading document", params); errorHandling(response);
}); });
} }
@@ -182,7 +185,7 @@ export function createDocument(
sid: CosmosClient.subscriptionId(), sid: CosmosClient.subscriptionId(),
rg: CosmosClient.resourceGroup(), rg: CosmosClient.resourceGroup(),
dba: databaseAccount.name, dba: databaseAccount.name,
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "", pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : ""
}; };
const endpoint = getEndpoint(databaseAccount); const endpoint = getEndpoint(databaseAccount);
@@ -193,14 +196,14 @@ export function createDocument(
body: JSON.stringify(documentContent), body: JSON.stringify(documentContent),
headers: { headers: {
...defaultHeaders, ...defaultHeaders,
...authHeaders(), ...authHeaders()
}, }
}) })
.then((response) => { .then(async response => {
if (response.ok) { if (response.ok) {
return response.json(); return response.json();
} }
return errorHandling(response, "creating document", params); errorHandling(response);
}); });
} }
@@ -225,9 +228,7 @@ export function updateDocument(
rg: CosmosClient.resourceGroup(), rg: CosmosClient.resourceGroup(),
dba: databaseAccount.name, dba: databaseAccount.name,
pk: pk:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
? documentId.partitionKeyProperty
: "",
}; };
const endpoint = getEndpoint(databaseAccount); const endpoint = getEndpoint(databaseAccount);
@@ -239,14 +240,14 @@ export function updateDocument(
...defaultHeaders, ...defaultHeaders,
...authHeaders(), ...authHeaders(),
[HttpHeaders.contentType]: "application/json", [HttpHeaders.contentType]: "application/json",
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()), [CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader())
}, }
}) })
.then((response) => { .then(async response => {
if (response.ok) { if (response.ok) {
return response.json(); return response.json();
} }
return errorHandling(response, "updating document", params); errorHandling(response);
}); });
} }
@@ -270,9 +271,7 @@ export function deleteDocument(
rg: CosmosClient.resourceGroup(), rg: CosmosClient.resourceGroup(),
dba: databaseAccount.name, dba: databaseAccount.name,
pk: pk:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
? documentId.partitionKeyProperty
: "",
}; };
const endpoint = getEndpoint(databaseAccount); const endpoint = getEndpoint(databaseAccount);
@@ -283,14 +282,14 @@ export function deleteDocument(
...defaultHeaders, ...defaultHeaders,
...authHeaders(), ...authHeaders(),
[HttpHeaders.contentType]: "application/json", [HttpHeaders.contentType]: "application/json",
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()), [CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader())
},
})
.then((response) => {
if (response.ok) {
return undefined;
} }
return errorHandling(response, "deleting document", params); })
.then(async response => {
if (response.ok) {
return;
}
errorHandling(response);
}); });
} }
@@ -319,7 +318,7 @@ export function createMongoCollectionWithProxy(
sid: CosmosClient.subscriptionId(), sid: CosmosClient.subscriptionId(),
rg: CosmosClient.resourceGroup(), rg: CosmosClient.resourceGroup(),
dba: databaseAccount.name, dba: databaseAccount.name,
isAutoPilot: false, isAutoPilot: false
}; };
if (autopilotOptions) { if (autopilotOptions) {
@@ -337,15 +336,19 @@ export function createMongoCollectionWithProxy(
headers: { headers: {
...defaultHeaders, ...defaultHeaders,
...authHeaders(), ...authHeaders(),
[HttpHeaders.contentType]: "application/json", [HttpHeaders.contentType]: "application/json"
}, }
} }
) )
.then((response) => { .then(async response => {
if (response.ok) { if (response.ok) {
return undefined; return;
} }
return errorHandling(response, "creating collection", params); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error creating collection: ${await response.json()}, Payload: ${params}`
);
errorHandling(response);
}); });
} }
@@ -376,7 +379,7 @@ export function createMongoCollectionWithARM(
sid: CosmosClient.subscriptionId(), sid: CosmosClient.subscriptionId(),
rg: CosmosClient.resourceGroup(), rg: CosmosClient.resourceGroup(),
dba: databaseAccount.name, dba: databaseAccount.name,
analyticalStorageTtl, analyticalStorageTtl
}; };
if (createDatabase) { if (createDatabase) {
@@ -404,16 +407,13 @@ export function getEndpoint(databaseAccount: ViewModels.DatabaseAccount): string
return url; return url;
} }
async function errorHandling(response: any, action: string, params: any): Promise<any> { async function errorHandling(response: any): Promise<any> {
const errorMessage = await response.text(); const errorMessage = await response.text();
// Log the error where the user can see it
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error ${action}: ${errorMessage}, Payload: ${JSON.stringify(params)}`
);
if (response.status === HttpStatusCodes.Forbidden) { if (response.status === HttpStatusCodes.Forbidden) {
MessageHandler.sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage }); MessageHandler.sendMessage({
return; type: MessageTypes.ForbiddenError,
reason: errorMessage
});
} }
throw new Error(errorMessage); throw new Error(errorMessage);
} }
@@ -432,10 +432,10 @@ export async function _createMongoCollectionWithARM(
const rpPayloadToCreateCollection: DataModels.MongoCreationRequest = { const rpPayloadToCreateCollection: DataModels.MongoCreationRequest = {
properties: { properties: {
resource: { resource: {
id: params.coll, id: params.coll
}, },
options: {}, options: {}
}, }
}; };
if (params.is) { if (params.is) {
@@ -462,6 +462,14 @@ export async function _createMongoCollectionWithARM(
rpPayloadToCreateCollection rpPayloadToCreateCollection
); );
} catch (response) { } catch (response) {
return errorHandling(response, "creating collection", undefined); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error creating collection: ${JSON.stringify(response)}`
);
if (response.status === HttpStatusCodes.Forbidden) {
MessageHandler.sendMessage({ type: MessageTypes.ForbiddenError });
return;
}
throw new Error(`Error creating collection`);
} }
} }

View File

@@ -1,168 +1,168 @@
/* Copyright 2013 10gen Inc. /* Copyright 2013 10gen Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
export default class MongoUtility { export default class MongoUtility {
public static tojson = function (x: any, indent: string, nolint: boolean) { public static tojson = function(x: any, indent: string, nolint: boolean) {
if (x === null || x === undefined) { if (x === null || x === undefined) {
return String(x); return String(x);
} }
indent = indent || ""; indent = indent || "";
switch (typeof x) { switch (typeof x) {
case "string": case "string":
var out = new Array(x.length + 1); var out = new Array(x.length + 1);
out[0] = '"'; out[0] = '"';
for (var i = 0; i < x.length; i++) { for (var i = 0; i < x.length; i++) {
if (x[i] === '"') { if (x[i] === '"') {
out[out.length] = '\\"'; out[out.length] = '\\"';
} else if (x[i] === "\\") { } else if (x[i] === "\\") {
out[out.length] = "\\\\"; out[out.length] = "\\\\";
} else if (x[i] === "\b") { } else if (x[i] === "\b") {
out[out.length] = "\\b"; out[out.length] = "\\b";
} else if (x[i] === "\f") { } else if (x[i] === "\f") {
out[out.length] = "\\f"; out[out.length] = "\\f";
} else if (x[i] === "\n") { } else if (x[i] === "\n") {
out[out.length] = "\\n"; out[out.length] = "\\n";
} else if (x[i] === "\r") { } else if (x[i] === "\r") {
out[out.length] = "\\r"; out[out.length] = "\\r";
} else if (x[i] === "\t") { } else if (x[i] === "\t") {
out[out.length] = "\\t"; out[out.length] = "\\t";
} else { } else {
var code = x.charCodeAt(i); var code = x.charCodeAt(i);
if (code < 0x20) { if (code < 0x20) {
out[out.length] = (code < 0x10 ? "\\u000" : "\\u00") + code.toString(16); out[out.length] = (code < 0x10 ? "\\u000" : "\\u00") + code.toString(16);
} else { } else {
out[out.length] = x[i]; out[out.length] = x[i];
} }
} }
} }
return out.join("") + '"'; return out.join("") + '"';
case "number": case "number":
/* falls through */ /* falls through */
case "boolean": case "boolean":
return "" + x; return "" + x;
case "object": case "object":
var func = $.isArray(x) ? MongoUtility.tojsonArray : MongoUtility.tojsonObject; var func = $.isArray(x) ? MongoUtility.tojsonArray : MongoUtility.tojsonObject;
var s = func(x, indent, nolint); var s = func(x, indent, nolint);
if ( if (
(nolint === null || nolint === undefined || nolint === true) && (nolint === null || nolint === undefined || nolint === true) &&
s.length < 80 && s.length < 80 &&
(indent === null || indent.length === 0) (indent === null || indent.length === 0)
) { ) {
s = s.replace(/[\t\r\n]+/gm, " "); s = s.replace(/[\t\r\n]+/gm, " ");
} }
return s; return s;
case "function": case "function":
return x.toString(); return x.toString();
default: default:
throw new Error("tojson can't handle type " + typeof x); throw new Error("tojson can't handle type " + typeof x);
} }
}; };
private static tojsonObject = function (x: any, indent: string, nolint: boolean) { private static tojsonObject = function(x: any, indent: string, nolint: boolean) {
var lineEnding = nolint ? " " : "\n"; var lineEnding = nolint ? " " : "\n";
var tabSpace = nolint ? "" : "\t"; var tabSpace = nolint ? "" : "\t";
indent = indent || ""; indent = indent || "";
if (typeof x.tojson === "function" && x.tojson !== MongoUtility.tojson) { if (typeof x.tojson === "function" && x.tojson !== MongoUtility.tojson) {
return x.tojson(indent, nolint); return x.tojson(indent, nolint);
} }
if (x.constructor && typeof x.constructor.tojson === "function" && x.constructor.tojson !== MongoUtility.tojson) { if (x.constructor && typeof x.constructor.tojson === "function" && x.constructor.tojson !== MongoUtility.tojson) {
return x.constructor.tojson(x, indent, nolint); return x.constructor.tojson(x, indent, nolint);
} }
if (MongoUtility.hasDefinedProperty(x, "toString") && !$.isArray(x)) { if (MongoUtility.hasDefinedProperty(x, "toString") && !$.isArray(x)) {
return x.toString(); return x.toString();
} }
if (x instanceof Error) { if (x instanceof Error) {
return x.toString(); return x.toString();
} }
if (MongoUtility.isObjectId(x)) { if (MongoUtility.isObjectId(x)) {
return 'ObjectId("' + x.$oid + '")'; return 'ObjectId("' + x.$oid + '")';
} }
// push one level of indent // push one level of indent
indent += tabSpace; indent += tabSpace;
var s = "{"; var s = "{";
var pairs = []; var pairs = [];
for (var k in x) { for (var k in x) {
if (x.hasOwnProperty(k)) { if (x.hasOwnProperty(k)) {
var val = x[k]; var val = x[k];
var pair = '"' + k + '" : ' + MongoUtility.tojson(val, indent, nolint); var pair = '"' + k + '" : ' + MongoUtility.tojson(val, indent, nolint);
if (k === "_id") { if (k === "_id") {
pairs.unshift(pair); pairs.unshift(pair);
} else { } else {
pairs.push(pair); pairs.push(pair);
} }
} }
} }
// Add proper line endings, indents, and commas to each line // Add proper line endings, indents, and commas to each line
s += $.map(pairs, function (pair) { s += $.map(pairs, function(pair) {
return lineEnding + indent + pair; return lineEnding + indent + pair;
}).join(","); }).join(",");
s += lineEnding; s += lineEnding;
// pop one level of indent // pop one level of indent
indent = indent.substring(1); indent = indent.substring(1);
return s + indent + "}"; return s + indent + "}";
}; };
private static tojsonArray = function (a: any, indent: string, nolint: boolean) { private static tojsonArray = function(a: any, indent: string, nolint: boolean) {
if (a.length === 0) { if (a.length === 0) {
return "[ ]"; return "[ ]";
} }
var lineEnding = nolint ? " " : "\n"; var lineEnding = nolint ? " " : "\n";
if (!indent || nolint) { if (!indent || nolint) {
indent = ""; indent = "";
} }
var s = "[" + lineEnding; var s = "[" + lineEnding;
indent += "\t"; indent += "\t";
for (var i = 0; i < a.length; i++) { for (var i = 0; i < a.length; i++) {
s += indent + MongoUtility.tojson(a[i], indent, nolint); s += indent + MongoUtility.tojson(a[i], indent, nolint);
if (i < a.length - 1) { if (i < a.length - 1) {
s += "," + lineEnding; s += "," + lineEnding;
} }
} }
if (a.length === 0) { if (a.length === 0) {
s += indent; s += indent;
} }
indent = indent.substring(1); indent = indent.substring(1);
s += lineEnding + indent + "]"; s += lineEnding + indent + "]";
return s; return s;
}; };
private static hasDefinedProperty = function (obj: any, prop: string): boolean { private static hasDefinedProperty = function(obj: any, prop: string): boolean {
if (Object.getPrototypeOf === undefined || Object.getPrototypeOf(obj) === null) { if (Object.getPrototypeOf === undefined || Object.getPrototypeOf(obj) === null) {
return false; return false;
} else if (obj.hasOwnProperty(prop)) { } else if (obj.hasOwnProperty(prop)) {
return true; return true;
} else { } else {
return MongoUtility.hasDefinedProperty(Object.getPrototypeOf(obj), prop); return MongoUtility.hasDefinedProperty(Object.getPrototypeOf(obj), prop);
} }
}; };
private static isObjectId(obj: any): boolean { private static isObjectId(obj: any): boolean {
var keys = Object.keys(obj); var keys = Object.keys(obj);
return keys.length === 1 && keys[0] === "$oid" && typeof obj.$oid === "string" && /^[0-9a-f]{24}$/.test(obj.$oid); return keys.length === 1 && keys[0] === "$oid" && typeof obj.$oid === "string" && /^[0-9a-f]{24}$/.test(obj.$oid);
} }
} }

View File

@@ -1,47 +1,47 @@
import "jquery"; import "jquery";
import * as Q from "q"; import * as Q from "q";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { CosmosClient } from "./CosmosClient"; import { CosmosClient } from "./CosmosClient";
export class NotificationsClientBase implements ViewModels.NotificationsClient { export class NotificationsClientBase implements ViewModels.NotificationsClient {
private _extensionEndpoint: string; private _extensionEndpoint: string;
private _notificationsApiSuffix: string; private _notificationsApiSuffix: string;
protected constructor(notificationsApiSuffix: string) { protected constructor(notificationsApiSuffix: string) {
this._notificationsApiSuffix = notificationsApiSuffix; this._notificationsApiSuffix = notificationsApiSuffix;
} }
public fetchNotifications(): Q.Promise<DataModels.Notification[]> { public fetchNotifications(): Q.Promise<DataModels.Notification[]> {
const deferred: Q.Deferred<DataModels.Notification[]> = Q.defer<DataModels.Notification[]>(); const deferred: Q.Deferred<DataModels.Notification[]> = Q.defer<DataModels.Notification[]>();
const databaseAccount: ViewModels.DatabaseAccount = CosmosClient.databaseAccount(); const databaseAccount: ViewModels.DatabaseAccount = CosmosClient.databaseAccount();
const subscriptionId: string = CosmosClient.subscriptionId(); const subscriptionId: string = CosmosClient.subscriptionId();
const resourceGroup: string = CosmosClient.resourceGroup(); const resourceGroup: string = CosmosClient.resourceGroup();
const url: string = `${this._extensionEndpoint}${this._notificationsApiSuffix}?accountName=${databaseAccount.name}&subscriptionId=${subscriptionId}&resourceGroup=${resourceGroup}`; const url: string = `${this._extensionEndpoint}${this._notificationsApiSuffix}?accountName=${databaseAccount.name}&subscriptionId=${subscriptionId}&resourceGroup=${resourceGroup}`;
const authorizationHeader: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader(); const authorizationHeader: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers: any = {}; const headers: any = {};
headers[authorizationHeader.header] = authorizationHeader.token; headers[authorizationHeader.header] = authorizationHeader.token;
$.ajax({ $.ajax({
url: url, url: url,
type: "GET", type: "GET",
headers: headers, headers: headers,
cache: false, cache: false
}).then( }).then(
(notifications: DataModels.Notification[], textStatus: string, xhr: JQueryXHR<any>) => { (notifications: DataModels.Notification[], textStatus: string, xhr: JQueryXHR<any>) => {
deferred.resolve(notifications); deferred.resolve(notifications);
}, },
(xhr: JQueryXHR<any>, textStatus: string, error: any) => { (xhr: JQueryXHR<any>, textStatus: string, error: any) => {
deferred.reject(xhr.responseText); deferred.reject(xhr.responseText);
} }
); );
return deferred.promise; return deferred.promise;
} }
public setExtensionEndpoint(extensionEndpoint: string): void { public setExtensionEndpoint(extensionEndpoint: string): void {
this._extensionEndpoint = extensionEndpoint; this._extensionEndpoint = extensionEndpoint;
} }
} }

View File

@@ -1,286 +1,286 @@
import * as _ from "underscore"; import * as _ from "underscore";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import * as ErrorParserUtility from "./ErrorParserUtility"; import * as ErrorParserUtility from "./ErrorParserUtility";
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants"; import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { CosmosClient } from "./CosmosClient"; import { CosmosClient } from "./CosmosClient";
import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import * as Logger from "./Logger"; import { Logger } from "./Logger";
import { NotificationConsoleUtils } from "../Utils/NotificationConsoleUtils"; import { NotificationConsoleUtils } from "../Utils/NotificationConsoleUtils";
import { QueryUtils } from "../Utils/QueryUtils"; import { QueryUtils } from "../Utils/QueryUtils";
export class QueriesClient implements ViewModels.QueriesClient { export class QueriesClient implements ViewModels.QueriesClient {
private static readonly PartitionKey: DataModels.PartitionKey = { private static readonly PartitionKey: DataModels.PartitionKey = {
paths: [`/${SavedQueries.PartitionKeyProperty}`], paths: [`/${SavedQueries.PartitionKeyProperty}`],
kind: BackendDefaults.partitionKeyKind, kind: BackendDefaults.partitionKeyKind,
version: BackendDefaults.partitionKeyVersion, version: BackendDefaults.partitionKeyVersion
}; };
private static readonly FetchQuery: string = "SELECT * FROM c"; private static readonly FetchQuery: string = "SELECT * FROM c";
private static readonly FetchMongoQuery: string = "{}"; private static readonly FetchMongoQuery: string = "{}";
public constructor(private container: ViewModels.Explorer) {} public constructor(private container: ViewModels.Explorer) {}
public async setupQueriesCollection(): Promise<DataModels.Collection> { public async setupQueriesCollection(): Promise<DataModels.Collection> {
const queriesCollection: ViewModels.Collection = this.findQueriesCollection(); const queriesCollection: ViewModels.Collection = this.findQueriesCollection();
if (queriesCollection) { if (queriesCollection) {
return Promise.resolve(queriesCollection.rawDataModel); return Promise.resolve(queriesCollection.rawDataModel);
} }
const id = NotificationConsoleUtils.logConsoleMessage( const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
"Setting up account for saving queries" "Setting up account for saving queries"
); );
return this.container.documentClientUtility return this.container.documentClientUtility
.getOrCreateDatabaseAndCollection({ .getOrCreateDatabaseAndCollection({
collectionId: SavedQueries.CollectionName, collectionId: SavedQueries.CollectionName,
databaseId: SavedQueries.DatabaseName, databaseId: SavedQueries.DatabaseName,
partitionKey: QueriesClient.PartitionKey, partitionKey: QueriesClient.PartitionKey,
offerThroughput: SavedQueries.OfferThroughput, offerThroughput: SavedQueries.OfferThroughput,
databaseLevelThroughput: undefined, databaseLevelThroughput: undefined
}) })
.then( .then(
(collection: DataModels.Collection) => { (collection: DataModels.Collection) => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info, ConsoleDataType.Info,
"Successfully set up account for saving queries" "Successfully set up account for saving queries"
); );
return Promise.resolve(collection); return Promise.resolve(collection);
}, },
(error: any) => { (error: any) => {
const stringifiedError: string = JSON.stringify(error); const stringifiedError: string = JSON.stringify(error);
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error, ConsoleDataType.Error,
`Failed to set up account for saving queries: ${stringifiedError}` `Failed to set up account for saving queries: ${stringifiedError}`
); );
Logger.logError(stringifiedError, "setupQueriesCollection"); Logger.logError(stringifiedError, "setupQueriesCollection");
return Promise.reject(stringifiedError); return Promise.reject(stringifiedError);
} }
) )
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
} }
public async saveQuery(query: DataModels.Query): Promise<void> { public async saveQuery(query: DataModels.Query): Promise<void> {
const queriesCollection = this.findQueriesCollection(); const queriesCollection = this.findQueriesCollection();
if (!queriesCollection) { if (!queriesCollection) {
const errorMessage: string = "Account not set up to perform saved query operations"; const errorMessage: string = "Account not set up to perform saved query operations";
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error, ConsoleDataType.Error,
`Failed to save query ${query.queryName}: ${errorMessage}` `Failed to save query ${query.queryName}: ${errorMessage}`
); );
return Promise.reject(errorMessage); return Promise.reject(errorMessage);
} }
try { try {
this.validateQuery(query); this.validateQuery(query);
} catch (error) { } catch (error) {
const errorMessage: string = "Invalid query specified"; const errorMessage: string = "Invalid query specified";
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error, ConsoleDataType.Error,
`Failed to save query ${query.queryName}: ${errorMessage}` `Failed to save query ${query.queryName}: ${errorMessage}`
); );
return Promise.reject(errorMessage); return Promise.reject(errorMessage);
} }
const id = NotificationConsoleUtils.logConsoleMessage( const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
`Saving query ${query.queryName}` `Saving query ${query.queryName}`
); );
query.id = query.queryName; query.id = query.queryName;
return this.container.documentClientUtility return this.container.documentClientUtility
.createDocument(queriesCollection, query) .createDocument(queriesCollection, query)
.then( .then(
(savedQuery: DataModels.Query) => { (savedQuery: DataModels.Query) => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info, ConsoleDataType.Info,
`Successfully saved query ${query.queryName}` `Successfully saved query ${query.queryName}`
); );
return Promise.resolve(); return Promise.resolve();
}, },
(error: any) => { (error: any) => {
let errorMessage: string; let errorMessage: string;
const parsedError: DataModels.ErrorDataModel = ErrorParserUtility.parse(error)[0]; const parsedError: DataModels.ErrorDataModel = ErrorParserUtility.parse(error)[0];
if (parsedError.code === HttpStatusCodes.Conflict.toString()) { if (parsedError.code === HttpStatusCodes.Conflict.toString()) {
errorMessage = `Query ${query.queryName} already exists`; errorMessage = `Query ${query.queryName} already exists`;
} else { } else {
errorMessage = parsedError.message; errorMessage = parsedError.message;
} }
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error, ConsoleDataType.Error,
`Failed to save query ${query.queryName}: ${errorMessage}` `Failed to save query ${query.queryName}: ${errorMessage}`
); );
Logger.logError(JSON.stringify(parsedError), "saveQuery"); Logger.logError(JSON.stringify(parsedError), "saveQuery");
return Promise.reject(errorMessage); return Promise.reject(errorMessage);
} }
) )
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
} }
public async getQueries(): Promise<DataModels.Query[]> { public async getQueries(): Promise<DataModels.Query[]> {
const queriesCollection = this.findQueriesCollection(); const queriesCollection = this.findQueriesCollection();
if (!queriesCollection) { if (!queriesCollection) {
const errorMessage: string = "Account not set up to perform saved query operations"; const errorMessage: string = "Account not set up to perform saved query operations";
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error, ConsoleDataType.Error,
`Failed to fetch saved queries: ${errorMessage}` `Failed to fetch saved queries: ${errorMessage}`
); );
return Promise.reject(errorMessage); return Promise.reject(errorMessage);
} }
const options: any = { enableCrossPartitionQuery: true }; const options: any = { enableCrossPartitionQuery: true };
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Fetching saved queries"); const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Fetching saved queries");
return this.container.documentClientUtility return this.container.documentClientUtility
.queryDocuments(SavedQueries.DatabaseName, SavedQueries.CollectionName, this.fetchQueriesQuery(), options) .queryDocuments(SavedQueries.DatabaseName, SavedQueries.CollectionName, this.fetchQueriesQuery(), options)
.then( .then(
(queryIterator: QueryIterator<ItemDefinition & Resource>) => { (queryIterator: QueryIterator<ItemDefinition & Resource>) => {
const fetchQueries = (firstItemIndex: number): Q.Promise<ViewModels.QueryResults> => const fetchQueries = (firstItemIndex: number): Q.Promise<ViewModels.QueryResults> =>
this.container.documentClientUtility.queryDocumentsPage( this.container.documentClientUtility.queryDocumentsPage(
queriesCollection.id(), queriesCollection.id(),
queryIterator, queryIterator,
firstItemIndex, firstItemIndex,
options options
); );
return QueryUtils.queryAllPages(fetchQueries).then( return QueryUtils.queryAllPages(fetchQueries).then(
(results: ViewModels.QueryResults) => { (results: ViewModels.QueryResults) => {
let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => { let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => {
if (!document) { if (!document) {
return undefined; return undefined;
} }
const { id, resourceId, query, queryName } = document; const { id, resourceId, query, queryName } = document;
const parsedQuery: DataModels.Query = { const parsedQuery: DataModels.Query = {
resourceId: resourceId, resourceId: resourceId,
queryName: queryName, queryName: queryName,
query: query, query: query,
id: id, id: id
}; };
try { try {
this.validateQuery(parsedQuery); this.validateQuery(parsedQuery);
return parsedQuery; return parsedQuery;
} catch (error) { } catch (error) {
return undefined; return undefined;
} }
}); });
queries = _.reject(queries, (parsedQuery: DataModels.Query) => !parsedQuery); queries = _.reject(queries, (parsedQuery: DataModels.Query) => !parsedQuery);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully fetched saved queries"); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully fetched saved queries");
return Promise.resolve(queries); return Promise.resolve(queries);
}, },
(error: any) => { (error: any) => {
const stringifiedError: string = JSON.stringify(error); const stringifiedError: string = JSON.stringify(error);
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error, ConsoleDataType.Error,
`Failed to fetch saved queries: ${stringifiedError}` `Failed to fetch saved queries: ${stringifiedError}`
); );
Logger.logError(stringifiedError, "getSavedQueries"); Logger.logError(stringifiedError, "getSavedQueries");
return Promise.reject(stringifiedError); return Promise.reject(stringifiedError);
} }
); );
}, },
(error: any) => { (error: any) => {
// should never get into this state but we handle this regardless // should never get into this state but we handle this regardless
const stringifiedError: string = JSON.stringify(error); const stringifiedError: string = JSON.stringify(error);
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error, ConsoleDataType.Error,
`Failed to fetch saved queries: ${stringifiedError}` `Failed to fetch saved queries: ${stringifiedError}`
); );
Logger.logError(stringifiedError, "getSavedQueries"); Logger.logError(stringifiedError, "getSavedQueries");
return Promise.reject(stringifiedError); return Promise.reject(stringifiedError);
} }
) )
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
} }
public async deleteQuery(query: DataModels.Query): Promise<void> { public async deleteQuery(query: DataModels.Query): Promise<void> {
const queriesCollection = this.findQueriesCollection(); const queriesCollection = this.findQueriesCollection();
if (!queriesCollection) { if (!queriesCollection) {
const errorMessage: string = "Account not set up to perform saved query operations"; const errorMessage: string = "Account not set up to perform saved query operations";
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error, ConsoleDataType.Error,
`Failed to fetch saved queries: ${errorMessage}` `Failed to fetch saved queries: ${errorMessage}`
); );
return Promise.reject(errorMessage); return Promise.reject(errorMessage);
} }
try { try {
this.validateQuery(query); this.validateQuery(query);
} catch (error) { } catch (error) {
const errorMessage: string = "Invalid query specified"; const errorMessage: string = "Invalid query specified";
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error, ConsoleDataType.Error,
`Failed to delete query ${query.queryName}: ${errorMessage}` `Failed to delete query ${query.queryName}: ${errorMessage}`
); );
} }
const id = NotificationConsoleUtils.logConsoleMessage( const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
`Deleting query ${query.queryName}` `Deleting query ${query.queryName}`
); );
query.id = query.queryName; query.id = query.queryName;
const documentId: ViewModels.DocumentId = new DocumentId( const documentId: ViewModels.DocumentId = new DocumentId(
{ {
partitionKey: QueriesClient.PartitionKey, partitionKey: QueriesClient.PartitionKey,
partitionKeyProperty: "id", partitionKeyProperty: "id"
} as ViewModels.DocumentsTab, } as ViewModels.DocumentsTab,
query, query,
query.queryName query.queryName
); // TODO: Remove DocumentId's dependency on DocumentsTab ); // TODO: Remove DocumentId's dependency on DocumentsTab
const options: any = { partitionKey: query.resourceId }; const options: any = { partitionKey: query.resourceId };
return this.container.documentClientUtility return this.container.documentClientUtility
.deleteDocument(queriesCollection, documentId) .deleteDocument(queriesCollection, documentId)
.then( .then(
() => { () => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info, ConsoleDataType.Info,
`Successfully deleted query ${query.queryName}` `Successfully deleted query ${query.queryName}`
); );
return Promise.resolve(); return Promise.resolve();
}, },
(error: any) => { (error: any) => {
const stringifiedError: string = JSON.stringify(error); const stringifiedError: string = JSON.stringify(error);
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error, ConsoleDataType.Error,
`Failed to delete query ${query.queryName}: ${stringifiedError}` `Failed to delete query ${query.queryName}: ${stringifiedError}`
); );
Logger.logError(stringifiedError, "deleteQuery"); Logger.logError(stringifiedError, "deleteQuery");
return Promise.reject(stringifiedError); return Promise.reject(stringifiedError);
} }
) )
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
} }
public getResourceId(): string { public getResourceId(): string {
const databaseAccount: ViewModels.DatabaseAccount = CosmosClient.databaseAccount(); const databaseAccount: ViewModels.DatabaseAccount = CosmosClient.databaseAccount();
const databaseAccountName: string = (databaseAccount && databaseAccount.name) || ""; const databaseAccountName: string = (databaseAccount && databaseAccount.name) || "";
const subscriptionId: string = CosmosClient.subscriptionId() || ""; const subscriptionId: string = CosmosClient.subscriptionId() || "";
const resourceGroup: string = CosmosClient.resourceGroup() || ""; const resourceGroup: string = CosmosClient.resourceGroup() || "";
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDb/databaseAccounts/${databaseAccountName}`; return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDb/databaseAccounts/${databaseAccountName}`;
} }
private findQueriesCollection(): ViewModels.Collection { private findQueriesCollection(): ViewModels.Collection {
const queriesDatabase: ViewModels.Database = _.find( const queriesDatabase: ViewModels.Database = _.find(
this.container.databases(), this.container.databases(),
(database: ViewModels.Database) => database.id() === SavedQueries.DatabaseName (database: ViewModels.Database) => database.id() === SavedQueries.DatabaseName
); );
if (!queriesDatabase) { if (!queriesDatabase) {
return undefined; return undefined;
} }
return _.find( return _.find(
queriesDatabase.collections(), queriesDatabase.collections(),
(collection: ViewModels.Collection) => collection.id() === SavedQueries.CollectionName (collection: ViewModels.Collection) => collection.id() === SavedQueries.CollectionName
); );
} }
private validateQuery(query: DataModels.Query): void { private validateQuery(query: DataModels.Query): void {
if (!query || query.queryName == null || query.query == null || query.resourceId == null) { if (!query || query.queryName == null || query.query == null || query.resourceId == null) {
throw new Error("Invalid query specified"); throw new Error("Invalid query specified");
} }
} }
private fetchQueriesQuery(): string { private fetchQueriesQuery(): string {
if (this.container.isPreferredApiMongoDB()) { if (this.container.isPreferredApiMongoDB()) {
return QueriesClient.FetchMongoQuery; return QueriesClient.FetchMongoQuery;
} }
return QueriesClient.FetchQuery; return QueriesClient.FetchQuery;
} }
} }

View File

@@ -1,106 +1,108 @@
import * as ko from "knockout"; import * as ko from "knockout";
import { SplitterMetrics } from "./Constants"; import { SplitterMetrics } from "./Constants";
export enum SplitterDirection { export enum SplitterDirection {
Horizontal = "horizontal", Horizontal = "horizontal",
Vertical = "vertical", Vertical = "vertical"
} }
export interface SplitterBounds { export interface SplitterBounds {
max: number; max: number;
min: number; min: number;
} }
export interface SplitterOptions { export interface SplitterOptions {
splitterId: string; splitterId: string;
leftId: string; leftId: string;
bounds: SplitterBounds; bounds: SplitterBounds;
direction: SplitterDirection; direction: SplitterDirection;
} }
export class Splitter { export class Splitter {
public splitterId: string; public splitterId: string;
public leftSideId: string; public leftSideId: string;
public splitter: HTMLElement; public splitter: HTMLElement;
public leftSide: HTMLElement; public leftSide: HTMLElement;
public lastX: number; public lastX: number;
public lastWidth: number; public lastWidth: number;
private isCollapsed: ko.Observable<boolean>; private isCollapsed: ko.Observable<boolean>;
private bounds: SplitterBounds; private bounds: SplitterBounds;
private direction: SplitterDirection; private direction: SplitterDirection;
constructor(options: SplitterOptions) { constructor(options: SplitterOptions) {
this.splitterId = options.splitterId; this.splitterId = options.splitterId;
this.leftSideId = options.leftId; this.leftSideId = options.leftId;
this.isCollapsed = ko.observable<boolean>(false); this.isCollapsed = ko.observable<boolean>(false);
this.bounds = options.bounds; this.bounds = options.bounds;
this.direction = options.direction; this.direction = options.direction;
this.initialize(); this.initialize();
} }
public initialize() { public initialize() {
this.splitter = document.getElementById(this.splitterId); this.splitter = document.getElementById(this.splitterId);
this.leftSide = document.getElementById(this.leftSideId); this.leftSide = document.getElementById(this.leftSideId);
const isVerticalSplitter: boolean = this.direction === SplitterDirection.Vertical; const isVerticalSplitter: boolean = this.direction === SplitterDirection.Vertical;
const splitterOptions: JQueryUI.ResizableOptions = { const splitterOptions: JQueryUI.ResizableOptions = {
animate: true, animate: true,
animateDuration: "fast", animateDuration: "fast",
start: this.onResizeStart, start: this.onResizeStart,
stop: this.onResizeStop, stop: this.onResizeStop
}; };
if (isVerticalSplitter) { if (isVerticalSplitter) {
$(this.leftSide).css("width", this.bounds.min); $(this.leftSide).css("width", this.bounds.min);
$(this.splitter).css("height", "100%"); $(this.splitter).css("height", "100%");
splitterOptions.maxWidth = this.bounds.max; splitterOptions.maxWidth = this.bounds.max;
splitterOptions.minWidth = this.bounds.min; splitterOptions.minWidth = this.bounds.min;
splitterOptions.handles = { e: "#" + this.splitterId }; splitterOptions.handles = { e: "#" + this.splitterId };
} else { } else {
$(this.leftSide).css("height", this.bounds.min); $(this.leftSide).css("height", this.bounds.min);
$(this.splitter).css("width", "100%"); $(this.splitter).css("width", "100%");
splitterOptions.maxHeight = this.bounds.max; splitterOptions.maxHeight = this.bounds.max;
splitterOptions.minHeight = this.bounds.min; splitterOptions.minHeight = this.bounds.min;
splitterOptions.handles = { s: "#" + this.splitterId }; splitterOptions.handles = { s: "#" + this.splitterId };
} }
$(this.leftSide).resizable(splitterOptions); $(this.leftSide).resizable(splitterOptions);
} }
private onResizeStart: JQueryUI.ResizableEvent = (e: Event, ui: JQueryUI.ResizableUIParams) => { private onResizeStart: JQueryUI.ResizableEvent = (e: Event, ui: JQueryUI.ResizableUIParams) => {
if (this.direction === SplitterDirection.Vertical) { if (this.direction === SplitterDirection.Vertical) {
$(".ui-resizable-helper").height("100%"); $(".ui-resizable-helper").height("100%");
} else { } else {
$(".ui-resizable-helper").width("100%"); $(".ui-resizable-helper").width("100%");
} }
$("iframe").css("pointer-events", "none"); $("iframe").css("pointer-events", "none");
}; };
private onResizeStop: JQueryUI.ResizableEvent = (e: Event, ui: JQueryUI.ResizableUIParams) => { private onResizeStop: JQueryUI.ResizableEvent = (e: Event, ui: JQueryUI.ResizableUIParams) => {
$("iframe").css("pointer-events", "auto"); $("iframe").css("pointer-events", "auto");
}; };
public collapseLeft() { public collapseLeft() {
this.lastX = $(this.splitter).position().left; this.lastX = $(this.splitter).position().left;
this.lastWidth = $(this.leftSide).width(); this.lastWidth = $(this.leftSide).width();
$(this.splitter).css("left", SplitterMetrics.CollapsedPositionLeft); $(this.splitter).css("left", SplitterMetrics.CollapsedPositionLeft);
$(this.leftSide).css("width", ""); $(this.leftSide).css("width", "");
$(this.leftSide).resizable("option", "disabled", true).removeClass("ui-resizable-disabled"); // remove class so splitter is visible $(this.leftSide)
$(this.splitter).removeClass("ui-resizable-e"); .resizable("option", "disabled", true)
this.isCollapsed(true); .removeClass("ui-resizable-disabled"); // remove class so splitter is visible
} $(this.splitter).removeClass("ui-resizable-e");
this.isCollapsed(true);
public expandLeft() { }
$(this.splitter).addClass("ui-resizable-e");
$(this.leftSide).css("width", this.lastWidth); public expandLeft() {
$(this.splitter).css("left", this.lastX); $(this.splitter).addClass("ui-resizable-e");
$(this.splitter).css("left", ""); // this ensures the splitter's position is not fixed and enables movement during resizing $(this.leftSide).css("width", this.lastWidth);
$(this.leftSide).resizable("enable"); $(this.splitter).css("left", this.lastX);
this.isCollapsed(false); $(this.splitter).css("left", ""); // this ensures the splitter's position is not fixed and enables movement during resizing
} $(this.leftSide).resizable("enable");
} this.isCollapsed(false);
}
}

View File

@@ -32,8 +32,8 @@ export default class UrlUtility {
type: type, type: type,
objectBody: { objectBody: {
id: id, id: id,
self: resourcePath, self: resourcePath
}, }
}; };
return result; return result;

View File

@@ -1,7 +1,7 @@
export enum Platform { export enum Platform {
Portal = "Portal", Portal = "Portal",
Hosted = "Hosted", Hosted = "Hosted",
Emulator = "Emulator", Emulator = "Emulator"
} }
interface Config { interface Config {
@@ -45,7 +45,7 @@ let config: Config = {
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net", ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/settings/applications/1189306 GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/settings/applications/1189306
JUNO_ENDPOINT: "https://tools.cosmos.azure.com", JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
AZURESAMPLESCOSMOSDBPAT: "99e38770e29b4a61d7c49f188780504efd35cc86", //[SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification=" // this PAT is a "no scopes" PAT with zero access to any projects, this is just used to get around the dev.github.com rate limit when accessing public samples repo.")] AZURESAMPLESCOSMOSDBPAT: "99e38770e29b4a61d7c49f188780504efd35cc86" //[SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification=" // this PAT is a "no scopes" PAT with zero access to any projects, this is just used to get around the dev.github.com rate limit when accessing public samples repo.")]
}; };
// Injected for local develpment. These will be removed in the production bundle by webpack // Injected for local develpment. These will be removed in the production bundle by webpack

View File

@@ -7,7 +7,7 @@ export enum TabKind {
TableEntities, TableEntities,
Graph, Graph,
SQLQuery, SQLQuery,
ScaleSettings, ScaleSettings
} }
/** /**
@@ -20,7 +20,7 @@ export enum PaneKind {
DeleteDatabase, DeleteDatabase,
GlobalSettings, GlobalSettings,
AdHocAccess, AdHocAccess,
SwitchDirectory, SwitchDirectory
} }
/** /**
@@ -79,5 +79,5 @@ export enum ActionType {
OpenCollectionTab, OpenCollectionTab,
OpenPane, OpenPane,
TransmitCachedData, TransmitCachedData,
OpenSampleNotebook, OpenSampleNotebook
} }

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,7 @@ export enum LogEntryLevel {
/** /**
* Error level. * Error level.
*/ */
Error = 2, Error = 2
} }
/** /**
* Schema of a log entry. * Schema of a log entry.

View File

@@ -1,38 +1,38 @@
import * as Versions from "./Versions"; import * as Versions from "./Versions";
import * as ActionContracts from "./ActionContracts"; import * as ActionContracts from "./ActionContracts";
import * as Diagnostics from "./Diagnostics"; import * as Diagnostics from "./Diagnostics";
/** /**
* Messaging types used with Data Explorer <-> Portal communication * Messaging types used with Data Explorer <-> Portal communication
* and Hosted <-> Explorer communication * and Hosted <-> Explorer communication
*/ */
export enum MessageTypes { export enum MessageTypes {
TelemetryInfo, TelemetryInfo,
LogInfo, LogInfo,
RefreshResources, RefreshResources,
AllDatabases, AllDatabases,
CollectionsForDatabase, CollectionsForDatabase,
RefreshOffers, RefreshOffers,
AllOffers, AllOffers,
UpdateLocationHash, UpdateLocationHash,
SingleOffer, SingleOffer,
RefreshOffer, RefreshOffer,
UpdateAccountName, UpdateAccountName,
ForbiddenError, ForbiddenError,
AadSignIn, AadSignIn,
GetAccessAadRequest, GetAccessAadRequest,
GetAccessAadResponse, GetAccessAadResponse,
UpdateAccountSwitch, UpdateAccountSwitch,
UpdateDirectoryControl, UpdateDirectoryControl,
SwitchAccount, SwitchAccount,
SendNotification, SendNotification,
ClearNotification, ClearNotification,
ExplorerClickEvent, ExplorerClickEvent,
LoadingStatus, LoadingStatus,
GetArcadiaToken, GetArcadiaToken,
CreateWorkspace, CreateWorkspace,
CreateSparkPool, CreateSparkPool,
RefreshDatabaseAccount, RefreshDatabaseAccount
} }
export { Versions, ActionContracts, Diagnostics }; export { Versions, ActionContracts, Diagnostics };

View File

@@ -1,4 +1,4 @@
/** /**
* Data Explorer version {major.minor.patch} * Data Explorer version {major.minor.patch}
*/ */
export const DataExplorer: string = "1.0.1"; export const DataExplorer: string = "1.0.1";

File diff suppressed because it is too large Load Diff

View File

@@ -6,19 +6,19 @@ describe("The Heatmap Control", () => {
const dataPoints = { const dataPoints = {
"1": { "1": {
"2019-06-19T00:59:10Z": { "2019-06-19T00:59:10Z": {
"Normalized Throughput": 0.35, "Normalized Throughput": 0.35
}, },
"2019-06-19T00:48:10Z": { "2019-06-19T00:48:10Z": {
"Normalized Throughput": 0.25, "Normalized Throughput": 0.25
}, }
}, }
}; };
const chartCaptions = { const chartCaptions = {
chartTitle: "chart title", chartTitle: "chart title",
yAxisTitle: "YAxisTitle", yAxisTitle: "YAxisTitle",
tooltipText: "Tooltip text", tooltipText: "Tooltip text",
timeWindow: 123456789, timeWindow: 123456789
}; };
let heatmap: Heatmap; let heatmap: Heatmap;
@@ -75,12 +75,12 @@ describe("The Heatmap Control", () => {
if (dayjs().utcOffset()) { if (dayjs().utcOffset()) {
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints).not.toEqual([ expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints).not.toEqual([
"2019-06-19T00:48:10Z", "2019-06-19T00:48:10Z",
"2019-06-19T00:59:10Z", "2019-06-19T00:59:10Z"
]); ]);
} else { } else {
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints).toEqual([ expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints).toEqual([
"2019-06-19T00:48:10Z", "2019-06-19T00:48:10Z",
"2019-06-19T00:59:10Z", "2019-06-19T00:59:10Z"
]); ]);
} }
}); });
@@ -106,9 +106,9 @@ describe("iframe rendering when there is no data", () => {
data: { data: {
chartData: {}, chartData: {},
chartSettings: {}, chartSettings: {},
theme: 4, theme: 4
}, }
}, }
}; };
const divElement: string = `<div id="${Heatmap.elementId}"></div>`; const divElement: string = `<div id="${Heatmap.elementId}"></div>`;
@@ -126,9 +126,9 @@ describe("iframe rendering when there is no data", () => {
data: { data: {
chartData: {}, chartData: {},
chartSettings: {}, chartSettings: {},
theme: 2, theme: 2
}, }
}, }
}; };
const divElement: string = `<div id="${Heatmap.elementId}"></div>`; const divElement: string = `<div id="${Heatmap.elementId}"></div>`;

View File

@@ -9,7 +9,7 @@ import {
HeatmapData, HeatmapData,
LayoutSettings, LayoutSettings,
PartitionTimeStampToData, PartitionTimeStampToData,
PortalTheme, PortalTheme
} from "./HeatmapDatatypes"; } from "./HeatmapDatatypes";
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation"; import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
import { MessageHandler } from "../../Common/MessageHandler"; import { MessageHandler } from "../../Common/MessageHandler";
@@ -43,7 +43,7 @@ export class Heatmap {
return { return {
family: StyleConstants.DataExplorerFont, family: StyleConstants.DataExplorerFont,
size, size,
color, color
}; };
} }
@@ -73,7 +73,7 @@ export class Heatmap {
return 0; return 0;
} }
} }
}), })
}; };
// go thru all rows and create 2d matrix for heatmap... // go thru all rows and create 2d matrix for heatmap...
for (let i = 0; i < rows.length; i++) { for (let i = 0; i < rows.length; i++) {
@@ -115,7 +115,7 @@ export class Heatmap {
[0.7, "#E46612"], [0.7, "#E46612"],
[0.8, "#E64914"], [0.8, "#E64914"],
[0.9, "#B80016"], [0.9, "#B80016"],
[1.0, "#B80016"], [1.0, "#B80016"]
], ],
name: "", name: "",
hovertemplate: this._heatmapCaptions.tooltipText, hovertemplate: this._heatmapCaptions.tooltipText,
@@ -123,11 +123,11 @@ export class Heatmap {
thickness: 15, thickness: 15,
outlinewidth: 0, outlinewidth: 0,
tickcolor: StyleConstants.BaseDark, tickcolor: StyleConstants.BaseDark,
tickfont: this._getFontStyles(10, this._defaultFontColor), tickfont: this._getFontStyles(10, this._defaultFontColor)
}, },
y: this._chartData.yAxisPoints, y: this._chartData.yAxisPoints,
x: this._chartData.xAxisPoints, x: this._chartData.xAxisPoints
}, }
]; ];
} }
@@ -138,7 +138,7 @@ export class Heatmap {
r: 10, r: 10,
b: 35, b: 35,
t: 30, t: 30,
pad: 0, pad: 0
}, },
paper_bgcolor: "transparent", paper_bgcolor: "transparent",
plot_bgcolor: "transparent", plot_bgcolor: "transparent",
@@ -154,7 +154,7 @@ export class Heatmap {
autotick: true, autotick: true,
fixedrange: true, fixedrange: true,
ticks: "", ticks: "",
showticklabels: false, showticklabels: false
}, },
xaxis: { xaxis: {
fixedrange: true, fixedrange: true,
@@ -167,13 +167,13 @@ export class Heatmap {
autotick: true, autotick: true,
tickformat: this._heatmapCaptions.timeWindow > 7 ? "%I:%M %p" : "%b %e", tickformat: this._heatmapCaptions.timeWindow > 7 ? "%I:%M %p" : "%b %e",
showticklabels: true, showticklabels: true,
tickfont: this._getFontStyles(10), tickfont: this._getFontStyles(10)
}, },
title: { title: {
text: this._heatmapCaptions.chartTitle, text: this._heatmapCaptions.chartTitle,
x: 0.01, x: 0.01,
font: this._getFontStyles(13, this._defaultFontColor), font: this._getFontStyles(13, this._defaultFontColor)
}, }
}; };
} }
@@ -181,7 +181,7 @@ export class Heatmap {
return { return {
/* heatmap can be fully responsive however the min-height needed in that case is greater than the iframe portal height, hence explicit width + height have been set in _getLayoutSettings /* heatmap can be fully responsive however the min-height needed in that case is greater than the iframe portal height, hence explicit width + height have been set in _getLayoutSettings
responsive: true,*/ responsive: true,*/
displayModeBar: false, displayModeBar: false
}; };
} }

View File

@@ -8,7 +8,7 @@ export enum PortalTheme {
blue = 1, blue = 1,
azure, azure,
light, light,
dark, dark
} }
export interface HeatmapData { export interface HeatmapData {

File diff suppressed because it is too large Load Diff

View File

@@ -1,34 +1,34 @@
/* Type definitions for code-runner's jquery-typeahead v2.8.0 /* Type definitions for code-runner's jquery-typeahead v2.8.0
* https://github.com/running-coder/jquery-typeahead * https://github.com/running-coder/jquery-typeahead
* *
* There is no DefinitelyTyped support for this library, yet, so we only define here what we use. * There is no DefinitelyTyped support for this library, yet, so we only define here what we use.
* https://github.com/running-coder/jquery-typeahead/issues/156 * https://github.com/running-coder/jquery-typeahead/issues/156
* TODO: Replace this minimum definition by the official one when it comes out. * TODO: Replace this minimum definition by the official one when it comes out.
*/ */
/// <reference path="jquery.d.ts" /> /// <reference path="jquery.d.ts" />
interface JQueryTypeaheadParam { interface JQueryTypeaheadParam {
input: string; input: string;
order?: string; order?: string;
source: any; source: any;
callback?: any; callback?: any;
minLength?: number; minLength?: number;
searchOnFocus?: boolean; searchOnFocus?: boolean;
template?: string | { (query: string, item: any): string }; template?: string | { (query: string, item: any): string };
dynamic?: boolean; dynamic?: boolean;
mustSelectItem?: boolean; mustSelectItem?: boolean;
} }
/** /**
* For use with: $.typeahead() * For use with: $.typeahead()
*/ */
interface JQueryStatic { interface JQueryStatic {
typeahead(arg: JQueryTypeaheadParam): void; typeahead(arg: JQueryTypeaheadParam): void;
} }
/** /**
* For use with $('').typehead() * For use with $('').typehead()
*/ */
// interface JQuery { // interface JQuery {
// typeahead(arg: JQueryTypeaheadParam): void; // typeahead(arg: JQueryTypeaheadParam): void;
// } // }

File diff suppressed because it is too large Load Diff

View File

@@ -1,42 +1,42 @@
// Type definitions for jQuery contextMenu 1.7.0 // Type definitions for jQuery contextMenu 1.7.0
// Project: http://medialize.github.com/jQuery-contextMenu/ // Project: http://medialize.github.com/jQuery-contextMenu/
// Definitions by: Natan Vivo <https://github.com/nvivo/> // Definitions by: Natan Vivo <https://github.com/nvivo/>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
/// <reference path="jquery.d.ts" /> /// <reference path="jquery.d.ts" />
interface JQueryContextMenuOptions { interface JQueryContextMenuOptions {
selector: string; selector: string;
appendTo?: string; appendTo?: string;
trigger?: string; trigger?: string;
autoHide?: boolean; autoHide?: boolean;
delay?: number; delay?: number;
determinePosition?: (menu: JQuery) => void; determinePosition?: (menu: JQuery) => void;
position?: (opt: JQuery, x: number, y: number) => void; position?: (opt: JQuery, x: number, y: number) => void;
positionSubmenu?: (menu: JQuery) => void; positionSubmenu?: (menu: JQuery) => void;
zIndex?: number; zIndex?: number;
animation?: { animation?: {
duration?: number; duration?: number;
show?: string; show?: string;
hide?: string; hide?: string;
}; };
events?: { events?: {
show?: () => void; show?: () => void;
hide?: () => void; hide?: () => void;
}; };
callback?: (key: any, options: any) => any; callback?: (key: any, options: any) => any;
items?: any; items?: any;
build?: (triggerElement: JQuery, e: Event) => any; build?: (triggerElement: JQuery, e: Event) => any;
reposition?: boolean; reposition?: boolean;
className?: string; className?: string;
itemClickEvent?: string; itemClickEvent?: string;
} }
interface JQueryStatic { interface JQueryStatic {
contextMenu(options?: JQueryContextMenuOptions): JQuery; contextMenu(options?: JQueryContextMenuOptions): JQuery;
contextMenu(type: string, selector?: any): JQuery; contextMenu(type: string, selector?: any): JQuery;
} }
interface JQuery { interface JQuery {
contextMenu(options?: any): JQuery; contextMenu(options?: any): JQuery;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,142 +1,178 @@
jest.mock("monaco-editor"); jest.mock("monaco-editor");
import * as ko from "knockout"; import * as ko from "knockout";
import "./ComponentRegisterer"; import "./ComponentRegisterer";
describe("Component Registerer", () => { describe("Component Registerer", () => {
it("should register input-typeahead component", () => { it("should register command-button component", () => {
expect(ko.components.isRegistered("input-typeahead")).toBe(true); expect(ko.components.isRegistered("command-button")).toBe(true);
}); });
it("should register new-vertex-form component", () => { it("should register input-typeahead component", () => {
expect(ko.components.isRegistered("new-vertex-form")).toBe(true); expect(ko.components.isRegistered("input-typeahead")).toBe(true);
}); });
it("should register error-display component", () => { it("should register new-vertex-form component", () => {
expect(ko.components.isRegistered("error-display")).toBe(true); expect(ko.components.isRegistered("new-vertex-form")).toBe(true);
}); });
it("should register graph-style component", () => { it("should register error-display component", () => {
expect(ko.components.isRegistered("graph-style")).toBe(true); expect(ko.components.isRegistered("error-display")).toBe(true);
}); });
it("should register collapsible-panel component", () => { it("should register graph-style component", () => {
expect(ko.components.isRegistered("collapsible-panel")).toBe(true); expect(ko.components.isRegistered("graph-style")).toBe(true);
}); });
it("should register json-editor component", () => { it("should register collapsible-panel component", () => {
expect(ko.components.isRegistered("json-editor")).toBe(true); expect(ko.components.isRegistered("collapsible-panel")).toBe(true);
}); });
it("should register documents-tab component", () => { it("should register json-editor component", () => {
expect(ko.components.isRegistered("documents-tab")).toBe(true); expect(ko.components.isRegistered("json-editor")).toBe(true);
}); });
it("should register stored-procedure-tab component", () => { it("should register documents-tab component", () => {
expect(ko.components.isRegistered("stored-procedure-tab")).toBe(true); expect(ko.components.isRegistered("documents-tab")).toBe(true);
}); });
it("should register trigger-tab component", () => { it("should register stored-procedure-tab component", () => {
expect(ko.components.isRegistered("trigger-tab")).toBe(true); expect(ko.components.isRegistered("stored-procedure-tab")).toBe(true);
}); });
it("should register user-defined-function-tab component", () => { it("should register trigger-tab component", () => {
expect(ko.components.isRegistered("user-defined-function-tab")).toBe(true); expect(ko.components.isRegistered("trigger-tab")).toBe(true);
}); });
it("should register settings-tab component", () => { it("should register user-defined-function-tab component", () => {
expect(ko.components.isRegistered("settings-tab")).toBe(true); expect(ko.components.isRegistered("user-defined-function-tab")).toBe(true);
}); });
it("should register query-tab component", () => { it("should register settings-tab component", () => {
expect(ko.components.isRegistered("query-tab")).toBe(true); expect(ko.components.isRegistered("settings-tab")).toBe(true);
}); });
it("should register tables-query-tab component", () => { it("should register query-tab component", () => {
expect(ko.components.isRegistered("tables-query-tab")).toBe(true); expect(ko.components.isRegistered("query-tab")).toBe(true);
}); });
it("should register graph-tab component", () => { it("should register tables-query-tab component", () => {
expect(ko.components.isRegistered("graph-tab")).toBe(true); expect(ko.components.isRegistered("tables-query-tab")).toBe(true);
}); });
it("should register notebookv2-tab component", () => { it("should register graph-tab component", () => {
expect(ko.components.isRegistered("notebookv2-tab")).toBe(true); expect(ko.components.isRegistered("graph-tab")).toBe(true);
}); });
it("should register terminal-tab component", () => { it("should register notebook-tab component", () => {
expect(ko.components.isRegistered("terminal-tab")).toBe(true); expect(ko.components.isRegistered("notebook-tab")).toBe(true);
}); });
it("should register spark-master-tab component", () => { it("should register notebookv2-tab component", () => {
expect(ko.components.isRegistered("spark-master-tab")).toBe(true); expect(ko.components.isRegistered("notebookv2-tab")).toBe(true);
}); });
it("should register mongo-shell-tab component", () => { it("should register terminal-tab component", () => {
expect(ko.components.isRegistered("mongo-shell-tab")).toBe(true); expect(ko.components.isRegistered("terminal-tab")).toBe(true);
}); });
it("should registeradd-collection-pane component", () => { it("should register spark-master-tab component", () => {
expect(ko.components.isRegistered("add-collection-pane")).toBe(true); expect(ko.components.isRegistered("spark-master-tab")).toBe(true);
}); });
it("should register delete-collection-confirmation-pane component", () => { it("should register mongo-shell-tab component", () => {
expect(ko.components.isRegistered("delete-collection-confirmation-pane")).toBe(true); expect(ko.components.isRegistered("mongo-shell-tab")).toBe(true);
}); });
it("should register delete-database-confirmation-pane component", () => { it("should register resource-tree component", () => {
expect(ko.components.isRegistered("delete-database-confirmation-pane")).toBe(true); expect(ko.components.isRegistered("resource-tree")).toBe(true);
}); });
it("should register save-query-pane component", () => { it("should register database-node component", () => {
expect(ko.components.isRegistered("save-query-pane")).toBe(true); expect(ko.components.isRegistered("database-node")).toBe(true);
}); });
it("should register browse-queries-pane component", () => { it("should register collection-node component", () => {
expect(ko.components.isRegistered("browse-queries-pane")).toBe(true); expect(ko.components.isRegistered("collection-node")).toBe(true);
}); });
it("should register graph-new-vertex-pane component", () => { it("should register stored-procedure-node component", () => {
expect(ko.components.isRegistered("graph-new-vertex-pane")).toBe(true); expect(ko.components.isRegistered("stored-procedure-node")).toBe(true);
}); });
it("should register graph-styling-pane component", () => { it("should register trigger-node component", () => {
expect(ko.components.isRegistered("graph-styling-pane")).toBe(true); expect(ko.components.isRegistered("trigger-node")).toBe(true);
}); });
it("should register upload-file-pane component", () => { it("should register user-defined-function-node component", () => {
expect(ko.components.isRegistered("upload-file-pane")).toBe(true); expect(ko.components.isRegistered("user-defined-function-node")).toBe(true);
}); });
it("should register string-input-pane component", () => { it("should registeradd-collection-pane component", () => {
expect(ko.components.isRegistered("string-input-pane")).toBe(true); expect(ko.components.isRegistered("add-collection-pane")).toBe(true);
}); });
it("should register setup-notebooks-pane component", () => { it("should register delete-collection-confirmation-pane component", () => {
expect(ko.components.isRegistered("setup-notebooks-pane")).toBe(true); expect(ko.components.isRegistered("delete-collection-confirmation-pane")).toBe(true);
}); });
it("should register setup-spark-cluster-pane component", () => { it("should register delete-database-confirmation-pane component", () => {
expect(ko.components.isRegistered("setup-spark-cluster-pane")).toBe(true); expect(ko.components.isRegistered("delete-database-confirmation-pane")).toBe(true);
}); });
it("should register manage-spark-cluster-pane component", () => { it("should register save-query-pane component", () => {
expect(ko.components.isRegistered("manage-spark-cluster-pane")).toBe(true); expect(ko.components.isRegistered("save-query-pane")).toBe(true);
}); });
it("should register dynamic-list component", () => { it("should register browse-queries-pane component", () => {
expect(ko.components.isRegistered("dynamic-list")).toBe(true); expect(ko.components.isRegistered("browse-queries-pane")).toBe(true);
}); });
it("should register throughput-input component", () => { it("should register graph-new-vertex-pane component", () => {
expect(ko.components.isRegistered("throughput-input")).toBe(true); expect(ko.components.isRegistered("graph-new-vertex-pane")).toBe(true);
}); });
it("should register library-manage-pane component", () => { it("should register graph-styling-pane component", () => {
expect(ko.components.isRegistered("library-manage-pane")).toBe(true); expect(ko.components.isRegistered("graph-styling-pane")).toBe(true);
}); });
it("should register cluster-library-pane component", () => { it("should register upload-file-pane component", () => {
expect(ko.components.isRegistered("cluster-library-pane")).toBe(true); expect(ko.components.isRegistered("upload-file-pane")).toBe(true);
}); });
});
it("should register string-input-pane component", () => {
expect(ko.components.isRegistered("string-input-pane")).toBe(true);
});
it("should register setup-notebooks-pane component", () => {
expect(ko.components.isRegistered("setup-notebooks-pane")).toBe(true);
});
it("should register setup-spark-cluster-pane component", () => {
expect(ko.components.isRegistered("setup-spark-cluster-pane")).toBe(true);
});
it("should register manage-spark-cluster-pane component", () => {
expect(ko.components.isRegistered("manage-spark-cluster-pane")).toBe(true);
});
it("should register collection-node-context-menu component", () => {
expect(ko.components.isRegistered("collection-node-context-menu")).toBe(true);
});
it("should register dynamic-list component", () => {
expect(ko.components.isRegistered("dynamic-list")).toBe(true);
});
it("should register throughput-input component", () => {
expect(ko.components.isRegistered("throughput-input")).toBe(true);
});
it("should register library-manage-pane component", () => {
expect(ko.components.isRegistered("library-manage-pane")).toBe(true);
});
it("should register cluster-library-pane component", () => {
expect(ko.components.isRegistered("cluster-library-pane")).toBe(true);
});
});

View File

@@ -1,83 +1,98 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as PaneComponents from "./Panes/PaneComponents"; import * as PaneComponents from "./Panes/PaneComponents";
import * as TabComponents from "./Tabs/TabComponents"; import * as TabComponents from "./Tabs/TabComponents";
import { CollapsiblePanelComponent } from "./Controls/CollapsiblePanel/CollapsiblePanelComponent"; import * as TreeComponents from "./Tree/TreeComponents";
import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent"; import { CollapsiblePanelComponent } from "./Controls/CollapsiblePanel/CollapsiblePanelComponent";
import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent"; import { CommandButtonComponent } from "./Controls/CommandButton/CommandButton";
import { EditorComponent } from "./Controls/Editor/EditorComponent"; import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent";
import { ErrorDisplayComponent } from "./Controls/ErrorDisplayComponent/ErrorDisplayComponent"; import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent";
import { GraphStyleComponent } from "./Graph/GraphStyleComponent/GraphStyleComponent"; import { EditorComponent } from "./Controls/Editor/EditorComponent";
import { InputTypeaheadComponent } from "./Controls/InputTypeahead/InputTypeahead"; import { ErrorDisplayComponent } from "./Controls/ErrorDisplayComponent/ErrorDisplayComponent";
import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent"; import { GraphStyleComponent } from "./Graph/GraphStyleComponent/GraphStyleComponent";
import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponent"; import { InputTypeaheadComponent } from "./Controls/InputTypeahead/InputTypeahead";
import { ThroughputInputComponent } from "./Controls/ThroughputInput/ThroughputInputComponent"; import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent";
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3"; import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponent";
import { ToolbarComponent } from "./Controls/Toolbar/Toolbar"; import { ThroughputInputComponent } from "./Controls/ThroughputInput/ThroughputInputComponent";
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
ko.components.register("toolbar", new ToolbarComponent()); import { ToolbarComponent } from "./Controls/Toolbar/Toolbar";
ko.components.register("input-typeahead", new InputTypeaheadComponent());
ko.components.register("new-vertex-form", NewVertexComponent); ko.components.register("command-button", CommandButtonComponent);
ko.components.register("error-display", new ErrorDisplayComponent()); ko.components.register("toolbar", new ToolbarComponent());
ko.components.register("graph-style", GraphStyleComponent); ko.components.register("input-typeahead", new InputTypeaheadComponent());
ko.components.register("collapsible-panel", new CollapsiblePanelComponent()); ko.components.register("new-vertex-form", NewVertexComponent);
ko.components.register("editor", new EditorComponent()); ko.components.register("error-display", new ErrorDisplayComponent());
ko.components.register("json-editor", new JsonEditorComponent()); ko.components.register("graph-style", GraphStyleComponent);
ko.components.register("diff-editor", new DiffEditorComponent()); ko.components.register("collapsible-panel", new CollapsiblePanelComponent());
ko.components.register("dynamic-list", DynamicListComponent); ko.components.register("editor", new EditorComponent());
ko.components.register("throughput-input", ThroughputInputComponent); ko.components.register("json-editor", new JsonEditorComponent());
ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3); ko.components.register("diff-editor", new DiffEditorComponent());
ko.components.register("dynamic-list", DynamicListComponent);
// Collection Tabs ko.components.register("throughput-input", ThroughputInputComponent);
ko.components.register("documents-tab", new TabComponents.DocumentsTab()); ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3);
ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTab());
ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab()); // Collection Tabs
ko.components.register("trigger-tab", new TabComponents.TriggerTab()); ko.components.register("documents-tab", new TabComponents.DocumentsTab());
ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab()); ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTab());
ko.components.register("settings-tab", new TabComponents.SettingsTab()); ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab());
ko.components.register("query-tab", new TabComponents.QueryTab()); ko.components.register("trigger-tab", new TabComponents.TriggerTab());
ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab()); ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab());
ko.components.register("graph-tab", new TabComponents.GraphTab()); ko.components.register("settings-tab", new TabComponents.SettingsTab());
ko.components.register("mongo-shell-tab", new TabComponents.MongoShellTab()); ko.components.register("query-tab", new TabComponents.QueryTab());
ko.components.register("conflicts-tab", new TabComponents.ConflictsTab()); ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab());
ko.components.register("notebookv2-tab", new TabComponents.NotebookV2Tab()); ko.components.register("graph-tab", new TabComponents.GraphTab());
ko.components.register("terminal-tab", new TabComponents.TerminalTab()); ko.components.register("mongo-shell-tab", new TabComponents.MongoShellTab());
ko.components.register("spark-master-tab", new TabComponents.SparkMasterTab()); ko.components.register("conflicts-tab", new TabComponents.ConflictsTab());
ko.components.register("gallery-tab", new TabComponents.GalleryTab()); ko.components.register("notebook-tab", new TabComponents.NotebookTab());
ko.components.register("notebook-viewer-tab", new TabComponents.NotebookViewerTab()); ko.components.register("notebookv2-tab", new TabComponents.NotebookV2Tab());
ko.components.register("terminal-tab", new TabComponents.TerminalTab());
// Database Tabs ko.components.register("spark-master-tab", new TabComponents.SparkMasterTab());
ko.components.register("database-settings-tab", new TabComponents.DatabaseSettingsTab()); ko.components.register("gallery-tab", new TabComponents.GalleryTab());
ko.components.register("notebook-viewer-tab", new TabComponents.NotebookViewerTab());
// Panes
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent()); // Database Tabs
ko.components.register("add-collection-pane", new PaneComponents.AddCollectionPaneComponent()); ko.components.register("database-settings-tab", new TabComponents.DatabaseSettingsTab());
ko.components.register(
"delete-collection-confirmation-pane", // Resource Tree nodes
new PaneComponents.DeleteCollectionConfirmationPaneComponent() ko.components.register("resource-tree", new TreeComponents.ResourceTree());
); ko.components.register("database-node", new TreeComponents.DatabaseTreeNode());
ko.components.register( ko.components.register("collection-node", new TreeComponents.CollectionTreeNode());
"delete-database-confirmation-pane", ko.components.register("stored-procedure-node", new TreeComponents.StoredProcedureTreeNode());
new PaneComponents.DeleteDatabaseConfirmationPaneComponent() ko.components.register("trigger-node", new TreeComponents.TriggerTreeNode());
); ko.components.register("user-defined-function-node", new TreeComponents.UserDefinedFunctionTreeNode());
ko.components.register("graph-new-vertex-pane", new PaneComponents.GraphNewVertexPaneComponent());
ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent()); // Panes
ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent()); ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
ko.components.register("table-edit-entity-pane", new PaneComponents.TableEditEntityPaneComponent()); ko.components.register("add-collection-pane", new PaneComponents.AddCollectionPaneComponent());
ko.components.register("table-column-options-pane", new PaneComponents.TableColumnOptionsPaneComponent()); ko.components.register(
ko.components.register("table-query-select-pane", new PaneComponents.TableQuerySelectPaneComponent()); "delete-collection-confirmation-pane",
ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent()); new PaneComponents.DeleteCollectionConfirmationPaneComponent()
ko.components.register("settings-pane", new PaneComponents.SettingsPaneComponent()); );
ko.components.register("execute-sproc-params-pane", new PaneComponents.ExecuteSprocParamsComponent()); ko.components.register(
ko.components.register("renew-adhoc-access-pane", new PaneComponents.RenewAdHocAccessPane()); "delete-database-confirmation-pane",
ko.components.register("upload-items-pane", new PaneComponents.UploadItemsPaneComponent()); new PaneComponents.DeleteDatabaseConfirmationPaneComponent()
ko.components.register("load-query-pane", new PaneComponents.LoadQueryPaneComponent()); );
ko.components.register("save-query-pane", new PaneComponents.SaveQueryPaneComponent()); ko.components.register("graph-new-vertex-pane", new PaneComponents.GraphNewVertexPaneComponent());
ko.components.register("browse-queries-pane", new PaneComponents.BrowseQueriesPaneComponent()); ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent());
ko.components.register("upload-file-pane", new PaneComponents.UploadFilePaneComponent()); ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent());
ko.components.register("string-input-pane", new PaneComponents.StringInputPaneComponent()); ko.components.register("table-edit-entity-pane", new PaneComponents.TableEditEntityPaneComponent());
ko.components.register("setup-notebooks-pane", new PaneComponents.SetupNotebooksPaneComponent()); ko.components.register("table-column-options-pane", new PaneComponents.TableColumnOptionsPaneComponent());
ko.components.register("setup-spark-cluster-pane", new PaneComponents.SetupSparkClusterPaneComponent()); ko.components.register("table-query-select-pane", new PaneComponents.TableQuerySelectPaneComponent());
ko.components.register("manage-spark-cluster-pane", new PaneComponents.ManageSparkClusterPaneComponent()); ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent());
ko.components.register("library-manage-pane", new PaneComponents.LibraryManagePaneComponent()); ko.components.register("settings-pane", new PaneComponents.SettingsPaneComponent());
ko.components.register("cluster-library-pane", new PaneComponents.ClusterLibraryPaneComponent()); ko.components.register("execute-sproc-params-pane", new PaneComponents.ExecuteSprocParamsComponent());
ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent()); ko.components.register("renew-adhoc-access-pane", new PaneComponents.RenewAdHocAccessPane());
ko.components.register("upload-items-pane", new PaneComponents.UploadItemsPaneComponent());
ko.components.register("load-query-pane", new PaneComponents.LoadQueryPaneComponent());
ko.components.register("save-query-pane", new PaneComponents.SaveQueryPaneComponent());
ko.components.register("browse-queries-pane", new PaneComponents.BrowseQueriesPaneComponent());
ko.components.register("upload-file-pane", new PaneComponents.UploadFilePaneComponent());
ko.components.register("string-input-pane", new PaneComponents.StringInputPaneComponent());
ko.components.register("setup-notebooks-pane", new PaneComponents.SetupNotebooksPaneComponent());
ko.components.register("setup-spark-cluster-pane", new PaneComponents.SetupSparkClusterPaneComponent());
ko.components.register("manage-spark-cluster-pane", new PaneComponents.ManageSparkClusterPaneComponent());
ko.components.register("library-manage-pane", new PaneComponents.LibraryManagePaneComponent());
ko.components.register("cluster-library-pane", new PaneComponents.ClusterLibraryPaneComponent());
ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent());
// Menus
ko.components.register("collection-node-context-menu", new TreeComponents.CollectionTreeNodeContextMenu());

View File

@@ -1,5 +1,6 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { CommandButtonOptions } from "./Controls/CommandButton/CommandButton";
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent"; import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
import AddCollectionIcon from "../../images/AddCollection.svg"; import AddCollectionIcon from "../../images/AddCollection.svg";
import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg"; import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg";
@@ -32,13 +33,13 @@ export class ResourceTreeContextMenuButtonFactory {
const newCollectionMenuItem: TreeNodeMenuItem = { const newCollectionMenuItem: TreeNodeMenuItem = {
iconSrc: AddCollectionIcon, iconSrc: AddCollectionIcon,
onClick: () => container.onNewCollectionClicked(), onClick: () => container.onNewCollectionClicked(),
label: container.addCollectionText(), label: container.addCollectionText()
}; };
const deleteDatabaseMenuItem = { const deleteDatabaseMenuItem = {
iconSrc: DeleteDatabaseIcon, iconSrc: DeleteDatabaseIcon,
onClick: () => container.deleteDatabaseConfirmationPane.open(), onClick: () => container.deleteDatabaseConfirmationPane.open(),
label: container.deleteDatabaseText(), label: container.deleteDatabaseText()
}; };
return [newCollectionMenuItem, deleteDatabaseMenuItem]; return [newCollectionMenuItem, deleteDatabaseMenuItem];
} }
@@ -52,7 +53,7 @@ export class ResourceTreeContextMenuButtonFactory {
items.push({ items.push({
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null), onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null),
label: "New SQL Query", label: "New SQL Query"
}); });
} }
@@ -60,7 +61,7 @@ export class ResourceTreeContextMenuButtonFactory {
items.push({ items.push({
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null), onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null),
label: "New Query", label: "New Query"
}); });
items.push({ items.push({
@@ -69,7 +70,7 @@ export class ResourceTreeContextMenuButtonFactory {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewMongoShellClick(); selectedCollection && selectedCollection.onNewMongoShellClick();
}, },
label: "New Shell", label: "New Shell"
}); });
} }
@@ -80,7 +81,7 @@ export class ResourceTreeContextMenuButtonFactory {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null); selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null);
}, },
label: "New Stored Procedure", label: "New Stored Procedure"
}); });
items.push({ items.push({
@@ -89,7 +90,7 @@ export class ResourceTreeContextMenuButtonFactory {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection, null); selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection, null);
}, },
label: "New UDF", label: "New UDF"
}); });
items.push({ items.push({
@@ -98,7 +99,7 @@ export class ResourceTreeContextMenuButtonFactory {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, null); selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, null);
}, },
label: "New Trigger", label: "New Trigger"
}); });
} }
@@ -108,16 +109,13 @@ export class ResourceTreeContextMenuButtonFactory {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onDeleteCollectionContextMenuClick(selectedCollection, null); selectedCollection && selectedCollection.onDeleteCollectionContextMenuClick(selectedCollection, null);
}, },
label: container.deleteCollectionText(), label: container.deleteCollectionText()
}); });
return items; return items;
} }
public static createStoreProcedureContextMenuItems( public static createStoreProcedureContextMenuItems(container: ViewModels.Explorer): TreeNodeMenuItem[] {
container: ViewModels.Explorer,
storedProcedure: ViewModels.StoredProcedure
): TreeNodeMenuItem[] {
if (container.isPreferredApiCassandra()) { if (container.isPreferredApiCassandra()) {
return []; return [];
} }
@@ -125,16 +123,16 @@ export class ResourceTreeContextMenuButtonFactory {
return [ return [
{ {
iconSrc: DeleteSprocIcon, iconSrc: DeleteSprocIcon,
onClick: () => storedProcedure.delete(), onClick: () => {
label: "Delete Store Procedure", const selectedStoreProcedure: ViewModels.StoredProcedure = container.findSelectedStoredProcedure();
}, selectedStoreProcedure && selectedStoreProcedure.delete(selectedStoreProcedure, null);
},
label: "Delete Store Procedure"
}
]; ];
} }
public static createTriggerContextMenuItems( public static createTriggerContextMenuItems(container: ViewModels.Explorer): TreeNodeMenuItem[] {
container: ViewModels.Explorer,
trigger: ViewModels.Trigger
): TreeNodeMenuItem[] {
if (container.isPreferredApiCassandra()) { if (container.isPreferredApiCassandra()) {
return []; return [];
} }
@@ -142,16 +140,16 @@ export class ResourceTreeContextMenuButtonFactory {
return [ return [
{ {
iconSrc: DeleteTriggerIcon, iconSrc: DeleteTriggerIcon,
onClick: () => trigger.delete(), onClick: () => {
label: "Delete Trigger", const selectedTrigger: ViewModels.Trigger = container.findSelectedTrigger();
}, selectedTrigger && selectedTrigger.delete(selectedTrigger, null);
},
label: "Delete Trigger"
}
]; ];
} }
public static createUserDefinedFunctionContextMenuItems( public static createUserDefinedFunctionContextMenuItems(container: ViewModels.Explorer): TreeNodeMenuItem[] {
container: ViewModels.Explorer,
userDefinedFunction: ViewModels.UserDefinedFunction
): TreeNodeMenuItem[] {
if (container.isPreferredApiCassandra()) { if (container.isPreferredApiCassandra()) {
return []; return [];
} }
@@ -159,9 +157,266 @@ export class ResourceTreeContextMenuButtonFactory {
return [ return [
{ {
iconSrc: DeleteUDFIcon, iconSrc: DeleteUDFIcon,
onClick: () => userDefinedFunction.delete(), onClick: () => {
label: "Delete User Defined Function", const selectedUDF: ViewModels.UserDefinedFunction = container.findSelectedUDF();
}, selectedUDF && selectedUDF.delete(selectedUDF, null);
},
label: "Delete User Defined Function"
}
]; ];
} }
} }
/**
* Current resource tree (in KO)
* TODO: Remove when switching to new resource tree
*/
export class ContextMenuButtonFactory {
public static createDatabaseContextMenuButton(
container: ViewModels.Explorer,
btnParams: DatabaseContextMenuButtonParams
): CommandButtonOptions[] {
const addCollectionId = `${btnParams.databaseId}-${container.addCollectionText()}`;
const deleteDatabaseId = `${btnParams.databaseId}-${container.deleteDatabaseText()}`;
const newCollectionButtonOptions: CommandButtonOptions = {
iconSrc: AddCollectionIcon,
id: addCollectionId,
onCommandClick: () => {
if (container.isPreferredApiCassandra()) {
container.cassandraAddCollectionPane.open();
} else {
container.addCollectionPane.open(container.selectedDatabaseId());
}
const selectedDatabase: ViewModels.Database = container.findSelectedDatabase();
selectedDatabase && selectedDatabase.contextMenu.hide(selectedDatabase, null);
},
commandButtonLabel: container.addCollectionText(),
hasPopup: true
};
const deleteDatabaseButtonOptions: CommandButtonOptions = {
iconSrc: DeleteDatabaseIcon,
id: deleteDatabaseId,
onCommandClick: () => {
const database: ViewModels.Database = container.findSelectedDatabase();
database.onDeleteDatabaseContextMenuClick(database, null);
},
commandButtonLabel: container.deleteDatabaseText(),
hasPopup: true,
disabled: ko.computed<boolean>(() => container.isNoneSelected()),
visible: ko.computed<boolean>(() => !container.isNoneSelected())
};
return [newCollectionButtonOptions, deleteDatabaseButtonOptions];
}
public static createCollectionContextMenuButton(
container: ViewModels.Explorer,
btnParams: CollectionContextMenuButtonParams
): CommandButtonOptions[] {
const newSqlQueryId = `${btnParams.databaseId}-${btnParams.collectionId}-newSqlQuery`;
const newSqlQueryForGraphId = `${btnParams.databaseId}-${btnParams.collectionId}-newSqlQueryForGraph`;
const newQueryForMongoId = `${btnParams.databaseId}-${btnParams.collectionId}-newQuery`;
const newShellForMongoId = `${btnParams.databaseId}-${btnParams.collectionId}-newShell`;
const newStoredProcedureId = `${btnParams.databaseId}-${btnParams.collectionId}-newStoredProcedure`;
const udfId = `${btnParams.databaseId}-${btnParams.collectionId}-udf`;
const newTriggerId = `${btnParams.databaseId}-${btnParams.collectionId}-newTrigger`;
const deleteCollectionId = `${btnParams.databaseId}-${btnParams.collectionId}-${container.deleteCollectionText()}`;
const newSQLQueryButtonOptions: CommandButtonOptions = {
iconSrc: AddSqlQueryIcon,
id: newSqlQueryId,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null);
},
commandButtonLabel: "New SQL Query",
hasPopup: true,
disabled: ko.computed<boolean>(
() => container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiDocumentDB()
),
visible: ko.computed<boolean>(
() => !container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiDocumentDB()
)
//TODO: Merge with add query logic below, same goes for CommandBarButtonFactory
};
const newSQLQueryButtonOptionsForGraph: CommandButtonOptions = {
iconSrc: AddSqlQueryIcon,
id: newSqlQueryForGraphId,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null);
},
commandButtonLabel: "New SQL Query",
hasPopup: true,
disabled: ko.computed<boolean>(() => container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiGraph()),
visible: ko.computed<boolean>(() => !container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiGraph())
};
const newMongoQueryButtonOptions: CommandButtonOptions = {
iconSrc: AddSqlQueryIcon,
id: newQueryForMongoId,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null);
},
commandButtonLabel: "New Query",
hasPopup: true,
disabled: ko.computed<boolean>(
() => container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB()
),
visible: ko.computed<boolean>(
() => !container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB()
)
};
const newMongoShellButtonOptions: CommandButtonOptions = {
iconSrc: HostedTerminalIcon,
id: newShellForMongoId,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewMongoShellClick();
},
commandButtonLabel: "New Shell",
hasPopup: true,
disabled: ko.computed<boolean>(
() => container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB()
),
visible: ko.computed<boolean>(
() => !container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB()
)
};
const newStoredProcedureButtonOptions: CommandButtonOptions = {
iconSrc: AddStoredProcedureIcon,
id: newStoredProcedureId,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null);
},
commandButtonLabel: "New Stored Procedure",
hasPopup: true,
disabled: ko.computed<boolean>(() => container.isDatabaseNodeOrNoneSelected()),
visible: ko.computed<boolean>(
() => !container.isDatabaseNodeOrNoneSelected() && !container.isPreferredApiCassandra()
)
};
const newUserDefinedFunctionButtonOptions: CommandButtonOptions = {
iconSrc: AddUdfIcon,
id: udfId,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection, null);
},
commandButtonLabel: "New UDF",
hasPopup: true,
disabled: ko.computed<boolean>(() => container.isDatabaseNodeOrNoneSelected()),
visible: ko.computed<boolean>(
() => !container.isDatabaseNodeOrNoneSelected() && !container.isPreferredApiCassandra()
)
};
const newTriggerButtonOptions: CommandButtonOptions = {
iconSrc: AddTriggerIcon,
id: newTriggerId,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, null);
},
commandButtonLabel: "New Trigger",
hasPopup: true,
disabled: ko.computed<boolean>(() => container.isDatabaseNodeOrNoneSelected()),
visible: ko.computed<boolean>(
() => !container.isDatabaseNodeOrNoneSelected() && !container.isPreferredApiCassandra()
)
};
const deleteCollectionButtonOptions: CommandButtonOptions = {
iconSrc: DeleteCollectionIcon,
id: deleteCollectionId,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onDeleteCollectionContextMenuClick(selectedCollection, null);
},
commandButtonLabel: container.deleteCollectionText(),
hasPopup: true,
disabled: ko.computed<boolean>(() => container.isDatabaseNodeOrNoneSelected()),
visible: ko.computed<boolean>(() => !container.isDatabaseNodeOrNoneSelected())
//TODO: Change to isCollectionNodeorNoneSelected and same in CommandBarButtonFactory
};
return [
newSQLQueryButtonOptions,
newSQLQueryButtonOptionsForGraph,
newMongoQueryButtonOptions,
newMongoShellButtonOptions,
newStoredProcedureButtonOptions,
newUserDefinedFunctionButtonOptions,
newTriggerButtonOptions,
deleteCollectionButtonOptions
];
}
public static createStoreProcedureContextMenuButton(container: ViewModels.Explorer): CommandButtonOptions[] {
const deleteStoredProcedureId = "Context Menu - Delete Stored Procedure";
const deleteStoreProcedureButtonOptions: CommandButtonOptions = {
iconSrc: DeleteSprocIcon,
id: deleteStoredProcedureId,
onCommandClick: () => {
const selectedStoreProcedure: ViewModels.StoredProcedure = container.findSelectedStoredProcedure();
selectedStoreProcedure && selectedStoreProcedure.delete(selectedStoreProcedure, null);
},
commandButtonLabel: "Delete Stored Procedure",
hasPopup: false,
disabled: ko.computed<boolean>(() => container.isDatabaseNodeOrNoneSelected()),
visible: ko.computed<boolean>(
() => !container.isDatabaseNodeOrNoneSelected() && !container.isPreferredApiCassandra()
)
};
return [deleteStoreProcedureButtonOptions];
}
public static createTriggerContextMenuButton(container: ViewModels.Explorer): CommandButtonOptions[] {
const deleteTriggerId = "Context Menu - Delete Trigger";
const deleteTriggerButtonOptions: CommandButtonOptions = {
iconSrc: DeleteTriggerIcon,
id: deleteTriggerId,
onCommandClick: () => {
const selectedTrigger: ViewModels.Trigger = container.findSelectedTrigger();
selectedTrigger && selectedTrigger.delete(selectedTrigger, null);
},
commandButtonLabel: "Delete Trigger",
hasPopup: false,
disabled: ko.computed<boolean>(() => container.isDatabaseNodeOrNoneSelected()),
visible: ko.computed<boolean>(
() => !container.isDatabaseNodeOrNoneSelected() && !container.isPreferredApiCassandra()
)
};
return [deleteTriggerButtonOptions];
}
public static createUserDefinedFunctionContextMenuButton(container: ViewModels.Explorer): CommandButtonOptions[] {
const deleteUserDefinedFunctionId = "Context Menu - Delete User Defined Function";
const deleteUserDefinedFunctionButtonOptions: CommandButtonOptions = {
iconSrc: DeleteUDFIcon,
id: deleteUserDefinedFunctionId,
onCommandClick: () => {
const selectedUDF: ViewModels.UserDefinedFunction = container.findSelectedUDF();
selectedUDF && selectedUDF.delete(selectedUDF, null);
},
commandButtonLabel: "Delete User Defined Function",
hasPopup: false,
disabled: ko.computed<boolean>(() => container.isDatabaseNodeOrNoneSelected()),
visible: ko.computed<boolean>(
() => !container.isDatabaseNodeOrNoneSelected() && !container.isPreferredApiCassandra()
)
};
return [deleteUserDefinedFunctionButtonOptions];
}
}

View File

@@ -31,7 +31,7 @@ export class AccessibleElement extends React.Component<AccessibleElementProps> {
...elementProps, ...elementProps,
onKeyPress: this.onKeyPress, onKeyPress: this.onKeyPress,
onClick: this.props.onActivated, onClick: this.props.onActivated,
tabIndex, tabIndex
}); });
} }
} }

View File

@@ -38,7 +38,7 @@ export class AccordionItemComponent extends React.Component<AccordionItemCompone
super(props); super(props);
this.isExpanded = props.isExpanded; this.isExpanded = props.isExpanded;
this.state = { this.state = {
isExpanded: true, isExpanded: true
}; };
} }
@@ -46,7 +46,7 @@ export class AccordionItemComponent extends React.Component<AccordionItemCompone
if (this.props.isExpanded !== this.isExpanded) { if (this.props.isExpanded !== this.isExpanded) {
this.isExpanded = this.props.isExpanded; this.isExpanded = this.props.isExpanded;
this.setState({ this.setState({
isExpanded: this.props.isExpanded, isExpanded: this.props.isExpanded
}); });
} }
} }

View File

@@ -16,7 +16,7 @@ const createBlankProps = (): AccountSwitchComponentProps => {
subscriptions: [], subscriptions: [],
selectedSubscriptionId: null, selectedSubscriptionId: null,
isLoadingSubscriptions: false, isLoadingSubscriptions: false,
onSubscriptionChange: jest.fn(), onSubscriptionChange: jest.fn()
}; };
}; };
@@ -28,7 +28,7 @@ const createBlankAccount = (): DatabaseAccount => {
properties: null, properties: null,
location: "", location: "",
tags: null, tags: null,
type: "", type: ""
}; };
}; };
@@ -40,7 +40,7 @@ const createBlankSubscription = (): Subscription => {
state: "", state: "",
subscriptionPolicies: null, subscriptionPolicies: null,
tenantId: "", tenantId: "",
uniqueDisplayName: "", uniqueDisplayName: ""
}; };
}; };

View File

@@ -34,13 +34,13 @@ export class AccountSwitchComponent extends React.Component<AccountSwitchCompone
items: [ items: [
{ {
key: "switchSubscription", key: "switchSubscription",
onRender: this._renderSubscriptionDropdown.bind(this), onRender: this._renderSubscriptionDropdown.bind(this)
}, },
{ {
key: "switchAccount", key: "switchAccount",
onRender: this._renderAccountDropDown.bind(this), onRender: this._renderAccountDropDown.bind(this)
}, }
], ]
}; };
const buttonStyles: IButtonStyles = { const buttonStyles: IButtonStyles = {
@@ -51,27 +51,27 @@ export class AccountSwitchComponent extends React.Component<AccountSwitchCompone
paddingLeft: 10, paddingLeft: 10,
marginRight: 5, marginRight: 5,
backgroundColor: StyleConstants.BaseDark, backgroundColor: StyleConstants.BaseDark,
color: StyleConstants.BaseLight, color: StyleConstants.BaseLight
}, },
rootHovered: { rootHovered: {
backgroundColor: StyleConstants.BaseHigh, backgroundColor: StyleConstants.BaseHigh,
color: StyleConstants.BaseLight, color: StyleConstants.BaseLight
}, },
rootFocused: { rootFocused: {
backgroundColor: StyleConstants.BaseHigh, backgroundColor: StyleConstants.BaseHigh,
color: StyleConstants.BaseLight, color: StyleConstants.BaseLight
}, },
rootPressed: { rootPressed: {
backgroundColor: StyleConstants.BaseHigh, backgroundColor: StyleConstants.BaseHigh,
color: StyleConstants.BaseLight, color: StyleConstants.BaseLight
}, },
rootExpanded: { rootExpanded: {
backgroundColor: StyleConstants.BaseHigh, backgroundColor: StyleConstants.BaseHigh,
color: StyleConstants.BaseLight, color: StyleConstants.BaseLight
}, },
textContainer: { textContainer: {
flexGrow: "initial", flexGrow: "initial"
}, }
}; };
const buttonProps: IButtonProps = { const buttonProps: IButtonProps = {
@@ -79,7 +79,7 @@ export class AccountSwitchComponent extends React.Component<AccountSwitchCompone
menuProps: menuProps, menuProps: menuProps,
styles: buttonStyles, styles: buttonStyles,
className: "accountSwitchButton", className: "accountSwitchButton",
id: "accountSwitchButton", id: "accountSwitchButton"
}; };
return <DefaultButton {...buttonProps} />; return <DefaultButton {...buttonProps} />;
@@ -87,11 +87,11 @@ export class AccountSwitchComponent extends React.Component<AccountSwitchCompone
private _renderSubscriptionDropdown(): JSX.Element { private _renderSubscriptionDropdown(): JSX.Element {
const { subscriptions, selectedSubscriptionId, isLoadingSubscriptions } = this.props; const { subscriptions, selectedSubscriptionId, isLoadingSubscriptions } = this.props;
const options: IDropdownOption[] = subscriptions.map((sub) => { const options: IDropdownOption[] = subscriptions.map(sub => {
return { return {
key: sub.subscriptionId, key: sub.subscriptionId,
text: sub.displayName, text: sub.displayName,
data: sub, data: sub
}; };
}); });
@@ -109,8 +109,8 @@ export class AccountSwitchComponent extends React.Component<AccountSwitchCompone
defaultSelectedKey: selectedSubscriptionId, defaultSelectedKey: selectedSubscriptionId,
placeholder: placeHolderText, placeholder: placeHolderText,
styles: { styles: {
callout: "accountSwitchSubscriptionDropdownMenu", callout: "accountSwitchSubscriptionDropdownMenu"
}, }
}; };
return <Dropdown {...dropdownProps} />; return <Dropdown {...dropdownProps} />;
@@ -126,11 +126,11 @@ export class AccountSwitchComponent extends React.Component<AccountSwitchCompone
private _renderAccountDropDown(): JSX.Element { private _renderAccountDropDown(): JSX.Element {
const { accounts, selectedAccountName, isLoadingAccounts } = this.props; const { accounts, selectedAccountName, isLoadingAccounts } = this.props;
const options: IDropdownOption[] = accounts.map((account) => { const options: IDropdownOption[] = accounts.map(account => {
return { return {
key: account.name, key: account.name,
text: account.name, text: account.name,
data: account, data: account
}; };
}); });
// Fabric UI will also try to select the first non-disabled option from dropdown. // Fabric UI will also try to select the first non-disabled option from dropdown.
@@ -138,7 +138,7 @@ export class AccountSwitchComponent extends React.Component<AccountSwitchCompone
options.unshift({ options.unshift({
key: "select from list", key: "select from list",
text: "Select Cosmos DB account from list", text: "Select Cosmos DB account from list",
data: undefined, data: undefined
}); });
const placeHolderText = isLoadingAccounts const placeHolderText = isLoadingAccounts
@@ -155,8 +155,8 @@ export class AccountSwitchComponent extends React.Component<AccountSwitchCompone
defaultSelectedKey: selectedAccountName, defaultSelectedKey: selectedAccountName,
placeholder: placeHolderText, placeholder: placeHolderText,
styles: { styles: {
callout: "accountSwitchAccountDropdownMenu", callout: "accountSwitchAccountDropdownMenu"
}, }
}; };
return <Dropdown {...dropdownProps} />; return <Dropdown {...dropdownProps} />;

View File

@@ -4,9 +4,9 @@ import { DefaultButton, IButtonStyles } from "office-ui-fabric-react/lib/Button"
import { import {
IContextualMenuItem, IContextualMenuItem,
IContextualMenuProps, IContextualMenuProps,
ContextualMenuItemType, ContextualMenuItemType
} from "office-ui-fabric-react/lib/ContextualMenu"; } from "office-ui-fabric-react/lib/ContextualMenu";
import * as Logger from "../../../Common/Logger"; import { Logger } from "../../../Common/Logger";
export interface ArcadiaMenuPickerProps { export interface ArcadiaMenuPickerProps {
selectText?: string; selectText?: string;
@@ -33,7 +33,7 @@ export class ArcadiaMenuPicker extends React.Component<ArcadiaMenuPickerProps, A
constructor(props: ArcadiaMenuPickerProps) { constructor(props: ArcadiaMenuPickerProps) {
super(props); super(props);
this.state = { this.state = {
selectedSparkPool: props.selectedSparkPool, selectedSparkPool: props.selectedSparkPool
}; };
} }
@@ -44,7 +44,7 @@ export class ArcadiaMenuPicker extends React.Component<ArcadiaMenuPickerProps, A
try { try {
this.props.onSparkPoolSelect(e, item); this.props.onSparkPoolSelect(e, item);
this.setState({ this.setState({
selectedSparkPool: item.text, selectedSparkPool: item.text
}); });
} catch (error) { } catch (error) {
Logger.logError(error, "ArcadiaMenuPicker/_onSparkPoolClicked"); Logger.logError(error, "ArcadiaMenuPicker/_onSparkPoolClicked");
@@ -68,28 +68,28 @@ export class ArcadiaMenuPicker extends React.Component<ArcadiaMenuPickerProps, A
public render() { public render() {
const { workspaces } = this.props; const { workspaces } = this.props;
let workspaceMenuItems: IContextualMenuItem[] = workspaces.map((workspace) => { let workspaceMenuItems: IContextualMenuItem[] = workspaces.map(workspace => {
let sparkPoolsMenuProps: IContextualMenuProps = { let sparkPoolsMenuProps: IContextualMenuProps = {
items: workspace.sparkPools.map( items: workspace.sparkPools.map(
(sparkpool): IContextualMenuItem => ({ (sparkpool): IContextualMenuItem => ({
key: sparkpool.id, key: sparkpool.id,
text: sparkpool.name, text: sparkpool.name,
onClick: this._onSparkPoolClicked, onClick: this._onSparkPoolClicked
}) })
), )
}; };
if (!sparkPoolsMenuProps.items.length) { if (!sparkPoolsMenuProps.items.length) {
sparkPoolsMenuProps.items.push({ sparkPoolsMenuProps.items.push({
key: workspace.id, key: workspace.id,
text: "Create new spark pool", text: "Create new spark pool",
onClick: this._onCreateNewSparkPoolClicked, onClick: this._onCreateNewSparkPoolClicked
}); });
} }
return { return {
key: workspace.id, key: workspace.id,
text: workspace.name, text: workspace.name,
subMenuProps: this.props.disableSubmenu ? undefined : sparkPoolsMenuProps, subMenuProps: this.props.disableSubmenu ? undefined : sparkPoolsMenuProps
}; };
}); });
@@ -97,7 +97,7 @@ export class ArcadiaMenuPicker extends React.Component<ArcadiaMenuPickerProps, A
workspaceMenuItems.push({ workspaceMenuItems.push({
key: "create_workspace", key: "create_workspace",
text: "Create new workspace", text: "Create new workspace",
onClick: this._onCreateNewWorkspaceClicked, onClick: this._onCreateNewWorkspaceClicked
}); });
} }
@@ -106,29 +106,29 @@ export class ArcadiaMenuPicker extends React.Component<ArcadiaMenuPickerProps, A
backgroundColor: "transparent", backgroundColor: "transparent",
margin: "auto 5px", margin: "auto 5px",
padding: "0", padding: "0",
border: "0", border: "0"
}, },
rootHovered: { rootHovered: {
backgroundColor: "transparent", backgroundColor: "transparent"
}, },
rootChecked: { rootChecked: {
backgroundColor: "transparent", backgroundColor: "transparent"
}, },
rootFocused: { rootFocused: {
backgroundColor: "transparent", backgroundColor: "transparent"
}, },
rootExpanded: { rootExpanded: {
backgroundColor: "transparent", backgroundColor: "transparent"
}, },
flexContainer: { flexContainer: {
height: "30px", height: "30px",
border: "1px solid #a6a6a6", border: "1px solid #a6a6a6",
padding: "0 8px", padding: "0 8px"
}, },
label: { label: {
fontWeight: "400", fontWeight: "400",
fontSize: "12px", fontSize: "12px"
}, }
}; };
return ( return (
@@ -137,7 +137,7 @@ export class ArcadiaMenuPicker extends React.Component<ArcadiaMenuPickerProps, A
persistMenu={true} persistMenu={true}
className="arcadia-menu-picker" className="arcadia-menu-picker"
menuProps={{ menuProps={{
items: workspaceMenuItems, items: workspaceMenuItems
}} }}
styles={dropdownStyle} styles={dropdownStyle}
/> />

View File

@@ -1,56 +1,56 @@
import * as ko from "knockout"; import * as ko from "knockout";
import template from "./collapsible-panel-component.html"; import template from "./collapsible-panel-component.html";
/** /**
* Helper class for ko component registration * Helper class for ko component registration
*/ */
export class CollapsiblePanelComponent { export class CollapsiblePanelComponent {
constructor() { constructor() {
return { return {
viewModel: CollapsiblePanelViewModel, viewModel: CollapsiblePanelViewModel,
template, template
}; };
} }
} }
/** /**
* Parameters for this component * Parameters for this component
*/ */
interface CollapsiblePanelParams { interface CollapsiblePanelParams {
collapsedTitle: ko.Observable<string>; collapsedTitle: ko.Observable<string>;
expandedTitle: ko.Observable<string>; expandedTitle: ko.Observable<string>;
isCollapsed?: ko.Observable<boolean>; isCollapsed?: ko.Observable<boolean>;
collapseToLeft?: boolean; collapseToLeft?: boolean;
} }
/** /**
* Collapsible panel: * Collapsible panel:
* Contains a header with [>] button to collapse and an title ("expandedTitle"). * Contains a header with [>] button to collapse and an title ("expandedTitle").
* Collapsing the panel: * Collapsing the panel:
* - shrinks width to narrow amount * - shrinks width to narrow amount
* - hides children * - hides children
* - shows [<] * - shows [<]
* - shows vertical title ("collapsedTitle") * - shows vertical title ("collapsedTitle")
* - the default behavior is to collapse to the right (ie, place this component on the right or use "collapseToLeft" parameter) * - the default behavior is to collapse to the right (ie, place this component on the right or use "collapseToLeft" parameter)
* *
* How to use in your markup: * How to use in your markup:
* <collapsible-panel params="{ collapsedTitle:'Properties', expandedTitle:'Expanded properties' }"> * <collapsible-panel params="{ collapsedTitle:'Properties', expandedTitle:'Expanded properties' }">
* <!-- add your markup here: the ko context is the same as outside of collapsible-panel (ie $data) --> * <!-- add your markup here: the ko context is the same as outside of collapsible-panel (ie $data) -->
* </collapsible-panel> * </collapsible-panel>
* *
* Use the optional "isCollapsed" parameter to programmatically collapse/expand the pane from outside the component. * Use the optional "isCollapsed" parameter to programmatically collapse/expand the pane from outside the component.
* Use the optional "collapseToLeft" parameter to collapse to the left. * Use the optional "collapseToLeft" parameter to collapse to the left.
*/ */
class CollapsiblePanelViewModel { class CollapsiblePanelViewModel {
private params: CollapsiblePanelParams; private params: CollapsiblePanelParams;
private isCollapsed: ko.Observable<boolean>; private isCollapsed: ko.Observable<boolean>;
public constructor(params: CollapsiblePanelParams) { public constructor(params: CollapsiblePanelParams) {
this.params = params; this.params = params;
this.isCollapsed = params.isCollapsed || ko.observable(false); this.isCollapsed = params.isCollapsed || ko.observable(false);
} }
private toggleCollapse(): void { private toggleCollapse(): void {
this.isCollapsed(!this.isCollapsed()); this.isCollapsed(!this.isCollapsed());
} }
} }

View File

@@ -1,44 +1,44 @@
<div class="collapsiblePanel" data-bind="css: { paneCollapsed:isCollapsed() }"> <div class="collapsiblePanel" data-bind="css: { paneCollapsed:isCollapsed() }">
<div class="panelHeader" data-bind="visible: !isCollapsed()"> <div class="panelHeader" data-bind="visible: !isCollapsed()">
<span <span
class="collapsedIconContainer collapseExpandButton" class="collapsedIconContainer collapseExpandButton"
data-bind="click:toggleCollapse, css: { 'pull-right':params.collapseToLeft }" data-bind="click:toggleCollapse, css: { 'pull-right':params.collapseToLeft }"
> >
<img <img
class="collapsedIcon imgVerticalAlignment" class="collapsedIcon imgVerticalAlignment"
src="/imgarrowlefticon.svg" src="/imgarrowlefticon.svg"
alt="Collapse" alt="Collapse"
data-bind="css: { expanded:!isCollapsed(), iconMirror:params.collapseToLeft }" data-bind="css: { expanded:!isCollapsed(), iconMirror:params.collapseToLeft }"
/> />
</span> </span>
<span <span
class="expandedTitle" class="expandedTitle"
data-bind="text: params.expandedTitle, css:{ iconSpacer:!params.collapseToLeft }" data-bind="text: params.expandedTitle, css:{ iconSpacer:!params.collapseToLeft }"
></span> ></span>
</div> </div>
<div class="collapsibleNav nav" data-bind="visible:isCollapsed"> <div class="collapsibleNav nav" data-bind="visible:isCollapsed">
<ul class="nav"> <ul class="nav">
<li class="collapsedBtn collapseExpandButton"> <li class="collapsedBtn collapseExpandButton">
<span class="collapsedIconContainer" data-bind="click: toggleCollapse"> <span class="collapsedIconContainer" data-bind="click: toggleCollapse">
<img <img
class="collapsedIcon" class="collapsedIcon"
src="/imgarrowlefticon.svg" src="/imgarrowlefticon.svg"
data-bind="css: { expanded:!isCollapsed(), iconMirror:params.collapseToLeft }" data-bind="css: { expanded:!isCollapsed(), iconMirror:params.collapseToLeft }"
alt="Expand" alt="Expand"
/> />
</span> </span>
<span class="rotatedInner" data-bind="click: toggleCollapse"> <span class="rotatedInner" data-bind="click: toggleCollapse">
<span data-bind="text: params.collapsedTitle"></span> <span data-bind="text: params.collapsedTitle"></span>
</span> </span>
</li> </li>
</ul> </ul>
</div> </div>
<div class="panelContent" data-bind="visible:!isCollapsed()"> <div class="panelContent" data-bind="visible:!isCollapsed()">
<!-- ko with:$parent --> <!-- ko with:$parent -->
<!-- ko template: { nodes: $componentTemplateNodes } --> <!-- ko template: { nodes: $componentTemplateNodes } -->
<!-- /ko --> <!-- /ko -->
<!-- /ko --> <!-- /ko -->
</div> </div>
</div> </div>

View File

@@ -0,0 +1,200 @@
@import "../../../../less/Common/Constants";
@ButtonIconSize: 18px;
.commandBar {
padding-left: @DefaultSpace;
border-bottom: @ButtonBorderWidth solid @BaseMedium;
display: flex;
overflow: hidden;
height: @topcommandbarheight;
.staticCommands {
list-style: none;
margin: 0px;
padding: 0px;
display: flex;
flex: 0 0 auto;
}
.overflowCommands {
display:flex;
flex: 1 0 auto;
.visibleCommands {
display: inline-flex;
list-style: none;
margin: 0px;
padding: 0px;
}
.partialSplitterContainer {
padding: @SmallSpace @DefaultSpace @SmallSpace @SmallSpace;
.flex-display();
}
}
.commandExpand {
border: none;
padding: 0px;
direction: rtl;
&:hover {
.hover();
cursor: pointer;
& > .commandDropdownContainer {
display: block !important; // TODO: Remove after reusing KO mouseover and mouseout event handlers
}
}
&:focus {
.focus();
}
.commandDropdownLauncher {
direction: ltr;
padding-top: @SmallSpace;
.commandIcon {
vertical-align: text-top;
}
.commandBarEllipses {
font-weight: bold;
font-size: 20px;
}
}
}
.hiddenCommandsContainer > .commandDropdownLauncher {
padding: 0px @DefaultSpace;
}
.commandDropdownContainer {
display: none;
z-index: 1000;
direction: ltr;
position: absolute;
width: fit-content;
padding: 0px;
background-color: @BaseLight;
box-shadow: 1px 2px 6px @BaseMediumHigh, -2px 2px 6px @BaseMediumHigh;
.commandDropdown {
display: flex;
flex-direction: column;
padding: 0px;
margin: 0px;
}
}
.feedbackButton {
margin-right: @LargeSpace;
white-space: nowrap;
}
}
command-button,
.commandButtonReact {
display: inline-flex;
.commandButtonComponent {
width: 100%;
color: @BaseHigh;
background-color: transparent;
text-decoration: none;
border: @ButtonBorderWidth solid transparent;
.flex-display();
&:hover:not(.commandDisabled) {
cursor: pointer;
.hover();
}
&:active:not(.commandDisabled) {
border: @ButtonBorderWidth dashed @AccentMedium;
.active();
}
&:focus:not(.commandDisabled) {
border: @ButtonBorderWidth dashed @AccentMedium;
}
.commandContent {
padding: @DefaultSpace @DefaultSpace @DefaultSpace;
flex: 0 0 auto;
.commandIcon {
margin: 0 @SmallSpace 0 0;
vertical-align: text-top;
width: @ButtonIconSize;
height: @ButtonIconSize;
}
.commandLabel {
padding: 0px;
}
}
.commandContent .hasHiddenItems {
padding-right: @SmallSpace;
}
}
.commandButtonComponent.commandDisabled {
color: @BaseMediumHigh;
opacity: 0.5;
}
.commandExpand {
padding-top: @SmallSpace;
padding-bottom: @SmallSpace;
&:hover {
.hover();
& > .commandDropdownContainer {
display: block !important; // TODO: Remove after reusing KO mouseover and mouseout event handlers
}
}
&:focus {
.focus();
}
.commandDropdownLauncher {
cursor: pointer;
display: inline-flex;
.commandButtonComponent {
padding: 0px;
}
}
.expandDropdown {
padding: @SmallSpace;
img {
vertical-align: top;
}
}
.partialSplitter {
margin: @SmallSpace 0px 6px;
}
}
.commandButtonComponent[tabindex]:focus {
outline: none;
}
.selectedButton {
background-color: @AccentLow;
outline: none
}
}
.partialSplitter {
border-left: @ButtonBorderWidth solid @BaseMediumHigh;
}
.commandDropdown .commandButtonComponent {
padding-left: 0px;
}

View File

@@ -0,0 +1,139 @@
import * as ko from "knockout";
import { CommandButtonComponent, CommandButtonOptions } from "./CommandButton";
const mockLabel = "Some Label";
const id = "Some id";
function buildComponent(buttonOptions: any) {
document.body.innerHTML = CommandButtonComponent.template as any;
const vm = new CommandButtonComponent.viewModel(buttonOptions);
ko.applyBindings(vm);
}
describe("Command Button Component", () => {
function buildButtonOptions(
onClick: () => void,
id?: string,
label?: string,
disabled?: ko.Observable<boolean>,
visible?: ko.Observable<boolean>,
tooltipText?: string
): { buttonProps: CommandButtonOptions } {
return {
buttonProps: {
iconSrc: "images/AddCollection.svg",
id: id,
commandButtonLabel: label || mockLabel,
disabled: disabled,
visible: visible,
tooltipText: tooltipText,
hasPopup: false,
onCommandClick: onClick
}
};
}
function buildSplitterButtonOptions(
onClick: () => void,
id?: string,
label?: string,
disabled?: ko.Observable<boolean>,
visible?: ko.Observable<boolean>,
tooltipText?: string
): { buttonProps: CommandButtonOptions } {
const child: CommandButtonOptions = {
iconSrc: "images/settings_15x15.svg",
id: id,
commandButtonLabel: label || mockLabel,
disabled: disabled,
visible: visible,
tooltipText: tooltipText,
hasPopup: false,
onCommandClick: onClick
};
return {
buttonProps: {
iconSrc: "images/AddCollection.svg",
id: id,
commandButtonLabel: label || mockLabel,
disabled: disabled,
visible: visible,
tooltipText: tooltipText,
hasPopup: false,
onCommandClick: onClick,
children: [child]
}
};
}
afterEach(() => {
ko.cleanNode(document);
document.body.innerHTML = "";
});
describe("Rendering", () => {
it("should display button label", () => {
const buttonOptions = buildButtonOptions(() => {
/** do nothing **/
}, mockLabel);
buildComponent(buttonOptions);
expect(document.getElementsByClassName("commandButtonComponent").item(0).textContent).toContain(mockLabel);
});
it("should display button icon", () => {
const buttonOptions = buildButtonOptions(() => {
/** do nothing **/
});
buildComponent(buttonOptions);
expect(
document
.getElementsByTagName("img")
.item(0)
.getAttribute("src")
).toBeDefined();
});
});
describe("Behavior", () => {
let clickSpy: jasmine.Spy;
beforeEach(() => {
clickSpy = jasmine.createSpy("Command button click spy");
});
it("should trigger the click handler when the command button is clicked", () => {
const buttonOptions = buildButtonOptions(() => clickSpy());
buildComponent(buttonOptions);
document
.getElementsByClassName("commandButtonComponent")
.item(0)
.dispatchEvent(new Event("click"));
expect(clickSpy).toHaveBeenCalled();
});
it("should not trigger the click handler when command button is disabled", () => {
const buttonOptions = buildButtonOptions(() => clickSpy(), id, mockLabel, ko.observable(true));
buildComponent(buttonOptions);
document
.getElementsByClassName("commandButtonComponent")
.item(0)
.dispatchEvent(new Event("click"));
expect(clickSpy).not.toHaveBeenCalled();
});
it("should not have a dropdown if it has no child", () => {
const buttonOptions = buildButtonOptions(() => clickSpy(), id, mockLabel, ko.observable(true));
buildComponent(buttonOptions);
const dropdownSize = document.getElementsByClassName("commandExpand").length;
expect(dropdownSize).toBe(0);
});
it("should have a dropdown if it has a child", () => {
const buttonOptions = buildSplitterButtonOptions(() => clickSpy(), id, mockLabel, ko.observable(true));
buildComponent(buttonOptions);
const dropdownSize = document.getElementsByClassName("commandExpand").length;
expect(dropdownSize).toBe(1);
});
});
});

View File

@@ -0,0 +1,191 @@
/**
* How to use this component:
*
* In your html markup, use:
* <command-button params="{
* iconSrc: '/icon/example/src/',
* onCommandClick: () => { doSomething },
* commandButtonLabel: 'Some Label'
* disabled: true/false
* }">
* </command-button>
*
*/
import * as ko from "knockout";
import * as ViewModels from "../../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel";
import { KeyCodes } from "../../../Common/Constants";
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import template from "./command-button.html";
/**
* Options for this component
*/
export interface CommandButtonOptions {
/**
* image source for the button icon
*/
iconSrc: string;
/**
* Id for the button icon
*/
id: string;
/**
* Click handler for command button click
*/
onCommandClick: () => void;
/**
* Label for the button
*/
commandButtonLabel: string | ko.Observable<string>;
/**
* True if this button opens a tab or pane, false otherwise.
*/
hasPopup: boolean;
/**
* Enabled/disabled state of command button
*/
disabled?: ko.Subscribable<boolean>;
/**
* Visibility/Invisibility of the button
*/
visible?: ko.Subscribable<boolean>;
/**
* Whether or not the button should have the 'selectedButton' styling
*/
isSelected?: ko.Observable<boolean>;
/**
* Text to displayed in the tooltip on hover
*/
tooltipText?: string | ko.Observable<string>;
/**
* Callback triggered when the template is bound to the component
*/
onTemplateReady?: () => void;
/**
* tabindex for the command button
*/
tabIndex?: ko.Observable<number>;
/**
* Childrens command buttons to hide in the dropdown
*/
children?: CommandButtonOptions[];
}
export class CommandButtonViewModel extends WaitsForTemplateViewModel implements ViewModels.CommandButton {
public commandClickCallback: () => void;
public commandButtonId: string;
public disabled: ko.Subscribable<boolean>;
public visible: ko.Subscribable<boolean>;
public isSelected: ko.Observable<boolean>;
public iconSrc: string;
public commandButtonLabel: ko.Observable<string>;
public tooltipText: ko.Observable<string>;
public tabIndex: ko.Observable<number>;
public isTemplateReady: ko.Observable<boolean>;
public hasPopup: boolean;
public children: ko.ObservableArray<CommandButtonOptions>;
public constructor(options: { buttonProps: CommandButtonOptions }) {
super();
const props = options.buttonProps;
const commandButtonLabel = props.commandButtonLabel;
const tooltipText = props.tooltipText;
this.commandButtonLabel =
typeof commandButtonLabel === "string" ? ko.observable<string>(commandButtonLabel) : commandButtonLabel;
this.commandButtonId = props.id;
this.disabled = props.disabled || ko.observable(false);
this.visible = props.visible || ko.observable(true);
this.isSelected = props.isSelected || ko.observable(false);
this.iconSrc = props.iconSrc;
this.tabIndex = props.tabIndex || ko.observable(0);
this.hasPopup = props.hasPopup;
this.children = ko.observableArray(props.children);
super.onTemplateReady((isTemplateReady: boolean) => {
if (isTemplateReady && props.onTemplateReady) {
props.onTemplateReady();
}
});
if (tooltipText && typeof tooltipText === "string") {
this.tooltipText = ko.observable<string>(tooltipText);
} else if (tooltipText && typeof tooltipText === "function") {
this.tooltipText = tooltipText;
} else {
this.tooltipText = this.commandButtonLabel;
}
this.commandClickCallback = () => {
if (this.disabled()) {
return;
}
const el = document.querySelector(".commandDropdownContainer") as HTMLElement;
if (el) {
el.style.display = "none";
}
props.onCommandClick();
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
commandButtonClicked: this.commandButtonLabel
});
};
}
public onKeyPress(source: any, event: KeyboardEvent): boolean {
if (event.keyCode === KeyCodes.Space || event.keyCode === KeyCodes.Enter) {
this.commandClickCallback && this.commandClickCallback();
event.stopPropagation();
return false;
}
return true;
}
public onLauncherKeyDown(source: any, event: KeyboardEvent): boolean {
// TODO: Convert JQuery code into Knockout
if (event.keyCode === KeyCodes.DownArrow) {
$(event.target)
.parent()
.siblings()
.children(".commandExpand")
.children(".commandDropdownContainer")
.hide();
$(event.target)
.children(".commandDropdownContainer")
.show()
.focus();
event.stopPropagation();
return false;
}
if (event.keyCode === KeyCodes.UpArrow) {
$(event.target)
.children(".commandDropdownContainer")
.hide();
event.stopPropagation();
return false;
}
return true;
}
}
/**
* Helper class for ko component registration
*/
export const CommandButtonComponent = {
viewModel: CommandButtonViewModel,
template
};

View File

@@ -15,20 +15,15 @@ import { ArcadiaMenuPickerProps } from "../Arcadia/ArcadiaMenuPicker";
* Options for this component * Options for this component
*/ */
export interface CommandButtonComponentProps { export interface CommandButtonComponentProps {
/**
* font icon name for the button
*/
iconName?: string;
/** /**
* image source for the button icon * image source for the button icon
*/ */
iconSrc?: string; iconSrc: string;
/** /**
* image alt for accessibility * image alt for accessibility
*/ */
iconAlt?: string; iconAlt: string;
/** /**
* Click handler for command button click * Click handler for command button click
@@ -149,7 +144,9 @@ export class CommandButtonComponent extends React.Component<CommandButtonCompone
private onLauncherKeyDown(event: React.KeyboardEvent<HTMLDivElement>): boolean { private onLauncherKeyDown(event: React.KeyboardEvent<HTMLDivElement>): boolean {
if (event.keyCode === KeyCodes.DownArrow) { if (event.keyCode === KeyCodes.DownArrow) {
$(this.dropdownElt).hide(); $(this.dropdownElt).hide();
$(this.dropdownElt).show().focus(); $(this.dropdownElt)
.show()
.focus();
event.stopPropagation(); event.stopPropagation();
return false; return false;
} }
@@ -185,7 +182,7 @@ export class CommandButtonComponent extends React.Component<CommandButtonCompone
} }
this.props.onCommandClick(e); this.props.onCommandClick(e);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
commandButtonClicked: this.props.commandButtonLabel, commandButtonClicked: this.props.commandButtonLabel
}); });
} }

View File

@@ -0,0 +1,40 @@
<span
class="commandButtonComponent"
role="menuitem"
tabindex="0"
data-bind="setTemplateReady: true,
css: {
commandDisabled: disabled,
selectedButton: isSelected
},
event: {
keypress: onKeyPress
},
attr: {
title: tooltipText,
id: commandButtonId,
tabindex: tabIndex ,
'aria-disabled': disabled,
'aria-haspopup': hasPopup
},
click: commandClickCallback,
visible: visible"
>
<div class="commandContent" data-bind="css: { hasHiddenItems: children().length > 0 }">
<img class="commandIcon" data-bind="attr: {src: iconSrc, alt: commandButtonLabel}" />
<span class="commandLabel" data-bind="text: commandButtonLabel"></span>
</div>
</span>
<!-- ko if: children().length > 0 -->
<div class="commandExpand" tabindex="0" data-bind="visible: visible, event: { keydown: onLauncherKeyDown }">
<div class="commandDropdownLauncher">
<span class="partialSplitter"></span>
<span class="expandDropdown"> <img src="/QueryBuilder/CollapseChevronDown_16x.png" /> </span>
</div>
<div class="commandDropdownContainer">
<div class="commandDropdown" data-bind="foreach: children">
<command-button params="{buttonProps: $data}"></command-button>
</div>
</div>
</div>
<!-- /ko -->

View File

@@ -1,94 +1,94 @@
import * as React from "react"; import * as React from "react";
import { Dialog, DialogType, DialogFooter, IDialogProps } from "office-ui-fabric-react/lib/Dialog"; import { Dialog, DialogType, DialogFooter, IDialogProps } from "office-ui-fabric-react/lib/Dialog";
import { IButtonProps, PrimaryButton, DefaultButton } from "office-ui-fabric-react/lib/Button"; import { IButtonProps, PrimaryButton, DefaultButton } from "office-ui-fabric-react/lib/Button";
import { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField"; import { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField";
import { Link } from "office-ui-fabric-react/lib/Link"; import { Link } from "office-ui-fabric-react/lib/Link";
export interface TextFieldProps extends ITextFieldProps { export interface TextFieldProps extends ITextFieldProps {
label: string; label: string;
multiline: boolean; multiline: boolean;
autoAdjustHeight: boolean; autoAdjustHeight: boolean;
rows: number; rows: number;
onChange: (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => void; onChange: (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => void;
defaultValue?: string; defaultValue?: string;
} }
export interface LinkProps { export interface LinkProps {
linkText: string; linkText: string;
linkUrl: string; linkUrl: string;
} }
export interface DialogProps { export interface DialogProps {
title: string; title: string;
subText: string; subText: string;
isModal: boolean; isModal: boolean;
visible: boolean; visible: boolean;
textFieldProps?: TextFieldProps; textFieldProps?: TextFieldProps;
linkProps?: LinkProps; linkProps?: LinkProps;
primaryButtonText: string; primaryButtonText: string;
secondaryButtonText: string; secondaryButtonText: string;
onPrimaryButtonClick: () => void; onPrimaryButtonClick: () => void;
onSecondaryButtonClick: () => void; onSecondaryButtonClick: () => void;
primaryButtonDisabled?: boolean; primaryButtonDisabled?: boolean;
type?: DialogType; type?: DialogType;
} }
const DIALOG_MIN_WIDTH = "400px"; const DIALOG_MIN_WIDTH = "400px";
const DIALOG_MAX_WIDTH = "600px"; const DIALOG_MAX_WIDTH = "600px";
const DIALOG_TITLE_FONT_SIZE = "17px"; const DIALOG_TITLE_FONT_SIZE = "17px";
const DIALOG_TITLE_FONT_WEIGHT = 400; const DIALOG_TITLE_FONT_WEIGHT = 400;
const DIALOG_SUBTEXT_FONT_SIZE = "15px"; const DIALOG_SUBTEXT_FONT_SIZE = "15px";
export class DialogComponent extends React.Component<DialogProps, {}> { export class DialogComponent extends React.Component<DialogProps, {}> {
constructor(props: DialogProps) { constructor(props: DialogProps) {
super(props); super(props);
} }
public render(): JSX.Element { public render(): JSX.Element {
const dialogProps: IDialogProps = { const dialogProps: IDialogProps = {
hidden: !this.props.visible, hidden: !this.props.visible,
dialogContentProps: { dialogContentProps: {
type: this.props.type || DialogType.normal, type: this.props.type || DialogType.normal,
title: this.props.title, title: this.props.title,
subText: this.props.subText, subText: this.props.subText,
styles: { styles: {
title: { fontSize: DIALOG_TITLE_FONT_SIZE, fontWeight: DIALOG_TITLE_FONT_WEIGHT }, title: { fontSize: DIALOG_TITLE_FONT_SIZE, fontWeight: DIALOG_TITLE_FONT_WEIGHT },
subText: { fontSize: DIALOG_SUBTEXT_FONT_SIZE }, subText: { fontSize: DIALOG_SUBTEXT_FONT_SIZE }
}, },
showCloseButton: false, showCloseButton: false
}, },
modalProps: { isBlocking: this.props.isModal }, modalProps: { isBlocking: this.props.isModal },
minWidth: DIALOG_MIN_WIDTH, minWidth: DIALOG_MIN_WIDTH,
maxWidth: DIALOG_MAX_WIDTH, maxWidth: DIALOG_MAX_WIDTH
}; };
const textFieldProps: ITextFieldProps = this.props.textFieldProps; const textFieldProps: ITextFieldProps = this.props.textFieldProps;
const linkProps: LinkProps = this.props.linkProps; const linkProps: LinkProps = this.props.linkProps;
const primaryButtonProps: IButtonProps = { const primaryButtonProps: IButtonProps = {
text: this.props.primaryButtonText, text: this.props.primaryButtonText,
disabled: this.props.primaryButtonDisabled || false, disabled: this.props.primaryButtonDisabled || false,
onClick: this.props.onPrimaryButtonClick, onClick: this.props.onPrimaryButtonClick
}; };
const secondaryButtonProps: IButtonProps = const secondaryButtonProps: IButtonProps =
this.props.secondaryButtonText && this.props.onSecondaryButtonClick this.props.secondaryButtonText && this.props.onSecondaryButtonClick
? { ? {
text: this.props.secondaryButtonText, text: this.props.secondaryButtonText,
onClick: this.props.onSecondaryButtonClick, onClick: this.props.onSecondaryButtonClick
} }
: undefined; : undefined;
return ( return (
<Dialog {...dialogProps}> <Dialog {...dialogProps}>
{textFieldProps && <TextField {...textFieldProps} />} {textFieldProps && <TextField {...textFieldProps} />}
{linkProps && ( {linkProps && (
<Link href={linkProps.linkUrl} target="_blank"> <Link href={linkProps.linkUrl} target="_blank">
{linkProps.linkText} {linkProps.linkText}
</Link> </Link>
)} )}
<DialogFooter> <DialogFooter>
<PrimaryButton {...primaryButtonProps} /> <PrimaryButton {...primaryButtonProps} />
{secondaryButtonProps && <DefaultButton {...secondaryButtonProps} />} {secondaryButtonProps && <DefaultButton {...secondaryButtonProps} />}
</DialogFooter> </DialogFooter>
</Dialog> </Dialog>
); );
} }
} }

View File

@@ -1,16 +1,16 @@
/** /**
* This adapter is responsible to render the Dialog React component * This adapter is responsible to render the Dialog React component
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate * If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
* and update any knockout observables passed from the parent. * and update any knockout observables passed from the parent.
*/ */
import * as React from "react"; import * as React from "react";
import { DialogComponent, DialogProps } from "./DialogComponent"; import { DialogComponent, DialogProps } from "./DialogComponent";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
export class DialogComponentAdapter implements ReactAdapter { export class DialogComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<DialogProps>; public parameters: ko.Observable<DialogProps>;
public renderComponent(): JSX.Element { public renderComponent(): JSX.Element {
return <DialogComponent {...this.parameters()} />; return <DialogComponent {...this.parameters()} />;
} }
} }

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