Compare commits

...

29 Commits

Author SHA1 Message Date
Steve Faulkner
1636f20978 Update to Prettier 2.0 2020-07-01 23:33:09 -05:00
Steve Faulkner
9a95c7d069 Update @azure/cosmos SDK to 3.7.4 (#67) 2020-06-30 18:32:19 -05:00
Steve Faulkner
ec07ff05a4 Bundle config.json with published nugets (#64)
Co-authored-by: Vignesh Rangaishenvi <virangai@microsoft.com>
Co-authored-by: Tanuj Mittal <tamitta@microsoft.com>
2020-06-30 13:49:14 -05:00
Tanuj Mittal
7512b3c1d5 Notebooks Gallery (#59)
* Initial commit

* Address PR comments

* Move notebook related stuff to NotebookManager and dynamically load it

* Add New gallery callout and other UI tweaks

* Update test snapshot
2020-06-30 11:47:21 -07:00
vchske
dd199e6565 Fixing errors in mongo document tab (#58)
* This fixes an issue where errors when editing documents in an API for MongoDB endpoint would not be presented in the UI.

* Changing null to undefined in several places

* Fixed style issue.
Unignored MongoProxyClient.ts from full lint

* More linter issues since the removal from lint ignore
2020-06-29 16:02:31 -07:00
Laurent Nguyen
8200cc521f Switch to Graph explorer gremlin queries to use id and pk inside single quoted strings (#57) 2020-06-26 16:52:54 +02:00
Laurent Nguyen
1d3b672a14 Fix focus to match portal (#56) 2020-06-26 16:52:28 +02:00
Steve Faulkner
e5fc6f2022 Runner Tweaks (#62)
Co-authored-by: Steve Faulkner <stfaul@microsoft.com>
2020-06-25 18:59:44 -05:00
Steve Faulkner
3bf42b23dd Initial Portal Runner (#51) 2020-06-24 14:07:01 -05:00
Steve Faulkner
d22cb598a9 Fix Typo (#54) 2020-06-24 13:35:30 -05:00
Steve Faulkner
269ea6a349 Add Additional Lint Rules (#55) 2020-06-23 10:45:51 -05:00
Laurent Nguyen
123902e7ee Allow multi-line input for query box in Graph (#41) 2020-06-23 09:35:16 +02:00
Steve Faulkner
bccebaade5 Update Webpack Plugins (#50) 2020-06-18 08:39:47 -05:00
Laurent Nguyen
9fedf63a77 Show splash screen for all accounts (#44) 2020-06-18 10:36:05 +02:00
Laurent Nguyen
4abfcc5e25 Fix graph tab height issue (bottom part too low and occluded by notification bar) (#48) 2020-06-17 12:17:22 -05:00
Laurent Nguyen
3d9256abc6 Add contribution guidelines (#40) 2020-06-17 08:48:39 +02:00
Steve Faulkner
7f1355b1a4 Linting Updates (#47) 2020-06-16 09:21:44 -05:00
Tanuj Mittal
3eff440680 Remove unused Telemetry Actions and update comment (#18) 2020-06-15 10:35:02 -07:00
Steve Faulkner
4da0887e5e End to End CI Test for Mongo and SQL (#33)
Co-authored-by: Ashwin Kumar M <v-asmuth@microsoft.com>
Co-authored-by: Steve Faulkner <stfaul@microsoft.com>
2020-06-15 11:25:59 -05:00
Steve Faulkner
b783445130 Add Compile + Lint + Format to CI (#45) 2020-06-15 10:08:54 -05:00
Laurent Nguyen
73f2c612ed Remove old resource tree and cleanup (#28)
* Remove old resource tree and its various components

* Fix stored procedure, udf, trigger not always deleting when context menu option chosen

* Reformat and fix eslint warnings

* Remove CommandButtonOptions
2020-06-15 12:16:52 +02:00
Laurent Nguyen
d70e30c4fc Add data explorer launcher (#23)
* Initial migration from ADO

* Bug fixes

* Fix bugs. Make active area smaller and require shift + ctrl + dbl click

* Add missing features

* Switch from HashMap to Map as it is already polyfilled
2020-06-15 10:50:55 +02:00
Steve Faulkner
f8f1df4183 Update @azure/cosmos to 3.7.1 (#43) 2020-06-12 17:57:03 -05:00
Steve Faulkner
d427fc729e Update webpack-dev-server to latest (#32) 2020-06-12 17:34:52 -05:00
Steve Faulkner
9c36782661 Copy Contracts into dist/ (#39) 2020-06-12 12:33:00 -05:00
Laurent Nguyen
d32bd8851e Remove NotebookTab (#35) 2020-06-12 09:26:39 +02:00
Laurent Nguyen
23b2d8100f Remove obsolete feature flags (and reformat) (#27)
* Reformat

* Remove unused feature flags: graph, cacheOptimizations, settingsPane, throughputOverview, enableNteract
2020-06-11 12:39:58 +02:00
Steve Faulkner
1662d20e8a Fix Auth Header + Firefox Bug in Emulator (#22) 2020-06-10 16:08:05 -05:00
victor-meng
582ac865ff Migrate UploadItemPane to react (#17)
* Create GenericPaneComponent and use it to migrate UploadItemsPane to React

* Add helper functions for building each panel section

* Address comments and some styling changes

* Unsubscribe to isNotificationConsoleExpanded when component unmounts
2020-06-10 00:15:32 -07:00
498 changed files with 58576 additions and 51811 deletions

6
.env.example Normal file
View File

@@ -0,0 +1,6 @@
# 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,4 +1,5 @@
**/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
@@ -25,7 +26,6 @@ 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,7 +202,6 @@ 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
@@ -216,7 +215,6 @@ 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,45 +1,44 @@
module.exports = { module.exports = {
env: { env: {
browser: true, browser: true,
es6: true es6: true,
}, },
plugins: ["@typescript-eslint"], plugins: ["@typescript-eslint", "no-null"],
extends: [ extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"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,6 +1,3 @@
# 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:
@@ -8,7 +5,41 @@ on:
pull_request: pull_request:
branches: [master] branches: [master]
jobs: jobs:
test: compile:
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:
@@ -36,13 +67,14 @@ 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: npm run copyToConsumers - run: cp -r ./Contracts ./dist/contracts
- 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/
endtoend: endtoendemulator:
name: "End to End Tests" name: "End To End Tests | Emulator | SQL"
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -67,9 +99,65 @@ 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: [build, test, endtoend] needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendsql, endtoendmongo]
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
@@ -82,6 +170,7 @@ 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
@@ -90,7 +179,7 @@ jobs:
path: "*.nupkg" path: "*.nupkg"
nugetmpac: nugetmpac:
name: Publish Nuget MPAC name: Publish Nuget MPAC
needs: [build, test, endtoend] needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendsql, endtoendmongo]
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
@@ -103,6 +192,7 @@ 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}"

20
.github/workflows/runners.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
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,4 +15,5 @@ cypress/fixtures
notebookapp/* notebookapp/*
Contracts/* Contracts/*
.DS_Store .DS_Store
.cache/ .cache/
.env

51
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,51 @@
# 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 Tests #### End to End CI Tests
[Cypress](https://www.cypress.io/) is used for end to end tests and are contained in `cypress/`. Currently, it operates as sub project with its own typescript config and dependencies. It also only operates against the emulator. To run cypress tests: [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,16 +80,13 @@ 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
This project welcomes contributions and suggestions. Most contributions require you to agree to a Please read the [contribution guidelines](./CONTRIBUTING.md).
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"],
}; };

3
configs/mpac.json Normal file
View File

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

3
configs/prod.json Normal file
View File

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

5
cypress/.gitignore vendored
View File

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

51
cypress/cleanup.js Normal file
View File

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

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,27 +32,15 @@ context("Cassandra API Test - createDatabase", () => {
.should("be.visible") .should("be.visible")
.click(); .click();
cy.wrap($body) cy.wrap($body).find('div[class="contextual-pane-in"]').should("be.visible").find('span[id="containerTitle"]');
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body) cy.wrap($body).find('input[id="keyspace-id"]').should("be.visible").type(keyspaceId);
.find('input[id="keyspace-id"]')
.should("be.visible")
.type(keyspaceId);
cy.wrap($body) cy.wrap($body).find('input[class="textfontclr"]').type(tableId);
.find('input[class="textfontclr"]')
.type(tableId);
cy.wrap($body) cy.wrap($body).find('input[data-test="databaseThroughputValue"]').should("have.value", "400");
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body) cy.wrap($body).find('data-test="addCollection-createCollection"').click();
.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,39 +33,21 @@ context("Graph API Test", () => {
.should("be.visible") .should("be.visible")
.click(); .click();
cy.wrap($body) cy.wrap($body).find('div[class="contextual-pane-in"]').should("be.visible").find('span[id="containerTitle"]');
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-createNewDatabase"]').check();
.find('input[data-test="addCollection-createNewDatabase"]')
.check();
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-newDatabaseId"]').should("be.visible").type(dbId);
.find('input[data-test="addCollection-newDatabaseId"]')
.should("be.visible")
.type(dbId);
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollectionPane-databaseSharedThroughput"]').check();
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
.check();
cy.wrap($body) cy.wrap($body).find('input[data-test="databaseThroughputValue"]').should("have.value", "400");
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-collectionId"]').type(graphId);
.find('input[data-test="addCollection-collectionId"]')
.type(graphId);
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-partitionKeyValue"]').type(partitionKey);
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(partitionKey);
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-createCollection"]').click();
.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.constants.mongo); connectionString.loginUsingConnectionString();
}); });
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,38 +33,21 @@ context("Mongo API Test - createDatabase", () => {
.should("be.visible") .should("be.visible")
.click(); .click();
cy.wrap($body) cy.wrap($body).find('div[class="contextual-pane-in"]').should("be.visible").find('span[id="containerTitle"]');
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-createNewDatabase"]').check();
.find('input[data-test="addCollection-createNewDatabase"]')
.check();
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-newDatabaseId"]').type(dbId);
.find('input[data-test="addCollection-newDatabaseId"]')
.type(dbId);
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollectionPane-databaseSharedThroughput"]').check();
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
.check();
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-collectionId"]').type(collectionId);
.find('input[data-test="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body) cy.wrap($body).find('input[data-test="databaseThroughputValue"]').should("have.value", "400");
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-partitionKeyValue"]').type(sharedKey);
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(sharedKey);
cy.wrap($body) cy.wrap($body).find("#submitBtnAddCollection").click();
.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.constants.mongo); connectionString.loginUsingConnectionString();
}); });
it("Create a new collection in Mongo API - Autopilot", () => { it.skip("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,34 +33,23 @@ context("Mongo API Test", () => {
.should("be.visible") .should("be.visible")
.click(); .click();
cy.wrap($body) cy.wrap($body).find('div[class="contextual-pane-in"]').should("be.visible").find('span[id="containerTitle"]');
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-createNewDatabase"]').check();
.find('input[data-test="addCollection-createNewDatabase"]')
.check();
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-newDatabaseId"]').type(dbId);
.find('input[data-test="addCollection-newDatabaseId"]')
.type(dbId);
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollectionPane-databaseSharedThroughput"]').check();
.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) cy.wrap($body).find('input[id="newContainer-databaseThroughput-autoPilotRadio"]').check();
.find('input[id="newContainer-databaseThroughput-autoPilotRadio"]')
.check();
cy.wrap($body) cy.wrap($body)
.find('select[name="autoPilotTiers"]') .find('select[name="autoPilotTiers"]')
@@ -68,19 +57,13 @@ 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) cy.wrap($body).find('input[data-test="addCollection-collectionId"]').type(collectionId);
.find('input[data-test="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-partitionKeyValue"]').type(sharedKey);
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(sharedKey);
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-createCollection"]').click();
.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.constants.mongo); connectionString.loginUsingConnectionString();
}); });
it("Create a new collection in existing database in Mongo API", () => { it.skip("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,30 +28,17 @@ context("Mongo API Test", () => {
.should("be.visible") .should("be.visible")
.click(); .click();
cy.wrap($body) cy.wrap($body).find('div[class="contextual-pane-in"]').should("be.visible").find('span[id="containerTitle"]');
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-existingDatabase"]').check();
.find('input[data-test="addCollection-existingDatabase"]')
.check();
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-existingDatabase"]').type(dbId1);
.find('input[data-test="addCollection-existingDatabase"]')
.type(dbId1);
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-collectionId"]').type(collectionId);
.find('input[data-test="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-partitionKeyValue"]').type(sharedKey);
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(sharedKey);
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-createCollection"]').click();
.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("Mongo API Test", () => { context.skip("Mongo API Test", () => {
beforeEach(() => { beforeEach(() => {
connectionString.loginUsingConnectionString(connectionString.constants.mongo); connectionString.loginUsingConnectionString();
}); });
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("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,50 +21,31 @@ context("Mongo API Test", () => {
.should("be.visible") .should("be.visible")
.click(); .click();
cy.wrap($body) cy.wrap($body).find('div[class="contextual-pane-in"]').should("be.visible").find('span[id="containerTitle"]');
.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) cy.wrap($body).find('input[data-test="addCollection-createNewDatabase"]').check();
.find('input[data-test="addCollection-createNewDatabase"]')
.check();
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollectionPane-databaseSharedThroughput"]').check();
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
.check();
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-newDatabaseId"]').type(dbId);
.find('input[data-test="addCollection-newDatabaseId"]')
.type(dbId);
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollectionPane-databaseSharedThroughput"]').check();
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
.check();
cy.wrap($body) cy.wrap($body).find('input[data-test="databaseThroughputValue"]').should("have.value", "400");
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-collectionId"]').type(collectionId);
.find('input[data-test="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-partitionKeyValue"]').type(sharedKey);
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(sharedKey);
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-createCollection"]').click();
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000); cy.wait(10000);
@@ -84,7 +65,7 @@ context("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"]')
@@ -93,42 +74,23 @@ context("Mongo API Test", () => {
.should("be.visible") .should("be.visible")
.click(); .click();
cy.wrap($body) cy.wrap($body).find('div[class="contextual-pane-in"]').should("be.visible").find('span[id="containerTitle"]');
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-createNewDatabase"]').check();
.find('input[data-test="addCollection-createNewDatabase"]')
.check();
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-newDatabaseId"]').type(dbId);
.find('input[data-test="addCollection-newDatabaseId"]')
.type(dbId);
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollectionPane-databaseSharedThroughput"]').uncheck();
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
.uncheck();
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-collectionId"]').type(collectionId);
.find('input[data-test="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body) cy.wrap($body).find('input[id="tab2"]').check({ force: true });
.find('input[id="tab2"]')
.check({ force: true });
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-partitionKeyValue"]').type(sharedKey);
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(sharedKey);
cy.wrap($body) cy.wrap($body).find('input[data-test="databaseThroughputValue"]').should("have.value", "400");
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-createCollection"]').click();
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000); cy.wait(10000);
@@ -147,7 +109,7 @@ context("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"]')
@@ -156,38 +118,21 @@ context("Mongo API Test", () => {
.should("be.visible") .should("be.visible")
.click(); .click();
cy.wrap($body) cy.wrap($body).find('div[class="contextual-pane-in"]').should("be.visible").find('span[id="containerTitle"]');
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-createNewDatabase"]').check();
.find('input[data-test="addCollection-createNewDatabase"]')
.check();
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-newDatabaseId"]').type(dbId);
.find('input[data-test="addCollection-newDatabaseId"]')
.type(dbId);
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollectionPane-databaseSharedThroughput"]').uncheck();
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
.uncheck();
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-collectionId"]').type(collectionId);
.find('input[data-test="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body) cy.wrap($body).find('input[id="tab1"]').check({ force: true });
.find('input[id="tab1"]')
.check({ force: true });
cy.wrap($body) cy.wrap($body).find('input[data-test="databaseThroughputValue"]').should("have.value", "400");
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-createCollection"]').click();
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000); cy.wait(10000);

View File

@@ -16,15 +16,16 @@ let crypt = require("crypto");
context("SQL API Test", () => { context("SQL API Test", () => {
beforeEach(() => { beforeEach(() => {
connectionString.loginUsingConnectionString(connectionString.constants.sql); connectionString.loginUsingConnectionString();
}); });
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"]')
@@ -33,38 +34,21 @@ context("SQL API Test", () => {
.should("be.visible") .should("be.visible")
.click(); .click();
cy.wrap($body) cy.wrap($body).find('div[class="contextual-pane-in"]').should("be.visible").find('span[id="containerTitle"]');
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-createNewDatabase"]').check();
.find('input[data-test="addCollection-createNewDatabase"]')
.check();
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollectionPane-databaseSharedThroughput"]').check();
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
.check();
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-newDatabaseId"]').type(dbId);
.find('input[data-test="addCollection-newDatabaseId"]')
.type(dbId);
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-collectionId"]').type(collectionId);
.find('input[data-test="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body) cy.wrap($body).find('input[data-test="databaseThroughputValue"]').should("have.value", "400");
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-partitionKeyValue"]').type(sharedKey);
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(sharedKey);
cy.wrap($body) cy.wrap($body).find("#submitBtnAddCollection").click();
.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,22 +31,13 @@ context("Table API Test", () => {
.should("be.visible") .should("be.visible")
.click(); .click();
cy.wrap($body) cy.wrap($body).find('div[class="contextual-pane-in"]').should("be.visible").find('span[id="containerTitle"]');
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-collectionId"]').type(collectionId);
.find('input[data-test="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body) cy.wrap($body).find('input[data-test="databaseThroughputValue"]').should("have.value", "400");
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body) cy.wrap($body).find('input[data-test="addCollection-createCollection"]').click();
.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,27 +38,15 @@ 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") cy.get(".dataResourceTree").contains(databaseId).click();
.contains(databaseId)
.click();
cy.get(".dataResourceTree").should("contain", collectionId); cy.get(".dataResourceTree").should("contain", collectionId);
cy.get(".dataResourceTree") cy.get(".dataResourceTree").contains(collectionId).click();
.contains(collectionId) cy.get(".dataResourceTree").contains("Items").click();
.click(); cy.get(".dataResourceTree").contains("Items").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") cy.get(".commandBarContainer").contains("New Item").click();
.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") cy.get(".commandBarContainer").contains("Save").click();
.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,25 +14,18 @@ context("Emulator - deleteCollection", () => {
}); });
it("Delete a collection", () => { it("Delete a collection", () => {
cy.get(".databaseId") cy.get(".databaseId").last().click();
.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"]') cy.get('span[data-test="collectionEllipsisMenu"]').invoke("show").last().click();
.invoke("show")
.last()
.click();
cy.get('div[data-test="collectionContextMenu"]') cy.get('div[data-test="collectionContextMenu"]').contains("Delete Container").click({ force: true });
.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,19 +59,14 @@ 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"]') cy.get('span[data-test="databaseEllipsisMenu"]').invoke("show").last().click();
.invoke("show")
.last()
.click();
cy.get('div[data-test="databaseContextMenu"]') cy.get('div[data-test="databaseContextMenu"]').contains("Delete Database").click({ force: true });
.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,29 +21,25 @@ 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") cy.get(".cell-container").as("cellContainer").click();
.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"]') cy.get('[data-test="Run"]').first().click();
.first()
.click();
// Verify results // Verify results
cy.get("@cellContainer").within(() => { cy.get("@cellContainer").within(() => {
@@ -51,39 +47,29 @@ context("New Notebook smoke test", () => {
}); });
// Restart kernel // Restart kernel
cy.get('[data-test="Run"] button') cy.get('[data-test="Run"] button').eq(-1).click();
.eq(-1) cy.get("li").contains("Restart Kernel").click();
.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") cy.get(".cell-container").as("cellContainer").find(".input").as("codeInput").click();
.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"]') cy.get('[data-test="Run"]').first().click();
.first()
.click();
// Verify results // Verify results
cy.get("@cellContainer").within(() => { cy.get("@cellContainer").within(() => {

View File

@@ -11,15 +11,11 @@ context("Resource tree notebook file manipulation", () => {
}; };
const clickContextMenuAndSelectOption = (nodeLabel, option) => { const clickContextMenuAndSelectOption = (nodeLabel, option) => {
cy.get(`.treeNodeHeader[data-test="${nodeLabel}"]`) cy.get(`.treeNodeHeader[data-test="${nodeLabel}"]`).find("button.treeMenuEllipsis").click();
.find("button.treeMenuEllipsis") cy.get('[data-test="treeComponentMenuItemContainer"]').contains(option).click();
.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(() => {
@@ -28,11 +24,9 @@ context("Resource tree notebook file manipulation", () => {
}); });
}; };
const deleteItem = nodeName => { const deleteItem = (nodeName) => {
clickContextMenuAndSelectOption(`${nodeName}`, "Delete"); clickContextMenuAndSelectOption(`${nodeName}`, "Delete");
cy.get(".ms-Dialog-main") cy.get(".ms-Dialog-main").contains("Delete").click();
.contains("Delete")
.click();
}; };
beforeEach(() => { beforeEach(() => {
@@ -56,9 +50,7 @@ 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"]') cy.get('input[name="collectionIdConfirmation"]').clear().type(renamedFolder);
.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");
@@ -75,16 +67,12 @@ 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") cy.get(".tabList").contains(newNotebookName).should("exist");
.contains(newNotebookName)
.should("exist");
// Close tab // Close tab
cy.get(`.tabList[title="notebooks/${folder}/${newNotebookName}"]`) cy.get(`.tabList[title="notebooks/${folder}/${newNotebookName}"]`).find(".cancelButton").click();
.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();
@@ -100,14 +88,10 @@ 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"]') cy.get('[data-test="treeComponentMenuItemContainer"]').contains("Delete").click();
.contains("Delete")
.click();
// Confirm // Confirm
cy.get(".ms-Dialog-main") cy.get(".ms-Dialog-main").contains("Delete").click();
.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}/`);
@@ -121,10 +105,8 @@ 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}"]`) cy.get(`.tabList[title="notebooks/${folder}/${newNotebookName}"]`).find(".cancelButton").click();
.find(".cancelButton") cy.get("body").then(($body) => {
.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();
@@ -140,14 +122,10 @@ 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"]') cy.get('[data-test="treeComponentMenuItemContainer"]').contains("Rename").click();
.contains("Rename")
.click();
cy.get("#stringInputPane").within(() => { cy.get("#stringInputPane").within(() => {
cy.get('input[name="collectionIdConfirmation"]') cy.get('input[name="collectionIdConfirmation"]').clear().type(renamedNotebookName);
.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");
@@ -157,14 +135,10 @@ 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"]') cy.get('[data-test="treeComponentMenuItemContainer"]').contains("Delete").click();
.contains("Delete")
.click();
// Confirm // Confirm
cy.get(".ms-Dialog-main") cy.get(".ms-Dialog-main").contains("Delete").click();
.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,77 +273,12 @@
"any-observable": "^0.3.0" "any-observable": "^0.3.0"
} }
}, },
"@types/blob-util": { "@types/sinonjs__fake-timers": {
"version": "1.3.3", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/@types/blob-util/-/blob-util-1.3.3.tgz", "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz",
"integrity": "sha512-4ahcL/QDnpjWA2Qs16ZMQif7HjGP2cw3AGjHabybjw7Vm1EKu+cfQN1D78BaZbS1WJNa1opSMF5HNMztx7lR0w==", "integrity": "sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA==",
"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",
@@ -827,24 +762,15 @@
} }
}, },
"cypress": { "cypress": {
"version": "4.5.0", "version": "4.8.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-4.5.0.tgz", "resolved": "https://registry.npmjs.org/cypress/-/cypress-4.8.0.tgz",
"integrity": "sha512-2A4g5FW5d2fHzq8HKUGAMVTnW6P8nlWYQALiCoGN4bqBLvgwhYM/oG9oKc2CS6LnvgHFiKivKzpm9sfk3uU3zQ==", "integrity": "sha512-Lsff8lF8pq6k/ioNua783tCsxGSLp6gqGXecdIfqCkqjYiOA53XKuEf1CaQJLUVs1dHSf49eDUp/sb620oJjVQ==",
"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/blob-util": "1.3.3", "@types/sinonjs__fake-timers": "6.0.1",
"@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",
@@ -878,14 +804,6 @@
"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": {
@@ -1081,12 +999,6 @@
"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",
@@ -1859,6 +1771,12 @@
"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,11 +5,13 @@
"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.5.0", "cypress": "^4.8.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,56 +1,41 @@
module.exports = { module.exports = {
loginUsingConnectionString: function (api) { loginUsingConnectionString: function() {
const prodUrl = Cypress.env("TEST_ENDPOINT") || "https://localhost:1234/hostedExplorer.html";
const timeout = 15000;
cy.visit(prodUrl);
cy.get('iframe[id="explorerMenu"]').should("be.visible");
const prodUrl = "https://cosmos.azure.com/"; cy.get("iframe").then($element => {
const timeout = 15000; const $body = $element.contents().find("body");
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("input[class='inputToken']") .find("#connectExplorer")
.should("exist") .should("exist")
.type(secret, { .find("div[class='connectExplorer']")
force: true .should("exist")
}); .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")
constants:{ .last()
sql: "sql", .click({ force: true });
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);
});
}
};

9
jest-puppeteer.config.js Normal file
View File

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

5
jest.config.e2e.js Normal file
View File

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

View File

@@ -14,6 +14,10 @@ body {
font-family: @DataExplorerFont; font-family: @DataExplorerFont;
font-size: 12px; font-size: 12px;
height: 100%; height: 100%;
:focus {
.focus()
}
} }
.float-right { .float-right {
@@ -174,7 +178,7 @@ body {
&:active { &:active {
.active(); .active();
} }
&:focus .urlTokenCopyTooltiptext, &:focus .urlTokenCopyTooltiptext { &:focus .urlTokenCopyTooltiptext, &:focus .urlTokenCopyTooltiptext {
.tooltipVisible(); .tooltipVisible();
} }
@@ -362,7 +366,7 @@ body {
} }
.splashLoaderContainer { .splashLoaderContainer {
z-index: 5; z-index: 5;
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
@@ -570,6 +574,12 @@ body {
} }
} }
.fileImportButton {
height: 24px;
border: @ButtonBorderWidth solid transparent;
vertical-align: top;
}
.fileUploadSummaryContainer { .fileUploadSummaryContainer {
margin-top: 40px; margin-top: 40px;
@@ -1016,6 +1026,18 @@ 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;
@@ -1232,23 +1254,25 @@ menuQuickStart {
padding: 2px 30px; padding: 2px 30px;
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 12px;
&:active {
border-color: #0072c6;
background-color: #0072c6;
}
} }
.btncreatecoll1:hover { .leftpanel-okbut .genericPaneSubmitBtn {
background: @AccentMediumHigh; border: 1px solid @AccentMediumHigh;
background-color: @AccentMediumHigh;
color: #fff; color: #fff;
border-color: @AccentMediumHigh;
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 12px;
} height: 24px;
.btncreatecoll1:active { &:active {
border: 1px solid #0072c6; border-color: #0072c6;
background-color: #0072c6; background-color: #0072c6;
color: white; }
padding: 2px 30px;
cursor: pointer;
font-size: 12px;
} }
.btncreatecoll1-off { .btncreatecoll1-off {
@@ -1361,6 +1385,15 @@ 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;
@@ -1420,7 +1453,7 @@ p {
.throughputModeRadio { .throughputModeRadio {
vertical-align: text-bottom; vertical-align: text-bottom;
} }
.nonFirstRadio { .nonFirstRadio {
margin-left: @LargeSpace; margin-left: @LargeSpace;
} }
@@ -1455,7 +1488,7 @@ p {
.largePartitionKeyDescription { .largePartitionKeyDescription {
margin: @DefaultSpace 0px 0px; margin: @DefaultSpace 0px 0px;
} }
} }
.enableAnalyticalStorage { .enableAnalyticalStorage {
@@ -1710,6 +1743,13 @@ 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;
@@ -2180,13 +2220,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 {

9843
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.6.3", "@azure/cosmos": "3.7.4",
"@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,22 +37,25 @@
"@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": "4.5.4", "copy-webpack-plugin": "6.0.2",
"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.8.2", "eslint-plugin-jest": "23.13.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",
@@ -110,8 +113,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": "2.25.0", "@typescript-eslint/eslint-plugin": "3.2.0",
"@typescript-eslint/parser": "2.25.0", "@typescript-eslint/parser": "3.2.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",
@@ -123,9 +126,10 @@
"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": "6.8.0", "eslint": "7.3.1",
"eslint-cli": "1.1.1", "eslint-cli": "1.1.1",
"eslint-plugin-react": "7.19.0", "eslint-plugin-no-null": "1.0.2",
"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",
@@ -133,29 +137,31 @@
"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": "24.9.0", "jest": "25.5.4",
"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": "1.19.1", "prettier": "2.0.5",
"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": "2.3.5", "terser-webpack-plugin": "3.0.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.41.2", "webpack": "4.43.0",
"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.9.0", "webpack-dev-server": "3.11.0",
"worker-loader": "2.0.0" "worker-loader": "2.0.0"
}, },
"scripts": { "scripts": {
@@ -168,8 +174,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",

View File

@@ -1,24 +0,0 @@
@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

@@ -21,13 +21,15 @@ const _global = typeof self === "undefined" ? window : self;
export const tokenProvider = async (requestInfo: RequestInfo) => { export const tokenProvider = async (requestInfo: RequestInfo) => {
const { verb, resourceId, resourceType, headers } = requestInfo; const { verb, resourceId, resourceType, headers } = requestInfo;
if (config.platform === Platform.Emulator) { if (config.platform === Platform.Emulator) {
// TODO Remove any. SDK expects a return value for tokenProvider, but we are mutating the header object instead. // TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
return setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey) as any; await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
return decodeURIComponent(headers.authorization);
} }
if (_masterKey) { if (_masterKey) {
// TODO Remove any. SDK expects a return value for tokenProvider, but we are mutating the header object instead. // TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
return setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, _masterKey) as any; await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
return decodeURIComponent(headers.authorization);
} }
if (_resourceToken) { if (_resourceToken) {
@@ -47,7 +49,9 @@ export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, next) =>
export const endpoint = () => { export const endpoint = () => {
if (config.platform === Platform.Emulator) { if (config.platform === Platform.Emulator) {
return config.EMULATOR_ENDPOINT || window.parent.location.origin; // In worker scope, _global(self).parent does not exist
const location = _global.parent ? _global.parent.location : _global.location;
return config.EMULATOR_ENDPOINT || location.origin;
} }
return _endpoint || (_databaseAccount && _databaseAccount.properties && _databaseAccount.properties.documentEndpoint); return _endpoint || (_databaseAccount && _databaseAccount.properties && _databaseAccount.properties.documentEndpoint);
}; };
@@ -59,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());
@@ -89,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.
@@ -172,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 { Logger } from "./Logger"; import * as 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,84 +4,68 @@ 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
export class Logger { // eslint-disable-next-line @typescript-eslint/no-explicit-any
public static logInfo(message: string | Record<string, any>, area: string, code?: number): void { export function 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 = Logger._generateLogEntry(
Diagnostics.LogEntryLevel.Verbose,
logMessage,
area,
code
);
return Logger._logEntry(entry);
}
public static logWarning(message: string, area: string, code?: number): void {
const entry: Diagnostics.LogEntry = Logger._generateLogEntry(
Diagnostics.LogEntryLevel.Warning,
message,
area,
code
);
return Logger._logEntry(entry);
}
public static 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 = 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
};
} }
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);
appInsights.trackTrace({ message: entry.message, severityLevel }, { area: entry.area });
}
function _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) { export function queryIterator(databaseId: string, collection: Collection, query: string): any {
let continuationToken: string = null; let continuationToken: string;
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;
let headers = {} as any; const 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,7 +78,9 @@ export function queryDocuments(
rg: CosmosClient.resourceGroup(), rg: CosmosClient.resourceGroup(),
dba: databaseAccount.name, dba: databaseAccount.name,
pk: pk:
collection && collection.partitionKey && !collection.partitionKey.systemKey ? collection.partitionKeyProperty : "" collection && collection.partitionKey && !collection.partitionKey.systemKey
? collection.partitionKeyProperty
: "",
}; };
const endpoint = getEndpoint(databaseAccount) || ""; const endpoint = getEndpoint(databaseAccount) || "";
@@ -91,7 +93,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) {
@@ -104,24 +106,17 @@ 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,
}; };
} }
const errorMessage = await response.text(); return errorHandling(response, "querying documents", params);
if (response.status === HttpStatusCodes.Forbidden) {
MessageHandler.sendMessage({
type: MessageTypes.ForbiddenError,
reason: errorMessage
});
}
throw new Error(errorMessage);
}); });
} }
@@ -145,7 +140,9 @@ export function readDocument(
rg: CosmosClient.resourceGroup(), rg: CosmosClient.resourceGroup(),
dba: databaseAccount.name, dba: databaseAccount.name,
pk: pk:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : "" documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
? documentId.partitionKeyProperty
: "",
}; };
const endpoint = getEndpoint(databaseAccount); const endpoint = getEndpoint(databaseAccount);
@@ -157,14 +154,14 @@ export function readDocument(
...authHeaders(), ...authHeaders(),
[CosmosSDKConstants.HttpHeaders.PartitionKey]: encodeURIComponent( [CosmosSDKConstants.HttpHeaders.PartitionKey]: encodeURIComponent(
JSON.stringify(documentId.partitionKeyHeader()) JSON.stringify(documentId.partitionKeyHeader())
) ),
} },
}) })
.then(async response => { .then((response) => {
if (response.ok) { if (response.ok) {
return response.json(); return response.json();
} }
errorHandling(response); return errorHandling(response, "reading document", params);
}); });
} }
@@ -185,7 +182,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);
@@ -196,14 +193,14 @@ export function createDocument(
body: JSON.stringify(documentContent), body: JSON.stringify(documentContent),
headers: { headers: {
...defaultHeaders, ...defaultHeaders,
...authHeaders() ...authHeaders(),
} },
}) })
.then(async response => { .then((response) => {
if (response.ok) { if (response.ok) {
return response.json(); return response.json();
} }
errorHandling(response); return errorHandling(response, "creating document", params);
}); });
} }
@@ -228,7 +225,9 @@ export function updateDocument(
rg: CosmosClient.resourceGroup(), rg: CosmosClient.resourceGroup(),
dba: databaseAccount.name, dba: databaseAccount.name,
pk: pk:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : "" documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
? documentId.partitionKeyProperty
: "",
}; };
const endpoint = getEndpoint(databaseAccount); const endpoint = getEndpoint(databaseAccount);
@@ -240,14 +239,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(async response => { .then((response) => {
if (response.ok) { if (response.ok) {
return response.json(); return response.json();
} }
errorHandling(response); return errorHandling(response, "updating document", params);
}); });
} }
@@ -271,7 +270,9 @@ export function deleteDocument(
rg: CosmosClient.resourceGroup(), rg: CosmosClient.resourceGroup(),
dba: databaseAccount.name, dba: databaseAccount.name,
pk: pk:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : "" documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
? documentId.partitionKeyProperty
: "",
}; };
const endpoint = getEndpoint(databaseAccount); const endpoint = getEndpoint(databaseAccount);
@@ -282,14 +283,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(async response => { .then((response) => {
if (response.ok) { if (response.ok) {
return; return undefined;
} }
errorHandling(response); return errorHandling(response, "deleting document", params);
}); });
} }
@@ -318,7 +319,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) {
@@ -336,19 +337,15 @@ export function createMongoCollectionWithProxy(
headers: { headers: {
...defaultHeaders, ...defaultHeaders,
...authHeaders(), ...authHeaders(),
[HttpHeaders.contentType]: "application/json" [HttpHeaders.contentType]: "application/json",
} },
} }
) )
.then(async response => { .then((response) => {
if (response.ok) { if (response.ok) {
return; return undefined;
} }
NotificationConsoleUtils.logConsoleMessage( return errorHandling(response, "creating collection", params);
ConsoleDataType.Error,
`Error creating collection: ${await response.json()}, Payload: ${params}`
);
errorHandling(response);
}); });
} }
@@ -379,7 +376,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) {
@@ -407,13 +404,16 @@ export function getEndpoint(databaseAccount: ViewModels.DatabaseAccount): string
return url; return url;
} }
async function errorHandling(response: any): Promise<any> { async function errorHandling(response: any, action: string, params: 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({ MessageHandler.sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage });
type: MessageTypes.ForbiddenError, return;
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,14 +462,6 @@ export async function _createMongoCollectionWithARM(
rpPayloadToCreateCollection rpPayloadToCreateCollection
); );
} catch (response) { } catch (response) {
NotificationConsoleUtils.logConsoleMessage( return errorHandling(response, "creating collection", undefined);
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 { Logger } from "./Logger"; import * as 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,108 +1,106 @@
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) $(this.leftSide).resizable("option", "disabled", true).removeClass("ui-resizable-disabled"); // remove class so splitter is visible
.resizable("option", "disabled", true) $(this.splitter).removeClass("ui-resizable-e");
.removeClass("ui-resizable-disabled"); // remove class so splitter is visible this.isCollapsed(true);
$(this.splitter).removeClass("ui-resizable-e"); }
this.isCollapsed(true);
} public expandLeft() {
$(this.splitter).addClass("ui-resizable-e");
public expandLeft() { $(this.leftSide).css("width", this.lastWidth);
$(this.splitter).addClass("ui-resizable-e"); $(this.splitter).css("left", this.lastX);
$(this.leftSide).css("width", this.lastWidth); $(this.splitter).css("left", ""); // this ensures the splitter's position is not fixed and enables movement during resizing
$(this.splitter).css("left", this.lastX); $(this.leftSide).resizable("enable");
$(this.splitter).css("left", ""); // this ensures the splitter's position is not fixed and enables movement during resizing this.isCollapsed(false);
$(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,178 +1,142 @@
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 command-button component", () => { it("should register input-typeahead component", () => {
expect(ko.components.isRegistered("command-button")).toBe(true); expect(ko.components.isRegistered("input-typeahead")).toBe(true);
}); });
it("should register input-typeahead component", () => { it("should register new-vertex-form component", () => {
expect(ko.components.isRegistered("input-typeahead")).toBe(true); expect(ko.components.isRegistered("new-vertex-form")).toBe(true);
}); });
it("should register new-vertex-form component", () => { it("should register error-display component", () => {
expect(ko.components.isRegistered("new-vertex-form")).toBe(true); expect(ko.components.isRegistered("error-display")).toBe(true);
}); });
it("should register error-display component", () => { it("should register graph-style component", () => {
expect(ko.components.isRegistered("error-display")).toBe(true); expect(ko.components.isRegistered("graph-style")).toBe(true);
}); });
it("should register graph-style component", () => { it("should register collapsible-panel component", () => {
expect(ko.components.isRegistered("graph-style")).toBe(true); expect(ko.components.isRegistered("collapsible-panel")).toBe(true);
}); });
it("should register collapsible-panel component", () => { it("should register json-editor component", () => {
expect(ko.components.isRegistered("collapsible-panel")).toBe(true); expect(ko.components.isRegistered("json-editor")).toBe(true);
}); });
it("should register json-editor component", () => { it("should register documents-tab component", () => {
expect(ko.components.isRegistered("json-editor")).toBe(true); expect(ko.components.isRegistered("documents-tab")).toBe(true);
}); });
it("should register documents-tab component", () => { it("should register stored-procedure-tab component", () => {
expect(ko.components.isRegistered("documents-tab")).toBe(true); expect(ko.components.isRegistered("stored-procedure-tab")).toBe(true);
}); });
it("should register stored-procedure-tab component", () => { it("should register trigger-tab component", () => {
expect(ko.components.isRegistered("stored-procedure-tab")).toBe(true); expect(ko.components.isRegistered("trigger-tab")).toBe(true);
}); });
it("should register trigger-tab component", () => { it("should register user-defined-function-tab component", () => {
expect(ko.components.isRegistered("trigger-tab")).toBe(true); expect(ko.components.isRegistered("user-defined-function-tab")).toBe(true);
}); });
it("should register user-defined-function-tab component", () => { it("should register settings-tab component", () => {
expect(ko.components.isRegistered("user-defined-function-tab")).toBe(true); expect(ko.components.isRegistered("settings-tab")).toBe(true);
}); });
it("should register settings-tab component", () => { it("should register query-tab component", () => {
expect(ko.components.isRegistered("settings-tab")).toBe(true); expect(ko.components.isRegistered("query-tab")).toBe(true);
}); });
it("should register query-tab component", () => { it("should register tables-query-tab component", () => {
expect(ko.components.isRegistered("query-tab")).toBe(true); expect(ko.components.isRegistered("tables-query-tab")).toBe(true);
}); });
it("should register tables-query-tab component", () => { it("should register graph-tab component", () => {
expect(ko.components.isRegistered("tables-query-tab")).toBe(true); expect(ko.components.isRegistered("graph-tab")).toBe(true);
}); });
it("should register graph-tab component", () => { it("should register notebookv2-tab component", () => {
expect(ko.components.isRegistered("graph-tab")).toBe(true); expect(ko.components.isRegistered("notebookv2-tab")).toBe(true);
}); });
it("should register notebook-tab component", () => { it("should register terminal-tab component", () => {
expect(ko.components.isRegistered("notebook-tab")).toBe(true); expect(ko.components.isRegistered("terminal-tab")).toBe(true);
}); });
it("should register notebookv2-tab component", () => { it("should register spark-master-tab component", () => {
expect(ko.components.isRegistered("notebookv2-tab")).toBe(true); expect(ko.components.isRegistered("spark-master-tab")).toBe(true);
}); });
it("should register terminal-tab component", () => { it("should register mongo-shell-tab component", () => {
expect(ko.components.isRegistered("terminal-tab")).toBe(true); expect(ko.components.isRegistered("mongo-shell-tab")).toBe(true);
}); });
it("should register spark-master-tab component", () => { it("should registeradd-collection-pane component", () => {
expect(ko.components.isRegistered("spark-master-tab")).toBe(true); expect(ko.components.isRegistered("add-collection-pane")).toBe(true);
}); });
it("should register mongo-shell-tab component", () => { it("should register delete-collection-confirmation-pane component", () => {
expect(ko.components.isRegistered("mongo-shell-tab")).toBe(true); expect(ko.components.isRegistered("delete-collection-confirmation-pane")).toBe(true);
}); });
it("should register resource-tree component", () => { it("should register delete-database-confirmation-pane component", () => {
expect(ko.components.isRegistered("resource-tree")).toBe(true); expect(ko.components.isRegistered("delete-database-confirmation-pane")).toBe(true);
}); });
it("should register database-node component", () => { it("should register save-query-pane component", () => {
expect(ko.components.isRegistered("database-node")).toBe(true); expect(ko.components.isRegistered("save-query-pane")).toBe(true);
}); });
it("should register collection-node component", () => { it("should register browse-queries-pane component", () => {
expect(ko.components.isRegistered("collection-node")).toBe(true); expect(ko.components.isRegistered("browse-queries-pane")).toBe(true);
}); });
it("should register stored-procedure-node component", () => { it("should register graph-new-vertex-pane component", () => {
expect(ko.components.isRegistered("stored-procedure-node")).toBe(true); expect(ko.components.isRegistered("graph-new-vertex-pane")).toBe(true);
}); });
it("should register trigger-node component", () => { it("should register graph-styling-pane component", () => {
expect(ko.components.isRegistered("trigger-node")).toBe(true); expect(ko.components.isRegistered("graph-styling-pane")).toBe(true);
}); });
it("should register user-defined-function-node component", () => { it("should register upload-file-pane component", () => {
expect(ko.components.isRegistered("user-defined-function-node")).toBe(true); expect(ko.components.isRegistered("upload-file-pane")).toBe(true);
}); });
it("should registeradd-collection-pane component", () => { it("should register string-input-pane component", () => {
expect(ko.components.isRegistered("add-collection-pane")).toBe(true); expect(ko.components.isRegistered("string-input-pane")).toBe(true);
}); });
it("should register delete-collection-confirmation-pane component", () => { it("should register setup-notebooks-pane component", () => {
expect(ko.components.isRegistered("delete-collection-confirmation-pane")).toBe(true); expect(ko.components.isRegistered("setup-notebooks-pane")).toBe(true);
}); });
it("should register delete-database-confirmation-pane component", () => { it("should register setup-spark-cluster-pane component", () => {
expect(ko.components.isRegistered("delete-database-confirmation-pane")).toBe(true); expect(ko.components.isRegistered("setup-spark-cluster-pane")).toBe(true);
}); });
it("should register save-query-pane component", () => { it("should register manage-spark-cluster-pane component", () => {
expect(ko.components.isRegistered("save-query-pane")).toBe(true); expect(ko.components.isRegistered("manage-spark-cluster-pane")).toBe(true);
}); });
it("should register browse-queries-pane component", () => { it("should register dynamic-list component", () => {
expect(ko.components.isRegistered("browse-queries-pane")).toBe(true); expect(ko.components.isRegistered("dynamic-list")).toBe(true);
}); });
it("should register graph-new-vertex-pane component", () => { it("should register throughput-input component", () => {
expect(ko.components.isRegistered("graph-new-vertex-pane")).toBe(true); expect(ko.components.isRegistered("throughput-input")).toBe(true);
}); });
it("should register graph-styling-pane component", () => { it("should register library-manage-pane component", () => {
expect(ko.components.isRegistered("graph-styling-pane")).toBe(true); expect(ko.components.isRegistered("library-manage-pane")).toBe(true);
}); });
it("should register upload-file-pane component", () => { it("should register cluster-library-pane component", () => {
expect(ko.components.isRegistered("upload-file-pane")).toBe(true); expect(ko.components.isRegistered("cluster-library-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,98 +1,83 @@
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 * as TreeComponents from "./Tree/TreeComponents"; import { CollapsiblePanelComponent } from "./Controls/CollapsiblePanel/CollapsiblePanelComponent";
import { CollapsiblePanelComponent } from "./Controls/CollapsiblePanel/CollapsiblePanelComponent"; import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent";
import { CommandButtonComponent } from "./Controls/CommandButton/CommandButton"; import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent";
import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent"; import { EditorComponent } from "./Controls/Editor/EditorComponent";
import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent"; import { ErrorDisplayComponent } from "./Controls/ErrorDisplayComponent/ErrorDisplayComponent";
import { EditorComponent } from "./Controls/Editor/EditorComponent"; import { GraphStyleComponent } from "./Graph/GraphStyleComponent/GraphStyleComponent";
import { ErrorDisplayComponent } from "./Controls/ErrorDisplayComponent/ErrorDisplayComponent"; import { InputTypeaheadComponent } from "./Controls/InputTypeahead/InputTypeahead";
import { GraphStyleComponent } from "./Graph/GraphStyleComponent/GraphStyleComponent"; import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent";
import { InputTypeaheadComponent } from "./Controls/InputTypeahead/InputTypeahead"; import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponent";
import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent"; import { ThroughputInputComponent } from "./Controls/ThroughputInput/ThroughputInputComponent";
import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponent"; import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
import { ThroughputInputComponent } from "./Controls/ThroughputInput/ThroughputInputComponent"; import { ToolbarComponent } from "./Controls/Toolbar/Toolbar";
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
import { ToolbarComponent } from "./Controls/Toolbar/Toolbar"; ko.components.register("toolbar", new ToolbarComponent());
ko.components.register("input-typeahead", new InputTypeaheadComponent());
ko.components.register("command-button", CommandButtonComponent); ko.components.register("new-vertex-form", NewVertexComponent);
ko.components.register("toolbar", new ToolbarComponent()); ko.components.register("error-display", new ErrorDisplayComponent());
ko.components.register("input-typeahead", new InputTypeaheadComponent()); ko.components.register("graph-style", GraphStyleComponent);
ko.components.register("new-vertex-form", NewVertexComponent); ko.components.register("collapsible-panel", new CollapsiblePanelComponent());
ko.components.register("error-display", new ErrorDisplayComponent()); ko.components.register("editor", new EditorComponent());
ko.components.register("graph-style", GraphStyleComponent); ko.components.register("json-editor", new JsonEditorComponent());
ko.components.register("collapsible-panel", new CollapsiblePanelComponent()); ko.components.register("diff-editor", new DiffEditorComponent());
ko.components.register("editor", new EditorComponent()); ko.components.register("dynamic-list", DynamicListComponent);
ko.components.register("json-editor", new JsonEditorComponent()); ko.components.register("throughput-input", ThroughputInputComponent);
ko.components.register("diff-editor", new DiffEditorComponent()); ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3);
ko.components.register("dynamic-list", DynamicListComponent);
ko.components.register("throughput-input", ThroughputInputComponent); // Collection Tabs
ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3); ko.components.register("documents-tab", new TabComponents.DocumentsTab());
ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTab());
// Collection Tabs ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab());
ko.components.register("documents-tab", new TabComponents.DocumentsTab()); ko.components.register("trigger-tab", new TabComponents.TriggerTab());
ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTab()); ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab());
ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab()); ko.components.register("settings-tab", new TabComponents.SettingsTab());
ko.components.register("trigger-tab", new TabComponents.TriggerTab()); ko.components.register("query-tab", new TabComponents.QueryTab());
ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab()); ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab());
ko.components.register("settings-tab", new TabComponents.SettingsTab()); ko.components.register("graph-tab", new TabComponents.GraphTab());
ko.components.register("query-tab", new TabComponents.QueryTab()); ko.components.register("mongo-shell-tab", new TabComponents.MongoShellTab());
ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab()); ko.components.register("conflicts-tab", new TabComponents.ConflictsTab());
ko.components.register("graph-tab", new TabComponents.GraphTab()); ko.components.register("notebookv2-tab", new TabComponents.NotebookV2Tab());
ko.components.register("mongo-shell-tab", new TabComponents.MongoShellTab()); ko.components.register("terminal-tab", new TabComponents.TerminalTab());
ko.components.register("conflicts-tab", new TabComponents.ConflictsTab()); ko.components.register("spark-master-tab", new TabComponents.SparkMasterTab());
ko.components.register("notebook-tab", new TabComponents.NotebookTab()); ko.components.register("gallery-tab", new TabComponents.GalleryTab());
ko.components.register("notebookv2-tab", new TabComponents.NotebookV2Tab()); ko.components.register("notebook-viewer-tab", new TabComponents.NotebookViewerTab());
ko.components.register("terminal-tab", new TabComponents.TerminalTab());
ko.components.register("spark-master-tab", new TabComponents.SparkMasterTab()); // Database Tabs
ko.components.register("gallery-tab", new TabComponents.GalleryTab()); ko.components.register("database-settings-tab", new TabComponents.DatabaseSettingsTab());
ko.components.register("notebook-viewer-tab", new TabComponents.NotebookViewerTab());
// Panes
// Database Tabs ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
ko.components.register("database-settings-tab", new TabComponents.DatabaseSettingsTab()); ko.components.register("add-collection-pane", new PaneComponents.AddCollectionPaneComponent());
ko.components.register(
// Resource Tree nodes "delete-collection-confirmation-pane",
ko.components.register("resource-tree", new TreeComponents.ResourceTree()); new PaneComponents.DeleteCollectionConfirmationPaneComponent()
ko.components.register("database-node", new TreeComponents.DatabaseTreeNode()); );
ko.components.register("collection-node", new TreeComponents.CollectionTreeNode()); ko.components.register(
ko.components.register("stored-procedure-node", new TreeComponents.StoredProcedureTreeNode()); "delete-database-confirmation-pane",
ko.components.register("trigger-node", new TreeComponents.TriggerTreeNode()); new PaneComponents.DeleteDatabaseConfirmationPaneComponent()
ko.components.register("user-defined-function-node", new TreeComponents.UserDefinedFunctionTreeNode()); );
ko.components.register("graph-new-vertex-pane", new PaneComponents.GraphNewVertexPaneComponent());
// Panes ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent());
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent()); ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent());
ko.components.register("add-collection-pane", new PaneComponents.AddCollectionPaneComponent()); ko.components.register("table-edit-entity-pane", new PaneComponents.TableEditEntityPaneComponent());
ko.components.register( ko.components.register("table-column-options-pane", new PaneComponents.TableColumnOptionsPaneComponent());
"delete-collection-confirmation-pane", ko.components.register("table-query-select-pane", new PaneComponents.TableQuerySelectPaneComponent());
new PaneComponents.DeleteCollectionConfirmationPaneComponent() ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent());
); ko.components.register("settings-pane", new PaneComponents.SettingsPaneComponent());
ko.components.register( ko.components.register("execute-sproc-params-pane", new PaneComponents.ExecuteSprocParamsComponent());
"delete-database-confirmation-pane", ko.components.register("renew-adhoc-access-pane", new PaneComponents.RenewAdHocAccessPane());
new PaneComponents.DeleteDatabaseConfirmationPaneComponent() ko.components.register("upload-items-pane", new PaneComponents.UploadItemsPaneComponent());
); ko.components.register("load-query-pane", new PaneComponents.LoadQueryPaneComponent());
ko.components.register("graph-new-vertex-pane", new PaneComponents.GraphNewVertexPaneComponent()); ko.components.register("save-query-pane", new PaneComponents.SaveQueryPaneComponent());
ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent()); ko.components.register("browse-queries-pane", new PaneComponents.BrowseQueriesPaneComponent());
ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent()); ko.components.register("upload-file-pane", new PaneComponents.UploadFilePaneComponent());
ko.components.register("table-edit-entity-pane", new PaneComponents.TableEditEntityPaneComponent()); ko.components.register("string-input-pane", new PaneComponents.StringInputPaneComponent());
ko.components.register("table-column-options-pane", new PaneComponents.TableColumnOptionsPaneComponent()); ko.components.register("setup-notebooks-pane", new PaneComponents.SetupNotebooksPaneComponent());
ko.components.register("table-query-select-pane", new PaneComponents.TableQuerySelectPaneComponent()); ko.components.register("setup-spark-cluster-pane", new PaneComponents.SetupSparkClusterPaneComponent());
ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent()); ko.components.register("manage-spark-cluster-pane", new PaneComponents.ManageSparkClusterPaneComponent());
ko.components.register("settings-pane", new PaneComponents.SettingsPaneComponent()); ko.components.register("library-manage-pane", new PaneComponents.LibraryManagePaneComponent());
ko.components.register("execute-sproc-params-pane", new PaneComponents.ExecuteSprocParamsComponent()); ko.components.register("cluster-library-pane", new PaneComponents.ClusterLibraryPaneComponent());
ko.components.register("renew-adhoc-access-pane", new PaneComponents.RenewAdHocAccessPane()); ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent());
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,6 +1,5 @@
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";
@@ -33,13 +32,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];
} }
@@ -53,7 +52,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",
}); });
} }
@@ -61,7 +60,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({
@@ -70,7 +69,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",
}); });
} }
@@ -81,7 +80,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({
@@ -90,7 +89,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({
@@ -99,7 +98,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",
}); });
} }
@@ -109,13 +108,16 @@ 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(container: ViewModels.Explorer): TreeNodeMenuItem[] { public static createStoreProcedureContextMenuItems(
container: ViewModels.Explorer,
storedProcedure: ViewModels.StoredProcedure
): TreeNodeMenuItem[] {
if (container.isPreferredApiCassandra()) { if (container.isPreferredApiCassandra()) {
return []; return [];
} }
@@ -123,16 +125,16 @@ export class ResourceTreeContextMenuButtonFactory {
return [ return [
{ {
iconSrc: DeleteSprocIcon, iconSrc: DeleteSprocIcon,
onClick: () => { onClick: () => storedProcedure.delete(),
const selectedStoreProcedure: ViewModels.StoredProcedure = container.findSelectedStoredProcedure(); label: "Delete Store Procedure",
selectedStoreProcedure && selectedStoreProcedure.delete(selectedStoreProcedure, null); },
},
label: "Delete Store Procedure"
}
]; ];
} }
public static createTriggerContextMenuItems(container: ViewModels.Explorer): TreeNodeMenuItem[] { public static createTriggerContextMenuItems(
container: ViewModels.Explorer,
trigger: ViewModels.Trigger
): TreeNodeMenuItem[] {
if (container.isPreferredApiCassandra()) { if (container.isPreferredApiCassandra()) {
return []; return [];
} }
@@ -140,16 +142,16 @@ export class ResourceTreeContextMenuButtonFactory {
return [ return [
{ {
iconSrc: DeleteTriggerIcon, iconSrc: DeleteTriggerIcon,
onClick: () => { onClick: () => trigger.delete(),
const selectedTrigger: ViewModels.Trigger = container.findSelectedTrigger(); label: "Delete Trigger",
selectedTrigger && selectedTrigger.delete(selectedTrigger, null); },
},
label: "Delete Trigger"
}
]; ];
} }
public static createUserDefinedFunctionContextMenuItems(container: ViewModels.Explorer): TreeNodeMenuItem[] { public static createUserDefinedFunctionContextMenuItems(
container: ViewModels.Explorer,
userDefinedFunction: ViewModels.UserDefinedFunction
): TreeNodeMenuItem[] {
if (container.isPreferredApiCassandra()) { if (container.isPreferredApiCassandra()) {
return []; return [];
} }
@@ -157,266 +159,9 @@ export class ResourceTreeContextMenuButtonFactory {
return [ return [
{ {
iconSrc: DeleteUDFIcon, iconSrc: DeleteUDFIcon,
onClick: () => { onClick: () => userDefinedFunction.delete(),
const selectedUDF: ViewModels.UserDefinedFunction = container.findSelectedUDF(); label: "Delete User Defined Function",
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 { Logger } from "../../../Common/Logger"; import * as 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

@@ -1,200 +0,0 @@
@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

@@ -1,139 +0,0 @@
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

@@ -1,191 +0,0 @@
/**
* 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,15 +15,20 @@ 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
@@ -144,9 +149,7 @@ 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) $(this.dropdownElt).show().focus();
.show()
.focus();
event.stopPropagation(); event.stopPropagation();
return false; return false;
} }
@@ -182,7 +185,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

@@ -1,40 +0,0 @@
<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