diff --git a/.eslintignore b/.eslintignore index a2012a696..a3dd4972d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -45,7 +45,6 @@ src/Definitions/jquery.d.ts src/Definitions/plotly.js-cartesian-dist.d-min.ts src/Definitions/png.d.ts src/Definitions/svg.d.ts -src/Definitions/worker.d.ts src/Explorer/ComponentRegisterer.test.ts src/Explorer/ComponentRegisterer.ts src/Explorer/ContextMenuButtonFactory.ts @@ -118,8 +117,6 @@ src/Explorer/Panes/AddCollectionPane.ts src/Explorer/Panes/BrowseQueriesPane.ts src/Explorer/Panes/CassandraAddCollectionPane.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.ts src/Explorer/Panes/GraphStylingPane.ts @@ -132,7 +129,6 @@ src/Explorer/Panes/SwitchDirectoryPane.ts src/Explorer/Panes/Tables/AddTableEntityPane.ts src/Explorer/Panes/Tables/EditTableEntityPane.ts src/Explorer/Panes/Tables/EntityPropertyViewModel.ts -src/Explorer/Panes/Tables/QuerySelectPane.ts src/Explorer/Panes/Tables/TableEntityPane.ts src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts @@ -248,7 +244,6 @@ src/Utils/QueryUtils.test.ts src/applyExplorerBindings.ts src/global.d.ts src/setupTests.ts -src/workers/upload/index.ts src/Explorer/Controls/AccessibleElement/AccessibleElement.tsx src/Explorer/Controls/Accordion/AccordionComponent.tsx src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.test.tsx @@ -296,8 +291,6 @@ src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.t src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx src/Explorer/Menus/CommandBar/CommandBarUtil.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/NotebookComponentAdapter.tsx src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx diff --git a/.eslintrc.js b/.eslintrc.js index c1b67a931..800988b8f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,7 +3,7 @@ module.exports = { browser: true, es6: true, }, - plugins: ["@typescript-eslint", "no-null", "prefer-arrow"], + plugins: ["@typescript-eslint", "no-null", "prefer-arrow", "react-hooks"], extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], globals: { Atomics: "readonly", @@ -20,7 +20,7 @@ module.exports = { overrides: [ { files: ["**/*.tsx"], - extends: ["plugin:react/recommended"], // TODO: Add react-hooks + extends: ["plugin:react/recommended"], plugins: ["react"], }, { @@ -42,6 +42,8 @@ module.exports = { "prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }], eqeqeq: "error", "react/display-name": "off", + "react-hooks/rules-of-hooks": "warn", // TODO: error + "react-hooks/exhaustive-deps": "warn", // TODO: error "no-restricted-syntax": [ "error", { diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..d2f7952c6 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1 @@ +[Preview this branch](https://cosmos-explorer-preview.azurewebsites.net/pull/EDIT_THIS_NUMBER_IN_THE_PR_DESCRIPTION?feature.someFeatureFlagYouMightNeed=true) diff --git a/jest.config.js b/jest.config.js index ca967e8b5..b1e027c3f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -69,7 +69,6 @@ module.exports = { moduleNameMapper: { "^.*[.](svg|png|gif|less|css)$": "/mockModule", "@nteract/stateful-components/(.*)$": "/mockModule", - "worker-loader": "/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 "^dnd-core$": "dnd-core/dist/cjs", "^react-dnd$": "react-dnd/dist/cjs", diff --git a/package-lock.json b/package-lock.json index b12e81ffb..fa3d053ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -301,27 +301,48 @@ "node": ">=8.0.0" } }, - "node_modules/@azure/core-tracing/node_modules/tslib": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", - "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" - }, - "node_modules/@azure/cosmos": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.9.0.tgz", - "integrity": "sha512-SA+QB54I8Dvg/ZolHpsEDLK/sbSB9sFmSU1ElnMTFw88TVik+LYHq4o/srU2TY6Gr1BketjPmgLVEqrmnRvjkw==", - "dependencies": { - "@types/debug": "^4.1.4", + "@azure/cosmos": { + "version": "3.10.5", + "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.10.5.tgz", + "integrity": "sha512-if1uApYNjNXzB+reNFvzEBHvinxdQOzU8fni9e9Fs9jcPv9m76t2pzmYJNrxxCiFLP0vbNr/QCfQzIPQVw6v/A==", + "requires": { + "@azure/core-auth": "^1.2.0", "debug": "^4.1.1", "fast-json-stable-stringify": "^2.0.0", "jsbi": "^3.1.3", - "node-abort-controller": "^1.0.4", + "node-abort-controller": "^1.2.0", "node-fetch": "^2.6.0", - "os-name": "^3.1.0", "priorityqueuejs": "^1.0.0", "semaphore": "^1.0.5", "tslib": "^2.0.0", - "uuid": "^8.1.0" + "universal-user-agent": "^6.0.0", + "uuid": "^8.3.0" + }, + "dependencies": { + "@azure/core-auth": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.3.0.tgz", + "integrity": "sha512-kSDSZBL6c0CYdhb+7KuutnKGf2geeT+bCJAgccB0DD7wmNJSsQPcF7TcuoZX83B7VK4tLz/u+8sOO/CnCsYp8A==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "tslib": "^2.0.0" + } + }, + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" + }, + "universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + } } }, "node_modules/@azure/cosmos-language-service": { @@ -2109,11 +2130,57 @@ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + } + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { @@ -2411,8 +2478,94 @@ "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", "dev": true, "dependencies": { - "@types/istanbul-lib-coverage": "*", - "@types/istanbul-lib-report": "*" + "@types/react": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.3.tgz", + "integrity": "sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg==", + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "csstype": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.7.tgz", + "integrity": "sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g==" + }, + "htmlparser2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz", + "integrity": "sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^3.0.0", + "domutils": "^2.0.0", + "entities": "^2.0.0" + } + }, + "postcss": { + "version": "7.0.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", + "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "react": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.1.tgz", + "integrity": "sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "react-dom": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz", + "integrity": "sha512-6eV150oJZ9U2t9svnsspTMrWNyHc6chX0KzDeAOXftRa8bNeOKTTfCJ7KorIwenkHd2xqVTBTCZd79yk/lx/Ug==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "scheduler": "^0.20.1" + } + }, + "sanitize-html": { + "version": "1.27.5", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.27.5.tgz", + "integrity": "sha512-M4M5iXDAUEcZKLXkmk90zSYWEtk5NH3JmojQxKxV371fnMh+x9t1rqdmXaGoyEHw3z/X/8vnFhKjGL5xFGOJ3A==", + "requires": { + "htmlparser2": "^4.1.0", + "lodash": "^4.17.15", + "parse-srcset": "^1.0.2", + "postcss": "^7.0.27" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "requires": { + "has-flag": "^3.0.0" + } + } } }, "node_modules/@jest/core/node_modules/ansi-styles": { @@ -3095,13 +3248,14 @@ "node": ">=8" } }, - "node_modules/@jest/globals/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" + "@nteract/markdown": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@nteract/markdown/-/markdown-4.6.0.tgz", + "integrity": "sha512-DIeUYSRsFlHlIJ+bz/w1ln/KKtwqr9LsYZ+Uj/2t7mlmYxeEW0JRBa/E51QqCVdEepjAlpg2XqfqNgjkZiFfvw==", + "requires": { + "@nteract/mathjax": "^4.0.11", + "@nteract/presentational-components": "^3.3.11", + "react-markdown": "^4.0.0" }, "engines": { "node": ">=8" @@ -3113,36 +3267,19 @@ "integrity": "sha512-3jbd8pPDTuhYJ7vqiHXbSwTJQNavczPs+f1kRprRDxETeE3u6srJ+f0NPuwvOmk+lmunZzPkYWIFZDLHQPkviw==", "dev": true, "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^25.5.0", - "@jest/test-result": "^25.5.0", - "@jest/transform": "^25.5.1", - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.2", - "graceful-fs": "^4.2.4", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^4.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.2", - "jest-haste-map": "^25.5.1", - "jest-resolve": "^25.5.1", - "jest-util": "^25.5.0", - "jest-worker": "^25.5.0", - "slash": "^3.0.0", - "source-map": "^0.6.0", - "string-length": "^3.1.0", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^4.1.3" - }, - "engines": { - "node": ">= 8.3" - }, - "optionalDependencies": { - "node-notifier": "^6.0.0" + "@nteract/presentational-components": { + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/@nteract/presentational-components/-/presentational-components-3.4.9.tgz", + "integrity": "sha512-fcCYOdBRFyuj9vvXnrr2L2ynqouHnexUxpzt5VGTs4Mf/72r93vksarBStw2BD19utCVci7Fb5z6tNkFgveAZA==", + "requires": { + "@blueprintjs/core": "^3.7.0", + "@blueprintjs/select": "^3.2.0", + "classnames": "^2.2.6", + "re-resizable": "^6.5.0", + "react-syntax-highlighter": "^12.0.0", + "react-toggle": "^4.1.1" + } + } } }, "node_modules/@jest/reporters/node_modules/@jest/transform": { @@ -4395,20 +4532,10 @@ "@lumino/collections": "^1.3.3" } }, - "node_modules/@lumino/polling": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@lumino/polling/-/polling-1.3.3.tgz", - "integrity": "sha512-uMRi6sPRnKW8m38WUY3qox1jxwzpvceafUbDJATCwyrZ48+YoY5Fxfmd9dqwioHS1aq9np5c6L35a9ZGuS0Maw==", - "dependencies": { - "@lumino/coreutils": "^1.5.3", - "@lumino/disposable": "^1.4.3", - "@lumino/signaling": "^1.4.3" - } - }, - "node_modules/@lumino/properties": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lumino/properties/-/properties-1.2.3.tgz", - "integrity": "sha512-dbS9V/L+RpQoRjxHMAGh1JYoXaLA6F7xkVbg/vmYXqdXZ7DguO5C3Qteu9tNp7Z7Q31TqFWUCrniTI9UJiJCoQ==" + "@types/dom4": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/dom4/-/dom4-2.0.1.tgz", + "integrity": "sha512-kSkVAvWmMZiCYtvqjqQEwOmvKwcH+V4uiv3qPQ8pAh1Xl39xggGEo8gHUqV4waYGHezdFw0rKBR8Jt0CrQSDZA==" }, "node_modules/@lumino/signaling": { "version": "1.4.3", @@ -4554,36 +4681,99 @@ "node": ">= 8" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", - "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==", - "engines": { - "node": ">= 8" + "@types/node": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.1.tgz", + "integrity": "sha512-TJtwsqZ39pqcljJpajeoofYRfeZ7/I/OMUQ5pR4q5wOKf2ocrUvBAZUMhWsOvKx3dVc/aaV5GluBivt0sWqA5A==" + }, + "@types/node-fetch": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.7.tgz", + "integrity": "sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==", + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + }, + "dependencies": { + "@types/node": { + "version": "14.14.22", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz", + "integrity": "sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw==" + }, + "form-data": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", - "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", - "dependencies": { - "@nodelib/fs.scandir": "2.1.4", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" + "@types/normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", + "dev": true + }, + "@types/prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==", + "dev": true + }, + "@types/promise.prototype.finally": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/promise.prototype.finally/-/promise.prototype.finally-2.0.3.tgz", + "integrity": "sha512-hQfmCK9Hw8diRIa3KoIDY4aimdxckamHUcmaZeB9tBMyb/Shi1yCBIPfry+nqN4jILNVThY1tnTwdMhQeMjqrw==", + "dev": true + }, + "@types/prop-types": { + "version": "15.5.8", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.5.8.tgz", + "integrity": "sha512-3AQoUxQcQtLHsK25wtTWIoIpgYjH3vSDroZOUr7PpCHw/jLY1RB9z9E8dBT/OSmwStVgkRNvdh+ZHNiomRieaw==" + }, + "@types/puppeteer": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@types/puppeteer/-/puppeteer-5.4.3.tgz", + "integrity": "sha512-3nE8YgR9DIsgttLW+eJf6mnXxq8Ge+27m5SU3knWmrlfl6+KOG0Bf9f7Ua7K+C4BnaTMAh3/UpySqdAYvrsvjg==", + "dev": true, + "requires": { + "@types/node": "*" } }, - "node_modules/@npmcli/move-file": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", - "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" + "@types/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.1.tgz", + "integrity": "sha512-eqz8c/0kwNi/OEHQfvIuJVLTst3in0e7uTKeuY+WL/zfKn0xVujOTp42bS/vUUokhK5P2BppLd9JXMOMHcgbjA==", + "dev": true + }, + "@types/react": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.3.tgz", + "integrity": "sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg==", + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" }, - "engines": { - "node": ">=10" + "dependencies": { + "csstype": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.7.tgz", + "integrity": "sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g==" + } + } + }, + "@types/react-dom": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.3.tgz", + "integrity": "sha512-4NnJbCeWE+8YBzupn/YrJxZ8VnjcJq5iR1laqQ1vkpQgBiA7bwk0Rp24fxsdNinzJY2U+HHS4dJJDPdoMjdJ7w==", + "dev": true, + "requires": { + "@types/react": "*" } }, "node_modules/@npmcli/move-file/node_modules/rimraf": { @@ -4635,7 +4825,63 @@ "url-join": "^4.0.0" } }, - "node_modules/@nteract/any-vega": { + "@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" + }, + "@types/sanitize-html": { + "version": "1.27.2", + "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-1.27.2.tgz", + "integrity": "sha512-DrH26m7CV6PB4YVckjbSIx+xloB7HBolr9Ctm0gZBffSu5dDV4yJKFQGPquJlReVW+xmg59gx+b/8/qYHxZEuw==", + "dev": true, + "requires": { + "htmlparser2": "^4.1.0" + }, + "dependencies": { + "htmlparser2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz", + "integrity": "sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^3.0.0", + "domutils": "^2.0.0", + "entities": "^2.0.0" + } + } + } + }, + "@types/scheduler": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz", + "integrity": "sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==" + }, + "@types/shallowequal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/shallowequal/-/shallowequal-1.1.1.tgz", + "integrity": "sha512-Lhni3aX80zbpdxRuWhnuYPm8j8UQaa571lHP/xI4W+7BAFhSIhRReXnqjEgT/XzPoXZTJkCqstFMJ8CZTK6IlQ==" + }, + "@types/signals": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/signals/-/signals-1.0.1.tgz", + "integrity": "sha512-t6qsSE/nUiDN4MO5pZIR7XdhCNt0No0oJL1y1JSYovQL7+GvWzdBI0eD0ADZ2gW4Lg+CB4/d0j1nXe9lMpEHow==", + "dev": true + }, + "@types/sinon": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-2.3.3.tgz", + "integrity": "sha512-bnoHhhCsx0p0yhLOywFg6T7Le37JjtnzLcWal6cuSPvIZUBzKRIsqM6E5OsKUIRVErCaBCghHIZmqtyGk5uXyA==", + "dev": true + }, + "@types/source-list-map": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", + "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", + "dev": true + }, + "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@nteract/any-vega/-/any-vega-1.0.1.tgz", "integrity": "sha512-RjECjm2bdQpEHuhpWyIjAg8dA8t5iWGDmx2VuStBhVSffciNrCn0WxhJiK5TSkyuBgk4rB4eAOspJ8UnfjOAyA==" @@ -6563,54 +6809,13 @@ "redux": "^4.0.4" } }, - "node_modules/@nteract/stateful-components/node_modules/@nteract/commutable": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@nteract/commutable/-/commutable-7.4.3.tgz", - "integrity": "sha512-1Jov+js+Fs+L1hv7qrQdRnlNbQBx2Gf3J4qhAWR2wBDOj5uVPPeHYlx5+8OnQCIAxQ9si1zExYjBRFoe3zVVgQ==", - "dependencies": { - "immutable": "^4.0.0-rc.12", - "uuid": "^8.0.0" - } - }, - "node_modules/@nteract/stateful-components/node_modules/@nteract/fixtures": { - "version": "2.3.17", - "resolved": "https://registry.npmjs.org/@nteract/fixtures/-/fixtures-2.3.17.tgz", - "integrity": "sha512-LjIBba7Zx2Mtoh0fkKpxfElNo6Ie1+NuON/wvwA1kDiDRvKO4bxb+grJMcW04TDm3+gDay5zu97mvAnsU8Ne1A==", - "dependencies": { - "@nteract/commutable": "^7.4.3", - "@nteract/reducers": "^5.1.7", - "@nteract/types": "^7.1.7" - } - }, - "node_modules/@nteract/stateful-components/node_modules/@nteract/markdown": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@nteract/markdown/-/markdown-4.5.1.tgz", - "integrity": "sha512-H5KtzBtVLwDd2jOHdvyDiT0vJdrjKLGQBQ9cYcS1qgu3j2d1dTLW0Gla8QcN2fAn5nenZdkFZX+O7ZN2+y0HNQ==", - "dependencies": { - "@nteract/mathjax": "^4.0.7", - "@nteract/presentational-components": "^3.3.11", - "react-markdown": "^4.0.0" - }, - "peerDependencies": { - "react": "^16.11.0", - "react-dom": "^16.3.2" - } - }, - "node_modules/@nteract/stateful-components/node_modules/@nteract/presentational-components": { - "version": "3.4.9", - "resolved": "https://registry.npmjs.org/@nteract/presentational-components/-/presentational-components-3.4.9.tgz", - "integrity": "sha512-fcCYOdBRFyuj9vvXnrr2L2ynqouHnexUxpzt5VGTs4Mf/72r93vksarBStw2BD19utCVci7Fb5z6tNkFgveAZA==", - "dependencies": { - "@blueprintjs/core": "^3.7.0", - "@blueprintjs/select": "^3.2.0", - "classnames": "^2.2.6", - "re-resizable": "^6.5.0", - "react-syntax-highlighter": "^12.0.0", - "react-toggle": "^4.1.1" - }, - "peerDependencies": { - "react": "^16.3.2", - "styled-components": ">= 5.0.1" + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" } }, "node_modules/@nteract/stateful-components/node_modules/@nteract/presentational-components/node_modules/re-resizable": { @@ -7051,7 +7256,68 @@ "resolved": "https://registry.npmjs.org/@phosphor/virtualdom/-/virtualdom-1.2.0.tgz", "integrity": "sha512-L9mKNhK2XtVjzjuHLG2uYuepSz8uPyu6vhF4EgCP0rt0TiLYaZeHwuNu3XeFbul9DMOn49eBpye/tfQVd4Ks+w==", "dependencies": { - "@phosphor/algorithm": "^1.2.0" + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "optional": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "optional": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "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==", + "dev": true, + "optional": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "optional": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "optional": true + }, + "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==", + "dev": true, + "optional": true, + "requires": { + "is-number": "^7.0.0" + } + } } }, "node_modules/@phosphor/widgets": { @@ -7581,7 +7847,29 @@ "integrity": "sha512-qpQe8G02tzUwt9sdWX1h8A/W0Q1+N48wMnYXVOkrzeLUkCfvzJYV9Ee3aORCS4dN4ONRLFmMvaXdziQ29XGLjQ==", "dev": true, "dependencies": { - "@types/d3-time": "*" + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + } } }, "node_modules/@types/d3-scale-chromatic": { @@ -8373,15 +8661,10 @@ "react-dom": ">=16.8.0 <17.0.0" } }, - "node_modules/@uifabric/icons": { - "version": "7.5.23", - "resolved": "https://registry.npmjs.org/@uifabric/icons/-/icons-7.5.23.tgz", - "integrity": "sha512-eIvUbH0EWgFgdfgFfINgqS2ZVZTyJ/9n5nR4bmcyAe75wsKxm4ser4WIT9IvaBF6+HFVfjUF/v6+VMD7y2LBng==", - "dependencies": { - "@uifabric/set-version": "^7.0.24", - "@uifabric/styling": "^7.19.0", - "tslib": "^1.10.0" - } + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" }, "node_modules/@uifabric/icons/node_modules/@fluentui/theme": { "version": "1.7.4", @@ -9863,10 +10146,17 @@ "deprecated": "core-js@<3 is no longer maintained and not recommended for usage due to the number of issues. Please, upgrade your dependencies to the actual version of core-js@3.", "hasInstallScript": true }, - "node_modules/babel-runtime/node_modules/regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "optional": true + }, + "filesize": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz", + "integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==", + "dev": true }, "node_modules/bail": { "version": "1.0.5", @@ -9910,27 +10200,63 @@ "node": ">=0.10.0" } }, - "node_modules/base16": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz", - "integrity": "sha1-4pf2DX7BAUp6lxo568ipjAtoHnA=" - }, - "node_modules/base64-arraybuffer": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz", - "integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" + "find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + } + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" }, { "type": "patreon", @@ -47360,41 +47686,187 @@ "@jest/types": "^26.6.2", "@types/node": "*" } - }, - "jest-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", - "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", - "dev": true, + } + } + }, + "html2canvas": { + "version": "1.0.0-rc.5", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.0.0-rc.5.tgz", + "integrity": "sha512-DtNqPxJNXPoTajs+lVQzGS1SULRI4GQaROeU5R41xH8acffHukxRh/NBVcTBsfCkJSkLq91rih5TpbEwUP9yWA==", + "requires": { + "css-line-break": "1.1.1" + } + }, + "htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + }, + "dependencies": { + "domhandler": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.1.0.tgz", + "integrity": "sha512-/6/kmsGlMY4Tup/nGVutdrK9yQi4YjWVcVeoQmixpzjOUK1U7pQkvAPHBJeUxOgxF0J8f8lwCJSlCfD0V4CMGQ==", "requires": { - "@jest/types": "^26.6.2", - "@types/node": "*", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "micromatch": "^4.0.2" + "domelementtype": "^2.2.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" + } } }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, + "domutils": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.5.2.tgz", + "integrity": "sha512-MHTthCb1zj8f1GVfRpeZUbohQf/HdBos0oX5gZcQFepOZPLLRyj6Wn7XS7EMnY7CVpwv8863u2vyE83Hfu28HQ==", "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.1.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" + } } - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw==", + } + } + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", + "dev": true + }, + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "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==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-middleware": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", + "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==", + "dev": true, + "requires": { + "http-proxy": "^1.17.0", + "is-glob": "^4.0.0", + "lodash": "^4.17.11", + "micromatch": "^3.1.10" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true + }, + "i18next": { + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-19.8.4.tgz", + "integrity": "sha512-FfVPNWv+felJObeZ6DSXZkj9QM1Ivvh7NcFCgA8XPtJWHz0iXVa9BUy+QY8EPrCLE+vWgDfV/sc96BgXVo6HAA==", + "requires": { + "@babel/runtime": "^7.12.0" + } + }, + "i18next-browser-languagedetector": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.0.1.tgz", + "integrity": "sha512-3H+OsNQn3FciomUU0d4zPFHsvJv4X66lBelXk9hnIDYDsveIgT7dWZ3/VvcSlpKk9lvCK770blRZ/CwHMXZqWw==", + "requires": { + "@babel/runtime": "^7.5.5" + } + }, + "i18next-http-backend": { + "version": "1.0.23", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-1.0.23.tgz", + "integrity": "sha512-2iXwUmawM4kozvGN+k7G9u/bYQdgqtTXVK0cWvLSOpUCTaW30ZzRhgu0FBfinb71XjwUEvdqb95jNrEFjrwGKw==", + "requires": { + "node-fetch": "2.6.1" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "icss-replace-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", + "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=", + "dev": true + }, + "icss-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-2.1.0.tgz", + "integrity": "sha1-g/Cg7DeL8yRheLbCrZE28TWxyWI=", + "dev": true, + "requires": { + "postcss": "^6.0.1" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", "dev": true, "requires": { "escape-string-regexp": "^2.0.0" @@ -48870,9 +49342,18 @@ "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "p-limit": "^2.2.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + } } }, "to-regex-range": { @@ -49727,6 +50208,11 @@ "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" }, + "klona": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz", + "integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==" + }, "knockout": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/knockout/-/knockout-3.5.1.tgz", @@ -50726,6 +51212,11 @@ "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", "optional": true }, + "nanoid": { + "version": "3.1.22", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz", + "integrity": "sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==" + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -50833,9 +51324,9 @@ } }, "node-abort-controller": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-1.2.0.tgz", - "integrity": "sha512-x6Pv6ACfOUmYGDW/SRjSKBJs7waJRxRPQ9FeXFqfBtEtvJQBfuPl5P74p0Ow+vl0w6WURvXwn+xq/3S95Z7e5Q==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-1.2.1.tgz", + "integrity": "sha512-79PYeJuj6S9+yOHirR0JBLFOgjB6sQCir10uN6xRx25iD+ZD4ULqgRn3MwWBRaQGB0vEgReJzWwJo42T1R6YbQ==" }, "node-fetch": { "version": "2.6.1", @@ -51458,20 +51949,22 @@ "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", "dev": true }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.0" - } - }, "p-locate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "requires": { "p-limit": "^2.0.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + } } }, "p-map": { @@ -51856,21 +52349,24 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" }, "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", - "dev": true, + "version": "8.2.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.10.tgz", + "integrity": "sha512-b/h7CPV7QEdrqIxtAf2j31U5ef05uBDuvoXv6L51Q4rcS1jdlXAVKJv+atCFdUXYl9dyTHGyoMzIepwowRJjFw==", "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" + "colorette": "^1.2.2", + "nanoid": "^3.1.22", + "source-map": "^0.6.1" }, "dependencies": { + "colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==" + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, @@ -52233,6 +52729,17 @@ "dev": true, "requires": { "p-limit": "^2.2.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + } } }, "path-exists": { @@ -53531,57 +54038,28 @@ } }, "sanitize-html": { - "version": "1.27.5", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.27.5.tgz", - "integrity": "sha512-M4M5iXDAUEcZKLXkmk90zSYWEtk5NH3JmojQxKxV371fnMh+x9t1rqdmXaGoyEHw3z/X/8vnFhKjGL5xFGOJ3A==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.3.3.tgz", + "integrity": "sha512-DCFXPt7Di0c6JUnlT90eIgrjs6TsJl/8HYU3KLdmrVclFN4O0heTcVbJiMa23OKVr6aR051XYtsgd8EWwEBwUA==", "requires": { - "htmlparser2": "^4.1.0", - "lodash": "^4.17.15", + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^6.0.0", + "is-plain-object": "^5.0.0", + "klona": "^2.0.3", "parse-srcset": "^1.0.2", - "postcss": "^7.0.27" + "postcss": "^8.0.2" }, "dependencies": { - "domhandler": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", - "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", - "requires": { - "domelementtype": "^2.0.1" - } + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" }, - "htmlparser2": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz", - "integrity": "sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==", - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^3.0.0", - "domutils": "^2.0.0", - "entities": "^2.0.0" - } - }, - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "requires": { - "has-flag": "^3.0.0" - } + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" } } }, @@ -57338,28 +57816,6 @@ "errno": "~0.1.7" } }, - "worker-loader": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-2.0.0.tgz", - "integrity": "sha512-tnvNp4K3KQOpfRnD20m8xltE3eWh89Ye+5oj7wXEEHKac1P4oZ6p9oTj8/8ExqoSBnk9nu5Pr4nKfQ1hn2APJw==", - "dev": true, - "requires": { - "loader-utils": "^1.0.0", - "schema-utils": "^0.4.0" - }, - "dependencies": { - "schema-utils": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", - "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, "wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", diff --git a/package.json b/package.json index c007e33d3..ba01ef819 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "dependencies": { "@azure/arm-cosmosdb": "9.1.0", - "@azure/cosmos": "3.9.0", + "@azure/cosmos": "3.10.5", "@azure/cosmos-language-service": "0.0.5", "@azure/identity": "1.2.1", "@azure/ms-rest-nodeauth": "3.0.7", @@ -25,7 +25,7 @@ "@nteract/iron-icons": "1.0.0", "@nteract/jupyter-widgets": "2.0.0", "@nteract/logos": "1.0.0", - "@nteract/markdown": "4.4.0", + "@nteract/markdown": "4.6.0", "@nteract/monaco-editor": "3.2.2", "@nteract/octicons": "2.0.0", "@nteract/outputs": "3.0.9", @@ -94,6 +94,7 @@ "reflect-metadata": "0.1.13", "rx-jupyter": "5.5.12", "rxjs": "6.6.3", + "sanitize-html": "2.3.3", "styled-components": "4.3.2", "swr": "0.4.0", "terser-webpack-plugin": "3.1.0", @@ -122,10 +123,11 @@ "@types/prop-types": "15.5.8", "@types/puppeteer": "5.4.3", "@types/q": "1.5.1", - "@types/react": "17.0.0", - "@types/react-dom": "17.0.0", + "@types/react": "17.0.3", + "@types/react-dom": "17.0.3", "@types/react-notification-system": "0.2.39", "@types/react-redux": "7.1.7", + "@types/sanitize-html": "1.27.2", "@types/sinon": "2.3.3", "@types/styled-components": "5.1.1", "@types/underscore": "1.7.36", @@ -178,8 +180,7 @@ "webpack": "4.43.0", "webpack-bundle-analyzer": "3.6.1", "webpack-cli": "3.3.10", - "webpack-dev-server": "3.11.0", - "worker-loader": "2.0.0" + "webpack-dev-server": "3.11.0" }, "scripts": { "start": "node --max-old-space-size=10196 node_modules/webpack-dev-server/bin/webpack-dev-server.js", @@ -202,8 +203,8 @@ "format:check": "prettier --check \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"", "lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"", "build:contracts": "npm run compile:contracts", - "strictEligibleFiles": "node ./strict-migration-tools/index.js", - "autoAddStrictEligibleFiles": "node ./strict-migration-tools/autoAdd.js", + "strict:find": "node ./strict-null-checks/find.js", + "strict:add": "node ./strict-null-checks/auto-add.js", "compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks", "generateARMClients": "ts-node --compiler-options '{\"module\":\"commonjs\"}' utils/armClientGenerator/generator.ts" }, diff --git a/preview/index.js b/preview/index.js index 249386060..c17eec9ee 100644 --- a/preview/index.js +++ b/preview/index.js @@ -1,6 +1,7 @@ const express = require("express"); const { createProxyMiddleware } = require("http-proxy-middleware"); const port = process.env.PORT || 3000; +const fetch = require("node-fetch"); const api = createProxyMiddleware("/api", { target: "https://main.documentdb.ext.azure.com", @@ -39,6 +40,29 @@ const app = express(); app.use(api); app.use(proxy); 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, () => { console.log(`Example app listening on port: ${port}`); }); diff --git a/preview/package-lock.json b/preview/package-lock.json index 669971aa0..58a7075b0 100644 --- a/preview/package-lock.json +++ b/preview/package-lock.json @@ -1,8 +1,658 @@ { - "name": "preview", + "name": "cosmos-explorer-preview", "version": "1.0.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "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": { "@types/http-proxy": { "version": "1.17.5", @@ -334,6 +984,11 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "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": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", diff --git a/preview/package.json b/preview/package.json index 44b7221a0..8118ce77a 100644 --- a/preview/package.json +++ b/preview/package.json @@ -12,6 +12,7 @@ "author": "Microsoft Corporation", "dependencies": { "express": "^4.17.1", - "http-proxy-middleware": "^1.1.0" + "http-proxy-middleware": "^1.1.0", + "node-fetch": "^2.6.1" } } diff --git a/src/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index 749557b30..4e3029290 100644 --- a/src/Common/CosmosClient.ts +++ b/src/Common/CosmosClient.ts @@ -75,7 +75,10 @@ export async function getTokenFromAuthService(verb: string, resourceType: string } } +let _client: Cosmos.CosmosClient; + export function client(): Cosmos.CosmosClient { + if (_client) return _client; 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 key: userContext.masterKey, @@ -89,5 +92,6 @@ export function client(): Cosmos.CosmosClient { if (configContext.PROXY_PATH !== undefined) { (options as any).plugins = [{ on: "request", plugin: requestPlugin }]; } - return new Cosmos.CosmosClient(options); + _client = new Cosmos.CosmosClient(options); + return _client; } diff --git a/src/Common/MessageHandler.ts b/src/Common/MessageHandler.ts index 73636a5fd..97201b8b2 100644 --- a/src/Common/MessageHandler.ts +++ b/src/Common/MessageHandler.ts @@ -48,32 +48,18 @@ export function sendCachedDataMessage( } export function sendMessage(data: any): void { - if (canSendMessage()) { - // We try to find data explorer window first, then fallback to current window - const portalChildWindow = getDataExplorerWindow(window) || window; - portalChildWindow.parent.postMessage( - { - signature: "pcIframe", - data: data, - }, - portalChildWindow.document.referrer || "*" - ); - } + _sendMessage({ + signature: "pcIframe", + data: data, + }); } export function sendReadyMessage(): void { - if (canSendMessage()) { - // We try to find data explorer window first, then fallback to current window - const portalChildWindow = getDataExplorerWindow(window) || window; - portalChildWindow.parent.postMessage( - { - signature: "pcIframe", - kind: "ready", - data: "ready", - }, - portalChildWindow.document.referrer || "*" - ); - } + _sendMessage({ + signature: "pcIframe", + kind: "ready", + data: "ready", + }); } export function canSendMessage(): boolean { @@ -89,3 +75,17 @@ 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 || "*"); + } + } +}; diff --git a/src/Common/dataAccess/bulkCreateDocument.ts b/src/Common/dataAccess/bulkCreateDocument.ts new file mode 100644 index 000000000..ec7ae7b8f --- /dev/null +++ b/src/Common/dataAccess/bulkCreateDocument.ts @@ -0,0 +1,39 @@ +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 => { + 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(); + } +}; diff --git a/src/Common/dataAccess/readCollections.ts b/src/Common/dataAccess/readCollections.ts index c067afe49..4d77a0597 100644 --- a/src/Common/dataAccess/readCollections.ts +++ b/src/Common/dataAccess/readCollections.ts @@ -1,15 +1,15 @@ -import * as DataModels from "../../Contracts/DataModels"; import { AuthType } from "../../AuthType"; +import * as DataModels from "../../Contracts/DataModels"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; -import { client } from "../CosmosClient"; -import { handleError } from "../ErrorHandlingUtils"; -import { listSqlContainers } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; +import { userContext } from "../../UserContext"; 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 { 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 { userContext } from "../../UserContext"; +import { client } from "../CosmosClient"; +import { handleError } from "../ErrorHandlingUtils"; export async function readCollections(databaseId: string): Promise { const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`); @@ -17,7 +17,6 @@ export async function readCollections(databaseId: string): Promise, options: RequestOptions = {} ): Promise { let collection: Collection; @@ -43,7 +41,6 @@ export async function updateCollection( if ( userContext.authType === AuthType.AAD && !userContext.useSDKOperations && - userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB && userContext.defaultExperience !== DefaultAccountExperienceType.Table ) { collection = await updateCollectionWithARM(databaseId, collectionId, newCollection); @@ -69,7 +66,7 @@ export async function updateCollection( async function updateCollectionWithARM( databaseId: string, collectionId: string, - newCollection: Collection + newCollection: Partial ): Promise { const subscriptionId = userContext.subscriptionId; const resourceGroup = userContext.resourceGroup; @@ -85,6 +82,15 @@ async function updateCollectionWithARM( return updateGremlinGraph(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection); case DefaultAccountExperienceType.Table: return updateTable(collectionId, subscriptionId, resourceGroup, accountName, newCollection); + case DefaultAccountExperienceType.MongoDB: + return updateMongoDBCollection( + databaseId, + collectionId, + subscriptionId, + resourceGroup, + accountName, + newCollection + ); default: throw new Error(`Unsupported default experience type: ${defaultExperience}`); } @@ -96,7 +102,7 @@ async function updateSqlContainer( subscriptionId: string, resourceGroup: string, accountName: string, - newCollection: Collection + newCollection: Partial ): Promise { const getResponse = await getSqlContainer(subscriptionId, resourceGroup, accountName, databaseId, collectionId); if (getResponse && getResponse.properties && getResponse.properties.resource) { @@ -115,35 +121,26 @@ async function updateSqlContainer( throw new Error(`Sql container to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`); } -export async function updateMongoDBCollectionThroughRP( +export async function updateMongoDBCollection( databaseId: string, collectionId: string, - newCollection: MongoDBCollectionResource, - updateOptions?: CreateUpdateOptions -): Promise { - const subscriptionId = userContext.subscriptionId; - const resourceGroup = userContext.resourceGroup; - const accountName = userContext.databaseAccount.name; - + subscriptionId: string, + resourceGroup: string, + accountName: string, + newCollection: Partial +): Promise { const getResponse = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId); if (getResponse && getResponse.properties && getResponse.properties.resource) { - const updateParams: MongoDBCollectionCreateUpdateParameters = { - properties: { - resource: newCollection, - options: updateOptions, - }, - }; - + getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties; const updateResponse = await createUpdateMongoDBCollection( subscriptionId, resourceGroup, accountName, databaseId, collectionId, - updateParams + getResponse as MongoDBCollectionCreateUpdateParameters ); - - return updateResponse && (updateResponse.properties.resource as MongoDBCollectionResource); + return updateResponse && (updateResponse.properties.resource as Collection); } throw new Error( @@ -157,7 +154,7 @@ async function updateCassandraTable( subscriptionId: string, resourceGroup: string, accountName: string, - newCollection: Collection + newCollection: Partial ): Promise { const getResponse = await getCassandraTable(subscriptionId, resourceGroup, accountName, databaseId, collectionId); if (getResponse && getResponse.properties && getResponse.properties.resource) { @@ -184,7 +181,7 @@ async function updateGremlinGraph( subscriptionId: string, resourceGroup: string, accountName: string, - newCollection: Collection + newCollection: Partial ): Promise { const getResponse = await getGremlinGraph(subscriptionId, resourceGroup, accountName, databaseId, collectionId); if (getResponse && getResponse.properties && getResponse.properties.resource) { @@ -208,7 +205,7 @@ async function updateTable( subscriptionId: string, resourceGroup: string, accountName: string, - newCollection: Collection + newCollection: Partial ): Promise { const getResponse = await getTable(subscriptionId, resourceGroup, accountName, collectionId); if (getResponse && getResponse.properties && getResponse.properties.resource) { diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index 4e23a64dd..4e3085b45 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -121,6 +121,10 @@ export interface ISchemaRequest { } export interface Collection extends Resource { + // Only in Mongo collections loaded via ARM + shardKey?: { + [key: string]: string; + }; defaultTtl?: number; indexingPolicy?: IndexingPolicy; partitionKey?: PartitionKey; diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index 240031f52..dc14bb86c 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -15,7 +15,6 @@ import StoredProcedure from "../Explorer/Tree/StoredProcedure"; import Trigger from "../Explorer/Tree/Trigger"; import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction"; import { SelfServeType } from "../SelfServe/SelfServeUtils"; -import { UploadDetails } from "../workers/upload/definitions"; import * as DataModels from "./DataModels"; import { SubscriptionType } from "./SubscriptionType"; @@ -23,6 +22,14 @@ export interface TokenProvider { getAuthHeader(): Promise; } +export interface UploadDetailsRecord { + fileName: string; + numSucceeded: number; + numFailed: number; + numThrottled: number; + errors: string[]; +} + export interface QueryResultsMetadata { hasMoreResults: boolean; firstItemIndex: number; @@ -174,7 +181,7 @@ export interface Collection extends CollectionBase { onDragOver(source: Collection, event: { originalEvent: DragEvent }): void; onDrop(source: Collection, event: { originalEvent: DragEvent }): void; - uploadFiles(fileList: FileList): Promise; + uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>; getLabel(): string; getPendingThroughputSplitNotification(): Promise; @@ -269,7 +276,6 @@ export interface TabOptions { tabKind: CollectionTabKind; title: string; tabPath: string; - isActive: ko.Observable; hashLocation: string; onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]) => void; isTabsContentExpanded?: ko.Observable; @@ -390,6 +396,9 @@ export interface DataExplorerInputsFrame { dataExplorerVersion?: string; defaultCollectionThroughput?: CollectionCreationDefaults; flights?: readonly string[]; + features?: { + [key: string]: string; + }; } export interface SelfServeFrameInputs { diff --git a/src/Definitions/worker.d.ts b/src/Definitions/worker.d.ts deleted file mode 100644 index 6a93b9569..000000000 --- a/src/Definitions/worker.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare module "worker-loader!*" { - class WebpackWorker extends Worker { - constructor(); - } - - export default WebpackWorker; -} diff --git a/src/Explorer/ComponentRegisterer.test.ts b/src/Explorer/ComponentRegisterer.test.ts index 4aa0fadc0..78e08fad1 100644 --- a/src/Explorer/ComponentRegisterer.test.ts +++ b/src/Explorer/ComponentRegisterer.test.ts @@ -73,10 +73,6 @@ describe("Component Registerer", () => { 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); }); diff --git a/src/Explorer/ComponentRegisterer.ts b/src/Explorer/ComponentRegisterer.ts index 264e88eab..f7e4576c7 100644 --- a/src/Explorer/ComponentRegisterer.ts +++ b/src/Explorer/ComponentRegisterer.ts @@ -57,16 +57,11 @@ ko.components.register("tabs-manager", { template: TabsManagerTemplate }); // Panes 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("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent()); 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("string-input-pane", new PaneComponents.StringInputPaneComponent()); ko.components.register("setup-notebooks-pane", new PaneComponents.SetupNotebooksPaneComponent()); diff --git a/src/Explorer/ContextMenuButtonFactory.ts b/src/Explorer/ContextMenuButtonFactory.ts index 7f670d85a..8337ec700 100644 --- a/src/Explorer/ContextMenuButtonFactory.ts +++ b/src/Explorer/ContextMenuButtonFactory.ts @@ -55,7 +55,7 @@ export class ResourceTreeContextMenuButtonFactory { selectedCollection: ViewModels.Collection ): TreeNodeMenuItem[] { const items: TreeNodeMenuItem[] = []; - if (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph()) { + if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") { items.push({ iconSrc: AddSqlQueryIcon, onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null), @@ -80,7 +80,7 @@ export class ResourceTreeContextMenuButtonFactory { }); } - if (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph()) { + if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") { items.push({ iconSrc: AddStoredProcedureIcon, onClick: () => { @@ -123,7 +123,7 @@ export class ResourceTreeContextMenuButtonFactory { container: Explorer, storedProcedure: StoredProcedure ): TreeNodeMenuItem[] { - if (container.isPreferredApiCassandra()) { + if (userContext.apiType === "Cassandra") { return []; } @@ -137,7 +137,7 @@ export class ResourceTreeContextMenuButtonFactory { } public static createTriggerContextMenuItems(container: Explorer, trigger: Trigger): TreeNodeMenuItem[] { - if (container.isPreferredApiCassandra()) { + if (userContext.apiType === "Cassandra") { return []; } @@ -154,7 +154,7 @@ export class ResourceTreeContextMenuButtonFactory { container: Explorer, userDefinedFunction: UserDefinedFunction ): TreeNodeMenuItem[] { - if (container.isPreferredApiCassandra()) { + if (userContext.apiType === "Cassandra") { return []; } diff --git a/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.tsx b/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.tsx index 3435b779c..2831ec866 100644 --- a/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.tsx +++ b/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.tsx @@ -21,18 +21,18 @@ import { Text, } from "office-ui-fabric-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 { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; +import { trace } from "../../../Shared/Telemetry/TelemetryProcessor"; import * as GalleryUtils from "../../../Utils/GalleryUtils"; +import Explorer from "../../Explorer"; import { Dialog, DialogProps } from "../Dialog"; import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent"; -import "./GalleryViewerComponent.less"; -import { HttpStatusCodes } from "../../../Common/Constants"; -import Explorer from "../../Explorer"; import { CodeOfConductComponent } from "./CodeOfConductComponent"; +import "./GalleryViewerComponent.less"; 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 { container?: Explorer; @@ -138,11 +138,11 @@ export class GalleryViewerComponent extends React.Component { - const isFavorite = this.favoriteNotebooks?.find((item) => item.id === data.id) !== undefined; + const isFavorite = + this.props.container && this.favoriteNotebooks?.find((item) => item.id === data.id) !== undefined; const props: GalleryCardComponentProps = { data, isFavorite, diff --git a/src/Explorer/Controls/Settings/SettingsComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsComponent.test.tsx index c6432aec2..69ee13439 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.test.tsx @@ -1,11 +1,11 @@ import { shallow } from "enzyme"; import ko from "knockout"; import React from "react"; -import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection"; +import { updateCollection } from "../../../Common/dataAccess/updateCollection"; import { updateOffer } from "../../../Common/dataAccess/updateOffer"; import * as DataModels from "../../../Contracts/DataModels"; import * as ViewModels from "../../../Contracts/ViewModels"; -import { MongoDBCollectionResource } from "../../../Utils/arm/generatedClients/2020-04-01/types"; +import { updateUserContext } from "../../../UserContext"; import Explorer from "../../Explorer"; import { CollectionSettingsTabV2 } from "../../Tabs/SettingsTabV2"; import { SettingsComponent, SettingsComponentProps, SettingsComponentState } from "./SettingsComponent"; @@ -23,13 +23,8 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({ changeFeedPolicy: undefined, analyticalStorageTtl: undefined, geospatialConfig: undefined, - } as DataModels.Collection), - updateMongoDBCollectionThroughRP: jest.fn().mockReturnValue({ - id: undefined, - shardKey: undefined, indexes: [], - analyticalStorageTtl: undefined, - } as MongoDBCollectionResource), + }), })); jest.mock("../../../Common/dataAccess/updateOffer", () => ({ updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer), @@ -44,7 +39,6 @@ describe("SettingsComponent", () => { tabPath: "", node: undefined, hashLocation: "settings", - isActive: ko.observable(false), onUpdateTabsButtons: undefined, }), }; @@ -113,7 +107,13 @@ describe("SettingsComponent", () => { expect(settingsComponentInstance.shouldShowKeyspaceSharedThroughputMessage()).toEqual(false); const newContainer = new Explorer(); - newContainer.isPreferredApiCassandra = ko.computed(() => true); + updateUserContext({ + databaseAccount: { + properties: { + capabilities: [{ name: "EnableCassandra" }], + }, + } as DataModels.DatabaseAccount, + }); const newCollection = { ...collection }; newCollection.container = newContainer; @@ -193,7 +193,6 @@ describe("SettingsComponent", () => { }; await settingsComponentInstance.onSaveClick(); expect(updateCollection).toBeCalled(); - expect(updateMongoDBCollectionThroughRP).toBeCalled(); expect(updateOffer).toBeCalled(); }); diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index 63c1c4b79..4c151eb51 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -6,7 +6,7 @@ import { AuthType } from "../../../AuthType"; import * as Constants from "../../../Common/Constants"; import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress"; import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection"; -import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection"; +import { updateCollection } from "../../../Common/dataAccess/updateCollection"; import { updateOffer } from "../../../Common/dataAccess/updateOffer"; import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import * as DataModels from "../../../Contracts/DataModels"; @@ -137,7 +137,7 @@ export class SettingsComponent extends React.Component - this.container && this.container.isPreferredApiCassandra() && hasDatabaseSharedThroughput(this.collection); + this.container && userContext.apiType === "Cassandra" && hasDatabaseSharedThroughput(this.collection); public hasConflictResolution = (): boolean => this.container?.databaseAccount && @@ -782,12 +782,12 @@ export class SettingsComponent extends React.Component { - container.isPreferredApiDocumentDB = ko.computed(() => true); - const baseProps: SubSettingsComponentProps = { collection: collection, container: container, @@ -106,8 +105,13 @@ describe("SubSettingsComponent", () => { it("partitionKey not visible", () => { const newContainer = new Explorer(); - - newContainer.isPreferredApiCassandra = ko.computed(() => true); + updateUserContext({ + databaseAccount: { + properties: { + capabilities: [{ name: "EnableCassandra" }], + }, + } as DatabaseAccount, + }); const props = { ...baseProps, container: newContainer }; const subSettingsComponent = new SubSettingsComponent(props); expect(subSettingsComponent.getPartitionKeyVisible()).toEqual(false); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx index 1f8133a35..5f05edd75 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx @@ -1,28 +1,38 @@ +import { + ChoiceGroup, + IChoiceGroupOption, + Label, + Link, + MessageBar, + Stack, + Text, + TextField, +} from "office-ui-fabric-react"; import * as React from "react"; import * as ViewModels from "../../../../Contracts/ViewModels"; -import { - GeospatialConfigType, - TtlType, - ChangeFeedPolicyState, - isDirty, - IsComponentDirtyResult, - TtlOn, - TtlOff, - TtlOnNoDefault, - getSanitizedInputValue, -} from "../SettingsUtils"; +import { userContext } from "../../../../UserContext"; import Explorer from "../../../Explorer"; import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon"; -import { Label, Text, TextField, Stack, IChoiceGroupOption, ChoiceGroup, MessageBar } from "office-ui-fabric-react"; import { - getTextFieldStyles, changeFeedPolicyToolTip, + getChoiceGroupStyles, + getTextFieldStyles, + messageBarStyles, subComponentStackProps, titleAndInputStackProps, - getChoiceGroupStyles, ttlWarning, - messageBarStyles, } from "../SettingsRenderUtils"; +import { + ChangeFeedPolicyState, + GeospatialConfigType, + getSanitizedInputValue, + IsComponentDirtyResult, + isDirty, + TtlOff, + TtlOn, + TtlOnNoDefault, + TtlType, +} from "../SettingsUtils"; import { ToolTipLabelComponent } from "./ToolTipLabelComponent"; export interface SubSettingsComponentProps { @@ -60,17 +70,15 @@ export interface SubSettingsComponentProps { export class SubSettingsComponent extends React.Component { private shouldCheckComponentIsDirty = true; - private ttlVisible: boolean; private geospatialVisible: boolean; private partitionKeyValue: string; private partitionKeyName: string; constructor(props: SubSettingsComponentProps) { super(props); - this.ttlVisible = (this.props.container && !this.props.container.isPreferredApiCassandra()) || false; - this.geospatialVisible = this.props.container.isPreferredApiDocumentDB(); + this.geospatialVisible = userContext.apiType === "SQL"; this.partitionKeyValue = "/" + this.props.collection.partitionKeyProperty; - this.partitionKeyName = this.props.container.isPreferredApiMongoDB() ? "Shard key" : "Partition key"; + this.partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key"; } componentDidMount(): void { @@ -170,39 +178,51 @@ export class SubSettingsComponent extends React.Component this.props.onChangeFeedPolicyChange(ChangeFeedPolicyState[option.key as keyof typeof ChangeFeedPolicyState]); - private getTtlComponent = (): JSX.Element => ( - - - {isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && ( - - {ttlWarning} - - )} - {this.props.timeToLive === TtlType.On && ( - + userContext.apiType === "Mongo" ? ( + + To enable time-to-live (TTL) for your collection/documents, + + create a TTL index + + . + + ) : ( + + - )} - - ); + {isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && ( + + {ttlWarning} + + )} + {this.props.timeToLive === TtlType.On && ( + + )} + + ); private analyticalTtlChoiceGroupOptions: IChoiceGroupOption[] = [ { key: TtlType.Off, text: "Off", disabled: true }, @@ -300,7 +320,7 @@ export class SubSettingsComponent extends React.Component { if ( - this.props.container.isPreferredApiCassandra() || + userContext.apiType === "Cassandra" || this.props.container.isPreferredApiTable() || !this.props.collection.partitionKeyProperty || (this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey) @@ -315,7 +335,7 @@ export class SubSettingsComponent extends React.Component - {this.ttlVisible && this.getTtlComponent()} + {userContext.apiType !== "Cassandra" && this.getTtlComponent()} {this.geospatialVisible && this.getGeoSpatialComponent()} diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index e98a362a4..0f5d98439 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -115,21 +115,6 @@ exports[`SettingsComponent renders 1`] = ` "useIndexingForSharedThroughput": [Function], "visible": [Function], }, - DeleteCollectionConfirmationPane { - "collectionIdConfirmation": [Function], - "collectionIdConfirmationText": [Function], - "container": [Circular], - "containerDeleteFeedback": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "id": "deletecollectionconfirmationpane", - "isExecuting": [Function], - "isTemplateReady": [Function], - "recordDeleteFeedback": [Function], - "title": [Function], - "visible": [Function], - }, GraphStylingPane { "container": [Circular], "firstFieldHasFocus": [Function], @@ -220,27 +205,6 @@ exports[`SettingsComponent renders 1`] = ` "title": [Function], "visible": [Function], }, - QuerySelectPane { - "allSelected": [Function], - "anyColumnSelected": [Function], - "availableColumnsTableQueryLabel": "Available Columns", - "canSelectAll": [Function], - "columnOptions": [Function], - "container": [Circular], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "handleClick": [Function], - "id": "queryselectpane", - "instructionLabel": "Select the columns that you want to query.", - "isExecuting": [Function], - "isTemplateReady": [Function], - "noColumnSelectedWarning": "At least one column should be selected.", - "selectedColumnOption": null, - "title": [Function], - "titleLabel": "Select Columns", - "visible": [Function], - }, NewVertexPane { "buildString": [Function], "container": [Circular], @@ -531,21 +495,6 @@ exports[`SettingsComponent renders 1`] = ` "databaseAccount": [Function], "databases": [Function], "defaultExperience": [Function], - "deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane { - "collectionIdConfirmation": [Function], - "collectionIdConfirmationText": [Function], - "container": [Circular], - "containerDeleteFeedback": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "id": "deletecollectionconfirmationpane", - "isExecuting": [Function], - "isTemplateReady": [Function], - "recordDeleteFeedback": [Function], - "title": [Function], - "visible": [Function], - }, "deleteCollectionText": [Function], "deleteDatabaseText": [Function], "editTableEntityPane": EditTableEntityPane { @@ -614,9 +563,6 @@ exports[`SettingsComponent renders 1`] = ` "isMongoIndexingEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], - "isPreferredApiCassandra": [Function], - "isPreferredApiDocumentDB": [Function], - "isPreferredApiGraph": [Function], "isPreferredApiMongoDB": [Function], "isPreferredApiTable": [Function], "isPublishNotebookPaneEnabled": [Function], @@ -656,27 +602,6 @@ exports[`SettingsComponent renders 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, - "querySelectPane": QuerySelectPane { - "allSelected": [Function], - "anyColumnSelected": [Function], - "availableColumnsTableQueryLabel": "Available Columns", - "canSelectAll": [Function], - "columnOptions": [Function], - "container": [Circular], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "handleClick": [Function], - "id": "queryselectpane", - "instructionLabel": "Select the columns that you want to query.", - "isExecuting": [Function], - "isTemplateReady": [Function], - "noColumnSelectedWarning": "At least one column should be selected.", - "selectedColumnOption": null, - "title": [Function], - "titleLabel": "Select Columns", - "visible": [Function], - }, "refreshDatabaseAccount": [Function], "refreshNotebookList": [Function], "refreshTreeTitle": [Function], @@ -868,21 +793,6 @@ exports[`SettingsComponent renders 1`] = ` "useIndexingForSharedThroughput": [Function], "visible": [Function], }, - DeleteCollectionConfirmationPane { - "collectionIdConfirmation": [Function], - "collectionIdConfirmationText": [Function], - "container": [Circular], - "containerDeleteFeedback": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "id": "deletecollectionconfirmationpane", - "isExecuting": [Function], - "isTemplateReady": [Function], - "recordDeleteFeedback": [Function], - "title": [Function], - "visible": [Function], - }, GraphStylingPane { "container": [Circular], "firstFieldHasFocus": [Function], @@ -973,27 +883,6 @@ exports[`SettingsComponent renders 1`] = ` "title": [Function], "visible": [Function], }, - QuerySelectPane { - "allSelected": [Function], - "anyColumnSelected": [Function], - "availableColumnsTableQueryLabel": "Available Columns", - "canSelectAll": [Function], - "columnOptions": [Function], - "container": [Circular], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "handleClick": [Function], - "id": "queryselectpane", - "instructionLabel": "Select the columns that you want to query.", - "isExecuting": [Function], - "isTemplateReady": [Function], - "noColumnSelectedWarning": "At least one column should be selected.", - "selectedColumnOption": null, - "title": [Function], - "titleLabel": "Select Columns", - "visible": [Function], - }, NewVertexPane { "buildString": [Function], "container": [Circular], @@ -1284,21 +1173,6 @@ exports[`SettingsComponent renders 1`] = ` "databaseAccount": [Function], "databases": [Function], "defaultExperience": [Function], - "deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane { - "collectionIdConfirmation": [Function], - "collectionIdConfirmationText": [Function], - "container": [Circular], - "containerDeleteFeedback": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "id": "deletecollectionconfirmationpane", - "isExecuting": [Function], - "isTemplateReady": [Function], - "recordDeleteFeedback": [Function], - "title": [Function], - "visible": [Function], - }, "deleteCollectionText": [Function], "deleteDatabaseText": [Function], "editTableEntityPane": EditTableEntityPane { @@ -1367,9 +1241,6 @@ exports[`SettingsComponent renders 1`] = ` "isMongoIndexingEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], - "isPreferredApiCassandra": [Function], - "isPreferredApiDocumentDB": [Function], - "isPreferredApiGraph": [Function], "isPreferredApiMongoDB": [Function], "isPreferredApiTable": [Function], "isPublishNotebookPaneEnabled": [Function], @@ -1409,27 +1280,6 @@ exports[`SettingsComponent renders 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, - "querySelectPane": QuerySelectPane { - "allSelected": [Function], - "anyColumnSelected": [Function], - "availableColumnsTableQueryLabel": "Available Columns", - "canSelectAll": [Function], - "columnOptions": [Function], - "container": [Circular], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "handleClick": [Function], - "id": "queryselectpane", - "instructionLabel": "Select the columns that you want to query.", - "isExecuting": [Function], - "isTemplateReady": [Function], - "noColumnSelectedWarning": "At least one column should be selected.", - "selectedColumnOption": null, - "title": [Function], - "titleLabel": "Select Columns", - "visible": [Function], - }, "refreshDatabaseAccount": [Function], "refreshNotebookList": [Function], "refreshTreeTitle": [Function], @@ -1634,21 +1484,6 @@ exports[`SettingsComponent renders 1`] = ` "useIndexingForSharedThroughput": [Function], "visible": [Function], }, - DeleteCollectionConfirmationPane { - "collectionIdConfirmation": [Function], - "collectionIdConfirmationText": [Function], - "container": [Circular], - "containerDeleteFeedback": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "id": "deletecollectionconfirmationpane", - "isExecuting": [Function], - "isTemplateReady": [Function], - "recordDeleteFeedback": [Function], - "title": [Function], - "visible": [Function], - }, GraphStylingPane { "container": [Circular], "firstFieldHasFocus": [Function], @@ -1739,27 +1574,6 @@ exports[`SettingsComponent renders 1`] = ` "title": [Function], "visible": [Function], }, - QuerySelectPane { - "allSelected": [Function], - "anyColumnSelected": [Function], - "availableColumnsTableQueryLabel": "Available Columns", - "canSelectAll": [Function], - "columnOptions": [Function], - "container": [Circular], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "handleClick": [Function], - "id": "queryselectpane", - "instructionLabel": "Select the columns that you want to query.", - "isExecuting": [Function], - "isTemplateReady": [Function], - "noColumnSelectedWarning": "At least one column should be selected.", - "selectedColumnOption": null, - "title": [Function], - "titleLabel": "Select Columns", - "visible": [Function], - }, NewVertexPane { "buildString": [Function], "container": [Circular], @@ -2050,21 +1864,6 @@ exports[`SettingsComponent renders 1`] = ` "databaseAccount": [Function], "databases": [Function], "defaultExperience": [Function], - "deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane { - "collectionIdConfirmation": [Function], - "collectionIdConfirmationText": [Function], - "container": [Circular], - "containerDeleteFeedback": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "id": "deletecollectionconfirmationpane", - "isExecuting": [Function], - "isTemplateReady": [Function], - "recordDeleteFeedback": [Function], - "title": [Function], - "visible": [Function], - }, "deleteCollectionText": [Function], "deleteDatabaseText": [Function], "editTableEntityPane": EditTableEntityPane { @@ -2133,9 +1932,6 @@ exports[`SettingsComponent renders 1`] = ` "isMongoIndexingEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], - "isPreferredApiCassandra": [Function], - "isPreferredApiDocumentDB": [Function], - "isPreferredApiGraph": [Function], "isPreferredApiMongoDB": [Function], "isPreferredApiTable": [Function], "isPublishNotebookPaneEnabled": [Function], @@ -2175,27 +1971,6 @@ exports[`SettingsComponent renders 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, - "querySelectPane": QuerySelectPane { - "allSelected": [Function], - "anyColumnSelected": [Function], - "availableColumnsTableQueryLabel": "Available Columns", - "canSelectAll": [Function], - "columnOptions": [Function], - "container": [Circular], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "handleClick": [Function], - "id": "queryselectpane", - "instructionLabel": "Select the columns that you want to query.", - "isExecuting": [Function], - "isTemplateReady": [Function], - "noColumnSelectedWarning": "At least one column should be selected.", - "selectedColumnOption": null, - "title": [Function], - "titleLabel": "Select Columns", - "visible": [Function], - }, "refreshDatabaseAccount": [Function], "refreshNotebookList": [Function], "refreshTreeTitle": [Function], @@ -2387,21 +2162,6 @@ exports[`SettingsComponent renders 1`] = ` "useIndexingForSharedThroughput": [Function], "visible": [Function], }, - DeleteCollectionConfirmationPane { - "collectionIdConfirmation": [Function], - "collectionIdConfirmationText": [Function], - "container": [Circular], - "containerDeleteFeedback": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "id": "deletecollectionconfirmationpane", - "isExecuting": [Function], - "isTemplateReady": [Function], - "recordDeleteFeedback": [Function], - "title": [Function], - "visible": [Function], - }, GraphStylingPane { "container": [Circular], "firstFieldHasFocus": [Function], @@ -2492,27 +2252,6 @@ exports[`SettingsComponent renders 1`] = ` "title": [Function], "visible": [Function], }, - QuerySelectPane { - "allSelected": [Function], - "anyColumnSelected": [Function], - "availableColumnsTableQueryLabel": "Available Columns", - "canSelectAll": [Function], - "columnOptions": [Function], - "container": [Circular], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "handleClick": [Function], - "id": "queryselectpane", - "instructionLabel": "Select the columns that you want to query.", - "isExecuting": [Function], - "isTemplateReady": [Function], - "noColumnSelectedWarning": "At least one column should be selected.", - "selectedColumnOption": null, - "title": [Function], - "titleLabel": "Select Columns", - "visible": [Function], - }, NewVertexPane { "buildString": [Function], "container": [Circular], @@ -2803,21 +2542,6 @@ exports[`SettingsComponent renders 1`] = ` "databaseAccount": [Function], "databases": [Function], "defaultExperience": [Function], - "deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane { - "collectionIdConfirmation": [Function], - "collectionIdConfirmationText": [Function], - "container": [Circular], - "containerDeleteFeedback": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "id": "deletecollectionconfirmationpane", - "isExecuting": [Function], - "isTemplateReady": [Function], - "recordDeleteFeedback": [Function], - "title": [Function], - "visible": [Function], - }, "deleteCollectionText": [Function], "deleteDatabaseText": [Function], "editTableEntityPane": EditTableEntityPane { @@ -2886,9 +2610,6 @@ exports[`SettingsComponent renders 1`] = ` "isMongoIndexingEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], - "isPreferredApiCassandra": [Function], - "isPreferredApiDocumentDB": [Function], - "isPreferredApiGraph": [Function], "isPreferredApiMongoDB": [Function], "isPreferredApiTable": [Function], "isPublishNotebookPaneEnabled": [Function], @@ -2928,27 +2649,6 @@ exports[`SettingsComponent renders 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, - "querySelectPane": QuerySelectPane { - "allSelected": [Function], - "anyColumnSelected": [Function], - "availableColumnsTableQueryLabel": "Available Columns", - "canSelectAll": [Function], - "columnOptions": [Function], - "container": [Circular], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "handleClick": [Function], - "id": "queryselectpane", - "instructionLabel": "Select the columns that you want to query.", - "isExecuting": [Function], - "isTemplateReady": [Function], - "noColumnSelectedWarning": "At least one column should be selected.", - "selectedColumnOption": null, - "title": [Function], - "titleLabel": "Select Columns", - "visible": [Function], - }, "refreshDatabaseAccount": [Function], "refreshNotebookList": [Function], "refreshTreeTitle": [Function], diff --git a/src/Explorer/DataSamples/ContainerSampleGenerator.test.ts b/src/Explorer/DataSamples/ContainerSampleGenerator.test.ts index 13ec16270..60308be0a 100644 --- a/src/Explorer/DataSamples/ContainerSampleGenerator.test.ts +++ b/src/Explorer/DataSamples/ContainerSampleGenerator.test.ts @@ -4,6 +4,7 @@ jest.mock("../../Common/dataAccess/createDocument"); import * as ko from "knockout"; import Q from "q"; import { createDocument } from "../../Common/dataAccess/createDocument"; +import { DatabaseAccount } from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; import { updateUserContext } from "../../UserContext"; import Explorer from "../Explorer"; @@ -13,11 +14,8 @@ describe("ContainerSampleGenerator", () => { const createExplorerStub = (database: ViewModels.Database): Explorer => { const explorerStub = {} as Explorer; explorerStub.databases = ko.observableArray([database]); - explorerStub.isPreferredApiGraph = ko.computed(() => false); explorerStub.isPreferredApiMongoDB = ko.computed(() => false); - explorerStub.isPreferredApiDocumentDB = ko.computed(() => false); explorerStub.isPreferredApiTable = ko.computed(() => false); - explorerStub.isPreferredApiCassandra = ko.computed(() => false); explorerStub.canExceedMaximumValue = ko.computed(() => false); explorerStub.findDatabaseWithId = () => database; explorerStub.refreshAllDatabases = () => Q.resolve(); @@ -31,7 +29,7 @@ describe("ContainerSampleGenerator", () => { it("should insert documents for sql API account", async () => { const sampleCollectionId = "SampleCollection"; const sampleDatabaseId = "SampleDB"; - + updateUserContext({}); const sampleData = { databaseId: sampleDatabaseId, offerThroughput: 400, @@ -66,7 +64,7 @@ describe("ContainerSampleGenerator", () => { database.findCollectionWithId = () => collection; const explorerStub = createExplorerStub(database); - explorerStub.isPreferredApiDocumentDB = ko.computed(() => true); + const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub); generator.setData(sampleData); @@ -116,7 +114,13 @@ describe("ContainerSampleGenerator", () => { collection.databaseId = database.id(); const explorerStub = createExplorerStub(database); - explorerStub.isPreferredApiGraph = ko.computed(() => true); + updateUserContext({ + databaseAccount: { + properties: { + capabilities: [{ name: "EnableGremlin" }], + }, + } as DatabaseAccount, + }); const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub); generator.setData(sampleData); @@ -125,31 +129,45 @@ describe("ContainerSampleGenerator", () => { }); it("should not create any sample for Mongo API account", async () => { - const experience = "not supported api"; + const experience = "Sample generation not supported for this API Mongo"; const explorerStub = createExplorerStub(undefined); - explorerStub.isPreferredApiMongoDB = ko.computed(() => true); - explorerStub.defaultExperience = ko.observable(experience); + updateUserContext({ + databaseAccount: { + properties: { + capabilities: [{ name: "EnableMongo" }], + }, + } as DatabaseAccount, + }); // Rejects with error that contains experience expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience); }); it("should not create any sample for Table API account", async () => { - const experience = "not supported api"; + const experience = "Sample generation not supported for this API Tables"; const explorerStub = createExplorerStub(undefined); - explorerStub.isPreferredApiTable = ko.computed(() => true); - explorerStub.defaultExperience = ko.observable(experience); + updateUserContext({ + databaseAccount: { + properties: { + capabilities: [{ name: "EnableTable" }], + }, + } as DatabaseAccount, + }); // Rejects with error that contains experience await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience); }); it("should not create any sample for Cassandra API account", async () => { - const experience = "not supported api"; + const experience = "Sample generation not supported for this API Cassandra"; + updateUserContext({ + databaseAccount: { + properties: { + capabilities: [{ name: "EnableCassandra" }], + }, + } as DatabaseAccount, + }); const explorerStub = createExplorerStub(undefined); - explorerStub.isPreferredApiCassandra = ko.computed(() => true); - explorerStub.defaultExperience = ko.observable(experience); - // Rejects with error that contains experience await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience); }); diff --git a/src/Explorer/DataSamples/ContainerSampleGenerator.ts b/src/Explorer/DataSamples/ContainerSampleGenerator.ts index 6c1270a83..7d6b42453 100644 --- a/src/Explorer/DataSamples/ContainerSampleGenerator.ts +++ b/src/Explorer/DataSamples/ContainerSampleGenerator.ts @@ -1,12 +1,12 @@ -import * as DataModels from "../../Contracts/DataModels"; -import * as ViewModels from "../../Contracts/ViewModels"; -import GraphTab from ".././Tabs/GraphTab"; -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 * as DataModels from "../../Contracts/DataModels"; +import * as ViewModels from "../../Contracts/ViewModels"; import { userContext } from "../../UserContext"; +import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; +import GraphTab from ".././Tabs/GraphTab"; +import Explorer from "../Explorer"; +import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient"; interface SampleDataFile extends DataModels.CreateCollectionParams { data: any[]; @@ -23,16 +23,16 @@ export class ContainerSampleGenerator { public static async createSampleGeneratorAsync(container: Explorer): Promise { const generator = new ContainerSampleGenerator(container); let dataFileContent: any; - if (container.isPreferredApiGraph()) { + if (userContext.apiType === "Gremlin") { dataFileContent = await import( /* webpackChunkName: "gremlinSampleJsonData" */ "../../../sampleData/gremlinSampleData.json" ); - } else if (container.isPreferredApiDocumentDB()) { + } else if (userContext.apiType === "SQL") { dataFileContent = await import( /* webpackChunkName: "sqlSampleJsonData" */ "../../../sampleData/sqlSampleData.json" ); } else { - return Promise.reject(`Sample generation not supported for this API ${container.defaultExperience()}`); + return Promise.reject(`Sample generation not supported for this API ${userContext.apiType}`); } generator.setData(dataFileContent); @@ -73,7 +73,7 @@ export class ContainerSampleGenerator { } const promises: Q.Promise[] = []; - if (this.container.isPreferredApiGraph()) { + if (userContext.apiType === "Gremlin") { // 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) const queries: string[] = this.sampleDataFile.data; diff --git a/src/Explorer/DataSamples/DataSamplesUtil.ts b/src/Explorer/DataSamples/DataSamplesUtil.ts index aa3d01253..809964352 100644 --- a/src/Explorer/DataSamples/DataSamplesUtil.ts +++ b/src/Explorer/DataSamples/DataSamplesUtil.ts @@ -1,4 +1,5 @@ import * as ViewModels from "../../Contracts/ViewModels"; +import { userContext } from "../../UserContext"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import Explorer from "../Explorer"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; @@ -56,6 +57,6 @@ export class DataSamplesUtil { } public isSampleContainerCreationSupported(): boolean { - return this.container.isPreferredApiDocumentDB() || this.container.isPreferredApiGraph(); + return userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; } } diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 6402216c1..a056ca735 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -36,6 +36,7 @@ import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationU import { stringToBlob } from "../Utils/BlobUtils"; import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; +import * as PricingUtils from "../Utils/PricingUtils"; import * as ComponentRegisterer from "./ComponentRegisterer"; import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker"; import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent"; @@ -48,11 +49,10 @@ import { NotebookContentItem, NotebookContentItemType } from "./Notebook/Noteboo import { NotebookUtil } from "./Notebook/NotebookUtil"; import AddCollectionPane from "./Panes/AddCollectionPane"; import { AddCollectionPanel } from "./Panes/AddCollectionPanel"; -import { AddDatabasePane } from "./Panes/AddDatabasePane"; +import AddDatabasePane from "./Panes/AddDatabasePane"; import { BrowseQueriesPanel } from "./Panes/BrowseQueriesPanel"; import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane"; import { ContextualPaneBase } from "./Panes/ContextualPaneBase"; -import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane"; import { DeleteCollectionConfirmationPanel } from "./Panes/DeleteCollectionConfirmationPanel"; import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel"; import { ExecuteSprocParamsPanel } from "./Panes/ExecuteSprocParamsPanel"; @@ -65,9 +65,10 @@ import { SetupNotebooksPane } from "./Panes/SetupNotebooksPane"; import { StringInputPane } from "./Panes/StringInputPane"; import AddTableEntityPane from "./Panes/Tables/AddTableEntityPane"; import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane"; -import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane"; +import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel"; import { UploadFilePane } from "./Panes/UploadFilePane"; import { UploadItemsPane } from "./Panes/UploadItemsPane"; +import QueryViewModel from "./Tables/QueryBuilder/QueryViewModel"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; import TabsBase from "./Tabs/TabsBase"; @@ -78,8 +79,6 @@ import ResourceTokenCollection from "./Tree/ResourceTokenCollection"; import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter"; import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken"; import StoredProcedure from "./Tree/StoredProcedure"; -import Trigger from "./Tree/Trigger"; -import UserDefinedFunction from "./Tree/UserDefinedFunction"; BindingHandlersRegisterer.registerBindingHandlers(); // Hold a reference to ComponentRegisterer to prevent transpiler to ignore import @@ -93,6 +92,7 @@ export interface ExplorerParams { closeSidePanel: () => void; closeDialog: () => void; openDialog: (props: DialogProps) => void; + tabsManager: TabsManager; } export default class Explorer { @@ -116,26 +116,12 @@ export default class Explorer { * Use userContext.apiType instead * */ public defaultExperience: ko.Observable; - /** - * @deprecated - * Compare a string with userContext.apiType instead: userContext.apiType === "SQL" - * */ - public isPreferredApiDocumentDB: ko.Computed; - /** - * @deprecated - * Compare a string with userContext.apiType instead: userContext.apiType === "Cassandra" - * */ - public isPreferredApiCassandra: ko.Computed; /** * @deprecated * Compare a string with userContext.apiType instead: userContext.apiType === "Mongo" * */ public isPreferredApiMongoDB: ko.Computed; - /** - * @deprecated - * Compare a string with userContext.apiType instead: userContext.apiType === "Gremlin" - * */ - public isPreferredApiGraph: ko.Computed; + /** * @deprecated * Compare a string with userContext.apiType instead: userContext.apiType === "Tables" @@ -188,12 +174,11 @@ export default class Explorer { public tabsManager: TabsManager; // Contextual panes + public addDatabasePane: AddDatabasePane; public addCollectionPane: AddCollectionPane; - public deleteCollectionConfirmationPane: DeleteCollectionConfirmationPane; public graphStylingPane: GraphStylingPane; public addTableEntityPane: AddTableEntityPane; public editTableEntityPane: EditTableEntityPane; - public querySelectPane: QuerySelectPane; public newVertexPane: NewVertexPane; public cassandraAddCollectionPane: CassandraAddCollectionPane; public stringInputPane: StringInputPane; @@ -420,20 +405,6 @@ 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(); @@ -497,7 +468,9 @@ export default class Explorer { this.isHostedDataExplorerEnabled = ko.computed( () => - configContext.platform === Platform.Portal && !this.isRunningOnNationalCloud() && !this.isPreferredApiGraph() + configContext.platform === Platform.Portal && + !this.isRunningOnNationalCloud() && + userContext.apiType !== "Gremlin" ); this.isRightPanelV2Enabled = ko.computed(() => userContext.features.enableRightPanelV2); this.selectedDatabaseId = ko.computed(() => { @@ -521,16 +494,16 @@ export default class Explorer { } }); - this.addCollectionPane = new AddCollectionPane({ - isPreferredApiTable: ko.computed(() => this.isPreferredApiTable()), - id: "addcollectionpane", + this.addDatabasePane = new AddDatabasePane({ + id: "adddatabasepane", visible: ko.observable(false), container: this, }); - this.deleteCollectionConfirmationPane = new DeleteCollectionConfirmationPane({ - id: "deletecollectionconfirmationpane", + this.addCollectionPane = new AddCollectionPane({ + isPreferredApiTable: ko.computed(() => this.isPreferredApiTable()), + id: "addcollectionpane", visible: ko.observable(false), container: this, @@ -557,13 +530,6 @@ export default class Explorer { container: this, }); - this.querySelectPane = new QuerySelectPane({ - id: "queryselectpane", - visible: ko.observable(false), - - container: this, - }); - this.newVertexPane = new NewVertexPane({ id: "newvertexpane", visible: ko.observable(false), @@ -592,21 +558,26 @@ export default class Explorer { container: this, }); - this.tabsManager = new TabsManager(); + this.tabsManager = params?.tabsManager ?? new TabsManager(); + this.tabsManager.openedTabs.subscribe((tabs) => { + if (tabs.length === 0) { + this.selectedNode(undefined); + this.onUpdateTabsButtons([]); + } + }); this._panes = [ + this.addDatabasePane, this.addCollectionPane, - this.deleteCollectionConfirmationPane, this.graphStylingPane, this.addTableEntityPane, this.editTableEntityPane, - this.querySelectPane, this.newVertexPane, this.cassandraAddCollectionPane, 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); document.addEventListener( @@ -634,8 +605,6 @@ export default class Explorer { this.addCollectionPane.collectionWithThroughputInSharedTitle( "Provision dedicated throughput for this container" ); - this.deleteCollectionConfirmationPane.title("Delete Container"); - this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the container id"); this.refreshTreeTitle("Refresh containers"); break; case "Mongo": @@ -662,8 +631,6 @@ export default class Explorer { this.addCollectionPane.title("Add Graph"); this.addCollectionPane.collectionIdTitle("Graph id"); 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"); break; case "Tables": @@ -679,8 +646,6 @@ export default class Explorer { this.refreshTreeTitle("Refresh tables"); this.addTableEntityPane.title("Add 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(); break; case "Cassandra": @@ -696,8 +661,6 @@ export default class Explorer { this.refreshTreeTitle("Refresh tables"); this.addTableEntityPane.title("Add 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(); break; } @@ -915,10 +878,8 @@ export default class Explorer { // TODO: Refactor const deferred: Q.Deferred = Q.defer(); - this._setLoadingStatusText("Fetching databases..."); readDatabases().then( (databases: DataModels.Database[]) => { - this._setLoadingStatusText("Successfully fetched databases."); TelemetryProcessor.traceSuccess( Action.LoadDatabases, { @@ -931,20 +892,16 @@ export default class Explorer { this.addDatabasesToList(deltaDatabases.toAdd); this.deleteDatabasesFromList(deltaDatabases.toDelete); this.selectedNode(currentlySelectedNode); - this._setLoadingStatusText("Fetching containers..."); this.refreshAndExpandNewDatabases(deltaDatabases.toAdd).then( () => { - this._setLoadingStatusText("Successfully fetched containers."); deferred.resolve(); }, (reason) => { - this._setLoadingStatusText("Failed to fetch containers."); deferred.reject(reason); } ); }, (error) => { - this._setLoadingStatusText("Failed to fetch databases."); deferred.reject(error); const errorMessage = getErrorMessage(error); TelemetryProcessor.traceFailure( @@ -1020,7 +977,7 @@ export default class Explorer { // Facade public provideFeedbackEmail = () => { - window.open(Constants.Urls.feedbackEmail, "_self"); + window.open(Constants.Urls.feedbackEmail, "_blank"); }; public async getArcadiaToken(): Promise { @@ -1291,49 +1248,6 @@ export default class Explorer { : 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 { this._panes.forEach((pane: ContextualPaneBase) => pane.close()); } @@ -1662,7 +1576,6 @@ export default class Explorer { collection: null, masterKey: userContext.masterKey || "", hashLocation: "notebooks", - isActive: ko.observable(false), isTabsContentExpanded: ko.observable(true), onLoadStartKey: null, onUpdateTabsButtons: this.onUpdateTabsButtons, @@ -2068,7 +1981,6 @@ export default class Explorer { tabPath: title, collection: null, hashLocation: hashLocation, - isActive: ko.observable(false), isTabsContentExpanded: ko.observable(true), onLoadStartKey: null, onUpdateTabsButtons: this.onUpdateTabsButtons, @@ -2173,7 +2085,7 @@ export default class Explorer { } public onNewCollectionClicked(): void { - if (this.isPreferredApiCassandra()) { + if (userContext.apiType === "Cassandra") { this.cassandraAddCollectionPane.open(); } else if (userContext.features.enableReactPane) { this.openAddCollectionPanel(); @@ -2216,32 +2128,6 @@ export default class Explorer { } } - private _setLoadingStatusText(text: string, title: string = "Welcome to Azure Cosmos DB") { - if (!text) { - return; - } - - const loadingText = document.getElementById("explorerLoadingStatusText"); - if (!loadingText) { - Logger.logError( - "getElementById('explorerLoadingStatusText') failed to find element", - "Explorer/_setLoadingStatusText" - ); - return; - } - loadingText.innerHTML = text; - - const loadingTitle = document.getElementById("explorerLoadingStatusTitle"); - if (!loadingTitle) { - Logger.logError( - "getElementById('explorerLoadingStatusTitle') failed to find element", - "Explorer/_setLoadingStatusText" - ); - } else { - loadingTitle.innerHTML = title; - } - } - private _openSetupNotebooksPaneForQuickstart(): void { const title = "Enable Notebooks (Preview)"; const description = @@ -2309,16 +2195,15 @@ export default class Explorer { } public openDeleteCollectionConfirmationPane(): void { - userContext.features.enableKOPanel - ? this.deleteCollectionConfirmationPane.open() - : this.openSidePanel( - "Delete Collection", - this.closeSidePanel()} - openNotificationConsole={() => this.expandConsole()} - /> - ); + let collectionName = PricingUtils.getCollectionName(userContext.defaultExperience); + this.openSidePanel( + "Delete " + collectionName, + + ); } public openDeleteDatabaseConfirmationPane(): void { @@ -2341,10 +2226,14 @@ export default class Explorer { this.openSidePanel("Settings", ); } - public openExecuteSprocParamsPanel(): void { + public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void { this.openSidePanel( "Input parameters", - this.closeSidePanel()} /> + this.closeSidePanel()} + /> ); } @@ -2389,4 +2278,11 @@ export default class Explorer { /> ); } + + public openTableSelectQueryPanel(queryViewModal: QueryViewModel): void { + this.openSidePanel( + "Select Column", + + ); + } } diff --git a/src/Explorer/Graph/GraphExplorerComponent/GraphUtil.ts b/src/Explorer/Graph/GraphExplorerComponent/GraphUtil.ts index 2faf25c67..fee98ea2f 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GraphUtil.ts +++ b/src/Explorer/Graph/GraphExplorerComponent/GraphUtil.ts @@ -1,6 +1,6 @@ -import { NeighborVertexBasicInfo } from "./GraphExplorer"; -import * as GraphData from "./GraphData"; import * as ViewModels from "../../../Contracts/ViewModels"; +import * as GraphData from "./GraphData"; +import { NeighborVertexBasicInfo } from "./GraphExplorer"; interface JoinArrayMaxCharOutput { result: string; // string output @@ -13,9 +13,9 @@ interface EdgePropertyType { inV?: string; } -export function getNeighborTitle(neighbor: NeighborVertexBasicInfo): string { +export const getNeighborTitle = (neighbor: NeighborVertexBasicInfo): string => { return `edge id: ${neighbor.edgeId}, vertex id: ${neighbor.id}`; -} +}; /** * Collect all edges from this node @@ -23,11 +23,11 @@ export function getNeighborTitle(neighbor: NeighborVertexBasicInfo): string { * @param graphData * @param newNodes (optional) object describing new nodes encountered */ -export function createEdgesfromNode( +export const createEdgesfromNode = ( vertex: GraphData.GremlinVertex, graphData: GraphData.GraphData, newNodes?: { [id: string]: boolean } -): void { +): void => { if (Object.prototype.hasOwnProperty.call(vertex, "outE")) { const outE = vertex.outE; for (const label in outE) { @@ -66,7 +66,7 @@ export function createEdgesfromNode( }); } } -} +}; /** * From ['id1', 'id2', 'idn'] build the following string "'id1','id2','idn'". @@ -75,7 +75,7 @@ export function createEdgesfromNode( * @param maxSize * @return */ -export function getLimitedArrayString(array: string[], maxSize: number): JoinArrayMaxCharOutput { +export const getLimitedArrayString = (array: string[], maxSize: number): JoinArrayMaxCharOutput => { if (!array || array.length === 0 || array[0].length + 2 > maxSize) { return { result: "", consumedCount: 0 }; } @@ -96,16 +96,16 @@ export function getLimitedArrayString(array: string[], maxSize: number): JoinArr result: output, consumedCount: i + 1, }; -} +}; -export function createFetchEdgePairQuery( +export const createFetchEdgePairQuery = ( outE: boolean, pkid: string, excludedEdgeIds: string[], startIndex: number, pageSize: number, withoutStepArgMaxLenght: number -): string { +): string => { let gremlinQuery: string; if (excludedEdgeIds.length > 0) { // build a string up to max char @@ -128,15 +128,15 @@ export function createFetchEdgePairQuery( }().as('v').select('e', 'v')`; } return gremlinQuery; -} +}; /** * Trim graph */ -export function trimGraph( +export const trimGraph = ( currentRoot: GraphData.GremlinVertex, graphData: GraphData.GraphData -) { +): void => { const importantNodes = [currentRoot.id].concat(currentRoot._ancestorsId); graphData.unloadAllVertices(importantNodes); @@ -144,32 +144,32 @@ export function trimGraph( $.each(graphData.ids, (index: number, id: string) => { graphData.getVertexById(id)._isFixedPosition = importantNodes.indexOf(id) !== -1; }); -} +}; -export function addRootChildToGraph( +export const addRootChildToGraph = ( root: GraphData.GremlinVertex, child: GraphData.GremlinVertex, graphData: GraphData.GraphData -) { +): void => { child._ancestorsId = (root._ancestorsId || []).concat([root.id]); graphData.addVertex(child); createEdgesfromNode(child, graphData); graphData.addNeighborInfo(child); -} +}; /** * TODO Perform minimal substitution to prevent breaking gremlin query and allow \"" for now. * @param value */ -export function escapeDoubleQuotes(value: string): string { +export const escapeDoubleQuotes = (value: string): string => { return value === undefined ? value : value.replace(/"/g, '\\"'); -} +}; /** * Surround with double-quotes if val is a string. * @param val */ -export function getQuotedPropValue(ip: ViewModels.InputPropertyValue): string { +export const getQuotedPropValue = (ip: ViewModels.InputPropertyValue): string => { switch (ip.type) { case "number": case "boolean": @@ -179,12 +179,12 @@ export function getQuotedPropValue(ip: ViewModels.InputPropertyValue): string { default: return `"${escapeDoubleQuotes(ip.value as string)}"`; } -} +}; /** * TODO Perform minimal substitution to prevent breaking gremlin query and allow \' for now. * @param value */ -export function escapeSingleQuotes(value: string): string { +export const escapeSingleQuotes = (value: string): string => { return value === undefined ? value : value.replace(/'/g, "\\'"); -} +}; diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index 1afce01fb..d93407c22 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -4,15 +4,15 @@ * and update any knockout observables passed from the parent. */ import * as ko from "knockout"; +import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar"; import * as React from "react"; import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; -import * as ViewModels from "../../../Contracts/ViewModels"; -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 Explorer from "../../Explorer"; +import * as ViewModels from "../../../Contracts/ViewModels"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; +import Explorer from "../../Explorer"; +import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory"; +import * as CommandBarUtil from "./CommandBarUtil"; export class CommandBarComponentAdapter implements ReactAdapter { public parameters: ko.Observable; @@ -23,17 +23,14 @@ export class CommandBarComponentAdapter implements ReactAdapter { constructor(container: Explorer) { this.container = container; this.tabsButtons = []; - this.isNotebookTabActive = ko.computed(() => - container.tabsManager.isTabActive(ViewModels.CollectionTabKind.NotebookV2) + this.isNotebookTabActive = ko.computed( + () => container.tabsManager.activeTab()?.tabKind === ViewModels.CollectionTabKind.NotebookV2 ); // These are the parameters watched by the react binding that will trigger a renderComponent() if one of the ko mutates const toWatch = [ container.isPreferredApiTable, container.isPreferredApiMongoDB, - container.isPreferredApiDocumentDB, - container.isPreferredApiCassandra, - container.isPreferredApiGraph, container.deleteCollectionText, container.deleteDatabaseText, container.addCollectionText, diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts index 450f79d26..12deb7569 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts @@ -1,5 +1,6 @@ import * as ko from "knockout"; import { AuthType } from "../../../AuthType"; +import { DatabaseAccount } from "../../../Contracts/DataModels"; import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService"; import { updateUserContext } from "../../../UserContext"; import Explorer from "../../Explorer"; @@ -17,7 +18,6 @@ describe("CommandBarComponentButtonFactory tests", () => { mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.isPreferredApiTable = ko.computed(() => true); mockExplorer.isPreferredApiMongoDB = ko.computed(() => false); - mockExplorer.isPreferredApiCassandra = ko.computed(() => false); mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSynapseLinkUpdating = ko.observable(false); @@ -56,7 +56,6 @@ describe("CommandBarComponentButtonFactory tests", () => { mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.isPreferredApiTable = ko.computed(() => true); mockExplorer.isPreferredApiMongoDB = ko.computed(() => false); - mockExplorer.isPreferredApiCassandra = ko.computed(() => false); mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSynapseLinkUpdating = ko.observable(false); @@ -119,7 +118,6 @@ describe("CommandBarComponentButtonFactory tests", () => { mockExplorer = {} as Explorer; mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.isPreferredApiTable = ko.computed(() => true); - mockExplorer.isPreferredApiCassandra = ko.computed(() => false); mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSynapseLinkUpdating = ko.observable(false); @@ -208,15 +206,26 @@ describe("CommandBarComponentButtonFactory tests", () => { }); beforeEach(() => { - mockExplorer.isPreferredApiCassandra = ko.computed(() => true); + updateUserContext({ + databaseAccount: { + properties: { + capabilities: [{ name: "EnableCassandra" }], + }, + } as DatabaseAccount, + }); mockExplorer.isNotebookEnabled = ko.observable(false); mockExplorer.isNotebooksEnabledForAccount = ko.observable(false); mockExplorer.isRunningOnNationalCloud = ko.observable(false); }); it("Cassandra Api not available - button should be hidden", () => { - mockExplorer.isPreferredApiCassandra = ko.computed(() => false); - + updateUserContext({ + databaseAccount: { + properties: { + capabilities: [{ name: "EnableMongo" }], + }, + } as DatabaseAccount, + }); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); expect(openCassandraShellBtn).toBeUndefined(); @@ -281,7 +290,6 @@ describe("CommandBarComponentButtonFactory tests", () => { mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.isPreferredApiTable = ko.computed(() => true); mockExplorer.isPreferredApiMongoDB = ko.computed(() => false); - mockExplorer.isPreferredApiCassandra = ko.computed(() => false); mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSparkEnabled = ko.observable(true); @@ -337,7 +345,6 @@ describe("CommandBarComponentButtonFactory tests", () => { beforeAll(() => { mockExplorer = {} as Explorer; mockExplorer.addCollectionText = ko.observable("mockText"); - mockExplorer.isPreferredApiDocumentDB = ko.computed(() => true); mockExplorer.isDatabaseNodeOrNoneSelected = () => true; mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true); mockExplorer.isServerlessEnabled = ko.computed(() => false); @@ -347,6 +354,11 @@ describe("CommandBarComponentButtonFactory tests", () => { }); it("should only show New SQL Query and Open Query buttons", () => { + updateUserContext({ + databaseAccount: { + kind: "DocumentDB", + } as DatabaseAccount, + }); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); expect(buttons.length).toBe(2); expect(buttons[0].commandButtonLabel).toBe("New SQL Query"); diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index d47de56a2..5322a6803 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -74,7 +74,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto buttons.push(createOpenMongoTerminalButton(container)); } - if (container.isPreferredApiCassandra()) { + if (userContext.apiType === "Cassandra") { buttons.push(createOpenCassandraTerminalButton(container)); } } @@ -90,15 +90,15 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto buttons.push(createDivider()); } - const isSqlQuerySupported = container.isPreferredApiDocumentDB() || container.isPreferredApiGraph(); + const isSqlQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; if (isSqlQuerySupported) { const newSqlQueryBtn = createNewSQLQueryButton(container); buttons.push(newSqlQueryBtn); } const isSupportedOpenQueryApi = - container.isPreferredApiDocumentDB() || container.isPreferredApiMongoDB() || container.isPreferredApiGraph(); - const isSupportedOpenQueryFromDiskApi = container.isPreferredApiDocumentDB() || container.isPreferredApiGraph(); + userContext.apiType === "SQL" || container.isPreferredApiMongoDB() || userContext.apiType === "Gremlin"; + const isSupportedOpenQueryFromDiskApi = userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; if (isSupportedOpenQueryApi && container.selectedNode() && container.findSelectedCollection()) { const openQueryBtn = createOpenQueryButton(container); openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)]; @@ -107,7 +107,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto buttons.push(createOpenQueryFromDiskButton(container)); } - if (areScriptsSupported(container)) { + if (areScriptsSupported()) { const label = "New Stored Procedure"; const newStoredProcedureBtn: CommandButtonComponentProps = { iconSrc: AddStoredProcedureIcon, @@ -154,25 +154,18 @@ export function createContextCommandBarButtons(container: Explorer): CommandButt } export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] { - const buttons: CommandButtonComponentProps[] = []; - if (configContext.platform === Platform.Hosted) { - return buttons; - } - - if (!container.isPreferredApiCassandra()) { - const label = "Settings"; - const settingsPaneButton: CommandButtonComponentProps = { + const buttons: CommandButtonComponentProps[] = [ + { iconSrc: SettingsIcon, - iconAlt: label, + iconAlt: "Settings", onCommandClick: () => container.openSettingPane(), commandButtonLabel: undefined, - ariaLabel: label, - tooltipText: label, + ariaLabel: "Settings", + tooltipText: "Settings", hasPopup: true, disabled: false, - }; - buttons.push(settingsPaneButton); - } + }, + ]; if (container.isHostedDataExplorerEnabled()) { const label = "Open Full Screen"; @@ -223,8 +216,8 @@ export function createDivider(): CommandButtonComponentProps { }; } -function areScriptsSupported(container: Explorer): boolean { - return container.isPreferredApiDocumentDB() || container.isPreferredApiGraph(); +function areScriptsSupported(): boolean { + return userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; } function createNewCollectionGroup(container: Explorer): CommandButtonComponentProps { @@ -286,7 +279,8 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps { iconSrc: AddDatabaseIcon, iconAlt: label, onCommandClick: () => { - container.openAddDatabasePane(); + container.addDatabasePane.open(); + // container.openAddDatabasePane(); document.getElementById("linkAddDatabase").focus(); }, commandButtonLabel: label, @@ -296,7 +290,7 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps { } function createNewSQLQueryButton(container: Explorer): CommandButtonComponentProps { - if (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph()) { + if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") { const label = "New SQL Query"; return { iconSrc: AddSqlQueryIcon, @@ -310,7 +304,7 @@ function createNewSQLQueryButton(container: Explorer): CommandButtonComponentPro hasPopup: true, disabled: container.isDatabaseNodeOrNoneSelected(), }; - } else if (container.isPreferredApiMongoDB()) { + } else if (userContext.apiType === "Mongo") { const label = "New Query"; return { iconSrc: AddSqlQueryIcon, @@ -332,8 +326,7 @@ function createNewSQLQueryButton(container: Explorer): CommandButtonComponentPro export function createScriptCommandButtons(container: Explorer): CommandButtonComponentProps[] { const buttons: CommandButtonComponentProps[] = []; - const shouldEnableScriptsCommands: boolean = - !container.isDatabaseNodeOrNoneSelected() && areScriptsSupported(container); + const shouldEnableScriptsCommands: boolean = !container.isDatabaseNodeOrNoneSelected() && areScriptsSupported(); if (shouldEnableScriptsCommands) { const label = "New Stored Procedure"; diff --git a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx index 692793050..05df0f46f 100644 --- a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx +++ b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx @@ -1,9 +1,9 @@ -import React from "react"; import { shallow } from "enzyme"; +import React from "react"; import { - NotificationConsoleComponentProps, - NotificationConsoleComponent, ConsoleDataType, + NotificationConsoleComponent, + NotificationConsoleComponentProps, } from "./NotificationConsoleComponent"; describe("NotificationConsoleComponent", () => { @@ -12,7 +12,7 @@ describe("NotificationConsoleComponent", () => { consoleData: undefined, isConsoleExpanded: false, inProgressConsoleDataIdToBeDeleted: "", - setIsConsoleExpanded: (isExpanded: boolean): void => {}, + setIsConsoleExpanded: (): void => undefined, }; }; @@ -98,7 +98,7 @@ describe("NotificationConsoleComponent", () => { wrapper.setProps(props); expect(wrapper.find(".notificationConsoleData .date").text()).toEqual(date); expect(wrapper.find(".notificationConsoleData .message").text()).toEqual(message); - expect(wrapper.exists(`.notificationConsoleData .${iconClassName}`)); + expect(wrapper.exists(`.notificationConsoleData .${iconClassName}`)).toBe(true); }; it("renders progress notifications", () => { @@ -139,7 +139,7 @@ describe("NotificationConsoleComponent", () => { wrapper.setProps(props); wrapper.find(".clearNotificationsButton").simulate("click"); - expect(!wrapper.exists(".notificationConsoleData")); + expect(wrapper.exists(".notificationConsoleData")).toBe(true); }); it("collapses and hide content", () => { @@ -155,7 +155,7 @@ describe("NotificationConsoleComponent", () => { wrapper.setProps(props); wrapper.find(".notificationConsoleHeader").simulate("click"); - expect(!wrapper.exists(".notificationConsoleContent")); + expect(wrapper.exists(".notificationConsoleContent")).toBe(false); }); it("display latest data in header", () => { diff --git a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx index e0810e671..5e4c75179 100644 --- a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx +++ b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx @@ -2,19 +2,20 @@ * React component for control bar */ -import * as React from "react"; -import { ClientDefaults, KeyCodes } from "../../../Common/Constants"; -import AnimateHeight from "react-animate-height"; import { Dropdown, IDropdownOption } from "office-ui-fabric-react"; -import LoadingIcon from "../../../../images/loading.svg"; +import * as React from "react"; +import AnimateHeight from "react-animate-height"; +import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif"; +import ClearIcon from "../../../../images/Clear.svg"; import ErrorBlackIcon from "../../../../images/error_black.svg"; +import ErrorRedIcon from "../../../../images/error_red.svg"; import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg"; import InfoIcon from "../../../../images/info_color.svg"; -import ErrorRedIcon from "../../../../images/error_red.svg"; -import ClearIcon from "../../../../images/Clear.svg"; -import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif"; -import ChevronUpIcon from "../../../../images/QueryBuilder/CollapseChevronUp_16x.png"; +import LoadingIcon from "../../../../images/loading.svg"; import ChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png"; +import ChevronUpIcon from "../../../../images/QueryBuilder/CollapseChevronUp_16x.png"; +import { ClientDefaults, KeyCodes } from "../../../Common/Constants"; +import { userContext } from "../../../UserContext"; /** * Log levels @@ -76,7 +77,7 @@ export class NotificationConsoleComponent extends React.Component< public componentDidUpdate( prevProps: NotificationConsoleComponentProps, prevState: NotificationConsoleComponentState - ) { + ): void { const currentHeaderStatus = NotificationConsoleComponent.extractHeaderStatus(this.props.consoleData); if ( @@ -97,7 +98,7 @@ export class NotificationConsoleComponent extends React.Component< } } - public setElememntRef = (element: HTMLElement) => { + public setElememntRef = (element: HTMLElement): void => { this.consoleHeaderElement = element; }; @@ -116,7 +117,7 @@ export class NotificationConsoleComponent extends React.Component< className="notificationConsoleHeader" id="notificationConsoleHeader" ref={this.setElememntRef} - onClick={(event: React.MouseEvent) => this.expandCollapseConsole()} + onClick={() => this.expandCollapseConsole()} onKeyDown={(event: React.KeyboardEvent) => this.onExpandCollapseKeyPress(event)} tabIndex={0} > @@ -135,6 +136,7 @@ export class NotificationConsoleComponent extends React.Component< {numInfoItems} + {userContext.features.pr && } {this.state.headerStatus} @@ -304,3 +306,18 @@ 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 ( + <> + + + {ref} + + + ); +}; diff --git a/src/Explorer/Notebook/NotebookClientV2.ts b/src/Explorer/Notebook/NotebookClientV2.ts index 138384b6f..940be1e3a 100644 --- a/src/Explorer/Notebook/NotebookClientV2.ts +++ b/src/Explorer/Notebook/NotebookClientV2.ts @@ -1,15 +1,16 @@ // Manages all the redux logic for the notebook nteract code // TODO: Merge with NotebookClient? -import { NotebookWorkspaceConnectionInfo } from "../../Contracts/DataModels"; -import * as Constants from "../../Common/Constants"; -import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types"; - // Vendor modules import { actions, AppState, + ContentRecord, createHostRef, createKernelspecsRef, + HostRecord, + HostRef, + IContentProvider, + KernelspecsRef, makeAppRecord, makeCommsRecord, makeContentsRecord, @@ -19,23 +20,22 @@ import { makeJupyterHostRecord, makeStateRecord, makeTransformsRecord, - ContentRecord, - HostRecord, - HostRef, - KernelspecsRef, - IContentProvider, } from "@nteract/core"; +import { configOption, createConfigCollection, defineConfigOption } from "@nteract/mythic-configuration"; import { Media } from "@nteract/outputs"; import TransformVDOM from "@nteract/transform-vdom"; import * as Immutable from "immutable"; -import { Store, AnyAction, MiddlewareAPI, Middleware, Dispatch } from "redux"; - -import configureStore from "./NotebookComponent/store"; - import { Notification } from "react-notification-system"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +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 { configOption, createConfigCollection, defineConfigOption } from "@nteract/mythic-configuration"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import { userContext } from "../../UserContext"; +import configureStore from "./NotebookComponent/store"; +import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types"; +import SandboxJavaScript from "./NotebookRenderer/outputs/SandboxJavaScript"; +import SanitizedHTML from "./NotebookRenderer/outputs/SanitizedHTML"; export type KernelSpecsDisplay = { name: string; displayName: string }; @@ -168,8 +168,10 @@ export class NotebookClientV2 { "application/vnd.vega.v5+json": NullTransform, "application/vdom.v1+json": TransformVDOM, "application/json": Media.Json, - "application/javascript": Media.JavaScript, - "text/html": Media.HTML, + "application/javascript": userContext.features.sandboxNotebookOutputs + ? SandboxJavaScript + : Media.JavaScript, + "text/html": userContext.features.sandboxNotebookOutputs ? SanitizedHTML : Media.HTML, "text/markdown": Media.Markdown, "text/latex": Media.LaTeX, "image/svg+xml": Media.SVG, diff --git a/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx b/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx index ae75450be..75133d7f3 100644 --- a/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx +++ b/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx @@ -1,18 +1,20 @@ -import * as React from "react"; -import "./base.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 { KernelOutputError, StreamText } from "@nteract/outputs"; +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 TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media"; +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 "./default.css"; import "./NotebookReadOnlyRenderer.less"; +import IFrameOutputs from "./outputs/IFrameOutputs"; export interface NotebookRendererProps { contentRef: any; @@ -60,6 +62,16 @@ class NotebookReadOnlyRenderer extends React.Component { {{ prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef), + outputs: userContext.features.sandboxNotebookOutputs + ? (props: any) => ( + + + + + + + ) + : undefined, editor: { monaco: (props: PassedEditorProps) => this.props.hideInputs ? <> : , diff --git a/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx b/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx index bb16b103a..b78fdab97 100644 --- a/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx +++ b/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx @@ -1,37 +1,33 @@ -import * as React from "react"; -import "./base.css"; -import "./default.css"; - -import { RawCell, Cells, CodeCell, MarkdownCell } from "@nteract/stateful-components"; +import { CellId } from "@nteract/commutable"; +import { CellType } from "@nteract/commutable/src"; +import { actions, ContentRef } from "@nteract/core"; +import { KernelOutputError, StreamText } from "@nteract/outputs"; +import { Cells, CodeCell, 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 from "./Prompt"; -import { promptContent } from "./PromptContent"; - -import { AzureTheme } from "./AzureTheme"; +import TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media"; +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 { 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 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 { 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 IFrameOutputs from "./outputs/IFrameOutputs"; +import Prompt from "./Prompt"; +import { promptContent } from "./PromptContent"; +import StatusBar from "./StatusBar"; +import CellToolbar from "./Toolbar"; export interface NotebookRendererBaseProps { contentRef: any; @@ -112,6 +108,16 @@ class BaseNotebookRenderer extends React.Component { ), toolbar: () => , + outputs: userContext.features.sandboxNotebookOutputs + ? (props: any) => ( + + + + + + + ) + : undefined, }} ), diff --git a/src/Explorer/Notebook/NotebookRenderer/markdown-cell.tsx b/src/Explorer/Notebook/NotebookRenderer/markdown-cell.tsx new file mode 100644 index 000000000..9971cc802 --- /dev/null +++ b/src/Explorer/Notebook/NotebookRenderer/markdown-cell.tsx @@ -0,0 +1,160 @@ +// 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 { + 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 ( +
+
+
{toolbar && toolbar()}
+
+ + + + {editor} + + + +
+
+
+ ); + } +} + +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; diff --git a/src/Explorer/Notebook/NotebookRenderer/outputs/IFrameOutputs.tsx b/src/Explorer/Notebook/NotebookRenderer/outputs/IFrameOutputs.tsx new file mode 100644 index 000000000..8ca9764f5 --- /dev/null +++ b/src/Explorer/Notebook/NotebookRenderer/outputs/IFrameOutputs.tsx @@ -0,0 +1,70 @@ +import { AppState, ContentRef, selectors } from "@nteract/core"; +import { Output } from "@nteract/outputs"; +import Immutable from "immutable"; +import React from "react"; +import { connect } from "react-redux"; +import { SandboxFrame } from "./SandboxFrame"; + +// Adapted from https://github.com/nteract/nteract/blob/main/packages/stateful-components/src/outputs/index.tsx +// to add support for sandboxing using + ); + } + + componentWillUnmount(): void { + this.resizeObserver?.disconnect(); + this.mutationObserver?.disconnect(); + } + + onFrameLoad(event: React.SyntheticEvent): void { + const doc = (event.target as HTMLIFrameElement).contentDocument; + copyStyles(document, doc); + + this.setState({ frameBody: doc.body }); + + this.mutationObserver = new MutationObserver(() => { + const bodyFirstElementChild = this.state.frameBody?.firstElementChild; + if (!this.resizeObserver && bodyFirstElementChild) { + this.resizeObserver = new ResizeObserver(() => + this.setState({ + frameHeight: this.state.frameBody?.firstElementChild.scrollHeight, + }) + ); + this.resizeObserver.observe(bodyFirstElementChild); + } + }); + this.mutationObserver.observe(doc.body, { childList: true }); + } +} diff --git a/src/Explorer/Notebook/NotebookRenderer/outputs/SandboxJavaScript.tsx b/src/Explorer/Notebook/NotebookRenderer/outputs/SandboxJavaScript.tsx new file mode 100644 index 000000000..c6420a30e --- /dev/null +++ b/src/Explorer/Notebook/NotebookRenderer/outputs/SandboxJavaScript.tsx @@ -0,0 +1,26 @@ +import { Media } from "@nteract/outputs"; +import React from "react"; + +interface Props { + /** + * The JavaScript code that we would like to execute. + */ + data: string; + /** + * The media type associated with our component. + */ + mediaType: "text/javascript"; +} + +export class SandboxJavaScript extends React.PureComponent { + static defaultProps = { + data: "", + mediaType: "application/javascript", + }; + + render(): JSX.Element { + return ${this.props.data}`} />; + } +} + +export default SandboxJavaScript; diff --git a/src/Explorer/Notebook/NotebookRenderer/outputs/SanitizedHTML.tsx b/src/Explorer/Notebook/NotebookRenderer/outputs/SanitizedHTML.tsx new file mode 100644 index 000000000..15bb198c9 --- /dev/null +++ b/src/Explorer/Notebook/NotebookRenderer/outputs/SanitizedHTML.tsx @@ -0,0 +1,38 @@ +import { Media } from "@nteract/outputs"; +import React from "react"; +import sanitizeHtml from "sanitize-html"; + +interface Props { + /** + * The HTML string that will be rendered. + */ + data: string; + /** + * The media type associated with the HTML + * string. This defaults to text/html. + */ + mediaType: "text/html"; +} + +export class SanitizedHTML extends React.PureComponent { + static defaultProps = { + data: "", + mediaType: "text/html", + }; + + render(): JSX.Element { + return ; + } +} + +function sanitize(html: string): string { + return sanitizeHtml(html, { + allowedTags: false, // allow all tags + allowedAttributes: false, // allow all attrs + transformTags: { + iframe: "iframe-disabled", // disable iframes + }, + }); +} + +export default SanitizedHTML; diff --git a/src/Explorer/Panes/AddCollectionPane.test.ts b/src/Explorer/Panes/AddCollectionPane.test.ts index 98fc2890d..887f8c5bb 100644 --- a/src/Explorer/Panes/AddCollectionPane.test.ts +++ b/src/Explorer/Panes/AddCollectionPane.test.ts @@ -1,7 +1,8 @@ import * as Constants from "../../Common/Constants"; -import AddCollectionPane from "./AddCollectionPane"; -import Explorer from "../Explorer"; import { DatabaseAccount } from "../../Contracts/DataModels"; +import { updateUserContext } from "../../UserContext"; +import Explorer from "../Explorer"; +import AddCollectionPane from "./AddCollectionPane"; describe("Add Collection Pane", () => { describe("isValid()", () => { @@ -50,7 +51,14 @@ describe("Add Collection Pane", () => { }); it("should be false if graph API and partition key is /id or /label", () => { - explorer.defaultExperience(Constants.DefaultAccountExperience.Graph.toLowerCase()); + updateUserContext({ + databaseAccount: { + properties: { + capabilities: [{ name: "EnableGremlin" }], + }, + } as DatabaseAccount, + }); + const addCollectionPane = explorer.addCollectionPane as AddCollectionPane; addCollectionPane.partitionKey("/id"); expect(addCollectionPane.isValid()).toBe(false); @@ -60,7 +68,13 @@ describe("Add Collection Pane", () => { }); it("should be true for any non-graph API with /id or /label partition key", () => { - explorer.defaultExperience(Constants.DefaultAccountExperience.DocumentDB.toLowerCase()); + updateUserContext({ + databaseAccount: { + properties: { + capabilities: [{ name: "EnableCassandra" }], + }, + } as DatabaseAccount, + }); const addCollectionPane = explorer.addCollectionPane as AddCollectionPane; addCollectionPane.partitionKey("/id"); diff --git a/src/Explorer/Panes/AddCollectionPane.ts b/src/Explorer/Panes/AddCollectionPane.ts index 611a1a95c..1cddb72b1 100644 --- a/src/Explorer/Panes/AddCollectionPane.ts +++ b/src/Explorer/Panes/AddCollectionPane.ts @@ -127,13 +127,13 @@ export default class AddCollectionPane extends ContextualPaneBase { }); this.partitionKey.extend({ rateLimit: 100 }); this.partitionKeyPattern = ko.pureComputed(() => { - if (this.container && this.container.isPreferredApiGraph()) { + if (userContext.apiType === "Gremlin") { return "^/[^/]*"; } return ".*"; }); this.partitionKeyTitle = ko.pureComputed(() => { - if (this.container && this.container.isPreferredApiGraph()) { + if (userContext.apiType === "Gremlin") { return "May not use composite partition key"; } return ""; @@ -331,7 +331,7 @@ export default class AddCollectionPane extends ContextualPaneBase { if (currentCollections >= maxCollections) { let typeOfContainer = "collection"; - if (this.container.isPreferredApiGraph() || this.container.isPreferredApiTable()) { + if (userContext.apiType === "Gremlin" || this.container.isPreferredApiTable()) { typeOfContainer = "container"; } @@ -368,7 +368,7 @@ export default class AddCollectionPane extends ContextualPaneBase { return "e.g., address.zipCode"; } - if (this.container && !!this.container.isPreferredApiGraph()) { + if (userContext.apiType === "Gremlin") { return "e.g., /address"; } @@ -384,17 +384,11 @@ export default class AddCollectionPane extends ContextualPaneBase { }); this.uniqueKeysVisible = ko.pureComputed(() => { - if ( - this.container == null || - !!this.container.isPreferredApiMongoDB() || - !!this.container.isPreferredApiTable() || - !!this.container.isPreferredApiCassandra() || - !!this.container.isPreferredApiGraph() - ) { - return false; + if (userContext.apiType === "SQL") { + return true; } - return true; + return false; }); this.partitionKeyVisible = ko.computed(() => { @@ -591,7 +585,7 @@ export default class AddCollectionPane extends ContextualPaneBase { return false; } - if (this.container.isPreferredApiDocumentDB()) { + if (userContext.apiType === "SQL") { return true; } @@ -599,7 +593,7 @@ export default class AddCollectionPane extends ContextualPaneBase { return true; } - if (this.container.isPreferredApiCassandra() && this.container.hasStorageAnalyticsAfecFeature()) { + if (userContext.apiType === "Cassandra" && this.container.hasStorageAnalyticsAfecFeature()) { return true; } @@ -1011,7 +1005,7 @@ export default class AddCollectionPane extends ContextualPaneBase { return false; } - if (this.container.isPreferredApiGraph() && (this.partitionKey() === "/id" || this.partitionKey() === "/label")) { + if (userContext.apiType === "Gremlin" && (this.partitionKey() === "/id" || this.partitionKey() === "/label")) { this.formErrors("/id and /label as partition keys are not allowed for graph."); return false; } diff --git a/src/Explorer/Panes/AddDatabasePane.ts b/src/Explorer/Panes/AddDatabasePane.ts new file mode 100644 index 000000000..d011f0338 --- /dev/null +++ b/src/Explorer/Panes/AddDatabasePane.ts @@ -0,0 +1,460 @@ +import * as ko from "knockout"; +import * as Constants from "../../Common/Constants"; +import { createDatabase } from "../../Common/dataAccess/createDatabase"; +import editable from "../../Common/EditableUtility"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; +import { configContext, Platform } from "../../ConfigContext"; +import * as DataModels from "../../Contracts/DataModels"; +import { SubscriptionType } from "../../Contracts/SubscriptionType"; +import * as ViewModels from "../../Contracts/ViewModels"; +import * as SharedConstants from "../../Shared/Constants"; +import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import { userContext } from "../../UserContext"; +import * as AutoPilotUtils from "../../Utils/AutoPilotUtils"; +import * as PricingUtils from "../../Utils/PricingUtils"; +import { ContextualPaneBase } from "./ContextualPaneBase"; + +export default class AddDatabasePane extends ContextualPaneBase { + public defaultExperience: ko.Computed; + public databaseIdLabel: ko.Computed; + public databaseIdPlaceHolder: ko.Computed; + public databaseId: ko.Observable; + public databaseIdTooltipText: ko.Computed; + public databaseLevelThroughputTooltipText: ko.Computed; + public databaseCreateNewShared: ko.Observable; + public formErrorsDetails: ko.Observable; + public throughput: ViewModels.Editable; + public maxThroughputRU: ko.Observable; + public minThroughputRU: ko.Observable; + public maxThroughputRUText: ko.PureComputed; + public throughputRangeText: ko.Computed; + public throughputSpendAckText: ko.Observable; + public throughputSpendAck: ko.Observable; + public throughputSpendAckVisible: ko.Computed; + public requestUnitsUsageCost: ko.Computed; + public canRequestSupport: ko.PureComputed; + public costsVisible: ko.PureComputed; + public upsellMessage: ko.PureComputed; + public upsellMessageAriaLabel: ko.PureComputed; + public upsellAnchorUrl: ko.PureComputed; + public upsellAnchorText: ko.PureComputed; + public isAutoPilotSelected: ko.Observable; + public maxAutoPilotThroughputSet: ko.Observable; + public autoPilotUsageCost: ko.Computed; + public canExceedMaximumValue: ko.PureComputed; + public ruToolTipText: ko.Computed; + public freeTierExceedThroughputTooltip: ko.Computed; + public isFreeTierAccount: ko.Computed; + public canConfigureThroughput: ko.PureComputed; + public showUpsellMessage: ko.PureComputed; + + constructor(options: ViewModels.PaneOptions) { + super(options); + this.title((this.container && this.container.addDatabaseText()) || "New Database"); + this.databaseId = ko.observable(); + this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText()); + this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled()); + + this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue()); + + // TODO 388844: get defaults from parent frame + this.databaseCreateNewShared = ko.observable(this.getSharedThroughputDefault()); + + this.databaseIdLabel = ko.computed(() => + userContext.apiType === "Cassandra" ? "Keyspace id" : "Database id" + ); + + this.databaseIdPlaceHolder = ko.computed(() => + userContext.apiType === "Cassandra" ? "Type a new keyspace id" : "Type a new database id" + ); + + this.databaseIdTooltipText = ko.computed(() => { + const isCassandraAccount: boolean = userContext.apiType === "Cassandra"; + return `A ${isCassandraAccount ? "keyspace" : "database"} is a logical container of one or more ${isCassandraAccount ? "tables" : "collections" + }`; + }); + this.databaseLevelThroughputTooltipText = ko.computed(() => { + const isCassandraAccount: boolean = userContext.apiType === "Cassandra"; + const databaseLabel: string = isCassandraAccount ? "keyspace" : "database"; + const collectionsLabel: string = isCassandraAccount ? "tables" : "collections"; + return `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`; + }); + + this.throughput = editable.observable(); + this.maxThroughputRU = ko.observable(); + this.minThroughputRU = ko.observable(); + this.throughputSpendAckText = ko.observable(); + this.throughputSpendAck = ko.observable(false); + this.isAutoPilotSelected = ko.observable(false); + this.maxAutoPilotThroughputSet = ko.observable(AutoPilotUtils.minAutoPilotThroughput); + this.autoPilotUsageCost = ko.pureComputed(() => { + const autoPilot = this._isAutoPilotSelectedAndWhatTier(); + if (!autoPilot) { + return ""; + } + return PricingUtils.getAutoPilotV3SpendHtml(autoPilot.maxThroughput, true /* isDatabaseThroughput */); + }); + this.throughputRangeText = ko.pureComputed(() => { + if (this.isAutoPilotSelected()) { + return AutoPilotUtils.getAutoPilotHeaderText(); + } + return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`; + }); + + this.requestUnitsUsageCost = ko.computed(() => { + const offerThroughput: number = this.throughput(); + if ( + offerThroughput < this.minThroughputRU() || + (offerThroughput > this.maxThroughputRU() && !this.canExceedMaximumValue()) + ) { + return ""; + } + + const account = this.container.databaseAccount(); + if (!account) { + return ""; + } + + const regions = + (account && + account.properties && + account.properties.readLocations && + account.properties.readLocations.length) || + 1; + const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false; + + let estimatedSpendAcknowledge: string; + let estimatedSpend: string; + if (!this.isAutoPilotSelected()) { + estimatedSpend = PricingUtils.getEstimatedSpendHtml( + offerThroughput, + userContext.portalEnv, + regions, + multimaster + ); + estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( + offerThroughput, + userContext.portalEnv, + regions, + multimaster, + this.isAutoPilotSelected() + ); + } else { + estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( + this.maxAutoPilotThroughputSet(), + userContext.portalEnv, + regions, + multimaster + ); + estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( + this.maxAutoPilotThroughputSet(), + userContext.portalEnv, + regions, + multimaster, + this.isAutoPilotSelected() + ); + } + // TODO: change throughputSpendAckText to be a computed value, instead of having this side effect + this.throughputSpendAckText(estimatedSpendAcknowledge); + return estimatedSpend; + }); + + this.canRequestSupport = ko.pureComputed(() => { + if ( + configContext.platform !== Platform.Emulator && + !userContext.isTryCosmosDBSubscription && + configContext.platform !== Platform.Portal + ) { + const offerThroughput: number = this.throughput(); + return offerThroughput <= 100000; + } + + return false; + }); + + this.isFreeTierAccount = ko.computed(() => { + const databaseAccount = this.container && this.container.databaseAccount && this.container.databaseAccount(); + const isFreeTierAccount = + databaseAccount && databaseAccount.properties && databaseAccount.properties.enableFreeTier; + return isFreeTierAccount; + }); + + this.showUpsellMessage = ko.pureComputed(() => { + if (this.container.isServerlessEnabled()) { + return false; + } + + if (this.isFreeTierAccount()) { + return this.databaseCreateNewShared(); + } + + return true; + }); + + this.maxThroughputRUText = ko.pureComputed(() => { + return this.maxThroughputRU().toLocaleString(); + }); + + this.costsVisible = ko.pureComputed(() => { + return configContext.platform !== Platform.Emulator; + }); + + this.throughputSpendAckVisible = ko.pureComputed(() => { + const autoscaleThroughput = this.maxAutoPilotThroughputSet() * 1; + if (this.isAutoPilotSelected()) { + return autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K; + } + + const selectedThroughput: number = this.throughput(); + const maxRU: number = this.maxThroughputRU && this.maxThroughputRU(); + + const isMaxRUGreaterThanDefault: boolean = maxRU > SharedConstants.CollectionCreation.DefaultCollectionRUs100K; + const isThroughputSetGreaterThanDefault: boolean = + selectedThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K; + + if (this.canExceedMaximumValue()) { + return isThroughputSetGreaterThanDefault; + } + + return isThroughputSetGreaterThanDefault && isMaxRUGreaterThanDefault; + }); + + this.databaseCreateNewShared.subscribe((useShared: boolean) => { + this._updateThroughputLimitByDatabase(); + }); + + this.resetData(); + + this.freeTierExceedThroughputTooltip = ko.pureComputed(() => + this.isFreeTierAccount() && !this.container.isFirstResourceCreated() + ? "The first 400 RU/s in this account are free. Billing will apply to any throughput beyond 400 RU/s." + : "" + ); + + this.upsellMessage = ko.pureComputed(() => { + return PricingUtils.getUpsellMessage( + userContext.portalEnv, + this.isFreeTierAccount(), + this.container.isFirstResourceCreated(), + this.container.defaultExperience(), + false + ); + }); + + this.upsellMessageAriaLabel = ko.pureComputed(() => { + return `${this.upsellMessage()}. Click ${this.isFreeTierAccount() ? "to learn more" : "for more details"}`; + }); + + this.upsellAnchorUrl = ko.pureComputed(() => { + return this.isFreeTierAccount() ? Constants.Urls.freeTierInformation : Constants.Urls.cosmosPricing; + }); + + this.upsellAnchorText = ko.pureComputed(() => { + return this.isFreeTierAccount() ? "Learn more" : "More details"; + }); + } + + public onMoreDetailsKeyPress = (source: any, event: KeyboardEvent): boolean => { + if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) { + this.showErrorDetails(); + return false; + } + return true; + }; + + public open() { + super.open(); + this.resetData(); + const addDatabasePaneOpenMessage = { + subscriptionType: userContext.subscriptionType, + subscriptionQuotaId: userContext.quotaId, + defaultsCheck: { + throughput: this.throughput(), + flight: userContext.addCollectionFlight, + }, + dataExplorerArea: Constants.Areas.ContextualPane, + }; + const focusElement = document.getElementById("database-id"); + focusElement && focusElement.focus(); + TelemetryProcessor.trace(Action.CreateDatabase, ActionModifiers.Open, addDatabasePaneOpenMessage); + } + + public submit() { + if (!this._isValid()) { + return; + } + + const offerThroughput: number = this._computeOfferThroughput(); + + const addDatabasePaneStartMessage = { + database: ko.toJS({ + id: this.databaseId(), + shared: this.databaseCreateNewShared(), + }), + offerThroughput, + subscriptionType: userContext.subscriptionType, + subscriptionQuotaId: userContext.quotaId, + defaultsCheck: { + flight: userContext.addCollectionFlight, + }, + dataExplorerArea: Constants.Areas.ContextualPane, + }; + const startKey: number = TelemetryProcessor.traceStart(Action.CreateDatabase, addDatabasePaneStartMessage); + this.formErrors(""); + this.isExecuting(true); + + const createDatabaseParams: DataModels.CreateDatabaseParams = { + databaseId: addDatabasePaneStartMessage.database.id, + databaseLevelThroughput: addDatabasePaneStartMessage.database.shared, + }; + + if (this.isAutoPilotSelected()) { + createDatabaseParams.autoPilotMaxThroughput = this.maxAutoPilotThroughputSet(); + } else { + createDatabaseParams.offerThroughput = addDatabasePaneStartMessage.offerThroughput; + } + + createDatabase(createDatabaseParams).then( + (database: DataModels.Database) => { + this._onCreateDatabaseSuccess(offerThroughput, startKey); + }, + (error: any) => { + this._onCreateDatabaseFailure(error, offerThroughput, startKey); + } + ); + } + + public resetData() { + this.databaseId(""); + this.databaseCreateNewShared(this.getSharedThroughputDefault()); + this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled()); + this.maxAutoPilotThroughputSet(AutoPilotUtils.minAutoPilotThroughput); + this._updateThroughputLimitByDatabase(); + this.throughputSpendAck(false); + super.resetData(); + } + + public getSharedThroughputDefault(): boolean { + const subscriptionType = userContext.subscriptionType; + + if (subscriptionType === SubscriptionType.EA || this.container.isServerlessEnabled()) { + return false; + } + + return true; + } + + private _onCreateDatabaseSuccess(offerThroughput: number, startKey: number): void { + this.isExecuting(false); + this.close(); + this.container.refreshAllDatabases(); + const addDatabasePaneSuccessMessage = { + database: ko.toJS({ + id: this.databaseId(), + shared: this.databaseCreateNewShared(), + }), + offerThroughput: offerThroughput, + subscriptionType: userContext.subscriptionType, + subscriptionQuotaId: userContext.quotaId, + defaultsCheck: { + flight: userContext.addCollectionFlight, + }, + dataExplorerArea: Constants.Areas.ContextualPane, + }; + TelemetryProcessor.traceSuccess(Action.CreateDatabase, addDatabasePaneSuccessMessage, startKey); + this.resetData(); + } + + private _onCreateDatabaseFailure(error: any, offerThroughput: number, startKey: number): void { + this.isExecuting(false); + const errorMessage = getErrorMessage(error); + this.formErrors(errorMessage); + this.formErrorsDetails(errorMessage); + const addDatabasePaneFailedMessage = { + database: ko.toJS({ + id: this.databaseId(), + shared: this.databaseCreateNewShared(), + }), + offerThroughput: offerThroughput, + subscriptionType: userContext.subscriptionType, + subscriptionQuotaId: userContext.quotaId, + defaultsCheck: { + flight: userContext.addCollectionFlight, + }, + dataExplorerArea: Constants.Areas.ContextualPane, + error: errorMessage, + errorStack: getErrorStack(error), + }; + TelemetryProcessor.traceFailure(Action.CreateDatabase, addDatabasePaneFailedMessage, startKey); + } + + private _getThroughput(): number { + const throughput: number = this.throughput(); + return isNaN(throughput) ? 0 : Number(throughput); + } + + private _computeOfferThroughput(): number { + if (!this.canConfigureThroughput()) { + return undefined; + } + + if (this.isAutoPilotSelected()) { + return undefined; + } + + return this._getThroughput(); + } + + private _isValid(): boolean { + // TODO add feature flag that disables validation for customers with custom accounts + if (this.isAutoPilotSelected()) { + const autoPilot = this._isAutoPilotSelectedAndWhatTier(); + if ( + !autoPilot || + !autoPilot.maxThroughput || + !AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput) + ) { + this.formErrors( + `Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput` + ); + return false; + } + } + const throughput = this._getThroughput(); + + if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !this.throughputSpendAck()) { + this.formErrors(`Please acknowledge the estimated daily spend.`); + return false; + } + + const autoscaleThroughput = this.maxAutoPilotThroughputSet() * 1; + + if ( + this.isAutoPilotSelected() && + autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && + !this.throughputSpendAck() + ) { + this.formErrors(`Please acknowledge the estimated monthly spend.`); + return false; + } + + return true; + } + + private _isAutoPilotSelectedAndWhatTier(): DataModels.AutoPilotCreationSettings { + if (this.isAutoPilotSelected() && this.maxAutoPilotThroughputSet()) { + return { + maxThroughput: this.maxAutoPilotThroughputSet() * 1, + }; + } + return undefined; + } + + private _updateThroughputLimitByDatabase() { + const throughputDefaults = this.container.collectionCreationDefaults.throughput; + this.throughput(throughputDefaults.shared); + this.maxThroughputRU(throughputDefaults.unlimitedmax); + this.minThroughputRU(throughputDefaults.unlimitedmin); + } +} diff --git a/src/Explorer/Panes/AddDatabasePane/index.test.tsx b/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.test.tsx similarity index 88% rename from src/Explorer/Panes/AddDatabasePane/index.test.tsx rename to src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.test.tsx index ad82e97ac..45760b6cd 100644 --- a/src/Explorer/Panes/AddDatabasePane/index.test.tsx +++ b/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.test.tsx @@ -1,7 +1,7 @@ import { shallow } from "enzyme"; import React from "react"; -import { AddDatabasePane } from "."; import Explorer from "../../Explorer"; +import { AddDatabasePane } from "../AddDatabasePane"; const props = { explorer: new Explorer(), closePanel: (): void => undefined, diff --git a/src/Explorer/Panes/AddDatabasePane/index.tsx b/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx similarity index 100% rename from src/Explorer/Panes/AddDatabasePane/index.tsx rename to src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx diff --git a/src/Explorer/Panes/AddDatabasePane/__snapshots__/index.test.tsx.snap b/src/Explorer/Panes/AddDatabasePanel/__snapshots__/index.test.tsx.snap similarity index 100% rename from src/Explorer/Panes/AddDatabasePane/__snapshots__/index.test.tsx.snap rename to src/Explorer/Panes/AddDatabasePanel/__snapshots__/index.test.tsx.snap diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane.html b/src/Explorer/Panes/DeleteCollectionConfirmationPane.html deleted file mode 100644 index 55d59def4..000000000 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane.html +++ /dev/null @@ -1,108 +0,0 @@ -
-
-
- -
-
- -
- -
- Close -
-
- - -
-
- Warning - - Warning! The action you are about to take cannot be undone. Continuing will permanently delete this - resource and all of its children resources. - -
-
- - -
-
- Error - - - More details - -
-
- - - -
-
- * -

- -

-
-
-
Help us improve Azure Cosmos DB!
-
What is the reason why you are deleting this container?
-

- -

-
-
-
-
- -
-
- -
-
- - -
- -
- -
-
diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane.ts b/src/Explorer/Panes/DeleteCollectionConfirmationPane.ts deleted file mode 100644 index 582e72082..000000000 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane.ts +++ /dev/null @@ -1,128 +0,0 @@ -import * as ko from "knockout"; -import * as ViewModels from "../../Contracts/ViewModels"; -import * as Constants from "../../Common/Constants"; -import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; -import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; -import { ContextualPaneBase } from "./ContextualPaneBase"; -import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility"; -import DeleteFeedback from "../../Common/DeleteFeedback"; -import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import { deleteCollection } from "../../Common/dataAccess/deleteCollection"; -import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; - -export default class DeleteCollectionConfirmationPane extends ContextualPaneBase { - public collectionIdConfirmationText: ko.Observable; - public collectionIdConfirmation: ko.Observable; - public containerDeleteFeedback: ko.Observable; - public recordDeleteFeedback: ko.Observable; - - constructor(options: ViewModels.PaneOptions) { - super(options); - this.collectionIdConfirmationText = ko.observable("Confirm by typing the collection id"); - this.collectionIdConfirmation = ko.observable(); - this.containerDeleteFeedback = ko.observable(); - this.recordDeleteFeedback = ko.observable(false); - this.title("Delete Collection"); - this.resetData(); - } - - public submit(): Promise { - if (!this._isValid()) { - const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection(); - this.formErrors("Input collection name does not match the selected collection"); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Error while deleting collection ${selectedCollection && selectedCollection.id()}: ${this.formErrors()}` - ); - return Promise.resolve(); - } - - this.formErrors(""); - this.isExecuting(true); - const selectedCollection = this.container.findSelectedCollection(); - const startKey: number = TelemetryProcessor.traceStart(Action.DeleteCollection, { - collectionId: selectedCollection.id(), - dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: this.title(), - }); - return deleteCollection(selectedCollection.databaseId, selectedCollection.id()).then( - () => { - this.isExecuting(false); - this.close(); - this.container.selectedNode(selectedCollection.database); - this.container.tabsManager?.closeTabsByComparator( - (tab) => - tab.node?.id() === selectedCollection.id() && - (tab.node as ViewModels.Collection).databaseId === selectedCollection.databaseId - ); - this.container.refreshAllDatabases(); - this.resetData(); - TelemetryProcessor.traceSuccess( - Action.DeleteCollection, - { - collectionId: selectedCollection.id(), - dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: this.title(), - }, - startKey - ); - if (this.shouldRecordFeedback()) { - let deleteFeedback = new DeleteFeedback( - this.container.databaseAccount().id, - this.container.databaseAccount().name, - DefaultExperienceUtility.getApiKindFromDefaultExperience(this.container.defaultExperience()), - this.containerDeleteFeedback() - ); - - TelemetryProcessor.trace(Action.DeleteCollection, ActionModifiers.Mark, { - message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)), - }); - - this.containerDeleteFeedback(""); - } - }, - (error: any) => { - this.isExecuting(false); - const errorMessage = getErrorMessage(error); - this.formErrors(errorMessage); - this.formErrorsDetails(errorMessage); - TelemetryProcessor.traceFailure( - Action.DeleteCollection, - { - collectionId: selectedCollection.id(), - dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: this.title(), - error: errorMessage, - errorStack: getErrorStack(error), - }, - startKey - ); - } - ); - } - - public resetData() { - this.collectionIdConfirmation(""); - super.resetData(); - } - - public open() { - this.recordDeleteFeedback(this.shouldRecordFeedback()); - super.open(); - } - - public shouldRecordFeedback(): boolean { - return this.container.isLastCollection() && !this.container.isSelectedDatabaseShared(); - } - - private _isValid(): boolean { - const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection(); - - if (!selectedCollection) { - return false; - } - - return this.collectionIdConfirmation() === selectedCollection.id(); - } -} diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPanel.tsx b/src/Explorer/Panes/DeleteCollectionConfirmationPanel.tsx deleted file mode 100644 index 93c39ee12..000000000 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPanel.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { Text, TextField } from "office-ui-fabric-react"; -import * as React from "react"; -import { Areas } from "../../Common/Constants"; -import { deleteCollection } from "../../Common/dataAccess/deleteCollection"; -import DeleteFeedback from "../../Common/DeleteFeedback"; -import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; -import { Collection } from "../../Contracts/ViewModels"; -import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility"; -import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import { userContext } from "../../UserContext"; -import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; -import Explorer from "../Explorer"; -import { PanelFooterComponent } from "./PanelFooterComponent"; -import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent"; -import { PanelLoadingScreen } from "./PanelLoadingScreen"; -export interface DeleteCollectionConfirmationPanelProps { - explorer: Explorer; - closePanel: () => void; - openNotificationConsole: () => void; -} - -export interface DeleteCollectionConfirmationPanelState { - formError: string; - isExecuting: boolean; -} - -export class DeleteCollectionConfirmationPanel extends React.Component< - DeleteCollectionConfirmationPanelProps, - DeleteCollectionConfirmationPanelState -> { - private inputCollectionName: string; - private deleteCollectionFeedback: string; - - constructor(props: DeleteCollectionConfirmationPanelProps) { - super(props); - - this.state = { - formError: "", - isExecuting: false, - }; - } - - render(): JSX.Element { - return ( -
- -
-
- * - Confirm by typing the collection id - { - this.inputCollectionName = newInput; - }} - /> -
- {this.shouldRecordFeedback() && ( -
- - Help us improve Azure Cosmos DB! - - - What is the reason why you are deleting this container? - - { - this.deleteCollectionFeedback = newInput; - }} - /> -
- )} -
- - {this.state.isExecuting && } - - ); - } - - private getPanelErrorProps(): PanelInfoErrorProps { - if (this.state.formError) { - return { - messageType: "error", - message: this.state.formError, - showErrorDetails: true, - openNotificationConsole: this.props.openNotificationConsole, - }; - } - - return { - messageType: "warning", - showErrorDetails: false, - message: - "Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.", - }; - } - - private shouldRecordFeedback(): boolean { - return this.props.explorer.isLastCollection() && !this.props.explorer.isSelectedDatabaseShared(); - } - - public async submit(event: React.FormEvent): Promise { - event.preventDefault(); - - const collection = this.props.explorer.findSelectedCollection(); - if (!collection || this.inputCollectionName !== collection.id()) { - const errorMessage = "Input collection name does not match the selected collection"; - this.setState({ formError: errorMessage }); - NotificationConsoleUtils.logConsoleError(`Error while deleting collection ${collection.id()}: ${errorMessage}`); - return; - } - - this.setState({ formError: "", isExecuting: true }); - - const startKey: number = TelemetryProcessor.traceStart(Action.DeleteCollection, { - collectionId: collection.id(), - dataExplorerArea: Areas.ContextualPane, - paneTitle: "Delete Collection", - }); - - try { - await deleteCollection(collection.databaseId, collection.id()); - - this.setState({ isExecuting: false }); - this.props.explorer.selectedNode(collection.database); - this.props.explorer.tabsManager?.closeTabsByComparator( - (tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId - ); - this.props.explorer.refreshAllDatabases(); - - TelemetryProcessor.traceSuccess( - Action.DeleteCollection, - { - collectionId: collection.id(), - dataExplorerArea: Areas.ContextualPane, - paneTitle: "Delete Collection", - }, - startKey - ); - - if (this.shouldRecordFeedback()) { - const deleteFeedback = new DeleteFeedback( - userContext.databaseAccount?.id, - userContext.databaseAccount?.name, - DefaultExperienceUtility.getApiKindFromDefaultExperience(userContext.defaultExperience), - this.deleteCollectionFeedback - ); - - TelemetryProcessor.trace(Action.DeleteCollection, ActionModifiers.Mark, { - message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)), - }); - } - - this.props.closePanel(); - } catch (error) { - const errorMessage = getErrorMessage(error); - this.setState({ formError: errorMessage, isExecuting: false }); - TelemetryProcessor.traceFailure( - Action.DeleteCollection, - { - collectionId: collection.id(), - dataExplorerArea: Areas.ContextualPane, - paneTitle: "Delete Collection", - error: errorMessage, - errorStack: getErrorStack(error), - }, - startKey - ); - } - } -} diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPanel/__snapshots__/index.test.tsx.snap b/src/Explorer/Panes/DeleteCollectionConfirmationPanel/__snapshots__/index.test.tsx.snap new file mode 100644 index 000000000..5999c9b22 --- /dev/null +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPanel/__snapshots__/index.test.tsx.snap @@ -0,0 +1,3631 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Delete Collection Confirmation Pane submit() should call delete collection 1`] = ` + + +
+
+
+
+
+ + Delete container + + + + *": Object { + "left": 0, + "position": "relative", + "top": 0, + }, + }, + "textAlign": "center", + "textDecoration": "none", + "userSelect": "none", + }, + Object { + "backgroundColor": "transparent", + "border": "none", + "color": "#0078d4", + "height": "32px", + "padding": "0 4px", + "width": "32px", + }, + ], + "rootChecked": Object { + "backgroundColor": "#edebe9", + "color": "#005a9e", + }, + "rootCheckedHovered": Object { + "backgroundColor": "#e1dfdd", + "color": "#005a9e", + }, + "rootDisabled": Array [ + Object { + "outline": "transparent", + "position": "relative", + "selectors": Object { + ".ms-Fabric--isFocusVisible &:focus:after": Object { + "border": "1px solid transparent", + "bottom": 2, + "content": "\\"\\"", + "left": 2, + "outline": "1px solid #605e5c", + "position": "absolute", + "right": 2, + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "bottom": -2, + "left": -2, + "outlineColor": "ButtonText", + "right": -2, + "top": -2, + }, + }, + "top": 2, + "zIndex": 1, + }, + "::-moz-focus-inner": Object { + "border": "0", + }, + }, + }, + Object { + "backgroundColor": "#f3f2f1", + "borderColor": "#f3f2f1", + "color": "#a19f9d", + "cursor": "default", + "pointerEvents": "none", + "selectors": Object { + ":focus": Object { + "outline": 0, + }, + ":hover": Object { + "outline": 0, + }, + }, + }, + Object { + "color": "#c8c6c4", + }, + ], + "rootExpanded": Object { + "backgroundColor": "#edebe9", + "color": "#005a9e", + }, + "rootHasMenu": Object { + "width": "auto", + }, + "rootHovered": Object { + "backgroundColor": "#f3f2f1", + "color": "#106ebe", + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "borderColor": "Highlight", + "color": "Highlight", + }, + }, + }, + "rootPressed": Object { + "backgroundColor": "#edebe9", + "color": "#005a9e", + }, + "screenReaderText": Object { + "border": 0, + "height": 1, + "margin": -1, + "overflow": "hidden", + "padding": 0, + "position": "absolute", + "width": 1, + }, + "splitButtonContainer": Array [ + Object { + "outline": "transparent", + "position": "relative", + "selectors": Object { + ".ms-Fabric--isFocusVisible &:focus:after": Object { + "border": "1px solid #ffffff", + "bottom": 3, + "content": "\\"\\"", + "left": 3, + "outline": "1px solid #605e5c", + "position": "absolute", + "right": 3, + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "border": "none", + "bottom": -2, + "left": -2, + "right": -2, + "top": -2, + }, + }, + "top": 3, + "zIndex": 1, + }, + "::-moz-focus-inner": Object { + "border": "0", + }, + }, + }, + Object { + "display": "inline-flex", + "selectors": Object { + ".ms-Button--default": Object { + "borderBottomRightRadius": "0", + "borderRight": "none", + "borderTopRightRadius": "0", + }, + ".ms-Button--primary": Object { + "border": "none", + "borderBottomRightRadius": "0", + "borderTopRightRadius": "0", + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "MsHighContrastAdjust": "none", + "backgroundColor": "Window", + "border": "1px solid WindowText", + "borderRightWidth": "0", + "color": "WindowText", + "forcedColorAdjust": "none", + }, + }, + }, + ".ms-Button--primary + .ms-Button": Object { + "border": "none", + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "border": "1px solid WindowText", + "borderLeftWidth": "0", + }, + }, + }, + }, + }, + ], + "splitButtonContainerChecked": Object { + "selectors": Object { + ".ms-Button--primary": Object { + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "MsHighContrastAdjust": "none", + "backgroundColor": "WindowText", + "color": "Window", + "forcedColorAdjust": "none", + }, + }, + }, + }, + }, + "splitButtonContainerCheckedHovered": Object { + "selectors": Object { + ".ms-Button--primary": Object { + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "MsHighContrastAdjust": "none", + "backgroundColor": "WindowText", + "color": "Window", + "forcedColorAdjust": "none", + }, + }, + }, + }, + }, + "splitButtonContainerDisabled": Object { + "border": "none", + "outline": "none", + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "MsHighContrastAdjust": "none", + "backgroundColor": "Window", + "borderColor": "GrayText", + "color": "GrayText", + "forcedColorAdjust": "none", + }, + }, + }, + "splitButtonContainerFocused": Object { + "outline": "none!important", + }, + "splitButtonContainerHovered": Object { + "selectors": Object { + ".ms-Button--primary": Object { + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "Highlight", + "color": "Window", + }, + }, + }, + ".ms-Button.is-disabled": Object { + "color": "#a19f9d", + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "Window", + "borderColor": "GrayText", + "color": "GrayText", + }, + }, + }, + }, + }, + "splitButtonDivider": Object { + "bottom": 8, + "position": "absolute", + "right": 31, + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "WindowText", + }, + }, + "top": 8, + "width": 1, + }, + "splitButtonDividerDisabled": Object { + "bottom": 8, + "position": "absolute", + "right": 31, + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "GrayText", + }, + }, + "top": 8, + "width": 1, + }, + "splitButtonFlexContainer": Object { + "alignItems": "center", + "display": "flex", + "flexWrap": "nowrap", + "height": "100%", + "justifyContent": "center", + }, + "splitButtonMenuButton": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + ".ms-Button-menuIcon": Object { + "color": "WindowText", + }, + }, + "border": "1px solid #8a8886", + "borderBottomRightRadius": "2px", + "borderLeft": "none", + "borderRadius": 0, + "borderTopRightRadius": "2px", + "boxSizing": "border-box", + "cursor": "pointer", + "display": "inline-block", + "height": "auto", + "marginBottom": 0, + "marginLeft": -1, + "marginRight": 0, + "marginTop": 0, + "outline": "transparent", + "padding": 6, + "textAlign": "center", + "textDecoration": "none", + "userSelect": "none", + "verticalAlign": "top", + "width": 32, + }, + "splitButtonMenuButtonDisabled": Object { + "border": "none", + "pointerEvents": "none", + "selectors": Object { + ".ms-Button--primary": Object { + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "Window", + "borderColor": "GrayText", + "color": "GrayText", + }, + }, + }, + ".ms-Button-menuIcon": Object { + "selectors": Object { + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "color": "GrayText", + }, + }, + }, + ":hover": Object { + "cursor": "default", + }, + "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { + "backgroundColor": "Window", + "border": "1px solid GrayText", + "color": "GrayText", + }, + }, + }, + "textContainer": Object { + "display": "block", + "flexGrow": 1, + }, + } + } + tabIndex={0} + theme={ + Object { + "disableGlobalClassNames": false, + "effects": Object { + "elevation16": "0 6.4px 14.4px 0 rgba(0, 0, 0, 0.132), 0 1.2px 3.6px 0 rgba(0, 0, 0, 0.108)", + "elevation4": "0 1.6px 3.6px 0 rgba(0, 0, 0, 0.132), 0 0.3px 0.9px 0 rgba(0, 0, 0, 0.108)", + "elevation64": "0 25.6px 57.6px 0 rgba(0, 0, 0, 0.22), 0 4.8px 14.4px 0 rgba(0, 0, 0, 0.18)", + "elevation8": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)", + "roundedCorner2": "2px", + "roundedCorner4": "4px", + "roundedCorner6": "6px", + }, + "fonts": Object { + "large": Object { + "MozOsxFontSmoothing": "grayscale", + "WebkitFontSmoothing": "antialiased", + "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", + "fontSize": "18px", + "fontWeight": 400, + }, + "medium": Object { + "MozOsxFontSmoothing": "grayscale", + "WebkitFontSmoothing": "antialiased", + "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", + "fontSize": "14px", + "fontWeight": 400, + }, + "mediumPlus": Object { + "MozOsxFontSmoothing": "grayscale", + "WebkitFontSmoothing": "antialiased", + "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", + "fontSize": "16px", + "fontWeight": 400, + }, + "mega": Object { + "MozOsxFontSmoothing": "grayscale", + "WebkitFontSmoothing": "antialiased", + "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", + "fontSize": "68px", + "fontWeight": 600, + }, + "small": Object { + "MozOsxFontSmoothing": "grayscale", + "WebkitFontSmoothing": "antialiased", + "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", + "fontSize": "12px", + "fontWeight": 400, + }, + "smallPlus": Object { + "MozOsxFontSmoothing": "grayscale", + "WebkitFontSmoothing": "antialiased", + "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", + "fontSize": "12px", + "fontWeight": 400, + }, + "superLarge": Object { + "MozOsxFontSmoothing": "grayscale", + "WebkitFontSmoothing": "antialiased", + "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", + "fontSize": "42px", + "fontWeight": 600, + }, + "tiny": Object { + "MozOsxFontSmoothing": "grayscale", + "WebkitFontSmoothing": "antialiased", + "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", + "fontSize": "10px", + "fontWeight": 400, + }, + "xLarge": Object { + "MozOsxFontSmoothing": "grayscale", + "WebkitFontSmoothing": "antialiased", + "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", + "fontSize": "20px", + "fontWeight": 600, + }, + "xLargePlus": Object { + "MozOsxFontSmoothing": "grayscale", + "WebkitFontSmoothing": "antialiased", + "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", + "fontSize": "24px", + "fontWeight": 600, + }, + "xSmall": Object { + "MozOsxFontSmoothing": "grayscale", + "WebkitFontSmoothing": "antialiased", + "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", + "fontSize": "10px", + "fontWeight": 400, + }, + "xxLarge": Object { + "MozOsxFontSmoothing": "grayscale", + "WebkitFontSmoothing": "antialiased", + "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", + "fontSize": "28px", + "fontWeight": 600, + }, + "xxLargePlus": Object { + "MozOsxFontSmoothing": "grayscale", + "WebkitFontSmoothing": "antialiased", + "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", + "fontSize": "32px", + "fontWeight": 600, + }, + }, + "isInverted": false, + "palette": Object { + "accent": "#0078d4", + "black": "#000000", + "blackTranslucent40": "rgba(0,0,0,.4)", + "blue": "#0078d4", + "blueDark": "#002050", + "blueLight": "#00bcf2", + "blueMid": "#00188f", + "green": "#107c10", + "greenDark": "#004b1c", + "greenLight": "#bad80a", + "magenta": "#b4009e", + "magentaDark": "#5c005c", + "magentaLight": "#e3008c", + "neutralDark": "#201f1e", + "neutralLight": "#edebe9", + "neutralLighter": "#f3f2f1", + "neutralLighterAlt": "#faf9f8", + "neutralPrimary": "#323130", + "neutralPrimaryAlt": "#3b3a39", + "neutralQuaternary": "#d2d0ce", + "neutralQuaternaryAlt": "#e1dfdd", + "neutralSecondary": "#605e5c", + "neutralSecondaryAlt": "#8a8886", + "neutralTertiary": "#a19f9d", + "neutralTertiaryAlt": "#c8c6c4", + "orange": "#d83b01", + "orangeLight": "#ea4300", + "orangeLighter": "#ff8c00", + "purple": "#5c2d91", + "purpleDark": "#32145a", + "purpleLight": "#b4a0ff", + "red": "#e81123", + "redDark": "#a4262c", + "teal": "#008272", + "tealDark": "#004b50", + "tealLight": "#00b294", + "themeDark": "#005a9e", + "themeDarkAlt": "#106ebe", + "themeDarker": "#004578", + "themeLight": "#c7e0f4", + "themeLighter": "#deecf9", + "themeLighterAlt": "#eff6fc", + "themePrimary": "#0078d4", + "themeSecondary": "#2b88d8", + "themeTertiary": "#71afe5", + "white": "#ffffff", + "whiteTranslucent40": "rgba(255,255,255,.4)", + "yellow": "#ffb900", + "yellowDark": "#d29200", + "yellowLight": "#fff100", + }, + "rtl": undefined, + "semanticColors": Object { + "accentButtonBackground": "#0078d4", + "accentButtonText": "#ffffff", + "actionLink": "#323130", + "actionLinkHovered": "#201f1e", + "blockingBackground": "#FDE7E9", + "blockingIcon": "#FDE7E9", + "bodyBackground": "#ffffff", + "bodyBackgroundChecked": "#edebe9", + "bodyBackgroundHovered": "#f3f2f1", + "bodyDivider": "#edebe9", + "bodyFrameBackground": "#ffffff", + "bodyFrameDivider": "#edebe9", + "bodyStandoutBackground": "#faf9f8", + "bodySubtext": "#605e5c", + "bodyText": "#323130", + "bodyTextChecked": "#000000", + "buttonBackground": "#ffffff", + "buttonBackgroundChecked": "#c8c6c4", + "buttonBackgroundCheckedHovered": "#edebe9", + "buttonBackgroundDisabled": "#f3f2f1", + "buttonBackgroundHovered": "#f3f2f1", + "buttonBackgroundPressed": "#edebe9", + "buttonBorder": "#8a8886", + "buttonBorderDisabled": "#f3f2f1", + "buttonText": "#323130", + "buttonTextChecked": "#201f1e", + "buttonTextCheckedHovered": "#000000", + "buttonTextDisabled": "#a19f9d", + "buttonTextHovered": "#201f1e", + "buttonTextPressed": "#201f1e", + "cardShadow": "0 1.6px 3.6px 0 rgba(0, 0, 0, 0.132), 0 0.3px 0.9px 0 rgba(0, 0, 0, 0.108)", + "cardShadowHovered": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)", + "cardStandoutBackground": "#ffffff", + "defaultStateBackground": "#faf9f8", + "disabledBackground": "#f3f2f1", + "disabledBodySubtext": "#c8c6c4", + "disabledBodyText": "#a19f9d", + "disabledBorder": "#c8c6c4", + "disabledSubtext": "#d2d0ce", + "disabledText": "#a19f9d", + "errorBackground": "#FDE7E9", + "errorIcon": "#A80000", + "errorText": "#a4262c", + "focusBorder": "#605e5c", + "infoBackground": "#f3f2f1", + "infoIcon": "#605e5c", + "inputBackground": "#ffffff", + "inputBackgroundChecked": "#0078d4", + "inputBackgroundCheckedHovered": "#005a9e", + "inputBorder": "#605e5c", + "inputBorderHovered": "#323130", + "inputFocusBorderAlt": "#0078d4", + "inputForegroundChecked": "#ffffff", + "inputIcon": "#0078d4", + "inputIconDisabled": "#a19f9d", + "inputIconHovered": "#005a9e", + "inputPlaceholderBackgroundChecked": "#deecf9", + "inputPlaceholderText": "#605e5c", + "inputText": "#323130", + "inputTextHovered": "#201f1e", + "link": "#0078d4", + "linkHovered": "#004578", + "listBackground": "#ffffff", + "listHeaderBackgroundHovered": "#f3f2f1", + "listHeaderBackgroundPressed": "#edebe9", + "listItemBackgroundChecked": "#edebe9", + "listItemBackgroundCheckedHovered": "#e1dfdd", + "listItemBackgroundHovered": "#f3f2f1", + "listText": "#323130", + "listTextColor": "#323130", + "menuBackground": "#ffffff", + "menuDivider": "#c8c6c4", + "menuHeader": "#0078d4", + "menuIcon": "#0078d4", + "menuItemBackgroundChecked": "#edebe9", + "menuItemBackgroundHovered": "#f3f2f1", + "menuItemBackgroundPressed": "#edebe9", + "menuItemText": "#323130", + "menuItemTextHovered": "#201f1e", + "messageLink": "#005A9E", + "messageLinkHovered": "#004578", + "messageText": "#323130", + "primaryButtonBackground": "#0078d4", + "primaryButtonBackgroundDisabled": "#f3f2f1", + "primaryButtonBackgroundHovered": "#106ebe", + "primaryButtonBackgroundPressed": "#005a9e", + "primaryButtonBorder": "transparent", + "primaryButtonText": "#ffffff", + "primaryButtonTextDisabled": "#d2d0ce", + "primaryButtonTextHovered": "#ffffff", + "primaryButtonTextPressed": "#ffffff", + "severeWarningBackground": "#FED9CC", + "severeWarningIcon": "#D83B01", + "smallInputBorder": "#605e5c", + "successBackground": "#DFF6DD", + "successIcon": "#107C10", + "successText": "#107C10", + "variantBorder": "#edebe9", + "variantBorderHovered": "#a19f9d", + "warningBackground": "#FFF4CE", + "warningHighlight": "#ffb900", + "warningIcon": "#797775", + "warningText": "#323130", + }, + "spacing": Object { + "l1": "20px", + "l2": "32px", + "m": "16px", + "s1": "8px", + "s2": "4px", + }, + } + } + title="Close pane" + variantClassName="ms-Button--icon" + > + + + + + +
+ +
+
+
+ + * + + + + Confirm by typing the + container + id + + + + +
+
+
+ +
+
+
+
+
+
+
+ + + Help us improve Azure Cosmos DB! + + + + + What is the reason why you are deleting this + container + ? + + + + +
+
+
+