mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-28 14:14:08 +00:00
Compare commits
1 Commits
MPAC-2020-
...
e2e-produc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82f7e0d4d1 |
@@ -202,6 +202,7 @@ src/Explorer/Tabs/GraphTab.ts
|
|||||||
src/Explorer/Tabs/MongoDocumentsTab.ts
|
src/Explorer/Tabs/MongoDocumentsTab.ts
|
||||||
src/Explorer/Tabs/MongoQueryTab.ts
|
src/Explorer/Tabs/MongoQueryTab.ts
|
||||||
src/Explorer/Tabs/MongoShellTab.ts
|
src/Explorer/Tabs/MongoShellTab.ts
|
||||||
|
src/Explorer/Tabs/NotebookTab.ts
|
||||||
src/Explorer/Tabs/NotebookV2Tab.ts
|
src/Explorer/Tabs/NotebookV2Tab.ts
|
||||||
src/Explorer/Tabs/QueryTab.test.ts
|
src/Explorer/Tabs/QueryTab.test.ts
|
||||||
src/Explorer/Tabs/QueryTab.ts
|
src/Explorer/Tabs/QueryTab.ts
|
||||||
@@ -215,6 +216,7 @@ src/Explorer/Tabs/TabComponents.ts
|
|||||||
src/Explorer/Tabs/TabsBase.ts
|
src/Explorer/Tabs/TabsBase.ts
|
||||||
src/Explorer/Tabs/TriggerTab.ts
|
src/Explorer/Tabs/TriggerTab.ts
|
||||||
src/Explorer/Tabs/UserDefinedFunctionTab.ts
|
src/Explorer/Tabs/UserDefinedFunctionTab.ts
|
||||||
|
src/Explorer/Tabs/__mocks__/NotebookTab.ts
|
||||||
src/Explorer/Tree/AccessibleVerticalList.ts
|
src/Explorer/Tree/AccessibleVerticalList.ts
|
||||||
src/Explorer/Tree/Collection.test.ts
|
src/Explorer/Tree/Collection.test.ts
|
||||||
src/Explorer/Tree/Collection.ts
|
src/Explorer/Tree/Collection.ts
|
||||||
@@ -341,7 +343,7 @@ src/Explorer/Controls/LibraryManagement/LibraryManageComponentAdapter.tsx
|
|||||||
src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx
|
src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx
|
||||||
src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx
|
src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx
|
||||||
src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.tsx
|
src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.tsx
|
||||||
src/NotebookViewer/NotebookViewer.tsx
|
src/Explorer/Controls/NotebookViewer/NotebookViewer.tsx
|
||||||
src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx
|
src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx
|
||||||
src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx
|
src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx
|
||||||
src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponentAdapter.tsx
|
src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponentAdapter.tsx
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ module.exports = {
|
|||||||
es6: true
|
es6: true
|
||||||
},
|
},
|
||||||
plugins: ["@typescript-eslint"],
|
plugins: ["@typescript-eslint"],
|
||||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended"
|
||||||
|
],
|
||||||
globals: {
|
globals: {
|
||||||
Atomics: "readonly",
|
Atomics: "readonly",
|
||||||
SharedArrayBuffer: "readonly"
|
SharedArrayBuffer: "readonly"
|
||||||
@@ -36,7 +40,6 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
curly: "error",
|
curly: "error"
|
||||||
"@typescript-eslint/no-unused-vars": "error"
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
88
.github/workflows/blank.yml
vendored
88
.github/workflows/blank.yml
vendored
@@ -8,41 +8,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
jobs:
|
jobs:
|
||||||
compile:
|
test:
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: "Compile TypeScript"
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Use Node.js 12.x
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: 12.x
|
|
||||||
- run: npm ci
|
|
||||||
- run: npm run compile
|
|
||||||
- run: npm run compile:strict
|
|
||||||
format:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: "Check Format"
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Use Node.js 12.x
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: 12.x
|
|
||||||
- run: npm ci
|
|
||||||
- run: npm run format:check
|
|
||||||
lint:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: "Lint"
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Use Node.js 12.x
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: 12.x
|
|
||||||
- run: npm ci
|
|
||||||
- run: npm run lint
|
|
||||||
unittest:
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: "Unit Tests"
|
name: "Unit Tests"
|
||||||
steps:
|
steps:
|
||||||
@@ -70,13 +36,13 @@ jobs:
|
|||||||
path: .cache
|
path: .cache
|
||||||
key: ${{ runner.os }}-build-cache
|
key: ${{ runner.os }}-build-cache
|
||||||
- run: npm run pack:prod
|
- run: npm run pack:prod
|
||||||
- run: cp -r ./Contracts ./dist/contracts
|
- run: npm run copyToConsumers
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
path: dist/
|
path: dist/
|
||||||
endtoendemulator:
|
endtoend:
|
||||||
name: "End To End Tests | Emulator | SQL"
|
name: "End to End Tests"
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@@ -101,36 +67,8 @@ jobs:
|
|||||||
EMULATOR_ENDPOINT: https://0.0.0.0:8081/
|
EMULATOR_ENDPOINT: https://0.0.0.0:8081/
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
||||||
endtoendsql:
|
endtoendprodcassandra:
|
||||||
name: "End To End Tests | SQL"
|
name: "End to End Tests Prod"
|
||||||
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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@@ -147,19 +85,17 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
npm start &
|
npm start &
|
||||||
cd cypress
|
npm ci --prefix ./cypress
|
||||||
npm ci
|
npm run test --prefix ./cypress
|
||||||
node cleanup.js
|
|
||||||
npm run wait-for-server
|
|
||||||
npx cypress run --browser chrome --headless --spec "./integration/dataexplorer/MONGO/*"
|
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
|
EMULATOR_ENDPOINT: https://0.0.0.0:8081/
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
||||||
CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
|
CONNECTION_STRING: ${{ secrets.CASSANDRA_CONNECTION_STRING }}
|
||||||
nuget:
|
nuget:
|
||||||
name: Publish Nuget
|
name: Publish Nuget
|
||||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendsql, endtoendmongo]
|
needs: [build, test, endtoend]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||||
@@ -180,7 +116,7 @@ jobs:
|
|||||||
path: "*.nupkg"
|
path: "*.nupkg"
|
||||||
nugetmpac:
|
nugetmpac:
|
||||||
name: Publish Nuget MPAC
|
name: Publish Nuget MPAC
|
||||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendsql, endtoendmongo]
|
needs: [build, test, endtoend]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
# Contribution guidelines to Data Explorer
|
|
||||||
|
|
||||||
This project welcomes contributions and suggestions. Most contributions require you to agree to a
|
|
||||||
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
|
|
||||||
the rights to use your contribution. For details, visit https://cla.microsoft.com.
|
|
||||||
|
|
||||||
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
|
|
||||||
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
|
|
||||||
provided by the bot. You will only need to do this once across all repos using our CLA.
|
|
||||||
|
|
||||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
|
||||||
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
|
||||||
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
|
||||||
|
|
||||||
## Microsoft Open Source Code of Conduct
|
|
||||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
|
||||||
|
|
||||||
Resources:
|
|
||||||
|
|
||||||
- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
|
|
||||||
- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
|
|
||||||
- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
|
|
||||||
|
|
||||||
## Browser support
|
|
||||||
Please make sure to support all modern browsers as well as Internet Explorer 11.
|
|
||||||
For IE support, polyfill is preferred over new usage of lodash or underscore. We already polyfill almost everything by importing babel-polyfill at the top of entry points.
|
|
||||||
|
|
||||||
|
|
||||||
## Coding guidelines, conventions and recommendations
|
|
||||||
### Typescript
|
|
||||||
* Follow this [typescript style guide](https://github.com/excelmicro/typescript) which is based on [airbnb's style guide](https://github.com/airbnb/javascript).
|
|
||||||
* Conventions speficic to this project:
|
|
||||||
* Use double-quotes for string
|
|
||||||
* Don't use null, use undefined
|
|
||||||
* Pascal case for private static readonly fields
|
|
||||||
* Camel case for classnames in markup
|
|
||||||
* Don't use class unless necessary
|
|
||||||
* Code related to notebooks should be dynamically imported so that it is loaded from a separate bundle only if the account is notebook-enabled. There are already top-level notebook components which are dynamically imported and their dependencies can be statically imported from these files.
|
|
||||||
|
|
||||||
### React
|
|
||||||
* Prefer using React class components over function components and hooks unless you have a simple component and require no nested functions:
|
|
||||||
* Nested functions may be harder to test independently
|
|
||||||
* Switching from function component to class component later mayb be painful
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
Any PR should not decrease testing coverage.
|
|
||||||
|
|
||||||
## Recommended Tools and VS Code extensions
|
|
||||||
* [Bookmarks](https://github.com/alefragnani/vscode-bookmarks)
|
|
||||||
* [Bracket pair colorizer](https://github.com/CoenraadS/Bracket-Pair-Colorizer-2)
|
|
||||||
* [GitHub Pull Requests and Issues](https://github.com/Microsoft/vscode-pull-request-github)
|
|
||||||
12
README.md
12
README.md
@@ -82,4 +82,14 @@ Unit tests are located adjacent to the code under test and run with [Jest](https
|
|||||||
|
|
||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
Please read the [contribution guidelines](./CONTRIBUTING.md).
|
This project welcomes contributions and suggestions. Most contributions require you to agree to a
|
||||||
|
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
|
||||||
|
the rights to use your contribution. For details, visit https://cla.microsoft.com.
|
||||||
|
|
||||||
|
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
|
||||||
|
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
|
||||||
|
provided by the bot. You will only need to do this once across all repos using our CLA.
|
||||||
|
|
||||||
|
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
||||||
|
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
||||||
|
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||||
|
|||||||
3
cypress/.gitignore
vendored
3
cypress/.gitignore
vendored
@@ -1,4 +1 @@
|
|||||||
cypress.env.json
|
cypress.env.json
|
||||||
cypress/report
|
|
||||||
cypress/screenshots
|
|
||||||
cypress/videos
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
// Cleans up old databases from previous test runs
|
|
||||||
const { CosmosClient } = require("@azure/cosmos");
|
|
||||||
|
|
||||||
// TODO: Add support for other API connection strings
|
|
||||||
const mongoRegex = RegExp("mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com");
|
|
||||||
|
|
||||||
async function cleanup() {
|
|
||||||
const connectionString = process.env.CYPRESS_CONNECTION_STRING;
|
|
||||||
if (!connectionString) {
|
|
||||||
throw new Error("Connection string not provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
let client;
|
|
||||||
switch (true) {
|
|
||||||
case connectionString.includes("mongodb://"): {
|
|
||||||
const [, key, accountName] = connectionString.match(mongoRegex);
|
|
||||||
client = new CosmosClient({
|
|
||||||
key,
|
|
||||||
endpoint: `https://${accountName}.documents.azure.com:443/`
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// TODO: Add support for other API connection strings
|
|
||||||
default:
|
|
||||||
client = new CosmosClient(connectionString);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await client.databases.readAll().fetchAll();
|
|
||||||
return Promise.all(
|
|
||||||
response.resources.map(async db => {
|
|
||||||
const dbTimestamp = new Date(db._ts * 1000);
|
|
||||||
const twentyMinutesAgo = new Date(Date.now() - 1000 * 60 * 20);
|
|
||||||
if (dbTimestamp < twentyMinutesAgo) {
|
|
||||||
await client.database(db.id).delete();
|
|
||||||
console.log(`DELETED: ${db.id} | Timestamp: ${dbTimestamp}`);
|
|
||||||
} else {
|
|
||||||
console.log(`SKIPPED: ${db.id} | Timestamp: ${dbTimestamp}`);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup()
|
|
||||||
.then(() => {
|
|
||||||
process.exit(0);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -16,7 +16,7 @@ let crypt = require("crypto");
|
|||||||
|
|
||||||
context("Mongo API Test - createDatabase", () => {
|
context("Mongo API Test - createDatabase", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
connectionString.loginUsingConnectionString();
|
connectionString.loginUsingConnectionString(connectionString.constants.mongo);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Create a new collection in Mongo API", () => {
|
it("Create a new collection in Mongo API", () => {
|
||||||
@@ -63,7 +63,7 @@ context("Mongo API Test - createDatabase", () => {
|
|||||||
.type(sharedKey);
|
.type(sharedKey);
|
||||||
|
|
||||||
cy.wrap($body)
|
cy.wrap($body)
|
||||||
.find("#submitBtnAddCollection")
|
.find('input[data-test="addCollection-createCollection"]')
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
cy.wait(10000);
|
cy.wait(10000);
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ let crypt = require("crypto");
|
|||||||
|
|
||||||
context("Mongo API Test", () => {
|
context("Mongo API Test", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
connectionString.loginUsingConnectionString();
|
connectionString.loginUsingConnectionString(connectionString.constants.mongo);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skip("Create a new collection in Mongo API - Autopilot", () => {
|
it("Create a new collection in Mongo API - Autopilot", () => {
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ let crypt = require("crypto");
|
|||||||
|
|
||||||
context("Mongo API Test", () => {
|
context("Mongo API Test", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
connectionString.loginUsingConnectionString();
|
connectionString.loginUsingConnectionString(connectionString.constants.mongo);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skip("Create a new collection in existing database in Mongo API", () => {
|
it("Create a new collection in existing database in Mongo API", () => {
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ const connectionString = require("../../../utilities/connectionString");
|
|||||||
|
|
||||||
let crypt = require("crypto");
|
let crypt = require("crypto");
|
||||||
|
|
||||||
context.skip("Mongo API Test", () => {
|
context("Mongo API Test", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
connectionString.loginUsingConnectionString();
|
connectionString.loginUsingConnectionString(connectionString.constants.mongo);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Create a new collection in Mongo API - Provision database throughput", () => {
|
it("Create a new collection in Mongo API - Provision database throughput", () => {
|
||||||
|
|||||||
@@ -16,14 +16,13 @@ let crypt = require("crypto");
|
|||||||
|
|
||||||
context("SQL API Test", () => {
|
context("SQL API Test", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
connectionString.loginUsingConnectionString();
|
connectionString.loginUsingConnectionString(connectionString.constants.sql);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Create a new container in SQL API", () => {
|
it("Create a new container in SQL API", () => {
|
||||||
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
|
||||||
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
|
||||||
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
|
||||||
connectionString.loginUsingConnectionString();
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
cy.get("iframe").then($element => {
|
||||||
const $body = $element.contents().find("body");
|
const $body = $element.contents().find("body");
|
||||||
@@ -64,7 +63,7 @@ context("SQL API Test", () => {
|
|||||||
.type(sharedKey);
|
.type(sharedKey);
|
||||||
|
|
||||||
cy.wrap($body)
|
cy.wrap($body)
|
||||||
.find("#submitBtnAddCollection")
|
.find('input[data-test="addCollection-createCollection"]')
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
cy.wait(10000);
|
cy.wait(10000);
|
||||||
|
|||||||
110
cypress/package-lock.json
generated
110
cypress/package-lock.json
generated
@@ -273,12 +273,77 @@
|
|||||||
"any-observable": "^0.3.0"
|
"any-observable": "^0.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/sinonjs__fake-timers": {
|
"@types/blob-util": {
|
||||||
"version": "6.0.1",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/blob-util/-/blob-util-1.3.3.tgz",
|
||||||
"integrity": "sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA==",
|
"integrity": "sha512-4ahcL/QDnpjWA2Qs16ZMQif7HjGP2cw3AGjHabybjw7Vm1EKu+cfQN1D78BaZbS1WJNa1opSMF5HNMztx7lR0w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/bluebird": {
|
||||||
|
"version": "3.5.29",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.29.tgz",
|
||||||
|
"integrity": "sha512-kmVtnxTuUuhCET669irqQmPAez4KFnFVKvpleVRyfC3g+SHD1hIkFZcWLim9BVcwUBLO59o8VZE4yGCmTif8Yw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@types/chai": {
|
||||||
|
"version": "4.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.7.tgz",
|
||||||
|
"integrity": "sha512-luq8meHGYwvky0O7u0eQZdA7B4Wd9owUCqvbw2m3XCrCU8mplYOujMBbvyS547AxJkC+pGnd0Cm15eNxEUNU8g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@types/chai-jquery": {
|
||||||
|
"version": "1.1.40",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/chai-jquery/-/chai-jquery-1.1.40.tgz",
|
||||||
|
"integrity": "sha512-mCNEZ3GKP7T7kftKeIs7QmfZZQM7hslGSpYzKbOlR2a2HCFf9ph4nlMRA9UnuOETeOQYJVhJQK7MwGqNZVyUtQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/chai": "*",
|
||||||
|
"@types/jquery": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/jquery": {
|
||||||
|
"version": "3.3.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.31.tgz",
|
||||||
|
"integrity": "sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/sizzle": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/lodash": {
|
||||||
|
"version": "4.14.149",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz",
|
||||||
|
"integrity": "sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@types/minimatch": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@types/mocha": {
|
||||||
|
"version": "5.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz",
|
||||||
|
"integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@types/sinon": {
|
||||||
|
"version": "7.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.5.1.tgz",
|
||||||
|
"integrity": "sha512-EZQUP3hSZQyTQRfiLqelC9NMWd1kqLcmQE0dMiklxBkgi84T+cHOhnKpgk4NnOWpGX863yE6+IaGnOXUNFqDnQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@types/sinon-chai": {
|
||||||
|
"version": "3.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.3.tgz",
|
||||||
|
"integrity": "sha512-TOUFS6vqS0PVL1I8NGVSNcFaNJtFoyZPXZ5zur+qlhDfOmQECZZM4H4kKgca6O8L+QceX/ymODZASfUfn+y4yQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/chai": "*",
|
||||||
|
"@types/sinon": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/sizzle": {
|
"@types/sizzle": {
|
||||||
"version": "2.3.2",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz",
|
||||||
@@ -762,15 +827,24 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cypress": {
|
"cypress": {
|
||||||
"version": "4.8.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-4.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/cypress/-/cypress-4.5.0.tgz",
|
||||||
"integrity": "sha512-Lsff8lF8pq6k/ioNua783tCsxGSLp6gqGXecdIfqCkqjYiOA53XKuEf1CaQJLUVs1dHSf49eDUp/sb620oJjVQ==",
|
"integrity": "sha512-2A4g5FW5d2fHzq8HKUGAMVTnW6P8nlWYQALiCoGN4bqBLvgwhYM/oG9oKc2CS6LnvgHFiKivKzpm9sfk3uU3zQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@cypress/listr-verbose-renderer": "0.4.1",
|
"@cypress/listr-verbose-renderer": "0.4.1",
|
||||||
"@cypress/request": "2.88.5",
|
"@cypress/request": "2.88.5",
|
||||||
"@cypress/xvfb": "1.2.4",
|
"@cypress/xvfb": "1.2.4",
|
||||||
"@types/sinonjs__fake-timers": "6.0.1",
|
"@types/blob-util": "1.3.3",
|
||||||
|
"@types/bluebird": "3.5.29",
|
||||||
|
"@types/chai": "4.2.7",
|
||||||
|
"@types/chai-jquery": "1.1.40",
|
||||||
|
"@types/jquery": "3.3.31",
|
||||||
|
"@types/lodash": "4.14.149",
|
||||||
|
"@types/minimatch": "3.0.3",
|
||||||
|
"@types/mocha": "5.2.7",
|
||||||
|
"@types/sinon": "7.5.1",
|
||||||
|
"@types/sinon-chai": "3.2.3",
|
||||||
"@types/sizzle": "2.3.2",
|
"@types/sizzle": "2.3.2",
|
||||||
"arch": "2.1.1",
|
"arch": "2.1.1",
|
||||||
"bluebird": "3.7.2",
|
"bluebird": "3.7.2",
|
||||||
@@ -804,6 +878,14 @@
|
|||||||
"untildify": "4.0.0",
|
"untildify": "4.0.0",
|
||||||
"url": "0.11.0",
|
"url": "0.11.0",
|
||||||
"yauzl": "2.10.0"
|
"yauzl": "2.10.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"minimist": {
|
||||||
|
"version": "1.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||||
|
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dashdash": {
|
"dashdash": {
|
||||||
@@ -999,6 +1081,12 @@
|
|||||||
"ms": "2.0.0"
|
"ms": "2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"minimist": {
|
||||||
|
"version": "1.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||||
|
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"mkdirp": {
|
"mkdirp": {
|
||||||
"version": "0.5.5",
|
"version": "0.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
||||||
@@ -1771,12 +1859,6 @@
|
|||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"minimist": {
|
|
||||||
"version": "1.2.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
|
||||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"mkdirp": {
|
"mkdirp": {
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
||||||
|
|||||||
@@ -5,13 +5,11 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "cypress run",
|
"test": "cypress run",
|
||||||
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
|
|
||||||
"test:sql": "cypress run --browser chrome --headless --spec \"./integration/dataexplorer/SQL/*\"",
|
|
||||||
"test:ci": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/ https-get://0.0.0.0:8081/_explorer/index.html && cypress run --browser chrome --headless",
|
"test:ci": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/ https-get://0.0.0.0:8081/_explorer/index.html && cypress run --browser chrome --headless",
|
||||||
"test:debug": "cypress open"
|
"test:debug": "cypress open"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cypress": "^4.8.0",
|
"cypress": "^4.5.0",
|
||||||
"mocha": "^7.0.1",
|
"mocha": "^7.0.1",
|
||||||
"mochawesome": "^4.1.0",
|
"mochawesome": "^4.1.0",
|
||||||
"mochawesome-merge": "^4.0.1",
|
"mochawesome-merge": "^4.0.1",
|
||||||
|
|||||||
@@ -1,41 +1,56 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
loginUsingConnectionString: function() {
|
loginUsingConnectionString: function (api) {
|
||||||
const prodUrl = Cypress.env("TEST_ENDPOINT") || "https://localhost:1234/hostedExplorer.html";
|
|
||||||
const timeout = 15000;
|
|
||||||
|
|
||||||
cy.visit(prodUrl);
|
|
||||||
cy.get('iframe[id="explorerMenu"]').should("be.visible");
|
|
||||||
|
|
||||||
cy.get("iframe").then($element => {
|
const prodUrl = "https://cosmos.azure.com/";
|
||||||
const $body = $element.contents().find("body");
|
const timeout = 15000;
|
||||||
|
|
||||||
cy.wrap($body)
|
cy.visit(prodUrl);
|
||||||
.find("#connectExplorer")
|
cy.get('iframe[id="explorerMenu"]').should("be.visible");
|
||||||
.should("exist")
|
|
||||||
.find("div[class='connectExplorer']")
|
|
||||||
.should("exist")
|
|
||||||
.find("p[class='welcomeText']")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
cy.wrap($body.find("div > p.switchConnectTypeText"))
|
cy.get("iframe").then($element => {
|
||||||
.should("exist")
|
const $body = $element.contents().find("body");
|
||||||
.last()
|
|
||||||
.click({ force: true });
|
|
||||||
|
|
||||||
const secret = Cypress.env("CONNECTION_STRING");
|
cy.wrap($body)
|
||||||
|
.find("#connectExplorer")
|
||||||
|
.should("exist")
|
||||||
|
.find("div[class='connectExplorer']")
|
||||||
|
.should("exist")
|
||||||
|
.find("p[class='welcomeText']")
|
||||||
|
.should("exist");
|
||||||
|
|
||||||
cy.wrap($body)
|
cy.wrap($body.find("div > p.switchConnectTypeText"))
|
||||||
.find("input[class='inputToken']")
|
.should("exist")
|
||||||
.should("exist")
|
.last()
|
||||||
.type(secret, {
|
.click({ force: true });
|
||||||
force: true
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.wrap($body.find("input[value='Connect']"), { timeout })
|
const secret = Cypress.env('connectionString')[api];
|
||||||
.first()
|
|
||||||
.click({ force: true });
|
|
||||||
|
|
||||||
cy.wait(15000);
|
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");
|
||||||
|
|
||||||
|
});
|
||||||
|
},
|
||||||
|
constants:{
|
||||||
|
sql: "sql",
|
||||||
|
mongo: "mongo",
|
||||||
|
table: "table",
|
||||||
|
graph: "graph",
|
||||||
|
cassandra: "cassandra"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -570,12 +570,6 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fileImportButton {
|
|
||||||
height: 24px;
|
|
||||||
border: @ButtonBorderWidth solid transparent;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fileUploadSummaryContainer {
|
.fileUploadSummaryContainer {
|
||||||
margin-top: 40px;
|
margin-top: 40px;
|
||||||
|
|
||||||
@@ -1022,18 +1016,6 @@ menuQuickStart {
|
|||||||
background: #262626;
|
background: #262626;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panelContent {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panelContentWrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contextual-pane {
|
.contextual-pane {
|
||||||
top: 0px;
|
top: 0px;
|
||||||
right: 0 !important;
|
right: 0 !important;
|
||||||
@@ -1250,25 +1232,23 @@ menuQuickStart {
|
|||||||
padding: 2px 30px;
|
padding: 2px 30px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
||||||
&:active {
|
|
||||||
border-color: #0072c6;
|
|
||||||
background-color: #0072c6;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.leftpanel-okbut .genericPaneSubmitBtn {
|
.btncreatecoll1:hover {
|
||||||
border: 1px solid @AccentMediumHigh;
|
background: @AccentMediumHigh;
|
||||||
background-color: @AccentMediumHigh;
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
border-color: @AccentMediumHigh;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
height: 24px;
|
}
|
||||||
|
|
||||||
&:active {
|
.btncreatecoll1:active {
|
||||||
border-color: #0072c6;
|
border: 1px solid #0072c6;
|
||||||
background-color: #0072c6;
|
background-color: #0072c6;
|
||||||
}
|
color: white;
|
||||||
|
padding: 2px 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btncreatecoll1-off {
|
.btncreatecoll1-off {
|
||||||
@@ -1381,15 +1361,6 @@ p {
|
|||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.headerline .closePaneBtn {
|
|
||||||
float: right;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 16px;
|
|
||||||
height: 100%;
|
|
||||||
margin-right: 4px;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.closeImg {
|
.closeImg {
|
||||||
float: right;
|
float: right;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -1739,13 +1710,6 @@ input::-webkit-calendar-picker-indicator {
|
|||||||
margin: (2 * @MediumSpace) 0px;
|
margin: (2 * @MediumSpace) 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contextual-pane .panelMainContent {
|
|
||||||
padding-left: 34px;
|
|
||||||
padding-right: 34px;
|
|
||||||
color: @BaseDark;
|
|
||||||
margin: (2 * @MediumSpace) 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contextual-pane .paneFooter {
|
.contextual-pane .paneFooter {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
|
|||||||
908
package-lock.json
generated
908
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@@ -4,10 +4,11 @@
|
|||||||
"description": "Cosmos Explorer",
|
"description": "Cosmos Explorer",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/cosmos": "3.7.1",
|
"@azure/cosmos": "3.6.3",
|
||||||
"@azure/cosmos-language-service": "0.0.4",
|
"@azure/cosmos-language-service": "0.0.4",
|
||||||
"@jupyterlab/services": "4.2.0",
|
"@jupyterlab/services": "4.2.0",
|
||||||
"@jupyterlab/terminal": "1.2.1",
|
"@jupyterlab/terminal": "1.2.1",
|
||||||
|
"@material-ui/core": "4.9.10",
|
||||||
"@microsoft/applicationinsights-web": "2.5.4",
|
"@microsoft/applicationinsights-web": "2.5.4",
|
||||||
"@nteract/commutable": "7.1.4",
|
"@nteract/commutable": "7.1.4",
|
||||||
"@nteract/connected-components": "6.7.8",
|
"@nteract/connected-components": "6.7.8",
|
||||||
@@ -32,7 +33,7 @@
|
|||||||
"@nteract/transform-plotly": "6.1.6",
|
"@nteract/transform-plotly": "6.1.6",
|
||||||
"@nteract/transform-vdom": "4.0.11",
|
"@nteract/transform-vdom": "4.0.11",
|
||||||
"@nteract/transform-vega": "7.0.6",
|
"@nteract/transform-vega": "7.0.6",
|
||||||
"@octokit/rest": "17.9.2",
|
"@octokit/rest": "17.5.1",
|
||||||
"@phosphor/widgets": "1.9.3",
|
"@phosphor/widgets": "1.9.3",
|
||||||
"@uifabric/react-cards": "0.109.53",
|
"@uifabric/react-cards": "0.109.53",
|
||||||
"@uifabric/styling": "7.11.2",
|
"@uifabric/styling": "7.11.2",
|
||||||
@@ -50,7 +51,7 @@
|
|||||||
"dayjs": "1.8.19",
|
"dayjs": "1.8.19",
|
||||||
"es6-object-assign": "1.1.0",
|
"es6-object-assign": "1.1.0",
|
||||||
"es6-symbol": "3.1.3",
|
"es6-symbol": "3.1.3",
|
||||||
"eslint-plugin-jest": "23.13.2",
|
"eslint-plugin-jest": "23.8.2",
|
||||||
"hasher": "1.2.0",
|
"hasher": "1.2.0",
|
||||||
"immutable": "4.0.0-rc.12",
|
"immutable": "4.0.0-rc.12",
|
||||||
"jquery": "3.4.0",
|
"jquery": "3.4.0",
|
||||||
@@ -110,8 +111,8 @@
|
|||||||
"@types/text-encoding": "0.0.33",
|
"@types/text-encoding": "0.0.33",
|
||||||
"@types/underscore": "1.7.36",
|
"@types/underscore": "1.7.36",
|
||||||
"@types/webfontloader": "1.6.29",
|
"@types/webfontloader": "1.6.29",
|
||||||
"@typescript-eslint/eslint-plugin": "3.2.0",
|
"@typescript-eslint/eslint-plugin": "2.25.0",
|
||||||
"@typescript-eslint/parser": "3.2.0",
|
"@typescript-eslint/parser": "2.25.0",
|
||||||
"adal-angular": "1.0.15",
|
"adal-angular": "1.0.15",
|
||||||
"babel-jest": "24.9.0",
|
"babel-jest": "24.9.0",
|
||||||
"babel-loader": "8.1.0",
|
"babel-loader": "8.1.0",
|
||||||
@@ -123,9 +124,9 @@
|
|||||||
"enzyme": "3.10.0",
|
"enzyme": "3.10.0",
|
||||||
"enzyme-adapter-react-16": "1.15.1",
|
"enzyme-adapter-react-16": "1.15.1",
|
||||||
"enzyme-to-json": "3.4.3",
|
"enzyme-to-json": "3.4.3",
|
||||||
"eslint": "7.2.0",
|
"eslint": "6.8.0",
|
||||||
"eslint-cli": "1.1.1",
|
"eslint-cli": "1.1.1",
|
||||||
"eslint-plugin-react": "7.20.0",
|
"eslint-plugin-react": "7.19.0",
|
||||||
"expose-loader": "0.7.5",
|
"expose-loader": "0.7.5",
|
||||||
"file-loader": "2.0.0",
|
"file-loader": "2.0.0",
|
||||||
"fs-extra": "7.0.0",
|
"fs-extra": "7.0.0",
|
||||||
@@ -155,7 +156,7 @@
|
|||||||
"webpack": "4.41.2",
|
"webpack": "4.41.2",
|
||||||
"webpack-bundle-analyzer": "3.6.1",
|
"webpack-bundle-analyzer": "3.6.1",
|
||||||
"webpack-cli": "3.3.10",
|
"webpack-cli": "3.3.10",
|
||||||
"webpack-dev-server": "3.11.0",
|
"webpack-dev-server": "3.9.0",
|
||||||
"worker-loader": "2.0.0"
|
"worker-loader": "2.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -104,9 +104,11 @@ export class CapabilityNames {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Features {
|
export class Features {
|
||||||
|
public static readonly graphs = "graphs";
|
||||||
public static readonly cosmosdb = "cosmosdb";
|
public static readonly cosmosdb = "cosmosdb";
|
||||||
public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy";
|
public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy";
|
||||||
public static readonly enableRupm = "enablerupm";
|
public static readonly enableRupm = "enablerupm";
|
||||||
|
public static readonly cacheOptimizations = "dataexplorercacheoptimizations";
|
||||||
public static readonly executeSproc = "dataexplorerexecutesproc";
|
public static readonly executeSproc = "dataexplorerexecutesproc";
|
||||||
public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
|
public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
|
||||||
public static readonly enableTtl = "enablettl";
|
public static readonly enableTtl = "enablettl";
|
||||||
@@ -114,14 +116,17 @@ export class Features {
|
|||||||
public static readonly enableGallery = "enablegallery";
|
public static readonly enableGallery = "enablegallery";
|
||||||
public static readonly enableSpark = "enablespark";
|
public static readonly enableSpark = "enablespark";
|
||||||
public static readonly livyEndpoint = "livyendpoint";
|
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 notebookServerUrl = "notebookserverurl";
|
||||||
public static readonly notebookServerToken = "notebookservertoken";
|
public static readonly notebookServerToken = "notebookservertoken";
|
||||||
public static readonly notebookBasePath = "notebookbasepath";
|
public static readonly notebookBasePath = "notebookbasepath";
|
||||||
|
public static readonly enableLegacyResourceTree = "enablelegacyresourcetree";
|
||||||
public static readonly canExceedMaximumValue = "canexceedmaximumvalue";
|
public static readonly canExceedMaximumValue = "canexceedmaximumvalue";
|
||||||
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
|
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
|
||||||
public static readonly enableAutoPilotV2 = "enableautopilotv2";
|
public static readonly enableAutoPilotV2 = "enableautopilotv2";
|
||||||
public static readonly ttl90Days = "ttl90days";
|
public static readonly ttl90Days = "ttl90days";
|
||||||
public static readonly enableRightPanelV2 = "enablerightpanelv2";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AfecFeatures {
|
export class AfecFeatures {
|
||||||
|
|||||||
@@ -21,15 +21,13 @@ const _global = typeof self === "undefined" ? window : self;
|
|||||||
export const tokenProvider = async (requestInfo: RequestInfo) => {
|
export const tokenProvider = async (requestInfo: RequestInfo) => {
|
||||||
const { verb, resourceId, resourceType, headers } = requestInfo;
|
const { verb, resourceId, resourceType, headers } = requestInfo;
|
||||||
if (config.platform === Platform.Emulator) {
|
if (config.platform === Platform.Emulator) {
|
||||||
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
|
// TODO Remove any. SDK expects a return value for tokenProvider, but we are mutating the header object instead.
|
||||||
await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
return setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey) as any;
|
||||||
return decodeURIComponent(headers.authorization);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_masterKey) {
|
if (_masterKey) {
|
||||||
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
|
// TODO Remove any. SDK expects a return value for tokenProvider, but we are mutating the header object instead.
|
||||||
await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
return setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, _masterKey) as any;
|
||||||
return decodeURIComponent(headers.authorization);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_resourceToken) {
|
if (_resourceToken) {
|
||||||
@@ -49,9 +47,7 @@ export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, next) =>
|
|||||||
|
|
||||||
export const endpoint = () => {
|
export const endpoint = () => {
|
||||||
if (config.platform === Platform.Emulator) {
|
if (config.platform === Platform.Emulator) {
|
||||||
// In worker scope, _global(self).parent does not exist
|
return config.EMULATOR_ENDPOINT || window.parent.location.origin;
|
||||||
const location = _global.parent ? _global.parent.location : _global.location;
|
|
||||||
return config.EMULATOR_ENDPOINT || location.origin;
|
|
||||||
}
|
}
|
||||||
return _endpoint || (_databaseAccount && _databaseAccount.properties && _databaseAccount.properties.documentEndpoint);
|
return _endpoint || (_databaseAccount && _databaseAccount.properties && _databaseAccount.properties.documentEndpoint);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { SeverityLevel } from "@microsoft/applicationinsights-web";
|
|||||||
|
|
||||||
// TODO: Move to a separate Diagnostics folder
|
// TODO: Move to a separate Diagnostics folder
|
||||||
export class Logger {
|
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 {
|
public static logInfo(message: string | Record<string, any>, area: string, code?: number): void {
|
||||||
let logMessage: string;
|
let logMessage: string;
|
||||||
if (typeof message === "string") {
|
if (typeof message === "string") {
|
||||||
|
|||||||
@@ -738,8 +738,6 @@ export interface GitHubInfoJunoResponse {
|
|||||||
gitUrl: string;
|
gitUrl: string;
|
||||||
htmlUrl: string;
|
htmlUrl: string;
|
||||||
metadata?: NotebookMetadata;
|
metadata?: NotebookMetadata;
|
||||||
officialSamplesIndex?: number;
|
|
||||||
isLikedNotebook?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LikedNotebooksJunoResponse {
|
export interface LikedNotebooksJunoResponse {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { AccessibleVerticalList } from "../Explorer/Tree/AccessibleVerticalList"
|
|||||||
import { ArcadiaWorkspaceItem } from "../Explorer/Controls/Arcadia/ArcadiaMenuPicker";
|
import { ArcadiaWorkspaceItem } from "../Explorer/Controls/Arcadia/ArcadiaMenuPicker";
|
||||||
import { CassandraTableKey, CassandraTableKeys, TableDataClient } from "../Explorer/Tables/TableDataClient";
|
import { CassandraTableKey, CassandraTableKeys, TableDataClient } from "../Explorer/Tables/TableDataClient";
|
||||||
import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||||
|
import { CommandButtonOptions } from "../Explorer/Controls/CommandButton/CommandButton";
|
||||||
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { ExecuteSprocParam } from "../Explorer/Panes/ExecuteSprocParamsPane";
|
import { ExecuteSprocParam } from "../Explorer/Panes/ExecuteSprocParamsPane";
|
||||||
import { GitHubClient } from "../GitHub/GitHubClient";
|
import { GitHubClient } from "../GitHub/GitHubClient";
|
||||||
@@ -26,7 +27,6 @@ import { Splitter } from "../Common/Splitter";
|
|||||||
import { StringInputPane } from "../Explorer/Panes/StringInputPane";
|
import { StringInputPane } from "../Explorer/Panes/StringInputPane";
|
||||||
import { TextFieldProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent";
|
import { TextFieldProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent";
|
||||||
import { UploadDetails } from "../workers/upload/definitions";
|
import { UploadDetails } from "../workers/upload/definitions";
|
||||||
import { UploadItemsPaneAdapter } from "../Explorer/Panes/UploadItemsPaneAdapter";
|
|
||||||
|
|
||||||
export interface ExplorerOptions {
|
export interface ExplorerOptions {
|
||||||
documentClientUtility: DocumentClientUtilityBase;
|
documentClientUtility: DocumentClientUtilityBase;
|
||||||
@@ -86,7 +86,7 @@ export interface Explorer {
|
|||||||
isFeatureEnabled: (feature: string) => boolean;
|
isFeatureEnabled: (feature: string) => boolean;
|
||||||
isGalleryEnabled: ko.Computed<boolean>;
|
isGalleryEnabled: ko.Computed<boolean>;
|
||||||
isGitHubPaneEnabled: ko.Observable<boolean>;
|
isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||||
isRightPanelV2Enabled: ko.Computed<boolean>;
|
isGraphsEnabled: ko.Computed<boolean>;
|
||||||
canExceedMaximumValue: ko.Computed<boolean>;
|
canExceedMaximumValue: ko.Computed<boolean>;
|
||||||
hasAutoPilotV2FeatureFlag: ko.Computed<boolean>;
|
hasAutoPilotV2FeatureFlag: ko.Computed<boolean>;
|
||||||
isHostedDataExplorerEnabled: ko.Computed<boolean>;
|
isHostedDataExplorerEnabled: ko.Computed<boolean>;
|
||||||
@@ -141,7 +141,6 @@ export interface Explorer {
|
|||||||
executeSprocParamsPane: ExecuteSprocParamsPane;
|
executeSprocParamsPane: ExecuteSprocParamsPane;
|
||||||
renewAdHocAccessPane: RenewAdHocAccessPane;
|
renewAdHocAccessPane: RenewAdHocAccessPane;
|
||||||
uploadItemsPane: UploadItemsPane;
|
uploadItemsPane: UploadItemsPane;
|
||||||
uploadItemsPaneAdapter: UploadItemsPaneAdapter;
|
|
||||||
loadQueryPane: LoadQueryPane;
|
loadQueryPane: LoadQueryPane;
|
||||||
saveQueryPane: ContextualPane;
|
saveQueryPane: ContextualPane;
|
||||||
browseQueriesPane: BrowseQueriesPane;
|
browseQueriesPane: BrowseQueriesPane;
|
||||||
@@ -230,12 +229,7 @@ export interface Explorer {
|
|||||||
importAndOpenFromGallery: (path: string, newName: string, content: any) => Promise<boolean>;
|
importAndOpenFromGallery: (path: string, newName: string, content: any) => Promise<boolean>;
|
||||||
openNotebookTerminal: (kind: TerminalKind) => void;
|
openNotebookTerminal: (kind: TerminalKind) => void;
|
||||||
openGallery: () => void;
|
openGallery: () => void;
|
||||||
openNotebookViewer: (
|
openNotebookViewer: (notebookUrl: string, notebookMetadata: DataModels.NotebookMetadata) => void;
|
||||||
notebookUrl: string,
|
|
||||||
notebookMetadata: DataModels.NotebookMetadata,
|
|
||||||
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
|
|
||||||
isLikedNotebook: boolean
|
|
||||||
) => void;
|
|
||||||
notebookWorkspaceManager: NotebookWorkspaceManager;
|
notebookWorkspaceManager: NotebookWorkspaceManager;
|
||||||
sparkClusterManager: SparkClusterManager;
|
sparkClusterManager: SparkClusterManager;
|
||||||
notebookContentProvider: IContentProvider;
|
notebookContentProvider: IContentProvider;
|
||||||
@@ -338,6 +332,17 @@ export interface Button {
|
|||||||
isSelected?: ko.Computed<boolean>;
|
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 {
|
export interface NotificationConsole {
|
||||||
filteredConsoleData: ko.ObservableArray<ConsoleData>;
|
filteredConsoleData: ko.ObservableArray<ConsoleData>;
|
||||||
isConsoleExpanded: ko.Observable<boolean>;
|
isConsoleExpanded: ko.Observable<boolean>;
|
||||||
@@ -361,6 +366,7 @@ export interface TreeNode {
|
|||||||
id: ko.Observable<string>;
|
id: ko.Observable<string>;
|
||||||
database?: Database;
|
database?: Database;
|
||||||
collection?: Collection;
|
collection?: Collection;
|
||||||
|
contextMenu?: ContextMenu;
|
||||||
|
|
||||||
onNewQueryClick?(source: any, event: MouseEvent): void;
|
onNewQueryClick?(source: any, event: MouseEvent): void;
|
||||||
onNewStoredProcedureClick?(source: Collection, event: MouseEvent): void;
|
onNewStoredProcedureClick?(source: Collection, event: MouseEvent): void;
|
||||||
@@ -525,7 +531,7 @@ export interface StoredProcedure extends TreeNode {
|
|||||||
id: ko.Observable<string>;
|
id: ko.Observable<string>;
|
||||||
body: ko.Observable<string>;
|
body: ko.Observable<string>;
|
||||||
|
|
||||||
delete(): void;
|
delete(source: TreeNode, event: MouseEvent | KeyboardEvent): void;
|
||||||
open: () => void;
|
open: () => void;
|
||||||
select(): void;
|
select(): void;
|
||||||
execute(params: string[], partitionKeyValue?: string): void;
|
execute(params: string[], partitionKeyValue?: string): void;
|
||||||
@@ -539,7 +545,7 @@ export interface UserDefinedFunction extends TreeNode {
|
|||||||
id: ko.Observable<string>;
|
id: ko.Observable<string>;
|
||||||
body: ko.Observable<string>;
|
body: ko.Observable<string>;
|
||||||
|
|
||||||
delete(): void;
|
delete(source: TreeNode, event: MouseEvent | KeyboardEvent): void;
|
||||||
open: () => void;
|
open: () => void;
|
||||||
select(): void;
|
select(): void;
|
||||||
}
|
}
|
||||||
@@ -554,7 +560,7 @@ export interface Trigger extends TreeNode {
|
|||||||
triggerType: ko.Observable<string>;
|
triggerType: ko.Observable<string>;
|
||||||
triggerOperation: ko.Observable<string>;
|
triggerOperation: ko.Observable<string>;
|
||||||
|
|
||||||
delete(): void;
|
delete(source: TreeNode, event: MouseEvent | KeyboardEvent): void;
|
||||||
open: () => void;
|
open: () => void;
|
||||||
select(): void;
|
select(): void;
|
||||||
}
|
}
|
||||||
@@ -881,8 +887,6 @@ export interface NotebookViewerTabOptions extends TabOptions {
|
|||||||
notebookUrl: string;
|
notebookUrl: string;
|
||||||
notebookName: string;
|
notebookName: string;
|
||||||
notebookMetadata: DataModels.NotebookMetadata;
|
notebookMetadata: DataModels.NotebookMetadata;
|
||||||
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>;
|
|
||||||
isLikedNotebook: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentsTabOptions extends TabOptions {
|
export interface DocumentsTabOptions extends TabOptions {
|
||||||
@@ -1163,6 +1167,7 @@ export interface TriggerTab extends ScriptTab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface GraphTab extends Tab {}
|
export interface GraphTab extends Tab {}
|
||||||
|
export interface NotebookTab extends Tab {}
|
||||||
export interface EditorPosition {
|
export interface EditorPosition {
|
||||||
line: number;
|
line: number;
|
||||||
column: number;
|
column: number;
|
||||||
@@ -1206,7 +1211,7 @@ export enum CollectionTabKind {
|
|||||||
MongoShell = 10,
|
MongoShell = 10,
|
||||||
DatabaseSettings = 11,
|
DatabaseSettings = 11,
|
||||||
Conflicts = 12,
|
Conflicts = 12,
|
||||||
Notebook = 13 /* Deprecated */,
|
Notebook = 13,
|
||||||
Terminal = 14,
|
Terminal = 14,
|
||||||
NotebookV2 = 15,
|
NotebookV2 = 15,
|
||||||
SparkMasterTab = 16,
|
SparkMasterTab = 16,
|
||||||
@@ -1220,6 +1225,17 @@ export enum TerminalKind {
|
|||||||
Cassandra = 2
|
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 {
|
export interface DataExplorerInputsFrame {
|
||||||
databaseAccount: any;
|
databaseAccount: any;
|
||||||
subscriptionId: string;
|
subscriptionId: string;
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import * as ko from "knockout";
|
|||||||
import "./ComponentRegisterer";
|
import "./ComponentRegisterer";
|
||||||
|
|
||||||
describe("Component Registerer", () => {
|
describe("Component Registerer", () => {
|
||||||
|
it("should register command-button component", () => {
|
||||||
|
expect(ko.components.isRegistered("command-button")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("should register input-typeahead component", () => {
|
it("should register input-typeahead component", () => {
|
||||||
expect(ko.components.isRegistered("input-typeahead")).toBe(true);
|
expect(ko.components.isRegistered("input-typeahead")).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -60,6 +64,10 @@ describe("Component Registerer", () => {
|
|||||||
expect(ko.components.isRegistered("graph-tab")).toBe(true);
|
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", () => {
|
it("should register notebookv2-tab component", () => {
|
||||||
expect(ko.components.isRegistered("notebookv2-tab")).toBe(true);
|
expect(ko.components.isRegistered("notebookv2-tab")).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -76,6 +84,30 @@ describe("Component Registerer", () => {
|
|||||||
expect(ko.components.isRegistered("mongo-shell-tab")).toBe(true);
|
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", () => {
|
it("should registeradd-collection-pane component", () => {
|
||||||
expect(ko.components.isRegistered("add-collection-pane")).toBe(true);
|
expect(ko.components.isRegistered("add-collection-pane")).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -124,6 +156,10 @@ describe("Component Registerer", () => {
|
|||||||
expect(ko.components.isRegistered("manage-spark-cluster-pane")).toBe(true);
|
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", () => {
|
it("should register dynamic-list component", () => {
|
||||||
expect(ko.components.isRegistered("dynamic-list")).toBe(true);
|
expect(ko.components.isRegistered("dynamic-list")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as PaneComponents from "./Panes/PaneComponents";
|
import * as PaneComponents from "./Panes/PaneComponents";
|
||||||
import * as TabComponents from "./Tabs/TabComponents";
|
import * as TabComponents from "./Tabs/TabComponents";
|
||||||
|
import * as TreeComponents from "./Tree/TreeComponents";
|
||||||
import { CollapsiblePanelComponent } from "./Controls/CollapsiblePanel/CollapsiblePanelComponent";
|
import { CollapsiblePanelComponent } from "./Controls/CollapsiblePanel/CollapsiblePanelComponent";
|
||||||
|
import { CommandButtonComponent } from "./Controls/CommandButton/CommandButton";
|
||||||
import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent";
|
import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent";
|
||||||
import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent";
|
import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent";
|
||||||
import { EditorComponent } from "./Controls/Editor/EditorComponent";
|
import { EditorComponent } from "./Controls/Editor/EditorComponent";
|
||||||
@@ -14,6 +16,7 @@ import { ThroughputInputComponent } from "./Controls/ThroughputInput/ThroughputI
|
|||||||
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
|
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
|
||||||
import { ToolbarComponent } from "./Controls/Toolbar/Toolbar";
|
import { ToolbarComponent } from "./Controls/Toolbar/Toolbar";
|
||||||
|
|
||||||
|
ko.components.register("command-button", CommandButtonComponent);
|
||||||
ko.components.register("toolbar", new ToolbarComponent());
|
ko.components.register("toolbar", new ToolbarComponent());
|
||||||
ko.components.register("input-typeahead", new InputTypeaheadComponent());
|
ko.components.register("input-typeahead", new InputTypeaheadComponent());
|
||||||
ko.components.register("new-vertex-form", NewVertexComponent);
|
ko.components.register("new-vertex-form", NewVertexComponent);
|
||||||
@@ -39,6 +42,7 @@ ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab());
|
|||||||
ko.components.register("graph-tab", new TabComponents.GraphTab());
|
ko.components.register("graph-tab", new TabComponents.GraphTab());
|
||||||
ko.components.register("mongo-shell-tab", new TabComponents.MongoShellTab());
|
ko.components.register("mongo-shell-tab", new TabComponents.MongoShellTab());
|
||||||
ko.components.register("conflicts-tab", new TabComponents.ConflictsTab());
|
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("notebookv2-tab", new TabComponents.NotebookV2Tab());
|
||||||
ko.components.register("terminal-tab", new TabComponents.TerminalTab());
|
ko.components.register("terminal-tab", new TabComponents.TerminalTab());
|
||||||
ko.components.register("spark-master-tab", new TabComponents.SparkMasterTab());
|
ko.components.register("spark-master-tab", new TabComponents.SparkMasterTab());
|
||||||
@@ -48,6 +52,14 @@ ko.components.register("notebook-viewer-tab", new TabComponents.NotebookViewerTa
|
|||||||
// Database Tabs
|
// Database Tabs
|
||||||
ko.components.register("database-settings-tab", new TabComponents.DatabaseSettingsTab());
|
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
|
// Panes
|
||||||
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
|
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
|
||||||
ko.components.register("add-collection-pane", new PaneComponents.AddCollectionPaneComponent());
|
ko.components.register("add-collection-pane", new PaneComponents.AddCollectionPaneComponent());
|
||||||
@@ -81,3 +93,6 @@ ko.components.register("manage-spark-cluster-pane", new PaneComponents.ManageSpa
|
|||||||
ko.components.register("library-manage-pane", new PaneComponents.LibraryManagePaneComponent());
|
ko.components.register("library-manage-pane", new PaneComponents.LibraryManagePaneComponent());
|
||||||
ko.components.register("cluster-library-pane", new PaneComponents.ClusterLibraryPaneComponent());
|
ko.components.register("cluster-library-pane", new PaneComponents.ClusterLibraryPaneComponent());
|
||||||
ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent());
|
ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent());
|
||||||
|
|
||||||
|
// Menus
|
||||||
|
ko.components.register("collection-node-context-menu", new TreeComponents.CollectionTreeNodeContextMenu());
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
|
import { CommandButtonOptions } from "./Controls/CommandButton/CommandButton";
|
||||||
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
|
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
|
||||||
import AddCollectionIcon from "../../images/AddCollection.svg";
|
import AddCollectionIcon from "../../images/AddCollection.svg";
|
||||||
import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg";
|
import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg";
|
||||||
@@ -114,10 +115,7 @@ export class ResourceTreeContextMenuButtonFactory {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createStoreProcedureContextMenuItems(
|
public static createStoreProcedureContextMenuItems(container: ViewModels.Explorer): TreeNodeMenuItem[] {
|
||||||
container: ViewModels.Explorer,
|
|
||||||
storedProcedure: ViewModels.StoredProcedure
|
|
||||||
): TreeNodeMenuItem[] {
|
|
||||||
if (container.isPreferredApiCassandra()) {
|
if (container.isPreferredApiCassandra()) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -125,16 +123,16 @@ export class ResourceTreeContextMenuButtonFactory {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
iconSrc: DeleteSprocIcon,
|
iconSrc: DeleteSprocIcon,
|
||||||
onClick: () => storedProcedure.delete(),
|
onClick: () => {
|
||||||
|
const selectedStoreProcedure: ViewModels.StoredProcedure = container.findSelectedStoredProcedure();
|
||||||
|
selectedStoreProcedure && selectedStoreProcedure.delete(selectedStoreProcedure, null);
|
||||||
|
},
|
||||||
label: "Delete Store Procedure"
|
label: "Delete Store Procedure"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createTriggerContextMenuItems(
|
public static createTriggerContextMenuItems(container: ViewModels.Explorer): TreeNodeMenuItem[] {
|
||||||
container: ViewModels.Explorer,
|
|
||||||
trigger: ViewModels.Trigger
|
|
||||||
): TreeNodeMenuItem[] {
|
|
||||||
if (container.isPreferredApiCassandra()) {
|
if (container.isPreferredApiCassandra()) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -142,16 +140,16 @@ export class ResourceTreeContextMenuButtonFactory {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
iconSrc: DeleteTriggerIcon,
|
iconSrc: DeleteTriggerIcon,
|
||||||
onClick: () => trigger.delete(),
|
onClick: () => {
|
||||||
|
const selectedTrigger: ViewModels.Trigger = container.findSelectedTrigger();
|
||||||
|
selectedTrigger && selectedTrigger.delete(selectedTrigger, null);
|
||||||
|
},
|
||||||
label: "Delete Trigger"
|
label: "Delete Trigger"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createUserDefinedFunctionContextMenuItems(
|
public static createUserDefinedFunctionContextMenuItems(container: ViewModels.Explorer): TreeNodeMenuItem[] {
|
||||||
container: ViewModels.Explorer,
|
|
||||||
userDefinedFunction: ViewModels.UserDefinedFunction
|
|
||||||
): TreeNodeMenuItem[] {
|
|
||||||
if (container.isPreferredApiCassandra()) {
|
if (container.isPreferredApiCassandra()) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -159,9 +157,266 @@ export class ResourceTreeContextMenuButtonFactory {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
iconSrc: DeleteUDFIcon,
|
iconSrc: DeleteUDFIcon,
|
||||||
onClick: () => userDefinedFunction.delete(),
|
onClick: () => {
|
||||||
|
const selectedUDF: ViewModels.UserDefinedFunction = container.findSelectedUDF();
|
||||||
|
selectedUDF && selectedUDF.delete(selectedUDF, null);
|
||||||
|
},
|
||||||
label: "Delete User Defined Function"
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
200
src/Explorer/Controls/CommandButton/CommandButton.less
Normal file
200
src/Explorer/Controls/CommandButton/CommandButton.less
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
@import "../../../../less/Common/Constants";
|
||||||
|
|
||||||
|
@ButtonIconSize: 18px;
|
||||||
|
|
||||||
|
.commandBar {
|
||||||
|
padding-left: @DefaultSpace;
|
||||||
|
border-bottom: @ButtonBorderWidth solid @BaseMedium;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
height: @topcommandbarheight;
|
||||||
|
|
||||||
|
.staticCommands {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflowCommands {
|
||||||
|
display:flex;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
|
||||||
|
.visibleCommands {
|
||||||
|
display: inline-flex;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partialSplitterContainer {
|
||||||
|
padding: @SmallSpace @DefaultSpace @SmallSpace @SmallSpace;
|
||||||
|
.flex-display();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.commandExpand {
|
||||||
|
border: none;
|
||||||
|
padding: 0px;
|
||||||
|
direction: rtl;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.hover();
|
||||||
|
cursor: pointer;
|
||||||
|
& > .commandDropdownContainer {
|
||||||
|
display: block !important; // TODO: Remove after reusing KO mouseover and mouseout event handlers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
.commandDropdownLauncher {
|
||||||
|
direction: ltr;
|
||||||
|
padding-top: @SmallSpace;
|
||||||
|
|
||||||
|
.commandIcon {
|
||||||
|
vertical-align: text-top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commandBarEllipses {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hiddenCommandsContainer > .commandDropdownLauncher {
|
||||||
|
padding: 0px @DefaultSpace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commandDropdownContainer {
|
||||||
|
display: none;
|
||||||
|
z-index: 1000;
|
||||||
|
direction: ltr;
|
||||||
|
position: absolute;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 0px;
|
||||||
|
background-color: @BaseLight;
|
||||||
|
box-shadow: 1px 2px 6px @BaseMediumHigh, -2px 2px 6px @BaseMediumHigh;
|
||||||
|
|
||||||
|
.commandDropdown {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedbackButton {
|
||||||
|
margin-right: @LargeSpace;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
command-button,
|
||||||
|
.commandButtonReact {
|
||||||
|
display: inline-flex;
|
||||||
|
.commandButtonComponent {
|
||||||
|
width: 100%;
|
||||||
|
color: @BaseHigh;
|
||||||
|
background-color: transparent;
|
||||||
|
text-decoration: none;
|
||||||
|
border: @ButtonBorderWidth solid transparent;
|
||||||
|
.flex-display();
|
||||||
|
|
||||||
|
&:hover:not(.commandDisabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
.hover();
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(.commandDisabled) {
|
||||||
|
border: @ButtonBorderWidth dashed @AccentMedium;
|
||||||
|
.active();
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus:not(.commandDisabled) {
|
||||||
|
border: @ButtonBorderWidth dashed @AccentMedium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commandContent {
|
||||||
|
padding: @DefaultSpace @DefaultSpace @DefaultSpace;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
|
.commandIcon {
|
||||||
|
margin: 0 @SmallSpace 0 0;
|
||||||
|
vertical-align: text-top;
|
||||||
|
width: @ButtonIconSize;
|
||||||
|
height: @ButtonIconSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commandLabel {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.commandContent .hasHiddenItems {
|
||||||
|
padding-right: @SmallSpace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.commandButtonComponent.commandDisabled {
|
||||||
|
color: @BaseMediumHigh;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commandExpand {
|
||||||
|
padding-top: @SmallSpace;
|
||||||
|
padding-bottom: @SmallSpace;
|
||||||
|
&:hover {
|
||||||
|
.hover();
|
||||||
|
& > .commandDropdownContainer {
|
||||||
|
display: block !important; // TODO: Remove after reusing KO mouseover and mouseout event handlers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
.commandDropdownLauncher {
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
|
||||||
|
.commandButtonComponent {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandDropdown {
|
||||||
|
padding: @SmallSpace;
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.partialSplitter {
|
||||||
|
margin: @SmallSpace 0px 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.commandButtonComponent[tabindex]:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectedButton {
|
||||||
|
background-color: @AccentLow;
|
||||||
|
outline: none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.partialSplitter {
|
||||||
|
border-left: @ButtonBorderWidth solid @BaseMediumHigh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commandDropdown .commandButtonComponent {
|
||||||
|
padding-left: 0px;
|
||||||
|
}
|
||||||
139
src/Explorer/Controls/CommandButton/CommandButton.test.ts
Normal file
139
src/Explorer/Controls/CommandButton/CommandButton.test.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import * as ko from "knockout";
|
||||||
|
import { CommandButtonComponent, CommandButtonOptions } from "./CommandButton";
|
||||||
|
|
||||||
|
const mockLabel = "Some Label";
|
||||||
|
const id = "Some id";
|
||||||
|
|
||||||
|
function buildComponent(buttonOptions: any) {
|
||||||
|
document.body.innerHTML = CommandButtonComponent.template as any;
|
||||||
|
const vm = new CommandButtonComponent.viewModel(buttonOptions);
|
||||||
|
ko.applyBindings(vm);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Command Button Component", () => {
|
||||||
|
function buildButtonOptions(
|
||||||
|
onClick: () => void,
|
||||||
|
id?: string,
|
||||||
|
label?: string,
|
||||||
|
disabled?: ko.Observable<boolean>,
|
||||||
|
visible?: ko.Observable<boolean>,
|
||||||
|
tooltipText?: string
|
||||||
|
): { buttonProps: CommandButtonOptions } {
|
||||||
|
return {
|
||||||
|
buttonProps: {
|
||||||
|
iconSrc: "images/AddCollection.svg",
|
||||||
|
id: id,
|
||||||
|
commandButtonLabel: label || mockLabel,
|
||||||
|
disabled: disabled,
|
||||||
|
visible: visible,
|
||||||
|
tooltipText: tooltipText,
|
||||||
|
hasPopup: false,
|
||||||
|
onCommandClick: onClick
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSplitterButtonOptions(
|
||||||
|
onClick: () => void,
|
||||||
|
id?: string,
|
||||||
|
label?: string,
|
||||||
|
disabled?: ko.Observable<boolean>,
|
||||||
|
visible?: ko.Observable<boolean>,
|
||||||
|
tooltipText?: string
|
||||||
|
): { buttonProps: CommandButtonOptions } {
|
||||||
|
const child: CommandButtonOptions = {
|
||||||
|
iconSrc: "images/settings_15x15.svg",
|
||||||
|
id: id,
|
||||||
|
commandButtonLabel: label || mockLabel,
|
||||||
|
disabled: disabled,
|
||||||
|
visible: visible,
|
||||||
|
tooltipText: tooltipText,
|
||||||
|
hasPopup: false,
|
||||||
|
onCommandClick: onClick
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
buttonProps: {
|
||||||
|
iconSrc: "images/AddCollection.svg",
|
||||||
|
id: id,
|
||||||
|
commandButtonLabel: label || mockLabel,
|
||||||
|
disabled: disabled,
|
||||||
|
visible: visible,
|
||||||
|
tooltipText: tooltipText,
|
||||||
|
hasPopup: false,
|
||||||
|
onCommandClick: onClick,
|
||||||
|
children: [child]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
ko.cleanNode(document);
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Rendering", () => {
|
||||||
|
it("should display button label", () => {
|
||||||
|
const buttonOptions = buildButtonOptions(() => {
|
||||||
|
/** do nothing **/
|
||||||
|
}, mockLabel);
|
||||||
|
buildComponent(buttonOptions);
|
||||||
|
expect(document.getElementsByClassName("commandButtonComponent").item(0).textContent).toContain(mockLabel);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should display button icon", () => {
|
||||||
|
const buttonOptions = buildButtonOptions(() => {
|
||||||
|
/** do nothing **/
|
||||||
|
});
|
||||||
|
buildComponent(buttonOptions);
|
||||||
|
expect(
|
||||||
|
document
|
||||||
|
.getElementsByTagName("img")
|
||||||
|
.item(0)
|
||||||
|
.getAttribute("src")
|
||||||
|
).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Behavior", () => {
|
||||||
|
let clickSpy: jasmine.Spy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
clickSpy = jasmine.createSpy("Command button click spy");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should trigger the click handler when the command button is clicked", () => {
|
||||||
|
const buttonOptions = buildButtonOptions(() => clickSpy());
|
||||||
|
buildComponent(buttonOptions);
|
||||||
|
document
|
||||||
|
.getElementsByClassName("commandButtonComponent")
|
||||||
|
.item(0)
|
||||||
|
.dispatchEvent(new Event("click"));
|
||||||
|
expect(clickSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not trigger the click handler when command button is disabled", () => {
|
||||||
|
const buttonOptions = buildButtonOptions(() => clickSpy(), id, mockLabel, ko.observable(true));
|
||||||
|
buildComponent(buttonOptions);
|
||||||
|
document
|
||||||
|
.getElementsByClassName("commandButtonComponent")
|
||||||
|
.item(0)
|
||||||
|
.dispatchEvent(new Event("click"));
|
||||||
|
expect(clickSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not have a dropdown if it has no child", () => {
|
||||||
|
const buttonOptions = buildButtonOptions(() => clickSpy(), id, mockLabel, ko.observable(true));
|
||||||
|
buildComponent(buttonOptions);
|
||||||
|
const dropdownSize = document.getElementsByClassName("commandExpand").length;
|
||||||
|
expect(dropdownSize).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have a dropdown if it has a child", () => {
|
||||||
|
const buttonOptions = buildSplitterButtonOptions(() => clickSpy(), id, mockLabel, ko.observable(true));
|
||||||
|
buildComponent(buttonOptions);
|
||||||
|
const dropdownSize = document.getElementsByClassName("commandExpand").length;
|
||||||
|
expect(dropdownSize).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
191
src/Explorer/Controls/CommandButton/CommandButton.ts
Normal file
191
src/Explorer/Controls/CommandButton/CommandButton.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
/**
|
||||||
|
* How to use this component:
|
||||||
|
*
|
||||||
|
* In your html markup, use:
|
||||||
|
* <command-button params="{
|
||||||
|
* iconSrc: '/icon/example/src/',
|
||||||
|
* onCommandClick: () => { doSomething },
|
||||||
|
* commandButtonLabel: 'Some Label'
|
||||||
|
* disabled: true/false
|
||||||
|
* }">
|
||||||
|
* </command-button>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as ko from "knockout";
|
||||||
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel";
|
||||||
|
import { KeyCodes } from "../../../Common/Constants";
|
||||||
|
|
||||||
|
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import template from "./command-button.html";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for this component
|
||||||
|
*/
|
||||||
|
export interface CommandButtonOptions {
|
||||||
|
/**
|
||||||
|
* image source for the button icon
|
||||||
|
*/
|
||||||
|
iconSrc: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Id for the button icon
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click handler for command button click
|
||||||
|
*/
|
||||||
|
onCommandClick: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Label for the button
|
||||||
|
*/
|
||||||
|
commandButtonLabel: string | ko.Observable<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if this button opens a tab or pane, false otherwise.
|
||||||
|
*/
|
||||||
|
hasPopup: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enabled/disabled state of command button
|
||||||
|
*/
|
||||||
|
disabled?: ko.Subscribable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visibility/Invisibility of the button
|
||||||
|
*/
|
||||||
|
visible?: ko.Subscribable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the button should have the 'selectedButton' styling
|
||||||
|
*/
|
||||||
|
isSelected?: ko.Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text to displayed in the tooltip on hover
|
||||||
|
*/
|
||||||
|
tooltipText?: string | ko.Observable<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback triggered when the template is bound to the component
|
||||||
|
*/
|
||||||
|
onTemplateReady?: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* tabindex for the command button
|
||||||
|
*/
|
||||||
|
tabIndex?: ko.Observable<number>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Childrens command buttons to hide in the dropdown
|
||||||
|
*/
|
||||||
|
children?: CommandButtonOptions[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CommandButtonViewModel extends WaitsForTemplateViewModel implements ViewModels.CommandButton {
|
||||||
|
public commandClickCallback: () => void;
|
||||||
|
public commandButtonId: string;
|
||||||
|
public disabled: ko.Subscribable<boolean>;
|
||||||
|
public visible: ko.Subscribable<boolean>;
|
||||||
|
public isSelected: ko.Observable<boolean>;
|
||||||
|
public iconSrc: string;
|
||||||
|
public commandButtonLabel: ko.Observable<string>;
|
||||||
|
public tooltipText: ko.Observable<string>;
|
||||||
|
public tabIndex: ko.Observable<number>;
|
||||||
|
public isTemplateReady: ko.Observable<boolean>;
|
||||||
|
public hasPopup: boolean;
|
||||||
|
public children: ko.ObservableArray<CommandButtonOptions>;
|
||||||
|
|
||||||
|
public constructor(options: { buttonProps: CommandButtonOptions }) {
|
||||||
|
super();
|
||||||
|
const props = options.buttonProps;
|
||||||
|
const commandButtonLabel = props.commandButtonLabel;
|
||||||
|
const tooltipText = props.tooltipText;
|
||||||
|
this.commandButtonLabel =
|
||||||
|
typeof commandButtonLabel === "string" ? ko.observable<string>(commandButtonLabel) : commandButtonLabel;
|
||||||
|
this.commandButtonId = props.id;
|
||||||
|
this.disabled = props.disabled || ko.observable(false);
|
||||||
|
this.visible = props.visible || ko.observable(true);
|
||||||
|
this.isSelected = props.isSelected || ko.observable(false);
|
||||||
|
this.iconSrc = props.iconSrc;
|
||||||
|
this.tabIndex = props.tabIndex || ko.observable(0);
|
||||||
|
this.hasPopup = props.hasPopup;
|
||||||
|
this.children = ko.observableArray(props.children);
|
||||||
|
|
||||||
|
super.onTemplateReady((isTemplateReady: boolean) => {
|
||||||
|
if (isTemplateReady && props.onTemplateReady) {
|
||||||
|
props.onTemplateReady();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tooltipText && typeof tooltipText === "string") {
|
||||||
|
this.tooltipText = ko.observable<string>(tooltipText);
|
||||||
|
} else if (tooltipText && typeof tooltipText === "function") {
|
||||||
|
this.tooltipText = tooltipText;
|
||||||
|
} else {
|
||||||
|
this.tooltipText = this.commandButtonLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.commandClickCallback = () => {
|
||||||
|
if (this.disabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const el = document.querySelector(".commandDropdownContainer") as HTMLElement;
|
||||||
|
if (el) {
|
||||||
|
el.style.display = "none";
|
||||||
|
}
|
||||||
|
props.onCommandClick();
|
||||||
|
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
||||||
|
commandButtonClicked: this.commandButtonLabel
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public onKeyPress(source: any, event: KeyboardEvent): boolean {
|
||||||
|
if (event.keyCode === KeyCodes.Space || event.keyCode === KeyCodes.Enter) {
|
||||||
|
this.commandClickCallback && this.commandClickCallback();
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public onLauncherKeyDown(source: any, event: KeyboardEvent): boolean {
|
||||||
|
// TODO: Convert JQuery code into Knockout
|
||||||
|
if (event.keyCode === KeyCodes.DownArrow) {
|
||||||
|
$(event.target)
|
||||||
|
.parent()
|
||||||
|
.siblings()
|
||||||
|
.children(".commandExpand")
|
||||||
|
.children(".commandDropdownContainer")
|
||||||
|
.hide();
|
||||||
|
$(event.target)
|
||||||
|
.children(".commandDropdownContainer")
|
||||||
|
.show()
|
||||||
|
.focus();
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (event.keyCode === KeyCodes.UpArrow) {
|
||||||
|
$(event.target)
|
||||||
|
.children(".commandDropdownContainer")
|
||||||
|
.hide();
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class for ko component registration
|
||||||
|
*/
|
||||||
|
export const CommandButtonComponent = {
|
||||||
|
viewModel: CommandButtonViewModel,
|
||||||
|
template
|
||||||
|
};
|
||||||
40
src/Explorer/Controls/CommandButton/command-button.html
Normal file
40
src/Explorer/Controls/CommandButton/command-button.html
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<span
|
||||||
|
class="commandButtonComponent"
|
||||||
|
role="menuitem"
|
||||||
|
tabindex="0"
|
||||||
|
data-bind="setTemplateReady: true,
|
||||||
|
css: {
|
||||||
|
commandDisabled: disabled,
|
||||||
|
selectedButton: isSelected
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
keypress: onKeyPress
|
||||||
|
},
|
||||||
|
attr: {
|
||||||
|
title: tooltipText,
|
||||||
|
id: commandButtonId,
|
||||||
|
tabindex: tabIndex ,
|
||||||
|
'aria-disabled': disabled,
|
||||||
|
'aria-haspopup': hasPopup
|
||||||
|
},
|
||||||
|
click: commandClickCallback,
|
||||||
|
visible: visible"
|
||||||
|
>
|
||||||
|
<div class="commandContent" data-bind="css: { hasHiddenItems: children().length > 0 }">
|
||||||
|
<img class="commandIcon" data-bind="attr: {src: iconSrc, alt: commandButtonLabel}" />
|
||||||
|
<span class="commandLabel" data-bind="text: commandButtonLabel"></span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<!-- ko if: children().length > 0 -->
|
||||||
|
<div class="commandExpand" tabindex="0" data-bind="visible: visible, event: { keydown: onLauncherKeyDown }">
|
||||||
|
<div class="commandDropdownLauncher">
|
||||||
|
<span class="partialSplitter"></span>
|
||||||
|
<span class="expandDropdown"> <img src="/QueryBuilder/CollapseChevronDown_16x.png" /> </span>
|
||||||
|
</div>
|
||||||
|
<div class="commandDropdownContainer">
|
||||||
|
<div class="commandDropdown" data-bind="foreach: children">
|
||||||
|
<command-button params="{buttonProps: $data}"></command-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- /ko -->
|
||||||
@@ -54,8 +54,7 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
|
|||||||
styles: {
|
styles: {
|
||||||
title: { fontSize: DIALOG_TITLE_FONT_SIZE, fontWeight: DIALOG_TITLE_FONT_WEIGHT },
|
title: { fontSize: DIALOG_TITLE_FONT_SIZE, fontWeight: DIALOG_TITLE_FONT_WEIGHT },
|
||||||
subText: { fontSize: DIALOG_SUBTEXT_FONT_SIZE }
|
subText: { fontSize: DIALOG_SUBTEXT_FONT_SIZE }
|
||||||
},
|
}
|
||||||
showCloseButton: false
|
|
||||||
},
|
},
|
||||||
modalProps: { isBlocking: this.props.isModal },
|
modalProps: { isBlocking: this.props.isModal },
|
||||||
minWidth: DIALOG_MIN_WIDTH,
|
minWidth: DIALOG_MIN_WIDTH,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
* React component for Switch Directory
|
* React component for Switch Directory
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import _ from "underscore";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Dropdown, IDropdownOption, IDropdownProps } from "office-ui-fabric-react/lib/Dropdown";
|
import { Dropdown, IDropdownOption, IDropdownProps } from "office-ui-fabric-react/lib/Dropdown";
|
||||||
import { Tenant } from "../../../Contracts/DataModels";
|
import { Tenant } from "../../../Contracts/DataModels";
|
||||||
@@ -61,7 +60,7 @@ export class DefaultDirectoryDropdownComponent extends React.Component<DefaultDi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedDirectory = _.find(this.props.directories, d => d.tenantId === option.key);
|
const selectedDirectory = this.props.directories.find(d => d.tenantId === option.key);
|
||||||
if (!selectedDirectory) {
|
if (!selectedDirectory) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import _ from "underscore";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { DefaultButton, IButtonProps } from "office-ui-fabric-react/lib/Button";
|
import { DefaultButton, IButtonProps } from "office-ui-fabric-react/lib/Button";
|
||||||
@@ -115,7 +114,7 @@ export class DirectoryListComponent extends React.Component<DirectoryListProps,
|
|||||||
}
|
}
|
||||||
const buttonElement = e.currentTarget;
|
const buttonElement = e.currentTarget;
|
||||||
const selectedDirectoryId = buttonElement.getElementsByClassName("directoryListItemId")[0].textContent;
|
const selectedDirectoryId = buttonElement.getElementsByClassName("directoryListItemId")[0].textContent;
|
||||||
const selectedDirectory = _.find(this.props.directories, d => d.tenantId === selectedDirectoryId);
|
const selectedDirectory = this.props.directories.find(d => d.tenantId === selectedDirectoryId);
|
||||||
|
|
||||||
this.props.onNewDirectorySelected(selectedDirectory);
|
this.props.onNewDirectorySelected(selectedDirectory);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
.featurePanelComponentContainer {
|
|
||||||
width: 800px;
|
|
||||||
|
|
||||||
.urlContainer {
|
|
||||||
padding: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.options {
|
|
||||||
padding: 10px;
|
|
||||||
overflow: auto;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkboxRow {
|
|
||||||
width: 390px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,312 +0,0 @@
|
|||||||
// 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>
|
|
||||||
`;
|
|
||||||
@@ -93,7 +93,7 @@ export class AddRepoComponent extends React.Component<AddRepoComponentProps, Add
|
|||||||
const repo = await this.props.getRepo(repoInfo.owner, repoInfo.repo);
|
const repo = await this.props.getRepo(repoInfo.owner, repoInfo.repo);
|
||||||
if (repo) {
|
if (repo) {
|
||||||
const item: RepoListItem = {
|
const item: RepoListItem = {
|
||||||
key: GitHubUtils.toRepoFullName(repo.owner, repo.name),
|
key: GitHubUtils.toRepoFullName(repo.owner.login, repo.name),
|
||||||
repo,
|
repo,
|
||||||
branches: [
|
branches: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
Text
|
Text
|
||||||
} from "office-ui-fabric-react";
|
} from "office-ui-fabric-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { IGitHubBranch, IGitHubPageInfo } from "../../../GitHub/GitHubClient";
|
import { IGitHubBranch } from "../../../GitHub/GitHubClient";
|
||||||
import { GitHubUtils } from "../../../Utils/GitHubUtils";
|
import { GitHubUtils } from "../../../Utils/GitHubUtils";
|
||||||
import { RepoListItem } from "./GitHubReposComponent";
|
import { RepoListItem } from "./GitHubReposComponent";
|
||||||
import {
|
import {
|
||||||
@@ -41,7 +41,6 @@ export interface ReposListComponentProps {
|
|||||||
|
|
||||||
export interface BranchesProps {
|
export interface BranchesProps {
|
||||||
branches: IGitHubBranch[];
|
branches: IGitHubBranch[];
|
||||||
lastPageInfo?: IGitHubPageInfo;
|
|
||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
loadMore: () => void;
|
loadMore: () => void;
|
||||||
@@ -140,7 +139,7 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const checkboxProps: ICheckboxProps = {
|
const checkboxProps: ICheckboxProps = {
|
||||||
...ReposListComponent.getCheckboxPropsForLabel(GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)),
|
...ReposListComponent.getCheckboxPropsForLabel(GitHubUtils.toRepoFullName(item.repo.owner.login, item.repo.name)),
|
||||||
styles: ReposListCheckboxStyles,
|
styles: ReposListCheckboxStyles,
|
||||||
defaultChecked: true,
|
defaultChecked: true,
|
||||||
onChange: () => this.props.unpinRepo(item)
|
onChange: () => this.props.unpinRepo(item)
|
||||||
@@ -154,7 +153,7 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
|
|||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)];
|
const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner.login, item.repo.name)];
|
||||||
const options: IDropdownOption[] = branchesProps.branches.map(branch => ({
|
const options: IDropdownOption[] = branchesProps.branches.map(branch => ({
|
||||||
key: branch.name,
|
key: branch.name,
|
||||||
text: branch.name,
|
text: branch.name,
|
||||||
@@ -223,7 +222,7 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
|
|||||||
|
|
||||||
private onRenderPinnedReposBranchesDropdownOption(option: IDropdownOption): JSX.Element {
|
private onRenderPinnedReposBranchesDropdownOption(option: IDropdownOption): JSX.Element {
|
||||||
const item: RepoListItem = option.data;
|
const item: RepoListItem = option.data;
|
||||||
const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)];
|
const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner.login, item.repo.name)];
|
||||||
|
|
||||||
if (option.index === ReposListComponent.FooterIndex) {
|
if (option.index === ReposListComponent.FooterIndex) {
|
||||||
const linkProps: ILinkProps = {
|
const linkProps: ILinkProps = {
|
||||||
@@ -268,7 +267,7 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const checkboxProps: ICheckboxProps = {
|
const checkboxProps: ICheckboxProps = {
|
||||||
...ReposListComponent.getCheckboxPropsForLabel(GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)),
|
...ReposListComponent.getCheckboxPropsForLabel(GitHubUtils.toRepoFullName(item.repo.owner.login, item.repo.name)),
|
||||||
styles: ReposListCheckboxStyles,
|
styles: ReposListCheckboxStyles,
|
||||||
onChange: () => {
|
onChange: () => {
|
||||||
const repoListItem = { ...item };
|
const repoListItem = { ...item };
|
||||||
|
|||||||
80
src/Explorer/Controls/Notebook/NotebookAppMessageHandler.ts
Normal file
80
src/Explorer/Controls/Notebook/NotebookAppMessageHandler.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { shallow } from "enzyme";
|
|
||||||
import { GalleryCardComponent, GalleryCardComponentProps } from "./GalleryCardComponent";
|
|
||||||
|
|
||||||
describe("GalleryCardComponent", () => {
|
|
||||||
it("renders", () => {
|
|
||||||
const props: GalleryCardComponentProps = {
|
|
||||||
name: "mycard",
|
|
||||||
url: "url",
|
|
||||||
notebookMetadata: undefined,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
||||||
onClick: () => {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallow(<GalleryCardComponent {...props} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`GalleryCardComponent renders 1`] = `
|
|
||||||
<Card
|
|
||||||
aria-label="Notebook Card"
|
|
||||||
onClick={[Function]}
|
|
||||||
tokens={
|
|
||||||
Object {
|
|
||||||
"childrenMargin": 12,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<CardSection>
|
|
||||||
<Text
|
|
||||||
styles={
|
|
||||||
Object {
|
|
||||||
"root": Object {
|
|
||||||
"color": "#333333",
|
|
||||||
"fontWeight": 600,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
mycard
|
|
||||||
</Text>
|
|
||||||
</CardSection>
|
|
||||||
</Card>
|
|
||||||
`;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
@import "../../../../less/Common/Constants";
|
|
||||||
|
|
||||||
.galleryContainer {
|
|
||||||
padding: @LargeSpace @LargeSpace 30px @LargeSpace;
|
|
||||||
height: 100%;
|
|
||||||
overflow-y: auto;
|
|
||||||
width: 100%;
|
|
||||||
font-family: @DataExplorerFont;
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { shallow } from "enzyme";
|
|
||||||
import {
|
|
||||||
GalleryViewerContainerComponent,
|
|
||||||
GalleryViewerContainerComponentProps,
|
|
||||||
FullWidthTabs,
|
|
||||||
FullWidthTabsProps,
|
|
||||||
GalleryCardsComponent,
|
|
||||||
GalleryCardsComponentProps,
|
|
||||||
GalleryViewerComponent,
|
|
||||||
GalleryViewerComponentProps
|
|
||||||
} from "./GalleryViewerComponent";
|
|
||||||
|
|
||||||
describe("GalleryCardsComponent", () => {
|
|
||||||
it("renders", () => {
|
|
||||||
// TODO Mock this
|
|
||||||
const props: GalleryCardsComponentProps = {
|
|
||||||
data: [],
|
|
||||||
userMetadata: undefined,
|
|
||||||
onNotebookMetadataChange: () => Promise.resolve(),
|
|
||||||
onClick: () => Promise.resolve()
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallow(<GalleryCardsComponent {...props} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("FullWidthTabs", () => {
|
|
||||||
it("renders", () => {
|
|
||||||
const props: FullWidthTabsProps = {
|
|
||||||
officialSamplesContent: [],
|
|
||||||
likedNotebooksContent: [],
|
|
||||||
userMetadata: undefined,
|
|
||||||
onClick: () => Promise.resolve()
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallow(<FullWidthTabs {...props} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("GalleryViewerContainerComponent", () => {
|
|
||||||
it("renders", () => {
|
|
||||||
const props: GalleryViewerContainerComponentProps = {
|
|
||||||
container: undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallow(<GalleryViewerContainerComponent {...props} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("GalleryCardComponent", () => {
|
|
||||||
it("renders", () => {
|
|
||||||
const props: GalleryViewerComponentProps = {
|
|
||||||
container: undefined,
|
|
||||||
officialSamplesData: [],
|
|
||||||
likedNotebookData: undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallow(<GalleryViewerComponent {...props} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,361 +0,0 @@
|
|||||||
/**
|
|
||||||
* Gallery Viewer
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
|
||||||
import { GalleryCardComponent } from "./Cards/GalleryCardComponent";
|
|
||||||
import { Stack, IStackTokens } from "office-ui-fabric-react";
|
|
||||||
import { JunoUtils } from "../../../Utils/JunoUtils";
|
|
||||||
import { CosmosClient } from "../../../Common/CosmosClient";
|
|
||||||
import { config } from "../../../Config";
|
|
||||||
import path from "path";
|
|
||||||
import { SessionStorageUtility, StorageKey } from "../../../Shared/StorageUtility";
|
|
||||||
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
|
|
||||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
|
||||||
import * as TabComponent from "../Tabs/TabComponent";
|
|
||||||
|
|
||||||
import "./GalleryViewerComponent.less";
|
|
||||||
|
|
||||||
export interface GalleryCardsComponentProps {
|
|
||||||
data: DataModels.GitHubInfoJunoResponse[];
|
|
||||||
userMetadata: DataModels.UserMetadata;
|
|
||||||
onNotebookMetadataChange: (
|
|
||||||
officialSamplesIndex: number,
|
|
||||||
notebookMetadata: DataModels.NotebookMetadata
|
|
||||||
) => Promise<void>;
|
|
||||||
onClick: (
|
|
||||||
url: string,
|
|
||||||
notebookMetadata: DataModels.NotebookMetadata,
|
|
||||||
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
|
|
||||||
isLikedNotebook: boolean
|
|
||||||
) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GalleryCardsComponent extends React.Component<GalleryCardsComponentProps> {
|
|
||||||
private sectionStackTokens: IStackTokens = { childrenGap: 30 };
|
|
||||||
|
|
||||||
public render(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Stack horizontal wrap tokens={this.sectionStackTokens}>
|
|
||||||
{this.props.data.map((githubInfo: DataModels.GitHubInfoJunoResponse) => {
|
|
||||||
const name = githubInfo.name;
|
|
||||||
const url = githubInfo.downloadUrl;
|
|
||||||
const notebookMetadata = githubInfo.metadata || {
|
|
||||||
date: "2008-12-01",
|
|
||||||
description: "Great notebook",
|
|
||||||
tags: ["favorite", "sample"],
|
|
||||||
author: "Laurent Nguyen",
|
|
||||||
views: 432,
|
|
||||||
likes: 123,
|
|
||||||
downloads: 56,
|
|
||||||
imageUrl:
|
|
||||||
"https://media.magazine.ferrari.com/images/2019/02/27/170304506-c1bcf028-b513-45f6-9f27-0cadac619c3d.jpg"
|
|
||||||
};
|
|
||||||
const officialSamplesIndex = githubInfo.officialSamplesIndex;
|
|
||||||
const isLikedNotebook = githubInfo.isLikedNotebook;
|
|
||||||
const updateTabsStatePerNotebook = this.props.onNotebookMetadataChange
|
|
||||||
? (notebookMetadata: DataModels.NotebookMetadata): Promise<void> =>
|
|
||||||
this.props.onNotebookMetadataChange(officialSamplesIndex, notebookMetadata)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return (
|
|
||||||
name !== ".gitignore" &&
|
|
||||||
url && (
|
|
||||||
<GalleryCardComponent
|
|
||||||
key={url}
|
|
||||||
name={name}
|
|
||||||
url={url}
|
|
||||||
notebookMetadata={notebookMetadata}
|
|
||||||
onClick={(): Promise<void> =>
|
|
||||||
this.props.onClick(url, notebookMetadata, updateTabsStatePerNotebook, isLikedNotebook)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FullWidthTabsProps {
|
|
||||||
officialSamplesContent: DataModels.GitHubInfoJunoResponse[];
|
|
||||||
likedNotebooksContent: DataModels.GitHubInfoJunoResponse[];
|
|
||||||
userMetadata: DataModels.UserMetadata;
|
|
||||||
onClick: (
|
|
||||||
url: string,
|
|
||||||
notebookMetadata: DataModels.NotebookMetadata,
|
|
||||||
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
|
|
||||||
isLikedNotebook: boolean
|
|
||||||
) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FullWidthTabsState {
|
|
||||||
activeTabIndex: number;
|
|
||||||
officialSamplesContent: DataModels.GitHubInfoJunoResponse[];
|
|
||||||
likedNotebooksContent: DataModels.GitHubInfoJunoResponse[];
|
|
||||||
userMetadata: DataModels.UserMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FullWidthTabs extends React.Component<FullWidthTabsProps, FullWidthTabsState> {
|
|
||||||
private authorizationToken = CosmosClient.authorizationToken();
|
|
||||||
private appTabs: TabComponent.Tab[];
|
|
||||||
|
|
||||||
constructor(props: FullWidthTabsProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
activeTabIndex: 0,
|
|
||||||
officialSamplesContent: this.props.officialSamplesContent,
|
|
||||||
likedNotebooksContent: this.props.likedNotebooksContent,
|
|
||||||
userMetadata: this.props.userMetadata
|
|
||||||
};
|
|
||||||
|
|
||||||
this.appTabs = [
|
|
||||||
{
|
|
||||||
title: "Official Samples",
|
|
||||||
content: {
|
|
||||||
className: "",
|
|
||||||
render: (): JSX.Element => (
|
|
||||||
<GalleryCardsComponent
|
|
||||||
data={this.state.officialSamplesContent}
|
|
||||||
onClick={this.props.onClick}
|
|
||||||
userMetadata={this.state.userMetadata}
|
|
||||||
onNotebookMetadataChange={this.updateTabsState}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
isVisible: (): boolean => true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Liked Notebooks",
|
|
||||||
content: {
|
|
||||||
className: "",
|
|
||||||
render: (): JSX.Element => (
|
|
||||||
<GalleryCardsComponent
|
|
||||||
data={this.state.likedNotebooksContent}
|
|
||||||
onClick={this.props.onClick}
|
|
||||||
userMetadata={this.state.userMetadata}
|
|
||||||
onNotebookMetadataChange={this.updateTabsState}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
isVisible: (): boolean => true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public updateTabsState = async (
|
|
||||||
officialSamplesIndex: number,
|
|
||||||
notebookMetadata: DataModels.NotebookMetadata
|
|
||||||
): Promise<void> => {
|
|
||||||
let currentLikedNotebooksContent = [...this.state.likedNotebooksContent];
|
|
||||||
let currentUserMetadata = { ...this.state.userMetadata };
|
|
||||||
let currentLikedNotebooks = [...currentUserMetadata.likedNotebooks];
|
|
||||||
|
|
||||||
const currentOfficialSamplesContent = [...this.state.officialSamplesContent];
|
|
||||||
const currentOfficialSamplesObject = { ...currentOfficialSamplesContent[officialSamplesIndex] };
|
|
||||||
const metadata = { ...currentOfficialSamplesObject.metadata };
|
|
||||||
const metadataLikesUpdates = metadata.likes - notebookMetadata.likes;
|
|
||||||
|
|
||||||
metadata.views = notebookMetadata.views;
|
|
||||||
metadata.downloads = notebookMetadata.downloads;
|
|
||||||
metadata.likes = notebookMetadata.likes;
|
|
||||||
currentOfficialSamplesObject.metadata = metadata;
|
|
||||||
|
|
||||||
// Notebook has been liked. Add To likedNotebooksContent, update isLikedNotebook flag
|
|
||||||
if (metadataLikesUpdates < 0) {
|
|
||||||
currentOfficialSamplesObject.isLikedNotebook = true;
|
|
||||||
currentLikedNotebooksContent = currentLikedNotebooksContent.concat(currentOfficialSamplesObject);
|
|
||||||
currentLikedNotebooks = currentLikedNotebooks.concat(currentOfficialSamplesObject.path);
|
|
||||||
currentUserMetadata = { likedNotebooks: currentLikedNotebooks };
|
|
||||||
} else if (metadataLikesUpdates > 0) {
|
|
||||||
// Notebook has been unliked. Remove from likedNotebooksContent after matching the path, update isLikedNotebook flag
|
|
||||||
|
|
||||||
currentOfficialSamplesObject.isLikedNotebook = false;
|
|
||||||
const likedNotebookIndex = currentLikedNotebooks.findIndex((path: string) => {
|
|
||||||
return path === currentOfficialSamplesObject.path;
|
|
||||||
});
|
|
||||||
currentLikedNotebooksContent.splice(likedNotebookIndex, 1);
|
|
||||||
currentLikedNotebooks.splice(likedNotebookIndex, 1);
|
|
||||||
currentUserMetadata = { likedNotebooks: currentLikedNotebooks };
|
|
||||||
}
|
|
||||||
|
|
||||||
currentOfficialSamplesContent[officialSamplesIndex] = currentOfficialSamplesObject;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
activeTabIndex: 0,
|
|
||||||
userMetadata: currentUserMetadata,
|
|
||||||
likedNotebooksContent: currentLikedNotebooksContent,
|
|
||||||
officialSamplesContent: currentOfficialSamplesContent
|
|
||||||
});
|
|
||||||
|
|
||||||
JunoUtils.updateNotebookMetadata(this.authorizationToken, notebookMetadata).then(
|
|
||||||
async () => {
|
|
||||||
if (metadataLikesUpdates !== 0) {
|
|
||||||
JunoUtils.updateUserMetadata(this.authorizationToken, currentUserMetadata);
|
|
||||||
// TODO: update state here?
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
|
||||||
ConsoleDataType.Error,
|
|
||||||
`Error updating notebook metadata: ${JSON.stringify(error)}`
|
|
||||||
);
|
|
||||||
// TODO add telemetry
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onTabIndexChange = (activeTabIndex: number): void => this.setState({ activeTabIndex });
|
|
||||||
|
|
||||||
public render(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<TabComponent.TabComponent
|
|
||||||
tabs={this.appTabs}
|
|
||||||
onTabIndexChange={this.onTabIndexChange.bind(this)}
|
|
||||||
currentTabIndex={this.state.activeTabIndex}
|
|
||||||
hideHeader={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GalleryViewerContainerComponentProps {
|
|
||||||
container: ViewModels.Explorer;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GalleryViewerContainerComponentState {
|
|
||||||
officialSamplesData: DataModels.GitHubInfoJunoResponse[];
|
|
||||||
likedNotebooksData: DataModels.LikedNotebooksJunoResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GalleryViewerContainerComponent extends React.Component<
|
|
||||||
GalleryViewerContainerComponentProps,
|
|
||||||
GalleryViewerContainerComponentState
|
|
||||||
> {
|
|
||||||
constructor(props: GalleryViewerContainerComponentProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
officialSamplesData: undefined,
|
|
||||||
likedNotebooksData: undefined
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount(): void {
|
|
||||||
const authToken = CosmosClient.authorizationToken();
|
|
||||||
JunoUtils.getOfficialSampleNotebooks(authToken).then(
|
|
||||||
(data1: DataModels.GitHubInfoJunoResponse[]) => {
|
|
||||||
const officialSamplesData = data1;
|
|
||||||
|
|
||||||
JunoUtils.getLikedNotebooks(authToken).then(
|
|
||||||
(data2: DataModels.LikedNotebooksJunoResponse) => {
|
|
||||||
const likedNotebooksData = data2;
|
|
||||||
|
|
||||||
officialSamplesData.map((value: DataModels.GitHubInfoJunoResponse, index: number) => {
|
|
||||||
value.officialSamplesIndex = index;
|
|
||||||
value.isLikedNotebook = likedNotebooksData.userMetadata.likedNotebooks.includes(value.path);
|
|
||||||
});
|
|
||||||
|
|
||||||
likedNotebooksData.likedNotebooksContent.map((value: DataModels.GitHubInfoJunoResponse) => {
|
|
||||||
value.isLikedNotebook = true;
|
|
||||||
value.officialSamplesIndex = officialSamplesData.findIndex(
|
|
||||||
(officialSample: DataModels.GitHubInfoJunoResponse) => {
|
|
||||||
return officialSample.path === value.path;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
officialSamplesData: officialSamplesData,
|
|
||||||
likedNotebooksData: likedNotebooksData
|
|
||||||
});
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
|
||||||
ConsoleDataType.Error,
|
|
||||||
`Error fetching liked notebooks: ${JSON.stringify(error)}`
|
|
||||||
);
|
|
||||||
// TODO Add telemetry
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
|
||||||
ConsoleDataType.Error,
|
|
||||||
`Error fetching sample notebooks: ${JSON.stringify(error)}`
|
|
||||||
);
|
|
||||||
// TODO Add telemetry
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public render(): JSX.Element {
|
|
||||||
return this.state.officialSamplesData && this.state.likedNotebooksData ? (
|
|
||||||
<GalleryViewerComponent
|
|
||||||
container={this.props.container}
|
|
||||||
officialSamplesData={this.state.officialSamplesData}
|
|
||||||
likedNotebookData={this.state.likedNotebooksData}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GalleryViewerComponentProps {
|
|
||||||
container: ViewModels.Explorer;
|
|
||||||
officialSamplesData: DataModels.GitHubInfoJunoResponse[];
|
|
||||||
likedNotebookData: DataModels.LikedNotebooksJunoResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GalleryViewerComponent extends React.Component<GalleryViewerComponentProps> {
|
|
||||||
public render(): JSX.Element {
|
|
||||||
return this.props.container ? (
|
|
||||||
<div className="galleryContainer">
|
|
||||||
<FullWidthTabs
|
|
||||||
officialSamplesContent={this.props.officialSamplesData}
|
|
||||||
likedNotebooksContent={this.props.likedNotebookData.likedNotebooksContent}
|
|
||||||
userMetadata={this.props.likedNotebookData.userMetadata}
|
|
||||||
onClick={this.openNotebookViewer}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="galleryContainer">
|
|
||||||
<GalleryCardsComponent
|
|
||||||
data={this.props.officialSamplesData}
|
|
||||||
onClick={this.openNotebookViewer}
|
|
||||||
userMetadata={undefined}
|
|
||||||
onNotebookMetadataChange={undefined}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getOfficialSamplesData(): DataModels.GitHubInfoJunoResponse[] {
|
|
||||||
return this.props.officialSamplesData;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getLikedNotebookData(): DataModels.LikedNotebooksJunoResponse {
|
|
||||||
return this.props.likedNotebookData;
|
|
||||||
}
|
|
||||||
|
|
||||||
public openNotebookViewer = async (
|
|
||||||
url: string,
|
|
||||||
notebookMetadata: DataModels.NotebookMetadata,
|
|
||||||
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
|
|
||||||
isLikedNotebook: boolean
|
|
||||||
): Promise<void> => {
|
|
||||||
if (!this.props.container) {
|
|
||||||
SessionStorageUtility.setEntryString(
|
|
||||||
StorageKey.NotebookMetadata,
|
|
||||||
notebookMetadata ? JSON.stringify(notebookMetadata) : null
|
|
||||||
);
|
|
||||||
SessionStorageUtility.setEntryString(StorageKey.NotebookName, path.basename(url));
|
|
||||||
window.open(`${config.hostedExplorerURL}notebookViewer.html?notebookurl=${url}`, "_blank");
|
|
||||||
} else {
|
|
||||||
this.props.container.openNotebookViewer(url, notebookMetadata, onNotebookMetadataChange, isLikedNotebook);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`FullWidthTabs renders 1`] = `
|
|
||||||
<TabComponent
|
|
||||||
currentTabIndex={0}
|
|
||||||
hideHeader={false}
|
|
||||||
onTabIndexChange={[Function]}
|
|
||||||
tabs={
|
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"content": Object {
|
|
||||||
"className": "",
|
|
||||||
"render": [Function],
|
|
||||||
},
|
|
||||||
"isVisible": [Function],
|
|
||||||
"title": "Official Samples",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"content": Object {
|
|
||||||
"className": "",
|
|
||||||
"render": [Function],
|
|
||||||
},
|
|
||||||
"isVisible": [Function],
|
|
||||||
"title": "Liked Notebooks",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`GalleryCardComponent renders 1`] = `
|
|
||||||
<div
|
|
||||||
className="galleryContainer"
|
|
||||||
>
|
|
||||||
<GalleryCardsComponent
|
|
||||||
data={Array []}
|
|
||||||
onClick={[Function]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`GalleryCardsComponent renders 1`] = `
|
|
||||||
<Stack
|
|
||||||
horizontal={true}
|
|
||||||
tokens={
|
|
||||||
Object {
|
|
||||||
"childrenGap": 30,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wrap={true}
|
|
||||||
/>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`GalleryViewerContainerComponent renders 1`] = `<Fragment />`;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
.notebookViewerMetadataContainer {
|
|
||||||
margin: 0px 10px;
|
|
||||||
|
|
||||||
.title, .decoration, .persona {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.extras {
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { shallow } from "enzyme";
|
|
||||||
import { NotebookMetadataComponentProps, NotebookMetadataComponent } from "./NotebookMetadataComponent";
|
|
||||||
|
|
||||||
describe("NotebookMetadataComponent", () => {
|
|
||||||
it("renders un-liked notebook", () => {
|
|
||||||
const props: NotebookMetadataComponentProps = {
|
|
||||||
notebookName: "My notebook",
|
|
||||||
container: undefined,
|
|
||||||
notebookMetadata: undefined,
|
|
||||||
notebookContent: {},
|
|
||||||
onNotebookMetadataChange: () => Promise.resolve(),
|
|
||||||
isLikedNotebook: false
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders liked notebook", () => {
|
|
||||||
const props: NotebookMetadataComponentProps = {
|
|
||||||
notebookName: "My notebook",
|
|
||||||
container: undefined,
|
|
||||||
notebookMetadata: undefined,
|
|
||||||
notebookContent: {},
|
|
||||||
onNotebookMetadataChange: () => Promise.resolve(),
|
|
||||||
isLikedNotebook: true
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO Add test for metadata display
|
|
||||||
});
|
|
||||||
@@ -6,97 +6,47 @@ import * as React from "react";
|
|||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import { NotebookMetadata } from "../../../Contracts/DataModels";
|
import { NotebookMetadata } from "../../../Contracts/DataModels";
|
||||||
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
||||||
import { Icon, Persona, Text, IconButton } from "office-ui-fabric-react";
|
import { Icon, Persona, Text } from "office-ui-fabric-react";
|
||||||
|
import CSS from "csstype";
|
||||||
import {
|
import {
|
||||||
siteTextStyles,
|
siteTextStyles,
|
||||||
subtleIconStyles,
|
subtleIconStyles,
|
||||||
iconStyles,
|
iconStyles,
|
||||||
iconButtonStyles,
|
|
||||||
mainHelpfulTextStyles,
|
mainHelpfulTextStyles,
|
||||||
subtleHelpfulTextStyles,
|
subtleHelpfulTextStyles,
|
||||||
helpfulTextStyles
|
helpfulTextStyles
|
||||||
} from "../NotebookGallery/Cards/CardStyleConstants";
|
} from "../../../GalleryViewer/Cards/CardStyleConstants";
|
||||||
|
|
||||||
import "./NotebookViewerComponent.less";
|
|
||||||
|
|
||||||
initializeIcons();
|
initializeIcons();
|
||||||
|
|
||||||
export interface NotebookMetadataComponentProps {
|
interface NotebookMetadataComponentProps {
|
||||||
notebookName: string;
|
notebookName: string;
|
||||||
container: ViewModels.Explorer;
|
container: ViewModels.Explorer;
|
||||||
notebookMetadata: NotebookMetadata;
|
notebookMetadata: NotebookMetadata;
|
||||||
notebookContent: any;
|
notebookContent: any;
|
||||||
onNotebookMetadataChange: (newNotebookMetadata: NotebookMetadata) => Promise<void>;
|
|
||||||
isLikedNotebook: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NotebookMetadatComponentState {
|
export class NotebookMetadataComponent extends React.Component<NotebookMetadataComponentProps> {
|
||||||
liked: boolean;
|
private inlineBlockStyle: CSS.Properties = {
|
||||||
notebookMetadata: NotebookMetadata;
|
display: "inline-block"
|
||||||
}
|
|
||||||
|
|
||||||
export class NotebookMetadataComponent extends React.Component<
|
|
||||||
NotebookMetadataComponentProps,
|
|
||||||
NotebookMetadatComponentState
|
|
||||||
> {
|
|
||||||
constructor(props: NotebookMetadataComponentProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
liked: this.props.isLikedNotebook,
|
|
||||||
notebookMetadata: this.props.notebookMetadata
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private onDownloadClick = (newNotebookName: string) => {
|
|
||||||
this.props.container
|
|
||||||
.importAndOpenFromGallery(this.props.notebookName, newNotebookName, JSON.stringify(this.props.notebookContent))
|
|
||||||
.then(() => {
|
|
||||||
if (this.props.notebookMetadata) {
|
|
||||||
if (this.props.onNotebookMetadataChange) {
|
|
||||||
const notebookMetadata = { ...this.state.notebookMetadata };
|
|
||||||
notebookMetadata.downloads += 1;
|
|
||||||
this.props.onNotebookMetadataChange(notebookMetadata).then(() => {
|
|
||||||
this.setState({ notebookMetadata: notebookMetadata });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
private marginTopStyle: CSS.Properties = {
|
||||||
if (this.props.onNotebookMetadataChange) {
|
marginTop: "5px"
|
||||||
const notebookMetadata = { ...this.state.notebookMetadata };
|
|
||||||
if (this.props.notebookMetadata) {
|
|
||||||
notebookMetadata.views += 1;
|
|
||||||
this.props.onNotebookMetadataChange(notebookMetadata).then(() => {
|
|
||||||
this.setState({ notebookMetadata: notebookMetadata });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onLike = (): void => {
|
|
||||||
if (this.props.onNotebookMetadataChange) {
|
|
||||||
const notebookMetadata = { ...this.state.notebookMetadata };
|
|
||||||
let liked: boolean;
|
|
||||||
if (this.state.liked) {
|
|
||||||
liked = false;
|
|
||||||
notebookMetadata.likes -= 1;
|
|
||||||
} else {
|
|
||||||
liked = true;
|
|
||||||
notebookMetadata.likes += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.onNotebookMetadataChange(notebookMetadata).then(() => {
|
|
||||||
this.setState({ liked: liked, notebookMetadata: notebookMetadata });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private onDownload = (): void => {
|
private onDownloadClick: (newNotebookName: string) => void = (newNotebookName: string) => {
|
||||||
|
this.props.container.importAndOpenFromGallery(
|
||||||
|
this.props.notebookName,
|
||||||
|
newNotebookName,
|
||||||
|
JSON.stringify(this.props.notebookContent)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
public render(): JSX.Element {
|
||||||
const promptForNotebookName = () => {
|
const promptForNotebookName = () => {
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
let newNotebookName = this.props.notebookName;
|
var newNotebookName = this.props.notebookName;
|
||||||
this.props.container.showOkCancelTextFieldModalDialog(
|
this.props.container.showOkCancelTextFieldModalDialog(
|
||||||
"Save notebook as",
|
"Save notebook as",
|
||||||
undefined,
|
undefined,
|
||||||
@@ -118,35 +68,27 @@ export class NotebookMetadataComponent extends React.Component<
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
promptForNotebookName().then((newNotebookName: string) => {
|
|
||||||
this.onDownloadClick(newNotebookName);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
public render(): JSX.Element {
|
|
||||||
return (
|
return (
|
||||||
<div className="notebookViewerMetadataContainer">
|
<div className="notebookViewerMetadataContainer">
|
||||||
<h3 className="title">{this.props.notebookName}</h3>
|
<h3 style={this.inlineBlockStyle}>{this.props.notebookName}</h3>
|
||||||
|
|
||||||
{this.props.notebookMetadata && (
|
{this.props.notebookMetadata && (
|
||||||
<div className="decoration">
|
<div style={this.inlineBlockStyle}>
|
||||||
{this.props.container ? (
|
<Icon iconName="Heart" styles={iconStyles} />
|
||||||
<IconButton
|
<Text variant="medium" styles={mainHelpfulTextStyles}>
|
||||||
iconProps={{ iconName: this.state.liked ? "HeartFill" : "Heart" }}
|
{this.props.notebookMetadata.likes} likes
|
||||||
styles={iconButtonStyles}
|
|
||||||
onClick={this.onLike}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Icon iconName="Heart" styles={iconStyles} />
|
|
||||||
)}
|
|
||||||
<Text variant="large" styles={mainHelpfulTextStyles}>
|
|
||||||
{this.state.notebookMetadata.likes} likes
|
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{this.props.container && (
|
{this.props.container && (
|
||||||
<button aria-label="downloadButton" className="downloadButton" onClick={this.onDownload}>
|
<button
|
||||||
|
aria-label="downloadButton"
|
||||||
|
className="downloadButton"
|
||||||
|
onClick={async () => {
|
||||||
|
promptForNotebookName().then(this.onDownloadClick);
|
||||||
|
}}
|
||||||
|
>
|
||||||
Download Notebook
|
Download Notebook
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -155,20 +97,20 @@ export class NotebookMetadataComponent extends React.Component<
|
|||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<Persona
|
<Persona
|
||||||
className="persona"
|
style={this.inlineBlockStyle}
|
||||||
text={this.props.notebookMetadata.author}
|
text={this.props.notebookMetadata.author}
|
||||||
secondaryText={this.props.notebookMetadata.date}
|
secondaryText={this.props.notebookMetadata.date}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="extras">
|
<div style={this.marginTopStyle}>
|
||||||
<Icon iconName="RedEye" styles={subtleIconStyles} />
|
<Icon iconName="RedEye" styles={subtleIconStyles} />
|
||||||
<Text variant="small" styles={subtleHelpfulTextStyles}>
|
<Text variant="small" styles={subtleHelpfulTextStyles}>
|
||||||
{this.state.notebookMetadata.views}
|
{this.props.notebookMetadata.views}
|
||||||
</Text>
|
</Text>
|
||||||
<Icon iconName="Download" styles={subtleIconStyles} />
|
<Icon iconName="Download" styles={subtleIconStyles} />
|
||||||
<Text variant="small" styles={subtleHelpfulTextStyles}>
|
<Text variant="small" styles={subtleHelpfulTextStyles}>
|
||||||
{this.state.notebookMetadata.downloads}
|
{this.props.notebookMetadata.downloads}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<Text variant="small" styles={siteTextStyles}>
|
<Text variant="small" styles={siteTextStyles}>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import * as ReactDOM from "react-dom";
|
import * as ReactDOM from "react-dom";
|
||||||
import "bootstrap/dist/css/bootstrap.css";
|
import "bootstrap/dist/css/bootstrap.css";
|
||||||
import { NotebookMetadata } from "../Contracts/DataModels";
|
import { NotebookMetadata } from "../../../Contracts/DataModels";
|
||||||
import { NotebookViewerComponent } from "../Explorer/Controls/NotebookViewer/NotebookViewerComponent";
|
import { NotebookViewerComponent } from "./NotebookViewerComponent";
|
||||||
import { SessionStorageUtility, StorageKey } from "../Shared/StorageUtility";
|
import { SessionStorageUtility, StorageKey } from "../../../Shared/StorageUtility";
|
||||||
|
|
||||||
const getNotebookUrl = (): string => {
|
const getNotebookUrl = (): string => {
|
||||||
const regex: RegExp = new RegExp("[?&]notebookurl=([^&#]*)|&|#|$");
|
const regex: RegExp = new RegExp("[?&]notebookurl=([^&#]*)|&|#|$");
|
||||||
@@ -26,14 +26,12 @@ const onInit = async () => {
|
|||||||
SessionStorageUtility.removeEntry(StorageKey.NotebookName);
|
SessionStorageUtility.removeEntry(StorageKey.NotebookName);
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
|
|
||||||
const notebookViewerComponent = (
|
const notebookViewerComponent = (
|
||||||
<NotebookViewerComponent
|
<NotebookViewerComponent
|
||||||
notebookMetadata={notebookMetadata}
|
notebookMetadata={notebookMetadata}
|
||||||
notebookName={notebookName}
|
notebookName={notebookName}
|
||||||
notebookUrl={getNotebookUrl()}
|
notebookUrl={getNotebookUrl()}
|
||||||
hideInputs={urlParams.get("hideinputs") === "true"}
|
container={null}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
ReactDOM.render(notebookViewerComponent, document.getElementById("notebookContent"));
|
ReactDOM.render(notebookViewerComponent, document.getElementById("notebookContent"));
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
padding: @DefaultSpace;
|
padding: @DefaultSpace;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.downloadButton {
|
.downloadButton {
|
||||||
|
|||||||
@@ -16,14 +16,12 @@ import "./NotebookViewerComponent.less";
|
|||||||
export interface NotebookViewerComponentProps {
|
export interface NotebookViewerComponentProps {
|
||||||
notebookName: string;
|
notebookName: string;
|
||||||
notebookUrl: string;
|
notebookUrl: string;
|
||||||
container?: ViewModels.Explorer;
|
container: ViewModels.Explorer;
|
||||||
notebookMetadata: NotebookMetadata;
|
notebookMetadata: NotebookMetadata;
|
||||||
onNotebookMetadataChange?: (newNotebookMetadata: NotebookMetadata) => Promise<void>;
|
|
||||||
isLikedNotebook?: boolean;
|
|
||||||
hideInputs?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NotebookViewerComponentState {
|
interface NotebookViewerComponentState {
|
||||||
|
element: JSX.Element;
|
||||||
content: any;
|
content: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +50,7 @@ export class NotebookViewerComponent extends React.Component<
|
|||||||
contentRef: createContentRef()
|
contentRef: createContentRef()
|
||||||
});
|
});
|
||||||
|
|
||||||
this.state = { content: undefined };
|
this.state = { element: undefined, content: undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getJsonNotebookContent(): Promise<any> {
|
private async getJsonNotebookContent(): Promise<any> {
|
||||||
@@ -67,25 +65,24 @@ export class NotebookViewerComponent extends React.Component<
|
|||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.getJsonNotebookContent().then((jsonContent: any) => {
|
this.getJsonNotebookContent().then((jsonContent: any) => {
|
||||||
this.notebookComponentBootstrapper.setContent("json", jsonContent);
|
this.notebookComponentBootstrapper.setContent("json", jsonContent);
|
||||||
this.setState({ content: jsonContent });
|
const notebookReadonlyComponent = this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer);
|
||||||
|
this.setState({ element: notebookReadonlyComponent, content: jsonContent });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
return (
|
return this.state != null ? (
|
||||||
<div className="notebookViewerContainer">
|
<div className="notebookViewerContainer">
|
||||||
<NotebookMetadataComponent
|
<NotebookMetadataComponent
|
||||||
notebookMetadata={this.props.notebookMetadata}
|
notebookMetadata={this.props.notebookMetadata}
|
||||||
notebookName={this.props.notebookName}
|
notebookName={this.props.notebookName}
|
||||||
container={this.props.container}
|
container={this.props.container}
|
||||||
notebookContent={this.state.content}
|
notebookContent={this.state.content}
|
||||||
onNotebookMetadataChange={this.props.onNotebookMetadataChange}
|
|
||||||
isLikedNotebook={this.props.isLikedNotebook}
|
|
||||||
/>
|
/>
|
||||||
{this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer, {
|
{this.state.element}
|
||||||
hideInputs: this.props.hideInputs
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`NotebookMetadataComponent renders liked notebook 1`] = `
|
|
||||||
<div
|
|
||||||
className="notebookViewerMetadataContainer"
|
|
||||||
>
|
|
||||||
<h3
|
|
||||||
className="title"
|
|
||||||
>
|
|
||||||
My notebook
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
|
|
||||||
<div
|
|
||||||
className="notebookViewerMetadataContainer"
|
|
||||||
>
|
|
||||||
<h3
|
|
||||||
className="title"
|
|
||||||
>
|
|
||||||
My notebook
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
@@ -1,29 +1,27 @@
|
|||||||
@import "../../../../less/Common/Constants";
|
@import "../../../../less/Common/Constants";
|
||||||
|
|
||||||
.tabComponentContainer {
|
.tabSwitch {
|
||||||
overflow: hidden;
|
margin-left: @LargeSpace;
|
||||||
height: 100%;
|
margin-bottom: 20px;
|
||||||
.flex-display();
|
|
||||||
.flex-direction();
|
|
||||||
|
|
||||||
.tabSwitch {
|
.tab {
|
||||||
margin-left: @LargeSpace;
|
margin-right: @MediumSpace;
|
||||||
margin-bottom: 20px;
|
}
|
||||||
|
|
||||||
.tab {
|
.toggleSwitch {
|
||||||
margin-right: @MediumSpace;
|
.toggleSwitch();
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggleSwitch {
|
.selectedToggle {
|
||||||
.toggleSwitch();
|
.selectedToggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectedToggle {
|
.unselectedToggle {
|
||||||
.selectedToggle();
|
.unselectedToggle();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.unselectedToggle {
|
|
||||||
.unselectedToggle();
|
.tabComponentContent {
|
||||||
}
|
height: calc(100% - 20px);
|
||||||
}
|
.flex-display();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
|
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
|
||||||
import "./TabComponent.less";
|
|
||||||
|
|
||||||
export interface TabContent {
|
export interface TabContent {
|
||||||
render: () => JSX.Element;
|
render: () => JSX.Element;
|
||||||
@@ -76,10 +75,10 @@ export class TabComponent extends React.Component<TabComponentProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tabComponentContainer">
|
<React.Fragment>
|
||||||
{!this.props.hideHeader && <div className="tabs tabSwitch">{this.renderTabTitles()}</div>}
|
{!this.props.hideHeader && <div className="tabs tabSwitch">{this.renderTabTitles()}</div>}
|
||||||
<div className={className}>{currentTabContent.render()}</div>
|
<div className={className}>{currentTabContent.render()}</div>
|
||||||
</div>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import EnvironmentUtility from "../Common/EnvironmentUtility";
|
|||||||
import GraphStylingPane from "./Panes/GraphStylingPane";
|
import GraphStylingPane from "./Panes/GraphStylingPane";
|
||||||
import hasher from "hasher";
|
import hasher from "hasher";
|
||||||
import NewVertexPane from "./Panes/NewVertexPane";
|
import NewVertexPane from "./Panes/NewVertexPane";
|
||||||
|
import NotebookTab from "./Tabs/NotebookTab";
|
||||||
import NotebookV2Tab from "./Tabs/NotebookV2Tab";
|
import NotebookV2Tab from "./Tabs/NotebookV2Tab";
|
||||||
import Q from "q";
|
import Q from "q";
|
||||||
import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
|
import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
|
||||||
@@ -85,7 +86,6 @@ import { StringInputPane } from "./Panes/StringInputPane";
|
|||||||
import { TableColumnOptionsPane } from "./Panes/Tables/TableColumnOptionsPane";
|
import { TableColumnOptionsPane } from "./Panes/Tables/TableColumnOptionsPane";
|
||||||
import { UploadFilePane } from "./Panes/UploadFilePane";
|
import { UploadFilePane } from "./Panes/UploadFilePane";
|
||||||
import { UploadItemsPane } from "./Panes/UploadItemsPane";
|
import { UploadItemsPane } from "./Panes/UploadItemsPane";
|
||||||
import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter";
|
|
||||||
|
|
||||||
BindingHandlersRegisterer.registerBindingHandlers();
|
BindingHandlersRegisterer.registerBindingHandlers();
|
||||||
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
||||||
@@ -154,6 +154,7 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
public selectedNode: ko.Observable<ViewModels.TreeNode>;
|
public selectedNode: ko.Observable<ViewModels.TreeNode>;
|
||||||
public isRefreshingExplorer: ko.Observable<boolean>;
|
public isRefreshingExplorer: ko.Observable<boolean>;
|
||||||
private resourceTree: ResourceTreeAdapter;
|
private resourceTree: ResourceTreeAdapter;
|
||||||
|
private enableLegacyResourceTree: ko.Observable<boolean>;
|
||||||
|
|
||||||
// Resource Token
|
// Resource Token
|
||||||
public resourceTokenDatabaseId: ko.Observable<string>;
|
public resourceTokenDatabaseId: ko.Observable<string>;
|
||||||
@@ -187,7 +188,6 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
public executeSprocParamsPane: ViewModels.ExecuteSprocParamsPane;
|
public executeSprocParamsPane: ViewModels.ExecuteSprocParamsPane;
|
||||||
public renewAdHocAccessPane: ViewModels.RenewAdHocAccessPane;
|
public renewAdHocAccessPane: ViewModels.RenewAdHocAccessPane;
|
||||||
public uploadItemsPane: ViewModels.UploadItemsPane;
|
public uploadItemsPane: ViewModels.UploadItemsPane;
|
||||||
public uploadItemsPaneAdapter: UploadItemsPaneAdapter;
|
|
||||||
public loadQueryPane: ViewModels.LoadQueryPane;
|
public loadQueryPane: ViewModels.LoadQueryPane;
|
||||||
public saveQueryPane: ViewModels.ContextualPane;
|
public saveQueryPane: ViewModels.ContextualPane;
|
||||||
public browseQueriesPane: ViewModels.BrowseQueriesPane;
|
public browseQueriesPane: ViewModels.BrowseQueriesPane;
|
||||||
@@ -203,8 +203,8 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
// features
|
// features
|
||||||
public isGalleryEnabled: ko.Computed<boolean>;
|
public isGalleryEnabled: ko.Computed<boolean>;
|
||||||
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||||
|
public isGraphsEnabled: ko.Computed<boolean>;
|
||||||
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
|
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
|
||||||
public isRightPanelV2Enabled: ko.Computed<boolean>;
|
|
||||||
public canExceedMaximumValue: ko.Computed<boolean>;
|
public canExceedMaximumValue: ko.Computed<boolean>;
|
||||||
public hasAutoPilotV2FeatureFlag: ko.Computed<boolean>;
|
public hasAutoPilotV2FeatureFlag: ko.Computed<boolean>;
|
||||||
|
|
||||||
@@ -303,7 +303,7 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
this.openedTabs &&
|
this.openedTabs &&
|
||||||
this.openedTabs().forEach(tab => {
|
this.openedTabs().forEach(tab => {
|
||||||
if (tab.tabKind === ViewModels.CollectionTabKind.Notebook) {
|
if (tab.tabKind === ViewModels.CollectionTabKind.Notebook) {
|
||||||
throw new Error("NotebookTab is deprecated. Use NotebookV2Tab");
|
(tab as NotebookTab).reconfigureServiceEndpoints();
|
||||||
} else if (tab.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
|
} else if (tab.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
|
||||||
(tab as NotebookV2Tab).reconfigureServiceEndpoints();
|
(tab as NotebookV2Tab).reconfigureServiceEndpoints();
|
||||||
}
|
}
|
||||||
@@ -381,6 +381,7 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
this.armEndpoint = ko.observable<string>(undefined);
|
this.armEndpoint = ko.observable<string>(undefined);
|
||||||
this.queriesClient = new QueriesClient(this);
|
this.queriesClient = new QueriesClient(this);
|
||||||
this.isTryCosmosDBSubscription = ko.observable<boolean>(false);
|
this.isTryCosmosDBSubscription = ko.observable<boolean>(false);
|
||||||
|
this.enableLegacyResourceTree = ko.observable<boolean>(false);
|
||||||
|
|
||||||
this.resourceTokenDatabaseId = ko.observable<string>();
|
this.resourceTokenDatabaseId = ko.observable<string>();
|
||||||
this.resourceTokenCollectionId = ko.observable<string>();
|
this.resourceTokenCollectionId = ko.observable<string>();
|
||||||
@@ -410,6 +411,9 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
this.shouldShowContextSwitchPrompt = ko.observable<boolean>(false);
|
this.shouldShowContextSwitchPrompt = ko.observable<boolean>(false);
|
||||||
this.isGalleryEnabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableGallery));
|
this.isGalleryEnabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableGallery));
|
||||||
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
|
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
|
||||||
|
this.isGraphsEnabled = ko.computed<boolean>(() => {
|
||||||
|
return this.isFeatureEnabled(Constants.Features.graphs);
|
||||||
|
});
|
||||||
|
|
||||||
this.canExceedMaximumValue = ko.computed<boolean>(() =>
|
this.canExceedMaximumValue = ko.computed<boolean>(() =>
|
||||||
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
|
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
|
||||||
@@ -547,9 +551,6 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
!this.isRunningOnNationalCloud() &&
|
!this.isRunningOnNationalCloud() &&
|
||||||
!this.isPreferredApiGraph()
|
!this.isPreferredApiGraph()
|
||||||
);
|
);
|
||||||
this.isRightPanelV2Enabled = ko.computed<boolean>(() =>
|
|
||||||
this.isFeatureEnabled(Constants.Features.enableRightPanelV2)
|
|
||||||
);
|
|
||||||
this.defaultExperience.subscribe((defaultExperience: string) => {
|
this.defaultExperience.subscribe((defaultExperience: string) => {
|
||||||
if (
|
if (
|
||||||
defaultExperience &&
|
defaultExperience &&
|
||||||
@@ -706,8 +707,6 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
container: this
|
container: this
|
||||||
});
|
});
|
||||||
|
|
||||||
this.uploadItemsPaneAdapter = new UploadItemsPaneAdapter(this);
|
|
||||||
|
|
||||||
this.loadQueryPane = new LoadQueryPane({
|
this.loadQueryPane = new LoadQueryPane({
|
||||||
documentClientUtility: this.documentClientUtility,
|
documentClientUtility: this.documentClientUtility,
|
||||||
id: "loadquerypane",
|
id: "loadquerypane",
|
||||||
@@ -1101,6 +1100,8 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
this.sparkClusterConnectionInfo.valueHasMutated();
|
this.sparkClusterConnectionInfo.valueHasMutated();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.enableLegacyResourceTree(this.isFeatureEnabled(Constants.Features.enableLegacyResourceTree));
|
||||||
|
|
||||||
featureSubcription.dispose();
|
featureSubcription.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2582,7 +2583,7 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
const item = NotebookUtil.createNotebookContentItem(name, path, "file");
|
const item = NotebookUtil.createNotebookContentItem(name, path, "file");
|
||||||
const parent = this.resourceTree.myNotebooksContentRoot;
|
const parent = this.resourceTree.myNotebooksContentRoot;
|
||||||
|
|
||||||
if (parent && parent.children && this.isNotebookEnabled() && this.notebookClient) {
|
if (parent && this.isNotebookEnabled() && this.notebookClient) {
|
||||||
if (this._filePathToImportAndOpen === path) {
|
if (this._filePathToImportAndOpen === path) {
|
||||||
this._filePathToImportAndOpen = null; // we don't want to try opening this path again
|
this._filePathToImportAndOpen = null; // we don't want to try opening this path again
|
||||||
}
|
}
|
||||||
@@ -2767,7 +2768,7 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
const openedNotebookTabs = this.openedTabs().filter(
|
const openedNotebookTabs = this.openedTabs().filter(
|
||||||
(tab: ViewModels.Tab) =>
|
(tab: ViewModels.Tab) =>
|
||||||
tab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
|
tab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
|
||||||
(tab as NotebookV2Tab).notebookPath() === notebookFile.path
|
(tab as NotebookTab).notebookPath() === notebookFile.path
|
||||||
);
|
);
|
||||||
if (openedNotebookTabs.length > 0) {
|
if (openedNotebookTabs.length > 0) {
|
||||||
this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again.");
|
this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again.");
|
||||||
@@ -2791,12 +2792,12 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
.filter(
|
.filter(
|
||||||
(tab: ViewModels.Tab) =>
|
(tab: ViewModels.Tab) =>
|
||||||
tab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
|
tab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
|
||||||
FileSystemUtil.isPathEqual((tab as NotebookV2Tab).notebookPath(), originalPath)
|
FileSystemUtil.isPathEqual((tab as NotebookTab).notebookPath(), originalPath)
|
||||||
)
|
)
|
||||||
.forEach(tab => {
|
.forEach(tab => {
|
||||||
tab.tabTitle(newNotebookFile.name);
|
tab.tabTitle(newNotebookFile.name);
|
||||||
tab.tabPath(newNotebookFile.path);
|
tab.tabPath(newNotebookFile.path);
|
||||||
(tab as NotebookV2Tab).notebookPath(newNotebookFile.path);
|
(tab as NotebookTab).notebookPath(newNotebookFile.path);
|
||||||
});
|
});
|
||||||
|
|
||||||
return newNotebookFile;
|
return newNotebookFile;
|
||||||
@@ -3034,7 +3035,7 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
// Don't delete if tab is open to avoid accidental deletion
|
// Don't delete if tab is open to avoid accidental deletion
|
||||||
const openedNotebookTabs = this.openedTabs().filter(
|
const openedNotebookTabs = this.openedTabs().filter(
|
||||||
(tab: ViewModels.Tab) =>
|
(tab: ViewModels.Tab) =>
|
||||||
tab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && (tab as NotebookV2Tab).notebookPath() === item.path
|
tab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && (tab as NotebookTab).notebookPath() === item.path
|
||||||
);
|
);
|
||||||
if (openedNotebookTabs.length > 0) {
|
if (openedNotebookTabs.length > 0) {
|
||||||
this.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again.");
|
this.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again.");
|
||||||
@@ -3277,12 +3278,7 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
newTab.onTabClick();
|
newTab.onTabClick();
|
||||||
}
|
}
|
||||||
|
|
||||||
public openNotebookViewer(
|
public openNotebookViewer(notebookUrl: string, notebookMetadata: DataModels.NotebookMetadata) {
|
||||||
notebookUrl: string,
|
|
||||||
notebookMetadata: DataModels.NotebookMetadata,
|
|
||||||
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
|
|
||||||
isLikedNotebook: boolean
|
|
||||||
) {
|
|
||||||
const notebookName = path.basename(notebookUrl);
|
const notebookName = path.basename(notebookUrl);
|
||||||
const title = notebookName;
|
const title = notebookName;
|
||||||
const hashLocation = notebookUrl;
|
const hashLocation = notebookUrl;
|
||||||
@@ -3324,9 +3320,7 @@ export default class Explorer implements ViewModels.Explorer {
|
|||||||
openedTabs: this.openedTabs(),
|
openedTabs: this.openedTabs(),
|
||||||
notebookUrl: notebookUrl,
|
notebookUrl: notebookUrl,
|
||||||
notebookName: notebookName,
|
notebookName: notebookName,
|
||||||
notebookMetadata: notebookMetadata,
|
notebookMetadata: notebookMetadata
|
||||||
onNotebookMetadataChange: onNotebookMetadataChange,
|
|
||||||
isLikedNotebook: isLikedNotebook
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.openedTabs.push(newTab);
|
this.openedTabs.push(newTab);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import _ from "underscore";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import { Observable } from "knockout";
|
import { Observable } from "knockout";
|
||||||
@@ -47,7 +46,7 @@ export class CommandBarUtil {
|
|||||||
text: btn.commandButtonLabel || btn.tooltipText,
|
text: btn.commandButtonLabel || btn.tooltipText,
|
||||||
"data-test": btn.commandButtonLabel || btn.tooltipText,
|
"data-test": btn.commandButtonLabel || btn.tooltipText,
|
||||||
title: btn.tooltipText,
|
title: btn.tooltipText,
|
||||||
name: btn.commandButtonLabel || btn.tooltipText,
|
name: "menuitem",
|
||||||
disabled: btn.disabled,
|
disabled: btn.disabled,
|
||||||
ariaLabel: btn.ariaLabel,
|
ariaLabel: btn.ariaLabel,
|
||||||
buttonStyles: {
|
buttonStyles: {
|
||||||
@@ -127,9 +126,6 @@ export class CommandBarUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (btn.isDropdown) {
|
if (btn.isDropdown) {
|
||||||
const selectedChild = _.find(btn.children, child => child.dropdownItemKey === btn.dropdownSelectedKey);
|
|
||||||
result.name = selectedChild?.commandButtonLabel || btn.dropdownPlaceholder;
|
|
||||||
|
|
||||||
const dropdownStyles: Partial<IDropdownStyles> = {
|
const dropdownStyles: Partial<IDropdownStyles> = {
|
||||||
root: { margin: 5 },
|
root: { margin: 5 },
|
||||||
dropdown: { width: btn.dropdownWidth },
|
dropdown: { width: btn.dropdownWidth },
|
||||||
|
|||||||
43
src/Explorer/Menus/ContextMenu.ts
Normal file
43
src/Explorer/Menus/ContextMenu.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { CellId } from "@nteract/commutable";
|
|
||||||
import { ContentRef } from "@nteract/core";
|
import { ContentRef } from "@nteract/core";
|
||||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
import { CellId } from "@nteract/commutable";
|
||||||
|
|
||||||
export const CLOSE_NOTEBOOK = "CLOSE_NOTEBOOK";
|
export const CLOSE_NOTEBOOK = "CLOSE_NOTEBOOK";
|
||||||
export interface CloseNotebookAction {
|
export interface CloseNotebookAction {
|
||||||
@@ -17,6 +16,25 @@ export const closeNotebook = (payload: { contentRef: ContentRef }): CloseNoteboo
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const UPDATE_LAST_MODIFIED = "UPDATE_LAST_MODIFIED";
|
||||||
|
export interface UpdateLastModifiedAction {
|
||||||
|
type: "UPDATE_LAST_MODIFIED";
|
||||||
|
payload: {
|
||||||
|
contentRef: ContentRef;
|
||||||
|
lastModified: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateLastModified = (payload: {
|
||||||
|
contentRef: ContentRef;
|
||||||
|
lastModified: string;
|
||||||
|
}): UpdateLastModifiedAction => {
|
||||||
|
return {
|
||||||
|
type: UPDATE_LAST_MODIFIED,
|
||||||
|
payload
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT = "EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT";
|
export const EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT = "EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT";
|
||||||
export interface ExecuteFocusedCellAndFocusNextAction {
|
export interface ExecuteFocusedCellAndFocusNextAction {
|
||||||
type: "EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT";
|
type: "EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT";
|
||||||
@@ -63,24 +81,3 @@ export const setHoveredCell = (payload: { cellId: CellId }): SetHoveredCellActio
|
|||||||
payload
|
payload
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TRACE_NOTEBOOK_TELEMETRY = "TRACE_NOTEBOOK_TELEMETRY";
|
|
||||||
export interface TraceNotebookTelemetryAction {
|
|
||||||
type: "TRACE_NOTEBOOK_TELEMETRY";
|
|
||||||
payload: {
|
|
||||||
action: Action;
|
|
||||||
actionModifier?: string;
|
|
||||||
data?: any;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const traceNotebookTelemetry = (payload: {
|
|
||||||
action: Action;
|
|
||||||
actionModifier?: string;
|
|
||||||
data?: any;
|
|
||||||
}): TraceNotebookTelemetryAction => {
|
|
||||||
return {
|
|
||||||
type: TRACE_NOTEBOOK_TELEMETRY,
|
|
||||||
payload
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { empty, merge, of, timer, concat, Subject, Subscriber, Observable, Observer } from "rxjs";
|
import { empty, merge, of, timer, interval, concat, Subject, Subscriber, Observable, Observer } from "rxjs";
|
||||||
import { webSocket } from "rxjs/webSocket";
|
import { webSocket } from "rxjs/webSocket";
|
||||||
import { ActionsObservable, StateObservable } from "redux-observable";
|
import { ActionsObservable, StateObservable } from "redux-observable";
|
||||||
import { ofType } from "redux-observable";
|
import { ofType } from "redux-observable";
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
map,
|
map,
|
||||||
switchMap,
|
switchMap,
|
||||||
take,
|
take,
|
||||||
|
distinctUntilChanged,
|
||||||
filter,
|
filter,
|
||||||
catchError,
|
catchError,
|
||||||
first,
|
first,
|
||||||
@@ -20,6 +21,7 @@ import {
|
|||||||
AppState,
|
AppState,
|
||||||
ServerConfig as JupyterServerConfig,
|
ServerConfig as JupyterServerConfig,
|
||||||
JupyterHostRecordProps,
|
JupyterHostRecordProps,
|
||||||
|
JupyterHostRecord,
|
||||||
RemoteKernelProps,
|
RemoteKernelProps,
|
||||||
castToSessionId,
|
castToSessionId,
|
||||||
createKernelRef,
|
createKernelRef,
|
||||||
@@ -27,7 +29,8 @@ import {
|
|||||||
ContentRef,
|
ContentRef,
|
||||||
KernelInfo,
|
KernelInfo,
|
||||||
actions,
|
actions,
|
||||||
selectors
|
selectors,
|
||||||
|
IContentProvider
|
||||||
} from "@nteract/core";
|
} from "@nteract/core";
|
||||||
import { message, JupyterMessage, Channels, createMessage, childOf, ofMessageType } from "@nteract/messaging";
|
import { message, JupyterMessage, Channels, createMessage, childOf, ofMessageType } from "@nteract/messaging";
|
||||||
import { sessions, kernels } from "rx-jupyter";
|
import { sessions, kernels } from "rx-jupyter";
|
||||||
@@ -749,6 +752,69 @@ export const cleanKernelOnConnectionLostEpic = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Workaround for issue: https://github.com/nteract/nteract/issues/4583
|
||||||
|
* We reajust the property
|
||||||
|
* @param action$
|
||||||
|
* @param state$
|
||||||
|
*/
|
||||||
|
const adjustLastModifiedOnSaveEpic = (
|
||||||
|
action$: ActionsObservable<actions.SaveFulfilled>,
|
||||||
|
state$: StateObservable<AppState>,
|
||||||
|
dependencies: { contentProvider: IContentProvider }
|
||||||
|
): Observable<{} | CdbActions.UpdateLastModifiedAction> => {
|
||||||
|
return action$.pipe(
|
||||||
|
ofType(actions.SAVE_FULFILLED),
|
||||||
|
mergeMap(action => {
|
||||||
|
const pollDelayMs = 500;
|
||||||
|
const nbAttempts = 4;
|
||||||
|
|
||||||
|
// Retry updating last modified
|
||||||
|
const currentHost = selectors.currentHost(state$.value);
|
||||||
|
const serverConfig = selectors.serverConfig(currentHost as JupyterHostRecord);
|
||||||
|
const filepath = selectors.filepath(state$.value, { contentRef: action.payload.contentRef });
|
||||||
|
const content = selectors.content(state$.value, { contentRef: action.payload.contentRef });
|
||||||
|
const lastSaved = (content.lastSaved as any) as string;
|
||||||
|
const contentProvider = dependencies.contentProvider;
|
||||||
|
|
||||||
|
// Query until value is stable
|
||||||
|
return interval(pollDelayMs)
|
||||||
|
.pipe(take(nbAttempts))
|
||||||
|
.pipe(
|
||||||
|
mergeMap(x =>
|
||||||
|
contentProvider.get(serverConfig, filepath, { content: 0 }).pipe(
|
||||||
|
map(xhr => {
|
||||||
|
if (xhr.status !== 200 || typeof xhr.response === "string") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const model = xhr.response;
|
||||||
|
const lastModified = model.last_modified;
|
||||||
|
if (lastModified === lastSaved) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
// Return last modified
|
||||||
|
return lastModified;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
mergeMap(lastModified => {
|
||||||
|
if (!lastModified) {
|
||||||
|
return empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
return of(
|
||||||
|
CdbActions.updateLastModified({
|
||||||
|
contentRef: action.payload.contentRef,
|
||||||
|
lastModified
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute focused cell and focus next cell
|
* Execute focused cell and focus next cell
|
||||||
* @param action$
|
* @param action$
|
||||||
@@ -851,6 +917,7 @@ export const allEpics = [
|
|||||||
acquireKernelInfoEpic,
|
acquireKernelInfoEpic,
|
||||||
handleKernelConnectionLostEpic,
|
handleKernelConnectionLostEpic,
|
||||||
cleanKernelOnConnectionLostEpic,
|
cleanKernelOnConnectionLostEpic,
|
||||||
|
adjustLastModifiedOnSaveEpic,
|
||||||
executeFocusedCellAndFocusNextEpic,
|
executeFocusedCellAndFocusNextEpic,
|
||||||
closeUnsupportedMimetypesEpic,
|
closeUnsupportedMimetypesEpic,
|
||||||
closeContentFailedToFetchEpic,
|
closeContentFailedToFetchEpic,
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { actions, CoreRecord, reducers as nteractReducers } from "@nteract/core";
|
|
||||||
import { Action } from "redux";
|
|
||||||
import { Areas } from "../../../Common/Constants";
|
|
||||||
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import * as cdbActions from "./actions";
|
import * as cdbActions from "./actions";
|
||||||
import { CdbRecord } from "./types";
|
import { CdbRecord } from "./types";
|
||||||
|
|
||||||
|
import { Action } from "redux";
|
||||||
|
import { actions, CoreRecord, reducers as nteractReducers } from "@nteract/core";
|
||||||
|
|
||||||
export const coreReducer = (state: CoreRecord, action: Action) => {
|
export const coreReducer = (state: CoreRecord, action: Action) => {
|
||||||
let typedAction;
|
let typedAction;
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
@@ -51,6 +50,11 @@ export const coreReducer = (state: CoreRecord, action: Action) => {
|
|||||||
.setIn(path.concat("displayName"), kernelspecs.displayName)
|
.setIn(path.concat("displayName"), kernelspecs.displayName)
|
||||||
.setIn(path.concat("language"), kernelspecs.language);
|
.setIn(path.concat("language"), kernelspecs.language);
|
||||||
}
|
}
|
||||||
|
case cdbActions.UPDATE_LAST_MODIFIED: {
|
||||||
|
typedAction = action as cdbActions.UpdateLastModifiedAction;
|
||||||
|
const path = ["entities", "contents", "byRef", typedAction.payload.contentRef, "lastSaved"];
|
||||||
|
return state.setIn(path, typedAction.payload.lastModified);
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return nteractReducers.core(state as any, action as any);
|
return nteractReducers.core(state as any, action as any);
|
||||||
}
|
}
|
||||||
@@ -71,17 +75,6 @@ export const cdbReducer = (state: CdbRecord, action: Action) => {
|
|||||||
const typedAction = action as cdbActions.SetHoveredCellAction;
|
const typedAction = action as cdbActions.SetHoveredCellAction;
|
||||||
return state.set("hoveredCellId", typedAction.payload.cellId);
|
return state.set("hoveredCellId", typedAction.payload.cellId);
|
||||||
}
|
}
|
||||||
|
|
||||||
case cdbActions.TRACE_NOTEBOOK_TELEMETRY: {
|
|
||||||
const typedAction = action as cdbActions.TraceNotebookTelemetryAction;
|
|
||||||
TelemetryProcessor.trace(typedAction.payload.action, typedAction.payload.actionModifier, {
|
|
||||||
...typedAction.payload.data,
|
|
||||||
databaseAccountName: state.databaseAccountName,
|
|
||||||
defaultExperience: state.defaultExperience,
|
|
||||||
dataExplorerArea: Areas.Notebook
|
|
||||||
});
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,12 +9,11 @@ import { connect } from "react-redux";
|
|||||||
import { Dispatch } from "redux";
|
import { Dispatch } from "redux";
|
||||||
import { actions, ContentRef } from "@nteract/core";
|
import { actions, ContentRef } from "@nteract/core";
|
||||||
import loadTransform from "../NotebookComponent/loadTransform";
|
import loadTransform from "../NotebookComponent/loadTransform";
|
||||||
import CodeMirrorEditor from "@nteract/stateful-components/lib/inputs/connected-editors/codemirror";
|
import CodeMirrorEditor from "@nteract/editor";
|
||||||
import "./NotebookReadOnlyRenderer.less";
|
import "./NotebookReadOnlyRenderer.less";
|
||||||
|
|
||||||
export interface NotebookRendererProps {
|
export interface NotebookRendererProps {
|
||||||
contentRef: any;
|
contentRef: any;
|
||||||
hideInputs?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PassedEditorProps {
|
interface PassedEditorProps {
|
||||||
@@ -47,8 +46,7 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
|
|||||||
<CodeCell id={id} contentRef={contentRef}>
|
<CodeCell id={id} contentRef={contentRef}>
|
||||||
{{
|
{{
|
||||||
editor: {
|
editor: {
|
||||||
codemirror: (props: PassedEditorProps) =>
|
codemirror: (props: PassedEditorProps) => <CodeMirrorEditor {...props} readOnly={"nocursor"} />
|
||||||
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} readOnly={"nocursor"} />
|
|
||||||
},
|
},
|
||||||
prompt: ({ id, contentRef }) => <></>
|
prompt: ({ id, contentRef }) => <></>
|
||||||
}}
|
}}
|
||||||
@@ -65,8 +63,7 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
|
|||||||
<RawCell id={id} contentRef={contentRef} cell_type="raw">
|
<RawCell id={id} contentRef={contentRef} cell_type="raw">
|
||||||
{{
|
{{
|
||||||
editor: {
|
editor: {
|
||||||
codemirror: (props: PassedEditorProps) =>
|
codemirror: (props: PassedEditorProps) => <CodeMirrorEditor {...props} readOnly={"nocursor"} />
|
||||||
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} readOnly={"nocursor"} />
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
</RawCell>
|
</RawCell>
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { actions, ContentRef, selectors } from "@nteract/core";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Dispatch } from "redux";
|
import { Dispatch } from "redux";
|
||||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
|
||||||
import * as cdbActions from "../NotebookComponent/actions";
|
import { ContentRef, selectors, actions } from "@nteract/core";
|
||||||
import { CdbAppState } from "../NotebookComponent/types";
|
import { CdbAppState } from "../NotebookComponent/types";
|
||||||
|
|
||||||
export interface PassedPromptProps {
|
export interface PassedPromptProps {
|
||||||
@@ -84,15 +83,7 @@ const mapDispatchToProps = (
|
|||||||
dispatch: Dispatch,
|
dispatch: Dispatch,
|
||||||
{ id, contentRef }: { id: string; contentRef: ContentRef }
|
{ id, contentRef }: { id: string; contentRef: ContentRef }
|
||||||
): DispatchProps => ({
|
): DispatchProps => ({
|
||||||
executeCell: () => {
|
executeCell: () => dispatch(actions.executeCell({ id, contentRef })),
|
||||||
dispatch(actions.executeCell({ id, contentRef }));
|
|
||||||
dispatch(
|
|
||||||
cdbActions.traceNotebookTelemetry({
|
|
||||||
action: Action.ExecuteCellPromptBtn,
|
|
||||||
actionModifier: ActionModifiers.Mark
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
stopExecution: () => dispatch(actions.interruptKernel({}))
|
stopExecution: () => dispatch(actions.interruptKernel({}))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
import { IGitHubRepo, IGitHubBranch } from "../../GitHub/GitHubClient";
|
|
||||||
|
|
||||||
export const SamplesRepo: IGitHubRepo = {
|
|
||||||
name: "cosmos-notebooks",
|
|
||||||
owner: "Azure-Samples",
|
|
||||||
private: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SamplesBranch: IGitHubBranch = {
|
|
||||||
name: "master"
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isSamplesCall = (owner: string, repo: string, branch?: string): boolean => {
|
|
||||||
return owner === SamplesRepo.owner && repo === SamplesRepo.name && (!branch || branch === SamplesBranch.name);
|
|
||||||
};
|
|
||||||
|
|
||||||
// GitHub API calls have a rate limit of 5000 requests per hour. So if we get high traffic on Data Explorer
|
|
||||||
// loading samples exceed that limit. Using this hard coded response for samples until we fix that.
|
|
||||||
export const SamplesContentsQueryResponse = {
|
|
||||||
repository: {
|
|
||||||
owner: {
|
|
||||||
login: "Azure-Samples"
|
|
||||||
},
|
|
||||||
name: "cosmos-notebooks",
|
|
||||||
isPrivate: false,
|
|
||||||
ref: {
|
|
||||||
name: "master",
|
|
||||||
target: {
|
|
||||||
history: {
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
oid: "cda7facb9e039b173f3376200c26c859896e7974",
|
|
||||||
message:
|
|
||||||
"Merge pull request #45 from Azure-Samples/users/deborahc/pythonSampleUpdates\n\nAdd bokeh version to notebook",
|
|
||||||
committer: {
|
|
||||||
date: "2020-05-28T11:28:01-07:00"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
object: {
|
|
||||||
entries: [
|
|
||||||
{
|
|
||||||
name: ".github",
|
|
||||||
type: "tree",
|
|
||||||
object: {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: ".gitignore",
|
|
||||||
type: "blob",
|
|
||||||
object: {
|
|
||||||
oid: "3e759b75bf455ac809d0987d369aab89137b5689",
|
|
||||||
byteSize: 5582
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1. GettingStarted.ipynb",
|
|
||||||
type: "blob",
|
|
||||||
object: {
|
|
||||||
oid: "0732ff5366e4aefdc4c378c61cbd968664f0acec",
|
|
||||||
byteSize: 3933
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "2. Visualization.ipynb",
|
|
||||||
type: "blob",
|
|
||||||
object: {
|
|
||||||
oid: "6b16b0740a77afdd38a95bc6c3ebd0f2f17d9465",
|
|
||||||
byteSize: 820317
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "3. RequestUnits.ipynb",
|
|
||||||
type: "blob",
|
|
||||||
object: {
|
|
||||||
oid: "252b79a4adc81e9f2ffde453231b695d75e270e8",
|
|
||||||
byteSize: 9490
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "4. Indexing.ipynb",
|
|
||||||
type: "blob",
|
|
||||||
object: {
|
|
||||||
oid: "e10dd67bd1c55c345226769e4f80e43659ef9cd5",
|
|
||||||
byteSize: 10394
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "5. StoredProcedures.ipynb",
|
|
||||||
type: "blob",
|
|
||||||
object: {
|
|
||||||
oid: "949941949920de4d2d111149e2182e9657cc8134",
|
|
||||||
byteSize: 11818
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "6. GlobalDistribution.ipynb",
|
|
||||||
type: "blob",
|
|
||||||
object: {
|
|
||||||
oid: "b91c31dacacbc9e35750d9054063dda4a5309f3b",
|
|
||||||
byteSize: 11375
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "7. IoTAnomalyDetection.ipynb",
|
|
||||||
type: "blob",
|
|
||||||
object: {
|
|
||||||
oid: "82057ae52a67721a5966e2361317f5dfbd0ee595",
|
|
||||||
byteSize: 377939
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "All_API_quickstarts",
|
|
||||||
type: "tree",
|
|
||||||
object: {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "CSharp_quickstarts",
|
|
||||||
type: "tree",
|
|
||||||
object: {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -155,7 +155,19 @@ function openPane(action: ActionContracts.OpenPane, explorer: ViewModels.Explore
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openFile(action: ActionContracts.OpenSampleNotebook, explorer: ViewModels.Explorer) {
|
function openFile(action: ActionContracts.OpenSampleNotebook, explorer: ViewModels.Explorer) {
|
||||||
explorer.handleOpenFileAction(decodeURIComponent(action.path));
|
let path: string;
|
||||||
|
if (action.hasOwnProperty("file")) {
|
||||||
|
// This is deprecated
|
||||||
|
const downloadUrl: string = (action as any).file.download_url;
|
||||||
|
path = downloadUrl.replace(
|
||||||
|
"raw.githubusercontent.com/Azure-Samples/cosmos-notebooks",
|
||||||
|
"github.com/Azure-Samples/cosmos-notebooks/blob"
|
||||||
|
); // convert raw download url to something which GitHubContentProvider understands
|
||||||
|
} else {
|
||||||
|
path = action.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
explorer.handleOpenFileAction(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateQueryText(action: ActionContracts.OpenQueryTab, partitionKeyProperty: string): string {
|
function generateQueryText(action: ActionContracts.OpenQueryTab, partitionKeyProperty: string): string {
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import { TableColumnOptionsPane } from "../../src/Explorer/Panes/Tables/TableCol
|
|||||||
import { TextFieldProps } from "./Controls/DialogReactComponent/DialogComponent";
|
import { TextFieldProps } from "./Controls/DialogReactComponent/DialogComponent";
|
||||||
import { UploadDetails } from "../workers/upload/definitions";
|
import { UploadDetails } from "../workers/upload/definitions";
|
||||||
import { UploadFilePane } from "./Panes/UploadFilePane";
|
import { UploadFilePane } from "./Panes/UploadFilePane";
|
||||||
import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter";
|
|
||||||
import { Versions } from "../../src/Contracts/ExplorerContracts";
|
import { Versions } from "../../src/Contracts/ExplorerContracts";
|
||||||
import { CollectionCreationDefaults } from "../Shared/Constants";
|
import { CollectionCreationDefaults } from "../Shared/Constants";
|
||||||
|
|
||||||
@@ -87,7 +86,6 @@ export class ExplorerStub implements ViewModels.Explorer {
|
|||||||
public settingsPane: ViewModels.SettingsPane;
|
public settingsPane: ViewModels.SettingsPane;
|
||||||
public executeSprocParamsPane: ViewModels.ExecuteSprocParamsPane;
|
public executeSprocParamsPane: ViewModels.ExecuteSprocParamsPane;
|
||||||
public uploadItemsPane: ViewModels.UploadItemsPane;
|
public uploadItemsPane: ViewModels.UploadItemsPane;
|
||||||
public uploadItemsPaneAdapter: UploadItemsPaneAdapter;
|
|
||||||
public loadQueryPane: ViewModels.LoadQueryPane;
|
public loadQueryPane: ViewModels.LoadQueryPane;
|
||||||
public saveQueryPane: ViewModels.ContextualPane;
|
public saveQueryPane: ViewModels.ContextualPane;
|
||||||
public browseQueriesPane: ViewModels.BrowseQueriesPane;
|
public browseQueriesPane: ViewModels.BrowseQueriesPane;
|
||||||
@@ -98,7 +96,7 @@ export class ExplorerStub implements ViewModels.Explorer {
|
|||||||
public manageSparkClusterPane: ViewModels.ContextualPane;
|
public manageSparkClusterPane: ViewModels.ContextualPane;
|
||||||
public isGalleryEnabled: ko.Computed<boolean>;
|
public isGalleryEnabled: ko.Computed<boolean>;
|
||||||
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||||
public isRightPanelV2Enabled: ko.Computed<boolean>;
|
public isGraphsEnabled: ko.Computed<boolean>;
|
||||||
public canExceedMaximumValue: ko.Computed<boolean>;
|
public canExceedMaximumValue: ko.Computed<boolean>;
|
||||||
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
|
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
|
||||||
public parentFrameDataExplorerVersion: ko.Observable<string> = ko.observable<string>(Versions.DataExplorer);
|
public parentFrameDataExplorerVersion: ko.Observable<string> = ko.observable<string>(Versions.DataExplorer);
|
||||||
@@ -449,6 +447,7 @@ export class DatabaseStub implements ViewModels.Database {
|
|||||||
public collections: ko.ObservableArray<ViewModels.Collection>;
|
public collections: ko.ObservableArray<ViewModels.Collection>;
|
||||||
public isDatabaseExpanded: ko.Observable<boolean>;
|
public isDatabaseExpanded: ko.Observable<boolean>;
|
||||||
public isDatabaseShared: ko.Computed<boolean>;
|
public isDatabaseShared: ko.Computed<boolean>;
|
||||||
|
public contextMenu: ViewModels.ContextMenu;
|
||||||
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
|
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
|
||||||
public offer: ko.Observable<DataModels.Offer>;
|
public offer: ko.Observable<DataModels.Offer>;
|
||||||
|
|
||||||
@@ -460,6 +459,7 @@ export class DatabaseStub implements ViewModels.Database {
|
|||||||
this.id = options.id;
|
this.id = options.id;
|
||||||
this.collections = options.collections;
|
this.collections = options.collections;
|
||||||
this.isDatabaseExpanded = options.isDatabaseExpanded;
|
this.isDatabaseExpanded = options.isDatabaseExpanded;
|
||||||
|
this.contextMenu = options.contextMenu;
|
||||||
this.offer = options.offer;
|
this.offer = options.offer;
|
||||||
this.selectedSubnodeKind = options.selectedSubnodeKind;
|
this.selectedSubnodeKind = options.selectedSubnodeKind;
|
||||||
}
|
}
|
||||||
@@ -562,6 +562,8 @@ export class CollectionStub implements ViewModels.Collection {
|
|||||||
public storedProceduresFocused: ko.Observable<boolean>;
|
public storedProceduresFocused: ko.Observable<boolean>;
|
||||||
public userDefinedFunctionsFocused: ko.Observable<boolean>;
|
public userDefinedFunctionsFocused: ko.Observable<boolean>;
|
||||||
public triggersFocused: ko.Observable<boolean>;
|
public triggersFocused: ko.Observable<boolean>;
|
||||||
|
public contextMenu: ViewModels.ContextMenu;
|
||||||
|
public documentsContextMenu: ViewModels.ContextMenu;
|
||||||
public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
||||||
public changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
public changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
||||||
public geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
|
public geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
|
||||||
@@ -606,8 +608,69 @@ export class CollectionStub implements ViewModels.Collection {
|
|||||||
this.storedProceduresFocused = options.storedProceduresFocused;
|
this.storedProceduresFocused = options.storedProceduresFocused;
|
||||||
this.userDefinedFunctionsFocused = options.userDefinedFunctionsFocused;
|
this.userDefinedFunctionsFocused = options.userDefinedFunctionsFocused;
|
||||||
this.triggersFocused = options.triggersFocused;
|
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() {
|
public expandCollapseCollection() {
|
||||||
throw new Error("Not implemented");
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import Explorer from "../Explorer";
|
|||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
import { AutopilotTier } from "../../Contracts/DataModels";
|
import { AutopilotTier } from "../../Contracts/DataModels";
|
||||||
|
|
||||||
|
jest.mock("../Tabs/NotebookTab");
|
||||||
|
|
||||||
describe("Add Collection Pane", () => {
|
describe("Add Collection Pane", () => {
|
||||||
describe("isValid()", () => {
|
describe("isValid()", () => {
|
||||||
let explorer: ViewModels.Explorer;
|
let explorer: ViewModels.Explorer;
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import * as ViewModels from "../../Contracts/ViewModels";
|
|||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import AddDatabasePane from "./AddDatabasePane";
|
import AddDatabasePane from "./AddDatabasePane";
|
||||||
|
|
||||||
|
jest.mock("../Tabs/NotebookTab");
|
||||||
|
|
||||||
describe("Add Database Pane", () => {
|
describe("Add Database Pane", () => {
|
||||||
describe("getSharedThroughputDefault()", () => {
|
describe("getSharedThroughputDefault()", () => {
|
||||||
let explorer: ViewModels.Explorer;
|
let explorer: ViewModels.Explorer;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import _ from "underscore";
|
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
@@ -82,7 +81,7 @@ export class ClusterLibraryPane extends ContextualPaneBase {
|
|||||||
|
|
||||||
private _onInstalledChanged = (libraryName: string, installed: boolean): void => {
|
private _onInstalledChanged = (libraryName: string, installed: boolean): void => {
|
||||||
const items = this._clusterLibraryProps().libraryItems;
|
const items = this._clusterLibraryProps().libraryItems;
|
||||||
const library = _.find(items, item => item.name === libraryName);
|
const library = items.find(item => item.name === libraryName);
|
||||||
library.installed = installed;
|
library.installed = installed;
|
||||||
this._clusterLibraryProps.valueHasMutated();
|
this._clusterLibraryProps.valueHasMutated();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
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();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
import _ from "underscore";
|
|
||||||
import { Areas, HttpStatusCodes } from "../../Common/Constants";
|
import { Areas, HttpStatusCodes } from "../../Common/Constants";
|
||||||
import { Logger } from "../../Common/Logger";
|
import { Logger } from "../../Common/Logger";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { GitHubClient, IGitHubPageInfo, IGitHubRepo } from "../../GitHub/GitHubClient";
|
import { GitHubClient, IGitHubRepo } from "../../GitHub/GitHubClient";
|
||||||
import { IPinnedRepo, JunoClient } from "../../Juno/JunoClient";
|
import { IPinnedRepo, JunoClient } from "../../Juno/JunoClient";
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { GitHubUtils } from "../../Utils/GitHubUtils";
|
import { GitHubUtils } from "../../Utils/GitHubUtils";
|
||||||
import { JunoUtils } from "../../Utils/JunoUtils";
|
|
||||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { AuthorizeAccessComponent } from "../Controls/GitHub/AuthorizeAccessComponent";
|
import { AuthorizeAccessComponent } from "../Controls/GitHub/AuthorizeAccessComponent";
|
||||||
import { GitHubReposComponent, GitHubReposComponentProps, RepoListItem } from "../Controls/GitHub/GitHubReposComponent";
|
import { GitHubReposComponentProps, RepoListItem, GitHubReposComponent } from "../Controls/GitHub/GitHubReposComponent";
|
||||||
import { GitHubReposComponentAdapter } from "../Controls/GitHub/GitHubReposComponentAdapter";
|
import { GitHubReposComponentAdapter } from "../Controls/GitHub/GitHubReposComponentAdapter";
|
||||||
import { BranchesProps, PinnedReposProps, UnpinnedReposProps } from "../Controls/GitHub/ReposListComponent";
|
import { BranchesProps, PinnedReposProps, UnpinnedReposProps } from "../Controls/GitHub/ReposListComponent";
|
||||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { ContextualPaneBase } from "./ContextualPaneBase";
|
import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||||
|
import { JunoUtils } from "../../Utils/JunoUtils";
|
||||||
|
|
||||||
export class GitHubReposPane extends ContextualPaneBase {
|
export class GitHubReposPane extends ContextualPaneBase {
|
||||||
private static readonly PageSize = 30;
|
private static readonly PageSize = 30;
|
||||||
@@ -30,7 +29,6 @@ export class GitHubReposPane extends ContextualPaneBase {
|
|||||||
private gitHubReposAdapter: GitHubReposComponentAdapter;
|
private gitHubReposAdapter: GitHubReposComponentAdapter;
|
||||||
|
|
||||||
private allGitHubRepos: IGitHubRepo[];
|
private allGitHubRepos: IGitHubRepo[];
|
||||||
private allGitHubReposLastPageInfo?: IGitHubPageInfo;
|
|
||||||
private pinnedReposUpdated: boolean;
|
private pinnedReposUpdated: boolean;
|
||||||
|
|
||||||
constructor(options: ViewModels.GitHubReposPaneOptions) {
|
constructor(options: ViewModels.GitHubReposPaneOptions) {
|
||||||
@@ -75,7 +73,6 @@ export class GitHubReposPane extends ContextualPaneBase {
|
|||||||
this.gitHubReposAdapter = new GitHubReposComponentAdapter(this.gitHubReposProps);
|
this.gitHubReposAdapter = new GitHubReposComponentAdapter(this.gitHubReposProps);
|
||||||
|
|
||||||
this.allGitHubRepos = [];
|
this.allGitHubRepos = [];
|
||||||
this.allGitHubReposLastPageInfo = undefined;
|
|
||||||
this.pinnedReposUpdated = false;
|
this.pinnedReposUpdated = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +115,6 @@ export class GitHubReposPane extends ContextualPaneBase {
|
|||||||
|
|
||||||
// Reset cached repos
|
// Reset cached repos
|
||||||
this.allGitHubRepos = [];
|
this.allGitHubRepos = [];
|
||||||
this.allGitHubReposLastPageInfo = undefined;
|
|
||||||
|
|
||||||
// Reset flags
|
// Reset flags
|
||||||
this.pinnedReposUpdated = false;
|
this.pinnedReposUpdated = false;
|
||||||
@@ -168,28 +164,29 @@ export class GitHubReposPane extends ContextualPaneBase {
|
|||||||
const unpinnedGitHubRepos = this.allGitHubRepos.filter(
|
const unpinnedGitHubRepos = this.allGitHubRepos.filter(
|
||||||
gitHubRepo =>
|
gitHubRepo =>
|
||||||
this.pinnedReposProps.repos.findIndex(
|
this.pinnedReposProps.repos.findIndex(
|
||||||
pinnedRepo => pinnedRepo.key === GitHubUtils.toRepoFullName(gitHubRepo.owner, gitHubRepo.name)
|
pinnedRepo => pinnedRepo.key === GitHubUtils.toRepoFullName(gitHubRepo.owner.login, gitHubRepo.name)
|
||||||
) === -1
|
) === -1
|
||||||
);
|
);
|
||||||
return unpinnedGitHubRepos.map(gitHubRepo => ({
|
return unpinnedGitHubRepos.map(gitHubRepo => ({
|
||||||
key: GitHubUtils.toRepoFullName(gitHubRepo.owner, gitHubRepo.name),
|
key: GitHubUtils.toRepoFullName(gitHubRepo.owner.login, gitHubRepo.name),
|
||||||
repo: gitHubRepo,
|
repo: gitHubRepo,
|
||||||
branches: []
|
branches: []
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadMoreBranches(repo: IGitHubRepo): Promise<void> {
|
private async loadMoreBranches(repo: IGitHubRepo): Promise<void> {
|
||||||
const branchesProps = this.branchesProps[GitHubUtils.toRepoFullName(repo.owner, repo.name)];
|
const branchesProps = this.branchesProps[GitHubUtils.toRepoFullName(repo.owner.login, repo.name)];
|
||||||
branchesProps.hasMore = true;
|
branchesProps.hasMore = true;
|
||||||
branchesProps.isLoading = true;
|
branchesProps.isLoading = true;
|
||||||
this.triggerRender();
|
this.triggerRender();
|
||||||
|
|
||||||
|
const nextPage = Math.floor(branchesProps.branches.length / GitHubReposPane.PageSize) + 1;
|
||||||
try {
|
try {
|
||||||
const response = await this.gitHubClient.getBranchesAsync(
|
const response = await this.gitHubClient.getBranchesAsync(
|
||||||
repo.owner,
|
repo.owner.login,
|
||||||
repo.name,
|
repo.name,
|
||||||
GitHubReposPane.PageSize,
|
nextPage,
|
||||||
branchesProps.lastPageInfo?.endCursor
|
GitHubReposPane.PageSize
|
||||||
);
|
);
|
||||||
if (response.status !== HttpStatusCodes.OK) {
|
if (response.status !== HttpStatusCodes.OK) {
|
||||||
throw new Error(`Received HTTP ${response.status} when fetching branches`);
|
throw new Error(`Received HTTP ${response.status} when fetching branches`);
|
||||||
@@ -197,7 +194,6 @@ export class GitHubReposPane extends ContextualPaneBase {
|
|||||||
|
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
branchesProps.branches = branchesProps.branches.concat(response.data);
|
branchesProps.branches = branchesProps.branches.concat(response.data);
|
||||||
branchesProps.lastPageInfo = response.pageInfo;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = `Failed to fetch branches: ${error}`;
|
const message = `Failed to fetch branches: ${error}`;
|
||||||
@@ -206,7 +202,7 @@ export class GitHubReposPane extends ContextualPaneBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
branchesProps.isLoading = false;
|
branchesProps.isLoading = false;
|
||||||
branchesProps.hasMore = branchesProps.lastPageInfo?.hasNextPage;
|
branchesProps.hasMore = branchesProps.branches.length === GitHubReposPane.PageSize * nextPage;
|
||||||
this.triggerRender();
|
this.triggerRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,18 +211,15 @@ export class GitHubReposPane extends ContextualPaneBase {
|
|||||||
this.unpinnedReposProps.hasMore = true;
|
this.unpinnedReposProps.hasMore = true;
|
||||||
this.triggerRender();
|
this.triggerRender();
|
||||||
|
|
||||||
|
const nextPage = Math.floor(this.allGitHubRepos.length / GitHubReposPane.PageSize) + 1;
|
||||||
try {
|
try {
|
||||||
const response = await this.gitHubClient.getReposAsync(
|
const response = await this.gitHubClient.getReposAsync(nextPage, GitHubReposPane.PageSize);
|
||||||
GitHubReposPane.PageSize,
|
|
||||||
this.allGitHubReposLastPageInfo?.endCursor
|
|
||||||
);
|
|
||||||
if (response.status !== HttpStatusCodes.OK) {
|
if (response.status !== HttpStatusCodes.OK) {
|
||||||
throw new Error(`Received HTTP ${response.status} when fetching unpinned repos`);
|
throw new Error(`Received HTTP ${response.status} when fetching unpinned repos`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
this.allGitHubRepos = this.allGitHubRepos.concat(response.data);
|
this.allGitHubRepos = this.allGitHubRepos.concat(response.data);
|
||||||
this.allGitHubReposLastPageInfo = response.pageInfo;
|
|
||||||
this.unpinnedReposProps.repos = this.calculateUnpinnedRepos();
|
this.unpinnedReposProps.repos = this.calculateUnpinnedRepos();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -236,7 +229,7 @@ export class GitHubReposPane extends ContextualPaneBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.unpinnedReposProps.isLoading = false;
|
this.unpinnedReposProps.isLoading = false;
|
||||||
this.unpinnedReposProps.hasMore = this.allGitHubReposLastPageInfo?.hasNextPage;
|
this.unpinnedReposProps.hasMore = this.allGitHubRepos.length === GitHubReposPane.PageSize * nextPage;
|
||||||
this.triggerRender();
|
this.triggerRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +253,7 @@ export class GitHubReposPane extends ContextualPaneBase {
|
|||||||
this.pinnedReposUpdated = true;
|
this.pinnedReposUpdated = true;
|
||||||
const initialReposLength = this.pinnedReposProps.repos.length;
|
const initialReposLength = this.pinnedReposProps.repos.length;
|
||||||
|
|
||||||
const existingRepo = _.find(this.pinnedReposProps.repos, repo => repo.key === item.key);
|
const existingRepo = this.pinnedReposProps.repos.find(repo => repo.key === item.key);
|
||||||
if (existingRepo) {
|
if (existingRepo) {
|
||||||
existingRepo.branches = item.branches;
|
existingRepo.branches = item.branches;
|
||||||
} else {
|
} else {
|
||||||
@@ -325,7 +318,6 @@ export class GitHubReposPane extends ContextualPaneBase {
|
|||||||
if (!this.branchesProps[item.key]) {
|
if (!this.branchesProps[item.key]) {
|
||||||
this.branchesProps[item.key] = {
|
this.branchesProps[item.key] = {
|
||||||
branches: [],
|
branches: [],
|
||||||
lastPageInfo: undefined,
|
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
loadMore: (): Promise<void> => this.loadMoreBranches(item.repo)
|
loadMore: (): Promise<void> => this.loadMoreBranches(item.repo)
|
||||||
@@ -337,7 +329,6 @@ export class GitHubReposPane extends ContextualPaneBase {
|
|||||||
|
|
||||||
private async refreshUnpinnedRepoListItems(): Promise<void> {
|
private async refreshUnpinnedRepoListItems(): Promise<void> {
|
||||||
this.allGitHubRepos = [];
|
this.allGitHubRepos = [];
|
||||||
this.allGitHubReposLastPageInfo = undefined;
|
|
||||||
this.unpinnedReposProps.repos = [];
|
this.unpinnedReposProps.repos = [];
|
||||||
this.loadMoreUnpinnedRepos();
|
this.loadMoreUnpinnedRepos();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import * as Constants from "../../Common/Constants";
|
|||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
|
|
||||||
|
jest.mock("../Tabs/NotebookTab");
|
||||||
|
|
||||||
describe("Settings Pane", () => {
|
describe("Settings Pane", () => {
|
||||||
describe("shouldShowQueryPageOptions()", () => {
|
describe("shouldShowQueryPageOptions()", () => {
|
||||||
let explorer: ViewModels.Explorer;
|
let explorer: ViewModels.Explorer;
|
||||||
|
|||||||
@@ -1,238 +0,0 @@
|
|||||||
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 = [];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -19,7 +19,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
>.title {
|
>.title {
|
||||||
position: relative; // To attach FeaturePanelLauncher as absolute
|
|
||||||
color: @BaseHigh;
|
color: @BaseHigh;
|
||||||
font-size: 48px;
|
font-size: 48px;
|
||||||
padding-left: 0px;
|
padding-left: 0px;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { Link } from "office-ui-fabric-react/lib/Link";
|
import { Link } from "office-ui-fabric-react/lib/Link";
|
||||||
import { FeaturePanelLauncher } from "../Controls/FeaturePanel/FeaturePanelLauncher";
|
|
||||||
|
|
||||||
export interface SplashScreenItem {
|
export interface SplashScreenItem {
|
||||||
iconSrc: string;
|
iconSrc: string;
|
||||||
@@ -30,10 +29,7 @@ export class SplashScreenComponent extends React.Component<SplashScreenComponent
|
|||||||
return (
|
return (
|
||||||
<div className="splashScreenContainer">
|
<div className="splashScreenContainer">
|
||||||
<div className="splashScreen">
|
<div className="splashScreen">
|
||||||
<div className="title">
|
<div className="title">Welcome to Cosmos DB</div>
|
||||||
Welcome to Cosmos DB
|
|
||||||
<FeaturePanelLauncher />
|
|
||||||
</div>
|
|
||||||
<div className="subtitle">Globally distributed, multi-model database service for any scale</div>
|
<div className="subtitle">Globally distributed, multi-model database service for any scale</div>
|
||||||
<div className="mainButtonsContainer">
|
<div className="mainButtonsContainer">
|
||||||
{this.props.mainItems.map((item: SplashScreenItem) => (
|
{this.props.mainItems.map((item: SplashScreenItem) => (
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { DataAccessUtility } from "../../Platform/Portal/DataAccessUtility";
|
|||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase";
|
import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase";
|
||||||
|
|
||||||
|
jest.mock("./NotebookTab");
|
||||||
|
|
||||||
describe("Documents tab", () => {
|
describe("Documents tab", () => {
|
||||||
describe("buildQuery", () => {
|
describe("buildQuery", () => {
|
||||||
it("should generate the right select query for SQL API", () => {
|
it("should generate the right select query for SQL API", () => {
|
||||||
|
|||||||
@@ -965,10 +965,7 @@ export default class DocumentsTab extends TabsBase implements ViewModels.Documen
|
|||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||||
const focusElement = document.getElementById("itemImportLink");
|
const focusElement = document.getElementById("itemImportLink");
|
||||||
const uploadItemsPane = container.isRightPanelV2Enabled()
|
selectedCollection && container.uploadItemsPane.open();
|
||||||
? container.uploadItemsPaneAdapter
|
|
||||||
: container.uploadItemsPane;
|
|
||||||
selectedCollection && uploadItemsPane.open();
|
|
||||||
focusElement && focusElement.focus();
|
focusElement && focusElement.focus();
|
||||||
},
|
},
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as ViewModels from "../../Contracts/ViewModels";
|
|||||||
import TabsBase from "./TabsBase";
|
import TabsBase from "./TabsBase";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||||
import { GalleryViewerContainerComponent } from "../Controls/NotebookGallery/GalleryViewerComponent";
|
import { GalleryViewerContainerComponent } from "../../GalleryViewer/GalleryViewerComponent";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notebook gallery tab
|
* Notebook gallery tab
|
||||||
|
|||||||
7
src/Explorer/Tabs/NotebookTab.html
Normal file
7
src/Explorer/Tabs/NotebookTab.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<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>
|
||||||
539
src/Explorer/Tabs/NotebookTab.ts
Normal file
539
src/Explorer/Tabs/NotebookTab.ts
Normal file
@@ -0,0 +1,539 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,9 +16,7 @@ class NotebookViewerComponentAdapter implements ReactAdapter {
|
|||||||
private notebookUrl: string,
|
private notebookUrl: string,
|
||||||
private notebookName: string,
|
private notebookName: string,
|
||||||
private container: ViewModels.Explorer,
|
private container: ViewModels.Explorer,
|
||||||
private notebookMetadata: DataModels.NotebookMetadata,
|
private notebookMetadata: DataModels.NotebookMetadata
|
||||||
private onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
|
|
||||||
private isLikedNotebook: boolean
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public renderComponent(): JSX.Element {
|
public renderComponent(): JSX.Element {
|
||||||
@@ -28,8 +26,6 @@ class NotebookViewerComponentAdapter implements ReactAdapter {
|
|||||||
notebookMetadata={this.notebookMetadata}
|
notebookMetadata={this.notebookMetadata}
|
||||||
notebookName={this.notebookName}
|
notebookName={this.notebookName}
|
||||||
container={this.container}
|
container={this.container}
|
||||||
onNotebookMetadataChange={this.onNotebookMetadataChange}
|
|
||||||
isLikedNotebook={this.isLikedNotebook}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
@@ -50,9 +46,7 @@ export default class NotebookViewerTab extends TabsBase implements ViewModels.Ta
|
|||||||
options.notebookUrl,
|
options.notebookUrl,
|
||||||
options.notebookName,
|
options.notebookName,
|
||||||
options.container,
|
options.container,
|
||||||
options.notebookMetadata,
|
options.notebookMetadata
|
||||||
options.onNotebookMetadataChange,
|
|
||||||
options.isLikedNotebook
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.notebookViewerComponentAdapter.parameters = ko.computed<boolean>(() => {
|
this.notebookViewerComponentAdapter.parameters = ko.computed<boolean>(() => {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import Explorer from "../Explorer";
|
|||||||
import { CollectionStub, DatabaseStub } from "../../Explorer/OpenActionsStubs";
|
import { CollectionStub, DatabaseStub } from "../../Explorer/OpenActionsStubs";
|
||||||
import QueryTab from "./QueryTab";
|
import QueryTab from "./QueryTab";
|
||||||
|
|
||||||
|
jest.mock("./NotebookTab");
|
||||||
|
|
||||||
describe("Query Tab", () => {
|
describe("Query Tab", () => {
|
||||||
function getNewQueryTabForContainer(container: ViewModels.Explorer): ViewModels.QueryTab {
|
function getNewQueryTabForContainer(container: ViewModels.Explorer): ViewModels.QueryTab {
|
||||||
const database: ViewModels.Database = new DatabaseStub({
|
const database: ViewModels.Database = new DatabaseStub({
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import Explorer from "../Explorer";
|
|||||||
import SettingsTab from "../Tabs/SettingsTab";
|
import SettingsTab from "../Tabs/SettingsTab";
|
||||||
import { DataAccessUtility } from "../../Platform/Portal/DataAccessUtility";
|
import { DataAccessUtility } from "../../Platform/Portal/DataAccessUtility";
|
||||||
|
|
||||||
|
jest.mock("./NotebookTab");
|
||||||
|
|
||||||
describe("Settings tab", () => {
|
describe("Settings tab", () => {
|
||||||
const baseCollection: DataModels.Collection = {
|
const baseCollection: DataModels.Collection = {
|
||||||
defaultTtl: 200,
|
defaultTtl: 200,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import DocumentsTabTemplate from "./DocumentsTab.html";
|
import DocumentsTabTemplate from "./DocumentsTab.html";
|
||||||
import ConflictsTabTemplate from "./ConflictsTab.html";
|
import ConflictsTabTemplate from "./ConflictsTab.html";
|
||||||
import GraphTabTemplate from "./GraphTab.html";
|
import GraphTabTemplate from "./GraphTab.html";
|
||||||
|
import NotebookTabTemplate from "./NotebookTab.html";
|
||||||
import SparkMasterTabTemplate from "./SparkMasterTab.html";
|
import SparkMasterTabTemplate from "./SparkMasterTab.html";
|
||||||
import NotebookV2TabTemplate from "./NotebookV2Tab.html";
|
import NotebookV2TabTemplate from "./NotebookV2Tab.html";
|
||||||
import TerminalTabTemplate from "./TerminalTab.html";
|
import TerminalTabTemplate from "./TerminalTab.html";
|
||||||
@@ -50,6 +51,15 @@ export class GraphTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class NotebookTab {
|
||||||
|
constructor() {
|
||||||
|
return {
|
||||||
|
viewModel: TabComponent,
|
||||||
|
template: NotebookTabTemplate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class SparkMasterTab {
|
export class SparkMasterTab {
|
||||||
constructor() {
|
constructor() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
8
src/Explorer/Tabs/__mocks__/NotebookTab.ts
Normal file
8
src/Explorer/Tabs/__mocks__/NotebookTab.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,8 @@ import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
|||||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { OfferUtils } from "../../Utils/OfferUtils";
|
import { OfferUtils } from "../../Utils/OfferUtils";
|
||||||
import { StartUploadMessageParams, UploadDetails, UploadDetailsRecord } from "../../workers/upload/definitions";
|
import { StartUploadMessageParams, UploadDetails, UploadDetailsRecord } from "../../workers/upload/definitions";
|
||||||
|
import { ContextMenuButtonFactory } from "../ContextMenuButtonFactory";
|
||||||
|
import ContextMenu from "../Menus/ContextMenu";
|
||||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient";
|
import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient";
|
||||||
import { ConflictsTab } from "../Tabs/ConflictsTab";
|
import { ConflictsTab } from "../Tabs/ConflictsTab";
|
||||||
@@ -30,7 +32,6 @@ import DocumentId from "./DocumentId";
|
|||||||
import StoredProcedure from "./StoredProcedure";
|
import StoredProcedure from "./StoredProcedure";
|
||||||
import Trigger from "./Trigger";
|
import Trigger from "./Trigger";
|
||||||
import UserDefinedFunction from "./UserDefinedFunction";
|
import UserDefinedFunction from "./UserDefinedFunction";
|
||||||
import { config } from "../../Config";
|
|
||||||
|
|
||||||
export default class Collection implements ViewModels.Collection {
|
export default class Collection implements ViewModels.Collection {
|
||||||
public nodeKind: string;
|
public nodeKind: string;
|
||||||
@@ -84,6 +85,9 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
public userDefinedFunctionsFocused: ko.Observable<boolean>;
|
public userDefinedFunctionsFocused: ko.Observable<boolean>;
|
||||||
public triggersFocused: ko.Observable<boolean>;
|
public triggersFocused: ko.Observable<boolean>;
|
||||||
|
|
||||||
|
public contextMenu: ViewModels.ContextMenu;
|
||||||
|
public documentsContextMenu: ViewModels.ContextMenu;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
container: ViewModels.Explorer,
|
container: ViewModels.Explorer,
|
||||||
databaseId: string,
|
databaseId: string,
|
||||||
@@ -211,8 +215,217 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
this.isStoredProceduresExpanded = ko.observable<boolean>(false);
|
this.isStoredProceduresExpanded = ko.observable<boolean>(false);
|
||||||
this.isUserDefinedFunctionsExpanded = ko.observable<boolean>(false);
|
this.isUserDefinedFunctionsExpanded = ko.observable<boolean>(false);
|
||||||
this.isTriggersExpanded = 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() {
|
public expandCollapseCollection() {
|
||||||
this.container.selectedNode(this);
|
this.container.selectedNode(this);
|
||||||
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
||||||
@@ -771,6 +984,9 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
|
|
||||||
// Activate
|
// Activate
|
||||||
queryTab.onTabClick();
|
queryTab.onTabClick();
|
||||||
|
|
||||||
|
// Hide Context Menu (if necessary)
|
||||||
|
collection.contextMenu.hide(this, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onNewMongoQueryClick(source: any, event: MouseEvent, queryText?: string) {
|
public onNewMongoQueryClick(source: any, event: MouseEvent, queryText?: string) {
|
||||||
@@ -808,6 +1024,9 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
|
|
||||||
// Activate
|
// Activate
|
||||||
queryTab.onTabClick();
|
queryTab.onTabClick();
|
||||||
|
|
||||||
|
// Hide Context Menu (if necessary)
|
||||||
|
collection.contextMenu.hide(this, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onNewGraphClick() {
|
public onNewGraphClick() {
|
||||||
@@ -816,6 +1035,8 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
this.container.openedTabs.push(graphTab);
|
this.container.openedTabs.push(graphTab);
|
||||||
// Activate
|
// Activate
|
||||||
graphTab.onTabClick();
|
graphTab.onTabClick();
|
||||||
|
// Hide Context Menu (if necessary)
|
||||||
|
this.contextMenu.hide(this, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onNewMongoShellClick() {
|
public onNewMongoShellClick() {
|
||||||
@@ -838,6 +1059,9 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
|
|
||||||
// Activate
|
// Activate
|
||||||
mongoShellTab.onTabClick();
|
mongoShellTab.onTabClick();
|
||||||
|
|
||||||
|
// Hide Context Menu (if necessary)
|
||||||
|
this.contextMenu.hide(this, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onNewStoredProcedureClick(source: ViewModels.Collection, event: MouseEvent) {
|
public onNewStoredProcedureClick(source: ViewModels.Collection, event: MouseEvent) {
|
||||||
@@ -1132,6 +1356,7 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onDeleteCollectionContextMenuClick(source: ViewModels.Collection, event: MouseEvent | KeyboardEvent) {
|
public onDeleteCollectionContextMenuClick(source: ViewModels.Collection, event: MouseEvent | KeyboardEvent) {
|
||||||
|
this._onContextMenuClick(source, event);
|
||||||
this.container.deleteCollectionConfirmationPane.open();
|
this.container.deleteCollectionConfirmationPane.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1191,7 +1416,7 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
masterKey: CosmosClient.masterKey(),
|
masterKey: CosmosClient.masterKey(),
|
||||||
endpoint: CosmosClient.endpoint(),
|
endpoint: CosmosClient.endpoint(),
|
||||||
accessToken: CosmosClient.accessToken(),
|
accessToken: CosmosClient.accessToken(),
|
||||||
platform: config.platform,
|
platform: window.dataExplorerPlatform,
|
||||||
databaseAccount: CosmosClient.databaseAccount()
|
databaseAccount: CosmosClient.databaseAccount()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1379,6 +1604,11 @@ 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 {
|
protected _getOfferForCollection(offers: DataModels.Offer[], collection: DataModels.Collection): DataModels.Offer {
|
||||||
return _.find(offers, (offer: DataModels.Offer) => offer.resource.indexOf(collection._rid) >= 0);
|
return _.find(offers, (offer: DataModels.Offer) => offer.resource.indexOf(collection._rid) >= 0);
|
||||||
}
|
}
|
||||||
|
|||||||
425
src/Explorer/Tree/CollectionTreeNode.html
Normal file
425
src/Explorer/Tree/CollectionTreeNode.html
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
<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"
|
||||||
|
>…</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>
|
||||||
16
src/Explorer/Tree/CollectionTreeNodeContextMenu.html
Normal file
16
src/Explorer/Tree/CollectionTreeNodeContextMenu.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<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>
|
||||||
@@ -7,9 +7,11 @@ import * as DataModels from "../../Contracts/DataModels";
|
|||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import DatabaseSettingsTab from "../Tabs/DatabaseSettingsTab";
|
import DatabaseSettingsTab from "../Tabs/DatabaseSettingsTab";
|
||||||
import Collection from "./Collection";
|
import Collection from "./Collection";
|
||||||
|
import ContextMenu from "../Menus/ContextMenu";
|
||||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
|
import { ContextMenuButtonFactory } from "../ContextMenuButtonFactory";
|
||||||
import { Logger } from "../../Common/Logger";
|
import { Logger } from "../../Common/Logger";
|
||||||
|
|
||||||
export default class Database implements ViewModels.Database {
|
export default class Database implements ViewModels.Database {
|
||||||
@@ -23,6 +25,7 @@ export default class Database implements ViewModels.Database {
|
|||||||
public isDatabaseExpanded: ko.Observable<boolean>;
|
public isDatabaseExpanded: ko.Observable<boolean>;
|
||||||
public isDatabaseShared: ko.Computed<boolean>;
|
public isDatabaseShared: ko.Computed<boolean>;
|
||||||
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
|
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
|
||||||
|
public contextMenu: ViewModels.ContextMenu;
|
||||||
|
|
||||||
constructor(container: ViewModels.Explorer, data: any, offer: DataModels.Offer) {
|
constructor(container: ViewModels.Explorer, data: any, offer: DataModels.Offer) {
|
||||||
this.nodeKind = "Database";
|
this.nodeKind = "Database";
|
||||||
@@ -33,12 +36,71 @@ export default class Database implements ViewModels.Database {
|
|||||||
this.offer = ko.observable(offer);
|
this.offer = ko.observable(offer);
|
||||||
this.collections = ko.observableArray<Collection>();
|
this.collections = ko.observableArray<Collection>();
|
||||||
this.isDatabaseExpanded = ko.observable<boolean>(false);
|
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.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
|
||||||
this.isDatabaseShared = ko.pureComputed(() => {
|
this.isDatabaseShared = ko.pureComputed(() => {
|
||||||
return this.offer && !!this.offer();
|
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 = () => {
|
public onSettingsClick = () => {
|
||||||
this.container.selectedNode(this);
|
this.container.selectedNode(this);
|
||||||
this.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
|
this.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
|
||||||
@@ -200,6 +262,8 @@ export default class Database implements ViewModels.Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onDeleteDatabaseContextMenuClick(source: ViewModels.Database, event: MouseEvent | KeyboardEvent) {
|
public onDeleteDatabaseContextMenuClick(source: ViewModels.Database, event: MouseEvent | KeyboardEvent) {
|
||||||
|
source.container.selectedNode(source);
|
||||||
|
source.contextMenu.hide(source, event);
|
||||||
this.container.deleteDatabaseConfirmationPane.open();
|
this.container.deleteDatabaseConfirmationPane.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,6 +346,7 @@ export default class Database implements ViewModels.Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public openAddCollection(database: Database, event: MouseEvent) {
|
public openAddCollection(database: Database, event: MouseEvent) {
|
||||||
|
database.contextMenu.hide(database, event);
|
||||||
database.container.addCollectionPane.databaseId(database.id());
|
database.container.addCollectionPane.databaseId(database.id());
|
||||||
database.container.addCollectionPane.open();
|
database.container.addCollectionPane.open();
|
||||||
}
|
}
|
||||||
|
|||||||
110
src/Explorer/Tree/DatabaseTreeNode.html
Normal file
110
src/Explorer/Tree/DatabaseTreeNode.html
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<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
|
||||||
|
"
|
||||||
|
>…</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>
|
||||||
11
src/Explorer/Tree/ResourceTree.html
Normal file
11
src/Explorer/Tree/ResourceTree.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<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>
|
||||||
@@ -6,6 +6,7 @@ import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeCompo
|
|||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
||||||
import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory";
|
import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory";
|
||||||
|
import NotebookTab from "../Tabs/NotebookTab";
|
||||||
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
|
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
|
||||||
import { CosmosClient } from "../../Common/CosmosClient";
|
import { CosmosClient } from "../../Common/CosmosClient";
|
||||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||||
@@ -13,6 +14,7 @@ import CollectionIcon from "../../../images/tree-collection.svg";
|
|||||||
import DeleteIcon from "../../../images/delete.svg";
|
import DeleteIcon from "../../../images/delete.svg";
|
||||||
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
|
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
|
||||||
import RefreshIcon from "../../../images/refresh-cosmos.svg";
|
import RefreshIcon from "../../../images/refresh-cosmos.svg";
|
||||||
|
import { IGitHubRepo, IGitHubBranch } from "../../GitHub/GitHubClient";
|
||||||
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
|
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
|
||||||
import FileIcon from "../../../images/notebook/file-cosmos.svg";
|
import FileIcon from "../../../images/notebook/file-cosmos.svg";
|
||||||
import { ArrayHashMap } from "../../Common/ArrayHashMap";
|
import { ArrayHashMap } from "../../Common/ArrayHashMap";
|
||||||
@@ -24,11 +26,21 @@ import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
|||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import { Areas } from "../../Common/Constants";
|
import { Areas } from "../../Common/Constants";
|
||||||
import { GitHubUtils } from "../../Utils/GitHubUtils";
|
import { GitHubUtils } from "../../Utils/GitHubUtils";
|
||||||
import { SamplesRepo, SamplesBranch } from "../Notebook/NotebookSamples";
|
|
||||||
|
|
||||||
export class ResourceTreeAdapter implements ReactAdapter {
|
export class ResourceTreeAdapter implements ReactAdapter {
|
||||||
private static readonly DataTitle = "DATA";
|
private static readonly DataTitle = "DATA";
|
||||||
private static readonly NotebooksTitle = "NOTEBOOKS";
|
private static readonly NotebooksTitle = "NOTEBOOKS";
|
||||||
|
|
||||||
|
private static readonly SamplesRepo: IGitHubRepo = {
|
||||||
|
name: "cosmos-notebooks",
|
||||||
|
owner: {
|
||||||
|
login: "Azure-Samples"
|
||||||
|
},
|
||||||
|
private: false
|
||||||
|
};
|
||||||
|
private static readonly SamplesBranch: IGitHubBranch = {
|
||||||
|
name: "master"
|
||||||
|
};
|
||||||
private static readonly PseudoDirPath = "PsuedoDir";
|
private static readonly PseudoDirPath = "PsuedoDir";
|
||||||
|
|
||||||
public parameters: ko.Observable<number>;
|
public parameters: ko.Observable<number>;
|
||||||
@@ -91,7 +103,12 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
|
|
||||||
this.sampleNotebooksContentRoot = {
|
this.sampleNotebooksContentRoot = {
|
||||||
name: "Sample Notebooks (View Only)",
|
name: "Sample Notebooks (View Only)",
|
||||||
path: GitHubUtils.toContentUri(SamplesRepo.owner, SamplesRepo.name, SamplesBranch.name, ""),
|
path: GitHubUtils.toContentUri(
|
||||||
|
ResourceTreeAdapter.SamplesRepo.owner.login,
|
||||||
|
ResourceTreeAdapter.SamplesRepo.name,
|
||||||
|
ResourceTreeAdapter.SamplesBranch.name,
|
||||||
|
""
|
||||||
|
),
|
||||||
type: NotebookContentItemType.Directory
|
type: NotebookContentItemType.Directory
|
||||||
};
|
};
|
||||||
refreshTasks.push(
|
refreshTasks.push(
|
||||||
@@ -301,7 +318,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
onClick: sp.open.bind(sp),
|
onClick: sp.open.bind(sp),
|
||||||
isSelected: () =>
|
isSelected: () =>
|
||||||
this.isDataNodeSelected(collection.rid, "Collection", ViewModels.CollectionTabKind.StoredProcedures),
|
this.isDataNodeSelected(collection.rid, "Collection", ViewModels.CollectionTabKind.StoredProcedures),
|
||||||
contextMenu: ResourceTreeContextMenuButtonFactory.createStoreProcedureContextMenuItems(this.container, sp)
|
contextMenu: ResourceTreeContextMenuButtonFactory.createStoreProcedureContextMenuItems(this.container)
|
||||||
})),
|
})),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.StoredProcedures);
|
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.StoredProcedures);
|
||||||
@@ -318,7 +335,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
onClick: udf.open.bind(udf),
|
onClick: udf.open.bind(udf),
|
||||||
isSelected: () =>
|
isSelected: () =>
|
||||||
this.isDataNodeSelected(collection.rid, "Collection", ViewModels.CollectionTabKind.UserDefinedFunctions),
|
this.isDataNodeSelected(collection.rid, "Collection", ViewModels.CollectionTabKind.UserDefinedFunctions),
|
||||||
contextMenu: ResourceTreeContextMenuButtonFactory.createUserDefinedFunctionContextMenuItems(this.container, udf)
|
contextMenu: ResourceTreeContextMenuButtonFactory.createUserDefinedFunctionContextMenuItems(this.container)
|
||||||
})),
|
})),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.UserDefinedFunctions);
|
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.UserDefinedFunctions);
|
||||||
@@ -334,7 +351,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
label: trigger.id(),
|
label: trigger.id(),
|
||||||
onClick: trigger.open.bind(trigger),
|
onClick: trigger.open.bind(trigger),
|
||||||
isSelected: () => this.isDataNodeSelected(collection.rid, "Collection", ViewModels.CollectionTabKind.Triggers),
|
isSelected: () => this.isDataNodeSelected(collection.rid, "Collection", ViewModels.CollectionTabKind.Triggers),
|
||||||
contextMenu: ResourceTreeContextMenuButtonFactory.createTriggerContextMenuItems(this.container, trigger)
|
contextMenu: ResourceTreeContextMenuButtonFactory.createTriggerContextMenuItems(this.container)
|
||||||
})),
|
})),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Triggers);
|
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Triggers);
|
||||||
@@ -525,10 +542,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
return (
|
return (
|
||||||
activeTab &&
|
activeTab &&
|
||||||
activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
|
activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
|
||||||
/* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab.
|
(activeTab as NotebookTab).notebookPath() === item.path
|
||||||
NotebookV2Tab could be dynamically imported, but not worth it to just get this type right.
|
|
||||||
*/
|
|
||||||
(activeTab as any).notebookPath() === item.path
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
contextMenu: createFileContextMenu
|
contextMenu: createFileContextMenu
|
||||||
@@ -642,10 +656,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
return (
|
return (
|
||||||
activeTab &&
|
activeTab &&
|
||||||
activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
|
activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
|
||||||
/* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab.
|
(activeTab as NotebookTab).notebookPath() === item.path
|
||||||
NotebookV2Tab could be dynamically imported, but not worth it to just get this type right.
|
|
||||||
*/
|
|
||||||
(activeTab as any).notebookPath() === item.path
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
contextMenu:
|
contextMenu:
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import * as DataModels from "../../Contracts/DataModels";
|
|||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
|
||||||
import StoredProcedureTab from "../Tabs/StoredProcedureTab";
|
import StoredProcedureTab from "../Tabs/StoredProcedureTab";
|
||||||
|
import ContextMenu from "../Menus/ContextMenu";
|
||||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import { ContextMenuButtonFactory } from "../ContextMenuButtonFactory";
|
||||||
|
|
||||||
const sampleStoredProcedureBody: string = `// SAMPLE STORED PROCEDURE
|
const sampleStoredProcedureBody: string = `// SAMPLE STORED PROCEDURE
|
||||||
function sample(prefix) {
|
function sample(prefix) {
|
||||||
@@ -42,6 +44,7 @@ export default class StoredProcedure implements ViewModels.StoredProcedure {
|
|||||||
public rid: string;
|
public rid: string;
|
||||||
public id: ko.Observable<string>;
|
public id: ko.Observable<string>;
|
||||||
public body: ko.Observable<string>;
|
public body: ko.Observable<string>;
|
||||||
|
public contextMenu: ViewModels.ContextMenu;
|
||||||
public isExecuteEnabled: boolean;
|
public isExecuteEnabled: boolean;
|
||||||
|
|
||||||
constructor(container: ViewModels.Explorer, collection: ViewModels.Collection, data: DataModels.StoredProcedure) {
|
constructor(container: ViewModels.Explorer, collection: ViewModels.Collection, data: DataModels.StoredProcedure) {
|
||||||
@@ -53,6 +56,9 @@ export default class StoredProcedure implements ViewModels.StoredProcedure {
|
|||||||
this.id = ko.observable(data.id);
|
this.id = ko.observable(data.id);
|
||||||
this.body = ko.observable(data.body);
|
this.body = ko.observable(data.body);
|
||||||
this.isExecuteEnabled = this.container.isFeatureEnabled(Constants.Features.executeSproc);
|
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) {
|
public static create(source: ViewModels.Collection, event: MouseEvent) {
|
||||||
@@ -83,6 +89,9 @@ export default class StoredProcedure implements ViewModels.StoredProcedure {
|
|||||||
|
|
||||||
// Activate
|
// Activate
|
||||||
storedProcedureTab.onTabClick();
|
storedProcedureTab.onTabClick();
|
||||||
|
|
||||||
|
// Hide Context Menu (if necessary)
|
||||||
|
source.contextMenu.hide(source, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
public select() {
|
public select() {
|
||||||
@@ -139,7 +148,10 @@ export default class StoredProcedure implements ViewModels.StoredProcedure {
|
|||||||
storedProcedureTab.onTabClick();
|
storedProcedureTab.onTabClick();
|
||||||
};
|
};
|
||||||
|
|
||||||
public delete() {
|
public delete(source: ViewModels.Collection, event: MouseEvent | KeyboardEvent) {
|
||||||
|
// Hide Context Menu (if necessary)
|
||||||
|
this.contextMenu.hide(source, event);
|
||||||
|
|
||||||
if (!window.confirm("Are you sure you want to delete the stored procedure?")) {
|
if (!window.confirm("Are you sure you want to delete the stored procedure?")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -180,6 +192,33 @@ 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 {
|
public onFocusAfterExecute(): void {
|
||||||
const focusElement = document.getElementById("execute-storedproc-toggles");
|
const focusElement = document.getElementById("execute-storedproc-toggles");
|
||||||
focusElement && focusElement.focus();
|
focusElement && focusElement.focus();
|
||||||
|
|||||||
63
src/Explorer/Tree/StoredProcedureTreeNode.html
Normal file
63
src/Explorer/Tree/StoredProcedureTreeNode.html
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<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
|
||||||
|
"
|
||||||
|
>…</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 -->
|
||||||
76
src/Explorer/Tree/TreeComponents.ts
Normal file
76
src/Explorer/Tree/TreeComponents.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,9 @@ import * as DataModels from "../../Contracts/DataModels";
|
|||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import Collection from "./Collection";
|
import Collection from "./Collection";
|
||||||
import TriggerTab from "../Tabs/TriggerTab";
|
import TriggerTab from "../Tabs/TriggerTab";
|
||||||
|
import ContextMenu from "../Menus/ContextMenu";
|
||||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import { ContextMenuButtonFactory } from "../ContextMenuButtonFactory";
|
||||||
|
|
||||||
export default class Trigger implements ViewModels.Trigger {
|
export default class Trigger implements ViewModels.Trigger {
|
||||||
public nodeKind: string;
|
public nodeKind: string;
|
||||||
@@ -17,6 +19,7 @@ export default class Trigger implements ViewModels.Trigger {
|
|||||||
public body: ko.Observable<string>;
|
public body: ko.Observable<string>;
|
||||||
public triggerType: ko.Observable<string>;
|
public triggerType: ko.Observable<string>;
|
||||||
public triggerOperation: ko.Observable<string>;
|
public triggerOperation: ko.Observable<string>;
|
||||||
|
public contextMenu: ViewModels.ContextMenu;
|
||||||
|
|
||||||
constructor(container: ViewModels.Explorer, collection: ViewModels.Collection, data: any) {
|
constructor(container: ViewModels.Explorer, collection: ViewModels.Collection, data: any) {
|
||||||
this.nodeKind = "Trigger";
|
this.nodeKind = "Trigger";
|
||||||
@@ -28,6 +31,9 @@ export default class Trigger implements ViewModels.Trigger {
|
|||||||
this.body = ko.observable(data.body);
|
this.body = ko.observable(data.body);
|
||||||
this.triggerOperation = ko.observable(data.triggerOperation);
|
this.triggerOperation = ko.observable(data.triggerOperation);
|
||||||
this.triggerType = ko.observable(data.triggerType);
|
this.triggerType = ko.observable(data.triggerType);
|
||||||
|
|
||||||
|
this.contextMenu = new ContextMenu(this.container, this.rid);
|
||||||
|
this.contextMenu.options(ContextMenuButtonFactory.createTriggerContextMenuButton(container));
|
||||||
}
|
}
|
||||||
|
|
||||||
public select() {
|
public select() {
|
||||||
@@ -72,6 +78,9 @@ export default class Trigger implements ViewModels.Trigger {
|
|||||||
|
|
||||||
// Activate
|
// Activate
|
||||||
triggerTab.onTabClick();
|
triggerTab.onTabClick();
|
||||||
|
|
||||||
|
// Hide Context Menu (if necessary)
|
||||||
|
source.contextMenu.hide(source, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
public open = () => {
|
public open = () => {
|
||||||
@@ -116,7 +125,10 @@ export default class Trigger implements ViewModels.Trigger {
|
|||||||
triggerTab.onTabClick();
|
triggerTab.onTabClick();
|
||||||
};
|
};
|
||||||
|
|
||||||
public delete() {
|
public delete(source: Collection, event: MouseEvent | KeyboardEvent) {
|
||||||
|
// Hide Context Menu (if necessary)
|
||||||
|
this.contextMenu.hide(source, event);
|
||||||
|
|
||||||
if (!window.confirm("Are you sure you want to delete the trigger?")) {
|
if (!window.confirm("Are you sure you want to delete the trigger?")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -138,4 +150,31 @@ export default class Trigger implements ViewModels.Trigger {
|
|||||||
reason => {}
|
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;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user