Compare commits

...

15 Commits

Author SHA1 Message Date
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
80 changed files with 1809 additions and 3536 deletions

View File

@@ -202,7 +202,6 @@ src/Explorer/Tabs/GraphTab.ts
src/Explorer/Tabs/MongoDocumentsTab.ts
src/Explorer/Tabs/MongoQueryTab.ts
src/Explorer/Tabs/MongoShellTab.ts
src/Explorer/Tabs/NotebookTab.ts
src/Explorer/Tabs/NotebookV2Tab.ts
src/Explorer/Tabs/QueryTab.test.ts
src/Explorer/Tabs/QueryTab.ts
@@ -216,7 +215,6 @@ src/Explorer/Tabs/TabComponents.ts
src/Explorer/Tabs/TabsBase.ts
src/Explorer/Tabs/TriggerTab.ts
src/Explorer/Tabs/UserDefinedFunctionTab.ts
src/Explorer/Tabs/__mocks__/NotebookTab.ts
src/Explorer/Tree/AccessibleVerticalList.ts
src/Explorer/Tree/Collection.test.ts
src/Explorer/Tree/Collection.ts

View File

@@ -4,11 +4,7 @@ module.exports = {
es6: true
},
plugins: ["@typescript-eslint"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
globals: {
Atomics: "readonly",
SharedArrayBuffer: "readonly"
@@ -40,6 +36,7 @@ module.exports = {
}
],
rules: {
curly: "error"
curly: "error",
"@typescript-eslint/no-unused-vars": "error"
}
};

View File

@@ -8,7 +8,41 @@ on:
pull_request:
branches: [master]
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
name: "Unit Tests"
steps:
@@ -36,13 +70,13 @@ jobs:
path: .cache
key: ${{ runner.os }}-build-cache
- run: npm run pack:prod
- run: npm run copyToConsumers
- run: cp -r ./Contracts ./dist/contracts
- uses: actions/upload-artifact@v2
with:
name: dist
path: dist/
endtoend:
name: "End to End Tests"
endtoendemulator:
name: "End To End Tests | Emulator | SQL"
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
@@ -67,9 +101,65 @@ jobs:
EMULATOR_ENDPOINT: https://0.0.0.0:8081/
NODE_TLS_REJECT_UNAUTHORIZED: 0
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:
name: Publish Nuget
needs: [build, test, endtoend]
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendsql, endtoendmongo]
runs-on: ubuntu-latest
env:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
@@ -90,7 +180,7 @@ jobs:
path: "*.nupkg"
nugetmpac:
name: Publish Nuget MPAC
needs: [build, test, endtoend]
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendsql, endtoendmongo]
runs-on: ubuntu-latest
env:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}

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

@@ -82,14 +82,4 @@ Unit tests are located adjacent to the code under test and run with [Jest](https
# Contributing
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.
Please read the [contribution guidelines](./CONTRIBUTING.md).

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

@@ -16,7 +16,7 @@ let crypt = require("crypto");
context("Mongo API Test - createDatabase", () => {
beforeEach(() => {
connectionString.loginUsingConnectionString(connectionString.constants.mongo);
connectionString.loginUsingConnectionString();
});
it("Create a new collection in Mongo API", () => {
@@ -63,7 +63,7 @@ context("Mongo API Test - createDatabase", () => {
.type(sharedKey);
cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.find("#submitBtnAddCollection")
.click();
cy.wait(10000);

View File

@@ -16,10 +16,10 @@ let crypt = require("crypto");
context("Mongo API Test", () => {
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 collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;

View File

@@ -4,10 +4,10 @@ let crypt = require("crypto");
context("Mongo API Test", () => {
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 sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;

View File

@@ -2,9 +2,9 @@ const connectionString = require("../../../utilities/connectionString");
let crypt = require("crypto");
context("Mongo API Test", () => {
context.skip("Mongo API Test", () => {
beforeEach(() => {
connectionString.loginUsingConnectionString(connectionString.constants.mongo);
connectionString.loginUsingConnectionString();
});
it("Create a new collection in Mongo API - Provision database throughput", () => {

View File

@@ -16,13 +16,14 @@ let crypt = require("crypto");
context("SQL API Test", () => {
beforeEach(() => {
connectionString.loginUsingConnectionString(connectionString.constants.sql);
connectionString.loginUsingConnectionString();
});
it("Create a new container in SQL API", () => {
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
connectionString.loginUsingConnectionString();
cy.get("iframe").then($element => {
const $body = $element.contents().find("body");
@@ -63,7 +64,7 @@ context("SQL API Test", () => {
.type(sharedKey);
cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.find("#submitBtnAddCollection")
.click();
cy.wait(10000);

View File

@@ -273,77 +273,12 @@
"any-observable": "^0.3.0"
}
},
"@types/blob-util": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@types/blob-util/-/blob-util-1.3.3.tgz",
"integrity": "sha512-4ahcL/QDnpjWA2Qs16ZMQif7HjGP2cw3AGjHabybjw7Vm1EKu+cfQN1D78BaZbS1WJNa1opSMF5HNMztx7lR0w==",
"@types/sinonjs__fake-timers": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz",
"integrity": "sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA==",
"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": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz",
@@ -827,24 +762,15 @@
}
},
"cypress": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-4.5.0.tgz",
"integrity": "sha512-2A4g5FW5d2fHzq8HKUGAMVTnW6P8nlWYQALiCoGN4bqBLvgwhYM/oG9oKc2CS6LnvgHFiKivKzpm9sfk3uU3zQ==",
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-4.8.0.tgz",
"integrity": "sha512-Lsff8lF8pq6k/ioNua783tCsxGSLp6gqGXecdIfqCkqjYiOA53XKuEf1CaQJLUVs1dHSf49eDUp/sb620oJjVQ==",
"dev": true,
"requires": {
"@cypress/listr-verbose-renderer": "0.4.1",
"@cypress/request": "2.88.5",
"@cypress/xvfb": "1.2.4",
"@types/blob-util": "1.3.3",
"@types/bluebird": "3.5.29",
"@types/chai": "4.2.7",
"@types/chai-jquery": "1.1.40",
"@types/jquery": "3.3.31",
"@types/lodash": "4.14.149",
"@types/minimatch": "3.0.3",
"@types/mocha": "5.2.7",
"@types/sinon": "7.5.1",
"@types/sinon-chai": "3.2.3",
"@types/sinonjs__fake-timers": "6.0.1",
"@types/sizzle": "2.3.2",
"arch": "2.1.1",
"bluebird": "3.7.2",
@@ -878,14 +804,6 @@
"untildify": "4.0.0",
"url": "0.11.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": {
@@ -1081,12 +999,6 @@
"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": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
@@ -1859,6 +1771,12 @@
"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": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",

View File

@@ -5,11 +5,13 @@
"main": "index.js",
"scripts": {
"test": "cypress run",
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
"test:sql": "cypress run --browser chrome --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:debug": "cypress open"
},
"devDependencies": {
"cypress": "^4.5.0",
"cypress": "^4.8.0",
"mocha": "^7.0.1",
"mochawesome": "^4.1.0",
"mochawesome-merge": "^4.0.1",

View File

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

View File

@@ -570,6 +570,12 @@ body {
}
}
.fileImportButton {
height: 24px;
border: @ButtonBorderWidth solid transparent;
vertical-align: top;
}
.fileUploadSummaryContainer {
margin-top: 40px;
@@ -1016,6 +1022,18 @@ menuQuickStart {
background: #262626;
}
.panelContent {
display: flex;
flex-direction: column;
flex: 1;
}
.panelContentWrapper {
display: flex;
flex-direction: column;
height: 100%;
}
.contextual-pane {
top: 0px;
right: 0 !important;
@@ -1232,23 +1250,25 @@ menuQuickStart {
padding: 2px 30px;
cursor: pointer;
font-size: 12px;
&:active {
border-color: #0072c6;
background-color: #0072c6;
}
}
.btncreatecoll1:hover {
background: @AccentMediumHigh;
.leftpanel-okbut .genericPaneSubmitBtn {
border: 1px solid @AccentMediumHigh;
background-color: @AccentMediumHigh;
color: #fff;
border-color: @AccentMediumHigh;
cursor: pointer;
font-size: 12px;
}
height: 24px;
.btncreatecoll1:active {
border: 1px solid #0072c6;
background-color: #0072c6;
color: white;
padding: 2px 30px;
cursor: pointer;
font-size: 12px;
&:active {
border-color: #0072c6;
background-color: #0072c6;
}
}
.btncreatecoll1-off {
@@ -1361,6 +1381,15 @@ p {
color: #000;
}
.headerline .closePaneBtn {
float: right;
cursor: pointer;
width: 16px;
height: 100%;
margin-right: 4px;
color: #000;
}
.closeImg {
float: right;
cursor: pointer;
@@ -1710,6 +1739,13 @@ input::-webkit-calendar-picker-indicator {
margin: (2 * @MediumSpace) 0px;
}
.contextual-pane .panelMainContent {
padding-left: 34px;
padding-right: 34px;
color: @BaseDark;
margin: (2 * @MediumSpace) 0px;
}
.contextual-pane .paneFooter {
width: 100%;
height: 60px;

599
package-lock.json generated
View File

@@ -5,9 +5,9 @@
"requires": true,
"dependencies": {
"@azure/cosmos": {
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.6.3.tgz",
"integrity": "sha512-JoCDxl0TnL6EHL4xD3KC9r2bMivK13q1jl7h69wd/YFLlt3aBTTCehtAX+y4alNSENpL53XdRdw/cna0mI2XDw==",
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.7.1.tgz",
"integrity": "sha512-P/80gz4c6gAjdilOTT7TofU3Vl1Wh21+U+/d/MAIdaXhsSspL44diyl0qfLRxXiosL4yT12SGHOE9w0n3U57yA==",
"requires": {
"@types/debug": "^4.1.4",
"debug": "^4.1.1",
@@ -17,8 +17,20 @@
"os-name": "^3.1.0",
"priorityqueuejs": "^1.0.0",
"semaphore": "^1.0.5",
"tslib": "^1.10.0",
"uuid": "^3.3.2"
"tslib": "^2.0.0",
"uuid": "^8.1.0"
},
"dependencies": {
"tslib": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz",
"integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g=="
},
"uuid": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.1.0.tgz",
"integrity": "sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg=="
}
}
},
"@azure/cosmos-language-service": {
@@ -1047,9 +1059,9 @@
}
},
"@babel/runtime-corejs3": {
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.9.6.tgz",
"integrity": "sha512-6toWAfaALQjt3KMZQc6fABqZwUDDuWzz+cAfPhqyEnzxvdWOAkjwPNxgF8xlmo7OWLsSjaKjsskpKHRLaMArOA==",
"version": "7.10.2",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.10.2.tgz",
"integrity": "sha512-+a2M/u7r15o3dV1NEizr9bRi+KUVnrs/qYxF0Z06DAPx/4VCWaz1WA7EcbE+uqGgt39lp5akWGmHsTseIkHkHg==",
"dev": true,
"requires": {
"core-js-pure": "^3.0.0",
@@ -3104,12 +3116,6 @@
"integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==",
"dev": true
},
"@types/events": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==",
"dev": true
},
"@types/geojson": {
"version": "7946.0.7",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.7.tgz",
@@ -3117,12 +3123,11 @@
"dev": true
},
"@types/glob": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz",
"integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==",
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-VgNIkxK+j7Nz5P7jvUZlRvhuPSmsEfS03b0alKcq5V/STUKAa3Plemsn5mrQUO7am6OErJ4rhGEGJbACclrtRA==",
"dev": true,
"requires": {
"@types/events": "*",
"@types/minimatch": "*",
"@types/node": "*"
}
@@ -3399,33 +3404,34 @@
"integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw=="
},
"@typescript-eslint/eslint-plugin": {
"version": "2.25.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.25.0.tgz",
"integrity": "sha512-W2YyMtjmlrOjtXc+FtTelVs9OhuR6OlYc4XKIslJ8PUJOqgYYAPRJhAqkYRQo3G4sjvG8jSodsNycEn4W2gHUw==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.2.0.tgz",
"integrity": "sha512-t9RTk/GyYilIXt6BmZurhBzuMT9kLKw3fQoJtK9ayv0tXTlznXEAnx07sCLXdkN3/tZDep1s1CEV95CWuARYWA==",
"dev": true,
"requires": {
"@typescript-eslint/experimental-utils": "2.25.0",
"@typescript-eslint/experimental-utils": "3.2.0",
"functional-red-black-tree": "^1.0.1",
"regexpp": "^3.0.0",
"semver": "^7.3.2",
"tsutils": "^3.17.1"
},
"dependencies": {
"@typescript-eslint/experimental-utils": {
"version": "2.25.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.25.0.tgz",
"integrity": "sha512-0IZ4ZR5QkFYbaJk+8eJ2kYeA+1tzOE1sBjbwwtSV85oNWYUBep+EyhlZ7DLUCyhMUGuJpcCCFL0fDtYAP1zMZw==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.2.0.tgz",
"integrity": "sha512-UbJBsk+xO9dIFKtj16+m42EvUvsjZbbgQ2O5xSTSfVT1Z3yGkL90DVu0Hd3029FZ5/uBgl+F3Vo8FAcEcqc6aQ==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.3",
"@typescript-eslint/typescript-estree": "2.25.0",
"@typescript-eslint/typescript-estree": "3.2.0",
"eslint-scope": "^5.0.0",
"eslint-utils": "^2.0.0"
}
},
"@typescript-eslint/typescript-estree": {
"version": "2.25.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.25.0.tgz",
"integrity": "sha512-VUksmx5lDxSi6GfmwSK7SSoIKSw9anukWWNitQPqt58LuYrKalzsgeuignbqnB+rK/xxGlSsCy8lYnwFfB6YJg==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.2.0.tgz",
"integrity": "sha512-uh+Y2QO7dxNrdLw7mVnjUqkwO/InxEqwN0wF+Za6eo3coxls9aH9kQ/5rSvW2GcNanebRTmsT5w1/92lAOb1bA==",
"dev": true,
"requires": {
"debug": "^4.1.1",
@@ -3433,57 +3439,57 @@
"glob": "^7.1.6",
"is-glob": "^4.0.1",
"lodash": "^4.17.15",
"semver": "^6.3.0",
"semver": "^7.3.2",
"tsutils": "^3.17.1"
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
"dev": true
}
}
},
"@typescript-eslint/experimental-utils": {
"version": "2.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.33.0.tgz",
"integrity": "sha512-qzPM2AuxtMrRq78LwyZa8Qn6gcY8obkIrBs1ehqmQADwkYzTE1Pb4y2W+U3rE/iFkSWcWHG2LS6MJfj6SmHApg==",
"version": "2.34.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz",
"integrity": "sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==",
"requires": {
"@types/json-schema": "^7.0.3",
"@typescript-eslint/typescript-estree": "2.33.0",
"@typescript-eslint/typescript-estree": "2.34.0",
"eslint-scope": "^5.0.0",
"eslint-utils": "^2.0.0"
}
},
"@typescript-eslint/parser": {
"version": "2.25.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.25.0.tgz",
"integrity": "sha512-mccBLaBSpNVgp191CP5W+8U1crTyXsRziWliCqzj02kpxdjKMvFHGJbK33NroquH3zB/gZ8H511HEsJBa2fNEg==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.2.0.tgz",
"integrity": "sha512-Vhu+wwdevDLVDjK1lIcoD6ZbuOa93fzqszkaO3iCnmrScmKwyW/AGkzc2UvfE5TCoCXqq7Jyt6SOXjsIlpqF4A==",
"dev": true,
"requires": {
"@types/eslint-visitor-keys": "^1.0.0",
"@typescript-eslint/experimental-utils": "2.25.0",
"@typescript-eslint/typescript-estree": "2.25.0",
"@typescript-eslint/experimental-utils": "3.2.0",
"@typescript-eslint/typescript-estree": "3.2.0",
"eslint-visitor-keys": "^1.1.0"
},
"dependencies": {
"@typescript-eslint/experimental-utils": {
"version": "2.25.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.25.0.tgz",
"integrity": "sha512-0IZ4ZR5QkFYbaJk+8eJ2kYeA+1tzOE1sBjbwwtSV85oNWYUBep+EyhlZ7DLUCyhMUGuJpcCCFL0fDtYAP1zMZw==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.2.0.tgz",
"integrity": "sha512-UbJBsk+xO9dIFKtj16+m42EvUvsjZbbgQ2O5xSTSfVT1Z3yGkL90DVu0Hd3029FZ5/uBgl+F3Vo8FAcEcqc6aQ==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.3",
"@typescript-eslint/typescript-estree": "2.25.0",
"@typescript-eslint/typescript-estree": "3.2.0",
"eslint-scope": "^5.0.0",
"eslint-utils": "^2.0.0"
}
},
"@typescript-eslint/typescript-estree": {
"version": "2.25.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.25.0.tgz",
"integrity": "sha512-VUksmx5lDxSi6GfmwSK7SSoIKSw9anukWWNitQPqt58LuYrKalzsgeuignbqnB+rK/xxGlSsCy8lYnwFfB6YJg==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.2.0.tgz",
"integrity": "sha512-uh+Y2QO7dxNrdLw7mVnjUqkwO/InxEqwN0wF+Za6eo3coxls9aH9kQ/5rSvW2GcNanebRTmsT5w1/92lAOb1bA==",
"dev": true,
"requires": {
"debug": "^4.1.1",
@@ -3491,22 +3497,22 @@
"glob": "^7.1.6",
"is-glob": "^4.0.1",
"lodash": "^4.17.15",
"semver": "^6.3.0",
"semver": "^7.3.2",
"tsutils": "^3.17.1"
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
"dev": true
}
}
},
"@typescript-eslint/typescript-estree": {
"version": "2.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.33.0.tgz",
"integrity": "sha512-d8rY6/yUxb0+mEwTShCQF2zYQdLlqihukNfG9IUlLYz5y1CH6G/9XYbrxQLq3Z14RNvkCC6oe+OcFlyUpwUbkg==",
"version": "2.34.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz",
"integrity": "sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg==",
"requires": {
"debug": "^4.1.1",
"eslint-visitor-keys": "^1.1.0",
@@ -7526,22 +7532,22 @@
}
},
"eslint": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz",
"integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==",
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.2.0.tgz",
"integrity": "sha512-B3BtEyaDKC5MlfDa2Ha8/D6DsS4fju95zs0hjS3HdGazw+LNayai38A25qMppK37wWGWNYSPOR6oYzlz5MHsRQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
"ajv": "^6.10.0",
"chalk": "^2.1.0",
"cross-spawn": "^6.0.5",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
"debug": "^4.0.1",
"doctrine": "^3.0.0",
"eslint-scope": "^5.0.0",
"eslint-utils": "^1.4.3",
"eslint-visitor-keys": "^1.1.0",
"espree": "^6.1.2",
"esquery": "^1.0.1",
"eslint-scope": "^5.1.0",
"eslint-utils": "^2.0.0",
"eslint-visitor-keys": "^1.2.0",
"espree": "^7.1.0",
"esquery": "^1.2.0",
"esutils": "^2.0.2",
"file-entry-cache": "^5.0.1",
"functional-red-black-tree": "^1.0.1",
@@ -7554,31 +7560,89 @@
"is-glob": "^4.0.0",
"js-yaml": "^3.13.1",
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.3.0",
"levn": "^0.4.1",
"lodash": "^4.17.14",
"minimatch": "^3.0.4",
"mkdirp": "^0.5.1",
"natural-compare": "^1.4.0",
"optionator": "^0.8.3",
"optionator": "^0.9.1",
"progress": "^2.0.0",
"regexpp": "^2.0.1",
"semver": "^6.1.2",
"strip-ansi": "^5.2.0",
"strip-json-comments": "^3.0.1",
"regexpp": "^3.1.0",
"semver": "^7.2.1",
"strip-ansi": "^6.0.0",
"strip-json-comments": "^3.1.0",
"table": "^5.2.3",
"text-table": "^0.2.0",
"v8-compile-cache": "^2.0.3"
},
"dependencies": {
"eslint-utils": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz",
"integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==",
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
},
"ansi-styles": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
"integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
"dev": true,
"requires": {
"eslint-visitor-keys": "^1.1.0"
"@types/color-name": "^1.1.1",
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dev": true,
"requires": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
}
},
"eslint-scope": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz",
"integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==",
"dev": true,
"requires": {
"esrecurse": "^4.1.0",
"estraverse": "^4.1.1"
}
},
"eslint-visitor-keys": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.2.0.tgz",
"integrity": "sha512-WFb4ihckKil6hu3Dp798xdzSfddwKKU3+nGniKF6HfeW6OLd2OUDEPP7TcHtB5+QXOKg2s6B2DaMPE1Nn/kxKQ==",
"dev": true
},
"globals": {
"version": "12.4.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz",
@@ -7588,29 +7652,116 @@
"type-fest": "^0.8.1"
}
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"ignore": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
"dev": true
},
"regexpp": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
"integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==",
"levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
"dev": true,
"requires": {
"prelude-ls": "^1.2.1",
"type-check": "~0.4.0"
}
},
"optionator": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
"integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
"dev": true,
"requires": {
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
"levn": "^0.4.1",
"prelude-ls": "^1.2.1",
"type-check": "^0.4.0",
"word-wrap": "^1.2.3"
}
},
"path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true
},
"prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
"dev": true
},
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"requires": {
"shebang-regex": "^3.0.0"
}
},
"shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true
},
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"dev": true,
"requires": {
"ansi-regex": "^5.0.0"
}
},
"strip-json-comments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz",
"integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==",
"dev": true
},
"supports-color": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
"integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
}
},
"type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
"requires": {
"prelude-ls": "^1.2.1"
}
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"requires": {
"isexe": "^2.0.0"
}
}
}
},
@@ -7643,17 +7794,17 @@
}
},
"eslint-plugin-jest": {
"version": "23.8.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-23.8.2.tgz",
"integrity": "sha512-xwbnvOsotSV27MtAe7s8uGWOori0nUsrXh2f1EnpmXua8sDfY6VZhHAhHg2sqK7HBNycRQExF074XSZ7DvfoFg==",
"version": "23.13.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-23.13.2.tgz",
"integrity": "sha512-qZit+moTXTyZFNDqSIR88/L3rdBlTU7CuW6XmyErD2FfHEkdoLgThkRbiQjzgYnX6rfgLx3Ci4eJmF4Ui5v1Cw==",
"requires": {
"@typescript-eslint/experimental-utils": "^2.5.0"
}
},
"eslint-plugin-react": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.19.0.tgz",
"integrity": "sha512-SPT8j72CGuAP+JFbT0sJHOB80TX/pu44gQ4vXH/cq+hQTiY2PuZ6IHkqXJV6x1b28GDdo1lbInjKUrrdUf0LOQ==",
"version": "7.20.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.20.0.tgz",
"integrity": "sha512-rqe1abd0vxMjmbPngo4NaYxTcR3Y4Hrmc/jg4T+sYz63yqlmJRknpEQfmWY+eDWPuMmix6iUIK+mv0zExjeLgA==",
"dev": true,
"requires": {
"array-includes": "^3.1.1",
@@ -7665,7 +7816,6 @@
"object.values": "^1.1.1",
"prop-types": "^15.7.2",
"resolve": "^1.15.1",
"semver": "^6.3.0",
"string.prototype.matchall": "^4.0.2",
"xregexp": "^4.3.0"
},
@@ -7680,22 +7830,15 @@
}
},
"object.entries": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.1.tgz",
"integrity": "sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.2.tgz",
"integrity": "sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA==",
"dev": true,
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.0-next.1",
"function-bind": "^1.1.1",
"es-abstract": "^1.17.5",
"has": "^1.0.3"
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
}
}
},
@@ -7722,20 +7865,26 @@
"integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A=="
},
"espree": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz",
"integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-7.1.0.tgz",
"integrity": "sha512-dcorZSyfmm4WTuTnE5Y7MEN1DyoPYy1ZR783QW1FJoenn7RailyWFsq/UL6ZAAA7uXurN9FIpYyUs3OfiIW+Qw==",
"dev": true,
"requires": {
"acorn": "^7.1.1",
"acorn": "^7.2.0",
"acorn-jsx": "^5.2.0",
"eslint-visitor-keys": "^1.1.0"
"eslint-visitor-keys": "^1.2.0"
},
"dependencies": {
"acorn": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.2.0.tgz",
"integrity": "sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ==",
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.3.1.tgz",
"integrity": "sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA==",
"dev": true
},
"eslint-visitor-keys": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.2.0.tgz",
"integrity": "sha512-WFb4ihckKil6hu3Dp798xdzSfddwKKU3+nGniKF6HfeW6OLd2OUDEPP7TcHtB5+QXOKg2s6B2DaMPE1Nn/kxKQ==",
"dev": true
}
}
@@ -9707,16 +9856,10 @@
"toidentifier": "1.0.0"
}
},
"http-parser-js": {
"version": "0.4.10",
"resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz",
"integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=",
"dev": true
},
"http-proxy": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz",
"integrity": "sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==",
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
"dev": true,
"requires": {
"eventemitter3": "^4.0.0",
@@ -9989,9 +10132,9 @@
}
},
"inquirer": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz",
"integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==",
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.2.0.tgz",
"integrity": "sha512-E0c4rPwr9ByePfNlTIB8z51kK1s2n6jrHuJeEHENl/sbq2G/S1auvibgEwNR4uSyiU+PiYHqSwsgGiXjG8p5ZQ==",
"dev": true,
"requires": {
"ansi-escapes": "^4.2.1",
@@ -11209,12 +11352,12 @@
}
},
"jsx-ast-utils": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz",
"integrity": "sha512-EdIHFMm+1BPynpKOpdPqiOsvnIrInRGJD7bzPZdPkjitQEqpdpUuFpq4T0npZFKTiB3RhWFdGN+oqOJIdhDhQA==",
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz",
"integrity": "sha512-z1xSldJ6imESSzOjd3NNkieVJKRlKYSOtMG8SFyCj2FIrvSaSuli/WjpBkEzCBoR9bYYYFgqJw61Xhu7Lcgk+w==",
"dev": true,
"requires": {
"array-includes": "^3.0.3",
"array-includes": "^3.1.1",
"object.assign": "^4.1.0"
}
},
@@ -15535,13 +15678,14 @@
}
},
"sockjs": {
"version": "0.3.19",
"resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz",
"integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==",
"version": "0.3.20",
"resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.20.tgz",
"integrity": "sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA==",
"dev": true,
"requires": {
"faye-websocket": "^0.10.0",
"uuid": "^3.0.1"
"uuid": "^3.4.0",
"websocket-driver": "0.6.5"
}
},
"sockjs-client": {
@@ -17314,9 +17458,9 @@
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
},
"v8-compile-cache": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz",
"integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz",
"integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==",
"dev": true
},
"v8flags": {
@@ -17983,9 +18127,9 @@
}
},
"webpack-dev-server": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.9.0.tgz",
"integrity": "sha512-E6uQ4kRrTX9URN9s/lIbqTAztwEPdvzVrcmHE8EQ9YnuT9J8Es5Wrd8n9BKg1a0oZ5EgEke/EQFgUsp18dSTBw==",
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz",
"integrity": "sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg==",
"dev": true,
"requires": {
"ansi-html": "0.0.7",
@@ -17996,31 +18140,31 @@
"debug": "^4.1.1",
"del": "^4.1.1",
"express": "^4.17.1",
"html-entities": "^1.2.1",
"html-entities": "^1.3.1",
"http-proxy-middleware": "0.19.1",
"import-local": "^2.0.0",
"internal-ip": "^4.3.0",
"ip": "^1.1.5",
"is-absolute-url": "^3.0.3",
"killable": "^1.0.1",
"loglevel": "^1.6.4",
"loglevel": "^1.6.8",
"opn": "^5.5.0",
"p-retry": "^3.0.1",
"portfinder": "^1.0.25",
"portfinder": "^1.0.26",
"schema-utils": "^1.0.0",
"selfsigned": "^1.10.7",
"semver": "^6.3.0",
"serve-index": "^1.9.1",
"sockjs": "0.3.19",
"sockjs": "0.3.20",
"sockjs-client": "1.4.0",
"spdy": "^4.0.1",
"spdy": "^4.0.2",
"strip-ansi": "^3.0.1",
"supports-color": "^6.1.0",
"url": "^0.11.0",
"webpack-dev-middleware": "^3.7.2",
"webpack-log": "^2.0.0",
"ws": "^6.2.1",
"yargs": "12.0.5"
"yargs": "^13.3.2"
},
"dependencies": {
"ansi-regex": {
@@ -18029,72 +18173,6 @@
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"dev": true
},
"cliui": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz",
"integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==",
"dev": true,
"requires": {
"string-width": "^2.1.1",
"strip-ansi": "^4.0.0",
"wrap-ansi": "^2.0.0"
},
"dependencies": {
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
"dev": true
},
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"dev": true,
"requires": {
"ansi-regex": "^3.0.0"
}
}
}
},
"get-caller-file": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
"integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
"dev": true
},
"invert-kv": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz",
"integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==",
"dev": true
},
"lcid": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz",
"integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==",
"dev": true,
"requires": {
"invert-kv": "^2.0.0"
}
},
"os-locale": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz",
"integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==",
"dev": true,
"requires": {
"execa": "^1.0.0",
"lcid": "^2.0.0",
"mem": "^4.0.0"
}
},
"require-main-filename": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
"integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
"dev": true
},
"schema-utils": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
@@ -18112,33 +18190,6 @@
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
},
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
"dev": true,
"requires": {
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^4.0.0"
},
"dependencies": {
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
"dev": true
},
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"dev": true,
"requires": {
"ansi-regex": "^3.0.0"
}
}
}
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
@@ -18157,38 +18208,6 @@
"has-flag": "^3.0.0"
}
},
"wrap-ansi": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
"integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
"dev": true,
"requires": {
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1"
},
"dependencies": {
"is-fullwidth-code-point": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"dev": true,
"requires": {
"number-is-nan": "^1.0.0"
}
},
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
"strip-ansi": "^3.0.0"
}
}
}
},
"ws": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
@@ -18197,36 +18216,6 @@
"requires": {
"async-limiter": "~1.0.0"
}
},
"yargs": {
"version": "12.0.5",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz",
"integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==",
"dev": true,
"requires": {
"cliui": "^4.0.0",
"decamelize": "^1.2.0",
"find-up": "^3.0.0",
"get-caller-file": "^1.0.1",
"os-locale": "^3.0.0",
"require-directory": "^2.1.1",
"require-main-filename": "^1.0.1",
"set-blocking": "^2.0.0",
"string-width": "^2.0.0",
"which-module": "^2.0.0",
"y18n": "^3.2.1 || ^4.0.0",
"yargs-parser": "^11.1.1"
}
},
"yargs-parser": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz",
"integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
},
@@ -18259,20 +18248,18 @@
}
},
"websocket-driver": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz",
"integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==",
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz",
"integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=",
"dev": true,
"requires": {
"http-parser-js": ">=0.4.0 <0.4.11",
"safe-buffer": ">=5.1.0",
"websocket-extensions": ">=0.1.1"
}
},
"websocket-extensions": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz",
"integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==",
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
"integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
"dev": true
},
"whatwg-encoding": {

View File

@@ -4,7 +4,7 @@
"description": "Cosmos Explorer",
"main": "index.js",
"dependencies": {
"@azure/cosmos": "3.6.3",
"@azure/cosmos": "3.7.1",
"@azure/cosmos-language-service": "0.0.4",
"@jupyterlab/services": "4.2.0",
"@jupyterlab/terminal": "1.2.1",
@@ -50,7 +50,7 @@
"dayjs": "1.8.19",
"es6-object-assign": "1.1.0",
"es6-symbol": "3.1.3",
"eslint-plugin-jest": "23.8.2",
"eslint-plugin-jest": "23.13.2",
"hasher": "1.2.0",
"immutable": "4.0.0-rc.12",
"jquery": "3.4.0",
@@ -110,8 +110,8 @@
"@types/text-encoding": "0.0.33",
"@types/underscore": "1.7.36",
"@types/webfontloader": "1.6.29",
"@typescript-eslint/eslint-plugin": "2.25.0",
"@typescript-eslint/parser": "2.25.0",
"@typescript-eslint/eslint-plugin": "3.2.0",
"@typescript-eslint/parser": "3.2.0",
"adal-angular": "1.0.15",
"babel-jest": "24.9.0",
"babel-loader": "8.1.0",
@@ -123,9 +123,9 @@
"enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.15.1",
"enzyme-to-json": "3.4.3",
"eslint": "6.8.0",
"eslint": "7.2.0",
"eslint-cli": "1.1.1",
"eslint-plugin-react": "7.19.0",
"eslint-plugin-react": "7.20.0",
"expose-loader": "0.7.5",
"file-loader": "2.0.0",
"fs-extra": "7.0.0",
@@ -155,7 +155,7 @@
"webpack": "4.41.2",
"webpack-bundle-analyzer": "3.6.1",
"webpack-cli": "3.3.10",
"webpack-dev-server": "3.9.0",
"webpack-dev-server": "3.11.0",
"worker-loader": "2.0.0"
},
"scripts": {

View File

@@ -104,11 +104,9 @@ export class CapabilityNames {
}
export class Features {
public static readonly graphs = "graphs";
public static readonly cosmosdb = "cosmosdb";
public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy";
public static readonly enableRupm = "enablerupm";
public static readonly cacheOptimizations = "dataexplorercacheoptimizations";
public static readonly executeSproc = "dataexplorerexecutesproc";
public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
public static readonly enableTtl = "enablettl";
@@ -116,17 +114,14 @@ export class Features {
public static readonly enableGallery = "enablegallery";
public static readonly enableSpark = "enablespark";
public static readonly livyEndpoint = "livyendpoint";
public static readonly settingsPane = "dataexplorersettingspane";
public static readonly throughputOverview = "throughputOverview";
public static readonly enableNteract = "enablenteract";
public static readonly notebookServerUrl = "notebookserverurl";
public static readonly notebookServerToken = "notebookservertoken";
public static readonly notebookBasePath = "notebookbasepath";
public static readonly enableLegacyResourceTree = "enablelegacyresourcetree";
public static readonly canExceedMaximumValue = "canexceedmaximumvalue";
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
public static readonly enableAutoPilotV2 = "enableautopilotv2";
public static readonly ttl90Days = "ttl90days";
public static readonly enableRightPanelV2 = "enablerightpanelv2";
}
export class AfecFeatures {

View File

@@ -21,13 +21,15 @@ const _global = typeof self === "undefined" ? window : self;
export const tokenProvider = async (requestInfo: RequestInfo) => {
const { verb, resourceId, resourceType, headers } = requestInfo;
if (config.platform === Platform.Emulator) {
// TODO Remove any. SDK expects a return value for tokenProvider, but we are mutating the header object instead.
return setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey) as any;
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
return decodeURIComponent(headers.authorization);
}
if (_masterKey) {
// TODO Remove any. SDK expects a return value for tokenProvider, but we are mutating the header object instead.
return setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, _masterKey) as any;
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
return decodeURIComponent(headers.authorization);
}
if (_resourceToken) {
@@ -47,7 +49,9 @@ export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, next) =>
export const endpoint = () => {
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);
};

View File

@@ -5,6 +5,7 @@ import { SeverityLevel } from "@microsoft/applicationinsights-web";
// 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 {
let logMessage: string;
if (typeof message === "string") {

View File

@@ -9,7 +9,6 @@ import { AccessibleVerticalList } from "../Explorer/Tree/AccessibleVerticalList"
import { ArcadiaWorkspaceItem } from "../Explorer/Controls/Arcadia/ArcadiaMenuPicker";
import { CassandraTableKey, CassandraTableKeys, TableDataClient } from "../Explorer/Tables/TableDataClient";
import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent";
import { CommandButtonOptions } from "../Explorer/Controls/CommandButton/CommandButton";
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { ExecuteSprocParam } from "../Explorer/Panes/ExecuteSprocParamsPane";
import { GitHubClient } from "../GitHub/GitHubClient";
@@ -27,6 +26,7 @@ import { Splitter } from "../Common/Splitter";
import { StringInputPane } from "../Explorer/Panes/StringInputPane";
import { TextFieldProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent";
import { UploadDetails } from "../workers/upload/definitions";
import { UploadItemsPaneAdapter } from "../Explorer/Panes/UploadItemsPaneAdapter";
export interface ExplorerOptions {
documentClientUtility: DocumentClientUtilityBase;
@@ -86,7 +86,7 @@ export interface Explorer {
isFeatureEnabled: (feature: string) => boolean;
isGalleryEnabled: ko.Computed<boolean>;
isGitHubPaneEnabled: ko.Observable<boolean>;
isGraphsEnabled: ko.Computed<boolean>;
isRightPanelV2Enabled: ko.Computed<boolean>;
canExceedMaximumValue: ko.Computed<boolean>;
hasAutoPilotV2FeatureFlag: ko.Computed<boolean>;
isHostedDataExplorerEnabled: ko.Computed<boolean>;
@@ -141,6 +141,7 @@ export interface Explorer {
executeSprocParamsPane: ExecuteSprocParamsPane;
renewAdHocAccessPane: RenewAdHocAccessPane;
uploadItemsPane: UploadItemsPane;
uploadItemsPaneAdapter: UploadItemsPaneAdapter;
loadQueryPane: LoadQueryPane;
saveQueryPane: ContextualPane;
browseQueriesPane: BrowseQueriesPane;
@@ -337,17 +338,6 @@ export interface Button {
isSelected?: ko.Computed<boolean>;
}
export interface CommandButton {
disabled: ko.Subscribable<boolean>;
visible: ko.Subscribable<boolean>;
iconSrc: string;
commandButtonLabel: string | ko.Observable<string>;
tooltipText: string | ko.Observable<string>;
children: ko.ObservableArray<CommandButtonOptions>;
commandClickCallback: () => void;
}
export interface NotificationConsole {
filteredConsoleData: ko.ObservableArray<ConsoleData>;
isConsoleExpanded: ko.Observable<boolean>;
@@ -371,7 +361,6 @@ export interface TreeNode {
id: ko.Observable<string>;
database?: Database;
collection?: Collection;
contextMenu?: ContextMenu;
onNewQueryClick?(source: any, event: MouseEvent): void;
onNewStoredProcedureClick?(source: Collection, event: MouseEvent): void;
@@ -536,7 +525,7 @@ export interface StoredProcedure extends TreeNode {
id: ko.Observable<string>;
body: ko.Observable<string>;
delete(source: TreeNode, event: MouseEvent | KeyboardEvent): void;
delete(): void;
open: () => void;
select(): void;
execute(params: string[], partitionKeyValue?: string): void;
@@ -550,7 +539,7 @@ export interface UserDefinedFunction extends TreeNode {
id: ko.Observable<string>;
body: ko.Observable<string>;
delete(source: TreeNode, event: MouseEvent | KeyboardEvent): void;
delete(): void;
open: () => void;
select(): void;
}
@@ -565,7 +554,7 @@ export interface Trigger extends TreeNode {
triggerType: ko.Observable<string>;
triggerOperation: ko.Observable<string>;
delete(source: TreeNode, event: MouseEvent | KeyboardEvent): void;
delete(): void;
open: () => void;
select(): void;
}
@@ -1174,7 +1163,6 @@ export interface TriggerTab extends ScriptTab {
}
export interface GraphTab extends Tab {}
export interface NotebookTab extends Tab {}
export interface EditorPosition {
line: number;
column: number;
@@ -1218,7 +1206,7 @@ export enum CollectionTabKind {
MongoShell = 10,
DatabaseSettings = 11,
Conflicts = 12,
Notebook = 13,
Notebook = 13 /* Deprecated */,
Terminal = 14,
NotebookV2 = 15,
SparkMasterTab = 16,
@@ -1232,17 +1220,6 @@ export enum TerminalKind {
Cassandra = 2
}
export interface ContextMenu {
container: Explorer;
visible: ko.Observable<boolean>;
elementId: string;
options: ko.ObservableArray<CommandButtonOptions>;
tabIndex: ko.Observable<number>;
show(source: any, event: MouseEvent | KeyboardEvent): void;
hide(source: any, event: MouseEvent | KeyboardEvent): void;
}
export interface DataExplorerInputsFrame {
databaseAccount: any;
subscriptionId: string;

View File

@@ -4,10 +4,6 @@ import * as ko from "knockout";
import "./ComponentRegisterer";
describe("Component Registerer", () => {
it("should register command-button component", () => {
expect(ko.components.isRegistered("command-button")).toBe(true);
});
it("should register input-typeahead component", () => {
expect(ko.components.isRegistered("input-typeahead")).toBe(true);
});
@@ -64,10 +60,6 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("graph-tab")).toBe(true);
});
it("should register notebook-tab component", () => {
expect(ko.components.isRegistered("notebook-tab")).toBe(true);
});
it("should register notebookv2-tab component", () => {
expect(ko.components.isRegistered("notebookv2-tab")).toBe(true);
});
@@ -84,30 +76,6 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("mongo-shell-tab")).toBe(true);
});
it("should register resource-tree component", () => {
expect(ko.components.isRegistered("resource-tree")).toBe(true);
});
it("should register database-node component", () => {
expect(ko.components.isRegistered("database-node")).toBe(true);
});
it("should register collection-node component", () => {
expect(ko.components.isRegistered("collection-node")).toBe(true);
});
it("should register stored-procedure-node component", () => {
expect(ko.components.isRegistered("stored-procedure-node")).toBe(true);
});
it("should register trigger-node component", () => {
expect(ko.components.isRegistered("trigger-node")).toBe(true);
});
it("should register user-defined-function-node component", () => {
expect(ko.components.isRegistered("user-defined-function-node")).toBe(true);
});
it("should registeradd-collection-pane component", () => {
expect(ko.components.isRegistered("add-collection-pane")).toBe(true);
});
@@ -156,10 +124,6 @@ describe("Component Registerer", () => {
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);
});

View File

@@ -1,9 +1,7 @@
import * as ko from "knockout";
import * as PaneComponents from "./Panes/PaneComponents";
import * as TabComponents from "./Tabs/TabComponents";
import * as TreeComponents from "./Tree/TreeComponents";
import { CollapsiblePanelComponent } from "./Controls/CollapsiblePanel/CollapsiblePanelComponent";
import { CommandButtonComponent } from "./Controls/CommandButton/CommandButton";
import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent";
import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent";
import { EditorComponent } from "./Controls/Editor/EditorComponent";
@@ -16,7 +14,6 @@ import { ThroughputInputComponent } from "./Controls/ThroughputInput/ThroughputI
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
import { ToolbarComponent } from "./Controls/Toolbar/Toolbar";
ko.components.register("command-button", CommandButtonComponent);
ko.components.register("toolbar", new ToolbarComponent());
ko.components.register("input-typeahead", new InputTypeaheadComponent());
ko.components.register("new-vertex-form", NewVertexComponent);
@@ -42,7 +39,6 @@ ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab());
ko.components.register("graph-tab", new TabComponents.GraphTab());
ko.components.register("mongo-shell-tab", new TabComponents.MongoShellTab());
ko.components.register("conflicts-tab", new TabComponents.ConflictsTab());
ko.components.register("notebook-tab", new TabComponents.NotebookTab());
ko.components.register("notebookv2-tab", new TabComponents.NotebookV2Tab());
ko.components.register("terminal-tab", new TabComponents.TerminalTab());
ko.components.register("spark-master-tab", new TabComponents.SparkMasterTab());
@@ -52,14 +48,6 @@ ko.components.register("notebook-viewer-tab", new TabComponents.NotebookViewerTa
// Database Tabs
ko.components.register("database-settings-tab", new TabComponents.DatabaseSettingsTab());
// Resource Tree nodes
ko.components.register("resource-tree", new TreeComponents.ResourceTree());
ko.components.register("database-node", new TreeComponents.DatabaseTreeNode());
ko.components.register("collection-node", new TreeComponents.CollectionTreeNode());
ko.components.register("stored-procedure-node", new TreeComponents.StoredProcedureTreeNode());
ko.components.register("trigger-node", new TreeComponents.TriggerTreeNode());
ko.components.register("user-defined-function-node", new TreeComponents.UserDefinedFunctionTreeNode());
// Panes
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
ko.components.register("add-collection-pane", new PaneComponents.AddCollectionPaneComponent());
@@ -93,6 +81,3 @@ ko.components.register("manage-spark-cluster-pane", new PaneComponents.ManageSpa
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 ViewModels from "../Contracts/ViewModels";
import { CommandButtonOptions } from "./Controls/CommandButton/CommandButton";
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
import AddCollectionIcon from "../../images/AddCollection.svg";
import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg";
@@ -115,7 +114,10 @@ export class ResourceTreeContextMenuButtonFactory {
return items;
}
public static createStoreProcedureContextMenuItems(container: ViewModels.Explorer): TreeNodeMenuItem[] {
public static createStoreProcedureContextMenuItems(
container: ViewModels.Explorer,
storedProcedure: ViewModels.StoredProcedure
): TreeNodeMenuItem[] {
if (container.isPreferredApiCassandra()) {
return [];
}
@@ -123,16 +125,16 @@ export class ResourceTreeContextMenuButtonFactory {
return [
{
iconSrc: DeleteSprocIcon,
onClick: () => {
const selectedStoreProcedure: ViewModels.StoredProcedure = container.findSelectedStoredProcedure();
selectedStoreProcedure && selectedStoreProcedure.delete(selectedStoreProcedure, null);
},
onClick: () => storedProcedure.delete(),
label: "Delete Store Procedure"
}
];
}
public static createTriggerContextMenuItems(container: ViewModels.Explorer): TreeNodeMenuItem[] {
public static createTriggerContextMenuItems(
container: ViewModels.Explorer,
trigger: ViewModels.Trigger
): TreeNodeMenuItem[] {
if (container.isPreferredApiCassandra()) {
return [];
}
@@ -140,16 +142,16 @@ export class ResourceTreeContextMenuButtonFactory {
return [
{
iconSrc: DeleteTriggerIcon,
onClick: () => {
const selectedTrigger: ViewModels.Trigger = container.findSelectedTrigger();
selectedTrigger && selectedTrigger.delete(selectedTrigger, null);
},
onClick: () => trigger.delete(),
label: "Delete Trigger"
}
];
}
public static createUserDefinedFunctionContextMenuItems(container: ViewModels.Explorer): TreeNodeMenuItem[] {
public static createUserDefinedFunctionContextMenuItems(
container: ViewModels.Explorer,
userDefinedFunction: ViewModels.UserDefinedFunction
): TreeNodeMenuItem[] {
if (container.isPreferredApiCassandra()) {
return [];
}
@@ -157,266 +159,9 @@ export class ResourceTreeContextMenuButtonFactory {
return [
{
iconSrc: DeleteUDFIcon,
onClick: () => {
const selectedUDF: ViewModels.UserDefinedFunction = container.findSelectedUDF();
selectedUDF && selectedUDF.delete(selectedUDF, null);
},
onClick: () => userDefinedFunction.delete(),
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

@@ -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

@@ -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

@@ -0,0 +1,20 @@
.featurePanelComponentContainer {
width: 800px;
.urlContainer {
padding: 10px;
margin-bottom: 10px;
white-space: nowrap;
overflow: auto;
}
.options {
padding: 10px;
overflow: auto;
height: 100%;
}
.checkboxRow {
width: 390px;
}
}

View File

@@ -0,0 +1,11 @@
import React from "react";
import { shallow } from "enzyme";
import { FeaturePanelComponent } from "./FeaturePanelComponent";
describe("Feature panel", () => {
it("renders all flags", () => {
const wrapper = shallow(<FeaturePanelComponent />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,252 @@
import * as React from "react";
import { Stack } from "office-ui-fabric-react/lib/Stack";
import { Dropdown, IDropdownOption, IDropdownStyles } from "office-ui-fabric-react/lib/Dropdown";
import { Checkbox } from "office-ui-fabric-react/lib/Checkbox";
import { TextField, ITextFieldStyles } from "office-ui-fabric-react/lib/TextField";
import { DefaultButton } from "office-ui-fabric-react";
import "./FeaturePanelComponent.less";
export const FeaturePanelComponent: React.FunctionComponent = () => {
// Initial conditions
const originalParams = new URLSearchParams(window.location.search);
const urlParams = new Map(); // Params with lowercase keys
originalParams.forEach((value: string, key: string) => urlParams.set(key.toLocaleLowerCase(), value));
const baseUrlOptions = [
{ key: "https://localhost:1234/explorer.html", text: "localhost:1234" },
{ key: "https://cosmos.azure.com/explorer.html", text: "cosmos.azure.com" },
{ key: "https://portal.azure.com", text: "portal" }
];
const platformOptions = [
{ key: "Hosted", text: "Hosted" },
{ key: "Portal", text: "Portal" },
{ key: "Emulator", text: "Emulator" },
{ key: "", text: "None" }
];
// React hooks to keep state
const [baseUrl, setBaseUrl] = React.useState<IDropdownOption>(
baseUrlOptions.find(o => o.key === window.location.origin + window.location.pathname) || baseUrlOptions[0]
);
const [platform, setPlatform] = React.useState<IDropdownOption>(
urlParams.has("platform")
? platformOptions.find(o => o.key === urlParams.get("platform")) || platformOptions[0]
: platformOptions[0]
);
const booleanFeatures: {
key: string;
label: string;
value: string;
disabled?: () => boolean;
reactState?: [boolean, React.Dispatch<React.SetStateAction<boolean>>];
onChange?: (_?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => void;
}[] = [
{ key: "feature.enablechangefeedpolicy", label: "Enable change feed policy", value: "true" },
{ key: "feature.enablerupm", label: "Enable RUPM", value: "true" },
{ key: "feature.dataexplorerexecutesproc", label: "Execute stored procedure", value: "true" },
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
{ key: "feature.enablettl", label: "Enable TTL", value: "true" },
{ key: "feature.enablegallery", label: "Enable Notebook Gallery", value: "true" },
{ key: "feature.canexceedmaximumvalue", label: "Can exceed max value", value: "true" },
{
key: "feature.enablefixedcollectionwithsharedthroughput",
label: "Enable fixed collection with shared throughput",
value: "true"
},
{ key: "feature.ttl90days", label: "TTL 90 days", value: "true" },
{ key: "feature.enablenotebooks", label: "Enable notebooks", value: "true" },
{
key: "feature.customportal",
label: "Force Production portal (portal only)",
value: "false",
disabled: (): boolean => baseUrl.key !== "https://portal.azure.com"
},
{ key: "feature.enablespark", label: "Enable Synapse", value: "true" },
{ key: "feature.enableautopilotv2", label: "Enable Auto-pilot V2", value: "true" }
];
const stringFeatures: {
key: string;
label: string;
placeholder: string;
disabled?: () => boolean;
reactState?: [string, React.Dispatch<React.SetStateAction<string>>];
onChange?: (_: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => void;
}[] = [
{ key: "feature.notebookserverurl", label: "Notebook server URL", placeholder: "https://notebookserver" },
{ key: "feature.notebookservertoken", label: "Notebook server token", placeholder: "" },
{ key: "feature.notebookbasepath", label: "Notebook base path", placeholder: "" },
{ key: "key", label: "Auth key", placeholder: "" },
{
key: "dataExplorerSource",
label: "Data Explorer Source (portal only)",
placeholder: "https://localhost:1234/explorer.html",
disabled: (): boolean => baseUrl.key !== "https://portal.azure.com"
},
{ key: "feature.livyendpoint", label: "Livy endpoint", placeholder: "" }
];
booleanFeatures.forEach(
f => (f.reactState = React.useState<boolean>(urlParams.has(f.key) ? urlParams.get(f.key) === "true" : false))
);
stringFeatures.forEach(
f => (f.reactState = React.useState<string>(urlParams.has(f.key) ? urlParams.get(f.key) : undefined))
);
const buildUrl = (): string => {
const fragments = (platform.key === "" ? [] : [`platform=${platform.key}`])
.concat(booleanFeatures.map(f => (f.reactState[0] ? `${f.key}=${f.value}` : "")))
.concat(stringFeatures.map(f => (f.reactState[0] ? `${f.key}=${encodeURIComponent(f.reactState[0])}` : "")))
.filter(v => v && v.length > 0);
const paramString = fragments.length < 1 ? "" : `?${fragments.join("&")}`;
return `${baseUrl.key}${paramString}`;
};
const onChangeBaseUrl = (event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {
setBaseUrl(option);
};
const onChangePlatform = (event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {
setPlatform(option);
};
booleanFeatures.forEach(
f =>
(f.onChange = (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean): void => {
f.reactState[1](checked);
})
);
stringFeatures.forEach(
f =>
(f.onChange = (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string): void => {
f.reactState[1](newValue);
})
);
const onNotebookShortcut = (): void => {
booleanFeatures.find(f => f.key === "feature.enablenotebooks").reactState[1](true);
stringFeatures
.find(f => f.key === "feature.notebookserverurl")
.reactState[1]("https://localhost:10001/12345/notebook/");
stringFeatures.find(f => f.key === "feature.notebookservertoken").reactState[1]("token");
stringFeatures.find(f => f.key === "feature.notebookbasepath").reactState[1](".");
setPlatform(platformOptions.find(o => o.key === "Hosted"));
};
const onPortalLocalDEShortcut = (): void => {
setBaseUrl(baseUrlOptions.find(o => o.key === "https://portal.azure.com"));
setPlatform(platformOptions.find(o => o.key === "Portal"));
stringFeatures.find(f => f.key === "dataExplorerSource").reactState[1]("https://localhost:1234/explorer.html");
};
const onReset = (): void => {
booleanFeatures.forEach(f => f.reactState[1](false));
stringFeatures.forEach(f => f.reactState[1](""));
};
const stackTokens = { childrenGap: 10 };
const dropdownStyles: Partial<IDropdownStyles> = { dropdown: { width: 200 } };
const textFieldStyles: Partial<ITextFieldStyles> = { fieldGroup: { width: 300 } };
// Show in 2 columns to keep it compact
let halfSize = Math.ceil(booleanFeatures.length / 2);
const leftBooleanFeatures = booleanFeatures.slice(0, halfSize);
const rightBooleanFeatures = booleanFeatures.slice(halfSize, booleanFeatures.length);
halfSize = Math.ceil(stringFeatures.length / 2);
const leftStringFeatures = stringFeatures.slice(0, halfSize);
const rightStringFeatures = stringFeatures.slice(halfSize, stringFeatures.length);
const anchorOptions = {
href: buildUrl(),
target: "_blank",
rel: "noopener"
};
return (
<div className="featurePanelComponentContainer">
<div className="urlContainer">
<a {...anchorOptions}>{buildUrl()}</a>
</div>
<Stack className="options" tokens={stackTokens}>
<Stack horizontal horizontalAlign="space-between" tokens={stackTokens}>
<DefaultButton onClick={onNotebookShortcut}>Notebooks on localhost</DefaultButton>
<DefaultButton onClick={onPortalLocalDEShortcut}>Portal points to local DE</DefaultButton>
<DefaultButton onClick={onReset}>Reset</DefaultButton>
</Stack>
<Stack horizontal horizontalAlign="start" tokens={stackTokens}>
<Dropdown
selectedKey={baseUrl.key}
options={baseUrlOptions}
onChange={onChangeBaseUrl}
label="Base Url"
styles={dropdownStyles}
/>
<Dropdown
label="Platform"
selectedKey={platform.key}
onChange={onChangePlatform}
options={platformOptions}
styles={dropdownStyles}
/>
</Stack>
<Stack horizontal>
<Stack className="checkboxRow" horizontalAlign="space-between">
{leftBooleanFeatures.map(f => (
<Checkbox
key={f.key}
label={f.label}
checked={f.reactState[0]}
onChange={f.onChange}
disabled={f.disabled && f.disabled()}
/>
))}
</Stack>
<Stack className="checkboxRow" horizontalAlign="space-between">
{rightBooleanFeatures.map(f => (
<Checkbox
key={f.key}
label={f.label}
checked={f.reactState[0]}
onChange={f.onChange}
disabled={f.disabled && f.disabled()}
/>
))}
</Stack>
</Stack>
<Stack horizontal tokens={stackTokens}>
<Stack horizontalAlign="space-between">
{leftStringFeatures.map(f => (
<TextField
key={f.key}
value={f.reactState[0]}
label={f.label}
onChange={f.onChange}
styles={textFieldStyles}
placeholder={f.placeholder}
disabled={f.disabled && f.disabled()}
/>
))}
</Stack>
<Stack horizontalAlign="space-between">
{rightStringFeatures.map(f => (
<TextField
key={f.key}
value={f.reactState[0]}
label={f.label}
onChange={f.onChange}
styles={textFieldStyles}
placeholder={f.placeholder}
disabled={f.disabled && f.disabled()}
/>
))}
</Stack>
</Stack>
</Stack>
</div>
);
};

View File

@@ -0,0 +1,18 @@
.featurePanelLauncherContainer {
.featurePanelLauncherModal {
overflow-y: hidden;
display: flex;
flex-direction: column;
}
.ms-Dialog-main {
overflow: hidden;
}
}
.activePatch {
position: absolute;
height: 20px;
width: 20px;
top: 20px;
left: -20px;
}

View File

@@ -0,0 +1,86 @@
import * as React from "react";
import { FeaturePanelComponent } from "./FeaturePanelComponent";
import { getTheme, mergeStyleSets, FontWeights, Modal, IconButton, IIconProps } from "office-ui-fabric-react";
import "./FeaturePanelLauncher.less";
// Modal wrapper
export const FeaturePanelLauncher: React.FunctionComponent = (): JSX.Element => {
const [isModalOpen, showModal] = React.useState<boolean>(false);
const onActivate = (event: React.MouseEvent<HTMLSpanElement>): void => {
if (!event.shiftKey || !event.ctrlKey) {
return;
}
event.stopPropagation();
showModal(true);
};
const theme = getTheme();
const contentStyles = mergeStyleSets({
container: {
display: "flex",
flexFlow: "column nowrap",
alignItems: "stretch"
},
header: [
// tslint:disable-next-line:deprecation
theme.fonts.xLargePlus,
{
flex: "1 1 auto",
borderTop: `4px solid ${theme.palette.themePrimary}`,
color: theme.palette.neutralPrimary,
display: "flex",
alignItems: "center",
fontWeight: FontWeights.semibold,
padding: "12px 12px 14px 24px"
}
],
body: {
flex: "4 4 auto",
overflowY: "hidden",
marginBottom: 40,
height: "100%",
display: "flex"
}
});
const iconButtonStyles = {
root: {
color: theme.palette.neutralPrimary,
marginLeft: "auto",
marginTop: "4px",
marginRight: "2px"
},
rootHovered: {
color: theme.palette.neutralDark
}
};
const cancelIcon: IIconProps = { iconName: "Cancel" };
const hideModal = (): void => showModal(false);
return (
<span className="activePatch" onDoubleClick={onActivate}>
<Modal
className="featurePanelLauncherContainer"
titleAriaId="Features"
isOpen={isModalOpen}
onDismiss={hideModal}
isBlocking={false}
scrollableContentClassName="featurePanelLauncherModal"
>
<div className={contentStyles.header}>
<span>Data Explorer Launcher</span>
<IconButton
styles={iconButtonStyles}
iconProps={cancelIcon}
ariaLabel="Close popup modal"
onClick={hideModal}
/>
</div>
<div className={contentStyles.body}>
<FeaturePanelComponent />
</div>
</Modal>
</span>
);
};

View File

@@ -0,0 +1,312 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Feature panel renders all flags 1`] = `
<div
className="featurePanelComponentContainer"
>
<div
className="urlContainer"
>
<a
href="https://localhost:1234/explorer.html?platform=Hosted"
rel="noopener"
target="_blank"
>
https://localhost:1234/explorer.html?platform=Hosted
</a>
</div>
<Stack
className="options"
tokens={
Object {
"childrenGap": 10,
}
}
>
<Stack
horizontal={true}
horizontalAlign="space-between"
tokens={
Object {
"childrenGap": 10,
}
}
>
<CustomizedDefaultButton
onClick={[Function]}
>
Notebooks on localhost
</CustomizedDefaultButton>
<CustomizedDefaultButton
onClick={[Function]}
>
Portal points to local DE
</CustomizedDefaultButton>
<CustomizedDefaultButton
onClick={[Function]}
>
Reset
</CustomizedDefaultButton>
</Stack>
<Stack
horizontal={true}
horizontalAlign="start"
tokens={
Object {
"childrenGap": 10,
}
}
>
<StyledWithResponsiveMode
label="Base Url"
onChange={[Function]}
options={
Array [
Object {
"key": "https://localhost:1234/explorer.html",
"text": "localhost:1234",
},
Object {
"key": "https://cosmos.azure.com/explorer.html",
"text": "cosmos.azure.com",
},
Object {
"key": "https://portal.azure.com",
"text": "portal",
},
]
}
selectedKey="https://localhost:1234/explorer.html"
styles={
Object {
"dropdown": Object {
"width": 200,
},
}
}
/>
<StyledWithResponsiveMode
label="Platform"
onChange={[Function]}
options={
Array [
Object {
"key": "Hosted",
"text": "Hosted",
},
Object {
"key": "Portal",
"text": "Portal",
},
Object {
"key": "Emulator",
"text": "Emulator",
},
Object {
"key": "",
"text": "None",
},
]
}
selectedKey="Hosted"
styles={
Object {
"dropdown": Object {
"width": 200,
},
}
}
/>
</Stack>
<Stack
horizontal={true}
>
<Stack
className="checkboxRow"
horizontalAlign="space-between"
>
<StyledCheckboxBase
checked={false}
key="feature.enablechangefeedpolicy"
label="Enable change feed policy"
onChange={[Function]}
/>
<StyledCheckboxBase
checked={false}
key="feature.enablerupm"
label="Enable RUPM"
onChange={[Function]}
/>
<StyledCheckboxBase
checked={false}
key="feature.dataexplorerexecutesproc"
label="Execute stored procedure"
onChange={[Function]}
/>
<StyledCheckboxBase
checked={false}
key="feature.hosteddataexplorerenabled"
label="Hosted Data Explorer (deprecated?)"
onChange={[Function]}
/>
<StyledCheckboxBase
checked={false}
key="feature.enablettl"
label="Enable TTL"
onChange={[Function]}
/>
<StyledCheckboxBase
checked={false}
key="feature.enablegallery"
label="Enable Notebook Gallery"
onChange={[Function]}
/>
<StyledCheckboxBase
checked={false}
key="feature.canexceedmaximumvalue"
label="Can exceed max value"
onChange={[Function]}
/>
</Stack>
<Stack
className="checkboxRow"
horizontalAlign="space-between"
>
<StyledCheckboxBase
checked={false}
key="feature.enablefixedcollectionwithsharedthroughput"
label="Enable fixed collection with shared throughput"
onChange={[Function]}
/>
<StyledCheckboxBase
checked={false}
key="feature.ttl90days"
label="TTL 90 days"
onChange={[Function]}
/>
<StyledCheckboxBase
checked={false}
key="feature.enablenotebooks"
label="Enable notebooks"
onChange={[Function]}
/>
<StyledCheckboxBase
checked={false}
disabled={true}
key="feature.customportal"
label="Force Production portal (portal only)"
onChange={[Function]}
/>
<StyledCheckboxBase
checked={false}
key="feature.enablespark"
label="Enable Synapse"
onChange={[Function]}
/>
<StyledCheckboxBase
checked={false}
key="feature.enableautopilotv2"
label="Enable Auto-pilot V2"
onChange={[Function]}
/>
</Stack>
</Stack>
<Stack
horizontal={true}
tokens={
Object {
"childrenGap": 10,
}
}
>
<Stack
horizontalAlign="space-between"
>
<StyledTextFieldBase
key="feature.notebookserverurl"
label="Notebook server URL"
onChange={[Function]}
placeholder="https://notebookserver"
styles={
Object {
"fieldGroup": Object {
"width": 300,
},
}
}
/>
<StyledTextFieldBase
key="feature.notebookservertoken"
label="Notebook server token"
onChange={[Function]}
placeholder=""
styles={
Object {
"fieldGroup": Object {
"width": 300,
},
}
}
/>
<StyledTextFieldBase
key="feature.notebookbasepath"
label="Notebook base path"
onChange={[Function]}
placeholder=""
styles={
Object {
"fieldGroup": Object {
"width": 300,
},
}
}
/>
</Stack>
<Stack
horizontalAlign="space-between"
>
<StyledTextFieldBase
key="key"
label="Auth key"
onChange={[Function]}
placeholder=""
styles={
Object {
"fieldGroup": Object {
"width": 300,
},
}
}
/>
<StyledTextFieldBase
disabled={true}
key="dataExplorerSource"
label="Data Explorer Source (portal only)"
onChange={[Function]}
placeholder="https://localhost:1234/explorer.html"
styles={
Object {
"fieldGroup": Object {
"width": 300,
},
}
}
/>
<StyledTextFieldBase
key="feature.livyendpoint"
label="Livy endpoint"
onChange={[Function]}
placeholder=""
styles={
Object {
"fieldGroup": Object {
"width": 300,
},
}
}
/>
</Stack>
</Stack>
</Stack>
</div>
`;

View File

@@ -1,80 +0,0 @@
/**
* Message handler to communicate with NotebookApp iframe
*/
import Q from "q";
import * as _ from "underscore";
import { HashMap } from "../../../Common/HashMap";
import { CachedDataPromise } from "../../../Common/MessageHandler";
import * as Constants from "../../../Common/Constants";
import {
MessageTypes,
FromNotebookMessage,
FromNotebookResponseMessage,
FromDataExplorerMessage
} from "../../../Terminal/NotebookAppContracts";
export class NotebookAppMessageHandler {
private requestMap: HashMap<CachedDataPromise<any>>;
constructor(private targetIFrameWindow: Window) {
this.requestMap = new HashMap();
}
public handleCachedDataMessage(message: FromNotebookMessage): void {
const messageContent = message && (message.message as FromNotebookResponseMessage);
if (
message == null ||
messageContent == null ||
messageContent.id == null ||
!this.requestMap.has(messageContent.id)
) {
return;
}
const cachedDataPromise = this.requestMap.get(messageContent.id);
if (messageContent.error != null) {
cachedDataPromise.deferred.reject(messageContent.error);
} else {
cachedDataPromise.deferred.resolve(messageContent.data);
}
this.runGarbageCollector();
}
public sendCachedDataMessage<TResponseDataModel>(
messageType: MessageTypes,
params?: any
): Q.Promise<TResponseDataModel> {
let cachedDataPromise: CachedDataPromise<TResponseDataModel> = {
deferred: Q.defer<TResponseDataModel>(),
startTime: new Date(),
id: _.uniqueId()
};
this.requestMap.set(cachedDataPromise.id, cachedDataPromise);
this.sendMessage({ type: messageType, params: params, id: cachedDataPromise.id });
//TODO: Use telemetry to measure optimal time to resolve/reject promises
return cachedDataPromise.deferred.promise.timeout(
Constants.ClientDefaults.requestTimeoutMs,
"Timed out while waiting for response from portal"
);
}
public sendMessage(data: FromDataExplorerMessage): void {
if (!this.targetIFrameWindow) {
console.error("targetIFrame not defined. This is not expected");
return;
}
this.targetIFrameWindow.postMessage(data, window.location.href || window.document.referrer);
}
protected runGarbageCollector() {
this.requestMap.keys().forEach((key: string) => {
const promise: Q.Promise<any> = this.requestMap.get(key).deferred.promise;
if (promise.isFulfilled() || promise.isRejected()) {
this.requestMap.delete(key);
}
});
}
}

View File

@@ -10,7 +10,6 @@ import {
GalleryViewerComponent,
GalleryViewerComponentProps
} from "./GalleryViewerComponent";
import * as DataModels from "../../../Contracts/DataModels";
describe("GalleryCardsComponent", () => {
it("renders", () => {
@@ -18,14 +17,8 @@ describe("GalleryCardsComponent", () => {
const props: GalleryCardsComponentProps = {
data: [],
userMetadata: undefined,
onNotebookMetadataChange: (officialSamplesIndex: number, notebookMetadata: DataModels.NotebookMetadata) =>
Promise.resolve(),
onClick: (
url: string,
notebookMetadata: DataModels.NotebookMetadata,
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
isLikedNotebook: boolean
) => Promise.resolve()
onNotebookMetadataChange: () => Promise.resolve(),
onClick: () => Promise.resolve()
};
const wrapper = shallow(<GalleryCardsComponent {...props} />);
@@ -39,12 +32,7 @@ describe("FullWidthTabs", () => {
officialSamplesContent: [],
likedNotebooksContent: [],
userMetadata: undefined,
onClick: (
url: string,
notebookMetadata: DataModels.NotebookMetadata,
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
isLikedNotebook: boolean
) => Promise.resolve()
onClick: () => Promise.resolve()
};
const wrapper = shallow(<FullWidthTabs {...props} />);

View File

@@ -39,7 +39,7 @@ export class GalleryCardsComponent extends React.Component<GalleryCardsComponent
public render(): JSX.Element {
return (
<Stack horizontal wrap tokens={this.sectionStackTokens}>
{this.props.data.map((githubInfo: DataModels.GitHubInfoJunoResponse, index: any) => {
{this.props.data.map((githubInfo: DataModels.GitHubInfoJunoResponse) => {
const name = githubInfo.name;
const url = githubInfo.downloadUrl;
const notebookMetadata = githubInfo.metadata || {
@@ -56,7 +56,7 @@ export class GalleryCardsComponent extends React.Component<GalleryCardsComponent
const officialSamplesIndex = githubInfo.officialSamplesIndex;
const isLikedNotebook = githubInfo.isLikedNotebook;
const updateTabsStatePerNotebook = this.props.onNotebookMetadataChange
? (notebookMetadata: DataModels.NotebookMetadata) =>
? (notebookMetadata: DataModels.NotebookMetadata): Promise<void> =>
this.props.onNotebookMetadataChange(officialSamplesIndex, notebookMetadata)
: undefined;
@@ -68,7 +68,9 @@ export class GalleryCardsComponent extends React.Component<GalleryCardsComponent
name={name}
url={url}
notebookMetadata={notebookMetadata}
onClick={() => this.props.onClick(url, notebookMetadata, updateTabsStatePerNotebook, isLikedNotebook)}
onClick={(): Promise<void> =>
this.props.onClick(url, notebookMetadata, updateTabsStatePerNotebook, isLikedNotebook)
}
/>
)
);
@@ -115,7 +117,7 @@ export class FullWidthTabs extends React.Component<FullWidthTabsProps, FullWidth
title: "Official Samples",
content: {
className: "",
render: () => (
render: (): JSX.Element => (
<GalleryCardsComponent
data={this.state.officialSamplesContent}
onClick={this.props.onClick}
@@ -124,13 +126,13 @@ export class FullWidthTabs extends React.Component<FullWidthTabsProps, FullWidth
/>
)
},
isVisible: () => true
isVisible: (): boolean => true
},
{
title: "Liked Notebooks",
content: {
className: "",
render: () => (
render: (): JSX.Element => (
<GalleryCardsComponent
data={this.state.likedNotebooksContent}
onClick={this.props.onClick}
@@ -139,12 +141,15 @@ export class FullWidthTabs extends React.Component<FullWidthTabsProps, FullWidth
/>
)
},
isVisible: () => true
isVisible: (): boolean => true
}
];
}
public updateTabsState = async (officialSamplesIndex: number, notebookMetadata: DataModels.NotebookMetadata) => {
public updateTabsState = async (
officialSamplesIndex: number,
notebookMetadata: DataModels.NotebookMetadata
): Promise<void> => {
let currentLikedNotebooksContent = [...this.state.likedNotebooksContent];
let currentUserMetadata = { ...this.state.userMetadata };
let currentLikedNotebooks = [...currentUserMetadata.likedNotebooks];
@@ -187,7 +192,7 @@ export class FullWidthTabs extends React.Component<FullWidthTabsProps, FullWidth
});
JunoUtils.updateNotebookMetadata(this.authorizationToken, notebookMetadata).then(
async returnedNotebookMetadata => {
async () => {
if (metadataLikesUpdates !== 0) {
JunoUtils.updateUserMetadata(this.authorizationToken, currentUserMetadata);
// TODO: update state here?
@@ -203,9 +208,9 @@ export class FullWidthTabs extends React.Component<FullWidthTabsProps, FullWidth
);
};
private onTabIndexChange = (activeTabIndex: number) => this.setState({ activeTabIndex });
private onTabIndexChange = (activeTabIndex: number): void => this.setState({ activeTabIndex });
public render() {
public render(): JSX.Element {
return (
<TabComponent.TabComponent
tabs={this.appTabs}
@@ -238,7 +243,7 @@ export class GalleryViewerContainerComponent extends React.Component<
};
}
componentDidMount() {
componentDidMount(): void {
const authToken = CosmosClient.authorizationToken();
JunoUtils.getOfficialSampleNotebooks(authToken).then(
(data1: DataModels.GitHubInfoJunoResponse[]) => {
@@ -341,7 +346,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
notebookMetadata: DataModels.NotebookMetadata,
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
isLikedNotebook: boolean
) => {
): Promise<void> => {
if (!this.props.container) {
SessionStorageUtility.setEntryString(
StorageKey.NotebookMetadata,

View File

@@ -1,7 +1,6 @@
import React from "react";
import { shallow } from "enzyme";
import { NotebookMetadataComponentProps, NotebookMetadataComponent } from "./NotebookMetadataComponent";
import { NotebookMetadata } from "../../../Contracts/DataModels";
describe("NotebookMetadataComponent", () => {
it("renders un-liked notebook", () => {
@@ -10,7 +9,7 @@ describe("NotebookMetadataComponent", () => {
container: undefined,
notebookMetadata: undefined,
notebookContent: {},
onNotebookMetadataChange: (newNotebookMetadata: NotebookMetadata) => Promise.resolve(),
onNotebookMetadataChange: () => Promise.resolve(),
isLikedNotebook: false
};
@@ -24,7 +23,7 @@ describe("NotebookMetadataComponent", () => {
container: undefined,
notebookMetadata: undefined,
notebookContent: {},
onNotebookMetadataChange: (newNotebookMetadata: NotebookMetadata) => Promise.resolve(),
onNotebookMetadataChange: () => Promise.resolve(),
isLikedNotebook: true
};

View File

@@ -1,6 +1,7 @@
@import "../../../../less/Common/Constants";
.tabComponentContainer {
overflow: hidden;
height: 100%;
.flex-display();
.flex-direction();

View File

@@ -21,7 +21,6 @@ import EnvironmentUtility from "../Common/EnvironmentUtility";
import GraphStylingPane from "./Panes/GraphStylingPane";
import hasher from "hasher";
import NewVertexPane from "./Panes/NewVertexPane";
import NotebookTab from "./Tabs/NotebookTab";
import NotebookV2Tab from "./Tabs/NotebookV2Tab";
import Q from "q";
import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
@@ -86,6 +85,7 @@ import { StringInputPane } from "./Panes/StringInputPane";
import { TableColumnOptionsPane } from "./Panes/Tables/TableColumnOptionsPane";
import { UploadFilePane } from "./Panes/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane";
import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter";
BindingHandlersRegisterer.registerBindingHandlers();
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
@@ -154,7 +154,6 @@ export default class Explorer implements ViewModels.Explorer {
public selectedNode: ko.Observable<ViewModels.TreeNode>;
public isRefreshingExplorer: ko.Observable<boolean>;
private resourceTree: ResourceTreeAdapter;
private enableLegacyResourceTree: ko.Observable<boolean>;
// Resource Token
public resourceTokenDatabaseId: ko.Observable<string>;
@@ -188,6 +187,7 @@ export default class Explorer implements ViewModels.Explorer {
public executeSprocParamsPane: ViewModels.ExecuteSprocParamsPane;
public renewAdHocAccessPane: ViewModels.RenewAdHocAccessPane;
public uploadItemsPane: ViewModels.UploadItemsPane;
public uploadItemsPaneAdapter: UploadItemsPaneAdapter;
public loadQueryPane: ViewModels.LoadQueryPane;
public saveQueryPane: ViewModels.ContextualPane;
public browseQueriesPane: ViewModels.BrowseQueriesPane;
@@ -203,8 +203,8 @@ export default class Explorer implements ViewModels.Explorer {
// features
public isGalleryEnabled: ko.Computed<boolean>;
public isGitHubPaneEnabled: ko.Observable<boolean>;
public isGraphsEnabled: ko.Computed<boolean>;
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
public isRightPanelV2Enabled: ko.Computed<boolean>;
public canExceedMaximumValue: ko.Computed<boolean>;
public hasAutoPilotV2FeatureFlag: ko.Computed<boolean>;
@@ -303,7 +303,7 @@ export default class Explorer implements ViewModels.Explorer {
this.openedTabs &&
this.openedTabs().forEach(tab => {
if (tab.tabKind === ViewModels.CollectionTabKind.Notebook) {
(tab as NotebookTab).reconfigureServiceEndpoints();
throw new Error("NotebookTab is deprecated. Use NotebookV2Tab");
} else if (tab.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
(tab as NotebookV2Tab).reconfigureServiceEndpoints();
}
@@ -381,7 +381,6 @@ export default class Explorer implements ViewModels.Explorer {
this.armEndpoint = ko.observable<string>(undefined);
this.queriesClient = new QueriesClient(this);
this.isTryCosmosDBSubscription = ko.observable<boolean>(false);
this.enableLegacyResourceTree = ko.observable<boolean>(false);
this.resourceTokenDatabaseId = ko.observable<string>();
this.resourceTokenCollectionId = ko.observable<string>();
@@ -411,9 +410,6 @@ export default class Explorer implements ViewModels.Explorer {
this.shouldShowContextSwitchPrompt = ko.observable<boolean>(false);
this.isGalleryEnabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableGallery));
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
this.isGraphsEnabled = ko.computed<boolean>(() => {
return this.isFeatureEnabled(Constants.Features.graphs);
});
this.canExceedMaximumValue = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
@@ -551,6 +547,9 @@ export default class Explorer implements ViewModels.Explorer {
!this.isRunningOnNationalCloud() &&
!this.isPreferredApiGraph()
);
this.isRightPanelV2Enabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableRightPanelV2)
);
this.defaultExperience.subscribe((defaultExperience: string) => {
if (
defaultExperience &&
@@ -707,6 +706,8 @@ export default class Explorer implements ViewModels.Explorer {
container: this
});
this.uploadItemsPaneAdapter = new UploadItemsPaneAdapter(this);
this.loadQueryPane = new LoadQueryPane({
documentClientUtility: this.documentClientUtility,
id: "loadquerypane",
@@ -1100,8 +1101,6 @@ export default class Explorer implements ViewModels.Explorer {
this.sparkClusterConnectionInfo.valueHasMutated();
}
this.enableLegacyResourceTree(this.isFeatureEnabled(Constants.Features.enableLegacyResourceTree));
featureSubcription.dispose();
});
@@ -2768,7 +2767,7 @@ export default class Explorer implements ViewModels.Explorer {
const openedNotebookTabs = this.openedTabs().filter(
(tab: ViewModels.Tab) =>
tab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
(tab as NotebookTab).notebookPath() === notebookFile.path
(tab as NotebookV2Tab).notebookPath() === notebookFile.path
);
if (openedNotebookTabs.length > 0) {
this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again.");
@@ -2792,12 +2791,12 @@ export default class Explorer implements ViewModels.Explorer {
.filter(
(tab: ViewModels.Tab) =>
tab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
FileSystemUtil.isPathEqual((tab as NotebookTab).notebookPath(), originalPath)
FileSystemUtil.isPathEqual((tab as NotebookV2Tab).notebookPath(), originalPath)
)
.forEach(tab => {
tab.tabTitle(newNotebookFile.name);
tab.tabPath(newNotebookFile.path);
(tab as NotebookTab).notebookPath(newNotebookFile.path);
(tab as NotebookV2Tab).notebookPath(newNotebookFile.path);
});
return newNotebookFile;
@@ -3035,7 +3034,7 @@ export default class Explorer implements ViewModels.Explorer {
// Don't delete if tab is open to avoid accidental deletion
const openedNotebookTabs = this.openedTabs().filter(
(tab: ViewModels.Tab) =>
tab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && (tab as NotebookTab).notebookPath() === item.path
tab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && (tab as NotebookV2Tab).notebookPath() === item.path
);
if (openedNotebookTabs.length > 0) {
this.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again.");

View File

@@ -1,43 +0,0 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import { CommandButtonOptions } from "./../Controls/CommandButton/CommandButton";
export default class ContextMenu implements ViewModels.ContextMenu {
public container: ViewModels.Explorer;
public visible: ko.Observable<boolean>;
public elementId: string;
public options: ko.ObservableArray<CommandButtonOptions>;
public tabIndex: ko.Observable<number>;
constructor(container: ViewModels.Explorer, rid: string) {
this.container = container;
this.visible = ko.observable<boolean>(false);
this.elementId = `contextMenu${rid}`;
this.options = ko.observableArray<CommandButtonOptions>([]);
this.tabIndex = ko.observable<number>(0);
}
public show(source: ViewModels.TreeNode, event: MouseEvent) {
if (source && source.contextMenu && source.contextMenu.visible && source.contextMenu.visible()) {
return;
}
this.container.selectedNode(source);
const elementId = source.contextMenu.elementId;
const htmlElement = document.getElementById(elementId);
htmlElement.style.left = `${event.clientX}px`;
htmlElement.style.top = `${event.clientY}px`;
!!source.contextMenu && source.contextMenu.visible(true);
source.contextMenu.tabIndex(0);
htmlElement.focus();
}
public hide(source: ViewModels.TreeNode, event: MouseEvent) {
if (!source || !source.contextMenu || !source.contextMenu.visible || !source.contextMenu.visible()) {
return;
}
source.contextMenu.tabIndex(-1);
source.contextMenu.visible(false);
}
}

View File

@@ -19,6 +19,7 @@ import { TableColumnOptionsPane } from "../../src/Explorer/Panes/Tables/TableCol
import { TextFieldProps } from "./Controls/DialogReactComponent/DialogComponent";
import { UploadDetails } from "../workers/upload/definitions";
import { UploadFilePane } from "./Panes/UploadFilePane";
import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter";
import { Versions } from "../../src/Contracts/ExplorerContracts";
import { CollectionCreationDefaults } from "../Shared/Constants";
@@ -86,6 +87,7 @@ export class ExplorerStub implements ViewModels.Explorer {
public settingsPane: ViewModels.SettingsPane;
public executeSprocParamsPane: ViewModels.ExecuteSprocParamsPane;
public uploadItemsPane: ViewModels.UploadItemsPane;
public uploadItemsPaneAdapter: UploadItemsPaneAdapter;
public loadQueryPane: ViewModels.LoadQueryPane;
public saveQueryPane: ViewModels.ContextualPane;
public browseQueriesPane: ViewModels.BrowseQueriesPane;
@@ -96,7 +98,7 @@ export class ExplorerStub implements ViewModels.Explorer {
public manageSparkClusterPane: ViewModels.ContextualPane;
public isGalleryEnabled: ko.Computed<boolean>;
public isGitHubPaneEnabled: ko.Observable<boolean>;
public isGraphsEnabled: ko.Computed<boolean>;
public isRightPanelV2Enabled: ko.Computed<boolean>;
public canExceedMaximumValue: ko.Computed<boolean>;
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
public parentFrameDataExplorerVersion: ko.Observable<string> = ko.observable<string>(Versions.DataExplorer);
@@ -447,7 +449,6 @@ export class DatabaseStub implements ViewModels.Database {
public collections: ko.ObservableArray<ViewModels.Collection>;
public isDatabaseExpanded: ko.Observable<boolean>;
public isDatabaseShared: ko.Computed<boolean>;
public contextMenu: ViewModels.ContextMenu;
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
public offer: ko.Observable<DataModels.Offer>;
@@ -459,7 +460,6 @@ export class DatabaseStub implements ViewModels.Database {
this.id = options.id;
this.collections = options.collections;
this.isDatabaseExpanded = options.isDatabaseExpanded;
this.contextMenu = options.contextMenu;
this.offer = options.offer;
this.selectedSubnodeKind = options.selectedSubnodeKind;
}
@@ -562,8 +562,6 @@ export class CollectionStub implements ViewModels.Collection {
public storedProceduresFocused: ko.Observable<boolean>;
public userDefinedFunctionsFocused: ko.Observable<boolean>;
public triggersFocused: ko.Observable<boolean>;
public contextMenu: ViewModels.ContextMenu;
public documentsContextMenu: ViewModels.ContextMenu;
public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
public changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
public geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
@@ -608,69 +606,8 @@ export class CollectionStub implements ViewModels.Collection {
this.storedProceduresFocused = options.storedProceduresFocused;
this.userDefinedFunctionsFocused = options.userDefinedFunctionsFocused;
this.triggersFocused = options.triggersFocused;
this.contextMenu = options.contextMenu;
this.documentsContextMenu = options.documentsContextMenu;
}
public onKeyPress = (source: any, event: KeyboardEvent): boolean => {
throw new Error("Not implemented");
};
public onKeyDown = (source: any, event: KeyboardEvent): boolean => {
throw new Error("Not implemented");
};
public onMenuKeyDown = (source: any, event: KeyboardEvent): boolean => {
throw new Error("Not implemented");
};
public onDocumentDBDocumentsKeyDown = (source: any, event: KeyboardEvent): boolean => {
throw new Error("Not implemented");
};
public onDocumentDBDocumentsKeyPress = (source: any, event: KeyboardEvent): boolean => {
throw new Error("Not implemented");
};
public onMongoDBDocumentsKeyDown = (source: any, event: KeyboardEvent): boolean => {
throw new Error("Not implemented");
};
public onMongoDBDocumentsKeyPress = (source: any, event: KeyboardEvent): boolean => {
throw new Error("Not implemented");
};
public onSettingsKeyDown = (source: any, event: KeyboardEvent): boolean => {
throw new Error("Not implemented");
};
public onSettingsKeyPress = (source: any, event: KeyboardEvent): boolean => {
throw new Error("Not implemented");
};
public onStoredProceduresKeyDown = (source: any, event: KeyboardEvent): boolean => {
throw new Error("Not implemented");
};
public onStoredProceduresKeyPress = (source: any, event: KeyboardEvent): boolean => {
throw new Error("Not implemented");
};
public onUserDefinedFunctionsKeyDown = (source: any, event: KeyboardEvent): boolean => {
throw new Error("Not implemented");
};
public onUserDefinedFunctionsKeyPress = (source: any, event: KeyboardEvent): boolean => {
throw new Error("Not implemented");
};
public onTriggersKeyDown = (source: any, event: KeyboardEvent): boolean => {
throw new Error("Not implemented");
};
public onTriggersKeyPress = (source: any, event: KeyboardEvent): boolean => {
throw new Error("Not implemented");
};
public expandCollapseCollection() {
throw new Error("Not implemented");
}

View File

@@ -5,8 +5,6 @@ import Explorer from "../Explorer";
import ko from "knockout";
import { AutopilotTier } from "../../Contracts/DataModels";
jest.mock("../Tabs/NotebookTab");
describe("Add Collection Pane", () => {
describe("isValid()", () => {
let explorer: ViewModels.Explorer;

View File

@@ -2,8 +2,6 @@ import * as ViewModels from "../../Contracts/ViewModels";
import Explorer from "../Explorer";
import AddDatabasePane from "./AddDatabasePane";
jest.mock("../Tabs/NotebookTab");
describe("Add Database Pane", () => {
describe("getSharedThroughputDefault()", () => {
let explorer: ViewModels.Explorer;

View File

@@ -0,0 +1,146 @@
import * as React from "react";
import * as ViewModels from "../../Contracts/ViewModels";
import { IconButton, PrimaryButton } from "office-ui-fabric-react/lib/Button";
import { KeyCodes } from "../../Common/Constants";
import { Subscription } from "knockout";
import ErrorRedIcon from "../../../images/error_red.svg";
import LoadingIndicatorIcon from "../../../images/LoadingIndicator_3Squares.gif";
export interface GenericRightPaneProps {
container: ViewModels.Explorer;
content: JSX.Element;
formError: string;
formErrorDetail: string;
id: string;
isExecuting: boolean;
onClose: () => void;
onSubmit: () => void;
submitButtonText: string;
title: string;
}
export interface GenericRightPaneState {
panelHeight: number;
}
export class GenericRightPaneComponent extends React.Component<GenericRightPaneProps, GenericRightPaneState> {
private notificationConsoleSubscription: Subscription;
constructor(props: GenericRightPaneProps) {
super(props);
this.state = {
panelHeight: this.getPanelHeight()
};
}
public componentDidMount(): void {
this.notificationConsoleSubscription = this.props.container.isNotificationConsoleExpanded.subscribe(() => {
this.setState({ panelHeight: this.getPanelHeight() });
});
this.props.container.isNotificationConsoleExpanded.extend({ rateLimit: 10 });
}
public componentWillUnmount(): void {
this.notificationConsoleSubscription && this.notificationConsoleSubscription.dispose();
}
public render(): JSX.Element {
return (
<div tabIndex={-1} onKeyDown={this.onKeyDown}>
<div className="contextual-pane-out" onClick={this.props.onClose}></div>
<div
className="contextual-pane"
id={this.props.id}
style={{ height: this.state.panelHeight }}
onKeyDown={this.onKeyDown}
>
<div className="panelContentWrapper">
{this.createPanelHeader()}
{this.createErrorSection()}
{this.props.content}
{this.createPanelFooter()}
</div>
{this.createLoadingScreen()}
</div>
</div>
);
}
private createPanelHeader = (): JSX.Element => {
return (
<div className="firstdivbg headerline">
<span id="databaseTitle">{this.props.title}</span>
<IconButton
ariaLabel="Close pane"
title="Close pane"
onClick={this.props.onClose}
tabIndex={0}
className="closePaneBtn"
iconProps={{ iconName: "Cancel" }}
/>
</div>
);
};
private createErrorSection = (): JSX.Element => {
return (
<div className="warningErrorContainer" aria-live="assertive" hidden={!this.props.formError}>
<div className="warningErrorContent">
<span>
<img className="paneErrorIcon" src={ErrorRedIcon} alt="Error" />
</span>
<span className="warningErrorDetailsLinkContainer">
<span className="formErrors" title={this.props.formError}>
{this.props.formError}
</span>
<a className="errorLink" role="link" hidden={!this.props.formErrorDetail} onClick={this.showErrorDetail}>
More details
</a>
</span>
</div>
</div>
);
};
private createPanelFooter = (): JSX.Element => {
return (
<div className="paneFooter">
<div className="leftpanel-okbut">
<PrimaryButton
ariaLabel="Submit"
title="Submit"
onClick={this.props.onSubmit}
tabIndex={0}
className="genericPaneSubmitBtn"
text={this.props.submitButtonText}
/>
</div>
</div>
);
};
private createLoadingScreen = (): JSX.Element => {
return (
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" hidden={!this.props.isExecuting}>
<img className="dataExplorerLoader" src={LoadingIndicatorIcon} />
</div>
);
};
private onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
if (event.keyCode === KeyCodes.Escape) {
this.props.onClose();
event.stopPropagation();
}
};
private showErrorDetail = (): void => {
this.props.container.expandConsole();
};
private getPanelHeight = (): number => {
const notificationConsoleElement: HTMLElement = document.getElementById("explorerNotificationConsole");
return window.innerHeight - $(notificationConsoleElement).height();
};
}

View File

@@ -2,8 +2,6 @@ import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import Explorer from "../Explorer";
jest.mock("../Tabs/NotebookTab");
describe("Settings Pane", () => {
describe("shouldShowQueryPageOptions()", () => {
let explorer: ViewModels.Explorer;

View File

@@ -0,0 +1,238 @@
import * as Constants from "../../Common/Constants";
import * as ErrorParserUtility from "../../Common/ErrorParserUtility";
import * as ko from "knockout";
import * as React from "react";
import * as ViewModels from "../../Contracts/ViewModels";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { IconButton } from "office-ui-fabric-react/lib/Button";
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import { UploadDetailsRecord, UploadDetails } from "../../workers/upload/definitions";
import InfoBubbleIcon from "../../../images/info-bubble.svg";
const UPLOAD_FILE_SIZE_LIMIT = 2097152;
export class UploadItemsPaneAdapter implements ReactAdapter {
public parameters: ko.Observable<number>;
private isOpened: boolean;
private isExecuting: boolean;
private formError: string;
private formErrorDetail: string;
private selectedFiles: FileList;
private selectedFilesTitle: string;
private uploadFileData: UploadDetailsRecord[];
public constructor(private container: ViewModels.Explorer) {
this.parameters = ko.observable(Date.now());
this.reset();
this.triggerRender();
}
public renderComponent(): JSX.Element {
if (!this.isOpened) {
return undefined;
}
const props: GenericRightPaneProps = {
container: this.container,
content: this.createContent(),
formError: this.formError,
formErrorDetail: this.formErrorDetail,
id: "uploaditemspane",
isExecuting: this.isExecuting,
title: "Upload Items",
submitButtonText: "Upload",
onClose: () => this.close(),
onSubmit: () => this.submit()
};
return <GenericRightPaneComponent {...props} />;
}
public triggerRender(): void {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}
public open(): void {
this.isOpened = true;
this.triggerRender();
}
public close(): void {
this.reset();
this.triggerRender();
}
public submit(): void {
this.formError = "";
if (!this.selectedFiles || this.selectedFiles.length === 0) {
this.formError = "No files specified";
this.formErrorDetail = "No files were specified. Please input at least one file.";
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
"Could not upload items -- No files were specified. Please input at least one file."
);
this.triggerRender();
return;
} else if (this._totalFileSizeForFileList() > UPLOAD_FILE_SIZE_LIMIT) {
this.formError = "Upload file size limit exceeded";
this.formErrorDetail = "Total file upload size exceeds the 2 MB file size limit.";
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
"Could not upload items -- Total file upload size exceeds the 2 MB file size limit."
);
this.triggerRender();
return;
}
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
this.isExecuting = true;
this.triggerRender();
selectedCollection &&
selectedCollection
.uploadFiles(this.selectedFiles)
.then(
(uploadDetails: UploadDetails) => {
this.uploadFileData = uploadDetails.data;
this.selectedFiles = undefined;
this.selectedFilesTitle = "";
},
error => {
const message = ErrorParserUtility.parse(error);
this.formError = message[0].message;
this.formErrorDetail = message[0].message;
}
)
.finally(() => {
this.triggerRender();
this.isExecuting = false;
});
}
private createContent = (): JSX.Element => {
return <div className="panelContent">{this.createMainContentSection()}</div>;
};
private createMainContentSection = (): JSX.Element => {
return (
<div className="paneMainContent">
<div className="renewUploadItemsHeader">
<span> Select JSON Files </span>
<span className="infoTooltip" role="tooltip" tabIndex={0}>
<img className="infoImg" src={InfoBubbleIcon} alt="More information" />
<span className="tooltiptext infoTooltipWidth">
Select one or more JSON files to upload. Each file can contain a single JSON document or an array of JSON
documents. The combined size of all files in an individual upload operation must be less than 2 MB. You
can perform multiple upload operations for larger data sets.
</span>
</span>
</div>
<input
className="importFilesTitle"
type="text"
disabled
value={this.selectedFilesTitle}
aria-label="Select JSON Files"
/>
<input
type="file"
id="importDocsInput"
title="Upload Icon"
multiple
accept="application/json"
role="button"
tabIndex={0}
style={{ display: "none" }}
onChange={this.updateSelectedFiles}
/>
<IconButton
iconProps={{ iconName: "FolderHorizontal" }}
className="fileImportButton"
alt="Select JSON files to upload"
title="Select JSON files to upload"
onClick={this.onImportButtonClick}
onKeyPress={this.onImportButtonKeyPress}
/>
<div className="fileUploadSummaryContainer" hidden={this.uploadFileData.length === 0}>
<b>File upload status</b>
<table className="fileUploadSummary">
<thead>
<tr className="fileUploadSummaryHeader fileUploadSummaryTuple">
<th>FILE NAME</th>
<th>STATUS</th>
</tr>
</thead>
<tbody>
{this.uploadFileData.map(
(data: UploadDetailsRecord): JSX.Element => {
return (
<tr className="fileUploadSummaryTuple" key={data.fileName}>
<td>{data.fileName}</td>
<td>{this.fileUploadSummaryText(data.numSucceeded, data.numFailed)}</td>
</tr>
);
}
)}
</tbody>
</table>
</div>
</div>
);
};
private updateSelectedFiles = (event: React.ChangeEvent<HTMLInputElement>): void => {
this.selectedFiles = event.target.files;
this._updateSelectedFilesTitle();
this.triggerRender();
};
private _updateSelectedFilesTitle = (): void => {
this.selectedFilesTitle = "";
if (!this.selectedFiles || this.selectedFiles.length === 0) {
return;
}
for (let i = 0; i < this.selectedFiles.length; i++) {
this.selectedFilesTitle += `"${this.selectedFiles.item(i).name}"`;
}
};
private _totalFileSizeForFileList(): number {
let totalFileSize = 0;
if (!this.selectedFiles) {
return totalFileSize;
}
for (let i = 0; i < this.selectedFiles.length; i++) {
totalFileSize += this.selectedFiles.item(i).size;
}
return totalFileSize;
}
private fileUploadSummaryText = (numSucceeded: number, numFailed: number): string => {
return `${numSucceeded} items created, ${numFailed} errors`;
};
private onImportButtonClick = (): void => {
document.getElementById("importDocsInput").click();
};
private onImportButtonKeyPress = (event: React.KeyboardEvent<HTMLButtonElement>): void => {
if (event.charCode === Constants.KeyCodes.Enter || event.charCode === Constants.KeyCodes.Space) {
this.onImportButtonClick();
event.stopPropagation();
}
};
private reset = (): void => {
this.isOpened = false;
this.isExecuting = false;
this.formError = "";
this.formErrorDetail = "";
this.selectedFiles = undefined;
this.selectedFilesTitle = "";
this.uploadFileData = [];
};
}

View File

@@ -19,6 +19,7 @@
}
>.title {
position: relative; // To attach FeaturePanelLauncher as absolute
color: @BaseHigh;
font-size: 48px;
padding-left: 0px;

View File

@@ -5,6 +5,7 @@
import * as React from "react";
import * as Constants from "../../Common/Constants";
import { Link } from "office-ui-fabric-react/lib/Link";
import { FeaturePanelLauncher } from "../Controls/FeaturePanel/FeaturePanelLauncher";
export interface SplashScreenItem {
iconSrc: string;
@@ -29,7 +30,10 @@ export class SplashScreenComponent extends React.Component<SplashScreenComponent
return (
<div className="splashScreenContainer">
<div className="splashScreen">
<div className="title">Welcome to Cosmos DB</div>
<div className="title">
Welcome to Cosmos DB
<FeaturePanelLauncher />
</div>
<div className="subtitle">Globally distributed, multi-model database service for any scale</div>
<div className="mainButtonsContainer">
{this.props.mainItems.map((item: SplashScreenItem) => (

View File

@@ -6,8 +6,6 @@ import { DataAccessUtility } from "../../Platform/Portal/DataAccessUtility";
import Explorer from "../Explorer";
import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase";
jest.mock("./NotebookTab");
describe("Documents tab", () => {
describe("buildQuery", () => {
it("should generate the right select query for SQL API", () => {

View File

@@ -965,7 +965,10 @@ export default class DocumentsTab extends TabsBase implements ViewModels.Documen
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const focusElement = document.getElementById("itemImportLink");
selectedCollection && container.uploadItemsPane.open();
const uploadItemsPane = container.isRightPanelV2Enabled()
? container.uploadItemsPaneAdapter
: container.uploadItemsPane;
selectedCollection && uploadItemsPane.open();
focusElement && focusElement.focus();
},
commandButtonLabel: label,

View File

@@ -1,7 +0,0 @@
<div style="width: 100%; height: 100%; margin-left: 3px;" data-bind="attr: { id: tabId }">
<!-- This runs the NotebookApp hosted by DataExplorer -->
<iframe
style="width:100%; height: 100%; border:none"
data-bind="setTemplateReady: true, attr: { id: notebookContainerId, src: notebookAppIFrameSrc }"
></iframe>
</div>

View File

@@ -1,539 +0,0 @@
import * as Q from "q";
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import * as DataModels from "../../Contracts/DataModels";
import TabsBase from "./TabsBase";
import NewCellIcon from "../../../images/notebook/Notebook-insert-cell.svg";
import CutIcon from "../../../images/notebook/Notebook-cut.svg";
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
import PasteIcon from "../../../images/notebook/Notebook-paste.svg";
import RunIcon from "../../../images/notebook/Notebook-run.svg";
import RunAllIcon from "../../../images/notebook/Notebook-run-all.svg";
import RestartIcon from "../../../images/notebook/Notebook-restart.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg";
import UndoIcon from "../../../images/notebook/Notebook-undo.svg";
import RedoIcon from "../../../images/notebook/Notebook-redo.svg";
import { CommandBarComponentButtonFactory } from "../Menus/CommandBar/CommandBarComponentButtonFactory";
import { NotebookAppMessageHandler } from "../Controls/Notebook/NotebookAppMessageHandler";
import * as NotebookAppContracts from "../../Terminal/NotebookAppContracts";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
import { NotebookTerminalComponent } from "../Controls/Notebook/NotebookTerminalComponent";
import { NotebookConfigurationUtils } from "../../Utils/NotebookConfigurationUtils";
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
interface Kernel {
name: string;
displayName: string;
}
export default class NotebookTab extends TabsBase implements ViewModels.Tab {
private notebookAppIFrameSrc: ko.Computed<string>;
private container: ViewModels.Explorer;
public notebookPath: ko.Observable<string>;
private messageListener: (ev: MessageEvent) => any;
private activeCellTypeStr: string;
private notebookContainerId: string;
private currentKernelName: string;
private availableKernels: Kernel[];
private messageHandler: NotebookAppMessageHandler;
private notificationProgressId: string;
private isSwitchingKernels: ko.Observable<boolean>;
constructor(options: ViewModels.NotebookTabOptions) {
super(options);
this.availableKernels = [];
this.isSwitchingKernels = ko.observable<boolean>(false);
this.messageListener = async (ev: MessageEvent) => {
if (isInvalidParentFrameOrigin(ev)) {
return;
}
const msg: NotebookAppContracts.FromNotebookMessage = ev.data;
if (msg.actionType === NotebookAppContracts.ActionTypes.Response) {
this.messageHandler.handleCachedDataMessage(msg);
} else if (msg.actionType === NotebookAppContracts.ActionTypes.Update) {
const updateMessage = msg.message as NotebookAppContracts.FromNotebookUpdateMessage;
switch (updateMessage.type) {
case NotebookAppContracts.NotebookUpdateTypes.ActiveCellType:
this.activeCellTypeStr = updateMessage.arg;
this.updateNavbarWithTabsButtons();
break;
case NotebookAppContracts.NotebookUpdateTypes.KernelChange:
this.isSwitchingKernels(false);
this.currentKernelName = updateMessage.arg;
this.messageHandler
.sendCachedDataMessage<NotebookAppContracts.KernelSpecs>(NotebookAppContracts.MessageTypes.KernelList)
.then(specs => {
this.availableKernels = Object.keys(specs.kernelSpecs)
.map((name: string) => ({ name: name, displayName: specs.kernelSpecs[name].displayName }))
.sort((a: NotebookAppContracts.KernelOption, b: NotebookAppContracts.KernelOption) => {
// Put default at the top, otherwise lexicographically compare
if (a.name === specs.defaultName) {
return -1;
} else if (b.name === specs.defaultName) {
return 1;
} else {
return a.displayName.localeCompare(b.displayName);
}
});
this.updateNavbarWithTabsButtons();
});
this.updateNavbarWithTabsButtons();
await this.configureServiceEndpoints(this.currentKernelName);
break;
case NotebookAppContracts.NotebookUpdateTypes.ClickEvent:
this.simulateClick();
break;
case NotebookAppContracts.NotebookUpdateTypes.SessionStatusChange: {
this.handleSessionStateChange(updateMessage.arg as NotebookAppContracts.KernelStatusStates);
break;
}
default:
console.error("Unknown command", updateMessage);
break;
}
}
};
super.onTemplateReady((isTemplateReady: boolean) => {
if (isTemplateReady) {
window.addEventListener("message", this.messageListener, false);
const iFrame: HTMLIFrameElement = document.getElementById(this.notebookContainerId) as HTMLIFrameElement;
this.messageHandler = new NotebookAppMessageHandler(iFrame.contentWindow);
this.sendMessageToNotebook(NotebookAppContracts.MessageTypes.Status);
}
});
this.container = options.container;
this.notebookAppIFrameSrc = ko.computed<string>(() =>
NotebookTerminalComponent.createNotebookAppSrc(
this.container.notebookServerInfo(),
new Map<string, string>([["notebookpath", options.notebookContentItem.path]])
)
);
this.notebookPath = ko.observable(options.notebookContentItem.path);
this.notebookContainerId = `notebookContainer-${this.tabId}`;
this.container.notebookServerInfo.subscribe((newValue: DataModels.NotebookWorkspaceConnectionInfo) => {
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "New notebook server info received.");
});
this.container &&
this.container.arcadiaToken.subscribe(async () => {
const currentKernel = this.currentKernelName;
if (!currentKernel) {
return;
}
await this.configureServiceEndpoints(currentKernel);
});
}
public onCloseTabButtonClick(): Q.Promise<any> {
const cleanup = () => {
this.sendMessageToNotebook(NotebookAppContracts.MessageTypes.Shutdown);
window.removeEventListener("message", this.messageListener);
this.isActive(false);
super.onCloseTabButtonClick();
};
return this.sendMessageToNotebook(NotebookAppContracts.MessageTypes.IsDirty).then((isDirty: boolean) => {
if (isDirty) {
this.container.showOkCancelModalDialog(
"Close without saving?",
`File has unsaved changes, close without saving?`,
"Close",
cleanup,
"Cancel",
undefined
);
return Q.resolve(null);
} else {
cleanup();
return Q.resolve(null);
}
});
}
public onActivate(): Q.Promise<any> {
if (this.messageHandler) {
this.sendMessageToNotebook(NotebookAppContracts.MessageTypes.Status);
}
return super.onActivate();
}
public async reconfigureServiceEndpoints() {
return await this.configureServiceEndpoints(this.currentKernelName);
}
private handleSessionStateChange(state: NotebookAppContracts.KernelStatusStates) {
switch (state) {
case "reconnecting":
this.clearProgressNotification();
this.notificationProgressId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
"Connection with Notebook Server lost. Reconnecting..."
);
break;
case "dead":
// This happens when the jupyter server detects that the kernel to which the cell was connected is no longer alive.
// It can be caused by the jupyter server going down and back up again and informing the client that the kernel to which
// it was previously connected to doesn't exist anymore. Send a restart kernel command.
if (!this.isSwitchingKernels()) {
this.notificationProgressId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
"Connection with Notebook Server dead. Trying to reconnect..."
);
this.sendMessageToNotebook(NotebookAppContracts.MessageTypes.RestartKernel);
}
break;
case "connected":
this.clearProgressNotification();
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
"Connection with Notebook Server established."
);
break;
default:
this.clearProgressNotification();
break;
}
}
private clearProgressNotification() {
if (this.notificationProgressId) {
NotificationConsoleUtils.clearInProgressMessageWithId(this.notificationProgressId);
this.notificationProgressId = undefined;
}
}
private static isUntitledNotebook(notebookFile: NotebookContentItem): boolean {
return notebookFile.name.indexOf("Untitled") === 0;
}
protected getContainer(): ViewModels.Explorer {
return this.container;
}
protected getTabsButtons(): ViewModels.NavbarButtonConfig[] {
const saveLabel = "Save";
const workspaceLabel = "Workspace";
const kernelLabel = "Kernel";
const runLabel = "Run";
const runActiveCellLabel = "Run Active Cell";
const runAllLabel = "Run All";
const restartKernelLabel = "Restart Kernel";
const clearLabel = "Clear outputs";
const newCellLabel = "New Cell";
const cellTypeLabel = "Cell Type";
const codeLabel = "Code";
const markdownLabel = "Markdown";
const rawLabel = "Raw";
const copyLabel = "Copy";
const cutLabel = "Cut";
const pasteLabel = "Paste";
const undoLabel = "Undo";
const redoLabel = "Redo";
const cellCodeType = "code";
const cellMarkdownType = "markdown";
const cellRawType = "raw";
let buttons: ViewModels.NavbarButtonConfig[] = [
{
iconSrc: SaveIcon,
iconAlt: saveLabel,
onCommandClick: () =>
this.sendMessageToNotebook(
NotebookAppContracts.MessageTypes.Save
).then((result: NotebookAppContracts.ContentItem) =>
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `File "${result.name}" was saved.`)
),
commandButtonLabel: saveLabel,
hasPopup: false,
disabled: false,
ariaLabel: saveLabel
},
{
iconSrc: null,
iconAlt: kernelLabel,
onCommandClick: () => {},
commandButtonLabel: null,
hasPopup: false,
disabled: this.availableKernels.length < 1,
isDropdown: true,
dropdownPlaceholder: kernelLabel,
dropdownSelectedKey: this.currentKernelName,
dropdownWidth: 100,
children: this.availableKernels.map((kernel: { name: string; displayName: string }) => ({
iconSrc: null,
iconAlt: kernel.displayName,
onCommandClick: () => {
this.isSwitchingKernels(true);
this.sendMessageToNotebook(NotebookAppContracts.MessageTypes.ChangeKernel, kernel.name);
},
commandButtonLabel: kernel.displayName,
dropdownItemKey: kernel.name,
hasPopup: false,
disabled: false,
ariaLabel: kernel.displayName
})),
ariaLabel: kernelLabel
},
{
iconSrc: RunIcon,
iconAlt: runLabel,
onCommandClick: () => this.sendMessageToNotebook(NotebookAppContracts.MessageTypes.RunAndAdvance),
commandButtonLabel: runLabel,
ariaLabel: runLabel,
hasPopup: false,
disabled: false,
children: [
{
iconSrc: RunIcon,
iconAlt: runActiveCellLabel,
onCommandClick: () => this.sendMessageToNotebook(NotebookAppContracts.MessageTypes.RunAndAdvance),
commandButtonLabel: runActiveCellLabel,
hasPopup: false,
disabled: false,
ariaLabel: runActiveCellLabel
},
{
iconSrc: RunAllIcon,
iconAlt: runAllLabel,
onCommandClick: () => this.sendMessageToNotebook(NotebookAppContracts.MessageTypes.RunAll),
commandButtonLabel: runAllLabel,
hasPopup: false,
disabled: false,
ariaLabel: runAllLabel
},
// {
// iconSrc: null,
// onCommandClick: () => this.postMessage("switchKernel"),
// commandButtonLabel: "Switch Kernel",
// hasPopup: false,
// disabled: false
// },
{
iconSrc: RestartIcon,
iconAlt: restartKernelLabel,
onCommandClick: () =>
this.sendMessageToNotebook(NotebookAppContracts.MessageTypes.RestartKernel).then(
(isSuccessful: boolean) => {
// Note: don't handle isSuccessful === false as it gets triggered if user cancels kernel restart modal dialog
if (isSuccessful) {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
"Kernel was successfully restarted"
);
}
}
),
commandButtonLabel: restartKernelLabel,
hasPopup: false,
disabled: false,
ariaLabel: restartKernelLabel
}
]
},
{
iconSrc: ClearAllOutputsIcon,
iconAlt: clearLabel,
onCommandClick: () => this.sendMessageToNotebook(NotebookAppContracts.MessageTypes.ClearAllOutputs),
commandButtonLabel: clearLabel,
hasPopup: false,
disabled: false,
ariaLabel: clearLabel
},
{
iconSrc: NewCellIcon,
iconAlt: newCellLabel,
onCommandClick: () => this.sendMessageToNotebook(NotebookAppContracts.MessageTypes.InsertBelow),
commandButtonLabel: newCellLabel,
ariaLabel: newCellLabel,
hasPopup: false,
disabled: false
},
CommandBarComponentButtonFactory.createDivider(),
{
iconSrc: null,
iconAlt: null,
onCommandClick: () => {},
commandButtonLabel: null,
ariaLabel: cellTypeLabel,
hasPopup: false,
disabled: false,
isDropdown: true,
dropdownPlaceholder: cellTypeLabel,
dropdownSelectedKey: this.activeCellTypeStr,
dropdownWidth: 110,
children: [
{
iconSrc: null,
iconAlt: null,
onCommandClick: () =>
this.sendMessageToNotebook(NotebookAppContracts.MessageTypes.ChangeCellType, cellCodeType),
commandButtonLabel: codeLabel,
ariaLabel: codeLabel,
dropdownItemKey: cellCodeType,
hasPopup: false,
disabled: false
},
{
iconSrc: null,
iconAlt: null,
onCommandClick: () =>
this.sendMessageToNotebook(NotebookAppContracts.MessageTypes.ChangeCellType, cellMarkdownType),
commandButtonLabel: markdownLabel,
ariaLabel: markdownLabel,
dropdownItemKey: cellMarkdownType,
hasPopup: false,
disabled: false
},
{
iconSrc: null,
iconAlt: null,
onCommandClick: () =>
this.sendMessageToNotebook(NotebookAppContracts.MessageTypes.ChangeCellType, cellRawType),
commandButtonLabel: rawLabel,
ariaLabel: rawLabel,
dropdownItemKey: cellRawType,
hasPopup: false,
disabled: false
}
]
},
{
iconSrc: CopyIcon,
iconAlt: copyLabel,
onCommandClick: () => this.sendMessageToNotebook(NotebookAppContracts.MessageTypes.Copy),
commandButtonLabel: copyLabel,
ariaLabel: copyLabel,
hasPopup: false,
disabled: false,
children: [
{
iconSrc: CopyIcon,
iconAlt: copyLabel,
onCommandClick: () => this.sendMessageToNotebook(NotebookAppContracts.MessageTypes.Copy),
commandButtonLabel: copyLabel,
ariaLabel: copyLabel,
hasPopup: false,
disabled: false
},
{
iconSrc: CutIcon,
iconAlt: cutLabel,
onCommandClick: () => this.sendMessageToNotebook(NotebookAppContracts.MessageTypes.Cut),
commandButtonLabel: cutLabel,
ariaLabel: cutLabel,
hasPopup: false,
disabled: false
},
{
iconSrc: PasteIcon,
iconAlt: pasteLabel,
onCommandClick: () => this.sendMessageToNotebook(NotebookAppContracts.MessageTypes.Paste),
commandButtonLabel: pasteLabel,
ariaLabel: pasteLabel,
hasPopup: false,
disabled: false
}
]
},
{
iconSrc: UndoIcon,
iconAlt: undoLabel,
onCommandClick: () => this.sendMessageToNotebook(NotebookAppContracts.MessageTypes.Undo),
commandButtonLabel: undoLabel,
ariaLabel: undoLabel,
hasPopup: false,
disabled: false,
children: [
{
iconSrc: UndoIcon,
iconAlt: undoLabel,
onCommandClick: () => this.sendMessageToNotebook(NotebookAppContracts.MessageTypes.Undo),
commandButtonLabel: undoLabel,
ariaLabel: undoLabel,
hasPopup: false,
disabled: false
},
{
iconSrc: RedoIcon,
iconAlt: redoLabel,
onCommandClick: () => this.sendMessageToNotebook(NotebookAppContracts.MessageTypes.Redo),
commandButtonLabel: redoLabel,
ariaLabel: redoLabel,
hasPopup: false,
disabled: false
}
]
}
];
if (this.container.hasStorageAnalyticsAfecFeature()) {
const arcadiaWorkspaceDropdown: ViewModels.NavbarButtonConfig = {
iconSrc: null,
iconAlt: workspaceLabel,
ariaLabel: workspaceLabel,
onCommandClick: () => {},
commandButtonLabel: null,
hasPopup: false,
disabled: this.container.arcadiaWorkspaces.length < 1,
isDropdown: false,
isArcadiaPicker: true,
arcadiaProps: {
selectedSparkPool: null,
workspaces: this.container.arcadiaWorkspaces(),
onSparkPoolSelect: () => {},
onCreateNewWorkspaceClicked: () => {
this.container.createWorkspace();
},
onCreateNewSparkPoolClicked: (workspaceResourceId: string) => {
this.container.createSparkPool(workspaceResourceId);
}
}
};
buttons.splice(1, 0, arcadiaWorkspaceDropdown);
}
return buttons;
}
protected buildCommandBarOptions(): void {
this.updateNavbarWithTabsButtons();
}
private async configureServiceEndpoints(kernelName: string) {
const notebookConnectionInfo = this.container && this.container.notebookServerInfo();
const sparkClusterConnectionInfo = this.container && this.container.sparkClusterConnectionInfo();
await NotebookConfigurationUtils.configureServiceEndpoints(
this.notebookPath(),
notebookConnectionInfo,
kernelName,
sparkClusterConnectionInfo
);
}
private sendMessageToNotebook(type: NotebookAppContracts.MessageTypes, arg?: string): Q.Promise<any> {
return this.messageHandler.sendCachedDataMessage(type, arg);
}
/**
* The iframe swallows any click event which breaks the logic to dismiss the menu, so we simulate a click from the message
*/
private simulateClick() {
if (!this.tabId) {
return;
}
const event = document.createEvent("Events");
event.initEvent("click", true, false);
document.getElementById(this.tabId).dispatchEvent(event);
}
}

View File

@@ -5,8 +5,6 @@ import Explorer from "../Explorer";
import { CollectionStub, DatabaseStub } from "../../Explorer/OpenActionsStubs";
import QueryTab from "./QueryTab";
jest.mock("./NotebookTab");
describe("Query Tab", () => {
function getNewQueryTabForContainer(container: ViewModels.Explorer): ViewModels.QueryTab {
const database: ViewModels.Database = new DatabaseStub({

View File

@@ -9,8 +9,6 @@ import Explorer from "../Explorer";
import SettingsTab from "../Tabs/SettingsTab";
import { DataAccessUtility } from "../../Platform/Portal/DataAccessUtility";
jest.mock("./NotebookTab");
describe("Settings tab", () => {
const baseCollection: DataModels.Collection = {
defaultTtl: 200,

View File

@@ -1,7 +1,6 @@
import DocumentsTabTemplate from "./DocumentsTab.html";
import ConflictsTabTemplate from "./ConflictsTab.html";
import GraphTabTemplate from "./GraphTab.html";
import NotebookTabTemplate from "./NotebookTab.html";
import SparkMasterTabTemplate from "./SparkMasterTab.html";
import NotebookV2TabTemplate from "./NotebookV2Tab.html";
import TerminalTabTemplate from "./TerminalTab.html";
@@ -51,15 +50,6 @@ export class GraphTab {
}
}
export class NotebookTab {
constructor() {
return {
viewModel: TabComponent,
template: NotebookTabTemplate
};
}
}
export class SparkMasterTab {
constructor() {
return {

View File

@@ -1,8 +0,0 @@
import * as ViewModels from "../../../Contracts/ViewModels";
import TabsBase from "../TabsBase";
export default class NotebookTab extends TabsBase implements ViewModels.Tab {
constructor(options: ViewModels.NotebookTabOptions) {
super(options);
}
}

View File

@@ -14,8 +14,6 @@ import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
import { OfferUtils } from "../../Utils/OfferUtils";
import { StartUploadMessageParams, UploadDetails, UploadDetailsRecord } from "../../workers/upload/definitions";
import { ContextMenuButtonFactory } from "../ContextMenuButtonFactory";
import ContextMenu from "../Menus/ContextMenu";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient";
import { ConflictsTab } from "../Tabs/ConflictsTab";
@@ -32,6 +30,7 @@ import DocumentId from "./DocumentId";
import StoredProcedure from "./StoredProcedure";
import Trigger from "./Trigger";
import UserDefinedFunction from "./UserDefinedFunction";
import { config } from "../../Config";
export default class Collection implements ViewModels.Collection {
public nodeKind: string;
@@ -85,9 +84,6 @@ export default class Collection implements ViewModels.Collection {
public userDefinedFunctionsFocused: ko.Observable<boolean>;
public triggersFocused: ko.Observable<boolean>;
public contextMenu: ViewModels.ContextMenu;
public documentsContextMenu: ViewModels.ContextMenu;
constructor(
container: ViewModels.Explorer,
databaseId: string,
@@ -215,217 +211,8 @@ export default class Collection implements ViewModels.Collection {
this.isStoredProceduresExpanded = ko.observable<boolean>(false);
this.isUserDefinedFunctionsExpanded = ko.observable<boolean>(false);
this.isTriggersExpanded = ko.observable<boolean>(false);
this.contextMenu = new ContextMenu(this.container, this.rid);
this.contextMenu.options(
ContextMenuButtonFactory.createCollectionContextMenuButton(container, {
databaseId: this.databaseId,
collectionId: this.id()
})
);
this.documentsContextMenu = new ContextMenu(this.container, `${this.rid}/documents`);
}
public onKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.expandCollapseCollection();
return false;
}
return true;
};
public onKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.key === "Delete") {
this.onDeleteCollectionContextMenuClick(source, event);
return false;
}
if (event.key === "ArrowRight" && !this.isCollectionExpanded()) {
this.expandCollection();
return false;
}
if (event.key === "ArrowDown" && !this.isCollectionExpanded()) {
this.expandCollection();
return false;
}
if (event.key === "ArrowLeft" && this.isCollectionExpanded()) {
this.collapseCollection();
return false;
}
return true;
};
public onMenuKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.key === "Escape") {
this.contextMenu.hide(source, event);
return false;
}
return true;
};
public onTableEntitiesKeyDown = (source: any, event: KeyboardEvent): boolean => {
return true;
};
public onTableEntitiesKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.onTableEntitiesClick();
event.stopPropagation();
return false;
}
return true;
};
public onGraphDocumentsKeyDown = (source: any, event: KeyboardEvent): boolean => {
return true;
};
public onGraphDocumentsKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.onGraphDocumentsClick();
event.stopPropagation();
return false;
}
return true;
};
public onDocumentDBDocumentsKeyDown = (source: any, event: KeyboardEvent): boolean => {
return true;
};
public onDocumentDBDocumentsKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.onDocumentDBDocumentsClick();
return false;
}
return true;
};
public onConflictsKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.onConflictsClick();
return false;
}
return true;
};
public onMongoDBDocumentsKeyDown = (source: any, event: KeyboardEvent): boolean => {
return true;
};
public onMongoDBDocumentsKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.onMongoDBDocumentsClick();
return false;
}
return true;
};
public onSettingsKeyDown = (source: any, event: KeyboardEvent): boolean => {
return true;
};
public onSettingsKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.onSettingsClick();
return false;
}
return true;
};
public onStoredProceduresKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.key === "ArrowRight" && !this.isStoredProceduresExpanded()) {
this.expandStoredProcedures();
return false;
}
if (event.key === "ArrowDown" && !this.isStoredProceduresExpanded()) {
this.expandStoredProcedures();
return false;
}
if (event.key === "ArrowLeft" && this.isStoredProceduresExpanded()) {
this.collapseStoredProcedures();
return false;
}
return true;
};
public onStoredProceduresKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.expandCollapseStoredProcedures();
return false;
}
return true;
};
public onUserDefinedFunctionsKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.key === "ArrowRight" && !this.isUserDefinedFunctionsExpanded()) {
this.expandUserDefinedFunctions();
return false;
}
if (event.key === "ArrowDown" && !this.isUserDefinedFunctionsExpanded()) {
this.expandUserDefinedFunctions();
return false;
}
if (event.key === "ArrowLeft" && this.isUserDefinedFunctionsExpanded()) {
this.collapseUserDefinedFunctions();
return false;
}
return true;
};
public onUserDefinedFunctionsKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.expandCollapseUserDefinedFunctions();
return false;
}
return true;
};
public onTriggersKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.key === "ArrowRight" && !this.isTriggersExpanded()) {
this.expandTriggers();
return false;
}
if (event.key === "ArrowDown" && !this.isTriggersExpanded()) {
this.expandTriggers();
return false;
}
if (event.key === "ArrowLeft" && this.isTriggersExpanded()) {
this.collapseTriggers();
return false;
}
return true;
};
public onTriggersKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.expandCollapseTriggers();
return false;
}
return true;
};
public expandCollapseCollection() {
this.container.selectedNode(this);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
@@ -984,9 +771,6 @@ export default class Collection implements ViewModels.Collection {
// Activate
queryTab.onTabClick();
// Hide Context Menu (if necessary)
collection.contextMenu.hide(this, null);
}
public onNewMongoQueryClick(source: any, event: MouseEvent, queryText?: string) {
@@ -1024,9 +808,6 @@ export default class Collection implements ViewModels.Collection {
// Activate
queryTab.onTabClick();
// Hide Context Menu (if necessary)
collection.contextMenu.hide(this, null);
}
public onNewGraphClick() {
@@ -1035,8 +816,6 @@ export default class Collection implements ViewModels.Collection {
this.container.openedTabs.push(graphTab);
// Activate
graphTab.onTabClick();
// Hide Context Menu (if necessary)
this.contextMenu.hide(this, null);
}
public onNewMongoShellClick() {
@@ -1059,9 +838,6 @@ export default class Collection implements ViewModels.Collection {
// Activate
mongoShellTab.onTabClick();
// Hide Context Menu (if necessary)
this.contextMenu.hide(this, null);
}
public onNewStoredProcedureClick(source: ViewModels.Collection, event: MouseEvent) {
@@ -1356,7 +1132,6 @@ export default class Collection implements ViewModels.Collection {
}
public onDeleteCollectionContextMenuClick(source: ViewModels.Collection, event: MouseEvent | KeyboardEvent) {
this._onContextMenuClick(source, event);
this.container.deleteCollectionConfirmationPane.open();
}
@@ -1416,7 +1191,7 @@ export default class Collection implements ViewModels.Collection {
masterKey: CosmosClient.masterKey(),
endpoint: CosmosClient.endpoint(),
accessToken: CosmosClient.accessToken(),
platform: window.dataExplorerPlatform,
platform: config.platform,
databaseAccount: CosmosClient.databaseAccount()
}
};
@@ -1604,11 +1379,6 @@ export default class Collection implements ViewModels.Collection {
});
}
private _onContextMenuClick(source: ViewModels.Collection, event: MouseEvent | KeyboardEvent) {
this.container.selectedNode(this);
this.contextMenu.hide(source, event);
}
protected _getOfferForCollection(offers: DataModels.Offer[], collection: DataModels.Collection): DataModels.Offer {
return _.find(offers, (offer: DataModels.Offer) => offer.resource.indexOf(collection._rid) >= 0);
}

View File

@@ -1,425 +0,0 @@
<div class="pointerCursor">
<div
role="treeitem"
data-test="collectionList"
tabindex="0"
class="collectionMenu treeHovermargin highlight"
data-bind="
click: $data.expandCollapseCollection,
clickBubble: false,
contextmenuBubble: false,
css:{
collectionNodeSelected: isCollectionNodeSelected(),
contextmenushowing: $data.contextMenu.visible
},
event: {
keydown: onKeyDown,
keypress: onKeyPress,
contextmenu: $data.contextMenu.show,
drop: $data.onDrop,
dragover: $data.onDragOver
},
attr:{
'aria-expanded': $data.isCollectionExpanded,
'aria-selected': isCollectionNodeSelected()
}"
>
<span
class="collectionList databaseCollChildTextOverflow"
data-bind="
attr: {
title: $data.id()
}"
>
<img
class="imgiconwidth collectionsTreeCollapseExpand"
src="/Triangle-right.svg"
alt="Show collection properties"
data-bind="visible: !$data.isCollectionExpanded()"
/>
<img
class="imgiconwidth collectionsTreeCollapseExpand"
src="/Triangle-down.svg"
alt="Hide collection properties"
data-bind="visible: $data.isCollectionExpanded()"
/>
<img
src="/tree-collection.svg"
data-bind="
attr: {
alt: container.addCollectionText
}"
/>
<!--ko text: $data.id-->
<!--/ko-->
</span>
<span
class="menuEllipsis"
data-test="collectionEllipsisMenu"
name="context menu"
role="button"
data-bind="click: $data.contextMenu.show, clickBubble: false"
>&hellip;</span
>
</div>
<!-- Collection node children - Start -->
<div
class="collectionChildList"
role="group"
data-bind="
visible: $data.isCollectionExpanded,
clickBubble: false"
>
<!-- Documents Node - Start -->
<div>
<div
role="treeitem"
tabindex="0"
class="documentsMenu"
data-bind="
visible: $root.isPreferredApiDocumentDB(),
click: $data.onDocumentDBDocumentsClick,
event: {
keydown: onDocumentDBDocumentsKeyDown,
keypress: onDocumentDBDocumentsKeyPress
},
clickBubble: false,
css: {
highlight: true,
collectionNodeSelected: isSubNodeSelected(0)
},
attr:{
'aria-selected': isSubNodeSelected(0)
}"
>
<span class="databaseDocuments">Items</span>
</div>
</div>
<!-- Documents Node - End -->
<!-- Entitites Node - Start -->
<div>
<div
role="treeitem"
tabindex="0"
class="documentsMenu"
data-bind="
visible: $root.isPreferredApiTable(),
click: $data.onTableEntitiesClick,
event: {
keydown: onTableEntitiesKeyDown,
keypress: onTableEntitiesKeyPress
},
clickBubble: false,
css: {
highlight: true,
collectionNodeSelected: $root.selectedNode && $root.selectedNode() && $root.selectedNode().rid === $data.rid && $data.selectedSubnodeKind() === 9
},
attr:{
'aria-selected': $root.selectedNode && $root.selectedNode() && $root.selectedNode().rid === $data.rid && $data.selectedSubnodeKind() === 9
}"
>
<span class="databaseDocuments">Entities</span>
</div>
</div>
<!-- Entitites Node - End -->
<!-- Rows Node - Start -->
<div>
<div
role="treeitem"
tabindex="0"
class="documentsMenu"
data-bind="
visible: $root.isPreferredApiCassandra(),
click: $data.onTableEntitiesClick,
event: {
keydown: onTableEntitiesKeyDown,
keypress: onTableEntitiesKeyPress
},
clickBubble: false,
css: {
highlight: true,
collectionNodeSelected: $root.selectedNode && $root.selectedNode() && $root.selectedNode().rid === $data.rid && $data.selectedSubnodeKind() === 9
},
attr:{
'aria-selected': $root.selectedNode && $root.selectedNode() && $root.selectedNode().rid === $data.rid && $data.selectedSubnodeKind() === 9
}"
>
<span class="databaseDocuments">Rows</span>
</div>
</div>
<!-- Rows Node - End -->
<!-- Graph Node - Start -->
<div>
<div
role="treeitem"
tabindex="0"
class="documentsMenu"
data-bind="
visible: $root.isPreferredApiGraph,
click: $data.onGraphDocumentsClick,
event: {
keydown: onGraphDocumentsKeyDown,
keypress: onGraphDocumentsKeyPress
},
clickBubble: false,
css: {
highlight: true,
collectionNodeSelected: $root.selectedNode && $root.selectedNode() && $root.selectedNode().rid === $data.rid && $data.selectedSubnodeKind() === 6
},
attr:{
'aria-selected': $root.selectedNode && $root.selectedNode() && $root.selectedNode().rid === $data.rid && $data.selectedSubnodeKind() === 6
}"
>
<span class="databaseDocuments">Graph</span>
</div>
</div>
<!-- Graph Node - End -->
<!-- MongoDB Documents Node - Start -->
<div>
<div
role="treeitem"
tabindex="0"
class="documentsMenu"
data-bind="
visible: $root.isPreferredApiMongoDB,
click: $data.onMongoDBDocumentsClick,
event: {
keydown: onMongoDBDocumentsKeyDown,
keypress: onMongoDBDocumentsKeyPress
},
clickBubble: false,
css: {
highlight: true,
collectionNodeSelected: $root.selectedNode && $root.selectedNode() && $root.selectedNode().rid === $data.rid && $data.selectedSubnodeKind() === 0
},
attr:{
'aria-selected': $root.selectedNode && $root.selectedNode() && $root.selectedNode().rid === $data.rid && $data.selectedSubnodeKind() === 0
}"
>
<span class="databaseDocuments">Documents</span>
</div>
</div>
<!-- MongoDB Documents Node - End -->
<!-- Scale & Setings Node - Start -->
<div>
<div
role="treeitem"
tabindex="0"
data-bind="
click: $data.onSettingsClick,
event: {
keydown: onSettingsKeyDown,
keypress: onSettingsKeyPress
},
css: {
highlight: true,
collectionNodeSelected: $root.selectedNode && $root.selectedNode() && $root.selectedNode().rid === $data.rid && $data.selectedSubnodeKind() === 1
},
attr:{
'aria-selected': $root.selectedNode && $root.selectedNode() && $root.selectedNode().rid === $data.rid && $data.selectedSubnodeKind() === 1
}"
>
<span class="databaseDocuments">
<!-- ko if: !$data.database.isDatabaseShared() -->
Scale & Settings
<!-- /ko -->
<!-- ko if: $data.database.isDatabaseShared() -->
Settings
<!-- /ko -->
</span>
</div>
</div>
<!-- Scale & Setings Node - End -->
<!-- Stored Procedures Node - Start -->
<div>
<div
role="treeitem"
tabindex="0"
class="storedProcedureMenu highlight"
data-bind="
click: $data.expandCollapseStoredProcedures,
event: {
keydown: onStoredProceduresKeyDown,
keypress: onStoredProceduresKeyPress
},
css: {
collectionNodeSelected: !isStoredProceduresExpanded() && isSubNodeSelected(2)
},
attr:{
'aria-expanded': $data.isStoredProceduresExpanded(),
'aria-selected': !isStoredProceduresExpanded() && isSubNodeSelected(2)
},
visible: showStoredProcedures"
>
<span
class="collectionMenuChildren"
data-bind="
attr: {
title: $data.id()
}"
>
<img
class="imgiconwidth collectionsTreeCollapseExpand"
src="/Triangle-right.svg"
alt="Show storedprocedures properties"
data-bind="visible: !$data.isStoredProceduresExpanded()"
/>
<img
class="imgiconwidth collectionsTreeCollapseExpand"
src="/Triangle-down.svg"
alt="Hide storedprocedures properties"
data-bind="visible: $data.isStoredProceduresExpanded()"
/>
Stored Procedures
</span>
</div>
<div
class="storedUdfTriggerMenu"
data-bind=" visible: $data.isStoredProceduresExpanded(), foreach: $data.storedProcedures"
>
<stored-procedure-node params="{data: $data}"></stored-procedure-node>
</div>
</div>
<!-- Stored Procedures Node - End -->
<!-- User Defined Functions Node - Start -->
<div>
<div
role="treeitem"
tabindex="0"
class="userDefinedMenu highlight"
data-bind="
click: $data.expandCollapseUserDefinedFunctions,
event: {
keydown: onUserDefinedFunctionsKeyDown,
keypress: onUserDefinedFunctionsKeyPress
},
css: {
collectionNodeSelected: !isUserDefinedFunctionsExpanded() && isSubNodeSelected(3)
},
attr:{
'aria-expanded': $data.isUserDefinedFunctionsExpanded(),
'aria-selected': !isUserDefinedFunctionsExpanded() && isSubNodeSelected(3)
},
visible: showUserDefinedFunctions"
>
<div>
<span
class="collectionMenuChildren"
data-bind="
attr: {
title: $data.id()
}"
>
<img
class="imgiconwidth collectionsTreeCollapseExpand"
src="/Triangle-right.svg"
alt="Show userdefinedfunctions properties"
data-bind="visible: !$data.isUserDefinedFunctionsExpanded()"
/>
<img
class="imgiconwidth collectionsTreeCollapseExpand"
src="/Triangle-down.svg"
alt="Hide userdefinedfunctions properties"
data-bind="visible: $data.isUserDefinedFunctionsExpanded()"
/>
User Defined Functions
</span>
</div>
</div>
<div
class="storedUdfTriggerMenu"
data-bind="visible: $data.isUserDefinedFunctionsExpanded(), foreach: $data.userDefinedFunctions"
>
<user-defined-function-node params="{data: $data}"></user-defined-function-node>
</div>
</div>
<!-- User Defined Functions Node - End -->
<!-- Triggers Node - Start -->
<div>
<div
role="treeitem"
tabindex="0"
class="triggersMenu highlight"
data-bind="
click: $data.expandCollapseTriggers,
event: {
keydown: onTriggersKeyDown,
keypress: onTriggersKeyPress
},
css: {
collectionNodeSelected: !isTriggersExpanded() && isSubNodeSelected(4)
},
attr:{
'aria-expanded': $data.isTriggersExpanded(),
'aria-selected': !isTriggersExpanded() && isSubNodeSelected(4)
},
visible: showTriggers"
>
<div>
<span
class="collectionMenuChildren"
data-bind="
attr: {
title: $data.id()
}"
>
<img
class="imgiconwidth collectionsTreeCollapseExpand"
src="/Triangle-right.svg"
alt="Show Triggers properties"
data-bind="visible: !$data.isTriggersExpanded()"
/>
<img
class="imgiconwidth collectionsTreeCollapseExpand"
src="/Triangle-down.svg"
alt="Hide Triggers properties"
data-bind="visible: $data.isTriggersExpanded()"
/>
Triggers
</span>
</div>
</div>
<div class="storedUdfTriggerMenu" data-bind="visible: $data.isTriggersExpanded(), foreach: $data.triggers">
<trigger-node params="{data: $data}"></trigger-node>
</div>
</div>
<!-- Triggers Node - End -->
<!-- Conflicts Node - Start -->
<div>
<div
role="treeitem"
tabindex="0"
data-bind="
click: $data.onConflictsClick,
event: {
keypress: onConflictsKeyPress
},
css: {
highlight: true,
collectionNodeSelected: isSubNodeSelected(12)
},
attr:{
'aria-selected': isSubNodeSelected(12)
},
visible: showConflicts"
>
<span class=" databaseDocuments"> Conflicts </span>
</div>
</div>
<!-- Conflicts Node - End -->
</div>
<!-- Collection node children - End -->
</div>

View File

@@ -1,16 +0,0 @@
<div data-bind="event: { keydown: onMenuKeyDown }">
<div
class="context-menu-background"
data-bind="
visible: $data.contextMenu.visible,
click: $data.contextMenu.hide"
></div>
<div
class="context-menu"
data-test="collectionContextMenu"
data-bind="attr:{ tabindex: $data.contextMenu.tabIndex, id: $data.contextMenu.elementId }, visible: $data.contextMenu.visible, foreach: $data.contextMenu.options"
>
<command-button class="context-menu-option" params="{buttonProps: $data}"></command-button>
</div>
</div>

View File

@@ -7,11 +7,9 @@ import * as DataModels from "../../Contracts/DataModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import DatabaseSettingsTab from "../Tabs/DatabaseSettingsTab";
import Collection from "./Collection";
import ContextMenu from "../Menus/ContextMenu";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContextMenuButtonFactory } from "../ContextMenuButtonFactory";
import { Logger } from "../../Common/Logger";
export default class Database implements ViewModels.Database {
@@ -25,7 +23,6 @@ export default class Database implements ViewModels.Database {
public isDatabaseExpanded: ko.Observable<boolean>;
public isDatabaseShared: ko.Computed<boolean>;
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
public contextMenu: ViewModels.ContextMenu;
constructor(container: ViewModels.Explorer, data: any, offer: DataModels.Offer) {
this.nodeKind = "Database";
@@ -36,71 +33,12 @@ export default class Database implements ViewModels.Database {
this.offer = ko.observable(offer);
this.collections = ko.observableArray<Collection>();
this.isDatabaseExpanded = ko.observable<boolean>(false);
this.contextMenu = new ContextMenu(this.container, this.rid);
this.contextMenu.options(
ContextMenuButtonFactory.createDatabaseContextMenuButton(container, { databaseId: this.id() })
);
this.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
this.isDatabaseShared = ko.pureComputed(() => {
return this.offer && !!this.offer();
});
}
public onKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.key === " " || event.key === "Enter") {
this.expandCollapseDatabase();
return false;
}
return true;
};
public onKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.key === "Delete") {
this.onDeleteDatabaseContextMenuClick(source, event);
return false;
}
if (event.key === "ArrowRight") {
this.expandDatabase();
return false;
}
if (event.key === "ArrowLeft") {
this.collapseDatabase();
return false;
}
if (event.key === "Enter") {
this.expandCollapseDatabase();
return false;
}
return true;
};
public onMenuKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.key === "Escape") {
this.contextMenu.hide(source, event);
return false;
}
return true;
};
public onSettingsKeyDown = (source: any, event: KeyboardEvent): boolean => {
return true;
};
public onSettingsKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.key === " " || event.key === "Enter") {
this.onSettingsClick();
return false;
}
return true;
};
public onSettingsClick = () => {
this.container.selectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
@@ -262,8 +200,6 @@ export default class Database implements ViewModels.Database {
}
public onDeleteDatabaseContextMenuClick(source: ViewModels.Database, event: MouseEvent | KeyboardEvent) {
source.container.selectedNode(source);
source.contextMenu.hide(source, event);
this.container.deleteDatabaseConfirmationPane.open();
}
@@ -346,7 +282,6 @@ export default class Database implements ViewModels.Database {
}
public openAddCollection(database: Database, event: MouseEvent) {
database.contextMenu.hide(database, event);
database.container.addCollectionPane.databaseId(database.id());
database.container.addCollectionPane.open();
}

View File

@@ -1,110 +0,0 @@
<div class="pointerCursor">
<div
role="treeitem"
tabindex="0"
data-test="databaseMenu"
class="databaseMenu treeHovermargin highlight"
data-bind="
click: $data.expandCollapseDatabase,
event: {
keydown: onKeyDown,
keypress: onKeyPress,
contextmenu: $data.contextMenu.show
},
clickBubble: false,
contextmenuBubble: false,
css:{
contextmenushowing: $data.contextMenu.visible,
highlight: true,
databaseNodeSelected: isDatabaseNodeSelected()
},
attr:{
'aria-expanded': $data.isDatabaseExpanded,
'aria-selected': isDatabaseNodeSelected()
}"
>
<span
class="databaseId databaseCollChildTextOverflow"
data-bind="
attr: {
title: $data.id()
}"
>
<img
class="imgiconwidth collectionsTreeCollapseExpand"
src="/Triangle-right.svg"
alt="Show database properties"
data-bind="visible: !$data.isDatabaseExpanded()"
/>
<img
class="imgiconwidth collectionsTreeCollapseExpand"
src="/Triangle-down.svg"
alt="Hide database properties"
data-bind="visible: $data.isDatabaseExpanded()"
/>
<img src="/Azure-Cosmos-DB.svg" alt="Database" />
<!--ko text: $data.id--><!--/ko-->
</span>
<span
class="menuEllipsis"
data-test="databaseEllipsisMenu"
name="context menu"
role="button"
data-bind="
click: $data.contextMenu.show,
clickBubble: false
"
>&hellip;</span
>
</div>
<div class="databaseList" data-test="databaseList" data-bind="visible: $data.isDatabaseExpanded">
<!-- Scale & Setings Node - Start -->
<div data-bind="visible: $data.isDatabaseShared">
<div
role="treeitem"
class="databaseCollChildTextOverflow treeHovermargin highlight"
tabindex="0"
data-bind="
click: $data.onSettingsClick,
event: {
keydown: onSettingsKeyDown,
keypress: onSettingsKeyPress
},
css: {
highlight: true,
collectionNodeSelected: $root.selectedNode && $root.selectedNode() && $root.selectedNode().rid === $data.rid && $data.selectedSubnodeKind() === 11
},
attr:{
'aria-selected': $root.selectedNode && $root.selectedNode() && $root.selectedNode().rid === $data.rid && $data.selectedSubnodeKind() === 11
}"
>
<span class="databaseDocuments"> Scale </span>
</div>
</div>
<!-- Scale & Setings Node - End -->
<div data-bind="foreach: $data.collections">
<collection-node params="{data: $data}"></collection-node>
<collection-node-context-menu params="{data: $data}"></collection-node-context-menu>
</div>
</div>
<!-- Database Context Menu - Start -->
<div data-bind="event: { keydown: onMenuKeyDown }">
<div
class="context-menu-background"
data-bind="
visible: $data.contextMenu.visible,
click: $data.contextMenu.hide"
></div>
<div
class="context-menu"
data-test="databaseContextMenu"
data-bind="attr:{ tabindex: $data.contextMenu.tabIndex, id: $data.contextMenu.elementId }, visible: $data.contextMenu.visible, foreach: $data.contextMenu.options"
>
<command-button class="context-menu-option" params="{buttonProps: $data}"></command-button>
</div>
</div>
<!-- Database Context Menu - End -->
</div>

View File

@@ -1,11 +0,0 @@
<div
class="collectionstree"
data-test="resoureTree-collectionsTree"
tabindex="0"
role="tree"
data-bind="attr: { 'aria-label': collectionTitle }"
>
<div class="databaseList" data-bind="foreach: nonSystemDatabases">
<database-node params="{data: $data}"></database-node>
</div>
</div>

View File

@@ -6,7 +6,6 @@ import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeCompo
import * as ViewModels from "../../Contracts/ViewModels";
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory";
import NotebookTab from "../Tabs/NotebookTab";
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
import { CosmosClient } from "../../Common/CosmosClient";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
@@ -302,7 +301,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
onClick: sp.open.bind(sp),
isSelected: () =>
this.isDataNodeSelected(collection.rid, "Collection", ViewModels.CollectionTabKind.StoredProcedures),
contextMenu: ResourceTreeContextMenuButtonFactory.createStoreProcedureContextMenuItems(this.container)
contextMenu: ResourceTreeContextMenuButtonFactory.createStoreProcedureContextMenuItems(this.container, sp)
})),
onClick: () => {
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.StoredProcedures);
@@ -319,7 +318,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
onClick: udf.open.bind(udf),
isSelected: () =>
this.isDataNodeSelected(collection.rid, "Collection", ViewModels.CollectionTabKind.UserDefinedFunctions),
contextMenu: ResourceTreeContextMenuButtonFactory.createUserDefinedFunctionContextMenuItems(this.container)
contextMenu: ResourceTreeContextMenuButtonFactory.createUserDefinedFunctionContextMenuItems(this.container, udf)
})),
onClick: () => {
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.UserDefinedFunctions);
@@ -335,7 +334,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
label: trigger.id(),
onClick: trigger.open.bind(trigger),
isSelected: () => this.isDataNodeSelected(collection.rid, "Collection", ViewModels.CollectionTabKind.Triggers),
contextMenu: ResourceTreeContextMenuButtonFactory.createTriggerContextMenuItems(this.container)
contextMenu: ResourceTreeContextMenuButtonFactory.createTriggerContextMenuItems(this.container, trigger)
})),
onClick: () => {
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Triggers);
@@ -526,7 +525,10 @@ export class ResourceTreeAdapter implements ReactAdapter {
return (
activeTab &&
activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
(activeTab as NotebookTab).notebookPath() === item.path
/* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab.
NotebookV2Tab could be dynamically imported, but not worth it to just get this type right.
*/
(activeTab as any).notebookPath() === item.path
);
},
contextMenu: createFileContextMenu
@@ -640,7 +642,10 @@ export class ResourceTreeAdapter implements ReactAdapter {
return (
activeTab &&
activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
(activeTab as NotebookTab).notebookPath() === item.path
/* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab.
NotebookV2Tab could be dynamically imported, but not worth it to just get this type right.
*/
(activeTab as any).notebookPath() === item.path
);
},
contextMenu:

View File

@@ -5,9 +5,7 @@ import * as DataModels from "../../Contracts/DataModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import StoredProcedureTab from "../Tabs/StoredProcedureTab";
import ContextMenu from "../Menus/ContextMenu";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { ContextMenuButtonFactory } from "../ContextMenuButtonFactory";
const sampleStoredProcedureBody: string = `// SAMPLE STORED PROCEDURE
function sample(prefix) {
@@ -44,7 +42,6 @@ export default class StoredProcedure implements ViewModels.StoredProcedure {
public rid: string;
public id: ko.Observable<string>;
public body: ko.Observable<string>;
public contextMenu: ViewModels.ContextMenu;
public isExecuteEnabled: boolean;
constructor(container: ViewModels.Explorer, collection: ViewModels.Collection, data: DataModels.StoredProcedure) {
@@ -56,9 +53,6 @@ export default class StoredProcedure implements ViewModels.StoredProcedure {
this.id = ko.observable(data.id);
this.body = ko.observable(data.body);
this.isExecuteEnabled = this.container.isFeatureEnabled(Constants.Features.executeSproc);
this.contextMenu = new ContextMenu(this.container, this.rid);
this.contextMenu.options(ContextMenuButtonFactory.createStoreProcedureContextMenuButton(container));
}
public static create(source: ViewModels.Collection, event: MouseEvent) {
@@ -89,9 +83,6 @@ export default class StoredProcedure implements ViewModels.StoredProcedure {
// Activate
storedProcedureTab.onTabClick();
// Hide Context Menu (if necessary)
source.contextMenu.hide(source, event);
}
public select() {
@@ -148,10 +139,7 @@ export default class StoredProcedure implements ViewModels.StoredProcedure {
storedProcedureTab.onTabClick();
};
public delete(source: ViewModels.Collection, event: MouseEvent | KeyboardEvent) {
// Hide Context Menu (if necessary)
this.contextMenu.hide(source, event);
public delete() {
if (!window.confirm("Are you sure you want to delete the stored procedure?")) {
return;
}
@@ -192,33 +180,6 @@ export default class StoredProcedure implements ViewModels.StoredProcedure {
});
}
public onKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.key === "Delete") {
this.delete(source, event);
return false;
}
return true;
};
public onMenuKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.key === "Escape") {
this.contextMenu.hide(source, event);
return false;
}
return true;
};
public onKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.key === " " || event.key === "Enter") {
this.open();
return false;
}
return true;
};
public onFocusAfterExecute(): void {
const focusElement = document.getElementById("execute-storedproc-toggles");
focusElement && focusElement.focus();

View File

@@ -1,63 +0,0 @@
<div
role="treeitem"
tabindex="0"
class="pointerCursor"
data-bind="
click: $data.open,
event: {
keydown: onKeyDown,
keypress: onKeyPress,
contextmenu: $data.contextMenu.show
},
clickBubble: false,
contextmenuBubble: false,
css: {
highlight: true,
collectionNodeSelected: $root.selectedNode && $root.selectedNode() && $root.selectedNode().rid === $data.rid,
contextmenushowing: $data.contextMenu.visible
},
attr:{
'aria-selected': $root.selectedNode && $root.selectedNode() && $root.selectedNode().rid === $data.rid
}"
>
<div class="storedChildMenu treeChildMenu">
<div
class="childMenu"
data-bind="
attr: {
title: $data.id()
}"
>
<!--ko text: $data.id-->
<!--/ko-->
</div>
<span
class="menuEllipsis"
name="context menu"
role="button"
data-bind="
click: $data.contextMenu.show,
clickBubble: false
"
>&hellip;</span
>
</div>
</div>
<!-- Stored Procedure Node Context Menu - Start -->
<div data-bind="event: { keydown: onMenuKeyDown }">
<div
class="context-menu-background"
data-bind="
visible: $data.contextMenu.visible,
click: $data.contextMenu.hide"
></div>
<div
class="context-menu"
data-bind="attr:{ tabindex: $data.contextMenu.tabIndex, id: $data.contextMenu.elementId }, visible: $data.contextMenu.visible, foreach: $data.contextMenu.options"
>
<command-button class="context-menu-option" params="{buttonProps: $data}"></command-button>
</div>
</div>
<!-- Stored Procedure Node Context Menu - End -->

View File

@@ -1,76 +0,0 @@
import resourceTreeTemplate from "./ResourceTree.html";
import databaseTreeNoteTemplate from "./DatabaseTreeNode.html";
import collectionTreeNodeTemplate from "./CollectionTreeNode.html";
import storedProcedureTreeNodeTemplate from "./StoredProcedureTreeNode.html";
import userDefinedFunctionTreeNodeTemplate from "./UserDefinedFunctionTreeNode.html";
import triggerTreeNodeTemplate from "./TriggerTreeNode.html";
import collectionTreeNodeContextMenuTemplate from "./CollectionTreeNodeContextMenu.html";
export class TreeNodeComponent {
constructor(data: any) {
return data.data;
}
}
export class ResourceTree {
constructor() {
return {
viewModel: TreeNodeComponent,
template: resourceTreeTemplate
};
}
}
export class DatabaseTreeNode {
constructor() {
return {
viewModel: TreeNodeComponent,
template: databaseTreeNoteTemplate
};
}
}
export class CollectionTreeNode {
constructor() {
return {
viewModel: TreeNodeComponent,
template: collectionTreeNodeTemplate
};
}
}
export class StoredProcedureTreeNode {
constructor() {
return {
viewModel: TreeNodeComponent,
template: storedProcedureTreeNodeTemplate
};
}
}
export class UserDefinedFunctionTreeNode {
constructor() {
return {
viewModel: TreeNodeComponent,
template: userDefinedFunctionTreeNodeTemplate
};
}
}
export class TriggerTreeNode {
constructor() {
return {
viewModel: TreeNodeComponent,
template: triggerTreeNodeTemplate
};
}
}
export class CollectionTreeNodeContextMenu {
constructor() {
return {
viewModel: TreeNodeComponent,
template: collectionTreeNodeContextMenuTemplate
};
}
}

View File

@@ -5,9 +5,7 @@ import * as DataModels from "../../Contracts/DataModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import Collection from "./Collection";
import TriggerTab from "../Tabs/TriggerTab";
import ContextMenu from "../Menus/ContextMenu";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { ContextMenuButtonFactory } from "../ContextMenuButtonFactory";
export default class Trigger implements ViewModels.Trigger {
public nodeKind: string;
@@ -19,7 +17,6 @@ export default class Trigger implements ViewModels.Trigger {
public body: ko.Observable<string>;
public triggerType: ko.Observable<string>;
public triggerOperation: ko.Observable<string>;
public contextMenu: ViewModels.ContextMenu;
constructor(container: ViewModels.Explorer, collection: ViewModels.Collection, data: any) {
this.nodeKind = "Trigger";
@@ -31,9 +28,6 @@ export default class Trigger implements ViewModels.Trigger {
this.body = ko.observable(data.body);
this.triggerOperation = ko.observable(data.triggerOperation);
this.triggerType = ko.observable(data.triggerType);
this.contextMenu = new ContextMenu(this.container, this.rid);
this.contextMenu.options(ContextMenuButtonFactory.createTriggerContextMenuButton(container));
}
public select() {
@@ -78,9 +72,6 @@ export default class Trigger implements ViewModels.Trigger {
// Activate
triggerTab.onTabClick();
// Hide Context Menu (if necessary)
source.contextMenu.hide(source, event);
}
public open = () => {
@@ -125,10 +116,7 @@ export default class Trigger implements ViewModels.Trigger {
triggerTab.onTabClick();
};
public delete(source: Collection, event: MouseEvent | KeyboardEvent) {
// Hide Context Menu (if necessary)
this.contextMenu.hide(source, event);
public delete() {
if (!window.confirm("Are you sure you want to delete the trigger?")) {
return;
}
@@ -150,31 +138,4 @@ export default class Trigger implements ViewModels.Trigger {
reason => {}
);
}
public onKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.key === "Delete") {
this.delete(source, event);
return false;
}
return true;
};
public onMenuKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.key === "Escape") {
this.contextMenu.hide(source, event);
return false;
}
return true;
};
public onKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.key === " " || event.key === "Enter") {
this.open();
return false;
}
return true;
};
}

View File

@@ -1,62 +0,0 @@
<div
role="treeitem"
tabindex="0"
class="pointerCursor"
data-bind="
click: $data.open,
event: {
keydown: onKeyDown,
keypress: onKeyPress,
contextmenu: $data.contextMenu.show
},
clickBubble: false,
contextmenuBubble: false,
css: {
highlight: true,
collectionNodeSelected: $root.selectedNode && $root.selectedNode() && $root.selectedNode().rid === $data.rid,
contextmenushowing: $data.contextMenu.visible
},
attr:{
'aria-selected': $root.selectedNode && $root.selectedNode() && $root.selectedNode().rid === $data.rid
}"
>
<div class="triggersChildMenu treeChildMenu">
<div
class="childMenu"
data-bind="
attr: {
title: $data.id()
}"
>
<!--ko text: $data.id-->
<!--/ko-->
</div>
<span
class="menuEllipsis"
name="context menu"
role="button"
data-bind="
click: $data.contextMenu.show,
clickBubble: false
"
>&hellip;</span
>
</div>
</div>
<!-- Trigger Node Context Menu - Start -->
<div data-bind="event: { keydown: onMenuKeyDown }">
<div
class="context-menu-background"
data-bind="
visible: $data.contextMenu.visible,
click: $data.contextMenu.hide"
></div>
<div
class="context-menu"
data-bind="attr:{ tabindex: $data.contextMenu.tabIndex, id: $data.contextMenu.elementId }, visible: $data.contextMenu.visible, foreach: $data.contextMenu.options"
>
<command-button class="context-menu-option" params="{buttonProps: $data}"></command-button>
</div>
</div>

View File

@@ -4,9 +4,7 @@ import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import UserDefinedFunctionTab from "../Tabs/UserDefinedFunctionTab";
import ContextMenu from "../Menus/ContextMenu";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { ContextMenuButtonFactory } from "../ContextMenuButtonFactory";
import Collection from "./Collection";
export default class UserDefinedFunction implements ViewModels.UserDefinedFunction {
@@ -17,7 +15,6 @@ export default class UserDefinedFunction implements ViewModels.UserDefinedFuncti
public rid: string;
public id: ko.Observable<string>;
public body: ko.Observable<string>;
public contextMenu: ViewModels.ContextMenu;
constructor(container: ViewModels.Explorer, collection: ViewModels.Collection, data: DataModels.UserDefinedFunction) {
this.nodeKind = "UserDefinedFunction";
@@ -28,9 +25,6 @@ export default class UserDefinedFunction implements ViewModels.UserDefinedFuncti
this.rid = data._rid;
this.id = ko.observable(data.id);
this.body = ko.observable(data.body);
this.contextMenu = new ContextMenu(this.container, this.rid);
this.contextMenu.options(ContextMenuButtonFactory.createUserDefinedFunctionContextMenuButton(container));
}
public static create(source: ViewModels.Collection, event: MouseEvent) {
@@ -61,9 +55,6 @@ export default class UserDefinedFunction implements ViewModels.UserDefinedFuncti
// Activate
userDefinedFunctionTab.onTabClick();
// Hide Context Menu (if necessary)
source.contextMenu.hide(source, event);
}
public open = () => {
@@ -115,10 +106,7 @@ export default class UserDefinedFunction implements ViewModels.UserDefinedFuncti
});
}
public delete(source: Collection, event: MouseEvent | KeyboardEvent) {
// Hide Context Menu (if necessary)
this.contextMenu.hide(source, event);
public delete() {
if (!window.confirm("Are you sure you want to delete the user defined function?")) {
return;
}
@@ -137,31 +125,4 @@ export default class UserDefinedFunction implements ViewModels.UserDefinedFuncti
reason => {}
);
}
public onKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.key === "Delete") {
this.delete(source, event);
return false;
}
return true;
};
public onMenuKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.key === "Escape") {
this.contextMenu.hide(source, event);
return false;
}
return true;
};
public onKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.key === " " || event.key === "Enter") {
this.open();
return false;
}
return true;
};
}

View File

@@ -1,62 +0,0 @@
<div
role="treeitem"
tabindex="0"
class="pointerCursor"
data-bind="
click: $data.open,
event: {
keydown: onKeyDown,
keypress: onKeyPress,
contextmenu: $data.contextMenu.show
},
clickBubble: false,
contextmenuBubble: false,
css: {
highlight: true,
collectionNodeSelected: $root.selectedNode && $root.selectedNode() && $root.selectedNode().rid === $data.rid,
contextmenushowing: $data.contextMenu.visible
},
attr:{
'aria-selected': $root.selectedNode && $root.selectedNode() && $root.selectedNode().rid === $data.rid
}"
>
<div class="userDefinedchildMenu treeChildMenu">
<div
class="childMenu"
data-bind="
attr: {
title: $data.id()
}"
>
<!--ko text: $data.id-->
<!--/ko-->
</div>
<span
class="menuEllipsis"
name="context menu"
role="button"
data-bind="
click: $data.contextMenu.show,
clickBubble: false
"
>&hellip;</span
>
</div>
</div>
<!-- User Defined Function Node Context Menu - Start -->
<div data-bind="event: { keydown: onMenuKeyDown }">
<div
class="context-menu-background"
data-bind="
visible: $data.contextMenu.visible,
click: $data.contextMenu.hide"
></div>
<div
class="context-menu"
data-bind="attr:{ tabindex: $data.contextMenu.tabIndex, id: $data.contextMenu.elementId }, visible: $data.contextMenu.visible, foreach: $data.contextMenu.options"
>
<command-button class="context-menu-option" params="{buttonProps: $data}"></command-button>
</div>
</div>
<!-- User Defined Function Node Context Menu - End -->

View File

@@ -7,7 +7,6 @@ import "../less/menus.less";
import "../less/infobox.less";
import "../less/messagebox.less";
import "./Explorer/Controls/InputTypeahead/InputTypeahead.less";
import "./Explorer/Controls/CommandButton/CommandButton.less";
import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less";
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";

View File

@@ -5,8 +5,6 @@ import * as Constants from "../Common/Constants";
import Explorer from "../Explorer/Explorer";
import { TabRouteHandler } from "./TabRouteHandler";
jest.mock("../Explorer/Tabs/NotebookTab");
describe("TabRouteHandler", () => {
let tabRouteHandler: TabRouteHandler;

View File

@@ -1,5 +1,5 @@
/**
* Defines constants related to logging telemetry. This file should be kept in sync with the one in the portal extension code as much as possible.
* Defines constants related to logging telemetry. Everything except Action should be kept in sync with the one in the Portal code as much as possible.
*
* TODO: Move this to ExplorerContracts (265329)
*/
@@ -8,13 +8,9 @@ export class General {
public static BladeNamePrefix: string = "Extension/Microsoft_Azure_DocumentDB/Blade/";
}
/**
* This is to be kept in sync with the one in portal. Please update the one in the portal if you add/remove any entry.
*/
// Data Explorer specific actions. No need to keep this in sync with the one in Portal.
export enum Action {
CollapseTreeNode,
CreateDatabaseAccount,
CreateAzureFunction,
CreateCollection,
CreateDocument,
CreateStoredProcedure,
@@ -23,7 +19,6 @@ export enum Action {
DeleteCollection,
DeleteDatabase,
DeleteDocument,
DownloadQuickstart,
ExpandTreeNode,
ExecuteQuery,
HasFeature,
@@ -33,27 +28,18 @@ export enum Action {
LoadDatabaseAccount,
LoadCollections,
LoadDatabases,
LoadMetrics,
LoadOffers,
LoadSingleCollectionWithOfferAndStatistics,
MongoShell,
OpenMetrics,
ContextualPane,
ScaleThroughput,
SelectItem,
SwitchQuickstartPlatform,
Tab,
UpdateDocument,
UpdateRegions,
UpdateSettings,
UpdateStoredProcedure,
UpdateTrigger,
UpdateUDF,
ViewWarning,
LoadBlade,
LoadResourceTree,
LoadMetricsTab,
AccountLevelThroughput,
CreateDatabase,
ResolveConflict,
DeleteConflict,

View File

@@ -66,8 +66,6 @@ c.NotebookApp.disable_check_xsrf = True
* There is a "New Cell" button in the CommandBar outside the jupyter iframe which will add a cell inside the notebook.
# Notes
* The iframe in the Data Explorer Tab loads jupyter with the server and notebook pathname passed in the query parameters (they're hardcoded right now):
cosmosdb-dataexplorer/Product/Portal/DataExplorer/src/Explorer/Tabs/NotebookTab.html
* The Emulator is located in: C:\Program Files\Azure Cosmos DB Emulator\Packages\DataExplorer
* Running "jupyter notebook" serves the jupyter traditional frontend. There is an alternate frontend also developed by jupyter which is modular and customizable called: JupyterLab. We use their "notebook" example in this project slightly modified to pass the server and notebook pathname via iframe url's parameters:
https://github.com/jupyterlab/jupyterlab/tree/master/examples/notebook

View File

@@ -128,15 +128,12 @@
</div>
</div>
<!-- Collections Window Title/Command Bar - End -->
<!-- ko if: !enableLegacyResourceTree() && !isAuthWithResourceToken() -->
<!-- ko if: !isAuthWithResourceToken() -->
<div style="overflow-y: auto" data-bind="react:resourceTree"></div>
<!-- /ko -->
<!-- ko if: !enableLegacyResourceTree() && isAuthWithResourceToken() -->
<!-- ko if: isAuthWithResourceToken() -->
<div style="overflow-y: auto" data-bind="react:resourceTreeForResourceToken"></div>
<!-- /ko -->
<!-- ko if: enableLegacyResourceTree -->
<resource-tree class="resourceTreeScroll" params="{data: $data}"></resource-tree>
<!-- /ko -->
</div>
<!-- Collections Window - End -->
</div>
@@ -337,10 +334,6 @@
<conflicts-tab params="{data: $data}"></conflicts-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 13 -->
<notebook-tab params="{data: $data}"></notebook-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 14 -->
<terminal-tab params="{data: $data}"></terminal-tab>
<!-- /ko -->
@@ -431,7 +424,7 @@
</div>
</div>
<!-- Global loader - End -->
<div data-bind="react:uploadItemsPaneAdapter"></div>
<add-database-pane params="{data: addDatabasePane}"></add-database-pane>
<add-collection-pane params="{data: addCollectionPane}"></add-collection-pane>
<delete-collection-confirmation-pane params="{data: deleteCollectionConfirmationPane}">

View File

@@ -1,5 +1,5 @@
import { DatabaseAccount } from "../../Contracts/DataModels";
import { PlatformType } from "../../PlatformType";
import { Platform } from "../../Config";
export interface StartUploadMessageParams {
files: FileList;
@@ -12,7 +12,7 @@ export interface DocumentClientParams {
masterKey: string;
endpoint: string;
accessToken: string;
platform: PlatformType;
platform: Platform;
databaseAccount: DatabaseAccount;
}

View File

@@ -1,6 +1,7 @@
import "babel-polyfill";
import { DocumentClientParams, UploadDetailsRecord, UploadDetails } from "./definitions";
import { CosmosClient } from "../../Common/CosmosClient";
import { config } from "../../Config";
let numUploadsSuccessful = 0;
let numUploadsFailed = 0;
@@ -33,8 +34,7 @@ onmessage = (event: MessageEvent) => {
CosmosClient.endpoint(clientParams.endpoint);
CosmosClient.accessToken(clientParams.accessToken);
CosmosClient.databaseAccount(clientParams.databaseAccount);
self.dataExplorerPlatform = clientParams.platform;
console.log(event);
config.platform = clientParams.platform;
if (!!files && files.length > 0) {
numFiles = files.length;
for (let i = 0; i < numFiles; i++) {
@@ -87,7 +87,7 @@ function createDocumentsFromFile(fileName: string, documentContent: string): voi
numUploadsSuccessful++;
})
.catch(error => {
console.log(error);
console.error(error);
recordUploadDetailErrorForFile(fileName, JSON.stringify(error));
numUploadsFailed++;
})
@@ -106,6 +106,7 @@ function createDocumentsFromFile(fileName: string, documentContent: string): voi
triggerCreateDocument(content);
}
} catch (e) {
console.log(e);
recordUploadDetailErrorForFile(fileName, e.message);
transmitResultIfUploadComplete();
}

View File

@@ -25,7 +25,6 @@
"./src/Contracts/Versions.ts",
"./src/Controls/Heatmap/HeatmapDatatypes.ts",
"./src/Definitions/plotly.js-cartesian-dist.d-min.ts",
"./src/Explorer/Controls/Notebook/NotebookAppMessageHandler.ts",
"./src/Explorer/Controls/Toolbar/IToolbarAction.ts",
"./src/Explorer/Controls/Toolbar/IToolbarDisplayable.ts",
"./src/Explorer/Controls/Toolbar/IToolbarDropDown.ts",