Compare commits

..

1 Commits

Author SHA1 Message Date
Steve Faulkner
2a5ccbb51b Force enable notebooks 2021-04-08 21:55:48 -05:00
243 changed files with 9092 additions and 22547 deletions

View File

@@ -45,6 +45,7 @@ src/Definitions/jquery.d.ts
src/Definitions/plotly.js-cartesian-dist.d-min.ts src/Definitions/plotly.js-cartesian-dist.d-min.ts
src/Definitions/png.d.ts src/Definitions/png.d.ts
src/Definitions/svg.d.ts src/Definitions/svg.d.ts
src/Definitions/worker.d.ts
src/Explorer/ComponentRegisterer.test.ts src/Explorer/ComponentRegisterer.test.ts
src/Explorer/ComponentRegisterer.ts src/Explorer/ComponentRegisterer.ts
src/Explorer/ContextMenuButtonFactory.ts src/Explorer/ContextMenuButtonFactory.ts
@@ -90,7 +91,8 @@ src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.test.ts
src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.ts src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.ts
src/Explorer/Graph/GraphStyleComponent/GraphStyle.test.ts src/Explorer/Graph/GraphStyleComponent/GraphStyle.test.ts
src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.ts src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.ts
src/Explorer/Graph/NewVertexComponent/NewVertex.test.ts
src/Explorer/Graph/NewVertexComponent/NewVertexComponent.ts
src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts
src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.ts src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.ts
src/Explorer/Menus/ContextMenu.ts src/Explorer/Menus/ContextMenu.ts
@@ -119,16 +121,21 @@ src/Explorer/Panes/AddDatabasePane.ts
src/Explorer/Panes/BrowseQueriesPane.ts src/Explorer/Panes/BrowseQueriesPane.ts
src/Explorer/Panes/CassandraAddCollectionPane.ts src/Explorer/Panes/CassandraAddCollectionPane.ts
src/Explorer/Panes/ContextualPaneBase.ts src/Explorer/Panes/ContextualPaneBase.ts
src/Explorer/Panes/DeleteCollectionConfirmationPane.test.ts
src/Explorer/Panes/DeleteCollectionConfirmationPane.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts
src/Explorer/Panes/GraphStylingPane.ts src/Explorer/Panes/GraphStylingPane.ts
# src/Explorer/Panes/NewVertexPane.ts src/Explorer/Panes/NewVertexPane.ts
src/Explorer/Panes/PaneComponents.ts src/Explorer/Panes/PaneComponents.ts
src/Explorer/Panes/RenewAdHocAccessPane.ts src/Explorer/Panes/RenewAdHocAccessPane.ts
src/Explorer/Panes/SetupNotebooksPane.ts
src/Explorer/Panes/StringInputPane.ts src/Explorer/Panes/StringInputPane.ts
src/Explorer/Panes/SwitchDirectoryPane.ts src/Explorer/Panes/SwitchDirectoryPane.ts
src/Explorer/Panes/Tables/AddTableEntityPane.ts
src/Explorer/Panes/Tables/EditTableEntityPane.ts src/Explorer/Panes/Tables/EditTableEntityPane.ts
src/Explorer/Panes/Tables/EntityPropertyViewModel.ts src/Explorer/Panes/Tables/EntityPropertyViewModel.ts
src/Explorer/Panes/Tables/QuerySelectPane.ts
src/Explorer/Panes/Tables/TableEntityPane.ts src/Explorer/Panes/Tables/TableEntityPane.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts
@@ -244,6 +251,7 @@ src/Utils/QueryUtils.test.ts
src/applyExplorerBindings.ts src/applyExplorerBindings.ts
src/global.d.ts src/global.d.ts
src/setupTests.ts src/setupTests.ts
src/workers/upload/index.ts
src/Explorer/Controls/AccessibleElement/AccessibleElement.tsx src/Explorer/Controls/AccessibleElement/AccessibleElement.tsx
src/Explorer/Controls/Accordion/AccordionComponent.tsx src/Explorer/Controls/Accordion/AccordionComponent.tsx
src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.test.tsx src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.test.tsx
@@ -291,6 +299,8 @@ src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.t
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx
src/Explorer/Menus/CommandBar/CommandBarUtil.tsx src/Explorer/Menus/CommandBar/CommandBarUtil.tsx
src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx
src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx
src/Explorer/Menus/NotificationConsole/NotificationConsoleComponentAdapter.tsx
src/Explorer/Notebook/NotebookComponent/NotebookComponent.tsx src/Explorer/Notebook/NotebookComponent/NotebookComponent.tsx
src/Explorer/Notebook/NotebookComponent/NotebookComponentAdapter.tsx src/Explorer/Notebook/NotebookComponent/NotebookComponentAdapter.tsx
src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx

View File

@@ -3,7 +3,7 @@ module.exports = {
browser: true, browser: true,
es6: true, es6: true,
}, },
plugins: ["@typescript-eslint", "no-null", "prefer-arrow", "react-hooks"], plugins: ["@typescript-eslint", "no-null", "prefer-arrow"],
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
globals: { globals: {
Atomics: "readonly", Atomics: "readonly",
@@ -20,7 +20,7 @@ module.exports = {
overrides: [ overrides: [
{ {
files: ["**/*.tsx"], files: ["**/*.tsx"],
extends: ["plugin:react/recommended"], extends: ["plugin:react/recommended"], // TODO: Add react-hooks
plugins: ["react"], plugins: ["react"],
}, },
{ {
@@ -42,8 +42,6 @@ module.exports = {
"prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }], "prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }],
eqeqeq: "error", eqeqeq: "error",
"react/display-name": "off", "react/display-name": "off",
"react-hooks/rules-of-hooks": "warn", // TODO: error
"react-hooks/exhaustive-deps": "warn", // TODO: error
"no-restricted-syntax": [ "no-restricted-syntax": [
"error", "error",
{ {

View File

@@ -1 +0,0 @@
[Preview this branch](https://cosmos-explorer-preview.azurewebsites.net/pull/EDIT_THIS_NUMBER_IN_THE_PR_DESCRIPTION?feature.someFeatureFlagYouMightNeed=true)

View File

@@ -101,8 +101,7 @@ jobs:
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }} PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
endtoendemulator: endtoendemulator:
name: "End To End Emulator Tests" name: "End To End Emulator Tests"
# Temporarily disabled. This test needs to be rewritten in playwright if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
if: false
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -127,21 +126,58 @@ jobs:
with: with:
name: screenshots name: screenshots
path: failed-* path: failed-*
endtoend: accessibility:
name: "E2E" name: "Accessibility | Hosted"
needs: [lint, format, compile, unittest]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14.x
uses: actions/setup-node@v1
with:
node-version: 14.x
- name: Accessibility Check
run: |
# Ubuntu gets mad when webpack runs too many files watchers
cat /proc/sys/fs/inotify/max_user_watches
sudo sysctl fs.inotify.max_user_watches=524288
sudo sysctl -p
npm ci
npm start &
npx wait-on -i 5000 https-get://0.0.0.0:1234/
node utils/accesibilityCheck.js
shell: bash
env:
NODE_TLS_REJECT_UNAUTHORIZED: 0
endtoendhosted:
name: "End to End Tests"
needs: [cleanupaccounts]
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
NODE_TLS_REJECT_UNAUTHORIZED: 0 NODE_TLS_REJECT_UNAUTHORIZED: 0
PORTAL_RUNNER_SUBSCRIPTION: ${{ secrets.PORTAL_RUNNER_SUBSCRIPTION }}
PORTAL_RUNNER_RESOURCE_GROUP: ${{ secrets.PORTAL_RUNNER_RESOURCE_GROUP }}
PORTAL_RUNNER_DATABASE_ACCOUNT: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT }}
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY }}
PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT: ${{ secrets.PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT }}
PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT_KEY: ${{ secrets.PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT_KEY }}
NOTEBOOKS_TEST_RUNNER_TENANT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_TENANT_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }} NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
PORTAL_RUNNER_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }}
TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }}
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/hostedExplorer.html"
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
test-file: test-file:
- ./test/cassandra/container.spec.ts - ./test/cassandra/container.spec.ts
- ./test/sql/container.spec.ts - ./test/mongo/mongoIndexPolicy.spec.ts
- ./test/mongo/container.spec.ts - ./test/notebooks/uploadAndOpenNotebook.spec.ts
- ./test/selfServe/selfServeExample.spec.ts - ./test/selfServe/selfServeExample.spec.ts
- ./test/notebooks/upload.spec.ts - ./test/sql/container.spec.ts
- ./test/sql/resourceToken.spec.ts - ./test/sql/resourceToken.spec.ts
- ./test/tables/container.spec.ts - ./test/tables/container.spec.ts
steps: steps:
@@ -152,17 +188,30 @@ jobs:
node-version: 14.x node-version: 14.x
- run: npm ci - run: npm ci
- run: npm start & - run: npm start &
- run: node utils/cleanupDBs.js
- run: npm run wait-for-server - run: npm run wait-for-server
- name: ${{ matrix['test-file'] }} - name: ${{ matrix['test-file'] }}
run: | run: npx jest -c ./jest.config.e2e.js --detectOpenHandles ${{ matrix['test-file'] }}
# Run tests up to three times
for i in $(seq 1 3); do npx jest -c ./jest.config.playwright.js ${{ matrix['test-file'] }} && s=0 && break || s=$? && sleep 1; done; (exit $s)
shell: bash shell: bash
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
if: failure() if: failure()
with: with:
name: screenshots name: screenshots
path: screenshots/ path: failed-*
cleanupaccounts:
name: "Cleanup Test Database Accounts"
runs-on: ubuntu-latest
env:
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14.x
uses: actions/setup-node@v1
with:
node-version: 14.x
- run: npm ci
- run: node utils/cleanupDBs.js
nuget: nuget:
name: Publish Nuget name: Publish Nuget
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')

View File

@@ -1,28 +0,0 @@
# This is a basic workflow to help you get started with Actions
name: Cleanup End to End Account Resources
on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
schedule:
# Once every hour
- cron: "0 * * * *"
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
cleanupaccounts:
name: "Cleanup Test Database Accounts"
runs-on: ubuntu-latest
env:
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14.x
uses: actions/setup-node@v1
with:
node-version: 14.x
- run: npm ci
- run: node utils/cleanupDBs.js

2
.gitignore vendored
View File

@@ -15,5 +15,3 @@ Contracts/*
.cache/ .cache/
.env .env
failure.png failure.png
screenshots/*
GettingStarted-ignore*.ipynb

View File

@@ -153,7 +153,7 @@ Cosmos Explorer has been under constant development for over 5 years. As a resul
✅ DO ✅ DO
- Use [Playwright](https://github.com/microsoft/playwright) and [Jest](https://jestjs.io/) - Use [Puppeteer](https://developers.google.com/web/tools/puppeteer) and [Jest](https://jestjs.io/)
- Write or modify an existing E2E test that covers the primary use case of any major feature. - Write or modify an existing E2E test that covers the primary use case of any major feature.
- Use caution. Do not try to cover every case. End to End tests can be slow and brittle. - Use caution. Do not try to cover every case. End to End tests can be slow and brittle.

File diff suppressed because one or more lines are too long

View File

@@ -1,13 +0,0 @@
const isCI = require("is-ci");
module.exports = {
exitOnPageError: false,
launchOptions: {
headless: isCI,
slowMo: 10,
timeout: 60000,
},
contextOptions: {
ignoreHTTPSErrors: true,
},
};

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

@@ -0,0 +1,12 @@
const isCI = require("is-ci");
module.exports = {
launch: {
headless: isCI,
slowMo: 55,
defaultViewport: null,
ignoreHTTPSErrors: true,
args: ["--disable-web-security"],
exitOnPageError: false,
},
};

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

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

View File

@@ -69,6 +69,7 @@ module.exports = {
moduleNameMapper: { moduleNameMapper: {
"^.*[.](svg|png|gif|less|css)$": "<rootDir>/mockModule", "^.*[.](svg|png|gif|less|css)$": "<rootDir>/mockModule",
"@nteract/stateful-components/(.*)$": "<rootDir>/mockModule", "@nteract/stateful-components/(.*)$": "<rootDir>/mockModule",
"worker-loader": "<rootDir>/mockModule",
"office-ui-fabric-react/lib/(.*)$": "office-ui-fabric-react/lib-commonjs/$1", // https://github.com/OfficeDev/office-ui-fabric-react/wiki/Fabric-6-Release-Notes "office-ui-fabric-react/lib/(.*)$": "office-ui-fabric-react/lib-commonjs/$1", // https://github.com/OfficeDev/office-ui-fabric-react/wiki/Fabric-6-Release-Notes
"^dnd-core$": "dnd-core/dist/cjs", "^dnd-core$": "dnd-core/dist/cjs",
"^react-dnd$": "react-dnd/dist/cjs", "^react-dnd$": "react-dnd/dist/cjs",

View File

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

4917
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@azure/arm-cosmosdb": "9.1.0", "@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "3.10.5", "@azure/cosmos": "3.9.0",
"@azure/cosmos-language-service": "0.0.5", "@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "1.2.1", "@azure/identity": "1.2.1",
"@azure/ms-rest-nodeauth": "3.0.7", "@azure/ms-rest-nodeauth": "3.0.7",
@@ -25,7 +25,7 @@
"@nteract/iron-icons": "1.0.0", "@nteract/iron-icons": "1.0.0",
"@nteract/jupyter-widgets": "2.0.0", "@nteract/jupyter-widgets": "2.0.0",
"@nteract/logos": "1.0.0", "@nteract/logos": "1.0.0",
"@nteract/markdown": "4.6.0", "@nteract/markdown": "4.4.0",
"@nteract/monaco-editor": "3.2.2", "@nteract/monaco-editor": "3.2.2",
"@nteract/octicons": "2.0.0", "@nteract/octicons": "2.0.0",
"@nteract/outputs": "3.0.9", "@nteract/outputs": "3.0.9",
@@ -65,7 +65,6 @@
"i18next": "19.8.4", "i18next": "19.8.4",
"i18next-browser-languagedetector": "6.0.1", "i18next-browser-languagedetector": "6.0.1",
"i18next-http-backend": "1.0.23", "i18next-http-backend": "1.0.23",
"iframe-resizer-react": "1.1.0",
"immutable": "4.0.0-rc.12", "immutable": "4.0.0-rc.12",
"is-ci": "2.0.0", "is-ci": "2.0.0",
"jquery": "3.5.1", "jquery": "3.5.1",
@@ -79,7 +78,6 @@
"office-ui-fabric-react": "7.164.2", "office-ui-fabric-react": "7.164.2",
"p-retry": "4.2.0", "p-retry": "4.2.0",
"plotly.js-cartesian-dist-min": "1.52.3", "plotly.js-cartesian-dist-min": "1.52.3",
"post-robot": "10.0.42",
"q": "1.5.1", "q": "1.5.1",
"react": "16.13.1", "react": "16.13.1",
"react-animate-height": "2.0.8", "react-animate-height": "2.0.8",
@@ -94,7 +92,6 @@
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"rx-jupyter": "5.5.12", "rx-jupyter": "5.5.12",
"rxjs": "6.6.3", "rxjs": "6.6.3",
"sanitize-html": "2.3.3",
"styled-components": "4.3.2", "styled-components": "4.3.2",
"swr": "0.4.0", "swr": "0.4.0",
"terser-webpack-plugin": "3.1.0", "terser-webpack-plugin": "3.1.0",
@@ -113,23 +110,26 @@
"@types/d3": "5.9.2", "@types/d3": "5.9.2",
"@types/enzyme": "3.10.7", "@types/enzyme": "3.10.7",
"@types/enzyme-adapter-react-16": "1.0.6", "@types/enzyme-adapter-react-16": "1.0.6",
"@types/expect-puppeteer": "4.4.5",
"@types/hasher": "0.0.31", "@types/hasher": "0.0.31",
"@types/jest": "26.0.20", "@types/jest": "26.0.20",
"@types/jest-environment-puppeteer": "4.4.1",
"@types/memoize-one": "4.1.1", "@types/memoize-one": "4.1.1",
"@types/node": "12.11.1", "@types/node": "12.11.1",
"@types/post-robot": "10.0.1",
"@types/promise.prototype.finally": "2.0.3", "@types/promise.prototype.finally": "2.0.3",
"@types/prop-types": "15.5.8",
"@types/puppeteer": "5.4.3",
"@types/q": "1.5.1", "@types/q": "1.5.1",
"@types/react": "17.0.3", "@types/react": "17.0.0",
"@types/react-dom": "17.0.3", "@types/react-dom": "17.0.0",
"@types/react-notification-system": "0.2.39", "@types/react-notification-system": "0.2.39",
"@types/react-redux": "7.1.7", "@types/react-redux": "7.1.7",
"@types/sanitize-html": "1.27.2",
"@types/sinon": "2.3.3", "@types/sinon": "2.3.3",
"@types/styled-components": "5.1.1", "@types/styled-components": "5.1.1",
"@types/underscore": "1.7.36", "@types/underscore": "1.7.36",
"@typescript-eslint/eslint-plugin": "4.0.1", "@typescript-eslint/eslint-plugin": "4.0.1",
"@typescript-eslint/parser": "4.0.1", "@typescript-eslint/parser": "4.0.1",
"axe-puppeteer": "1.1.0",
"babel-jest": "24.9.0", "babel-jest": "24.9.0",
"babel-loader": "8.1.0", "babel-loader": "8.1.0",
"buffer": "5.1.0", "buffer": "5.1.0",
@@ -144,18 +144,16 @@
"eslint-plugin-no-null": "1.0.2", "eslint-plugin-no-null": "1.0.2",
"eslint-plugin-prefer-arrow": "1.2.2", "eslint-plugin-prefer-arrow": "1.2.2",
"eslint-plugin-react-hooks": "4.2.0", "eslint-plugin-react-hooks": "4.2.0",
"expect-playwright": "0.3.3",
"expose-loader": "0.7.5", "expose-loader": "0.7.5",
"fast-glob": "3.2.5", "fast-glob": "3.2.5",
"file-loader": "2.0.0", "file-loader": "2.0.0",
"fs-extra": "7.0.0", "fs-extra": "7.0.0",
"html-inline-css-webpack-plugin": "1.11.0",
"html-loader": "0.5.5", "html-loader": "0.5.5",
"html-loader-jest": "0.2.1", "html-loader-jest": "0.2.1",
"html-webpack-plugin": "4.5.2", "html-webpack-plugin": "3.2.0",
"jest": "25.5.4", "jest": "25.5.4",
"jest-canvas-mock": "2.1.0", "jest-canvas-mock": "2.1.0",
"jest-playwright-preset": "1.5.1", "jest-puppeteer": "4.4.0",
"jest-trx-results-processor": "0.0.7", "jest-trx-results-processor": "0.0.7",
"less": "3.8.1", "less": "3.8.1",
"less-loader": "4.1.0", "less-loader": "4.1.0",
@@ -163,23 +161,23 @@
"mini-css-extract-plugin": "0.4.3", "mini-css-extract-plugin": "0.4.3",
"monaco-editor-webpack-plugin": "1.7.0", "monaco-editor-webpack-plugin": "1.7.0",
"node-fetch": "2.6.1", "node-fetch": "2.6.1",
"playwright": "1.10.0",
"prettier": "2.2.1", "prettier": "2.2.1",
"puppeteer": "8.0.0",
"raw-loader": "0.5.1", "raw-loader": "0.5.1",
"react-dev-utils": "11.0.4",
"rimraf": "3.0.0", "rimraf": "3.0.0",
"sinon": "3.2.1", "sinon": "3.2.1",
"style-loader": "0.23.0", "style-loader": "0.23.0",
"ts-loader": "6.2.2", "ts-loader": "6.2.2",
"tslint": "5.11.0", "tslint": "5.11.0",
"tslint-microsoft-contrib": "6.0.0", "tslint-microsoft-contrib": "6.0.0",
"typescript": "4.2.4", "typescript": "4.2.3",
"url-loader": "1.1.1", "url-loader": "1.1.1",
"wait-on": "4.0.2", "wait-on": "4.0.2",
"webpack": "4.43.0", "webpack": "4.43.0",
"webpack-bundle-analyzer": "3.6.1", "webpack-bundle-analyzer": "3.6.1",
"webpack-cli": "3.3.10", "webpack-cli": "3.3.10",
"webpack-dev-server": "3.11.0" "webpack-dev-server": "3.11.0",
"worker-loader": "2.0.0"
}, },
"scripts": { "scripts": {
"start": "node --max-old-space-size=10196 node_modules/webpack-dev-server/bin/webpack-dev-server.js", "start": "node --max-old-space-size=10196 node_modules/webpack-dev-server/bin/webpack-dev-server.js",
@@ -202,8 +200,8 @@
"format:check": "prettier --check \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"", "format:check": "prettier --check \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
"lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"", "lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"",
"build:contracts": "npm run compile:contracts", "build:contracts": "npm run compile:contracts",
"strict:find": "node ./strict-null-checks/find.js", "strictEligibleFiles": "node ./strict-migration-tools/index.js",
"strict:add": "node ./strict-null-checks/auto-add.js", "autoAddStrictEligibleFiles": "node ./strict-migration-tools/autoAdd.js",
"compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks", "compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks",
"generateARMClients": "ts-node --compiler-options '{\"module\":\"commonjs\"}' utils/armClientGenerator/generator.ts" "generateARMClients": "ts-node --compiler-options '{\"module\":\"commonjs\"}' utils/armClientGenerator/generator.ts"
}, },

View File

@@ -1,7 +1,6 @@
const express = require("express"); const express = require("express");
const { createProxyMiddleware } = require("http-proxy-middleware"); const { createProxyMiddleware } = require("http-proxy-middleware");
const port = process.env.PORT || 3000; const port = process.env.PORT || 3000;
const fetch = require("node-fetch");
const api = createProxyMiddleware("/api", { const api = createProxyMiddleware("/api", {
target: "https://main.documentdb.ext.azure.com", target: "https://main.documentdb.ext.azure.com",
@@ -40,29 +39,6 @@ const app = express();
app.use(api); app.use(api);
app.use(proxy); app.use(proxy);
app.use(commit); app.use(commit);
app.get("/pull/:pr(\\d+)", (req, res) => {
const pr = req.params.pr;
const [, query] = req.originalUrl.split("?");
const search = new URLSearchParams(query);
fetch("https://api.github.com/repos/Azure/cosmos-explorer/pulls/" + pr)
.then((response) => response.json())
.then(({ head: { ref, sha } }) => {
const prUrl = new URL("https://github.com/Azure/cosmos-explorer/pull/" + pr);
prUrl.hash = ref;
search.set("feature.pr", prUrl.href);
const explorer = new URL("https://cosmos-explorer-preview.azurewebsites.net/commit/" + sha + "/explorer.html");
explorer.search = search.toString();
const portal = new URL("https://ms.portal.azure.com/");
portal.searchParams.set("dataExplorerSource", explorer.href);
return res.redirect(portal.href);
})
.catch(() => res.sendStatus(500));
});
app.listen(port, () => { app.listen(port, () => {
console.log(`Example app listening on port: ${port}`); console.log(`Example app listening on port: ${port}`);
}); });

View File

@@ -1,658 +1,8 @@
{ {
"name": "cosmos-explorer-preview", "name": "preview",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 2, "lockfileVersion": 1,
"requires": true, "requires": true,
"packages": {
"": {
"name": "cosmos-explorer-preview",
"version": "1.0.0",
"dependencies": {
"express": "^4.17.1",
"http-proxy-middleware": "^1.1.0",
"node-fetch": "^2.6.1"
}
},
"node_modules/@types/http-proxy": {
"version": "1.17.5",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.5.tgz",
"integrity": "sha512-GNkDE7bTv6Sf8JbV2GksknKOsk7OznNYHSdrtvPJXO0qJ9odZig6IZKUi5RFGi6d1bf6dgIAe4uXi3DBc7069Q==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "14.14.37",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz",
"integrity": "sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw=="
},
"node_modules/accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"dependencies": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"node_modules/body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"dependencies": {
"bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dependencies": {
"fill-range": "^7.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/camelcase": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz",
"integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==",
"engines": {
"node": ">=10"
}
},
"node_modules/content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
"dependencies": {
"safe-buffer": "5.1.2"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
},
"node_modules/express": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
"dependencies": {
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"content-type": "~1.0.4",
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"statuses": "~1.5.0",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/follow-redirects": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz",
"integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==",
"engines": {
"node": ">=4.0"
}
},
"node_modules/forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"dependencies": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/http-proxy": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
"dependencies": {
"eventemitter3": "^4.0.0",
"follow-redirects": "^1.0.0",
"requires-port": "^1.0.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/http-proxy-middleware": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.1.0.tgz",
"integrity": "sha512-OnjU5vyVgcZVe2AjLJyMrk8YLNOC2lspCHirB5ldM+B/dwEfZ5bgVTrFyzE9R7xRWAP/i/FXtvIqKjTNEZBhBg==",
"dependencies": {
"@types/http-proxy": "^1.17.5",
"camelcase": "^6.2.0",
"http-proxy": "^1.18.1",
"is-glob": "^4.0.1",
"is-plain-obj": "^3.0.0",
"micromatch": "^4.0.2"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/is-plain-obj": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
"integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==",
"engines": {
"node": ">=10"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/micromatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
"integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
"dependencies": {
"braces": "^3.0.1",
"picomatch": "^2.0.5"
},
"engines": {
"node": ">=8"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.46.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz",
"integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.29",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.29.tgz",
"integrity": "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==",
"dependencies": {
"mime-db": "1.46.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"node_modules/negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/node-fetch": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
"engines": {
"node": "4.x || >=6.0.0"
}
},
"node_modules/on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"node_modules/picomatch": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
"integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
"engines": {
"node": ">=8.6"
}
},
"node_modules/proxy-addr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
"integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
"dependencies": {
"forwarded": "~0.1.2",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"dependencies": {
"bytes": "3.1.0",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"dependencies": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.7.2",
"mime": "1.6.0",
"ms": "2.1.1",
"on-finished": "~2.3.0",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
},
"node_modules/serve-static": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"dependencies": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"node_modules/statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
"engines": {
"node": ">= 0.8"
}
}
},
"dependencies": { "dependencies": {
"@types/http-proxy": { "@types/http-proxy": {
"version": "1.17.5", "version": "1.17.5",
@@ -984,11 +334,6 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
}, },
"node-fetch": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
},
"on-finished": { "on-finished": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",

View File

@@ -12,7 +12,6 @@
"author": "Microsoft Corporation", "author": "Microsoft Corporation",
"dependencies": { "dependencies": {
"express": "^4.17.1", "express": "^4.17.1",
"http-proxy-middleware": "^1.1.0", "http-proxy-middleware": "^1.1.0"
"node-fetch": "^2.6.1"
} }
} }

View File

@@ -1,68 +0,0 @@
import { createImmutableOutput, JSONObject, OnDiskOutput } from "@nteract/commutable";
// import outputs individually to avoid increasing the bundle size
import { KernelOutputError } from "@nteract/outputs/lib/components/kernel-output-error";
import { Output } from "@nteract/outputs/lib/components/output";
import { StreamText } from "@nteract/outputs/lib/components/stream-text";
import { ContentRef } from "@nteract/types";
import "bootstrap/dist/css/bootstrap.css";
import postRobot from "post-robot";
import * as React from "react";
import * as ReactDOM from "react-dom";
import "../../externals/iframeResizer.contentWindow.min.js"; // Required for iFrameResizer to work
import "../Explorer/Notebook/NotebookRenderer/base.css";
import "../Explorer/Notebook/NotebookRenderer/default.css";
import { TransformMedia } from "./TransformMedia";
export interface CellOutputViewerProps {
id: string;
contentRef: ContentRef;
hidden: boolean;
expanded: boolean;
outputs: OnDiskOutput[];
onMetadataChange: (metadata: JSONObject, mediaType: string, index?: number) => void;
}
const onInit = async () => {
postRobot.on(
"props",
{
window: window.parent,
domain: window.location.origin,
},
(event) => {
// Typescript definition for event is wrong. So read props by casting to <any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const props = (event as any).data as CellOutputViewerProps;
const outputs = (
<div
data-iframe-height
className={`nteract-cell-outputs ${props.hidden ? "hidden" : ""} ${props.expanded ? "expanded" : ""}`}
>
{props.outputs?.map((output, index) => (
<Output output={createImmutableOutput(output)} key={index}>
<TransformMedia
output_type={"display_data"}
id={props.id}
contentRef={props.contentRef}
onMetadataChange={(metadata, mediaType) => props.onMetadataChange(metadata, mediaType, index)}
/>
<TransformMedia
output_type={"execute_result"}
id={props.id}
contentRef={props.contentRef}
onMetadataChange={(metadata, mediaType) => props.onMetadataChange(metadata, mediaType, index)}
/>
<KernelOutputError />
<StreamText />
</Output>
))}
</div>
);
ReactDOM.render(outputs, document.getElementById("cellOutput"));
}
);
};
// Entry point
window.addEventListener("load", onInit);

View File

@@ -1,138 +0,0 @@
import { ImmutableDisplayData, ImmutableExecuteResult, JSONObject } from "@nteract/commutable";
// import outputs individually to avoid increasing the bundle size
import { HTML } from "@nteract/outputs/lib/components/media/html";
import { Image } from "@nteract/outputs/lib/components/media/image";
import { JavaScript } from "@nteract/outputs/lib/components/media/javascript";
import { Json } from "@nteract/outputs/lib/components/media/json";
import { LaTeX } from "@nteract/outputs/lib/components/media/latex";
import { Plain } from "@nteract/outputs/lib/components/media/plain";
import { SVG } from "@nteract/outputs/lib/components/media/svg";
import { ContentRef } from "@nteract/types";
import React, { Suspense } from "react";
const EmptyTransform = (): JSX.Element => <></>;
const displayOrder = [
"application/vnd.jupyter.widget-view+json",
"application/vnd.vega.v5+json",
"application/vnd.vega.v4+json",
"application/vnd.vega.v3+json",
"application/vnd.vega.v2+json",
"application/vnd.vegalite.v4+json",
"application/vnd.vegalite.v3+json",
"application/vnd.vegalite.v2+json",
"application/vnd.vegalite.v1+json",
"application/geo+json",
"application/vnd.plotly.v1+json",
"text/vnd.plotly.v1+html",
"application/x-nteract-model-debug+json",
"application/vnd.dataresource+json",
"application/vdom.v1+json",
"application/json",
"application/javascript",
"text/html",
"text/markdown",
"text/latex",
"image/svg+xml",
"image/gif",
"image/png",
"image/jpeg",
"text/plain",
];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const transformsById = new Map<string, React.ComponentType<any>>([
["text/vnd.plotly.v1+html", React.lazy(() => import("@nteract/transform-plotly"))],
["application/vnd.plotly.v1+json", React.lazy(() => import("@nteract/transform-plotly"))],
["application/geo+json", EmptyTransform], // TODO: The geojson transform will likely need some work because of the basemap URL(s)
["application/x-nteract-model-debug+json", React.lazy(() => import("@nteract/transform-model-debug"))],
["application/vnd.dataresource+json", React.lazy(() => import("@nteract/data-explorer"))],
["application/vnd.jupyter.widget-view+json", React.lazy(() => import("./transforms/WidgetDisplay"))],
["application/vnd.vegalite.v1+json", React.lazy(() => import("./transforms/VegaLite1"))],
["application/vnd.vegalite.v2+json", React.lazy(() => import("./transforms/VegaLite2"))],
["application/vnd.vegalite.v3+json", React.lazy(() => import("./transforms/VegaLite3"))],
["application/vnd.vegalite.v4+json", React.lazy(() => import("./transforms/VegaLite4"))],
["application/vnd.vega.v2+json", React.lazy(() => import("./transforms/Vega2"))],
["application/vnd.vega.v3+json", React.lazy(() => import("./transforms/Vega3"))],
["application/vnd.vega.v4+json", React.lazy(() => import("./transforms/Vega4"))],
["application/vnd.vega.v5+json", React.lazy(() => import("./transforms/Vega5"))],
["application/vdom.v1+json", React.lazy(() => import("@nteract/transform-vdom"))],
["application/json", Json],
["application/javascript", JavaScript],
["text/html", HTML],
["text/markdown", React.lazy(() => import("@nteract/outputs/lib/components/media/markdown"))], // Markdown increases the bundle size so lazy load it
["text/latex", LaTeX],
["image/svg+xml", SVG],
["image/gif", Image],
["image/png", Image],
["image/jpeg", Image],
["text/plain", Plain],
]);
interface TransformMediaProps {
output_type: string;
id: string;
contentRef: ContentRef;
output?: ImmutableDisplayData | ImmutableExecuteResult;
onMetadataChange: (metadata: JSONObject, mediaType: string) => void;
}
export const TransformMedia = (props: TransformMediaProps): JSX.Element => {
const { Media, mediaType, data, metadata } = getMediaInfo(props);
// If we had no valid result, return an empty output
if (!mediaType || !data) {
return <></>;
}
return (
<Suspense fallback={<div>Loading...</div>}>
<Media
onMetadataChange={props.onMetadataChange}
data={data}
metadata={metadata}
contentRef={props.contentRef}
id={props.id}
/>
</Suspense>
);
};
const getMediaInfo = (props: TransformMediaProps) => {
const { output, output_type } = props;
// This component should only be used with display data and execute result
if (!output || !(output_type === "display_data" || output_type === "execute_result")) {
console.warn("connected transform media managed to get a non media bundle output");
return {
Media: EmptyTransform,
};
}
// Find the first mediaType in the output data that we support with a handler
const mediaType = displayOrder.find(
(key) =>
Object.prototype.hasOwnProperty.call(output.data, key) &&
(Object.prototype.hasOwnProperty.call(transformsById, key) || transformsById.get(key))
);
if (mediaType) {
const metadata = output.metadata.get(mediaType);
const data = output.data[mediaType];
const Media = transformsById.get(mediaType);
return {
Media,
mediaType,
data,
metadata,
};
}
return {
Media: EmptyTransform,
mediaType,
output,
};
};
export default TransformMedia;

View File

@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="height=device-height, width=device-width, initial-scale=1.0" />
<title>Cell Output Viewer</title>
</head>
<body>
<div class="cellOutput" id="cellOutput"></div>
</body>
</html>

View File

@@ -1 +0,0 @@
export { Vega2 as default } from "@nteract/transform-vega";

View File

@@ -1 +0,0 @@
export { Vega3 as default } from "@nteract/transform-vega";

View File

@@ -1 +0,0 @@
export { Vega4 as default } from "@nteract/transform-vega";

View File

@@ -1 +0,0 @@
export { Vega5 as default } from "@nteract/transform-vega";

View File

@@ -1 +0,0 @@
export { VegaLite1 as default } from "@nteract/transform-vega";

View File

@@ -1 +0,0 @@
export { VegaLite2 as default } from "@nteract/transform-vega";

View File

@@ -1 +0,0 @@
export { VegaLite3 as default } from "@nteract/transform-vega";

View File

@@ -1 +0,0 @@
export { VegaLite4 as default } from "@nteract/transform-vega";

View File

@@ -1 +0,0 @@
export { WidgetDisplay as default } from "@nteract/jupyter-widgets";

View File

@@ -75,10 +75,7 @@ export async function getTokenFromAuthService(verb: string, resourceType: string
} }
} }
let _client: Cosmos.CosmosClient;
export function client(): Cosmos.CosmosClient { export function client(): Cosmos.CosmosClient {
if (_client) return _client;
const options: Cosmos.CosmosClientOptions = { const options: Cosmos.CosmosClientOptions = {
endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called
key: userContext.masterKey, key: userContext.masterKey,
@@ -92,6 +89,5 @@ export function client(): Cosmos.CosmosClient {
if (configContext.PROXY_PATH !== undefined) { if (configContext.PROXY_PATH !== undefined) {
(options as any).plugins = [{ on: "request", plugin: requestPlugin }]; (options as any).plugins = [{ on: "request", plugin: requestPlugin }];
} }
_client = new Cosmos.CosmosClient(options); return new Cosmos.CosmosClient(options);
return _client;
} }

View File

@@ -1,61 +0,0 @@
import { DatePicker, TextField } from "office-ui-fabric-react";
import React, { FunctionComponent } from "react";
export interface TableEntityProps {
entityValueLabel?: string;
entityValuePlaceholder: string;
entityValue: string | Date;
isEntityTypeDate: boolean;
entityTimeValue: string;
entityValueType: string;
onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
onSelectDate: (date: Date | null | undefined) => void;
onEntityTimeValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
}
export const EntityValue: FunctionComponent<TableEntityProps> = ({
entityValueLabel,
entityValuePlaceholder,
entityValue,
isEntityTypeDate,
entityTimeValue,
entityValueType,
onEntityValueChange,
onSelectDate,
onEntityTimeValueChange,
}: TableEntityProps): JSX.Element => {
if (isEntityTypeDate) {
return (
<>
<DatePicker
className="addEntityDatePicker"
placeholder={entityValuePlaceholder}
value={entityValue && new Date(entityValue)}
ariaLabel={entityValuePlaceholder}
onSelectDate={onSelectDate}
/>
<TextField
label={entityValueLabel && entityValueLabel}
id="entityTimeId"
autoFocus
type="time"
value={entityTimeValue}
onChange={onEntityTimeValueChange}
/>
</>
);
}
return (
<TextField
label={entityValueLabel && entityValueLabel}
className="addEntityTextField"
id="entityValueId"
autoFocus
type={entityValueType}
placeholder={entityValuePlaceholder}
value={typeof entityValue === "string" && entityValue}
onChange={onEntityValueChange}
/>
);
};

View File

@@ -48,18 +48,32 @@ export function sendCachedDataMessage<TResponseDataModel>(
} }
export function sendMessage(data: any): void { export function sendMessage(data: any): void {
_sendMessage({ if (canSendMessage()) {
signature: "pcIframe", // We try to find data explorer window first, then fallback to current window
data: data, const portalChildWindow = getDataExplorerWindow(window) || window;
}); portalChildWindow.parent.postMessage(
{
signature: "pcIframe",
data: data,
},
portalChildWindow.document.referrer || "*"
);
}
} }
export function sendReadyMessage(): void { export function sendReadyMessage(): void {
_sendMessage({ if (canSendMessage()) {
signature: "pcIframe", // We try to find data explorer window first, then fallback to current window
kind: "ready", const portalChildWindow = getDataExplorerWindow(window) || window;
data: "ready", portalChildWindow.parent.postMessage(
}); {
signature: "pcIframe",
kind: "ready",
data: "ready",
},
portalChildWindow.document.referrer || "*"
);
}
} }
export function canSendMessage(): boolean { export function canSendMessage(): boolean {
@@ -75,17 +89,3 @@ export function runGarbageCollector() {
} }
}); });
} }
const _sendMessage = (message: any): void => {
if (canSendMessage()) {
// Portal window can receive messages from only child windows
const portalChildWindow = getDataExplorerWindow(window) || window;
if (portalChildWindow === window) {
// Current window is a child of portal, send message to portal window
portalChildWindow.parent.postMessage(message, portalChildWindow.document.referrer || "*");
} else {
// Current window is not a child of portal, send message to the child window instead (which is data explorer)
portalChildWindow.postMessage(message, portalChildWindow.location.origin || "*");
}
}
};

View File

@@ -5,10 +5,11 @@ import { configContext } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { Collection } from "../Contracts/ViewModels"; import { Collection } from "../Contracts/ViewModels";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import { userContext } from "../UserContext"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants"; import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
import { userContext } from "../UserContext";
import { MinimalQueryIterator } from "./IteratorUtilities"; import { MinimalQueryIterator } from "./IteratorUtilities";
import { sendMessage } from "./MessageHandler"; import { sendMessage } from "./MessageHandler";
@@ -347,7 +348,10 @@ export function getEndpoint(): string {
async function errorHandling(response: Response, action: string, params: unknown): Promise<void> { async function errorHandling(response: Response, action: string, params: unknown): Promise<void> {
const errorMessage = await response.text(); const errorMessage = await response.text();
// Log the error where the user can see it // Log the error where the user can see it
logConsoleError(`Error ${action}: ${errorMessage}, Payload: ${JSON.stringify(params)}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error ${action}: ${errorMessage}, Payload: ${JSON.stringify(params)}`
);
if (response.status === HttpStatusCodes.Forbidden) { if (response.status === HttpStatusCodes.Forbidden) {
sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage }); sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage });
return; return;

View File

@@ -1,136 +0,0 @@
import {
Dropdown,
IDropdownOption,
IDropdownStyles,
IImageProps,
Image,
IStackTokens,
Stack,
TextField,
TooltipHost,
} from "office-ui-fabric-react";
import React, { FunctionComponent } from "react";
import DeleteIcon from "../../images/delete.svg";
import EditIcon from "../../images/Edit_entity.svg";
import { CassandraType, TableType } from "../Explorer/Tables/Constants";
import { userContext } from "../UserContext";
import { EntityValue } from "./EntityValue";
const dropdownStyles: Partial<IDropdownStyles> = { dropdown: { width: 100 } };
export interface TableEntityProps {
entityTypeLabel?: string;
entityPropertyLabel?: string;
entityValueLabel?: string;
isDeleteOptionVisible: boolean;
entityProperty: string;
entityPropertyPlaceHolder: string;
selectedKey: string | number;
entityValuePlaceholder: string;
entityValue: string | Date;
isEntityTypeDate: boolean;
options: { key: string; text: string }[];
isPropertyTypeDisable: boolean;
entityTimeValue: string;
onDeleteEntity?: () => void;
onEditEntity?: () => void;
onEntityPropertyChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
onEntityTypeChange: (event: React.FormEvent<HTMLElement>, selectedParam: IDropdownOption) => void;
onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
onSelectDate: (date: Date | null | undefined) => void;
onEntityTimeValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
}
export const TableEntity: FunctionComponent<TableEntityProps> = ({
entityTypeLabel,
entityPropertyLabel,
isDeleteOptionVisible,
entityProperty,
selectedKey,
entityPropertyPlaceHolder,
entityValueLabel,
entityValuePlaceholder,
entityValue,
options,
isPropertyTypeDisable,
isEntityTypeDate,
entityTimeValue,
onEditEntity,
onDeleteEntity,
onEntityPropertyChange,
onEntityTypeChange,
onEntityValueChange,
onSelectDate,
onEntityTimeValueChange,
}: TableEntityProps): JSX.Element => {
const imageProps: IImageProps = {
width: 16,
height: 30,
className: entityPropertyLabel ? "addRemoveIconLabel" : "addRemoveIcon",
};
const sectionStackTokens: IStackTokens = { childrenGap: 12 };
const getEntityValueType = (): string => {
const { Int, Smallint, Tinyint } = CassandraType;
const { Double, Int32, Int64 } = TableType;
if (
selectedKey === Double ||
selectedKey === Int32 ||
selectedKey === Int64 ||
selectedKey === Int ||
selectedKey === Smallint ||
selectedKey === Tinyint
) {
return "number";
}
return "string";
};
return (
<>
<Stack horizontal tokens={sectionStackTokens}>
<TextField
label={entityPropertyLabel && entityPropertyLabel}
id="entityPropertyId"
autoFocus
disabled={isPropertyTypeDisable}
placeholder={entityPropertyPlaceHolder}
value={entityProperty}
onChange={onEntityPropertyChange}
required
/>
<Dropdown
label={entityTypeLabel && entityTypeLabel}
selectedKey={selectedKey}
onChange={onEntityTypeChange}
options={options}
disabled={isPropertyTypeDisable}
id="entityTypeId"
styles={dropdownStyles}
/>
<EntityValue
entityValueLabel={entityValueLabel}
entityValueType={getEntityValueType()}
entityValuePlaceholder={entityValuePlaceholder}
entityValue={entityValue}
isEntityTypeDate={isEntityTypeDate}
entityTimeValue={entityTimeValue}
onEntityValueChange={onEntityValueChange}
onSelectDate={onSelectDate}
onEntityTimeValueChange={onEntityTimeValueChange}
/>
<TooltipHost content="Edit property" id="editTooltip">
<Image {...imageProps} src={EditIcon} alt="editEntity" id="editEntity" onClick={onEditEntity} />
</TooltipHost>
{isDeleteOptionVisible && userContext.apiType !== "Cassandra" && (
<TooltipHost content="Delete property" id="deleteTooltip">
<Image {...imageProps} src={DeleteIcon} alt="delete entity" id="deleteEntity" onClick={onDeleteEntity} />
</TooltipHost>
)}
</Stack>
</>
);
};

View File

@@ -1,8 +1,8 @@
import { Image, Stack, TextField } from "office-ui-fabric-react"; import { Image, Stack, TextField } from "office-ui-fabric-react";
import React, { ChangeEvent, FunctionComponent, KeyboardEvent, useRef, useState } from "react"; import React, { ChangeEvent, FunctionComponent, KeyboardEvent, useRef, useState } from "react";
import FolderIcon from "../../../images/folder_16x16.svg"; import FolderIcon from "../../../images/folder_16x16.svg";
import * as Constants from "../Constants"; import * as Constants from "../../Common/Constants";
import { Tooltip } from "../Tooltip/Tooltip"; import { Tooltip } from "../Tooltip";
interface UploadProps { interface UploadProps {
label: string; label: string;

View File

@@ -1,39 +0,0 @@
import { JSONObject, OperationResponse } from "@azure/cosmos";
import { CollectionBase } from "../../Contracts/ViewModels";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export const bulkCreateDocument = async (
collection: CollectionBase,
documents: JSONObject[]
): Promise<OperationResponse[]> => {
const clearMessage = logConsoleProgress(
`Executing ${documents.length} bulk operations for container ${collection.id()}`
);
try {
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.items.bulk(
documents.map((doc) => ({ operationType: "Create", resourceBody: doc })),
{ continueOnError: true }
);
const successCount = response.filter((r) => r.statusCode === 201).length;
const throttledCount = response.filter((r) => r.statusCode === 429).length;
logConsoleInfo(
`${
documents.length
} operations completed for container ${collection.id()}. ${successCount} operations succeeded. ${throttledCount} operations throttled`
);
return response;
} catch (error) {
handleError(error, "BulkCreateDocument", `Error bulk creating items for container ${collection.id()}`);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -1,15 +1,15 @@
import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { userContext } from "../../UserContext";
import { listCassandraTables } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { listMongoDBCollections } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { listSqlContainers } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { listTables } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils"; import { handleError } from "../ErrorHandlingUtils";
import { listSqlContainers } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { listCassandraTables } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { listMongoDBCollections } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { listTables } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { userContext } from "../../UserContext";
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> { export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`); const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
@@ -17,6 +17,7 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations && !userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table userContext.defaultExperience !== DefaultAccountExperienceType.Table
) { ) {
return await readCollectionsWithARM(databaseId); return await readCollectionsWithARM(databaseId);

View File

@@ -1,37 +1,39 @@
import { ContainerDefinition } from "@azure/cosmos";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { Collection } from "../../Contracts/DataModels"; import { Collection } from "../../Contracts/DataModels";
import { ContainerDefinition } from "@azure/cosmos";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { userContext } from "../../UserContext"; import {
CreateUpdateOptions,
ExtendedResourceProperties,
MongoDBCollectionCreateUpdateParameters,
MongoDBCollectionResource,
SqlContainerCreateUpdateParameters,
SqlContainerResource,
} from "../../Utils/arm/generatedClients/2020-04-01/types";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { import {
createUpdateCassandraTable, createUpdateCassandraTable,
getCassandraTable, getCassandraTable,
} from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import {
createUpdateGremlinGraph,
getGremlinGraph,
} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { import {
createUpdateMongoDBCollection, createUpdateMongoDBCollection,
getMongoDBCollection, getMongoDBCollection,
} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { import {
ExtendedResourceProperties, createUpdateGremlinGraph,
MongoDBCollectionCreateUpdateParameters, getGremlinGraph,
SqlContainerCreateUpdateParameters, } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
SqlContainerResource, import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
} from "../../Utils/arm/generatedClients/2020-04-01/types";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils"; import { handleError } from "../ErrorHandlingUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { userContext } from "../../UserContext";
export async function updateCollection( export async function updateCollection(
databaseId: string, databaseId: string,
collectionId: string, collectionId: string,
newCollection: Partial<Collection>, newCollection: Collection,
options: RequestOptions = {} options: RequestOptions = {}
): Promise<Collection> { ): Promise<Collection> {
let collection: Collection; let collection: Collection;
@@ -41,6 +43,7 @@ export async function updateCollection(
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations && !userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table userContext.defaultExperience !== DefaultAccountExperienceType.Table
) { ) {
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection); collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
@@ -66,7 +69,7 @@ export async function updateCollection(
async function updateCollectionWithARM( async function updateCollectionWithARM(
databaseId: string, databaseId: string,
collectionId: string, collectionId: string,
newCollection: Partial<Collection> newCollection: Collection
): Promise<Collection> { ): Promise<Collection> {
const subscriptionId = userContext.subscriptionId; const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup; const resourceGroup = userContext.resourceGroup;
@@ -82,15 +85,6 @@ async function updateCollectionWithARM(
return updateGremlinGraph(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection); return updateGremlinGraph(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection);
case DefaultAccountExperienceType.Table: case DefaultAccountExperienceType.Table:
return updateTable(collectionId, subscriptionId, resourceGroup, accountName, newCollection); return updateTable(collectionId, subscriptionId, resourceGroup, accountName, newCollection);
case DefaultAccountExperienceType.MongoDB:
return updateMongoDBCollection(
databaseId,
collectionId,
subscriptionId,
resourceGroup,
accountName,
newCollection
);
default: default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`); throw new Error(`Unsupported default experience type: ${defaultExperience}`);
} }
@@ -102,7 +96,7 @@ async function updateSqlContainer(
subscriptionId: string, subscriptionId: string,
resourceGroup: string, resourceGroup: string,
accountName: string, accountName: string,
newCollection: Partial<Collection> newCollection: Collection
): Promise<Collection> { ): Promise<Collection> {
const getResponse = await getSqlContainer(subscriptionId, resourceGroup, accountName, databaseId, collectionId); const getResponse = await getSqlContainer(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) { if (getResponse && getResponse.properties && getResponse.properties.resource) {
@@ -121,26 +115,35 @@ async function updateSqlContainer(
throw new Error(`Sql container to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`); throw new Error(`Sql container to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`);
} }
export async function updateMongoDBCollection( export async function updateMongoDBCollectionThroughRP(
databaseId: string, databaseId: string,
collectionId: string, collectionId: string,
subscriptionId: string, newCollection: MongoDBCollectionResource,
resourceGroup: string, updateOptions?: CreateUpdateOptions
accountName: string, ): Promise<MongoDBCollectionResource> {
newCollection: Partial<Collection> const subscriptionId = userContext.subscriptionId;
): Promise<Collection> { const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const getResponse = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId); const getResponse = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) { if (getResponse && getResponse.properties && getResponse.properties.resource) {
getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties; const updateParams: MongoDBCollectionCreateUpdateParameters = {
properties: {
resource: newCollection,
options: updateOptions,
},
};
const updateResponse = await createUpdateMongoDBCollection( const updateResponse = await createUpdateMongoDBCollection(
subscriptionId, subscriptionId,
resourceGroup, resourceGroup,
accountName, accountName,
databaseId, databaseId,
collectionId, collectionId,
getResponse as MongoDBCollectionCreateUpdateParameters updateParams
); );
return updateResponse && (updateResponse.properties.resource as Collection);
return updateResponse && (updateResponse.properties.resource as MongoDBCollectionResource);
} }
throw new Error( throw new Error(
@@ -154,7 +157,7 @@ async function updateCassandraTable(
subscriptionId: string, subscriptionId: string,
resourceGroup: string, resourceGroup: string,
accountName: string, accountName: string,
newCollection: Partial<Collection> newCollection: Collection
): Promise<Collection> { ): Promise<Collection> {
const getResponse = await getCassandraTable(subscriptionId, resourceGroup, accountName, databaseId, collectionId); const getResponse = await getCassandraTable(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) { if (getResponse && getResponse.properties && getResponse.properties.resource) {
@@ -181,7 +184,7 @@ async function updateGremlinGraph(
subscriptionId: string, subscriptionId: string,
resourceGroup: string, resourceGroup: string,
accountName: string, accountName: string,
newCollection: Partial<Collection> newCollection: Collection
): Promise<Collection> { ): Promise<Collection> {
const getResponse = await getGremlinGraph(subscriptionId, resourceGroup, accountName, databaseId, collectionId); const getResponse = await getGremlinGraph(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) { if (getResponse && getResponse.properties && getResponse.properties.resource) {
@@ -205,7 +208,7 @@ async function updateTable(
subscriptionId: string, subscriptionId: string,
resourceGroup: string, resourceGroup: string,
accountName: string, accountName: string,
newCollection: Partial<Collection> newCollection: Collection
): Promise<Collection> { ): Promise<Collection> {
const getResponse = await getTable(subscriptionId, resourceGroup, accountName, collectionId); const getResponse = await getTable(subscriptionId, resourceGroup, accountName, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) { if (getResponse && getResponse.properties && getResponse.properties.resource) {

View File

@@ -26,7 +26,6 @@ export interface ConfigContext {
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it. GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
hostedExplorerURL: string; hostedExplorerURL: string;
armAPIVersion?: string; armAPIVersion?: string;
allowedJunoOrigins: string[];
} }
// Default configuration // Default configuration
@@ -54,13 +53,6 @@ let configContext: Readonly<ConfigContext> = {
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/settings/applications/1189306 GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/settings/applications/1189306
JUNO_ENDPOINT: "https://tools.cosmos.azure.com", JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
allowedJunoOrigins: [
"https://juno-test.documents-dev.windows-int.net",
"https://juno-test2.documents-dev.windows-int.net",
"https://tools.cosmos.azure.com",
"https://tools-staging.cosmos.azure.com",
"https://localhost",
],
}; };
export function resetConfigContext(): void { export function resetConfigContext(): void {
@@ -94,18 +86,13 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
}); });
if (response.status === 200) { if (response.status === 200) {
try { try {
const { allowedParentFrameOrigins, allowedJunoOrigins, ...externalConfig } = await response.json(); const { allowedParentFrameOrigins, ...externalConfig } = await response.json();
Object.assign(configContext, externalConfig); Object.assign(configContext, externalConfig);
if (allowedParentFrameOrigins && allowedParentFrameOrigins.length > 0) { if (allowedParentFrameOrigins && allowedParentFrameOrigins.length > 0) {
updateConfigContext({ updateConfigContext({
allowedParentFrameOrigins: [...configContext.allowedParentFrameOrigins, ...allowedParentFrameOrigins], allowedParentFrameOrigins: [...configContext.allowedParentFrameOrigins, ...allowedParentFrameOrigins],
}); });
} }
if (allowedJunoOrigins && allowedJunoOrigins.length > 0) {
updateConfigContext({
allowedJunoOrigins: [...configContext.allowedJunoOrigins, ...allowedJunoOrigins],
});
}
} catch (error) { } catch (error) {
console.error("Unable to parse json in config file"); console.error("Unable to parse json in config file");
console.error(error); console.error(error);

View File

@@ -4,7 +4,6 @@
export enum TabKind { export enum TabKind {
SQLDocuments, SQLDocuments,
MongoDocuments, MongoDocuments,
SchemaAnalyzer,
TableEntities, TableEntities,
Graph, Graph,
SQLQuery, SQLQuery,

View File

@@ -121,10 +121,6 @@ export interface ISchemaRequest {
} }
export interface Collection extends Resource { export interface Collection extends Resource {
// Only in Mongo collections loaded via ARM
shardKey?: {
[key: string]: string;
};
defaultTtl?: number; defaultTtl?: number;
indexingPolicy?: IndexingPolicy; indexingPolicy?: IndexingPolicy;
partitionKey?: PartitionKey; partitionKey?: PartitionKey;

View File

@@ -15,6 +15,7 @@ import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import Trigger from "../Explorer/Tree/Trigger"; import Trigger from "../Explorer/Tree/Trigger";
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction"; import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
import { SelfServeType } from "../SelfServe/SelfServeUtils"; import { SelfServeType } from "../SelfServe/SelfServeUtils";
import { UploadDetails } from "../workers/upload/definitions";
import * as DataModels from "./DataModels"; import * as DataModels from "./DataModels";
import { SubscriptionType } from "./SubscriptionType"; import { SubscriptionType } from "./SubscriptionType";
@@ -22,14 +23,6 @@ export interface TokenProvider {
getAuthHeader(): Promise<Headers>; getAuthHeader(): Promise<Headers>;
} }
export interface UploadDetailsRecord {
fileName: string;
numSucceeded: number;
numFailed: number;
numThrottled: number;
errors: string[];
}
export interface QueryResultsMetadata { export interface QueryResultsMetadata {
hasMoreResults: boolean; hasMoreResults: boolean;
firstItemIndex: number; firstItemIndex: number;
@@ -141,7 +134,6 @@ export interface Collection extends CollectionBase {
onTableEntitiesClick(): void; onTableEntitiesClick(): void;
onGraphDocumentsClick(): void; onGraphDocumentsClick(): void;
onMongoDBDocumentsClick(): void; onMongoDBDocumentsClick(): void;
onSchemaAnalyzerClick(): void;
openTab(): void; openTab(): void;
onSettingsClick: () => Promise<void>; onSettingsClick: () => Promise<void>;
@@ -182,7 +174,7 @@ export interface Collection extends CollectionBase {
onDragOver(source: Collection, event: { originalEvent: DragEvent }): void; onDragOver(source: Collection, event: { originalEvent: DragEvent }): void;
onDrop(source: Collection, event: { originalEvent: DragEvent }): void; onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>; uploadFiles(fileList: FileList): Promise<UploadDetails>;
getLabel(): string; getLabel(): string;
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>; getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
@@ -277,6 +269,7 @@ export interface TabOptions {
tabKind: CollectionTabKind; tabKind: CollectionTabKind;
title: string; title: string;
tabPath: string; tabPath: string;
isActive: ko.Observable<boolean>;
hashLocation: string; hashLocation: string;
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]) => void; onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]) => void;
isTabsContentExpanded?: ko.Observable<boolean>; isTabsContentExpanded?: ko.Observable<boolean>;
@@ -367,7 +360,6 @@ export enum CollectionTabKind {
Schema = 19, Schema = 19,
CollectionSettingsV2 = 20, CollectionSettingsV2 = 20,
DatabaseSettingsV2 = 21, DatabaseSettingsV2 = 21,
SchemaAnalyzer = 22,
} }
export enum TerminalKind { export enum TerminalKind {
@@ -398,9 +390,6 @@ export interface DataExplorerInputsFrame {
dataExplorerVersion?: string; dataExplorerVersion?: string;
defaultCollectionThroughput?: CollectionCreationDefaults; defaultCollectionThroughput?: CollectionCreationDefaults;
flights?: readonly string[]; flights?: readonly string[];
features?: {
[key: string]: string;
};
} }
export interface SelfServeFrameInputs { export interface SelfServeFrameInputs {

7
src/Definitions/worker.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
declare module "worker-loader!*" {
class WebpackWorker extends Worker {
constructor();
}
export default WebpackWorker;
}

View File

@@ -8,6 +8,10 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("input-typeahead")).toBe(true); expect(ko.components.isRegistered("input-typeahead")).toBe(true);
}); });
it("should register new-vertex-form component", () => {
expect(ko.components.isRegistered("new-vertex-form")).toBe(true);
});
it("should register error-display component", () => { it("should register error-display component", () => {
expect(ko.components.isRegistered("error-display")).toBe(true); expect(ko.components.isRegistered("error-display")).toBe(true);
}); });
@@ -69,6 +73,14 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("add-collection-pane")).toBe(true); expect(ko.components.isRegistered("add-collection-pane")).toBe(true);
}); });
it("should register delete-collection-confirmation-pane component", () => {
expect(ko.components.isRegistered("delete-collection-confirmation-pane")).toBe(true);
});
it("should register graph-new-vertex-pane component", () => {
expect(ko.components.isRegistered("graph-new-vertex-pane")).toBe(true);
});
it("should register graph-styling-pane component", () => { it("should register graph-styling-pane component", () => {
expect(ko.components.isRegistered("graph-styling-pane")).toBe(true); expect(ko.components.isRegistered("graph-styling-pane")).toBe(true);
}); });
@@ -77,6 +89,10 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("string-input-pane")).toBe(true); expect(ko.components.isRegistered("string-input-pane")).toBe(true);
}); });
it("should register setup-notebooks-pane component", () => {
expect(ko.components.isRegistered("setup-notebooks-pane")).toBe(true);
});
it("should register 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);
}); });

View File

@@ -7,6 +7,7 @@ import { InputTypeaheadComponent } from "./Controls/InputTypeahead/InputTypeahea
import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent"; import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent";
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3"; import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
import { GraphStyleComponent } from "./Graph/GraphStyleComponent/GraphStyleComponent"; import { GraphStyleComponent } from "./Graph/GraphStyleComponent/GraphStyleComponent";
import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponent";
import * as PaneComponents from "./Panes/PaneComponents"; import * as PaneComponents from "./Panes/PaneComponents";
import ConflictsTab from "./Tabs/ConflictsTab"; import ConflictsTab from "./Tabs/ConflictsTab";
import DocumentsTab from "./Tabs/DocumentsTab"; import DocumentsTab from "./Tabs/DocumentsTab";
@@ -17,14 +18,15 @@ import NotebookTabV2 from "./Tabs/NotebookV2Tab";
import NotebookViewerTab from "./Tabs/NotebookViewerTab"; import NotebookViewerTab from "./Tabs/NotebookViewerTab";
import QueryTab from "./Tabs/QueryTab"; import QueryTab from "./Tabs/QueryTab";
import QueryTablesTab from "./Tabs/QueryTablesTab"; import QueryTablesTab from "./Tabs/QueryTablesTab";
import SchemaAnalyzerTab from "./Tabs/SchemaAnalyzerTab";
import { DatabaseSettingsTabV2, SettingsTabV2 } from "./Tabs/SettingsTabV2"; import { DatabaseSettingsTabV2, SettingsTabV2 } from "./Tabs/SettingsTabV2";
import StoredProcedureTab from "./Tabs/StoredProcedureTab"; import StoredProcedureTab from "./Tabs/StoredProcedureTab";
import TabsManagerTemplate from "./Tabs/TabsManager.html";
import TerminalTab from "./Tabs/TerminalTab"; import TerminalTab from "./Tabs/TerminalTab";
import TriggerTab from "./Tabs/TriggerTab"; import TriggerTab from "./Tabs/TriggerTab";
import UserDefinedFunctionTab from "./Tabs/UserDefinedFunctionTab"; import UserDefinedFunctionTab from "./Tabs/UserDefinedFunctionTab";
ko.components.register("input-typeahead", new InputTypeaheadComponent()); ko.components.register("input-typeahead", new InputTypeaheadComponent());
ko.components.register("new-vertex-form", NewVertexComponent);
ko.components.register("error-display", new ErrorDisplayComponent()); ko.components.register("error-display", new ErrorDisplayComponent());
ko.components.register("graph-style", GraphStyleComponent); ko.components.register("graph-style", GraphStyleComponent);
ko.components.register("editor", new EditorComponent()); ko.components.register("editor", new EditorComponent());
@@ -32,6 +34,7 @@ ko.components.register("json-editor", new JsonEditorComponent());
ko.components.register("diff-editor", new DiffEditorComponent()); ko.components.register("diff-editor", new DiffEditorComponent());
ko.components.register("dynamic-list", DynamicListComponent); ko.components.register("dynamic-list", DynamicListComponent);
ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3); ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3);
ko.components.register("tabs-manager", { template: TabsManagerTemplate });
// Collection Tabs // Collection Tabs
[ [
@@ -50,15 +53,22 @@ ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponent
GalleryTab, GalleryTab,
NotebookViewerTab, NotebookViewerTab,
DatabaseSettingsTabV2, DatabaseSettingsTabV2,
SchemaAnalyzerTab,
].forEach(({ component: { name, template } }) => ko.components.register(name, { template })); ].forEach(({ component: { name, template } }) => ko.components.register(name, { template }));
// 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());
ko.components.register(
"delete-collection-confirmation-pane",
new PaneComponents.DeleteCollectionConfirmationPaneComponent()
);
ko.components.register("graph-new-vertex-pane", new PaneComponents.GraphNewVertexPaneComponent());
ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent()); ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent());
ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent()); ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent());
ko.components.register("table-edit-entity-pane", new PaneComponents.TableEditEntityPaneComponent()); ko.components.register("table-edit-entity-pane", new PaneComponents.TableEditEntityPaneComponent());
ko.components.register("table-query-select-pane", new PaneComponents.TableQuerySelectPaneComponent());
ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent()); ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent());
ko.components.register("string-input-pane", new PaneComponents.StringInputPaneComponent()); ko.components.register("string-input-pane", new PaneComponents.StringInputPaneComponent());
ko.components.register("setup-notebooks-pane", new PaneComponents.SetupNotebooksPaneComponent());
ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent()); ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent());

View File

@@ -55,7 +55,7 @@ export class ResourceTreeContextMenuButtonFactory {
selectedCollection: ViewModels.Collection selectedCollection: ViewModels.Collection
): TreeNodeMenuItem[] { ): TreeNodeMenuItem[] {
const items: TreeNodeMenuItem[] = []; const items: TreeNodeMenuItem[] = [];
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") { if (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph()) {
items.push({ items.push({
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null), onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null),
@@ -80,7 +80,7 @@ export class ResourceTreeContextMenuButtonFactory {
}); });
} }
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") { if (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph()) {
items.push({ items.push({
iconSrc: AddStoredProcedureIcon, iconSrc: AddStoredProcedureIcon,
onClick: () => { onClick: () => {
@@ -123,7 +123,7 @@ export class ResourceTreeContextMenuButtonFactory {
container: Explorer, container: Explorer,
storedProcedure: StoredProcedure storedProcedure: StoredProcedure
): TreeNodeMenuItem[] { ): TreeNodeMenuItem[] {
if (userContext.apiType === "Cassandra") { if (container.isPreferredApiCassandra()) {
return []; return [];
} }
@@ -137,7 +137,7 @@ export class ResourceTreeContextMenuButtonFactory {
} }
public static createTriggerContextMenuItems(container: Explorer, trigger: Trigger): TreeNodeMenuItem[] { public static createTriggerContextMenuItems(container: Explorer, trigger: Trigger): TreeNodeMenuItem[] {
if (userContext.apiType === "Cassandra") { if (container.isPreferredApiCassandra()) {
return []; return [];
} }
@@ -154,7 +154,7 @@ export class ResourceTreeContextMenuButtonFactory {
container: Explorer, container: Explorer,
userDefinedFunction: UserDefinedFunction userDefinedFunction: UserDefinedFunction
): TreeNodeMenuItem[] { ): TreeNodeMenuItem[] {
if (userContext.apiType === "Cassandra") { if (container.isPreferredApiCassandra()) {
return []; return [];
} }

View File

@@ -1,3 +1,8 @@
import * as React from "react";
import { Dialog as FluentDialog, DialogType, DialogFooter, IDialogProps } from "office-ui-fabric-react/lib/Dialog";
import { IButtonProps, PrimaryButton, DefaultButton } from "office-ui-fabric-react/lib/Button";
import { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField";
import { Link } from "office-ui-fabric-react/lib/Link";
import { import {
ChoiceGroup, ChoiceGroup,
FontIcon, FontIcon,
@@ -5,11 +10,6 @@ import {
IProgressIndicatorProps, IProgressIndicatorProps,
ProgressIndicator, ProgressIndicator,
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import { DefaultButton, IButtonProps, PrimaryButton } from "office-ui-fabric-react/lib/Button";
import { Dialog as FluentDialog, DialogFooter, DialogType, IDialogProps } from "office-ui-fabric-react/lib/Dialog";
import { Link } from "office-ui-fabric-react/lib/Link";
import { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField";
import React, { FunctionComponent } from "react";
export interface TextFieldProps extends ITextFieldProps { export interface TextFieldProps extends ITextFieldProps {
label: string; label: string;
@@ -50,69 +50,61 @@ const DIALOG_TITLE_FONT_SIZE = "17px";
const DIALOG_TITLE_FONT_WEIGHT = 400; const DIALOG_TITLE_FONT_WEIGHT = 400;
const DIALOG_SUBTEXT_FONT_SIZE = "15px"; const DIALOG_SUBTEXT_FONT_SIZE = "15px";
export const Dialog: FunctionComponent<DialogProps> = ({ export class Dialog extends React.Component<DialogProps> {
title, constructor(props: DialogProps) {
subText, super(props);
isModal, }
visible,
choiceGroupProps, public render(): JSX.Element {
textFieldProps, const dialogProps: IDialogProps = {
linkProps, hidden: !this.props.visible,
progressIndicatorProps, dialogContentProps: {
primaryButtonText, type: this.props.type || DialogType.normal,
secondaryButtonText, title: this.props.title,
onPrimaryButtonClick, subText: this.props.subText,
onSecondaryButtonClick, styles: {
primaryButtonDisabled, title: { fontSize: DIALOG_TITLE_FONT_SIZE, fontWeight: DIALOG_TITLE_FONT_WEIGHT },
type, subText: { fontSize: DIALOG_SUBTEXT_FONT_SIZE },
showCloseButton, },
onDismiss, showCloseButton: this.props.showCloseButton || false,
}: DialogProps) => { onDismiss: this.props.onDismiss,
const dialogProps: IDialogProps = {
hidden: !visible,
dialogContentProps: {
type: type || DialogType.normal,
title,
subText,
styles: {
title: { fontSize: DIALOG_TITLE_FONT_SIZE, fontWeight: DIALOG_TITLE_FONT_WEIGHT },
subText: { fontSize: DIALOG_SUBTEXT_FONT_SIZE },
}, },
showCloseButton: showCloseButton || false, modalProps: { isBlocking: this.props.isModal, isDarkOverlay: false },
onDismiss, minWidth: DIALOG_MIN_WIDTH,
}, maxWidth: DIALOG_MAX_WIDTH,
modalProps: { isBlocking: isModal, isDarkOverlay: false }, };
minWidth: DIALOG_MIN_WIDTH, const choiceGroupProps: IChoiceGroupProps = this.props.choiceGroupProps;
maxWidth: DIALOG_MAX_WIDTH, const textFieldProps: ITextFieldProps = this.props.textFieldProps;
}; const linkProps: LinkProps = this.props.linkProps;
const progressIndicatorProps: IProgressIndicatorProps = this.props.progressIndicatorProps;
const primaryButtonProps: IButtonProps = {
text: this.props.primaryButtonText,
disabled: this.props.primaryButtonDisabled || false,
onClick: this.props.onPrimaryButtonClick,
};
const secondaryButtonProps: IButtonProps =
this.props.secondaryButtonText && this.props.onSecondaryButtonClick
? {
text: this.props.secondaryButtonText,
onClick: this.props.onSecondaryButtonClick,
}
: undefined;
const primaryButtonProps: IButtonProps = { return (
text: primaryButtonText, <FluentDialog {...dialogProps}>
disabled: primaryButtonDisabled || false, {choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />}
onClick: onPrimaryButtonClick, {textFieldProps && <TextField {...textFieldProps} />}
}; {linkProps && (
const secondaryButtonProps: IButtonProps = <Link href={linkProps.linkUrl} target="_blank">
secondaryButtonText && onSecondaryButtonClick {linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
? { </Link>
text: secondaryButtonText, )}
onClick: onSecondaryButtonClick, {progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
} <DialogFooter>
: undefined; <PrimaryButton {...primaryButtonProps} />
{secondaryButtonProps && <DefaultButton {...secondaryButtonProps} />}
return ( </DialogFooter>
<FluentDialog {...dialogProps}> </FluentDialog>
{choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />} );
{textFieldProps && <TextField {...textFieldProps} />} }
{linkProps && ( }
<Link href={linkProps.linkUrl} target="_blank">
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
</Link>
)}
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
<DialogFooter>
<PrimaryButton {...primaryButtonProps} />
{secondaryButtonProps && <DefaultButton {...secondaryButtonProps} />}
</DialogFooter>
</FluentDialog>
);
};

View File

@@ -21,18 +21,18 @@ import {
Text, Text,
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import { HttpStatusCodes } from "../../../Common/Constants";
import { handleError } from "../../../Common/ErrorHandlingUtils";
import { IGalleryItem, IJunoResponse, IPublicGalleryData, JunoClient } from "../../../Juno/JunoClient"; import { IGalleryItem, IJunoResponse, IPublicGalleryData, JunoClient } from "../../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { trace } from "../../../Shared/Telemetry/TelemetryProcessor";
import * as GalleryUtils from "../../../Utils/GalleryUtils"; import * as GalleryUtils from "../../../Utils/GalleryUtils";
import Explorer from "../../Explorer";
import { Dialog, DialogProps } from "../Dialog"; import { Dialog, DialogProps } from "../Dialog";
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent"; import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
import { CodeOfConductComponent } from "./CodeOfConductComponent";
import "./GalleryViewerComponent.less"; import "./GalleryViewerComponent.less";
import { HttpStatusCodes } from "../../../Common/Constants";
import Explorer from "../../Explorer";
import { CodeOfConductComponent } from "./CodeOfConductComponent";
import { InfoComponent } from "./InfoComponent/InfoComponent"; import { InfoComponent } from "./InfoComponent/InfoComponent";
import { handleError } from "../../../Common/ErrorHandlingUtils";
import { trace } from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
export interface GalleryViewerComponentProps { export interface GalleryViewerComponentProps {
container?: Explorer; container?: Explorer;
@@ -138,11 +138,11 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
key: SortBy.MostRecent, key: SortBy.MostRecent,
text: GalleryViewerComponent.mostRecentText, text: GalleryViewerComponent.mostRecentText,
}, },
{
key: SortBy.MostFavorited,
text: GalleryViewerComponent.mostFavoritedText,
},
]; ];
this.sortingOptions.push({
key: SortBy.MostFavorited,
text: GalleryViewerComponent.mostFavoritedText,
});
this.loadTabContent(this.state.selectedTab, this.state.searchText, this.state.sortBy, false); this.loadTabContent(this.state.selectedTab, this.state.searchText, this.state.sortBy, false);
this.loadFavoriteNotebooks(this.state.searchText, this.state.sortBy, false); // Need this to show correct favorite button state this.loadFavoriteNotebooks(this.state.searchText, this.state.sortBy, false); // Need this to show correct favorite button state
@@ -654,8 +654,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
}; };
private onRenderCell = (data?: IGalleryItem): JSX.Element => { private onRenderCell = (data?: IGalleryItem): JSX.Element => {
const isFavorite = const isFavorite = this.favoriteNotebooks?.find((item) => item.id === data.id) !== undefined;
this.props.container && this.favoriteNotebooks?.find((item) => item.id === data.id) !== undefined;
const props: GalleryCardComponentProps = { const props: GalleryCardComponentProps = {
data, data,
isFavorite, isFavorite,

View File

@@ -1,11 +1,11 @@
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import ko from "knockout"; import ko from "knockout";
import React from "react"; import React from "react";
import { updateCollection } from "../../../Common/dataAccess/updateCollection"; import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
import { updateOffer } from "../../../Common/dataAccess/updateOffer"; import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { updateUserContext } from "../../../UserContext"; import { MongoDBCollectionResource } from "../../../Utils/arm/generatedClients/2020-04-01/types";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { CollectionSettingsTabV2 } from "../../Tabs/SettingsTabV2"; import { CollectionSettingsTabV2 } from "../../Tabs/SettingsTabV2";
import { SettingsComponent, SettingsComponentProps, SettingsComponentState } from "./SettingsComponent"; import { SettingsComponent, SettingsComponentProps, SettingsComponentState } from "./SettingsComponent";
@@ -23,8 +23,13 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
changeFeedPolicy: undefined, changeFeedPolicy: undefined,
analyticalStorageTtl: undefined, analyticalStorageTtl: undefined,
geospatialConfig: undefined, geospatialConfig: undefined,
} as DataModels.Collection),
updateMongoDBCollectionThroughRP: jest.fn().mockReturnValue({
id: undefined,
shardKey: undefined,
indexes: [], indexes: [],
}), analyticalStorageTtl: undefined,
} as MongoDBCollectionResource),
})); }));
jest.mock("../../../Common/dataAccess/updateOffer", () => ({ jest.mock("../../../Common/dataAccess/updateOffer", () => ({
updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer), updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer),
@@ -39,6 +44,7 @@ describe("SettingsComponent", () => {
tabPath: "", tabPath: "",
node: undefined, node: undefined,
hashLocation: "settings", hashLocation: "settings",
isActive: ko.observable(false),
onUpdateTabsButtons: undefined, onUpdateTabsButtons: undefined,
}), }),
}; };
@@ -107,13 +113,7 @@ describe("SettingsComponent", () => {
expect(settingsComponentInstance.shouldShowKeyspaceSharedThroughputMessage()).toEqual(false); expect(settingsComponentInstance.shouldShowKeyspaceSharedThroughputMessage()).toEqual(false);
const newContainer = new Explorer(); const newContainer = new Explorer();
updateUserContext({ newContainer.isPreferredApiCassandra = ko.computed(() => true);
databaseAccount: {
properties: {
capabilities: [{ name: "EnableCassandra" }],
},
} as DataModels.DatabaseAccount,
});
const newCollection = { ...collection }; const newCollection = { ...collection };
newCollection.container = newContainer; newCollection.container = newContainer;
@@ -193,6 +193,7 @@ describe("SettingsComponent", () => {
}; };
await settingsComponentInstance.onSaveClick(); await settingsComponentInstance.onSaveClick();
expect(updateCollection).toBeCalled(); expect(updateCollection).toBeCalled();
expect(updateMongoDBCollectionThroughRP).toBeCalled();
expect(updateOffer).toBeCalled(); expect(updateOffer).toBeCalled();
}); });

View File

@@ -6,7 +6,7 @@ import { AuthType } from "../../../AuthType";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress"; import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection"; import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
import { updateCollection } from "../../../Common/dataAccess/updateCollection"; import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
import { updateOffer } from "../../../Common/dataAccess/updateOffer"; import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
@@ -137,7 +137,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.offer = this.collection?.offer(); this.offer = this.collection?.offer();
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl(); this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
this.shouldShowIndexingPolicyEditor = this.shouldShowIndexingPolicyEditor =
this.container && userContext.apiType !== "Cassandra" && !this.container.isPreferredApiMongoDB(); this.container && !this.container.isPreferredApiCassandra() && !this.container.isPreferredApiMongoDB();
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy; this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
@@ -299,7 +299,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.state.wasAutopilotOriginallySet !== this.state.isAutoPilotSelected; this.state.wasAutopilotOriginallySet !== this.state.isAutoPilotSelected;
public shouldShowKeyspaceSharedThroughputMessage = (): boolean => public shouldShowKeyspaceSharedThroughputMessage = (): boolean =>
this.container && userContext.apiType === "Cassandra" && hasDatabaseSharedThroughput(this.collection); this.container && this.container.isPreferredApiCassandra() && hasDatabaseSharedThroughput(this.collection);
public hasConflictResolution = (): boolean => public hasConflictResolution = (): boolean =>
this.container?.databaseAccount && this.container?.databaseAccount &&
@@ -782,12 +782,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
if (this.state.isMongoIndexingPolicySaveable && this.mongoDBCollectionResource) { if (this.state.isMongoIndexingPolicySaveable && this.mongoDBCollectionResource) {
try { try {
const newMongoIndexes = this.getMongoIndexesToSave(); const newMongoIndexes = this.getMongoIndexesToSave();
const newMongoCollection = { const newMongoCollection: MongoDBCollectionResource = {
...this.mongoDBCollectionResource, ...this.mongoDBCollectionResource,
indexes: newMongoIndexes, indexes: newMongoIndexes,
}; };
this.mongoDBCollectionResource = await updateCollection( this.mongoDBCollectionResource = await updateMongoDBCollectionThroughRP(
this.collection.databaseId, this.collection.databaseId,
this.collection.id(), this.collection.id(),
newMongoCollection newMongoCollection

View File

@@ -1,13 +1,14 @@
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import React from "react"; import React from "react";
import { DatabaseAccount } from "../../../../Contracts/DataModels";
import { updateUserContext } from "../../../../UserContext";
import Explorer from "../../../Explorer";
import { ChangeFeedPolicyState, GeospatialConfigType, TtlOff, TtlOn, TtlOnNoDefault, TtlType } from "../SettingsUtils";
import { collection, container } from "../TestUtils";
import { SubSettingsComponent, SubSettingsComponentProps } from "./SubSettingsComponent"; import { SubSettingsComponent, SubSettingsComponentProps } from "./SubSettingsComponent";
import { container, collection } from "../TestUtils";
import { TtlType, GeospatialConfigType, ChangeFeedPolicyState, TtlOnNoDefault, TtlOn, TtlOff } from "../SettingsUtils";
import ko from "knockout";
import Explorer from "../../../Explorer";
describe("SubSettingsComponent", () => { describe("SubSettingsComponent", () => {
container.isPreferredApiDocumentDB = ko.computed(() => true);
const baseProps: SubSettingsComponentProps = { const baseProps: SubSettingsComponentProps = {
collection: collection, collection: collection,
container: container, container: container,
@@ -105,13 +106,8 @@ describe("SubSettingsComponent", () => {
it("partitionKey not visible", () => { it("partitionKey not visible", () => {
const newContainer = new Explorer(); const newContainer = new Explorer();
updateUserContext({
databaseAccount: { newContainer.isPreferredApiCassandra = ko.computed(() => true);
properties: {
capabilities: [{ name: "EnableCassandra" }],
},
} as DatabaseAccount,
});
const props = { ...baseProps, container: newContainer }; const props = { ...baseProps, container: newContainer };
const subSettingsComponent = new SubSettingsComponent(props); const subSettingsComponent = new SubSettingsComponent(props);
expect(subSettingsComponent.getPartitionKeyVisible()).toEqual(false); expect(subSettingsComponent.getPartitionKeyVisible()).toEqual(false);

View File

@@ -1,38 +1,28 @@
import {
ChoiceGroup,
IChoiceGroupOption,
Label,
Link,
MessageBar,
Stack,
Text,
TextField,
} from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels"; import * as ViewModels from "../../../../Contracts/ViewModels";
import { userContext } from "../../../../UserContext"; import {
GeospatialConfigType,
TtlType,
ChangeFeedPolicyState,
isDirty,
IsComponentDirtyResult,
TtlOn,
TtlOff,
TtlOnNoDefault,
getSanitizedInputValue,
} from "../SettingsUtils";
import Explorer from "../../../Explorer"; import Explorer from "../../../Explorer";
import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon"; import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
import { Label, Text, TextField, Stack, IChoiceGroupOption, ChoiceGroup, MessageBar } from "office-ui-fabric-react";
import { import {
changeFeedPolicyToolTip,
getChoiceGroupStyles,
getTextFieldStyles, getTextFieldStyles,
messageBarStyles, changeFeedPolicyToolTip,
subComponentStackProps, subComponentStackProps,
titleAndInputStackProps, titleAndInputStackProps,
getChoiceGroupStyles,
ttlWarning, ttlWarning,
messageBarStyles,
} from "../SettingsRenderUtils"; } from "../SettingsRenderUtils";
import {
ChangeFeedPolicyState,
GeospatialConfigType,
getSanitizedInputValue,
IsComponentDirtyResult,
isDirty,
TtlOff,
TtlOn,
TtlOnNoDefault,
TtlType,
} from "../SettingsUtils";
import { ToolTipLabelComponent } from "./ToolTipLabelComponent"; import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
export interface SubSettingsComponentProps { export interface SubSettingsComponentProps {
@@ -70,15 +60,17 @@ export interface SubSettingsComponentProps {
export class SubSettingsComponent extends React.Component<SubSettingsComponentProps> { export class SubSettingsComponent extends React.Component<SubSettingsComponentProps> {
private shouldCheckComponentIsDirty = true; private shouldCheckComponentIsDirty = true;
private ttlVisible: boolean;
private geospatialVisible: boolean; private geospatialVisible: boolean;
private partitionKeyValue: string; private partitionKeyValue: string;
private partitionKeyName: string; private partitionKeyName: string;
constructor(props: SubSettingsComponentProps) { constructor(props: SubSettingsComponentProps) {
super(props); super(props);
this.geospatialVisible = userContext.apiType === "SQL"; this.ttlVisible = (this.props.container && !this.props.container.isPreferredApiCassandra()) || false;
this.geospatialVisible = this.props.container.isPreferredApiDocumentDB();
this.partitionKeyValue = "/" + this.props.collection.partitionKeyProperty; this.partitionKeyValue = "/" + this.props.collection.partitionKeyProperty;
this.partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key"; this.partitionKeyName = this.props.container.isPreferredApiMongoDB() ? "Shard key" : "Partition key";
} }
componentDidMount(): void { componentDidMount(): void {
@@ -178,51 +170,39 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
): void => ): void =>
this.props.onChangeFeedPolicyChange(ChangeFeedPolicyState[option.key as keyof typeof ChangeFeedPolicyState]); this.props.onChangeFeedPolicyChange(ChangeFeedPolicyState[option.key as keyof typeof ChangeFeedPolicyState]);
private getTtlComponent = (): JSX.Element => private getTtlComponent = (): JSX.Element => (
userContext.apiType === "Mongo" ? ( <Stack {...titleAndInputStackProps}>
<MessageBar <ChoiceGroup
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }} id="timeToLive"
styles={{ text: { fontSize: 14 } }} label="Time to Live"
> selectedKey={this.props.timeToLive}
To enable time-to-live (TTL) for your collection/documents, options={this.ttlChoiceGroupOptions}
<Link href="https://docs.microsoft.com/en-us/azure/cosmos-db/mongodb-time-to-live" target="_blank"> onChange={this.onTtlChange}
create a TTL index styles={getChoiceGroupStyles(this.props.timeToLive, this.props.timeToLiveBaseline)}
</Link> />
. {isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && (
</MessageBar> <MessageBar
) : ( messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
<Stack {...titleAndInputStackProps}> styles={messageBarStyles}
<ChoiceGroup >
id="timeToLive" {ttlWarning}
label="Time to Live" </MessageBar>
selectedKey={this.props.timeToLive} )}
options={this.ttlChoiceGroupOptions} {this.props.timeToLive === TtlType.On && (
onChange={this.onTtlChange} <TextField
styles={getChoiceGroupStyles(this.props.timeToLive, this.props.timeToLiveBaseline)} id="timeToLiveSeconds"
styles={getTextFieldStyles(this.props.timeToLiveSeconds, this.props.timeToLiveSecondsBaseline)}
type="number"
required
min={1}
max={Int32.Max}
value={this.props.timeToLiveSeconds?.toString()}
onChange={this.onTimeToLiveSecondsChange}
suffix="second(s)"
/> />
{isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && ( )}
<MessageBar </Stack>
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }} );
styles={messageBarStyles}
>
{ttlWarning}
</MessageBar>
)}
{this.props.timeToLive === TtlType.On && (
<TextField
id="timeToLiveSeconds"
styles={getTextFieldStyles(this.props.timeToLiveSeconds, this.props.timeToLiveSecondsBaseline)}
type="number"
required
min={1}
max={Int32.Max}
value={this.props.timeToLiveSeconds?.toString()}
onChange={this.onTimeToLiveSecondsChange}
suffix="second(s)"
/>
)}
</Stack>
);
private analyticalTtlChoiceGroupOptions: IChoiceGroupOption[] = [ private analyticalTtlChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: TtlType.Off, text: "Off", disabled: true }, { key: TtlType.Off, text: "Off", disabled: true },
@@ -320,8 +300,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
public getPartitionKeyVisible = (): boolean => { public getPartitionKeyVisible = (): boolean => {
if ( if (
userContext.apiType === "Cassandra" || this.props.container.isPreferredApiCassandra() ||
userContext.apiType === "Tables" || this.props.container.isPreferredApiTable() ||
!this.props.collection.partitionKeyProperty || !this.props.collection.partitionKeyProperty ||
(this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey) (this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey)
) { ) {
@@ -335,7 +315,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
public render(): JSX.Element { public render(): JSX.Element {
return ( return (
<Stack {...subComponentStackProps}> <Stack {...subComponentStackProps}>
{userContext.apiType !== "Cassandra" && this.getTtlComponent()} {this.ttlVisible && this.getTtlComponent()}
{this.geospatialVisible && this.getGeoSpatialComponent()} {this.geospatialVisible && this.getGeoSpatialComponent()}

View File

@@ -4,7 +4,6 @@ jest.mock("../../Common/dataAccess/createDocument");
import * as ko from "knockout"; import * as ko from "knockout";
import Q from "q"; import Q from "q";
import { createDocument } from "../../Common/dataAccess/createDocument"; import { createDocument } from "../../Common/dataAccess/createDocument";
import { DatabaseAccount } from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { updateUserContext } from "../../UserContext"; import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
@@ -14,7 +13,11 @@ describe("ContainerSampleGenerator", () => {
const createExplorerStub = (database: ViewModels.Database): Explorer => { const createExplorerStub = (database: ViewModels.Database): Explorer => {
const explorerStub = {} as Explorer; const explorerStub = {} as Explorer;
explorerStub.databases = ko.observableArray<ViewModels.Database>([database]); explorerStub.databases = ko.observableArray<ViewModels.Database>([database]);
explorerStub.isPreferredApiGraph = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiMongoDB = ko.computed<boolean>(() => false); explorerStub.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiDocumentDB = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiTable = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiCassandra = ko.computed<boolean>(() => false);
explorerStub.canExceedMaximumValue = ko.computed<boolean>(() => false); explorerStub.canExceedMaximumValue = ko.computed<boolean>(() => false);
explorerStub.findDatabaseWithId = () => database; explorerStub.findDatabaseWithId = () => database;
explorerStub.refreshAllDatabases = () => Q.resolve(); explorerStub.refreshAllDatabases = () => Q.resolve();
@@ -28,7 +31,7 @@ describe("ContainerSampleGenerator", () => {
it("should insert documents for sql API account", async () => { it("should insert documents for sql API account", async () => {
const sampleCollectionId = "SampleCollection"; const sampleCollectionId = "SampleCollection";
const sampleDatabaseId = "SampleDB"; const sampleDatabaseId = "SampleDB";
updateUserContext({});
const sampleData = { const sampleData = {
databaseId: sampleDatabaseId, databaseId: sampleDatabaseId,
offerThroughput: 400, offerThroughput: 400,
@@ -63,7 +66,7 @@ describe("ContainerSampleGenerator", () => {
database.findCollectionWithId = () => collection; database.findCollectionWithId = () => collection;
const explorerStub = createExplorerStub(database); const explorerStub = createExplorerStub(database);
explorerStub.isPreferredApiDocumentDB = ko.computed<boolean>(() => true);
const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub); const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub);
generator.setData(sampleData); generator.setData(sampleData);
@@ -113,13 +116,7 @@ describe("ContainerSampleGenerator", () => {
collection.databaseId = database.id(); collection.databaseId = database.id();
const explorerStub = createExplorerStub(database); const explorerStub = createExplorerStub(database);
updateUserContext({ explorerStub.isPreferredApiGraph = ko.computed<boolean>(() => true);
databaseAccount: {
properties: {
capabilities: [{ name: "EnableGremlin" }],
},
} as DatabaseAccount,
});
const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub); const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub);
generator.setData(sampleData); generator.setData(sampleData);
@@ -128,45 +125,31 @@ describe("ContainerSampleGenerator", () => {
}); });
it("should not create any sample for Mongo API account", async () => { it("should not create any sample for Mongo API account", async () => {
const experience = "Sample generation not supported for this API Mongo"; const experience = "not supported api";
const explorerStub = createExplorerStub(undefined); const explorerStub = createExplorerStub(undefined);
updateUserContext({ explorerStub.isPreferredApiMongoDB = ko.computed<boolean>(() => true);
databaseAccount: { explorerStub.defaultExperience = ko.observable<string>(experience);
properties: {
capabilities: [{ name: "EnableMongo" }],
},
} as DatabaseAccount,
});
// Rejects with error that contains experience // Rejects with error that contains experience
expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience); expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
}); });
it("should not create any sample for Table API account", async () => { it("should not create any sample for Table API account", async () => {
const experience = "Sample generation not supported for this API Tables"; const experience = "not supported api";
const explorerStub = createExplorerStub(undefined); const explorerStub = createExplorerStub(undefined);
updateUserContext({ explorerStub.isPreferredApiTable = ko.computed<boolean>(() => true);
databaseAccount: { explorerStub.defaultExperience = ko.observable<string>(experience);
properties: {
capabilities: [{ name: "EnableTable" }],
},
} as DatabaseAccount,
});
// Rejects with error that contains experience // Rejects with error that contains experience
await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience); await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
}); });
it("should not create any sample for Cassandra API account", async () => { it("should not create any sample for Cassandra API account", async () => {
const experience = "Sample generation not supported for this API Cassandra"; const experience = "not supported api";
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableCassandra" }],
},
} as DatabaseAccount,
});
const explorerStub = createExplorerStub(undefined); const explorerStub = createExplorerStub(undefined);
explorerStub.isPreferredApiCassandra = ko.computed<boolean>(() => true);
explorerStub.defaultExperience = ko.observable<string>(experience);
// Rejects with error that contains experience // Rejects with error that contains experience
await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience); await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
}); });

View File

@@ -1,12 +1,12 @@
import { createCollection } from "../../Common/dataAccess/createCollection";
import { createDocument } from "../../Common/dataAccess/createDocument";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { userContext } from "../../UserContext";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import GraphTab from ".././Tabs/GraphTab"; import GraphTab from ".././Tabs/GraphTab";
import Explorer from "../Explorer";
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient"; import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { createCollection } from "../../Common/dataAccess/createCollection";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { userContext } from "../../UserContext";
interface SampleDataFile extends DataModels.CreateCollectionParams { interface SampleDataFile extends DataModels.CreateCollectionParams {
data: any[]; data: any[];
@@ -23,16 +23,16 @@ export class ContainerSampleGenerator {
public static async createSampleGeneratorAsync(container: Explorer): Promise<ContainerSampleGenerator> { public static async createSampleGeneratorAsync(container: Explorer): Promise<ContainerSampleGenerator> {
const generator = new ContainerSampleGenerator(container); const generator = new ContainerSampleGenerator(container);
let dataFileContent: any; let dataFileContent: any;
if (userContext.apiType === "Gremlin") { if (container.isPreferredApiGraph()) {
dataFileContent = await import( dataFileContent = await import(
/* webpackChunkName: "gremlinSampleJsonData" */ "../../../sampleData/gremlinSampleData.json" /* webpackChunkName: "gremlinSampleJsonData" */ "../../../sampleData/gremlinSampleData.json"
); );
} else if (userContext.apiType === "SQL") { } else if (container.isPreferredApiDocumentDB()) {
dataFileContent = await import( dataFileContent = await import(
/* webpackChunkName: "sqlSampleJsonData" */ "../../../sampleData/sqlSampleData.json" /* webpackChunkName: "sqlSampleJsonData" */ "../../../sampleData/sqlSampleData.json"
); );
} else { } else {
return Promise.reject(`Sample generation not supported for this API ${userContext.apiType}`); return Promise.reject(`Sample generation not supported for this API ${container.defaultExperience()}`);
} }
generator.setData(dataFileContent); generator.setData(dataFileContent);
@@ -73,7 +73,7 @@ export class ContainerSampleGenerator {
} }
const promises: Q.Promise<any>[] = []; const promises: Q.Promise<any>[] = [];
if (userContext.apiType === "Gremlin") { if (this.container.isPreferredApiGraph()) {
// For Gremlin, all queries are executed sequentially, because some queries might be dependent on other queries // For Gremlin, all queries are executed sequentially, because some queries might be dependent on other queries
// (e.g. adding edge requires vertices to be present) // (e.g. adding edge requires vertices to be present)
const queries: string[] = this.sampleDataFile.data; const queries: string[] = this.sampleDataFile.data;

View File

@@ -1,7 +1,7 @@
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { userContext } from "../../UserContext"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
export class DataSamplesUtil { export class DataSamplesUtil {
@@ -20,16 +20,18 @@ export class DataSamplesUtil {
if (this.hasContainer(databaseName, containerName, this.container.databases())) { if (this.hasContainer(databaseName, containerName, this.container.databases())) {
const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`; const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`;
this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg); this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
logConsoleError(msg); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
return; return;
} }
await generator await generator
.createSampleContainerAsync() .createSampleContainerAsync()
.catch((error) => logConsoleError(`Error creating sample container: ${error}`)); .catch((error) =>
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Error creating sample container: ${error}`)
);
const msg = `The sample ${containerName} in database ${databaseName} has been successfully created.`; const msg = `The sample ${containerName} in database ${databaseName} has been successfully created.`;
this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg); this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
logConsoleInfo(msg); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
} }
/** /**
@@ -54,6 +56,6 @@ export class DataSamplesUtil {
} }
public isSampleContainerCreationSupported(): boolean { public isSampleContainerCreationSupported(): boolean {
return userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; return this.container.isPreferredApiDocumentDB() || this.container.isPreferredApiGraph();
} }
} }

View File

@@ -36,43 +36,40 @@ import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationU
import { stringToBlob } from "../Utils/BlobUtils"; import { stringToBlob } from "../Utils/BlobUtils";
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
import * as PricingUtils from "../Utils/PricingUtils";
import * as ComponentRegisterer from "./ComponentRegisterer"; import * as ComponentRegisterer from "./ComponentRegisterer";
import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker"; import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
import { DialogProps, TextFieldProps } from "./Controls/Dialog"; import { DialogProps, TextFieldProps } from "./Controls/Dialog";
import { GalleryTab } from "./Controls/NotebookGallery/GalleryViewerComponent"; import { GalleryTab } from "./Controls/NotebookGallery/GalleryViewerComponent";
import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter"; import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter";
import { ConsoleData } from "./Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent";
import * as FileSystemUtil from "./Notebook/FileSystemUtil"; import * as FileSystemUtil from "./Notebook/FileSystemUtil";
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
import { NotebookUtil } from "./Notebook/NotebookUtil"; import { NotebookUtil } from "./Notebook/NotebookUtil";
import AddCollectionPane from "./Panes/AddCollectionPane"; import AddCollectionPane from "./Panes/AddCollectionPane";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel"; import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import AddDatabasePane from "./Panes/AddDatabasePane"; import AddDatabasePane from "./Panes/AddDatabasePane";
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane/BrowseQueriesPane"; import { BrowseQueriesPanel } from "./Panes/BrowseQueriesPanel";
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane"; import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
import { ContextualPaneBase } from "./Panes/ContextualPaneBase"; import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane"; import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
import { DeleteCollectionConfirmationPanel } from "./Panes/DeleteCollectionConfirmationPanel";
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel"; import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane"; import { ExecuteSprocParamsPanel } from "./Panes/ExecuteSprocParamsPanel";
import GraphStylingPane from "./Panes/GraphStylingPane"; import GraphStylingPane from "./Panes/GraphStylingPane";
import { LoadQueryPane } from "./Panes/LoadQueryPane/LoadQueryPane"; import { LoadQueryPanel } from "./Panes/LoadQueryPanel";
import { SaveQueryPane } from "./Panes/SaveQueryPane/SaveQueryPane"; import NewVertexPane from "./Panes/NewVertexPane";
import { SettingsPane } from "./Panes/SettingsPane/SettingsPane"; import { SaveQueryPanel } from "./Panes/SaveQueryPanel";
import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksPanel"; import { SettingsPane } from "./Panes/SettingsPane";
import { SetupNotebooksPane } from "./Panes/SetupNotebooksPane";
import { StringInputPane } from "./Panes/StringInputPane"; import { StringInputPane } from "./Panes/StringInputPane";
import { AddTableEntityPanel } from "./Panes/Tables/AddTableEntityPanel"; import AddTableEntityPane from "./Panes/Tables/AddTableEntityPane";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane"; import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel"; import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane";
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane"; import { UploadFilePane } from "./Panes/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane"; import { UploadItemsPane } from "./Panes/UploadItemsPane";
import TableListViewModal from "./Tables/DataTable/TableEntityListViewModel";
import QueryViewModel from "./Tables/QueryBuilder/QueryViewModel";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import QueryTablesTab from "./Tabs/QueryTablesTab";
import TabsBase from "./Tabs/TabsBase"; import TabsBase from "./Tabs/TabsBase";
import { TabsManager } from "./Tabs/TabsManager"; import { TabsManager } from "./Tabs/TabsManager";
import TerminalTab from "./Tabs/TerminalTab"; import TerminalTab from "./Tabs/TerminalTab";
@@ -81,6 +78,8 @@ import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter"; import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter";
import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken"; import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken";
import StoredProcedure from "./Tree/StoredProcedure"; import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger";
import UserDefinedFunction from "./Tree/UserDefinedFunction";
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
@@ -118,11 +117,31 @@ export default class Explorer {
* Use userContext.apiType instead * Use userContext.apiType instead
* */ * */
public defaultExperience: ko.Observable<string>; public defaultExperience: ko.Observable<string>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "SQL"
* */
public isPreferredApiDocumentDB: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Cassandra"
* */
public isPreferredApiCassandra: ko.Computed<boolean>;
/** /**
* @deprecated * @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Mongo" * Compare a string with userContext.apiType instead: userContext.apiType === "Mongo"
* */ * */
public isPreferredApiMongoDB: ko.Computed<boolean>; public isPreferredApiMongoDB: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Gremlin"
* */
public isPreferredApiGraph: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Tables"
* */
public isPreferredApiTable: ko.Computed<boolean>;
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>; public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
/** /**
* @deprecated * @deprecated
@@ -172,16 +191,23 @@ export default class Explorer {
// Contextual panes // Contextual panes
public addDatabasePane: AddDatabasePane; public addDatabasePane: AddDatabasePane;
public addCollectionPane: AddCollectionPane; public addCollectionPane: AddCollectionPane;
public deleteCollectionConfirmationPane: DeleteCollectionConfirmationPane;
public graphStylingPane: GraphStylingPane; public graphStylingPane: GraphStylingPane;
public addTableEntityPane: AddTableEntityPane;
public editTableEntityPane: EditTableEntityPane; public editTableEntityPane: EditTableEntityPane;
public querySelectPane: QuerySelectPane;
public newVertexPane: NewVertexPane;
public cassandraAddCollectionPane: CassandraAddCollectionPane; public cassandraAddCollectionPane: CassandraAddCollectionPane;
public stringInputPane: StringInputPane; public stringInputPane: StringInputPane;
public setupNotebooksPane: SetupNotebooksPane;
public gitHubReposPane: ContextualPaneBase; public gitHubReposPane: ContextualPaneBase;
public publishNotebookPaneAdapter: ReactAdapter; public publishNotebookPaneAdapter: ReactAdapter;
public copyNotebookPaneAdapter: ReactAdapter;
// features // features
public isGitHubPaneEnabled: ko.Observable<boolean>; public isGitHubPaneEnabled: ko.Observable<boolean>;
public isPublishNotebookPaneEnabled: ko.Observable<boolean>; public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
public isHostedDataExplorerEnabled: ko.Computed<boolean>; public isHostedDataExplorerEnabled: ko.Computed<boolean>;
public isRightPanelV2Enabled: ko.Computed<boolean>; public isRightPanelV2Enabled: ko.Computed<boolean>;
public isMongoIndexingEnabled: ko.Observable<boolean>; public isMongoIndexingEnabled: ko.Observable<boolean>;
@@ -274,11 +300,8 @@ export default class Explorer {
); );
Promise.all([this._refreshNotebooksEnabledStateForAccount(), this._refreshSparkEnabledStateForAccount()]).then( Promise.all([this._refreshNotebooksEnabledStateForAccount(), this._refreshSparkEnabledStateForAccount()]).then(
async () => { async () => {
this.isNotebookEnabled( this.isNotebookEnabled(false);
userContext.authType !== AuthType.ResourceToken &&
((await this._containsDefaultNotebookWorkspace(this.databaseAccount())) ||
userContext.features.enableNotebooks)
);
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, { TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
isNotebookEnabled: this.isNotebookEnabled(), isNotebookEnabled: this.isNotebookEnabled(),
dataExplorerArea: Constants.Areas.Notebook, dataExplorerArea: Constants.Areas.Notebook,
@@ -331,6 +354,7 @@ export default class Explorer {
this.isGitHubPaneEnabled = ko.observable<boolean>(false); this.isGitHubPaneEnabled = ko.observable<boolean>(false);
this.isMongoIndexingEnabled = ko.observable<boolean>(false); this.isMongoIndexingEnabled = ko.observable<boolean>(false);
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false); this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
this.canExceedMaximumValue = ko.computed<boolean>(() => userContext.features.canExceedMaximumValue); this.canExceedMaximumValue = ko.computed<boolean>(() => userContext.features.canExceedMaximumValue);
@@ -394,6 +418,25 @@ export default class Explorer {
}); });
}); });
this.isPreferredApiDocumentDB = ko.computed(() => {
const defaultExperience = (this.defaultExperience && this.defaultExperience()) || "";
return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.DocumentDB.toLowerCase();
});
this.isPreferredApiCassandra = ko.computed(() => {
const defaultExperience = (this.defaultExperience && this.defaultExperience()) || "";
return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Cassandra.toLowerCase();
});
this.isPreferredApiGraph = ko.computed(() => {
const defaultExperience = (this.defaultExperience && this.defaultExperience()) || "";
return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Graph.toLowerCase();
});
this.isPreferredApiTable = ko.computed(() => {
const defaultExperience = (this.defaultExperience && this.defaultExperience()) || "";
return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Table.toLowerCase();
});
this.isFixedCollectionWithSharedThroughputSupported = ko.computed(() => { this.isFixedCollectionWithSharedThroughputSupported = ko.computed(() => {
if (userContext.features.enableFixedCollectionWithSharedThroughput) { if (userContext.features.enableFixedCollectionWithSharedThroughput) {
return true; return true;
@@ -452,9 +495,7 @@ export default class Explorer {
this.isHostedDataExplorerEnabled = ko.computed<boolean>( this.isHostedDataExplorerEnabled = ko.computed<boolean>(
() => () =>
configContext.platform === Platform.Portal && configContext.platform === Platform.Portal && !this.isRunningOnNationalCloud() && !this.isPreferredApiGraph()
!this.isRunningOnNationalCloud() &&
userContext.apiType !== "Gremlin"
); );
this.isRightPanelV2Enabled = ko.computed<boolean>(() => userContext.features.enableRightPanelV2); this.isRightPanelV2Enabled = ko.computed<boolean>(() => userContext.features.enableRightPanelV2);
this.selectedDatabaseId = ko.computed<string>(() => { this.selectedDatabaseId = ko.computed<string>(() => {
@@ -486,13 +527,20 @@ export default class Explorer {
}); });
this.addCollectionPane = new AddCollectionPane({ this.addCollectionPane = new AddCollectionPane({
isPreferredApiTable: ko.computed(() => userContext.apiType === "Tables"), isPreferredApiTable: ko.computed(() => this.isPreferredApiTable()),
id: "addcollectionpane", id: "addcollectionpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
container: this, container: this,
}); });
this.deleteCollectionConfirmationPane = new DeleteCollectionConfirmationPane({
id: "deletecollectionconfirmationpane",
visible: ko.observable<boolean>(false),
container: this,
});
this.graphStylingPane = new GraphStylingPane({ this.graphStylingPane = new GraphStylingPane({
id: "graphstylingpane", id: "graphstylingpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -500,6 +548,13 @@ export default class Explorer {
container: this, container: this,
}); });
this.addTableEntityPane = new AddTableEntityPane({
id: "addtableentitypane",
visible: ko.observable<boolean>(false),
container: this,
});
this.editTableEntityPane = new EditTableEntityPane({ this.editTableEntityPane = new EditTableEntityPane({
id: "edittableentitypane", id: "edittableentitypane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -507,6 +562,20 @@ export default class Explorer {
container: this, container: this,
}); });
this.querySelectPane = new QuerySelectPane({
id: "queryselectpane",
visible: ko.observable<boolean>(false),
container: this,
});
this.newVertexPane = new NewVertexPane({
id: "newvertexpane",
visible: ko.observable<boolean>(false),
container: this,
});
this.cassandraAddCollectionPane = new CassandraAddCollectionPane({ this.cassandraAddCollectionPane = new CassandraAddCollectionPane({
id: "cassandraaddcollectionpane", id: "cassandraaddcollectionpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -521,21 +590,27 @@ export default class Explorer {
container: this, container: this,
}); });
this.tabsManager = params?.tabsManager ?? new TabsManager(); this.setupNotebooksPane = new SetupNotebooksPane({
this.tabsManager.openedTabs.subscribe((tabs) => { id: "setupnotebookspane",
if (tabs.length === 0) { visible: ko.observable<boolean>(false),
this.selectedNode(undefined);
this.onUpdateTabsButtons([]); container: this,
}
}); });
this.tabsManager = params?.tabsManager ?? new TabsManager();
this._panes = [ this._panes = [
this.addDatabasePane, this.addDatabasePane,
this.addCollectionPane, this.addCollectionPane,
this.deleteCollectionConfirmationPane,
this.graphStylingPane, this.graphStylingPane,
this.addTableEntityPane,
this.editTableEntityPane, this.editTableEntityPane,
this.querySelectPane,
this.newVertexPane,
this.cassandraAddCollectionPane, this.cassandraAddCollectionPane,
this.stringInputPane, this.stringInputPane,
this.setupNotebooksPane,
]; ];
this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText)); this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText));
this.isTabsContentExpanded = ko.observable(false); this.isTabsContentExpanded = ko.observable(false);
@@ -565,6 +640,8 @@ export default class Explorer {
this.addCollectionPane.collectionWithThroughputInSharedTitle( this.addCollectionPane.collectionWithThroughputInSharedTitle(
"Provision dedicated throughput for this container" "Provision dedicated throughput for this container"
); );
this.deleteCollectionConfirmationPane.title("Delete Container");
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the container id");
this.refreshTreeTitle("Refresh containers"); this.refreshTreeTitle("Refresh containers");
break; break;
case "Mongo": case "Mongo":
@@ -591,6 +668,8 @@ export default class Explorer {
this.addCollectionPane.title("Add Graph"); this.addCollectionPane.title("Add Graph");
this.addCollectionPane.collectionIdTitle("Graph id"); this.addCollectionPane.collectionIdTitle("Graph id");
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this graph"); this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this graph");
this.deleteCollectionConfirmationPane.title("Delete Graph");
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the graph id");
this.refreshTreeTitle("Refresh graphs"); this.refreshTreeTitle("Refresh graphs");
break; break;
case "Tables": case "Tables":
@@ -604,7 +683,10 @@ export default class Explorer {
this.addCollectionPane.collectionIdTitle("Table id"); this.addCollectionPane.collectionIdTitle("Table id");
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table"); this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table");
this.refreshTreeTitle("Refresh tables"); this.refreshTreeTitle("Refresh tables");
this.addTableEntityPane.title("Add Table Entity");
this.editTableEntityPane.title("Edit Table Entity"); this.editTableEntityPane.title("Edit Table Entity");
this.deleteCollectionConfirmationPane.title("Delete Table");
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id");
this.tableDataClient = new TablesAPIDataClient(); this.tableDataClient = new TablesAPIDataClient();
break; break;
case "Cassandra": case "Cassandra":
@@ -618,7 +700,10 @@ export default class Explorer {
this.addCollectionPane.collectionIdTitle("Table id"); this.addCollectionPane.collectionIdTitle("Table id");
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table"); this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table");
this.refreshTreeTitle("Refresh tables"); this.refreshTreeTitle("Refresh tables");
this.addTableEntityPane.title("Add Table Row");
this.editTableEntityPane.title("Edit Table Row"); this.editTableEntityPane.title("Edit Table Row");
this.deleteCollectionConfirmationPane.title("Delete Table");
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id");
this.tableDataClient = new CassandraAPIDataClient(); this.tableDataClient = new CassandraAPIDataClient();
break; break;
} }
@@ -713,7 +798,8 @@ export default class Explorer {
onPrimaryButtonClick: async () => { onPrimaryButtonClick: async () => {
const startTime = TelemetryProcessor.traceStart(Action.EnableAzureSynapseLink); const startTime = TelemetryProcessor.traceStart(Action.EnableAzureSynapseLink);
const clearInProgressMessage = logConsoleProgress( const logId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
"Enabling Azure Synapse Link for this account. This may take a few minutes before you can enable analytical store for this account." "Enabling Azure Synapse Link for this account. This may take a few minutes before you can enable analytical store for this account."
); );
this.isSynapseLinkUpdating(true); this.isSynapseLinkUpdating(true);
@@ -731,13 +817,19 @@ export default class Explorer {
}, },
} }
); );
clearInProgressMessage(); NotificationConsoleUtils.clearInProgressMessageWithId(logId);
logConsoleInfo("Enabled Azure Synapse Link for this account"); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
"Enabled Azure Synapse Link for this account"
);
TelemetryProcessor.traceSuccess(Action.EnableAzureSynapseLink, {}, startTime); TelemetryProcessor.traceSuccess(Action.EnableAzureSynapseLink, {}, startTime);
this.databaseAccount(databaseAccount); this.databaseAccount(databaseAccount);
} catch (error) { } catch (error) {
clearInProgressMessage(); NotificationConsoleUtils.clearInProgressMessageWithId(logId);
logConsoleError(`Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`
);
TelemetryProcessor.traceFailure(Action.EnableAzureSynapseLink, {}, startTime); TelemetryProcessor.traceFailure(Action.EnableAzureSynapseLink, {}, startTime);
} finally { } finally {
this.isSynapseLinkUpdating(false); this.isSynapseLinkUpdating(false);
@@ -864,7 +956,10 @@ export default class Explorer {
}, },
startKey startKey
); );
logConsoleError(`Error while refreshing databases: ${errorMessage}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while refreshing databases: ${errorMessage}`
);
} }
); );
@@ -925,7 +1020,7 @@ export default class Explorer {
// Facade // Facade
public provideFeedbackEmail = () => { public provideFeedbackEmail = () => {
window.open(Constants.Urls.feedbackEmail, "_blank"); window.open(Constants.Urls.feedbackEmail, "_self");
}; };
public async getArcadiaToken(): Promise<string> { public async getArcadiaToken(): Promise<string> {
@@ -1085,20 +1180,20 @@ export default class Explorer {
private _resetNotebookWorkspace = async () => { private _resetNotebookWorkspace = async () => {
this._closeModalDialog(); this._closeModalDialog();
const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace"); const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Resetting notebook workspace");
try { try {
await this.notebookManager?.notebookClient.resetWorkspace(); await this.notebookManager?.notebookClient.resetWorkspace();
logConsoleInfo("Successfully reset notebook workspace"); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully reset notebook workspace");
TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace); TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace);
} catch (error) { } catch (error) {
logConsoleError(`Failed to reset notebook workspace: ${error}`); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to reset notebook workspace: ${error}`);
TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, { TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, {
error: getErrorMessage(error), error: getErrorMessage(error),
errorStack: getErrorStack(error), errorStack: getErrorStack(error),
}); });
throw error; throw error;
} finally { } finally {
clearInProgressMessage(); NotificationConsoleUtils.clearInProgressMessageWithId(id);
} }
}; };
@@ -1196,6 +1291,49 @@ export default class Explorer {
: this.selectedNode().collection) as ViewModels.Collection; : this.selectedNode().collection) as ViewModels.Collection;
} }
// TODO: Refactor below methods, minimize dependencies and add unit tests where necessary
public findSelectedStoredProcedure(): StoredProcedure {
const selectedCollection: ViewModels.Collection = this.findSelectedCollection();
return _.find(selectedCollection.storedProcedures(), (storedProcedure: StoredProcedure) => {
const openedSprocTab = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.StoredProcedures,
(tab) => tab.node && tab.node.rid === storedProcedure.rid
);
return (
storedProcedure.rid === this.selectedNode().rid ||
(!!openedSprocTab && openedSprocTab.length > 0 && openedSprocTab[0].isActive())
);
});
}
public findSelectedUDF(): UserDefinedFunction {
const selectedCollection: ViewModels.Collection = this.findSelectedCollection();
return _.find(selectedCollection.userDefinedFunctions(), (userDefinedFunction: UserDefinedFunction) => {
const openedUdfTab = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.UserDefinedFunctions,
(tab) => tab.node && tab.node.rid === userDefinedFunction.rid
);
return (
userDefinedFunction.rid === this.selectedNode().rid ||
(!!openedUdfTab && openedUdfTab.length > 0 && openedUdfTab[0].isActive())
);
});
}
public findSelectedTrigger(): Trigger {
const selectedCollection: ViewModels.Collection = this.findSelectedCollection();
return _.find(selectedCollection.triggers(), (trigger: Trigger) => {
const openedTriggerTab = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.Triggers,
(tab) => tab.node && tab.node.rid === trigger.rid
);
return (
trigger.rid === this.selectedNode().rid ||
(!!openedTriggerTab && openedTriggerTab.length > 0 && openedTriggerTab[0].isActive())
);
});
}
public closeAllPanes(): void { public closeAllPanes(): void {
this._panes.forEach((pane: ContextualPaneBase) => pane.close()); this._panes.forEach((pane: ContextualPaneBase) => pane.close());
} }
@@ -1433,7 +1571,11 @@ export default class Explorer {
} }
public copyNotebook(name: string, content: string): void { public copyNotebook(name: string, content: string): void {
this.notebookManager?.openCopyNotebookPane(name, content); if (this.notebookManager) {
this.notebookManager.openCopyNotebookPane(name, content);
this.copyNotebookPaneAdapter = this.notebookManager.copyNotebookPaneAdapter;
this.isCopyNotebookPaneEnabled(true);
}
} }
public showOkModalDialog(title: string, msg: string): void { public showOkModalDialog(title: string, msg: string): void {
@@ -1520,6 +1662,7 @@ export default class Explorer {
collection: null, collection: null,
masterKey: userContext.masterKey || "", masterKey: userContext.masterKey || "",
hashLocation: "notebooks", hashLocation: "notebooks",
isActive: ko.observable(false),
isTabsContentExpanded: ko.observable(true), isTabsContentExpanded: ko.observable(true),
onLoadStartKey: null, onLoadStartKey: null,
onUpdateTabsButtons: this.onUpdateTabsButtons, onUpdateTabsButtons: this.onUpdateTabsButtons,
@@ -1651,7 +1794,11 @@ export default class Explorer {
clearMessage(); clearMessage();
}, },
(error: any) => { (error: any) => {
logConsoleError(`Could not download notebook ${getErrorMessage(error)}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Could not download notebook ${getErrorMessage(error)}`
);
clearMessage(); clearMessage();
} }
); );
@@ -1659,11 +1806,7 @@ export default class Explorer {
private async _refreshNotebooksEnabledStateForAccount(): Promise<void> { private async _refreshNotebooksEnabledStateForAccount(): Promise<void> {
const authType = userContext.authType; const authType = userContext.authType;
if ( if (true) {
authType === AuthType.EncryptedToken ||
authType === AuthType.ResourceToken ||
authType === AuthType.MasterKey
) {
this.isNotebooksEnabledForAccount(false); this.isNotebooksEnabledForAccount(false);
return; return;
} }
@@ -1803,8 +1946,15 @@ export default class Explorer {
} }
return this.notebookManager?.notebookContentClient.deleteContentItem(item).then( return this.notebookManager?.notebookContentClient.deleteContentItem(item).then(
() => logConsoleInfo(`Successfully deleted: ${item.path}`), () => {
(reason: any) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`) NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully deleted: ${item.path}`);
},
(reason: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to delete "${item.path}": ${JSON.stringify(reason)}`
);
}
); );
} }
@@ -1820,7 +1970,11 @@ export default class Explorer {
parent = parent || this.resourceTree.myNotebooksContentRoot; parent = parent || this.resourceTree.myNotebooksContentRoot;
const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`); const notificationProgressId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Creating new notebook in ${parent.path}`
);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, { const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, {
dataExplorerArea: Constants.Areas.Notebook, dataExplorerArea: Constants.Areas.Notebook,
}); });
@@ -1828,7 +1982,7 @@ export default class Explorer {
this.notebookManager?.notebookContentClient this.notebookManager?.notebookContentClient
.createNewNotebookFile(parent) .createNewNotebookFile(parent)
.then((newFile: NotebookContentItem) => { .then((newFile: NotebookContentItem) => {
logConsoleInfo(`Successfully created: ${newFile.name}`); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully created: ${newFile.name}`);
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.CreateNewNotebook, Action.CreateNewNotebook,
{ {
@@ -1841,7 +1995,7 @@ export default class Explorer {
.then(() => this.resourceTree.triggerRender()) .then(() => this.resourceTree.triggerRender())
.catch((error: any) => { .catch((error: any) => {
const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`; const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`;
logConsoleError(errorMessage); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, errorMessage);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.CreateNewNotebook, Action.CreateNewNotebook,
{ {
@@ -1852,7 +2006,7 @@ export default class Explorer {
startKey startKey
); );
}) })
.finally(clearInProgressMessage); .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(notificationProgressId));
} }
public refreshContentItem(item: NotebookContentItem): Promise<void> { public refreshContentItem(item: NotebookContentItem): Promise<void> {
@@ -1910,6 +2064,7 @@ export default class Explorer {
tabPath: title, tabPath: title,
collection: null, collection: null,
hashLocation: hashLocation, hashLocation: hashLocation,
isActive: ko.observable(false),
isTabsContentExpanded: ko.observable(true), isTabsContentExpanded: ko.observable(true),
onLoadStartKey: null, onLoadStartKey: null,
onUpdateTabsButtons: this.onUpdateTabsButtons, onUpdateTabsButtons: this.onUpdateTabsButtons,
@@ -2014,7 +2169,7 @@ export default class Explorer {
} }
public onNewCollectionClicked(): void { public onNewCollectionClicked(): void {
if (userContext.apiType === "Cassandra") { if (this.isPreferredApiCassandra()) {
this.cassandraAddCollectionPane.open(); this.cassandraAddCollectionPane.open();
} else if (userContext.features.enableReactPane) { } else if (userContext.features.enableReactPane) {
this.openAddCollectionPanel(); this.openAddCollectionPanel();
@@ -2062,7 +2217,7 @@ export default class Explorer {
const description = const description =
"You have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account."; "You have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account.";
this.openSetupNotebooksPanel(title, description); this.setupNotebooksPane.openWithTitleAndDescription(title, description);
} }
public async handleOpenFileAction(path: string): Promise<void> { public async handleOpenFileAction(path: string): Promise<void> {
@@ -2124,15 +2279,16 @@ export default class Explorer {
} }
public openDeleteCollectionConfirmationPane(): void { public openDeleteCollectionConfirmationPane(): void {
let collectionName = PricingUtils.getCollectionName(userContext.defaultExperience); userContext.features.enableKOPanel
this.openSidePanel( ? this.deleteCollectionConfirmationPane.open()
"Delete " + collectionName, : this.openSidePanel(
<DeleteCollectionConfirmationPane "Delete Collection",
explorer={this} <DeleteCollectionConfirmationPanel
collectionName={collectionName} explorer={this}
closePanel={this.closeSidePanel} closePanel={() => this.closeSidePanel()}
/> openNotificationConsole={() => this.expandConsole()}
); />
);
} }
public openDeleteDatabaseConfirmationPane(): void { public openDeleteDatabaseConfirmationPane(): void {
@@ -2155,14 +2311,10 @@ export default class Explorer {
this.openSidePanel("Settings", <SettingsPane explorer={this} closePanel={this.closeSidePanel} />); this.openSidePanel("Settings", <SettingsPane explorer={this} closePanel={this.closeSidePanel} />);
} }
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void { public openExecuteSprocParamsPanel(): void {
this.openSidePanel( this.openSidePanel(
"Input parameters", "Input parameters",
<ExecuteSprocParamsPane <ExecuteSprocParamsPanel explorer={this} closePanel={() => this.closeSidePanel()} />
explorer={this}
storedProcedure={storedProcedure}
closePanel={() => this.closeSidePanel()}
/>
); );
} }
@@ -2179,15 +2331,15 @@ export default class Explorer {
} }
public openBrowseQueriesPanel(): void { public openBrowseQueriesPanel(): void {
this.openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={this} closePanel={this.closeSidePanel} />); this.openSidePanel("Open Saved Queries", <BrowseQueriesPanel explorer={this} closePanel={this.closeSidePanel} />);
} }
public openLoadQueryPanel(): void { public openLoadQueryPanel(): void {
this.openSidePanel("Load Query", <LoadQueryPane explorer={this} closePanel={() => this.closeSidePanel()} />); this.openSidePanel("Load Query", <LoadQueryPanel explorer={this} closePanel={() => this.closeSidePanel()} />);
} }
public openSaveQueryPanel(): void { public openSaveQueryPanel(): void {
this.openSidePanel("Save Query", <SaveQueryPane explorer={this} closePanel={() => this.closeSidePanel()} />); this.openSidePanel("Save Query", <SaveQueryPanel explorer={this} closePanel={() => this.closeSidePanel()} />);
} }
public openUploadFilePanel(parent?: NotebookContentItem): void { public openUploadFilePanel(parent?: NotebookContentItem): void {
@@ -2201,36 +2353,4 @@ export default class Explorer {
/> />
); );
} }
public openAddTableEntityPanel(queryTablesTab: QueryTablesTab, tableEntityListViewModel: TableListViewModal): void {
this.openSidePanel(
"Add Table Entity",
<AddTableEntityPanel
explorer={this}
closePanel={this.closeSidePanel}
queryTablesTab={queryTablesTab}
tableEntityListViewModel={tableEntityListViewModel}
cassandraApiClient={new CassandraAPIDataClient()}
/>
);
}
public openSetupNotebooksPanel(title: string, description: string): void {
this.openSidePanel(
title,
<SetupNoteBooksPanel
explorer={this}
closePanel={this.closeSidePanel}
openNotificationConsole={() => this.expandConsole()}
panelTitle={title}
panelDescription={description}
/>
);
}
public openTableSelectQueryPanel(queryViewModal: QueryViewModel): void {
this.openSidePanel(
"Select Column",
<TableQuerySelectPanel explorer={this} closePanel={this.closeSidePanel} queryViewModel={queryViewModal} />
);
}
} }

View File

@@ -1,22 +1,24 @@
import { BaseType } from "d3"; import * as ko from "knockout";
import { map as d3Map } from "d3-collection";
import { D3DragEvent, drag } from "d3-drag";
import { forceCollide, forceLink, forceManyBody, forceSimulation } from "d3-force";
import { interpolate, interpolateNumber } from "d3-interpolate";
import { scaleOrdinal } from "d3-scale";
import { schemeCategory10 } from "d3-scale-chromatic";
import { select, selectAll } from "d3-selection";
import { zoom, zoomIdentity } from "d3-zoom";
import * as ko from "knockout";
import Q from "q"; import Q from "q";
import { schemeCategory10 } from "d3-scale-chromatic";
import { selectAll, select } from "d3-selection";
import { zoom, zoomIdentity } from "d3-zoom";
import { scaleOrdinal } from "d3-scale";
import { forceSimulation, forceLink, forceCollide, forceManyBody } from "d3-force";
import { interpolateNumber, interpolate } from "d3-interpolate";
import { map as d3Map } from "d3-collection";
import { drag, D3DragEvent } from "d3-drag";
import _ from "underscore"; import _ from "underscore";
import * as Constants from "../../../Common/Constants";
import { HashMap } from "../../../Common/HashMap";
import { NeighborType } from "../../../Contracts/ViewModels"; import { NeighborType } from "../../../Contracts/ViewModels";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils"; import { GraphData, D3Node, D3Link } from "./GraphData";
import { HashMap } from "../../../Common/HashMap";
import { BaseType } from "d3";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { GraphConfig } from "../../Tabs/GraphTab"; import { GraphConfig } from "../../Tabs/GraphTab";
import { D3Link, D3Node, GraphData } from "./GraphData";
import { GraphExplorer } from "./GraphExplorer"; import { GraphExplorer } from "./GraphExplorer";
import * as Constants from "../../../Common/Constants";
export interface D3GraphIconMap { export interface D3GraphIconMap {
[key: string]: { data: string; format: string }; [key: string]: { data: string; format: string };
@@ -1003,7 +1005,7 @@ export class D3ForceGraph implements GraphRenderer {
*/ */
private loadNeighbors(v: D3Node, pageAction: PAGE_ACTION) { private loadNeighbors(v: D3Node, pageAction: PAGE_ACTION) {
if (!this.graphDataWrapper.hasVertexId(v.id)) { if (!this.graphDataWrapper.hasVertexId(v.id)) {
logConsoleError(`Clicked node not in graph data. id: ${v.id}`); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Clicked node not in graph data. id: ${v.id}`);
return; return;
} }

View File

@@ -1,36 +1,37 @@
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import * as Q from "q"; import * as Q from "q";
import * as React from "react"; import * as React from "react";
import * as LeftPane from "./LeftPaneComponent";
import { MiddlePaneComponent } from "./MiddlePaneComponent";
import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent";
import * as NodeProperties from "./NodePropertiesComponent";
import * as D3ForceGraph from "./D3ForceGraph";
import { GraphVizComponentProps } from "./GraphVizComponent";
import * as GraphData from "./GraphData";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import * as GraphUtil from "./GraphUtil";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import * as GremlinClient from "./GremlinClient";
import * as StorageUtility from "../../../Shared/StorageUtility";
import { ArraysByKeyCache } from "./ArraysByKeyCache";
import { EdgeInfoCache } from "./EdgeInfoCache";
import * as TabComponent from "../../Controls/Tabs/TabComponent";
import { LocalStorageUtility, StorageKey } from "../../../Shared/StorageUtility";
import { QueryContainerComponent } from "./QueryContainerComponent";
import { GraphConfig } from "../../Tabs/GraphTab";
import { EditorReact } from "../../Controls/Editor/EditorReact";
import LoadGraphIcon from "../../../../images/LoadGraph.png"; import LoadGraphIcon from "../../../../images/LoadGraph.png";
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif"; import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import { InputProperty } from "../../../Contracts/ViewModels";
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
import { queryDocuments } from "../../../Common/dataAccess/queryDocuments"; import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage"; import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
import * as DataModels from "../../../Contracts/DataModels"; import { FeedOptions } from "@azure/cosmos";
import * as ViewModels from "../../../Contracts/ViewModels";
import { InputProperty } from "../../../Contracts/ViewModels";
import * as StorageUtility from "../../../Shared/StorageUtility";
import { LocalStorageUtility, StorageKey } from "../../../Shared/StorageUtility";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
import { EditorReact } from "../../Controls/Editor/EditorReact";
import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent";
import * as TabComponent from "../../Controls/Tabs/TabComponent";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { GraphConfig } from "../../Tabs/GraphTab";
import { ArraysByKeyCache } from "./ArraysByKeyCache";
import * as D3ForceGraph from "./D3ForceGraph";
import { EdgeInfoCache } from "./EdgeInfoCache";
import * as GraphData from "./GraphData";
import * as GraphUtil from "./GraphUtil";
import { GraphVizComponentProps } from "./GraphVizComponent";
import * as GremlinClient from "./GremlinClient";
import * as LeftPane from "./LeftPaneComponent";
import { MiddlePaneComponent } from "./MiddlePaneComponent";
import * as NodeProperties from "./NodePropertiesComponent";
import { QueryContainerComponent } from "./QueryContainerComponent";
export interface GraphAccessor { export interface GraphAccessor {
applyFilter: () => void; applyFilter: () => void;
@@ -696,13 +697,13 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* @param cmd * @param cmd
*/ */
public submitToBackend(cmd: string): Q.Promise<GremlinClient.GremlinRequestResult> { public submitToBackend(cmd: string): Q.Promise<GremlinClient.GremlinRequestResult> {
const clearConsoleProgress = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${cmd}`); const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${cmd}`);
this.setExecuteCounter(this.executeCounter + 1); this.setExecuteCounter(this.executeCounter + 1);
return this.gremlinClient.execute(cmd).then( return this.gremlinClient.execute(cmd).then(
(result: GremlinClient.GremlinRequestResult) => { (result: GremlinClient.GremlinRequestResult) => {
this.setExecuteCounter(this.executeCounter - 1); this.setExecuteCounter(this.executeCounter - 1);
clearConsoleProgress(); GraphExplorer.clearConsoleProgress(id);
if (result.isIncomplete) { if (result.isIncomplete) {
const msg = `The query results are too large and only partial results are displayed for: ${cmd}`; const msg = `The query results are too large and only partial results are displayed for: ${cmd}`;
GraphExplorer.reportToConsole(ConsoleDataType.Error, msg); GraphExplorer.reportToConsole(ConsoleDataType.Error, msg);
@@ -717,7 +718,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
(err: string) => { (err: string) => {
this.setExecuteCounter(this.executeCounter - 1); this.setExecuteCounter(this.executeCounter - 1);
GraphExplorer.reportToConsole(ConsoleDataType.Error, `Gremlin query failed: ${cmd}`, err); GraphExplorer.reportToConsole(ConsoleDataType.Error, `Gremlin query failed: ${cmd}`, err);
clearConsoleProgress(); GraphExplorer.clearConsoleProgress(id);
throw err; throw err;
} }
); );
@@ -1082,26 +1083,13 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* @param errorData additional errors * @param errorData additional errors
* @return id * @return id
*/ */
public static reportToConsole(type: ConsoleDataType.InProgress, msg: string, ...errorData: any[]): () => void; public static reportToConsole(type: ConsoleDataType, msg: string, ...errorData: any[]): string {
public static reportToConsole(type: ConsoleDataType.Info, msg: string, ...errorData: any[]): void;
public static reportToConsole(type: ConsoleDataType.Error, msg: string, ...errorData: any[]): void;
public static reportToConsole(type: ConsoleDataType, msg: string, ...errorData: any[]): void | (() => void) {
let errorDataStr: string = ""; let errorDataStr: string = "";
if (errorData && errorData.length > 0) { if (errorData && errorData.length > 0) {
console.error(msg, errorData); console.error(msg, errorData);
errorDataStr = ": " + JSON.stringify(errorData); errorDataStr = ": " + JSON.stringify(errorData);
} }
return NotificationConsoleUtils.logConsoleMessage(type, `${msg}${errorDataStr}`);
const consoleMessage = `${msg}${errorDataStr}`;
switch (type) {
case ConsoleDataType.Error:
return logConsoleError(consoleMessage);
case ConsoleDataType.Info:
return logConsoleInfo(consoleMessage);
case ConsoleDataType.InProgress:
return logConsoleProgress(consoleMessage);
}
} }
private setNodePropertiesViewMode(viewMode: NodeProperties.Mode) { private setNodePropertiesViewMode(viewMode: NodeProperties.Mode) {
@@ -1380,7 +1368,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
let { id } = d; let { id } = d;
if (typeof id !== "string") { if (typeof id !== "string") {
const error = `Vertex id is not a string: ${JSON.stringify(id)}.`; const error = `Vertex id is not a string: ${JSON.stringify(id)}.`;
logConsoleError(error); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
throw new Error(error); throw new Error(error);
} }
@@ -1392,7 +1380,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
pk = pk[0]["_value"]; pk = pk[0]["_value"];
} else { } else {
const error = `Vertex pk is not a string nor a non-empty array: ${JSON.stringify(pk)}.`; const error = `Vertex pk is not a string nor a non-empty array: ${JSON.stringify(pk)}.`;
logConsoleError(error); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
throw new Error(error); throw new Error(error);
} }
} }
@@ -1779,10 +1767,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
const queryInfoStr = `${this.currentDocDBQueryInfo.query} (${this.currentDocDBQueryInfo.index + 1}-${ const queryInfoStr = `${this.currentDocDBQueryInfo.query} (${this.currentDocDBQueryInfo.index + 1}-${
this.currentDocDBQueryInfo.index + GraphExplorer.ROOT_LIST_PAGE_SIZE this.currentDocDBQueryInfo.index + GraphExplorer.ROOT_LIST_PAGE_SIZE
})`; })`;
const clearConsoleProgress = GraphExplorer.reportToConsole( const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${queryInfoStr}`);
ConsoleDataType.InProgress,
`Executing: ${queryInfoStr}`
);
try { try {
const results: ViewModels.QueryResults = await queryDocumentsPage( const results: ViewModels.QueryResults = await queryDocumentsPage(
@@ -1791,7 +1776,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
this.currentDocDBQueryInfo.index this.currentDocDBQueryInfo.index
); );
clearConsoleProgress(); GraphExplorer.clearConsoleProgress(id);
this.currentDocDBQueryInfo.index = results.lastItemIndex + 1; this.currentDocDBQueryInfo.index = results.lastItemIndex + 1;
this.setState({ hasMoreRoots: results.hasMoreResults }); this.setState({ hasMoreRoots: results.hasMoreResults });
RU = results.requestCharge.toString(); RU = results.requestCharge.toString();
@@ -1808,7 +1793,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return { requestCharge: RU }; return { requestCharge: RU };
} catch (error) { } catch (error) {
clearConsoleProgress(); GraphExplorer.clearConsoleProgress(id);
const errorMsg = `Failed to query: ${this.currentDocDBQueryInfo.query}. Reason:${getErrorMessage(error)}`; const errorMsg = `Failed to query: ${this.currentDocDBQueryInfo.query}. Reason:${getErrorMessage(error)}`;
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg); GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg);
this.setState({ this.setState({
@@ -2018,4 +2003,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
</React.Fragment> </React.Fragment>
); );
} }
private static clearConsoleProgress(id: string) {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
}
} }

View File

@@ -1,6 +1,6 @@
import * as ViewModels from "../../../Contracts/ViewModels";
import * as GraphData from "./GraphData";
import { NeighborVertexBasicInfo } from "./GraphExplorer"; import { NeighborVertexBasicInfo } from "./GraphExplorer";
import * as GraphData from "./GraphData";
import * as ViewModels from "../../../Contracts/ViewModels";
interface JoinArrayMaxCharOutput { interface JoinArrayMaxCharOutput {
result: string; // string output result: string; // string output
@@ -13,9 +13,9 @@ interface EdgePropertyType {
inV?: string; inV?: string;
} }
export const getNeighborTitle = (neighbor: NeighborVertexBasicInfo): string => { export function getNeighborTitle(neighbor: NeighborVertexBasicInfo): string {
return `edge id: ${neighbor.edgeId}, vertex id: ${neighbor.id}`; return `edge id: ${neighbor.edgeId}, vertex id: ${neighbor.id}`;
}; }
/** /**
* Collect all edges from this node * Collect all edges from this node
@@ -23,11 +23,11 @@ export const getNeighborTitle = (neighbor: NeighborVertexBasicInfo): string => {
* @param graphData * @param graphData
* @param newNodes (optional) object describing new nodes encountered * @param newNodes (optional) object describing new nodes encountered
*/ */
export const createEdgesfromNode = ( export function createEdgesfromNode(
vertex: GraphData.GremlinVertex, vertex: GraphData.GremlinVertex,
graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>, graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>,
newNodes?: { [id: string]: boolean } newNodes?: { [id: string]: boolean }
): void => { ): void {
if (Object.prototype.hasOwnProperty.call(vertex, "outE")) { if (Object.prototype.hasOwnProperty.call(vertex, "outE")) {
const outE = vertex.outE; const outE = vertex.outE;
for (const label in outE) { for (const label in outE) {
@@ -66,7 +66,7 @@ export const createEdgesfromNode = (
}); });
} }
} }
}; }
/** /**
* From ['id1', 'id2', 'idn'] build the following string "'id1','id2','idn'". * From ['id1', 'id2', 'idn'] build the following string "'id1','id2','idn'".
@@ -75,7 +75,7 @@ export const createEdgesfromNode = (
* @param maxSize * @param maxSize
* @return * @return
*/ */
export const getLimitedArrayString = (array: string[], maxSize: number): JoinArrayMaxCharOutput => { export function getLimitedArrayString(array: string[], maxSize: number): JoinArrayMaxCharOutput {
if (!array || array.length === 0 || array[0].length + 2 > maxSize) { if (!array || array.length === 0 || array[0].length + 2 > maxSize) {
return { result: "", consumedCount: 0 }; return { result: "", consumedCount: 0 };
} }
@@ -96,16 +96,16 @@ export const getLimitedArrayString = (array: string[], maxSize: number): JoinArr
result: output, result: output,
consumedCount: i + 1, consumedCount: i + 1,
}; };
}; }
export const createFetchEdgePairQuery = ( export function createFetchEdgePairQuery(
outE: boolean, outE: boolean,
pkid: string, pkid: string,
excludedEdgeIds: string[], excludedEdgeIds: string[],
startIndex: number, startIndex: number,
pageSize: number, pageSize: number,
withoutStepArgMaxLenght: number withoutStepArgMaxLenght: number
): string => { ): string {
let gremlinQuery: string; let gremlinQuery: string;
if (excludedEdgeIds.length > 0) { if (excludedEdgeIds.length > 0) {
// build a string up to max char // build a string up to max char
@@ -128,15 +128,15 @@ export const createFetchEdgePairQuery = (
}().as('v').select('e', 'v')`; }().as('v').select('e', 'v')`;
} }
return gremlinQuery; return gremlinQuery;
}; }
/** /**
* Trim graph * Trim graph
*/ */
export const trimGraph = ( export function trimGraph(
currentRoot: GraphData.GremlinVertex, currentRoot: GraphData.GremlinVertex,
graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge> graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>
): void => { ) {
const importantNodes = [currentRoot.id].concat(currentRoot._ancestorsId); const importantNodes = [currentRoot.id].concat(currentRoot._ancestorsId);
graphData.unloadAllVertices(importantNodes); graphData.unloadAllVertices(importantNodes);
@@ -144,32 +144,32 @@ export const trimGraph = (
$.each(graphData.ids, (index: number, id: string) => { $.each(graphData.ids, (index: number, id: string) => {
graphData.getVertexById(id)._isFixedPosition = importantNodes.indexOf(id) !== -1; graphData.getVertexById(id)._isFixedPosition = importantNodes.indexOf(id) !== -1;
}); });
}; }
export const addRootChildToGraph = ( export function addRootChildToGraph(
root: GraphData.GremlinVertex, root: GraphData.GremlinVertex,
child: GraphData.GremlinVertex, child: GraphData.GremlinVertex,
graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge> graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>
): void => { ) {
child._ancestorsId = (root._ancestorsId || []).concat([root.id]); child._ancestorsId = (root._ancestorsId || []).concat([root.id]);
graphData.addVertex(child); graphData.addVertex(child);
createEdgesfromNode(child, graphData); createEdgesfromNode(child, graphData);
graphData.addNeighborInfo(child); graphData.addNeighborInfo(child);
}; }
/** /**
* TODO Perform minimal substitution to prevent breaking gremlin query and allow \"" for now. * TODO Perform minimal substitution to prevent breaking gremlin query and allow \"" for now.
* @param value * @param value
*/ */
export const escapeDoubleQuotes = (value: string): string => { export function escapeDoubleQuotes(value: string): string {
return value === undefined ? value : value.replace(/"/g, '\\"'); return value === undefined ? value : value.replace(/"/g, '\\"');
}; }
/** /**
* Surround with double-quotes if val is a string. * Surround with double-quotes if val is a string.
* @param val * @param val
*/ */
export const getQuotedPropValue = (ip: ViewModels.InputPropertyValue): string => { export function getQuotedPropValue(ip: ViewModels.InputPropertyValue): string {
switch (ip.type) { switch (ip.type) {
case "number": case "number":
case "boolean": case "boolean":
@@ -179,12 +179,12 @@ export const getQuotedPropValue = (ip: ViewModels.InputPropertyValue): string =>
default: default:
return `"${escapeDoubleQuotes(ip.value as string)}"`; return `"${escapeDoubleQuotes(ip.value as string)}"`;
} }
}; }
/** /**
* TODO Perform minimal substitution to prevent breaking gremlin query and allow \' for now. * TODO Perform minimal substitution to prevent breaking gremlin query and allow \' for now.
* @param value * @param value
*/ */
export const escapeSingleQuotes = (value: string): string => { export function escapeSingleQuotes(value: string): string {
return value === undefined ? value : value.replace(/'/g, "\\'"); return value === undefined ? value : value.replace(/'/g, "\\'");
}; }

View File

@@ -3,10 +3,11 @@
*/ */
import * as Q from "q"; import * as Q from "q";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
import { HashMap } from "../../../Common/HashMap";
import { logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
import { GremlinSimpleClient, Result } from "./GremlinSimpleClient"; import { GremlinSimpleClient, Result } from "./GremlinSimpleClient";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { HashMap } from "../../../Common/HashMap";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
export interface GremlinClientParameters { export interface GremlinClientParameters {
endpoint: string; endpoint: string;
@@ -76,7 +77,9 @@ export class GremlinClient {
this.abortPendingRequest(requestId, errorMessage, result.requestCharge); this.abortPendingRequest(requestId, errorMessage, result.requestCharge);
} }
}, },
infoCallback: logConsoleInfo, infoCallback: (msg: string) => {
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
},
}); });
} }

View File

@@ -0,0 +1,75 @@
import * as ko from "knockout";
import { NewVertexComponent, NewVertexViewModel } from "./NewVertexComponent";
const component = NewVertexComponent;
describe("New Vertex Component", () => {
let vm: NewVertexViewModel;
let partitionKeyProperty: ko.Observable<string>;
beforeEach(async () => {
document.body.innerHTML = component.template as any;
partitionKeyProperty = ko.observable(null);
vm = new component.viewModel({
newVertexData: null,
partitionKeyProperty,
});
ko.applyBindings(vm);
});
afterEach(() => {
ko.cleanNode(document);
});
describe("Rendering", () => {
it("should display property list with input and +Add Property", () => {
expect(document.querySelector(".newVertexComponent .newVertexForm")).not.toBeNull();
expect(document.querySelector(".newVertexComponent .edgeInput")).not.toBeNull();
expect(document.querySelector(".newVertexComponent .rightPaneAddPropertyBtn")).not.toBeNull();
});
it("should display partition key property if set", () => {
partitionKeyProperty("testKey");
expect(
(document.querySelector(".newVertexComponent .newVertexForm .labelCol input") as HTMLInputElement).value
).toEqual("testKey");
});
it("should NOT display partition key property if NOT set", () => {
expect(document.getElementsByClassName("valueCol").length).toBe(0);
});
});
describe("Behavior", () => {
let clickSpy: jasmine.Spy;
beforeEach(() => {
clickSpy = jasmine.createSpy("Command button click spy");
});
it("should add new property row when +Add property button is pressed", () => {
document.querySelector(".newVertexComponent .rightPaneAddPropertyBtn").dispatchEvent(new Event("click"));
document.querySelector(".newVertexComponent .rightPaneAddPropertyBtn").dispatchEvent(new Event("click"));
document.querySelector(".newVertexComponent .rightPaneAddPropertyBtn").dispatchEvent(new Event("click"));
expect(document.getElementsByClassName("valueCol").length).toBe(3);
expect(document.getElementsByClassName("rightPaneTrashIcon").length).toBe(3);
});
it("should remove property row when trash button is pressed", () => {
document.querySelector(".newVertexComponent .rightPaneAddPropertyBtn").dispatchEvent(new Event("click"));
document.querySelector(".newVertexComponent .rightPaneAddPropertyBtn").dispatchEvent(new Event("click"));
// Mark this one to delete
const elts = document.querySelectorAll(".newVertexComponent .rightPaneTrashIconImg");
elts[elts.length - 1].className += " deleteme";
document.querySelector(".newVertexComponent .rightPaneAddPropertyBtn").dispatchEvent(new Event("click"));
document
.querySelector(".newVertexComponent .rightPaneTrashIconImg.deleteme")
.parentElement.dispatchEvent(new Event("click"));
expect(document.getElementsByClassName("valueCol").length).toBe(2);
expect(document.getElementsByClassName("rightPaneTrashIcon").length).toBe(2);
expect(document.querySelectorAll(".newVertexComponent .rightPaneTrashIconImg.deleteme").length).toBe(0);
});
});
});

View File

@@ -0,0 +1,74 @@
<div class="newVertexComponent" data-bind="setTemplateReady: true">
<div class="newVertexForm">
<div class="newVertexFormRow">
<label for="VertexLabel" class="labelCol">Label</label>
<input
class="edgeInput"
type="text"
data-bind="textInput:$data.newVertexData().label, hasFocus: $data.firstFieldHasFocus"
aria-label="Enter vertex label"
role="textbox"
tabindex="0"
placeholder="Enter vertex label"
autocomplete="off"
id="VertexLabel"
/>
<div class="actionCol"></div>
</div>
<!-- ko foreach:{ data:newVertexData().properties, as: 'property' } -->
<div class="newVertexFormRow">
<div class="labelCol">
<input
type="text"
id="propertyKeyNewVertexPane"
data-bind="textInput: property.key, attr: { 'aria-label': 'Enter key for property '+ ($index() + 1) }"
placeholder="Key"
autocomplete="off"
/>
</div>
<div class="valueCol">
<input
class="edgeInput"
type="text"
data-bind="textInput: property.values[0].value, , attr: { 'aria-label': 'Enter value for property '+ ($index() + 1) }"
placeholder="Value"
autocomplete="off"
/>
</div>
<div>
<select
class="typeSelect"
required
data-bind="options:$parent.propertyTypes, value:property.values[0].type, attr: { 'aria-label': property.values[0].type + ': for property '+ ($index() + 1) }"
></select>
</div>
<div class="actionCol">
<div
class="rightPaneTrashIcon rightPaneBtns"
data-bind="click:$parent.removeNewVertexProperty.bind($parent, $index()), event: { keypress: $parent.removeNewVertexPropertyKeyPress.bind($parent, $index()) }, attr: { 'aria-label': 'Remove property '+ ($index() + 1) }"
tabindex="0"
role="button"
>
<img class="refreshcol rightPaneTrashIconImg" src="/delete.svg" alt="Remove property" />
</div>
</div>
</div>
<!-- /ko -->
<div class="newVertexFormRow">
<span class="rightPaneAddPropertyBtnPadding">
<span
class="rightPaneAddPropertyBtn rightPaneBtns"
id="addProperyNewVertexBtn"
data-bind="click:onAddNewProperty, event: { keypress: onAddNewPropertyKeyPress }"
tabindex="0"
role="button"
>
<img class="refreshcol rightPaneAddPropertyImg" src="/Add-property.svg" alt="Add property" /> Add
Property</span
>
</span>
</div>
</div>
</div>

View File

@@ -1,97 +0,0 @@
@import "../../../../less/Common/Constants";
.newVertexComponent {
padding: @LargeSpace 20px 20px 0px;
width: 400px;
.newVertexForm {
width: 100%;
.flex-display();
.flex-direction();
.newVertexFormRow {
.flex-display();
.flex-direction(@direction: row);
padding: 4px 5px;
label {
padding: 0px;
}
.valueCol {
flex-grow: 1;
padding-right: 5px;
}
.rightPaneAddPropertyBtnPadding {
padding-top: 14px;
}
.edgeLabel {
padding-right: 41px;
}
}
}
.actionCol {
min-width: 30px;
padding: 0px 4px;
}
.labelCol {
width: 72px;
min-width: 72px;
input {
max-width: 65px;
padding-left: 4px;
}
}
.edgeInput {
width: 100%;
padding-left: 4px;
}
.typeSelect {
height: 23px;
width: 70px;
}
.rightPaneTrashIcon {
padding: 4px 1px 0px 4px;
height: 100%;
}
.rightPaneTrashIconImg {
vertical-align: top;
}
.rightPaneAddPropertyBtn {
padding: 7px 7px 8px 8px;
margin-left: -8px;
}
.rightPaneBtns {
cursor: pointer;
&:hover {
background-color: @BaseLow;
}
&:active {
background-color: @AccentMediumLow;
}
}
.rightPaneAddPropertyImg {
margin-right: 5px;
margin-bottom: 4px;
}
.contentScroll {
overflow-y: auto;
overflow-x: hidden;
white-space: nowrap;
}
}

View File

@@ -1,118 +0,0 @@
import { fireEvent, render, screen } from "@testing-library/react";
import React from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import { NewVertexComponent } from "./NewVertexComponent";
describe("New Vertex Component", () => {
beforeEach(() => {
const fakeNewVertexData: ViewModels.NewVertexData = {
label: "",
properties: [
{
key: "test1",
values: [
{
value: "",
type: "string",
},
],
},
],
};
const props = {
newVertexDataProp: fakeNewVertexData,
partitionKeyPropertyProp: "test1",
onChangeProp: (): void => undefined,
};
render(<NewVertexComponent {...props} />);
});
it("should render default prpoerty", () => {
const fakeNewVertexData: ViewModels.NewVertexData = {
label: "",
properties: [],
};
const props = {
newVertexDataProp: fakeNewVertexData,
partitionKeyPropertyProp: "",
onChangeProp: (): void => undefined,
};
const { asFragment } = render(<NewVertexComponent {...props} />);
expect(asFragment).toMatchSnapshot();
});
it("should render Add property button", () => {
const span = screen.getByText("Add Property");
expect(span).toBeDefined();
});
it("should call onAddNewProperty method on span click", () => {
const onAddNewProperty = jest.fn();
const span = screen.getByText("Add Property");
span.onclick = onAddNewProperty();
fireEvent.click(span);
expect(onAddNewProperty).toHaveBeenCalled();
});
it("should call onAddNewPropertyKeyPress method on span keyPress", () => {
const onAddNewPropertyKeyPress = jest.fn();
const span = screen.getByText("Add Property");
span.onkeypress = onAddNewPropertyKeyPress();
fireEvent.keyPress(span, { key: "Enter", code: 13, charCode: 13 });
expect(onAddNewPropertyKeyPress).toHaveBeenCalled();
});
it("should call onLabelChange method on input change", () => {
const onLabelChange = jest.fn();
const input = screen.getByLabelText("Label");
input.onchange = onLabelChange();
fireEvent.change(input, { target: { value: "Label" } });
expect(onLabelChange).toHaveBeenCalled();
});
it("should call onKeyChange method on key input change", () => {
const onKeyChange = jest.fn();
const input = screen.queryByPlaceholderText("Key");
input.onchange = onKeyChange();
fireEvent.change(input, { target: { value: "pk1" } });
expect(onKeyChange).toHaveBeenCalled();
});
it("should call onValueChange method on value input change", () => {
const onValueChange = jest.fn();
const input = screen.queryByPlaceholderText("Value");
input.onchange = onValueChange();
fireEvent.change(input, { target: { value: "abc" } });
expect(onValueChange).toHaveBeenCalled();
});
it("should call removeNewVertexProperty method on remove button click", () => {
const removeNewVertexProperty = jest.fn();
const div = screen.getAllByRole("button");
div[0].onclick = removeNewVertexProperty();
fireEvent.click(div[0]);
expect(removeNewVertexProperty).toHaveBeenCalled();
});
it("should call removeNewVertexProperty method on remove button keyPress", () => {
const removeNewVertexPropertyKeyPress = jest.fn();
const div = screen.getAllByRole("button");
div[0].onkeypress = removeNewVertexPropertyKeyPress();
fireEvent.keyPress(div[0], { key: "Enter", code: 13, charCode: 13 });
expect(removeNewVertexPropertyKeyPress).toHaveBeenCalled();
});
it("should call onTypeChange method on type dropdown change", () => {
const DOWN_ARROW = { keyCode: 40 };
const onTypeChange = jest.fn();
const dropdown = screen.getByRole("listbox");
dropdown.onclick = onTypeChange();
dropdown.onkeydown = onTypeChange();
fireEvent.keyDown(screen.getByRole("listbox"), DOWN_ARROW);
fireEvent.click(screen.getByText(/number/));
expect(onTypeChange).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,99 @@
import * as ko from "knockout";
import { EditorNodePropertiesComponent } from "../GraphExplorerComponent/EditorNodePropertiesComponent";
import { NewVertexData, InputProperty } from "../../../Contracts/ViewModels";
import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel";
import * as Constants from "../../../Common/Constants";
import template from "./NewVertexComponent.html";
/**
* Parameters for this component
*/
export interface NewVertexParams {
// Data to be edited by the component
newVertexData: ko.Observable<NewVertexData>;
partitionKeyProperty: ko.Observable<string>;
firstFieldHasFocus?: ko.Observable<boolean>;
/**
* Callback triggered when the template is bound to the component (for testing purposes)
*/
onTemplateReady?: () => void;
}
export class NewVertexViewModel extends WaitsForTemplateViewModel {
private static readonly DEFAULT_PROPERTY_TYPE = "string";
private newVertexData: ko.Observable<NewVertexData>;
private firstFieldHasFocus: ko.Observable<boolean>;
private propertyTypes: string[];
public constructor(params: NewVertexParams) {
super();
super.onTemplateReady((isTemplateReady: boolean) => {
if (isTemplateReady && params.onTemplateReady) {
params.onTemplateReady();
}
});
this.newVertexData =
params.newVertexData ||
ko.observable({
label: "",
properties: <InputProperty[]>[],
});
this.firstFieldHasFocus = params.firstFieldHasFocus || ko.observable(false);
this.propertyTypes = EditorNodePropertiesComponent.VERTEX_PROPERTY_TYPES;
if (params.partitionKeyProperty) {
params.partitionKeyProperty.subscribe((newKeyProp: string) => {
if (!newKeyProp) {
return;
}
this.addNewVertexProperty(newKeyProp);
});
}
}
public onAddNewProperty() {
this.addNewVertexProperty();
document.getElementById("propertyKeyNewVertexPane").focus();
}
public onAddNewPropertyKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.onAddNewProperty();
event.stopPropagation();
return false;
}
return true;
};
public addNewVertexProperty(key?: string) {
let ap = this.newVertexData().properties;
ap.push({ key: key || "", values: [{ value: "", type: NewVertexViewModel.DEFAULT_PROPERTY_TYPE }] });
this.newVertexData.valueHasMutated();
}
public removeNewVertexProperty(index: number) {
let ap = this.newVertexData().properties;
ap.splice(index, 1);
this.newVertexData.valueHasMutated();
document.getElementById("addProperyNewVertexBtn").focus();
}
public removeNewVertexPropertyKeyPress = (index: number, source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.removeNewVertexProperty(index);
event.stopPropagation();
return false;
}
return true;
};
}
/**
* Helper class for ko component registration
*/
export const NewVertexComponent = {
viewModel: NewVertexViewModel,
template,
};

View File

@@ -1,213 +0,0 @@
import { Dropdown, IDropdownOption, Stack, TextField } from "office-ui-fabric-react";
import React, { FunctionComponent, useRef, useState } from "react";
import AddIcon from "../../../../images/Add-property.svg";
import DeleteIcon from "../../../../images/delete.svg";
import { NormalizedEventKey } from "../../../Common/Constants";
import { GremlinPropertyValueType, InputPropertyValueTypeString, NewVertexData } from "../../../Contracts/ViewModels";
import { EditorNodePropertiesComponent } from "../GraphExplorerComponent/EditorNodePropertiesComponent";
import "./NewVertexComponent.less";
export interface INewVertexComponentProps {
newVertexDataProp: NewVertexData;
partitionKeyPropertyProp: string;
onChangeProp: (labelData: NewVertexData) => void;
}
export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = ({
newVertexDataProp,
partitionKeyPropertyProp,
onChangeProp,
}: INewVertexComponentProps): JSX.Element => {
const DEFAULT_PROPERTY_TYPE = "string";
const [newVertexData, setNewVertexData] = useState<NewVertexData>(
newVertexDataProp || {
label: "",
properties: [
{
key: partitionKeyPropertyProp,
values: [{ value: "", type: DEFAULT_PROPERTY_TYPE }],
},
],
}
);
const propertyTypes: string[] = EditorNodePropertiesComponent.VERTEX_PROPERTY_TYPES;
const input = useRef(undefined);
const onAddNewProperty = () => {
addNewVertexProperty();
setTimeout(() => {
input.current.focus();
}, 100);
};
const onAddNewPropertyKeyPress = (event: React.KeyboardEvent) => {
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
onAddNewProperty();
event.stopPropagation();
}
};
const addNewVertexProperty = () => {
let key: string;
const ap = newVertexData.properties;
if (ap.length === 0) {
key = partitionKeyPropertyProp;
}
ap.push({
key: key || "",
values: [{ value: "", type: DEFAULT_PROPERTY_TYPE }],
});
setNewVertexData((prevData) => ({
...prevData,
properties: ap,
}));
onChangeProp(newVertexData);
};
const removeNewVertexProperty = (event?: React.MouseEvent<HTMLDivElement>, index?: number) => {
const ap = newVertexData.properties;
ap.splice(index, 1);
setNewVertexData((prevData) => ({
...prevData,
properties: ap,
}));
onChangeProp(newVertexData);
document.getElementById("addProperyNewVertexBtn").focus();
};
const removeNewVertexPropertyKeyPress = (event: React.KeyboardEvent<HTMLDivElement>, index: number) => {
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
removeNewVertexProperty(undefined, index);
event.stopPropagation();
}
};
const onLabelChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setNewVertexData((prevData) => ({
...prevData,
label: event.target.value,
}));
onChangeProp(newVertexData);
};
const onKeyChange = (event: React.ChangeEvent<HTMLInputElement>, index: number) => {
const newState = { ...newVertexData };
newState.properties[index].key = event.target.value;
setNewVertexData(newState);
onChangeProp(newVertexData);
};
const onValueChange = (event: React.ChangeEvent<HTMLInputElement>, index: number) => {
const newState = { ...newVertexData };
newState.properties[index].values[0].value = event.target.value as GremlinPropertyValueType;
setNewVertexData(newState);
onChangeProp(newVertexData);
};
const onTypeChange = (option: string, index: number) => {
const newState = { ...newVertexData };
if (newState.properties[index]) {
newState.properties[index].values[0].type = option as InputPropertyValueTypeString;
setNewVertexData(newState);
onChangeProp(newVertexData);
}
};
return (
<Stack>
<div className="newVertexComponent">
<div className="newVertexForm">
<div className="newVertexFormRow">
<TextField
label="Label"
className="edgeInput"
type="text"
ariaLabel="Enter vertex label"
role="textbox"
tabIndex={0}
placeholder="Enter vertex label"
autoComplete="off"
id="VertexLabel"
value={newVertexData.label}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
onLabelChange(event);
}}
/>
<div className="actionCol"></div>
</div>
{newVertexData.properties.map((data, index) => {
return (
<div key={index} className="newVertexFormRow">
<div className="labelCol">
<TextField
className="edgeInput"
type="text"
id="propertyKeyNewVertexPane"
componentRef={input}
placeholder="Key"
autoComplete="off"
aria-label={`Enter value for propery ${index + 1}`}
value={data.key}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onKeyChange(event, index)}
/>
</div>
<div className="valueCol">
<TextField
className="edgeInput"
type="text"
placeholder="Value"
autoComplete="off"
aria-label={`Enter value for propery ${index + 1}`}
value={data.values[0].value.toString()}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onValueChange(event, index)}
/>
</div>
<div>
<Dropdown
role="listbox"
placeholder="Select an option"
defaultSelectedKey={data.values[0].type}
style={{ width: 100 }}
options={propertyTypes.map((type) => ({
key: type,
text: type,
}))}
onChange={(_, options: IDropdownOption) => onTypeChange(options.key.toString(), index)}
/>
</div>
<div className="actionCol">
<div
className="rightPaneTrashIcon rightPaneBtns"
tabIndex={0}
role="button"
onClick={(event: React.MouseEvent<HTMLDivElement>) => removeNewVertexProperty(event, index)}
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) =>
removeNewVertexPropertyKeyPress(event, index)
}
>
<img className="refreshcol rightPaneTrashIconImg" src={DeleteIcon} alt="Remove property" />
</div>
</div>
</div>
);
})}
<div className="newVertexFormRow">
<span className="rightPaneAddPropertyBtnPadding">
<span
className="rightPaneAddPropertyBtn rightPaneBtns"
id="addProperyNewVertexBtn"
tabIndex={0}
role="button"
onClick={onAddNewProperty}
onKeyPress={(event: React.KeyboardEvent<HTMLSpanElement>) => onAddNewPropertyKeyPress(event)}
>
<img className="refreshcol rightPaneAddPropertyImg" src={AddIcon} alt="Add property" /> Add Property
</span>
</span>
</div>
</div>
</div>
</Stack>
);
};

View File

@@ -1,3 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`New Vertex Component should render default prpoerty 1`] = `[Function]`;

View File

@@ -0,0 +1,97 @@
@import "../../../../less/Common/Constants";
.newVertexComponent {
padding: @LargeSpace 20px 20px 0px;
width: 400px;
.newVertexForm {
width: 100%;
.flex-display();
.flex-direction();
.newVertexFormRow {
.flex-display();
.flex-direction(@direction: row);
padding: 4px 5px;
label {
padding: 0px;
}
.valueCol {
flex-grow: 1;
padding-right: 5px;
}
.rightPaneAddPropertyBtnPadding {
padding-top: 14px;
}
.edgeLabel {
padding-right: 41px;
}
}
}
.actionCol {
min-width: 30px;
padding: 0px 4px;
}
.labelCol {
width: 72px;
min-width: 72px;
input {
max-width: 65px;
padding-left: 4px;
}
}
.edgeInput {
width: 100%;
padding-left: 4px;
}
.typeSelect {
height: 23px;
width: 70px;
}
.rightPaneTrashIcon {
padding: 4px 1px 0px 4px;
height: 100%;
}
.rightPaneTrashIconImg {
vertical-align: top;
}
.rightPaneAddPropertyBtn {
padding: 7px 7px 8px 8px;
margin-left: -8px;
}
.rightPaneBtns {
cursor: pointer;
&:hover {
background-color: @BaseLow ;
}
&:active {
background-color: @AccentMediumLow;
}
}
.rightPaneAddPropertyImg {
margin-right: 5px;
margin-bottom: 4px;
}
.contentScroll {
overflow-y: auto;
overflow-x: hidden;
white-space: nowrap;
}
}

View File

@@ -4,15 +4,15 @@
* and update any knockout observables passed from the parent. * and update any knockout observables passed from the parent.
*/ */
import * as ko from "knockout"; import * as ko from "knockout";
import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
import * as React from "react"; import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { StyleConstants } from "../../../Common/Constants";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory"; import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
import { StyleConstants } from "../../../Common/Constants";
import * as CommandBarUtil from "./CommandBarUtil"; import * as CommandBarUtil from "./CommandBarUtil";
import Explorer from "../../Explorer";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
export class CommandBarComponentAdapter implements ReactAdapter { export class CommandBarComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>; public parameters: ko.Observable<number>;
@@ -23,13 +23,17 @@ export class CommandBarComponentAdapter implements ReactAdapter {
constructor(container: Explorer) { constructor(container: Explorer) {
this.container = container; this.container = container;
this.tabsButtons = []; this.tabsButtons = [];
this.isNotebookTabActive = ko.computed( this.isNotebookTabActive = ko.computed(() =>
() => container.tabsManager.activeTab()?.tabKind === ViewModels.CollectionTabKind.NotebookV2 container.tabsManager.isTabActive(ViewModels.CollectionTabKind.NotebookV2)
); );
// These are the parameters watched by the react binding that will trigger a renderComponent() if one of the ko mutates // These are the parameters watched by the react binding that will trigger a renderComponent() if one of the ko mutates
const toWatch = [ const toWatch = [
container.isPreferredApiTable,
container.isPreferredApiMongoDB, container.isPreferredApiMongoDB,
container.isPreferredApiDocumentDB,
container.isPreferredApiCassandra,
container.isPreferredApiGraph,
container.deleteCollectionText, container.deleteCollectionText,
container.deleteDatabaseText, container.deleteDatabaseText,
container.addCollectionText, container.addCollectionText,

View File

@@ -1,6 +1,5 @@
import * as ko from "knockout"; import * as ko from "knockout";
import { AuthType } from "../../../AuthType"; import { AuthType } from "../../../AuthType";
import { DatabaseAccount } from "../../../Contracts/DataModels";
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService"; import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { updateUserContext } from "../../../UserContext"; import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
@@ -16,14 +15,9 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
updateUserContext({ mockExplorer.isPreferredApiTable = ko.computed(() => true);
databaseAccount: {
properties: {
capabilities: [{ name: "EnableTable" }],
},
} as DatabaseAccount,
});
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
@@ -60,14 +54,9 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
updateUserContext({ mockExplorer.isPreferredApiTable = ko.computed(() => true);
databaseAccount: {
properties: {
capabilities: [{ name: "EnableTable" }],
},
} as DatabaseAccount,
});
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
@@ -129,13 +118,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
updateUserContext({ mockExplorer.isPreferredApiTable = ko.computed(() => true);
databaseAccount: { mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
properties: {
capabilities: [{ name: "EnableTable" }],
},
} as DatabaseAccount,
});
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
@@ -213,15 +197,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addDatabaseText = ko.observable("mockText");
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
updateUserContext({ mockExplorer.isPreferredApiTable = ko.computed(() => true);
databaseAccount: {
properties: {
capabilities: [{ name: "EnableTable" }],
},
} as DatabaseAccount,
});
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
@@ -231,27 +208,15 @@ describe("CommandBarComponentButtonFactory tests", () => {
}); });
beforeEach(() => { beforeEach(() => {
updateUserContext({ mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => true);
databaseAccount: {
properties: {
capabilities: [{ name: "EnableCassandra" }],
},
} as DatabaseAccount,
});
mockExplorer.isNotebookEnabled = ko.observable(false); mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false); mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.isRunningOnNationalCloud = ko.observable(false); mockExplorer.isRunningOnNationalCloud = ko.observable(false);
}); });
it("Cassandra Api not available - button should be hidden", () => { it("Cassandra Api not available - button should be hidden", () => {
updateUserContext({ mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
databaseAccount: {
properties: {
capabilities: [{ name: "EnableMongo" }],
},
} as DatabaseAccount,
});
console.log(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined(); expect(openCassandraShellBtn).toBeUndefined();
@@ -314,14 +279,9 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
updateUserContext({ mockExplorer.isPreferredApiTable = ko.computed(() => true);
databaseAccount: {
properties: {
capabilities: [{ name: "EnableTable" }],
},
} as DatabaseAccount,
});
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
@@ -377,6 +337,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isPreferredApiDocumentDB = ko.computed(() => true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true; mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true); mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true);
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false); mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
@@ -386,11 +347,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
}); });
it("should only show New SQL Query and Open Query buttons", () => { it("should only show New SQL Query and Open Query buttons", () => {
updateUserContext({
databaseAccount: {
kind: "DocumentDB",
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
expect(buttons.length).toBe(2); expect(buttons.length).toBe(2);
expect(buttons[0].commandButtonLabel).toBe("New SQL Query"); expect(buttons[0].commandButtonLabel).toBe("New SQL Query");

View File

@@ -47,7 +47,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
buttons.push(addSynapseLink); buttons.push(addSynapseLink);
} }
if (userContext.apiType !== "Tables") { if (!container.isPreferredApiTable()) {
newCollectionBtn.children = [createNewCollectionGroup(container)]; newCollectionBtn.children = [createNewCollectionGroup(container)];
const newDatabaseBtn = createNewDatabase(container); const newDatabaseBtn = createNewDatabase(container);
newCollectionBtn.children.push(newDatabaseBtn); newCollectionBtn.children.push(newDatabaseBtn);
@@ -74,7 +74,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
buttons.push(createOpenMongoTerminalButton(container)); buttons.push(createOpenMongoTerminalButton(container));
} }
if (userContext.apiType === "Cassandra") { if (container.isPreferredApiCassandra()) {
buttons.push(createOpenCassandraTerminalButton(container)); buttons.push(createOpenCassandraTerminalButton(container));
} }
} }
@@ -90,15 +90,15 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
buttons.push(createDivider()); buttons.push(createDivider());
} }
const isSqlQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; const isSqlQuerySupported = container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
if (isSqlQuerySupported) { if (isSqlQuerySupported) {
const newSqlQueryBtn = createNewSQLQueryButton(container); const newSqlQueryBtn = createNewSQLQueryButton(container);
buttons.push(newSqlQueryBtn); buttons.push(newSqlQueryBtn);
} }
const isSupportedOpenQueryApi = const isSupportedOpenQueryApi =
userContext.apiType === "SQL" || container.isPreferredApiMongoDB() || userContext.apiType === "Gremlin"; container.isPreferredApiDocumentDB() || container.isPreferredApiMongoDB() || container.isPreferredApiGraph();
const isSupportedOpenQueryFromDiskApi = userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; const isSupportedOpenQueryFromDiskApi = container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
if (isSupportedOpenQueryApi && container.selectedNode() && container.findSelectedCollection()) { if (isSupportedOpenQueryApi && container.selectedNode() && container.findSelectedCollection()) {
const openQueryBtn = createOpenQueryButton(container); const openQueryBtn = createOpenQueryButton(container);
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)]; openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)];
@@ -107,7 +107,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
buttons.push(createOpenQueryFromDiskButton(container)); buttons.push(createOpenQueryFromDiskButton(container));
} }
if (areScriptsSupported()) { if (areScriptsSupported(container)) {
const label = "New Stored Procedure"; const label = "New Stored Procedure";
const newStoredProcedureBtn: CommandButtonComponentProps = { const newStoredProcedureBtn: CommandButtonComponentProps = {
iconSrc: AddStoredProcedureIcon, iconSrc: AddStoredProcedureIcon,
@@ -154,18 +154,25 @@ export function createContextCommandBarButtons(container: Explorer): CommandButt
} }
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] { export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [ const buttons: CommandButtonComponentProps[] = [];
{ if (configContext.platform === Platform.Hosted) {
return buttons;
}
if (!container.isPreferredApiCassandra()) {
const label = "Settings";
const settingsPaneButton: CommandButtonComponentProps = {
iconSrc: SettingsIcon, iconSrc: SettingsIcon,
iconAlt: "Settings", iconAlt: label,
onCommandClick: () => container.openSettingPane(), onCommandClick: () => container.openSettingPane(),
commandButtonLabel: undefined, commandButtonLabel: undefined,
ariaLabel: "Settings", ariaLabel: label,
tooltipText: "Settings", tooltipText: label,
hasPopup: true, hasPopup: true,
disabled: false, disabled: false,
}, };
]; buttons.push(settingsPaneButton);
}
if (container.isHostedDataExplorerEnabled()) { if (container.isHostedDataExplorerEnabled()) {
const label = "Open Full Screen"; const label = "Open Full Screen";
@@ -216,8 +223,8 @@ export function createDivider(): CommandButtonComponentProps {
}; };
} }
function areScriptsSupported(): boolean { function areScriptsSupported(container: Explorer): boolean {
return userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; return container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
} }
function createNewCollectionGroup(container: Explorer): CommandButtonComponentProps { function createNewCollectionGroup(container: Explorer): CommandButtonComponentProps {
@@ -289,7 +296,7 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
} }
function createNewSQLQueryButton(container: Explorer): CommandButtonComponentProps { function createNewSQLQueryButton(container: Explorer): CommandButtonComponentProps {
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") { if (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph()) {
const label = "New SQL Query"; const label = "New SQL Query";
return { return {
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
@@ -303,7 +310,7 @@ function createNewSQLQueryButton(container: Explorer): CommandButtonComponentPro
hasPopup: true, hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(), disabled: container.isDatabaseNodeOrNoneSelected(),
}; };
} else if (userContext.apiType === "Mongo") { } else if (container.isPreferredApiMongoDB()) {
const label = "New Query"; const label = "New Query";
return { return {
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
@@ -325,7 +332,8 @@ function createNewSQLQueryButton(container: Explorer): CommandButtonComponentPro
export function createScriptCommandButtons(container: Explorer): CommandButtonComponentProps[] { export function createScriptCommandButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = []; const buttons: CommandButtonComponentProps[] = [];
const shouldEnableScriptsCommands: boolean = !container.isDatabaseNodeOrNoneSelected() && areScriptsSupported(); const shouldEnableScriptsCommands: boolean =
!container.isDatabaseNodeOrNoneSelected() && areScriptsSupported(container);
if (shouldEnableScriptsCommands) { if (shouldEnableScriptsCommands) {
const label = "New Stored Procedure"; const label = "New Stored Procedure";
@@ -445,10 +453,10 @@ function createEnableNotebooksButton(container: Explorer): CommandButtonComponen
return { return {
iconSrc: EnableNotebooksIcon, iconSrc: EnableNotebooksIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => container.openSetupNotebooksPanel(label, description), onCommandClick: () => container.setupNotebooksPane.openWithTitleAndDescription(label, description),
commandButtonLabel: label, commandButtonLabel: label,
hasPopup: false, hasPopup: false,
disabled: !container.isNotebooksEnabledForAccount(), disabled: false,
ariaLabel: label, ariaLabel: label,
tooltipText: container.isNotebooksEnabledForAccount() ? "" : tooltip, tooltipText: container.isNotebooksEnabledForAccount() ? "" : tooltip,
}; };
@@ -482,7 +490,7 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
if (container.isNotebookEnabled()) { if (container.isNotebookEnabled()) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else { } else {
container.openSetupNotebooksPanel(title, description); container.setupNotebooksPane.openWithTitleAndDescription(title, description);
} }
}, },
commandButtonLabel: label, commandButtonLabel: label,
@@ -508,7 +516,7 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
if (container.isNotebookEnabled()) { if (container.isNotebookEnabled()) {
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra); container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
} else { } else {
container.openSetupNotebooksPanel(title, description); container.setupNotebooksPane.openWithTitleAndDescription(title, description);
} }
}, },
commandButtonLabel: label, commandButtonLabel: label,

View File

@@ -1,9 +1,9 @@
import { shallow } from "enzyme";
import React from "react"; import React from "react";
import { shallow } from "enzyme";
import { import {
ConsoleDataType,
NotificationConsoleComponent,
NotificationConsoleComponentProps, NotificationConsoleComponentProps,
NotificationConsoleComponent,
ConsoleDataType,
} from "./NotificationConsoleComponent"; } from "./NotificationConsoleComponent";
describe("NotificationConsoleComponent", () => { describe("NotificationConsoleComponent", () => {
@@ -12,7 +12,7 @@ describe("NotificationConsoleComponent", () => {
consoleData: undefined, consoleData: undefined,
isConsoleExpanded: false, isConsoleExpanded: false,
inProgressConsoleDataIdToBeDeleted: "", inProgressConsoleDataIdToBeDeleted: "",
setIsConsoleExpanded: (): void => undefined, setIsConsoleExpanded: (isExpanded: boolean): void => {},
}; };
}; };
@@ -98,7 +98,7 @@ describe("NotificationConsoleComponent", () => {
wrapper.setProps(props); wrapper.setProps(props);
expect(wrapper.find(".notificationConsoleData .date").text()).toEqual(date); expect(wrapper.find(".notificationConsoleData .date").text()).toEqual(date);
expect(wrapper.find(".notificationConsoleData .message").text()).toEqual(message); expect(wrapper.find(".notificationConsoleData .message").text()).toEqual(message);
expect(wrapper.exists(`.notificationConsoleData .${iconClassName}`)).toBe(true); expect(wrapper.exists(`.notificationConsoleData .${iconClassName}`));
}; };
it("renders progress notifications", () => { it("renders progress notifications", () => {
@@ -139,7 +139,7 @@ describe("NotificationConsoleComponent", () => {
wrapper.setProps(props); wrapper.setProps(props);
wrapper.find(".clearNotificationsButton").simulate("click"); wrapper.find(".clearNotificationsButton").simulate("click");
expect(wrapper.exists(".notificationConsoleData")).toBe(true); expect(!wrapper.exists(".notificationConsoleData"));
}); });
it("collapses and hide content", () => { it("collapses and hide content", () => {
@@ -155,7 +155,7 @@ describe("NotificationConsoleComponent", () => {
wrapper.setProps(props); wrapper.setProps(props);
wrapper.find(".notificationConsoleHeader").simulate("click"); wrapper.find(".notificationConsoleHeader").simulate("click");
expect(wrapper.exists(".notificationConsoleContent")).toBe(false); expect(!wrapper.exists(".notificationConsoleContent"));
}); });
it("display latest data in header", () => { it("display latest data in header", () => {

View File

@@ -2,20 +2,19 @@
* React component for control bar * React component for control bar
*/ */
import { Dropdown, IDropdownOption } from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import { ClientDefaults, KeyCodes } from "../../../Common/Constants";
import AnimateHeight from "react-animate-height"; import AnimateHeight from "react-animate-height";
import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif"; import { Dropdown, IDropdownOption } from "office-ui-fabric-react";
import ClearIcon from "../../../../images/Clear.svg"; import LoadingIcon from "../../../../images/loading.svg";
import ErrorBlackIcon from "../../../../images/error_black.svg"; import ErrorBlackIcon from "../../../../images/error_black.svg";
import ErrorRedIcon from "../../../../images/error_red.svg";
import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg"; import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg";
import InfoIcon from "../../../../images/info_color.svg"; import InfoIcon from "../../../../images/info_color.svg";
import LoadingIcon from "../../../../images/loading.svg"; import ErrorRedIcon from "../../../../images/error_red.svg";
import ChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png"; import ClearIcon from "../../../../images/Clear.svg";
import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif";
import ChevronUpIcon from "../../../../images/QueryBuilder/CollapseChevronUp_16x.png"; import ChevronUpIcon from "../../../../images/QueryBuilder/CollapseChevronUp_16x.png";
import { ClientDefaults, KeyCodes } from "../../../Common/Constants"; import ChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
import { userContext } from "../../../UserContext";
/** /**
* Log levels * Log levels
@@ -77,7 +76,7 @@ export class NotificationConsoleComponent extends React.Component<
public componentDidUpdate( public componentDidUpdate(
prevProps: NotificationConsoleComponentProps, prevProps: NotificationConsoleComponentProps,
prevState: NotificationConsoleComponentState prevState: NotificationConsoleComponentState
): void { ) {
const currentHeaderStatus = NotificationConsoleComponent.extractHeaderStatus(this.props.consoleData); const currentHeaderStatus = NotificationConsoleComponent.extractHeaderStatus(this.props.consoleData);
if ( if (
@@ -98,7 +97,7 @@ export class NotificationConsoleComponent extends React.Component<
} }
} }
public setElememntRef = (element: HTMLElement): void => { public setElememntRef = (element: HTMLElement) => {
this.consoleHeaderElement = element; this.consoleHeaderElement = element;
}; };
@@ -117,7 +116,7 @@ export class NotificationConsoleComponent extends React.Component<
className="notificationConsoleHeader" className="notificationConsoleHeader"
id="notificationConsoleHeader" id="notificationConsoleHeader"
ref={this.setElememntRef} ref={this.setElememntRef}
onClick={() => this.expandCollapseConsole()} onClick={(event: React.MouseEvent<HTMLDivElement>) => this.expandCollapseConsole()}
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)} onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
tabIndex={0} tabIndex={0}
> >
@@ -136,7 +135,6 @@ export class NotificationConsoleComponent extends React.Component<
<span className="numInfoItems">{numInfoItems}</span> <span className="numInfoItems">{numInfoItems}</span>
</span> </span>
</span> </span>
{userContext.features.pr && <PrPreview pr={userContext.features.pr} />}
<span className="consoleSplitter" /> <span className="consoleSplitter" />
<span className="headerStatus"> <span className="headerStatus">
<span className="headerStatusEllipsis">{this.state.headerStatus}</span> <span className="headerStatusEllipsis">{this.state.headerStatus}</span>
@@ -306,18 +304,3 @@ export class NotificationConsoleComponent extends React.Component<
); );
}; };
} }
const PrPreview = (props: { pr: string }) => {
const url = new URL(props.pr);
const [, ref] = url.hash.split("#");
url.hash = "";
return (
<>
<span className="consoleSplitter" />
<a target="_blank" rel="noreferrer" href={url.href} style={{ marginRight: "1em", fontWeight: "bold" }}>
{ref}
</a>
</>
);
};

View File

@@ -1,16 +1,15 @@
// Manages all the redux logic for the notebook nteract code // Manages all the redux logic for the notebook nteract code
// TODO: Merge with NotebookClient? // TODO: Merge with NotebookClient?
import { NotebookWorkspaceConnectionInfo } from "../../Contracts/DataModels";
import * as Constants from "../../Common/Constants";
import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types";
// Vendor modules // Vendor modules
import { import {
actions, actions,
AppState, AppState,
ContentRecord,
createHostRef, createHostRef,
createKernelspecsRef, createKernelspecsRef,
HostRecord,
HostRef,
IContentProvider,
KernelspecsRef,
makeAppRecord, makeAppRecord,
makeCommsRecord, makeCommsRecord,
makeContentsRecord, makeContentsRecord,
@@ -20,20 +19,23 @@ import {
makeJupyterHostRecord, makeJupyterHostRecord,
makeStateRecord, makeStateRecord,
makeTransformsRecord, makeTransformsRecord,
ContentRecord,
HostRecord,
HostRef,
KernelspecsRef,
IContentProvider,
} from "@nteract/core"; } from "@nteract/core";
import { configOption, createConfigCollection, defineConfigOption } from "@nteract/mythic-configuration";
import { Media } from "@nteract/outputs"; import { Media } from "@nteract/outputs";
import TransformVDOM from "@nteract/transform-vdom"; import TransformVDOM from "@nteract/transform-vdom";
import * as Immutable from "immutable"; import * as Immutable from "immutable";
import { Notification } from "react-notification-system"; import { Store, AnyAction, MiddlewareAPI, Middleware, Dispatch } from "redux";
import { AnyAction, Dispatch, Middleware, MiddlewareAPI, Store } from "redux";
import * as Constants from "../../Common/Constants";
import { NotebookWorkspaceConnectionInfo } from "../../Contracts/DataModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import configureStore from "./NotebookComponent/store"; import configureStore from "./NotebookComponent/store";
import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types";
import { Notification } from "react-notification-system";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { configOption, createConfigCollection, defineConfigOption } from "@nteract/mythic-configuration";
export type KernelSpecsDisplay = { name: string; displayName: string }; export type KernelSpecsDisplay = { name: string; displayName: string };
@@ -123,62 +125,60 @@ export class NotebookClientV2 {
contents: makeContentsRecord({ contents: makeContentsRecord({
byRef: Immutable.Map<string, ContentRecord>(), byRef: Immutable.Map<string, ContentRecord>(),
}), }),
transforms: userContext.features.sandboxNotebookOutputs transforms: makeTransformsRecord({
? undefined displayOrder: Immutable.List([
: makeTransformsRecord({ "application/vnd.jupyter.widget-view+json",
displayOrder: Immutable.List([ "application/vnd.vega.v5+json",
"application/vnd.jupyter.widget-view+json", "application/vnd.vega.v4+json",
"application/vnd.vega.v5+json", "application/vnd.vega.v3+json",
"application/vnd.vega.v4+json", "application/vnd.vega.v2+json",
"application/vnd.vega.v3+json", "application/vnd.vegalite.v3+json",
"application/vnd.vega.v2+json", "application/vnd.vegalite.v2+json",
"application/vnd.vegalite.v3+json", "application/vnd.vegalite.v1+json",
"application/vnd.vegalite.v2+json", "application/geo+json",
"application/vnd.vegalite.v1+json", "application/vnd.plotly.v1+json",
"application/geo+json", "text/vnd.plotly.v1+html",
"application/vnd.plotly.v1+json", "application/x-nteract-model-debug+json",
"text/vnd.plotly.v1+html", "application/vnd.dataresource+json",
"application/x-nteract-model-debug+json", "application/vdom.v1+json",
"application/vnd.dataresource+json", "application/json",
"application/vdom.v1+json", "application/javascript",
"application/json", "text/html",
"application/javascript", "text/markdown",
"text/html", "text/latex",
"text/markdown", "image/svg+xml",
"text/latex", "image/gif",
"image/svg+xml", "image/png",
"image/gif", "image/jpeg",
"image/png", "text/plain",
"image/jpeg", ]),
"text/plain", byId: Immutable.Map({
]), "text/vnd.plotly.v1+html": NullTransform,
byId: Immutable.Map({ "application/vnd.plotly.v1+json": NullTransform,
"text/vnd.plotly.v1+html": NullTransform, "application/geo+json": NullTransform,
"application/vnd.plotly.v1+json": NullTransform, "application/x-nteract-model-debug+json": NullTransform,
"application/geo+json": NullTransform, "application/vnd.dataresource+json": NullTransform,
"application/x-nteract-model-debug+json": NullTransform, "application/vnd.jupyter.widget-view+json": NullTransform,
"application/vnd.dataresource+json": NullTransform, "application/vnd.vegalite.v1+json": NullTransform,
"application/vnd.jupyter.widget-view+json": NullTransform, "application/vnd.vegalite.v2+json": NullTransform,
"application/vnd.vegalite.v1+json": NullTransform, "application/vnd.vegalite.v3+json": NullTransform,
"application/vnd.vegalite.v2+json": NullTransform, "application/vnd.vega.v2+json": NullTransform,
"application/vnd.vegalite.v3+json": NullTransform, "application/vnd.vega.v3+json": NullTransform,
"application/vnd.vega.v2+json": NullTransform, "application/vnd.vega.v4+json": NullTransform,
"application/vnd.vega.v3+json": NullTransform, "application/vnd.vega.v5+json": NullTransform,
"application/vnd.vega.v4+json": NullTransform, "application/vdom.v1+json": TransformVDOM,
"application/vnd.vega.v5+json": NullTransform, "application/json": Media.Json,
"application/vdom.v1+json": TransformVDOM, "application/javascript": Media.JavaScript,
"application/json": Media.Json, "text/html": Media.HTML,
"application/javascript": Media.JavaScript, "text/markdown": Media.Markdown,
"text/html": Media.HTML, "text/latex": Media.LaTeX,
"text/markdown": Media.Markdown, "image/svg+xml": Media.SVG,
"text/latex": Media.LaTeX, "image/gif": Media.Image,
"image/svg+xml": Media.SVG, "image/png": Media.Image,
"image/gif": Media.Image, "image/jpeg": Media.Image,
"image/png": Media.Image, "text/plain": Media.Plain,
"image/jpeg": Media.Image, }),
"text/plain": Media.Plain, }),
}),
}),
}), }),
}), }),
cdb: makeCdbRecord({ cdb: makeCdbRecord({
@@ -200,11 +200,10 @@ export class NotebookClientV2 {
case actions.FETCH_KERNELSPECS_FULFILLED: { case actions.FETCH_KERNELSPECS_FULFILLED: {
const payload = ((action as unknown) as actions.FetchKernelspecsFulfilled).payload; const payload = ((action as unknown) as actions.FetchKernelspecsFulfilled).payload;
const defaultKernelName = payload.defaultKernelName; const defaultKernelName = payload.defaultKernelName;
this.kernelSpecsForDisplay = Object.values(payload.kernelspecs) this.kernelSpecsForDisplay = Object.keys(payload.kernelspecs)
.filter((spec) => !spec.metadata?.hasOwnProperty("hidden")) .map((name) => ({
.map((spec) => ({ name,
name: spec.name, displayName: payload.kernelspecs[name].displayName,
displayName: spec.displayName,
})) }))
.sort((a: KernelSpecsDisplay, b: KernelSpecsDisplay) => { .sort((a: KernelSpecsDisplay, b: KernelSpecsDisplay) => {
// Put default at the top, otherwise lexicographically compare // Put default at the top, otherwise lexicographically compare

View File

@@ -1,49 +1,52 @@
import { EMPTY, merge, of, timer, concat, Subject, Subscriber, Observable, Observer, from } from "rxjs";
import { webSocket } from "rxjs/webSocket";
import { StateObservable } from "redux-observable";
import { ofType } from "redux-observable";
import { import {
actions,
AppState,
castToSessionId,
ContentRef,
createKernelRef,
JupyterHostRecordProps,
KernelInfo,
KernelRef,
RemoteKernelProps,
selectors,
ServerConfig as JupyterServerConfig,
} from "@nteract/core";
import { Channels, childOf, createMessage, JupyterMessage, message, ofMessageType } from "@nteract/messaging";
import { RecordOf } from "immutable";
import { AnyAction } from "redux";
import { ofType, StateObservable } from "redux-observable";
import { kernels, sessions } from "rx-jupyter";
import { concat, EMPTY, from, merge, Observable, Observer, of, Subject, Subscriber, timer } from "rxjs";
import {
catchError,
concatMap,
delayWhen,
filter,
first,
map,
mergeMap, mergeMap,
tap,
retryWhen, retryWhen,
delayWhen,
map,
switchMap, switchMap,
take, take,
tap, filter,
catchError,
first,
concatMap,
timeout, timeout,
} from "rxjs/operators"; } from "rxjs/operators";
import { webSocket } from "rxjs/webSocket"; import {
AppState,
ServerConfig as JupyterServerConfig,
JupyterHostRecordProps,
RemoteKernelProps,
castToSessionId,
createKernelRef,
KernelRef,
ContentRef,
KernelInfo,
actions,
selectors,
} from "@nteract/core";
import { message, JupyterMessage, Channels, createMessage, childOf, ofMessageType } from "@nteract/messaging";
import { sessions, kernels } from "rx-jupyter";
import { RecordOf } from "immutable";
import { AnyAction } from "redux";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import { Areas } from "../../../Common/Constants"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import * as CdbActions from "./actions";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { CdbAppState } from "./types";
import { decryptJWTToken } from "../../../Utils/AuthorizationUtils"; import { decryptJWTToken } from "../../../Utils/AuthorizationUtils";
import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationConsoleUtils"; import * as TextFile from "./contents/file/text-file";
import { NotebookUtil } from "../NotebookUtil";
import * as FileSystemUtil from "../FileSystemUtil"; import * as FileSystemUtil from "../FileSystemUtil";
import * as cdbActions from "../NotebookComponent/actions"; import * as cdbActions from "../NotebookComponent/actions";
import { NotebookUtil } from "../NotebookUtil"; import { Areas } from "../../../Common/Constants";
import * as CdbActions from "./actions";
import * as TextFile from "./contents/file/text-file";
import { CdbAppState } from "./types";
interface NotebookServiceConfig extends JupyterServerConfig { interface NotebookServiceConfig extends JupyterServerConfig {
userPuid?: string; userPuid?: string;
@@ -308,7 +311,7 @@ export const launchWebSocketKernelEpic = (
if (currentKernelspecs) { if (currentKernelspecs) {
kernelSpecToLaunch = currentKernelspecs.defaultKernelName; kernelSpecToLaunch = currentKernelspecs.defaultKernelName;
const msg = `No kernelspec name specified to launch, using default kernel: ${kernelSpecToLaunch}`; const msg = `No kernelspec name specified to launch, using default kernel: ${kernelSpecToLaunch}`;
logConsoleInfo(msg); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
logFailureToTelemetry(state$.value, "Launching alternate kernel", msg); logFailureToTelemetry(state$.value, "Launching alternate kernel", msg);
} else { } else {
return of( return of(
@@ -334,7 +337,7 @@ export const launchWebSocketKernelEpic = (
kernelSpecToLaunch = currentKernelspecs.defaultKernelName; kernelSpecToLaunch = currentKernelspecs.defaultKernelName;
msg += ` Using default kernel: ${kernelSpecToLaunch}`; msg += ` Using default kernel: ${kernelSpecToLaunch}`;
} }
logConsoleInfo(msg); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
logFailureToTelemetry(state$.value, "Launching alternate kernel", msg); logFailureToTelemetry(state$.value, "Launching alternate kernel", msg);
} }
@@ -631,7 +634,7 @@ const notificationsToUserEpic = (action$: Observable<any>, state$: StateObservab
case actions.RESTART_KERNEL_SUCCESSFUL: { case actions.RESTART_KERNEL_SUCCESSFUL: {
const title = "Kernel restart"; const title = "Kernel restart";
const msg = "Kernel successfully restarted"; const msg = "Kernel successfully restarted";
logConsoleInfo(msg); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
logFailureToTelemetry(state$.value, title, msg); logFailureToTelemetry(state$.value, title, msg);
break; break;
} }
@@ -642,7 +645,7 @@ const notificationsToUserEpic = (action$: Observable<any>, state$: StateObservab
case actions.SAVE_FAILED: { case actions.SAVE_FAILED: {
const title = "Save failure"; const title = "Save failure";
const msg = `Failed to save notebook: ${(action as actions.SaveFailed).payload.error}`; const msg = `Failed to save notebook: ${(action as actions.SaveFailed).payload.error}`;
logConsoleError(msg); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
logFailureToTelemetry(state$.value, title, msg); logFailureToTelemetry(state$.value, title, msg);
break; break;
} }
@@ -651,7 +654,7 @@ const notificationsToUserEpic = (action$: Observable<any>, state$: StateObservab
const filepath = selectors.filepath(state$.value, { contentRef: typedAction.payload.contentRef }); const filepath = selectors.filepath(state$.value, { contentRef: typedAction.payload.contentRef });
const title = "Fetching content failure"; const title = "Fetching content failure";
const msg = `Failed to fetch notebook content: ${filepath}, error: ${typedAction.payload.error}`; const msg = `Failed to fetch notebook content: ${filepath}, error: ${typedAction.payload.error}`;
logConsoleError(msg); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
logFailureToTelemetry(state$.value, title, msg); logFailureToTelemetry(state$.value, title, msg);
break; break;
} }
@@ -676,7 +679,7 @@ const handleKernelConnectionLostEpic = (
const state = state$.value; const state = state$.value;
const msg = "Notebook was disconnected from kernel"; const msg = "Notebook was disconnected from kernel";
logConsoleError(msg); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
logFailureToTelemetry(state, "Error", "Kernel connection error"); logFailureToTelemetry(state, "Error", "Kernel connection error");
const host = selectors.currentHost(state); const host = selectors.currentHost(state);
@@ -689,7 +692,7 @@ const handleKernelConnectionLostEpic = (
if (delayMs > Constants.Notebook.kernelRestartMaxDelayMs) { if (delayMs > Constants.Notebook.kernelRestartMaxDelayMs) {
const msg = const msg =
"Restarted kernel too many times. Please reload the page to enable Data Explorer to restart the kernel automatically."; "Restarted kernel too many times. Please reload the page to enable Data Explorer to restart the kernel automatically.";
logConsoleError(msg); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
logFailureToTelemetry(state, "Kernel restart error", msg); logFailureToTelemetry(state, "Kernel restart error", msg);
const explorer = window.dataExplorer; const explorer = window.dataExplorer;
@@ -807,7 +810,7 @@ const closeUnsupportedMimetypesEpic = (
); );
const msg = `${filepath} cannot be rendered. Please download the file, in order to view it outside of Data Explorer.`; const msg = `${filepath} cannot be rendered. Please download the file, in order to view it outside of Data Explorer.`;
explorer.showOkModalDialog("File cannot be rendered", msg); explorer.showOkModalDialog("File cannot be rendered", msg);
logConsoleError(msg); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
} }
return EMPTY; return EMPTY;
}) })
@@ -835,7 +838,7 @@ const closeContentFailedToFetchEpic = (
); );
const msg = `Failed to load file: ${filepath}.`; const msg = `Failed to load file: ${filepath}.`;
explorer.showOkModalDialog("Failure to load", msg); explorer.showOkModalDialog("Failure to load", msg);
logConsoleError(msg); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
} }
return EMPTY; return EMPTY;
}) })

View File

@@ -1,14 +1,15 @@
/** /**
* Notebook container related stuff * Notebook container related stuff
*/ */
import * as Constants from "../../Common/Constants";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import * as Constants from "../../Common/Constants";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as Logger from "../../Common/Logger";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
export class NotebookContainerClient { export class NotebookContainerClient {
private clearReconnectionAttemptMessage? = () => {}; private reconnectingNotificationId: string;
private isResettingWorkspace: boolean; private isResettingWorkspace: boolean;
constructor( constructor(
@@ -60,9 +61,9 @@ export class NotebookContainerClient {
}, },
}); });
if (response.ok) { if (response.ok) {
if (this.clearReconnectionAttemptMessage) { if (this.reconnectingNotificationId) {
this.clearReconnectionAttemptMessage(); NotificationConsoleUtils.clearInProgressMessageWithId(this.reconnectingNotificationId);
this.clearReconnectionAttemptMessage = undefined; this.reconnectingNotificationId = "";
} }
const memoryUsageInfo = await response.json(); const memoryUsageInfo = await response.json();
if (memoryUsageInfo) { if (memoryUsageInfo) {
@@ -75,8 +76,9 @@ export class NotebookContainerClient {
return undefined; return undefined;
} catch (error) { } catch (error) {
Logger.logError(getErrorMessage(error), "NotebookContainerClient/getMemoryUsage"); Logger.logError(getErrorMessage(error), "NotebookContainerClient/getMemoryUsage");
if (!this.clearReconnectionAttemptMessage) { if (!this.reconnectingNotificationId) {
this.clearReconnectionAttemptMessage = logConsoleProgress( this.reconnectingNotificationId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
"Connection lost with Notebook server. Attempting to reconnect..." "Connection lost with Notebook server. Attempting to reconnect..."
); );
} }

View File

@@ -2,31 +2,30 @@
* Contains all notebook related stuff meant to be dynamically loaded by explorer * Contains all notebook related stuff meant to be dynamically loaded by explorer
*/ */
import { ImmutableNotebook } from "@nteract/commutable";
import { IContentProvider } from "@nteract/core";
import ko from "knockout";
import React from "react";
import { contents } from "rx-jupyter";
import { Areas, HttpStatusCodes } from "../../Common/Constants";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import { MemoryUsageInfo } from "../../Contracts/DataModels";
import { GitHubClient } from "../../GitHub/GitHubClient";
import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider";
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
import { JunoClient } from "../../Juno/JunoClient"; import { JunoClient } from "../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
import { GitHubClient } from "../../GitHub/GitHubClient";
import * as Logger from "../../Common/Logger";
import { HttpStatusCodes, Areas } from "../../Common/Constants";
import { GitHubReposPane } from "../Panes/GitHubReposPane";
import ko from "knockout";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { IContentProvider } from "@nteract/core";
import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider";
import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider";
import { contents } from "rx-jupyter";
import { NotebookContainerClient } from "./NotebookContainerClient";
import { MemoryUsageInfo } from "../../Contracts/DataModels";
import { NotebookContentClient } from "./NotebookContentClient";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter";
import { getFullName } from "../../Utils/UserUtils"; import { getFullName } from "../../Utils/UserUtils";
import { ImmutableNotebook } from "@nteract/commutable";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { ContextualPaneBase } from "../Panes/ContextualPaneBase"; import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane"; import { CopyNotebookPaneAdapter } from "../Panes/CopyNotebookPane";
import { GitHubReposPane } from "../Panes/GitHubReposPane"; import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider";
import { NotebookContainerClient } from "./NotebookContainerClient";
import { NotebookContentClient } from "./NotebookContentClient";
export interface NotebookManagerOptions { export interface NotebookManagerOptions {
container: Explorer; container: Explorer;
@@ -50,6 +49,7 @@ export default class NotebookManager {
public gitHubReposPane: ContextualPaneBase; public gitHubReposPane: ContextualPaneBase;
public publishNotebookPaneAdapter: PublishNotebookPaneAdapter; public publishNotebookPaneAdapter: PublishNotebookPaneAdapter;
public copyNotebookPaneAdapter: CopyNotebookPaneAdapter;
public initialize(params: NotebookManagerOptions): void { public initialize(params: NotebookManagerOptions): void {
this.params = params; this.params = params;
@@ -89,6 +89,12 @@ export default class NotebookManager {
this.publishNotebookPaneAdapter = new PublishNotebookPaneAdapter(this.params.container, this.junoClient); this.publishNotebookPaneAdapter = new PublishNotebookPaneAdapter(this.params.container, this.junoClient);
this.copyNotebookPaneAdapter = new CopyNotebookPaneAdapter(
this.params.container,
this.junoClient,
this.gitHubOAuthService
);
this.gitHubOAuthService.getTokenObservable().subscribe((token) => { this.gitHubOAuthService.getTokenObservable().subscribe((token) => {
this.gitHubClient.setToken(token?.access_token); this.gitHubClient.setToken(token?.access_token);
@@ -123,18 +129,7 @@ export default class NotebookManager {
} }
public openCopyNotebookPane(name: string, content: string): void { public openCopyNotebookPane(name: string, content: string): void {
const { container } = this.params; this.copyNotebookPaneAdapter.open(name, content);
container.openSidePanel(
"Copy Notebook",
<CopyNotebookPane
container={container}
closePanel={container.closeSidePanel}
junoClient={this.junoClient}
gitHubOAuthService={this.gitHubOAuthService}
name={name}
content={content}
/>
);
} }
// Octokit's error handler uses any // Octokit's error handler uses any

View File

@@ -1,18 +1,18 @@
import { actions, ContentRef } from "@nteract/core";
import { Cells, CodeCell, MarkdownCell, RawCell } from "@nteract/stateful-components";
import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
import Prompt, { PassedPromptProps } from "@nteract/stateful-components/lib/inputs/prompt";
import * as React from "react"; import * as React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { userContext } from "../../../UserContext";
import loadTransform from "../NotebookComponent/loadTransform";
import { AzureTheme } from "./AzureTheme";
import "./base.css"; import "./base.css";
import "./default.css"; import "./default.css";
import { CodeCell, RawCell, Cells, MarkdownCell } from "@nteract/stateful-components";
import Prompt, { PassedPromptProps } from "@nteract/stateful-components/lib/inputs/prompt";
import { AzureTheme } from "./AzureTheme";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { actions, ContentRef } from "@nteract/core";
import loadTransform from "../NotebookComponent/loadTransform";
import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
import "./NotebookReadOnlyRenderer.less"; import "./NotebookReadOnlyRenderer.less";
import SandboxOutputs from "./outputs/SandboxOutputs";
export interface NotebookRendererProps { export interface NotebookRendererProps {
contentRef: any; contentRef: any;
@@ -25,9 +25,7 @@ export interface NotebookRendererProps {
*/ */
class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> { class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
componentDidMount() { componentDidMount() {
if (!userContext.features.sandboxNotebookOutputs) { loadTransform(this.props as any);
loadTransform(this.props as any);
}
} }
private renderPrompt(id: string, contentRef: string): JSX.Element { private renderPrompt(id: string, contentRef: string): JSX.Element {
@@ -62,9 +60,6 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
<CodeCell id={id} contentRef={contentRef}> <CodeCell id={id} contentRef={contentRef}>
{{ {{
prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef), prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef),
outputs: userContext.features.sandboxNotebookOutputs
? () => <SandboxOutputs id={id} contentRef={contentRef} />
: undefined,
editor: { editor: {
monaco: (props: PassedEditorProps) => monaco: (props: PassedEditorProps) =>
this.props.hideInputs ? <></> : <MonacoEditor readOnly={true} {...props} editorType={"monaco"} />, this.props.hideInputs ? <></> : <MonacoEditor readOnly={true} {...props} editorType={"monaco"} />,

View File

@@ -1,31 +1,37 @@
import { CellId } from "@nteract/commutable"; import * as React from "react";
import { CellType } from "@nteract/commutable/src"; import "./base.css";
import { actions, ContentRef } from "@nteract/core"; import "./default.css";
import { Cells, CodeCell, RawCell } from "@nteract/stateful-components";
import { RawCell, Cells, CodeCell, MarkdownCell } from "@nteract/stateful-components";
import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor"; import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor"; import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
import * as React from "react";
import { DndProvider } from "react-dnd";
import HTML5Backend from "react-dnd-html5-backend";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { userContext } from "../../../UserContext";
import * as cdbActions from "../NotebookComponent/actions";
import loadTransform from "../NotebookComponent/loadTransform";
import { AzureTheme } from "./AzureTheme";
import "./base.css";
import CellCreator from "./decorators/CellCreator";
import CellLabeler from "./decorators/CellLabeler";
import HoverableCell from "./decorators/HoverableCell";
import KeyboardShortcuts from "./decorators/kbd-shortcuts";
import "./default.css";
import MarkdownCell from "./markdown-cell";
import "./NotebookRenderer.less";
import SandboxOutputs from "./outputs/SandboxOutputs";
import Prompt from "./Prompt"; import Prompt from "./Prompt";
import { promptContent } from "./PromptContent"; import { promptContent } from "./PromptContent";
import StatusBar from "./StatusBar";
import { AzureTheme } from "./AzureTheme";
import { DndProvider } from "react-dnd";
import HTML5Backend from "react-dnd-html5-backend";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { actions, ContentRef } from "@nteract/core";
import { CellId } from "@nteract/commutable";
import loadTransform from "../NotebookComponent/loadTransform";
import DraggableCell from "./decorators/draggable";
import CellCreator from "./decorators/CellCreator";
import KeyboardShortcuts from "./decorators/kbd-shortcuts";
import CellToolbar from "./Toolbar"; import CellToolbar from "./Toolbar";
import StatusBar from "./StatusBar";
import HijackScroll from "./decorators/hijack-scroll";
import { CellType } from "@nteract/commutable/src";
import "./NotebookRenderer.less";
import HoverableCell from "./decorators/HoverableCell";
import CellLabeler from "./decorators/CellLabeler";
import * as cdbActions from "../NotebookComponent/actions";
export interface NotebookRendererBaseProps { export interface NotebookRendererBaseProps {
contentRef: any; contentRef: any;
@@ -69,9 +75,7 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
} }
componentDidMount() { componentDidMount() {
if (!userContext.features.sandboxNotebookOutputs) { loadTransform(this.props as any);
loadTransform(this.props as any);
}
this.props.updateNotebookParentDomElt(this.props.contentRef, this.notebookRendererRef.current); this.props.updateNotebookParentDomElt(this.props.contentRef, this.notebookRendererRef.current);
} }
@@ -108,9 +112,6 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
</Prompt> </Prompt>
), ),
toolbar: () => <CellToolbar id={id} contentRef={contentRef} />, toolbar: () => <CellToolbar id={id} contentRef={contentRef} />,
outputs: userContext.features.sandboxNotebookOutputs
? () => <SandboxOutputs id={id} contentRef={contentRef} />
: undefined,
}} }}
</CodeCell> </CodeCell>
), ),

View File

@@ -1,160 +0,0 @@
// TODO The purpose of importing this source file https://github.com/nteract/nteract/blob/main/packages/stateful-components/src/cells/markdown-cell.tsx
// into our source is to be able to overwrite the version of react-markdown which has this fix ("escape html to false")
// https://github.com/nteract/markdown/commit/e19c7cc590a4379fc507f67a7b4228363b9d8631 without having to upgrade
// @nteract/stateful-component which causes runtime issues.
import { ImmutableCell } from "@nteract/commutable/src";
import { actions, AppState, ContentRef, selectors } from "@nteract/core";
import { MarkdownPreviewer } from "@nteract/markdown";
import { defineConfigOption } from "@nteract/mythic-configuration";
import { Source as BareSource } from "@nteract/presentational-components";
import Editor, { EditorSlots } from "@nteract/stateful-components/lib/inputs/editor";
import React from "react";
import { ReactMarkdownProps } from "react-markdown";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import styled from "styled-components";
const { selector: markdownConfig } = defineConfigOption({
key: "markdownOptions",
label: "Markdown Editor Options",
defaultValue: {},
});
interface NamedMDCellSlots {
editor?: EditorSlots;
toolbar?: () => JSX.Element;
}
interface ComponentProps {
id: string;
contentRef: ContentRef;
cell_type?: "markdown";
children?: NamedMDCellSlots;
}
interface StateProps {
isCellFocused: boolean;
isEditorFocused: boolean;
cell?: ImmutableCell;
markdownOptions: ReactMarkdownProps;
}
interface DispatchProps {
focusAboveCell: () => void;
focusBelowCell: () => void;
focusEditor: () => void;
unfocusEditor: () => void;
}
// Add missing style to make the editor show https://github.com/nteract/nteract/commit/7fa580011578350e56deac81359f6294fdfcad20#diff-07829a1908e4bf98d4420f868a1c6f890b95d77297b9805c9590d2dba11e80ce
export const Source = styled(BareSource)`
width: 100%;
width: -webkit-fill-available;
width: -moz-available;
`;
export class PureMarkdownCell extends React.Component<ComponentProps & DispatchProps & StateProps> {
render() {
const { contentRef, id, cell, children } = this.props;
const { isEditorFocused, isCellFocused, markdownOptions } = this.props;
const { focusAboveCell, focusBelowCell, focusEditor, unfocusEditor } = this.props;
/**
* We don't set the editor slots as defaults to support dynamic imports
* Users can continue to add the editorSlots as children
*/
const editor = children?.editor;
const toolbar = children?.toolbar;
const source = cell ? cell.get("source", "") : "";
return (
<div className="nteract-md-cell nteract-cell">
<div className="nteract-cell-row">
<div className="nteract-cell-gutter">{toolbar && toolbar()}</div>
<div className="nteract-cell-body">
<MarkdownPreviewer
focusAbove={focusAboveCell}
focusBelow={focusBelowCell}
focusEditor={focusEditor}
cellFocused={isCellFocused}
editorFocused={isEditorFocused}
unfocusEditor={unfocusEditor}
source={source}
markdownOptions={markdownOptions}
>
<Source className="nteract-cell-source">
<Editor id={id} contentRef={contentRef}>
{editor}
</Editor>
</Source>
</MarkdownPreviewer>
</div>
</div>
</div>
);
}
}
export const makeMapStateToProps = (
initialState: AppState,
ownProps: ComponentProps
): ((state: AppState) => StateProps) => {
const { id, contentRef } = ownProps;
const mapStateToProps = (state: AppState): StateProps => {
const model = selectors.model(state, { contentRef });
let isCellFocused = false;
let isEditorFocused = false;
let cell;
if (model && model.type === "notebook") {
cell = selectors.notebook.cellById(model, { id });
isCellFocused = model.cellFocused === id;
isEditorFocused = model.editorFocused === id;
}
const markdownOptionsDefaults = {
linkTarget: "_blank",
};
const currentMarkdownOptions = markdownConfig(state);
const markdownOptions = Object.assign({}, markdownOptionsDefaults, currentMarkdownOptions);
return {
cell,
isCellFocused,
isEditorFocused,
markdownOptions,
};
};
return mapStateToProps;
};
const makeMapDispatchToProps = (
initialDispatch: Dispatch,
ownProps: ComponentProps
): ((dispatch: Dispatch) => DispatchProps) => {
const { id, contentRef } = ownProps;
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
focusAboveCell: () => {
dispatch(actions.focusPreviousCell({ id, contentRef }));
dispatch(actions.focusPreviousCellEditor({ id, contentRef }));
},
focusBelowCell: () => {
dispatch(actions.focusNextCell({ id, createCellIfUndefined: true, contentRef }));
dispatch(actions.focusNextCellEditor({ id, contentRef }));
},
focusEditor: () => dispatch(actions.focusCellEditor({ id, contentRef })),
unfocusEditor: () => dispatch(actions.focusCellEditor({ id: undefined, contentRef })),
});
return mapDispatchToProps;
};
const MarkdownCell = connect(makeMapStateToProps, makeMapDispatchToProps)(PureMarkdownCell);
export default MarkdownCell;

View File

@@ -1,132 +0,0 @@
import { JSONObject } from "@nteract/commutable";
import { outputToJS } from "@nteract/commutable/lib/v4";
import { actions, AppState, ContentRef, selectors } from "@nteract/core";
import IframeResizer from "iframe-resizer-react";
import Immutable from "immutable";
import postRobot from "post-robot";
import React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { CellOutputViewerProps } from "../../../../CellOutputViewer/CellOutputViewer";
// Adapted from https://github.com/nteract/nteract/blob/main/packages/stateful-components/src/outputs/index.tsx
// to add support for sandboxing using <iframe>
interface ComponentProps {
id: string;
contentRef: ContentRef;
}
interface StateProps {
hidden: boolean;
expanded: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
outputs: Immutable.List<any>;
}
interface DispatchProps {
onMetadataChange?: (metadata: JSONObject, mediaType: string, index?: number) => void;
}
export class SandboxOutputs extends React.PureComponent<ComponentProps & StateProps & DispatchProps> {
private childWindow: Window;
render(): JSX.Element {
// Using min-width to set the width of the iFrame, works around an issue in iOS that can prevent the iFrame from sizing correctly.
return (
<IframeResizer
checkOrigin={false}
loading="lazy"
heightCalculationMethod="taggedElement"
onLoad={(event) => this.handleFrameLoad(event)}
src="./cellOutputViewer.html"
style={{ height: "1px", width: "1px", minWidth: "100%", border: "none" }}
sandbox="allow-downloads allow-popups allow-forms allow-pointer-lock allow-scripts allow-popups-to-escape-sandbox"
/>
);
}
handleFrameLoad(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void {
this.childWindow = (event.target as HTMLIFrameElement).contentWindow;
this.sendPropsToFrame();
}
sendPropsToFrame(): void {
if (!this.childWindow) {
return;
}
const props: CellOutputViewerProps = {
id: this.props.id,
contentRef: this.props.contentRef,
hidden: this.props.hidden,
expanded: this.props.expanded,
outputs: this.props.outputs.toArray().map((output) => outputToJS(output)),
onMetadataChange: this.props.onMetadataChange,
};
postRobot.send(this.childWindow, "props", props);
}
componentDidMount(): void {
this.sendPropsToFrame();
}
componentDidUpdate(): void {
this.sendPropsToFrame();
}
}
export const makeMapStateToProps = (
initialState: AppState,
ownProps: ComponentProps
): ((state: AppState) => StateProps) => {
const mapStateToProps = (state: AppState): StateProps => {
let outputs = Immutable.List();
let hidden = false;
let expanded = false;
const { contentRef, id } = ownProps;
const model = selectors.model(state, { contentRef });
if (model && model.type === "notebook") {
const cell = selectors.notebook.cellById(model, { id });
if (cell) {
outputs = cell.get("outputs", Immutable.List());
hidden = cell.cell_type === "code" && cell.getIn(["metadata", "jupyter", "outputs_hidden"]);
expanded = cell.cell_type === "code" && cell.getIn(["metadata", "collapsed"]) === false;
}
}
return { outputs, hidden, expanded };
};
return mapStateToProps;
};
export const makeMapDispatchToProps = (
initialDispath: Dispatch,
ownProps: ComponentProps
): ((dispatch: Dispatch) => DispatchProps) => {
const { id, contentRef } = ownProps;
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
onMetadataChange: (metadata: JSONObject, mediaType: string, index?: number) => {
dispatch(
actions.updateOutputMetadata({
id,
contentRef,
metadata,
index: index || 0,
mediaType,
})
);
},
};
};
return mapDispatchToProps;
};
export default connect<StateProps, DispatchProps, ComponentProps, AppState>(
makeMapStateToProps,
makeMapDispatchToProps
)(SandboxOutputs);

View File

@@ -1,10 +0,0 @@
.shemaAnalyzerComponent {
width: 100%;
height: 100%;
overflow-y: auto;
}
.schemaAnalyzerCard {
max-width: 4096px;
width: 100%;
}

View File

@@ -1,238 +0,0 @@
import { ImmutableOutput } from "@nteract/commutable";
import { actions, AppState, ContentRef, KernelRef, selectors } from "@nteract/core";
import { KernelOutputError, Output, StreamText } from "@nteract/outputs";
import TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media";
import { Card } from "@uifabric/react-cards";
import Immutable from "immutable";
import { FontIcon, PrimaryButton, Spinner, SpinnerSize, Stack, Text, TextField } from "office-ui-fabric-react";
import * as React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import loadTransform from "../NotebookComponent/loadTransform";
import "./SchemaAnalyzerComponent.less";
interface SchemaAnalyzerComponentPureProps {
contentRef: ContentRef;
kernelRef: KernelRef;
databaseId: string;
collectionId: string;
}
interface SchemaAnalyzerComponentDispatchProps {
runCell: (contentRef: ContentRef, cellId: string) => void;
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => void;
updateCell: (text: string, id: string, contentRef: ContentRef) => void;
}
type OutputType = "rich" | "json";
interface SchemaAnalyzerComponentState {
outputType: OutputType;
filter?: string;
isFiltering: boolean;
}
type SchemaAnalyzerComponentProps = SchemaAnalyzerComponentPureProps &
StateProps &
SchemaAnalyzerComponentDispatchProps;
export class SchemaAnalyzerComponent extends React.Component<
SchemaAnalyzerComponentProps,
SchemaAnalyzerComponentState
> {
constructor(props: SchemaAnalyzerComponentProps) {
super(props);
this.state = {
outputType: "rich",
isFiltering: false,
};
}
componentDidMount(): void {
loadTransform(this.props);
}
private onFilterTextFieldChange = (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => {
this.setState({
filter: newValue,
});
};
private onAnalyzeButtonClick = () => {
const query = {
command: "listSchema",
database: this.props.databaseId,
collection: this.props.collectionId,
outputType: this.state.outputType,
filter: this.state.filter,
};
if (this.state.filter) {
this.setState({
isFiltering: true,
});
}
this.props.updateCell(JSON.stringify(query), this.props.firstCellId, this.props.contentRef);
this.props.runCell(this.props.contentRef, this.props.firstCellId);
};
render(): JSX.Element {
const { firstCellId: id, contentRef, kernelStatus, outputs } = this.props;
if (!id) {
return <></>;
}
const isKernelBusy = kernelStatus === "busy";
const isKernelIdle = kernelStatus === "idle";
const showSchemaOutput = isKernelIdle && outputs.size > 0;
return (
<Stack className="schemaAnalyzerComponent" horizontalAlign="center" tokens={{ childrenGap: 20, padding: 20 }}>
<Stack.Item grow styles={{ root: { display: "contents" } }}>
<Stack horizontal tokens={{ childrenGap: 20 }} styles={{ root: { width: "100%" } }}>
<Stack.Item grow align="end">
<TextField
value={this.state.filter}
onChange={this.onFilterTextFieldChange}
label="Filter"
placeholder="{ field: 'value' }"
disabled={!isKernelIdle}
/>
</Stack.Item>
<Stack.Item align="end">
<PrimaryButton
text={isKernelBusy ? "Analyzing..." : "Analyze"}
onClick={this.onAnalyzeButtonClick}
disabled={!isKernelIdle}
/>
</Stack.Item>
</Stack>
</Stack.Item>
{showSchemaOutput ? (
outputs.map((output, index) => (
<Card className="schemaAnalyzerCard" key={index}>
<Card.Item tokens={{ padding: 10 }}>
<Output output={output}>
<TransformMedia output_type={"display_data"} id={id} contentRef={contentRef} />
<TransformMedia output_type={"execute_result"} id={id} contentRef={contentRef} />
<KernelOutputError />
<StreamText />
</Output>
</Card.Item>
</Card>
))
) : this.state.isFiltering ? (
<Stack.Item>
{isKernelBusy && <Spinner styles={{ root: { marginTop: 40 } }} size={SpinnerSize.large} />}
</Stack.Item>
) : (
<>
<Stack.Item>
<FontIcon iconName="Chart" style={{ fontSize: 100, color: "#43B1E5", marginTop: 40 }} />
</Stack.Item>
<Stack.Item>
<Text variant="xxLarge">Explore your schema</Text>
</Stack.Item>
<Stack.Item>
<Text variant="large">
Quickly visualize your schema to infer the frequency, types and ranges of fields in your data set.
</Text>
</Stack.Item>
<Stack.Item>
<PrimaryButton
styles={{ root: { fontSize: 18, padding: 30 } }}
text={isKernelBusy ? "Analyzing..." : "Analyze Schema"}
onClick={this.onAnalyzeButtonClick}
disabled={kernelStatus !== "idle"}
/>
</Stack.Item>
<Stack.Item>{isKernelBusy && <Spinner size={SpinnerSize.large} />}</Stack.Item>
</>
)}
</Stack>
);
}
}
interface StateProps {
firstCellId: string;
kernelStatus: string;
outputs: Immutable.List<ImmutableOutput>;
}
interface InitialProps {
kernelRef: string;
contentRef: string;
}
// Redux
const makeMapStateToProps = (state: AppState, initialProps: InitialProps) => {
const { kernelRef, contentRef } = initialProps;
const mapStateToProps = (state: AppState) => {
let kernelStatus;
let firstCellId;
let outputs;
const kernel = selectors.kernel(state, { kernelRef });
if (kernel) {
kernelStatus = kernel.status;
}
const content = selectors.content(state, { contentRef });
if (content?.type === "notebook") {
const cellOrder = selectors.notebook.cellOrder(content.model);
if (cellOrder.size > 0) {
firstCellId = cellOrder.first() as string;
const model = selectors.model(state, { contentRef });
if (model && model.type === "notebook") {
const cell = selectors.notebook.cellById(model, { id: firstCellId });
if (cell) {
outputs = cell.get("outputs", Immutable.List());
}
}
}
}
return {
firstCellId,
kernelStatus,
outputs,
};
};
return mapStateToProps;
};
const makeMapDispatchToProps = () => {
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => {
return dispatch(
actions.addTransform({
mediaType: transform.MIMETYPE,
component: transform,
})
);
},
runCell: (contentRef: ContentRef, cellId: string) => {
return dispatch(
actions.executeCell({
contentRef,
id: cellId,
})
);
},
updateCell: (text: string, id: string, contentRef: ContentRef) => {
dispatch(actions.updateCellSource({ id, contentRef, value: text }));
},
};
};
return mapDispatchToProps;
};
export default connect(makeMapStateToProps, makeMapDispatchToProps)(SchemaAnalyzerComponent);

View File

@@ -1,88 +0,0 @@
import { Notebook } from "@nteract/commutable";
import { actions, createContentRef, createKernelRef, IContent, KernelRef } from "@nteract/core";
import * as React from "react";
import { Provider } from "react-redux";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import {
NotebookComponentBootstrapper,
NotebookComponentBootstrapperOptions,
} from "../NotebookComponent/NotebookComponentBootstrapper";
import SchemaAnalyzerComponent from "./SchemaAnalyzerComponent";
export class SchemaAnalyzerComponentAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
public parameters: unknown;
private kernelRef: KernelRef;
constructor(options: NotebookComponentBootstrapperOptions, private databaseId: string, private collectionId: string) {
super(options);
if (!this.contentRef) {
this.contentRef = createContentRef();
this.kernelRef = createKernelRef();
const notebook: Notebook = {
cells: [
{
cell_type: "code",
metadata: {},
execution_count: 0,
outputs: [],
source: "",
},
],
metadata: {
kernelspec: {
displayName: "Mongo",
language: "mongocli",
name: "mongo",
},
language_info: {
file_extension: "ipynb",
mimetype: "application/json",
name: "mongo",
version: "1.0",
},
},
nbformat: 4,
nbformat_minor: 4,
};
const model: IContent<"notebook"> = {
name: "schema-analyzer-component-notebook.ipynb",
path: "schema-analyzer-component-notebook.ipynb",
type: "notebook",
writable: true,
created: "",
last_modified: "",
mimetype: "application/x-ipynb+json",
content: notebook,
format: "json",
};
// Request fetching notebook content
this.getStore().dispatch(
actions.fetchContentFulfilled({
filepath: model.path,
model,
kernelRef: this.kernelRef,
contentRef: this.contentRef,
})
);
}
}
public renderComponent(): JSX.Element {
const props = {
contentRef: this.contentRef,
kernelRef: this.kernelRef,
databaseId: this.databaseId,
collectionId: this.collectionId,
};
return (
<Provider store={this.getStore()}>
<SchemaAnalyzerComponent {...props} />;
</Provider>
);
}
}

View File

@@ -1,10 +1,10 @@
import * as ko from "knockout"; import * as ko from "knockout";
import { ActionContracts } from "../Contracts/ExplorerContracts";
import * as ViewModels from "../Contracts/ViewModels";
import Explorer from "./Explorer";
import { handleOpenAction } from "./OpenActions"; import { handleOpenAction } from "./OpenActions";
import AddCollectionPane from "./Panes/AddCollectionPane"; import * as ViewModels from "../Contracts/ViewModels";
import { ActionContracts } from "../Contracts/ExplorerContracts";
import Explorer from "./Explorer";
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane"; import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
import AddCollectionPane from "./Panes/AddCollectionPane";
describe("OpenActions", () => { describe("OpenActions", () => {
describe("handleOpenAction", () => { describe("handleOpenAction", () => {
@@ -33,7 +33,6 @@ describe("OpenActions", () => {
collection.expandCollection = jest.fn(); collection.expandCollection = jest.fn();
collection.onDocumentDBDocumentsClick = jest.fn(); collection.onDocumentDBDocumentsClick = jest.fn();
collection.onMongoDBDocumentsClick = jest.fn(); collection.onMongoDBDocumentsClick = jest.fn();
collection.onSchemaAnalyzerClick = jest.fn();
collection.onTableEntitiesClick = jest.fn(); collection.onTableEntitiesClick = jest.fn();
collection.onGraphDocumentsClick = jest.fn(); collection.onGraphDocumentsClick = jest.fn();
collection.onNewQueryClick = jest.fn(); collection.onNewQueryClick = jest.fn();

View File

@@ -79,14 +79,6 @@ function openCollectionTab(
break; break;
} }
if (
action.tabKind === ActionContracts.TabKind.SchemaAnalyzer ||
(<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer]
) {
collection.onSchemaAnalyzerClick();
break;
}
if ( if (
action.tabKind === ActionContracts.TabKind.TableEntities || action.tabKind === ActionContracts.TabKind.TableEntities ||
(<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities] (<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities]

View File

@@ -1,8 +1,7 @@
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { DatabaseAccount } from "../../Contracts/DataModels";
import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer";
import AddCollectionPane from "./AddCollectionPane"; import AddCollectionPane from "./AddCollectionPane";
import Explorer from "../Explorer";
import { DatabaseAccount } from "../../Contracts/DataModels";
describe("Add Collection Pane", () => { describe("Add Collection Pane", () => {
describe("isValid()", () => { describe("isValid()", () => {
@@ -51,14 +50,7 @@ describe("Add Collection Pane", () => {
}); });
it("should be false if graph API and partition key is /id or /label", () => { it("should be false if graph API and partition key is /id or /label", () => {
updateUserContext({ explorer.defaultExperience(Constants.DefaultAccountExperience.Graph.toLowerCase());
databaseAccount: {
properties: {
capabilities: [{ name: "EnableGremlin" }],
},
} as DatabaseAccount,
});
const addCollectionPane = explorer.addCollectionPane as AddCollectionPane; const addCollectionPane = explorer.addCollectionPane as AddCollectionPane;
addCollectionPane.partitionKey("/id"); addCollectionPane.partitionKey("/id");
expect(addCollectionPane.isValid()).toBe(false); expect(addCollectionPane.isValid()).toBe(false);
@@ -68,13 +60,7 @@ describe("Add Collection Pane", () => {
}); });
it("should be true for any non-graph API with /id or /label partition key", () => { it("should be true for any non-graph API with /id or /label partition key", () => {
updateUserContext({ explorer.defaultExperience(Constants.DefaultAccountExperience.DocumentDB.toLowerCase());
databaseAccount: {
properties: {
capabilities: [{ name: "EnableCassandra" }],
},
} as DatabaseAccount,
});
const addCollectionPane = explorer.addCollectionPane as AddCollectionPane; const addCollectionPane = explorer.addCollectionPane as AddCollectionPane;
addCollectionPane.partitionKey("/id"); addCollectionPane.partitionKey("/id");

View File

@@ -127,13 +127,13 @@ export default class AddCollectionPane extends ContextualPaneBase {
}); });
this.partitionKey.extend({ rateLimit: 100 }); this.partitionKey.extend({ rateLimit: 100 });
this.partitionKeyPattern = ko.pureComputed(() => { this.partitionKeyPattern = ko.pureComputed(() => {
if (userContext.apiType === "Gremlin") { if (this.container && this.container.isPreferredApiGraph()) {
return "^/[^/]*"; return "^/[^/]*";
} }
return ".*"; return ".*";
}); });
this.partitionKeyTitle = ko.pureComputed(() => { this.partitionKeyTitle = ko.pureComputed(() => {
if (userContext.apiType === "Gremlin") { if (this.container && this.container.isPreferredApiGraph()) {
return "May not use composite partition key"; return "May not use composite partition key";
} }
return ""; return "";
@@ -331,7 +331,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
if (currentCollections >= maxCollections) { if (currentCollections >= maxCollections) {
let typeOfContainer = "collection"; let typeOfContainer = "collection";
if (userContext.apiType === "Gremlin" || userContext.apiType === "Tables") { if (this.container.isPreferredApiGraph() || this.container.isPreferredApiTable()) {
typeOfContainer = "container"; typeOfContainer = "container";
} }
@@ -368,7 +368,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return "e.g., address.zipCode"; return "e.g., address.zipCode";
} }
if (userContext.apiType === "Gremlin") { if (this.container && !!this.container.isPreferredApiGraph()) {
return "e.g., /address"; return "e.g., /address";
} }
@@ -384,15 +384,21 @@ export default class AddCollectionPane extends ContextualPaneBase {
}); });
this.uniqueKeysVisible = ko.pureComputed<boolean>(() => { this.uniqueKeysVisible = ko.pureComputed<boolean>(() => {
if (userContext.apiType === "SQL") { if (
return true; this.container == null ||
!!this.container.isPreferredApiMongoDB() ||
!!this.container.isPreferredApiTable() ||
!!this.container.isPreferredApiCassandra() ||
!!this.container.isPreferredApiGraph()
) {
return false;
} }
return false; return true;
}); });
this.partitionKeyVisible = ko.computed<boolean>(() => { this.partitionKeyVisible = ko.computed<boolean>(() => {
if (this.container == null || userContext.apiType === "Tables") { if (this.container == null || !!this.container.isPreferredApiTable()) {
return false; return false;
} }
@@ -585,7 +591,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return false; return false;
} }
if (userContext.apiType === "SQL") { if (this.container.isPreferredApiDocumentDB()) {
return true; return true;
} }
@@ -593,7 +599,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return true; return true;
} }
if (userContext.apiType === "Cassandra" && this.container.hasStorageAnalyticsAfecFeature()) { if (this.container.isPreferredApiCassandra() && this.container.hasStorageAnalyticsAfecFeature()) {
return true; return true;
} }
@@ -757,7 +763,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return; return;
} }
if (userContext.apiType === "Tables") { if (!!this.container.isPreferredApiTable()) {
// Table require fixed Database: TablesDB, and fixed Partition Key: /'$pk' // Table require fixed Database: TablesDB, and fixed Partition Key: /'$pk'
this.databaseId(SharedConstants.CollectionCreation.TablesAPIDefaultDatabase); this.databaseId(SharedConstants.CollectionCreation.TablesAPIDefaultDatabase);
this.partitionKey("/'$pk'"); this.partitionKey("/'$pk'");
@@ -917,10 +923,8 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.databaseId(""); this.databaseId("");
this.partitionKey(""); this.partitionKey("");
this.throughputSpendAck(false); this.throughputSpendAck(false);
if (!this.container.isServerlessEnabled()) { this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled());
this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled()); this.isSharedAutoPilotSelected(this.container.isAutoscaleDefaultEnabled());
this.isSharedAutoPilotSelected(this.container.isAutoscaleDefaultEnabled());
}
this.autoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); this.autoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
@@ -954,7 +958,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
} }
public isNonTableApi = (): boolean => { public isNonTableApi = (): boolean => {
return userContext.apiType !== "Tables"; return !this.container.isPreferredApiTable();
}; };
public isUnlimitedStorageSelected = (): boolean => { public isUnlimitedStorageSelected = (): boolean => {
@@ -1007,7 +1011,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return false; return false;
} }
if (userContext.apiType === "Gremlin" && (this.partitionKey() === "/id" || this.partitionKey() === "/label")) { if (this.container.isPreferredApiGraph() && (this.partitionKey() === "/id" || this.partitionKey() === "/label")) {
this.formErrors("/id and /label as partition keys are not allowed for graph."); this.formErrors("/id and /label as partition keys are not allowed for graph.");
return false; return false;
} }
@@ -1028,7 +1032,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
private _setFocus() { private _setFocus() {
// Autofocus is enabled on AddCollectionPane based on the preferred API // Autofocus is enabled on AddCollectionPane based on the preferred API
if (userContext.apiType === "Tables") { if (this.container.isPreferredApiTable()) {
const focusTableId = document.getElementById("containerId"); const focusTableId = document.getElementById("containerId");
focusTableId && focusTableId.focus(); focusTableId && focusTableId.focus();
return; return;

View File

@@ -62,21 +62,21 @@ export default class AddDatabasePane extends ContextualPaneBase {
this.databaseCreateNewShared = ko.observable<boolean>(this.getSharedThroughputDefault()); this.databaseCreateNewShared = ko.observable<boolean>(this.getSharedThroughputDefault());
this.databaseIdLabel = ko.computed<string>(() => this.databaseIdLabel = ko.computed<string>(() =>
userContext.apiType === "Cassandra" ? "Keyspace id" : "Database id" this.container.isPreferredApiCassandra() ? "Keyspace id" : "Database id"
); );
this.databaseIdPlaceHolder = ko.computed<string>(() => this.databaseIdPlaceHolder = ko.computed<string>(() =>
userContext.apiType === "Cassandra" ? "Type a new keyspace id" : "Type a new database id" this.container.isPreferredApiCassandra() ? "Type a new keyspace id" : "Type a new database id"
); );
this.databaseIdTooltipText = ko.computed<string>(() => { this.databaseIdTooltipText = ko.computed<string>(() => {
const isCassandraAccount: boolean = userContext.apiType === "Cassandra"; const isCassandraAccount: boolean = this.container.isPreferredApiCassandra();
return `A ${isCassandraAccount ? "keyspace" : "database"} is a logical container of one or more ${ return `A ${isCassandraAccount ? "keyspace" : "database"} is a logical container of one or more ${
isCassandraAccount ? "tables" : "collections" isCassandraAccount ? "tables" : "collections"
}`; }`;
}); });
this.databaseLevelThroughputTooltipText = ko.computed<string>(() => { this.databaseLevelThroughputTooltipText = ko.computed<string>(() => {
const isCassandraAccount: boolean = userContext.apiType === "Cassandra"; const isCassandraAccount: boolean = this.container.isPreferredApiCassandra();
const databaseLabel: string = isCassandraAccount ? "keyspace" : "database"; const databaseLabel: string = isCassandraAccount ? "keyspace" : "database";
const collectionsLabel: string = isCassandraAccount ? "tables" : "collections"; const collectionsLabel: string = isCassandraAccount ? "tables" : "collections";
return `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`; return `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`;

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Browse queries panel Should render Default properly 1`] = ` exports[`Browse queries panel Should render Default properly 1`] = `
<BrowseQueriesPane <BrowseQueriesPanel
closePanel={[Function]} closePanel={[Function]}
explorer={ explorer={
Object { Object {
@@ -54,5 +54,5 @@ exports[`Browse queries panel Should render Default properly 1`] = `
</QueriesGridComponent> </QueriesGridComponent>
</div> </div>
</div> </div>
</BrowseQueriesPane> </BrowseQueriesPanel>
`; `;

View File

@@ -4,13 +4,13 @@ import React from "react";
import { QueriesClient } from "../../../Common/QueriesClient"; import { QueriesClient } from "../../../Common/QueriesClient";
import { Query } from "../../../Contracts/DataModels"; import { Query } from "../../../Contracts/DataModels";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { BrowseQueriesPane } from "./BrowseQueriesPane"; import { BrowseQueriesPanel } from "./index";
describe("Browse queries panel", () => { describe("Browse queries panel", () => {
const fakeExplorer = {} as Explorer; const fakeExplorer = {} as Explorer;
fakeExplorer.canSaveQueries = ko.computed<boolean>(() => true); fakeExplorer.canSaveQueries = ko.computed<boolean>(() => true);
const fakeClientQuery = {} as QueriesClient; const fakeClientQuery = {} as QueriesClient;
const fakeQueryData = [] as Query[]; const fakeQueryData = {} as Query[];
fakeClientQuery.getQueries = async () => fakeQueryData; fakeClientQuery.getQueries = async () => fakeQueryData;
fakeExplorer.queriesClient = fakeClientQuery; fakeExplorer.queriesClient = fakeClientQuery;
const props = { const props = {
@@ -19,12 +19,12 @@ describe("Browse queries panel", () => {
}; };
it("Should render Default properly", () => { it("Should render Default properly", () => {
const wrapper = mount(<BrowseQueriesPane {...props} />); const wrapper = mount(<BrowseQueriesPanel {...props} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it("Should show empty view when query is empty []", () => { it("Should show empty view when query is empty []", () => {
const wrapper = mount(<BrowseQueriesPane {...props} />); const wrapper = mount(<BrowseQueriesPanel {...props} />);
expect(wrapper.exists("#emptyQueryBanner")).toBe(true); expect(wrapper.exists("#emptyQueryBanner")).toBe(true);
}); });
}); });

View File

@@ -13,15 +13,15 @@ import {
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import QueryTab from "../../Tabs/QueryTab"; import QueryTab from "../../Tabs/QueryTab";
interface BrowseQueriesPaneProps { interface BrowseQueriesPanelProps {
explorer: Explorer; explorer: Explorer;
closePanel: () => void; closePanel: () => void;
} }
export const BrowseQueriesPane: FunctionComponent<BrowseQueriesPaneProps> = ({ export const BrowseQueriesPanel: FunctionComponent<BrowseQueriesPanelProps> = ({
explorer, explorer,
closePanel, closePanel,
}: BrowseQueriesPaneProps): JSX.Element => { }: BrowseQueriesPanelProps): JSX.Element => {
const loadSavedQuery = (savedQuery: Query): void => { const loadSavedQuery = (savedQuery: Query): void => {
const selectedCollection: Collection = explorer && explorer.findSelectedCollection(); const selectedCollection: Collection = explorer && explorer.findSelectedCollection();
if (!selectedCollection) { if (!selectedCollection) {

View File

@@ -0,0 +1,194 @@
import ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import { JunoClient, IPinnedRepo } from "../../Juno/JunoClient";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
import { IDropdownOption } from "office-ui-fabric-react";
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
import { HttpStatusCodes } from "../../Common/Constants";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import { NotebookContentItemType, NotebookContentItem } from "../Notebook/NotebookContentItem";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { handleError, getErrorMessage } from "../../Common/ErrorHandlingUtils";
interface Location {
type: "MyNotebooks" | "GitHub";
// GitHub
owner?: string;
repo?: string;
branch?: string;
}
export class CopyNotebookPaneAdapter implements ReactAdapter {
private static readonly BranchNameWhiteSpace = " ";
parameters: ko.Observable<number>;
private isOpened: boolean;
private isExecuting: boolean;
private formError: string;
private formErrorDetail: string;
private name: string;
private content: string;
private pinnedRepos: IPinnedRepo[];
private selectedLocation: Location;
constructor(
private container: Explorer,
private junoClient: JunoClient,
private gitHubOAuthService: GitHubOAuthService
) {
this.parameters = ko.observable(Date.now());
this.reset();
this.triggerRender();
}
public renderComponent(): JSX.Element {
if (!this.isOpened) {
return undefined;
}
const genericPaneProps: GenericRightPaneProps = {
container: this.container,
formError: this.formError,
formErrorDetail: this.formErrorDetail,
id: "copynotebookpane",
isExecuting: this.isExecuting,
title: "Copy notebook",
submitButtonText: "OK",
onClose: () => this.close(),
onSubmit: () => this.submit(),
};
const copyNotebookPaneProps: CopyNotebookPaneProps = {
name: this.name,
pinnedRepos: this.pinnedRepos,
onDropDownChange: this.onDropDownChange,
};
return (
<GenericRightPaneComponent {...genericPaneProps}>
<CopyNotebookPaneComponent {...copyNotebookPaneProps} />
</GenericRightPaneComponent>
);
}
public triggerRender(): void {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}
public async open(name: string, content: string): Promise<void> {
this.name = name;
this.content = content;
this.isOpened = true;
this.triggerRender();
if (this.gitHubOAuthService.isLoggedIn()) {
const response = await this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
handleError(`Received HTTP ${response.status} when fetching pinned repos`, "CopyNotebookPaneAdapter/submit");
}
if (response.data?.length > 0) {
this.pinnedRepos = response.data;
this.triggerRender();
}
}
}
public close(): void {
this.reset();
this.triggerRender();
}
public async submit(): Promise<void> {
let destination: string = this.selectedLocation?.type;
let clearMessage: () => void;
this.isExecuting = true;
this.triggerRender();
try {
if (!this.selectedLocation) {
throw new Error(`No location selected`);
}
if (this.selectedLocation.type === "GitHub") {
destination = `${destination} - ${GitHubUtils.toRepoFullName(
this.selectedLocation.owner,
this.selectedLocation.repo
)} - ${this.selectedLocation.branch}`;
}
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${this.name} to ${destination}`);
const notebookContentItem = await this.copyNotebook(this.selectedLocation);
if (!notebookContentItem) {
throw new Error(`Failed to upload ${this.name}`);
}
NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${this.name} to ${destination}`);
} catch (error) {
const errorMessage = getErrorMessage(error);
this.formError = `Failed to copy ${this.name} to ${destination}`;
this.formErrorDetail = `${errorMessage}`;
handleError(errorMessage, "CopyNotebookPaneAdapter/submit", this.formError);
return;
} finally {
clearMessage && clearMessage();
this.isExecuting = false;
this.triggerRender();
}
this.close();
}
private copyNotebook = async (location: Location): Promise<NotebookContentItem> => {
let parent: NotebookContentItem;
switch (location.type) {
case "MyNotebooks":
parent = {
name: ResourceTreeAdapter.MyNotebooksTitle,
path: this.container.getNotebookBasePath(),
type: NotebookContentItemType.Directory,
};
break;
case "GitHub":
parent = {
name: ResourceTreeAdapter.GitHubReposTitle,
path: GitHubUtils.toContentUri(
this.selectedLocation.owner,
this.selectedLocation.repo,
this.selectedLocation.branch,
""
),
type: NotebookContentItemType.Directory,
};
break;
default:
throw new Error(`Unsupported location type ${location.type}`);
}
return this.container.uploadFile(this.name, this.content, parent);
};
private onDropDownChange = (_: React.FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {
this.selectedLocation = option?.data;
};
private reset = (): void => {
this.isOpened = false;
this.isExecuting = false;
this.formError = undefined;
this.formErrorDetail = undefined;
this.name = undefined;
this.content = undefined;
this.pinnedRepos = undefined;
this.selectedLocation = undefined;
};
}

View File

@@ -1,156 +0,0 @@
import { IDropdownOption } from "office-ui-fabric-react";
import React, { FormEvent, FunctionComponent, useEffect, useState } from "react";
import { HttpStatusCodes } from "../../../Common/Constants";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient";
import * as GitHubUtils from "../../../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem";
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
import {
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
interface Location {
type: "MyNotebooks" | "GitHub";
// GitHub
owner?: string;
repo?: string;
branch?: string;
}
export interface CopyNotebookPanelProps {
name: string;
content: string;
container: Explorer;
junoClient: JunoClient;
gitHubOAuthService: GitHubOAuthService;
closePanel: () => void;
}
export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
name,
content,
container,
junoClient,
gitHubOAuthService,
closePanel,
}: CopyNotebookPanelProps) => {
const [isExecuting, setIsExecuting] = useState<boolean>();
const [formError, setFormError] = useState<string>("");
const [formErrorDetail, setFormErrorDetail] = useState<string>("");
const [pinnedRepos, setPinnedRepos] = useState<IPinnedRepo[]>();
const [selectedLocation, setSelectedLocation] = useState<Location>();
useEffect(() => {
open();
}, []);
const open = async (): Promise<void> => {
if (gitHubOAuthService.isLoggedIn()) {
const response = await junoClient.getPinnedRepos(gitHubOAuthService.getTokenObservable()()?.scope);
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
handleError(`Received HTTP ${response.status} when fetching pinned repos`, "CopyNotebookPaneAdapter/submit");
}
if (response.data?.length > 0) {
setPinnedRepos(response.data);
}
}
};
const submit = async (): Promise<void> => {
let destination: string = selectedLocation?.type;
let clearMessage: () => void;
setIsExecuting(true);
try {
if (!selectedLocation) {
throw new Error(`No location selected`);
}
if (selectedLocation.type === "GitHub") {
destination = `${destination} - ${GitHubUtils.toRepoFullName(
selectedLocation.owner,
selectedLocation.repo
)} - ${selectedLocation.branch}`;
}
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`);
const notebookContentItem = await copyNotebook(selectedLocation);
if (!notebookContentItem) {
throw new Error(`Failed to upload ${name}`);
}
NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${name} to ${destination}`);
closePanel();
} catch (error) {
const errorMessage = getErrorMessage(error);
setFormError(`Failed to copy ${name} to ${destination}`);
setFormErrorDetail(`${errorMessage}`);
handleError(errorMessage, "CopyNotebookPaneAdapter/submit", formError);
} finally {
clearMessage && clearMessage();
setIsExecuting(false);
}
};
const copyNotebook = async (location: Location): Promise<NotebookContentItem> => {
let parent: NotebookContentItem;
switch (location.type) {
case "MyNotebooks":
parent = {
name: ResourceTreeAdapter.MyNotebooksTitle,
path: container.getNotebookBasePath(),
type: NotebookContentItemType.Directory,
};
break;
case "GitHub":
parent = {
name: ResourceTreeAdapter.GitHubReposTitle,
path: GitHubUtils.toContentUri(selectedLocation.owner, selectedLocation.repo, selectedLocation.branch, ""),
type: NotebookContentItemType.Directory,
};
break;
default:
throw new Error(`Unsupported location type ${location.type}`);
}
return container.uploadFile(name, content, parent);
};
const onDropDownChange = (_: FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {
setSelectedLocation(option?.data);
};
const genericPaneProps: GenericRightPaneProps = {
container,
formError,
formErrorDetail,
id: "copynotebookpane",
isExecuting: isExecuting,
title: "Copy notebook",
submitButtonText: "OK",
onClose: closePanel,
onSubmit: () => submit(),
};
const copyNotebookPaneProps: CopyNotebookPaneProps = {
name,
pinnedRepos,
onDropDownChange: onDropDownChange,
};
return (
<GenericRightPaneComponent {...genericPaneProps}>
<CopyNotebookPaneComponent {...copyNotebookPaneProps} />
</GenericRightPaneComponent>
);
};

View File

@@ -1,18 +1,18 @@
import * as GitHubUtils from "../../Utils/GitHubUtils";
import * as React from "react";
import { IPinnedRepo } from "../../Juno/JunoClient";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { import {
Stack,
Label,
Text,
Dropdown, Dropdown,
IDropdownOption,
IDropdownProps, IDropdownProps,
IDropdownOption,
SelectableOptionMenuItemType,
IRenderFunction, IRenderFunction,
ISelectableOption, ISelectableOption,
Label,
SelectableOptionMenuItemType,
Stack,
Text,
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import React, { FormEvent, FunctionComponent } from "react";
import { IPinnedRepo } from "../../../Juno/JunoClient";
import * as GitHubUtils from "../../../Utils/GitHubUtils";
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
interface Location { interface Location {
type: "MyNotebooks" | "GitHub"; type: "MyNotebooks" | "GitHub";
@@ -26,25 +26,46 @@ interface Location {
export interface CopyNotebookPaneProps { export interface CopyNotebookPaneProps {
name: string; name: string;
pinnedRepos: IPinnedRepo[]; pinnedRepos: IPinnedRepo[];
onDropDownChange: (_: FormEvent<HTMLDivElement>, option?: IDropdownOption) => void; onDropDownChange: (_: React.FormEvent<HTMLDivElement>, option?: IDropdownOption) => void;
} }
export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps> = ({ export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneProps> {
name, private static readonly BranchNameWhiteSpace = " ";
pinnedRepos,
onDropDownChange,
}: CopyNotebookPaneProps) => {
const BranchNameWhiteSpace = " ";
const onRenderDropDownTitle: IRenderFunction<IDropdownOption[]> = (options: IDropdownOption[]): JSX.Element => { public render(): JSX.Element {
const dropDownProps: IDropdownProps = {
label: "Location",
ariaLabel: "Location",
placeholder: "Select an option",
onRenderTitle: this.onRenderDropDownTitle,
onRenderOption: this.onRenderDropDownOption,
options: this.getDropDownOptions(),
onChange: this.props.onDropDownChange,
};
return (
<div className="paneMainContent">
<Stack tokens={{ childrenGap: 10 }}>
<Stack.Item>
<Label htmlFor="notebookName">Name</Label>
<Text id="notebookName">{this.props.name}</Text>
</Stack.Item>
<Dropdown {...dropDownProps} />
</Stack>
</div>
);
}
private onRenderDropDownTitle: IRenderFunction<IDropdownOption[]> = (options: IDropdownOption[]): JSX.Element => {
return <span>{options.length && options[0].title}</span>; return <span>{options.length && options[0].title}</span>;
}; };
const onRenderDropDownOption: IRenderFunction<ISelectableOption> = (option: ISelectableOption): JSX.Element => { private onRenderDropDownOption: IRenderFunction<ISelectableOption> = (option: ISelectableOption): JSX.Element => {
return <span style={{ whiteSpace: "pre-wrap" }}>{option.text}</span>; return <span style={{ whiteSpace: "pre-wrap" }}>{option.text}</span>;
}; };
const getDropDownOptions = (): IDropdownOption[] => { private getDropDownOptions = (): IDropdownOption[] => {
const options: IDropdownOption[] = []; const options: IDropdownOption[] = [];
options.push({ options.push({
@@ -56,7 +77,7 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
} as Location, } as Location,
}); });
if (pinnedRepos && pinnedRepos.length > 0) { if (this.props.pinnedRepos && this.props.pinnedRepos.length > 0) {
options.push({ options.push({
key: "GitHub-Header-Divider", key: "GitHub-Header-Divider",
text: undefined, text: undefined,
@@ -69,7 +90,7 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
itemType: SelectableOptionMenuItemType.Header, itemType: SelectableOptionMenuItemType.Header,
}); });
pinnedRepos.forEach((pinnedRepo) => { this.props.pinnedRepos.forEach((pinnedRepo) => {
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name); const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
options.push({ options.push({
key: `GitHub-Repo-${repoFullName}`, key: `GitHub-Repo-${repoFullName}`,
@@ -80,7 +101,7 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
pinnedRepo.branches.forEach((branch) => pinnedRepo.branches.forEach((branch) =>
options.push({ options.push({
key: `GitHub-Repo-${repoFullName}-${branch.name}`, key: `GitHub-Repo-${repoFullName}-${branch.name}`,
text: `${BranchNameWhiteSpace}${branch.name}`, text: `${CopyNotebookPaneComponent.BranchNameWhiteSpace}${branch.name}`,
title: `${repoFullName} - ${branch.name}`, title: `${repoFullName} - ${branch.name}`,
data: { data: {
type: "GitHub", type: "GitHub",
@@ -95,26 +116,4 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
return options; return options;
}; };
const dropDownProps: IDropdownProps = { }
label: "Location",
ariaLabel: "Location",
placeholder: "Select an option",
onRenderTitle: onRenderDropDownTitle,
onRenderOption: onRenderDropDownOption,
options: getDropDownOptions(),
onChange: onDropDownChange,
};
return (
<div className="paneMainContent">
<Stack tokens={{ childrenGap: 10 }}>
<Stack.Item>
<Label htmlFor="notebookName">Name</Label>
<Text id="notebookName">{name}</Text>
</Stack.Item>
<Dropdown {...dropDownProps} />
</Stack>
</div>
);
};

View File

@@ -0,0 +1,108 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" id="deletecollectionconfirmationpane">
<!-- Delete Collection Confirmation form - Start -->
<div class="contextual-pane-in">
<form
class="paneContentContainer"
data-bind="
submit: submit"
>
<!-- Delete Collection Confirmation header - Start -->
<div class="firstdivbg headerline">
<span role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="
click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Delete Collection Confirmation header - End -->
<div class="warningErrorContainer" data-bind="visible: !formErrors()">
<div class="warningErrorContent">
<span><img class="paneWarningIcon" src="/warning.svg" alt="Warning" /></span>
<span class="warningErrorDetailsLinkContainer">
Warning! The action you are about to take cannot be undone. Continuing will permanently delete this
resource and all of its children resources.
</span>
</div>
</div>
<!-- Delete Collection Confirmation errors - Start -->
<div
class="warningErrorContainer"
aria-live="assertive"
data-bind="
visible: formErrors() && formErrors() !== ''"
>
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/error_red.svg" alt="Error" /></span>
<span class="warningErrorDetailsLinkContainer">
<span class="formErrors" data-bind="text: formErrors, attr: { title: formErrors }"></span>
<a class="errorLink" role="link" data-bind="click: showErrorDetails">More details</a>
</span>
</div>
</div>
<!-- Delete Collection Confirmation errors - End -->
<!-- Delete Collection Confirmation inputs - Start -->
<div class="paneMainContent">
<div>
<span class="mandatoryStar">*</span> <span data-bind="text: collectionIdConfirmationText"></span>
<p>
<input
type="text"
data-test="confirmCollectionId"
name="collectionIdConfirmation"
required
class="collid"
data-bind="value: collectionIdConfirmation, hasFocus: firstFieldHasFocus, attr: { 'aria-label': collectionIdConfirmationText }"
/>
</p>
</div>
<div data-bind="visible: recordDeleteFeedback">
<div>Help us improve Azure Cosmos DB!</div>
<div>What is the reason why you are deleting this container?</div>
<p>
<textarea
type="text"
data-test="containerDeleteFeedback"
name="containerDeleteFeedback"
rows="3"
cols="53"
class="collid"
maxlength="512"
data-bind="value: containerDeleteFeedback"
aria-label="Help us improve Azure Cosmos DB! What is the reason why you are deleting this container?"
>
</textarea>
</p>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input type="submit" data-test="deleteCollection" value="OK" class="btncreatecoll1" />
</div>
</div>
<!-- Delete Collection Confirmation inputs - End -->
</form>
</div>
<!-- Delete Collection Confirmation form - End -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -1,18 +1,19 @@
jest.mock("../../../Common/dataAccess/deleteCollection"); jest.mock("../../Common/dataAccess/deleteCollection");
jest.mock("../../../Shared/Telemetry/TelemetryProcessor"); jest.mock("../../Shared/Telemetry/TelemetryProcessor");
import { mount, ReactWrapper, shallow } from "enzyme";
import * as ko from "knockout"; import * as ko from "knockout";
import { ApiKind, DatabaseAccount } from "../../Contracts/DataModels";
import { Collection, Database } from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { mount, ReactWrapper, shallow } from "enzyme";
import React from "react"; import React from "react";
import { deleteCollection } from "../../../Common/dataAccess/deleteCollection"; import DeleteFeedback from "../../Common/DeleteFeedback";
import DeleteFeedback from "../../../Common/DeleteFeedback"; import Explorer from "../Explorer";
import { ApiKind, DatabaseAccount } from "../../../Contracts/DataModels"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Collection, Database, TreeNode } from "../../../Contracts/ViewModels"; import { TreeNode } from "../../Contracts/ViewModels";
import { DefaultAccountExperienceType } from "../../../DefaultAccountExperienceType"; import { deleteCollection } from "../../Common/dataAccess/deleteCollection";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { DeleteCollectionConfirmationPanel } from "./DeleteCollectionConfirmationPanel";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { updateUserContext } from "../../../UserContext"; import { updateUserContext } from "../../UserContext";
import Explorer from "../../Explorer";
import { DeleteCollectionConfirmationPane } from "./DeleteCollectionConfirmationPane";
describe("Delete Collection Confirmation Pane", () => { describe("Delete Collection Confirmation Pane", () => {
describe("Explorer.isLastCollection()", () => { describe("Explorer.isLastCollection()", () => {
@@ -63,9 +64,9 @@ describe("Delete Collection Confirmation Pane", () => {
const props = { const props = {
explorer: fakeExplorer, explorer: fakeExplorer,
closePanel: (): void => undefined, closePanel: (): void => undefined,
collectionName: "container", openNotificationConsole: (): void => undefined,
}; };
const wrapper = shallow(<DeleteCollectionConfirmationPane {...props} />); const wrapper = shallow(<DeleteCollectionConfirmationPanel {...props} />);
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true); expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true);
props.explorer.isLastCollection = () => true; props.explorer.isLastCollection = () => true;
@@ -117,9 +118,9 @@ describe("Delete Collection Confirmation Pane", () => {
const props = { const props = {
explorer: fakeExplorer, explorer: fakeExplorer,
closePanel: (): void => undefined, closePanel: (): void => undefined,
collectionName: "container", openNotificationConsole: (): void => undefined,
}; };
wrapper = mount(<DeleteCollectionConfirmationPane {...props} />); wrapper = mount(<DeleteCollectionConfirmationPanel {...props} />);
}); });
it("should call delete collection", () => { it("should call delete collection", () => {
@@ -131,8 +132,8 @@ describe("Delete Collection Confirmation Pane", () => {
.hostNodes() .hostNodes()
.simulate("change", { target: { value: selectedCollectionId } }); .simulate("change", { target: { value: selectedCollectionId } });
expect(wrapper.exists(".genericPaneSubmitBtn")).toBe(true); expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
wrapper.find(".genericPaneSubmitBtn").hostNodes().simulate("click"); wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit");
expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId); expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId);
wrapper.unmount(); wrapper.unmount();
@@ -152,8 +153,8 @@ describe("Delete Collection Confirmation Pane", () => {
.hostNodes() .hostNodes()
.simulate("change", { target: { value: feedbackText } }); .simulate("change", { target: { value: feedbackText } });
expect(wrapper.exists(".genericPaneSubmitBtn")).toBe(true); expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
wrapper.find(".genericPaneSubmitBtn").hostNodes().simulate("click"); wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit");
expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId); expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId);
const deleteFeedback = new DeleteFeedback( const deleteFeedback = new DeleteFeedback(

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