mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-09 20:49:12 +00:00
Compare commits
1 Commits
users/srna
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
780cf23b30 |
@@ -44,6 +44,7 @@ src/Definitions/png.d.ts
|
||||
src/Definitions/svg.d.ts
|
||||
src/Explorer/ComponentRegisterer.test.ts
|
||||
src/Explorer/ComponentRegisterer.ts
|
||||
src/Explorer/ContextMenuButtonFactory.ts
|
||||
src/Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.ts
|
||||
src/Explorer/Controls/DiffEditor/DiffEditorComponent.ts
|
||||
src/Explorer/Controls/DynamicList/DynamicList.test.ts
|
||||
@@ -104,6 +105,8 @@ src/Explorer/Notebook/NotebookContainerClient.ts
|
||||
src/Explorer/Notebook/NotebookContentClient.ts
|
||||
src/Explorer/Notebook/NotebookContentItem.ts
|
||||
src/Explorer/Notebook/NotebookUtil.ts
|
||||
src/Explorer/OpenActions.test.ts
|
||||
src/Explorer/OpenActions.ts
|
||||
src/Explorer/OpenActionsStubs.ts
|
||||
src/Explorer/Panes/AddDatabasePane.ts
|
||||
src/Explorer/Panes/AddDatabasePane.test.ts
|
||||
@@ -132,6 +135,7 @@ src/Explorer/Tables/QueryBuilder/ClauseGroupViewModel.ts
|
||||
src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts
|
||||
src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts
|
||||
src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts
|
||||
src/Explorer/Tables/QueryBuilder/QueryViewModel.ts
|
||||
src/Explorer/Tables/TableDataClient.ts
|
||||
src/Explorer/Tables/TableEntityProcessor.ts
|
||||
src/Explorer/Tables/Utilities.ts
|
||||
@@ -141,11 +145,14 @@ src/Explorer/Tabs/DocumentsTab.test.ts
|
||||
src/Explorer/Tabs/DocumentsTab.ts
|
||||
src/Explorer/Tabs/GraphTab.ts
|
||||
src/Explorer/Tabs/MongoDocumentsTab.ts
|
||||
# src/Explorer/Tabs/MongoQueryTab.ts
|
||||
# src/Explorer/Tabs/MongoShellTab.ts
|
||||
src/Explorer/Tabs/MongoQueryTab.ts
|
||||
src/Explorer/Tabs/MongoShellTab.ts
|
||||
src/Explorer/Tabs/NotebookV2Tab.ts
|
||||
src/Explorer/Tabs/QueryTab.test.ts
|
||||
src/Explorer/Tabs/QueryTab.ts
|
||||
src/Explorer/Tabs/QueryTablesTab.ts
|
||||
src/Explorer/Tabs/ScriptTabBase.ts
|
||||
# src/Explorer/Tabs/StoredProcedureTab.ts
|
||||
src/Explorer/Tabs/StoredProcedureTab.ts
|
||||
src/Explorer/Tabs/TabComponents.ts
|
||||
src/Explorer/Tabs/TabsBase.ts
|
||||
src/Explorer/Tabs/TriggerTab.ts
|
||||
@@ -198,6 +205,9 @@ src/ResourceProvider/IResourceProviderClient.test.ts
|
||||
src/ResourceProvider/IResourceProviderClient.ts
|
||||
src/ResourceProvider/ResourceProviderClient.ts
|
||||
src/ResourceProvider/ResourceProviderClientFactory.ts
|
||||
src/RouteHandlers/RouteHandler.ts
|
||||
src/RouteHandlers/TabRouteHandler.test.ts
|
||||
src/RouteHandlers/TabRouteHandler.ts
|
||||
src/Shared/Constants.ts
|
||||
src/Shared/DefaultExperienceUtility.test.ts
|
||||
src/Shared/DefaultExperienceUtility.ts
|
||||
@@ -253,6 +263,7 @@ src/Explorer/Graph/GraphExplorerComponent/EditorNodePropertiesComponent.test.tsx
|
||||
src/Explorer/Graph/GraphExplorerComponent/EditorNodePropertiesComponent.tsx
|
||||
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx
|
||||
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx
|
||||
src/Explorer/Graph/GraphExplorerComponent/GraphExplorerAdapter.tsx
|
||||
src/Explorer/Graph/GraphExplorerComponent/GraphVizComponent.tsx
|
||||
src/Explorer/Graph/GraphExplorerComponent/LeftPaneComponent.tsx
|
||||
src/Explorer/Graph/GraphExplorerComponent/MiddlePaneComponent.tsx
|
||||
|
||||
@@ -724,24 +724,45 @@ execute-sproc-params-pane {
|
||||
|
||||
.results-container,
|
||||
.errors-container {
|
||||
padding: @MediumSpace 0px 0px @MediumSpace;
|
||||
height: 100%;
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
overflow: hidden;
|
||||
|
||||
.enterInputParameters {
|
||||
padding: @LargeSpace @MediumSpace;
|
||||
.toggles {
|
||||
height: @ToggleHeight;
|
||||
width: @ToggleWidth;
|
||||
margin-left: @MediumSpace;
|
||||
|
||||
&:focus {
|
||||
.focus();
|
||||
}
|
||||
|
||||
.tab {
|
||||
margin-right: @MediumSpace;
|
||||
}
|
||||
|
||||
.toggleSwitch {
|
||||
.toggleSwitch();
|
||||
}
|
||||
|
||||
.selectedToggle {
|
||||
.selectedToggle();
|
||||
}
|
||||
|
||||
.unselectedToggle {
|
||||
.unselectedToggle();
|
||||
}
|
||||
}
|
||||
|
||||
div[role="tabpanel"] {
|
||||
height: 100%;
|
||||
padding-bottom: 50px;
|
||||
.enterInputParameters {
|
||||
padding: @LargeSpace @MediumSpace;
|
||||
}
|
||||
}
|
||||
|
||||
.errors-container {
|
||||
padding-left: (2 * @MediumSpace);
|
||||
padding: @MediumSpace 0px 0px @MediumSpace;
|
||||
.errors-header {
|
||||
font-weight: 700;
|
||||
font-size: @DefaultFontSize;
|
||||
@@ -3067,11 +3088,4 @@ settings-pane {
|
||||
.hiddenMain {
|
||||
display: none;
|
||||
height: 0px;
|
||||
}
|
||||
.spinner {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
background: white;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
@@ -200,12 +200,4 @@
|
||||
|
||||
.migration:disabled {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.trigger-field {
|
||||
width: 40%;
|
||||
margin-top: 10px
|
||||
}
|
||||
.trigger-form {
|
||||
padding: 10px 30px 10px 30px;
|
||||
}
|
||||
175
package-lock.json
generated
175
package-lock.json
generated
@@ -171,6 +171,11 @@
|
||||
"react": "16.5.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"monaco-editor": {
|
||||
"version": "0.18.1",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.18.1.tgz",
|
||||
"integrity": "sha512-fmL+RFZ2Hrezy+X/5ZczQW51LUmvzfcqOurnkCIRFTyjdVjzR7JvENzI6+VKBJzJdPh6EYL4RoWl92b2Hrk9fw=="
|
||||
},
|
||||
"react": {
|
||||
"version": "16.5.2",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-16.5.2.tgz",
|
||||
@@ -3709,84 +3714,14 @@
|
||||
}
|
||||
},
|
||||
"@nteract/editor": {
|
||||
"version": "10.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@nteract/editor/-/editor-10.1.12.tgz",
|
||||
"integrity": "sha512-bsUrCctukjWdpKNWQOQmhfxMCQ/SBVIO6+RkazI4y4dVeeP3KMP8nxfhzIbzTMNSkyynps/deZFjpDWqRhG+Dg==",
|
||||
"version": "10.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@nteract/editor/-/editor-10.1.2.tgz",
|
||||
"integrity": "sha512-Wtj0kJUSoBZsWUh82JGt6miqYS0jt0k+3SD3cnW9socayxp2KB0Qbqhh2NtrF9ysxVHWnQT8iUarJjpGIdNyng==",
|
||||
"requires": {
|
||||
"@nteract/messaging": "^7.0.19",
|
||||
"@nteract/outputs": "^3.0.11",
|
||||
"codemirror": "5.61.1",
|
||||
"@nteract/messaging": "^7.0.10",
|
||||
"@nteract/outputs": "^3.0.9",
|
||||
"codemirror": "5.57.0",
|
||||
"rxjs": "^6.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nteract/commutable": {
|
||||
"version": "7.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@nteract/commutable/-/commutable-7.4.5.tgz",
|
||||
"integrity": "sha512-RYqyMvkFt/04GQ9T+hGYgr9/LEy0dAYJ2QKn930TFX004KjfBT6Tt8VSLFyHWkXqPwyJ0jKMCJwqLcGOI/atqg==",
|
||||
"requires": {
|
||||
"immutable": "^4.0.0-rc.12",
|
||||
"uuid": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"@nteract/messaging": {
|
||||
"version": "7.0.19",
|
||||
"resolved": "https://registry.npmjs.org/@nteract/messaging/-/messaging-7.0.19.tgz",
|
||||
"integrity": "sha512-gRPMxJr741/BshrfCcPSbm5iVyRU2TKmAv9jeQzk0MZEGy+Y1A0REO+eptkt4Ma0OXlvDxON6JEDauk8+2xt4w==",
|
||||
"requires": {
|
||||
"@nteract/types": "^7.1.9",
|
||||
"@types/uuid": "^8.0.0",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"rxjs": "^6.6.0",
|
||||
"uuid": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"@nteract/outputs": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@nteract/outputs/-/outputs-3.0.11.tgz",
|
||||
"integrity": "sha512-LeT9ViBf+fTPSubZ9dMe7128kg0rl1jIG54V0n2GiU5RuYnUz21FU0IOaLMPUfFMO1VyVEOW5jDc3PAQx5/Kwg==",
|
||||
"requires": {
|
||||
"@nteract/markdown": "^4.5.2",
|
||||
"@nteract/mathjax": "^4.0.11",
|
||||
"ansi-to-react": "^6.0.5",
|
||||
"react-json-tree": "^0.12.1"
|
||||
}
|
||||
},
|
||||
"@nteract/types": {
|
||||
"version": "7.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@nteract/types/-/types-7.1.9.tgz",
|
||||
"integrity": "sha512-a7lGMWdjfz2QGlZbAiFHifU9Nhk9ntwg/iKUTMIMRPY1Wfs5UreHSMt+vZ8OY5HGjxicfHozBatGDKXeKXFHMQ==",
|
||||
"requires": {
|
||||
"@nteract/commutable": "^7.4.5",
|
||||
"immutable": "^4.0.0-rc.12",
|
||||
"rxjs": "^6.6.0",
|
||||
"uuid": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"react-base16-styling": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.7.0.tgz",
|
||||
"integrity": "sha512-lTa/VSFdU6BOAj+FryOe7OTZ0OBP8GXPOnCS0QnZi7G3zhssWgIgwl0eUL77onXx/WqKPFndB3ZeC77QC/l4Dw==",
|
||||
"requires": {
|
||||
"base16": "^1.0.0",
|
||||
"lodash.curry": "^4.1.1",
|
||||
"lodash.flow": "^3.5.0",
|
||||
"pure-color": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"react-json-tree": {
|
||||
"version": "0.12.1",
|
||||
"resolved": "https://registry.npmjs.org/react-json-tree/-/react-json-tree-0.12.1.tgz",
|
||||
"integrity": "sha512-j6fkRY7ha9XMv1HPVakRCsvyFwHGR5AZuwO8naBBeZXnZbbLor5tpcUxS/8XD01+D1v7ZN5p+7LU+9V1uyASiQ==",
|
||||
"requires": {
|
||||
"prop-types": "^15.7.2",
|
||||
"react-base16-styling": "^0.7.0"
|
||||
}
|
||||
},
|
||||
"uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@nteract/epics": {
|
||||
@@ -4071,6 +4006,11 @@
|
||||
"uuid": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"monaco-editor": {
|
||||
"version": "0.18.1",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.18.1.tgz",
|
||||
"integrity": "sha512-fmL+RFZ2Hrezy+X/5ZczQW51LUmvzfcqOurnkCIRFTyjdVjzR7JvENzI6+VKBJzJdPh6EYL4RoWl92b2Hrk9fw=="
|
||||
},
|
||||
"react-redux": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-6.0.1.tgz",
|
||||
@@ -5720,15 +5660,6 @@
|
||||
"redux": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@types/react-splitter-layout": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-splitter-layout/-/react-splitter-layout-3.0.1.tgz",
|
||||
"integrity": "sha512-NsKq32LdG11G/Uj+xo2QmC9S8YSe8JRtxkBhsBE7ODFs0zcnzNEqFAQirP0H7rPe2WFGiu+d/44xbHsew7QAJw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-table": {
|
||||
"version": "6.8.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-table/-/react-table-6.8.7.tgz",
|
||||
@@ -8137,9 +8068,9 @@
|
||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
|
||||
},
|
||||
"codemirror": {
|
||||
"version": "5.61.1",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.61.1.tgz",
|
||||
"integrity": "sha512-+D1NZjAucuzE93vJGbAaXzvoBHwp9nJZWWWF9utjv25+5AZUiah6CIlfb4ikG4MoDsFsCG8niiJH5++OO2LgIQ=="
|
||||
"version": "5.57.0",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.57.0.tgz",
|
||||
"integrity": "sha512-WGc6UL7Hqt+8a6ZAsj/f1ApQl3NPvHY/UQSzG6fB6l4BjExgVdhFaxd7mRTw1UCiYe/6q86zHP+kfvBQcZGvUg=="
|
||||
},
|
||||
"collapse-white-space": {
|
||||
"version": "1.0.6",
|
||||
@@ -9701,9 +9632,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"dns-packet": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz",
|
||||
"integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==",
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz",
|
||||
"integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ip": "^1.1.0",
|
||||
@@ -17769,6 +17700,12 @@
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@@ -18572,9 +18509,9 @@
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
|
||||
},
|
||||
"lodash-es": {
|
||||
"version": "4.17.20",
|
||||
@@ -18801,9 +18738,9 @@
|
||||
"integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg=="
|
||||
},
|
||||
"marked": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-2.0.6.tgz",
|
||||
"integrity": "sha512-S2mYj0FzTQa0dLddssqwRVW4EOJOVJ355Xm2Vcbm+LU7GQRGWvwbO5K87OaPSOux2AwTSgtPPaXmc8sDPrhn2A==",
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-2.0.3.tgz",
|
||||
"integrity": "sha512-5otztIIcJfPc2qGTN8cVtOJEjNJZ0jwa46INMagrYfk0EvqtRuEHLsEe0LrFS0/q+ZRKT0+kXK7P2T1AN5lWRA==",
|
||||
"dev": true
|
||||
},
|
||||
"martinez-polygon-clipping": {
|
||||
@@ -19165,17 +19102,30 @@
|
||||
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
|
||||
},
|
||||
"monaco-editor": {
|
||||
"version": "0.18.1",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.18.1.tgz",
|
||||
"integrity": "sha512-fmL+RFZ2Hrezy+X/5ZczQW51LUmvzfcqOurnkCIRFTyjdVjzR7JvENzI6+VKBJzJdPh6EYL4RoWl92b2Hrk9fw=="
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.24.0.tgz",
|
||||
"integrity": "sha512-o1f0Lz6ABFNTtnEqqqvlY9qzNx24rQZx1RgYNQ8SkWkE+Ka63keHH/RqxQ4QhN4fs/UYOnvAtEUZsPrzccH++A=="
|
||||
},
|
||||
"monaco-editor-webpack-plugin": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.7.0.tgz",
|
||||
"integrity": "sha512-oItymcnlL14Sjd7EF7q+CMhucfwR/2BxsqrXIBrWL6LQplFfAfV+grLEQRmVHeGSBZ/Gk9ptzfueXnWcoEcFuA==",
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-3.1.0.tgz",
|
||||
"integrity": "sha512-TP5NkCAV0OeFTry5k/d60KR7CkhTXL4kgJKtE3BzjgbDb5TGEPEhoKmHBrSa6r7Oc0sNbPLZhKD/TP2ig7A+/A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/webpack": "^4.4.19"
|
||||
"loader-utils": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"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==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"moo": {
|
||||
@@ -21708,11 +21658,6 @@
|
||||
"react-is": "^16.9.0"
|
||||
}
|
||||
},
|
||||
"react-splitter-layout": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-splitter-layout/-/react-splitter-layout-4.0.0.tgz",
|
||||
"integrity": "sha512-SLqOjBOxRuizWUa83w6q5/u9cDWa9/yj9Iko9V9JFN8x+cqIXiDlUFWSx+icz3IIgvsN/oRIw3za5/32RjIwrA=="
|
||||
},
|
||||
"react-syntax-highlighter": {
|
||||
"version": "12.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-12.2.1.tgz",
|
||||
@@ -24445,6 +24390,12 @@
|
||||
"universalify": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"universalify": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
|
||||
@@ -24460,9 +24411,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.4.tgz",
|
||||
"integrity": "sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==",
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz",
|
||||
"integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==",
|
||||
"dev": true
|
||||
},
|
||||
"typestyle": {
|
||||
|
||||
10
package.json
10
package.json
@@ -22,7 +22,7 @@
|
||||
"@nteract/data-explorer": "8.0.3",
|
||||
"@nteract/directory-listing": "2.0.6",
|
||||
"@nteract/dropdown-menu": "1.0.1",
|
||||
"@nteract/editor": "10.1.12",
|
||||
"@nteract/editor": "10.1.2",
|
||||
"@nteract/fixtures": "2.3.0",
|
||||
"@nteract/iron-icons": "1.0.0",
|
||||
"@nteract/jupyter-widgets": "2.0.0",
|
||||
@@ -74,7 +74,7 @@
|
||||
"jquery-ui-dist": "1.12.1",
|
||||
"knockout": "3.5.1",
|
||||
"mkdirp": "1.0.4",
|
||||
"monaco-editor": "0.18.1",
|
||||
"monaco-editor": "0.24.0",
|
||||
"ms": "2.1.3",
|
||||
"p-retry": "4.2.0",
|
||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||
@@ -89,7 +89,6 @@
|
||||
"react-i18next": "11.8.5",
|
||||
"react-notification-system": "0.2.17",
|
||||
"react-redux": "7.1.3",
|
||||
"react-splitter-layout": "4.0.0",
|
||||
"redux": "4.0.4",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rx-jupyter": "5.5.12",
|
||||
@@ -124,7 +123,6 @@
|
||||
"@types/react-dom": "17.0.3",
|
||||
"@types/react-notification-system": "0.2.39",
|
||||
"@types/react-redux": "7.1.7",
|
||||
"@types/react-splitter-layout": "3.0.1",
|
||||
"@types/sanitize-html": "1.27.2",
|
||||
"@types/sinon": "2.3.3",
|
||||
"@types/styled-components": "5.1.1",
|
||||
@@ -161,7 +159,7 @@
|
||||
"less-loader": "4.1.0",
|
||||
"less-vars-loader": "1.1.0",
|
||||
"mini-css-extract-plugin": "0.4.3",
|
||||
"monaco-editor-webpack-plugin": "1.7.0",
|
||||
"monaco-editor-webpack-plugin": "3.1.0",
|
||||
"node-fetch": "2.6.1",
|
||||
"playwright": "1.10.0",
|
||||
"prettier": "2.2.1",
|
||||
@@ -174,7 +172,7 @@
|
||||
"tslint": "5.11.0",
|
||||
"tslint-microsoft-contrib": "6.0.0",
|
||||
"typedoc": "0.20.36",
|
||||
"typescript": "4.3.4",
|
||||
"typescript": "4.2.4",
|
||||
"url-loader": "1.1.1",
|
||||
"wait-on": "4.0.2",
|
||||
"webpack": "4.46.0",
|
||||
|
||||
@@ -158,6 +158,16 @@ export class DocumentsGridMetrics {
|
||||
public static DocumentEditorMaxWidthRatio: number = 0.4;
|
||||
}
|
||||
|
||||
export class ExplorerMetrics {
|
||||
public static SplitterMinWidth: number = 240;
|
||||
public static SplitterMaxWidth: number = 400;
|
||||
public static CollapsedResourceTreeWidth: number = 36;
|
||||
}
|
||||
|
||||
export class SplitterMetrics {
|
||||
public static CollapsedPositionLeft: number = ExplorerMetrics.CollapsedResourceTreeWidth;
|
||||
}
|
||||
|
||||
export class Areas {
|
||||
public static ResourceTree: string = "Resource Tree";
|
||||
public static ContextualPane: string = "Contextual Pane";
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Collection } from "../Contracts/ViewModels";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { updateUserContext } from "../UserContext";
|
||||
import { deleteDocument, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
|
||||
jest.mock("../ResourceProvider/ResourceProviderClient.ts");
|
||||
|
||||
const databaseId = "testDB";
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ export function queryDocuments(
|
||||
headers: response.headers,
|
||||
};
|
||||
}
|
||||
await errorHandling(response, "querying documents", params);
|
||||
errorHandling(response, "querying documents", params);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
@@ -153,11 +153,11 @@ export function readDocument(
|
||||
),
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
return await errorHandling(response, "reading document", params);
|
||||
return errorHandling(response, "reading document", params);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -192,11 +192,11 @@ export function createDocument(
|
||||
...authHeaders(),
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
return await errorHandling(response, "creating document", params);
|
||||
return errorHandling(response, "creating document", params);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -238,11 +238,11 @@ export function updateDocument(
|
||||
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
return await errorHandling(response, "updating document", params);
|
||||
return errorHandling(response, "updating document", params);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -278,11 +278,11 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
|
||||
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return undefined;
|
||||
}
|
||||
return await errorHandling(response, "deleting document", params);
|
||||
return errorHandling(response, "deleting document", params);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -325,11 +325,11 @@ export function createMongoCollectionWithProxy(
|
||||
},
|
||||
}
|
||||
)
|
||||
.then(async (response) => {
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
return await errorHandling(response, "creating collection", mongoParams);
|
||||
return errorHandling(response, "creating collection", mongoParams);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import * as _ from "underscore";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import DocumentsTab from "../Explorer/Tabs/DocumentsTab";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { useDatabases } from "../Explorer/useDatabases";
|
||||
import { userContext } from "../UserContext";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import * as QueryUtils from "../Utils/QueryUtils";
|
||||
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
|
||||
import { createCollection } from "./dataAccess/createCollection";
|
||||
import { createDocument } from "./dataAccess/createDocument";
|
||||
import { deleteDocument } from "./dataAccess/deleteDocument";
|
||||
import { queryDocuments } from "./dataAccess/queryDocuments";
|
||||
import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage";
|
||||
import { handleError } from "./ErrorHandlingUtils";
|
||||
|
||||
export class QueriesClient {
|
||||
@@ -98,35 +100,45 @@ export class QueriesClient {
|
||||
|
||||
const options: any = { enableCrossPartitionQuery: true };
|
||||
const clearMessage = NotificationConsoleUtils.logConsoleProgress("Fetching saved queries");
|
||||
const results = await queryDocuments(
|
||||
const queryIterator: QueryIterator<ItemDefinition & Resource> = queryDocuments(
|
||||
SavedQueries.DatabaseName,
|
||||
SavedQueries.CollectionName,
|
||||
this.fetchQueriesQuery(),
|
||||
options
|
||||
).fetchAll();
|
||||
|
||||
let queries: DataModels.Query[] = _.map(results.resources, (document: DataModels.Query) => {
|
||||
if (!document) {
|
||||
return undefined;
|
||||
}
|
||||
const { id, resourceId, query, queryName } = document;
|
||||
const parsedQuery: DataModels.Query = {
|
||||
resourceId: resourceId,
|
||||
queryName: queryName,
|
||||
query: query,
|
||||
id: id,
|
||||
};
|
||||
try {
|
||||
this.validateQuery(parsedQuery);
|
||||
return parsedQuery;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
queries = _.reject(queries, (parsedQuery: DataModels.Query) => !parsedQuery);
|
||||
NotificationConsoleUtils.logConsoleInfo("Successfully fetched saved queries");
|
||||
clearMessage();
|
||||
return queries;
|
||||
);
|
||||
const fetchQueries = async (firstItemIndex: number): Promise<ViewModels.QueryResults> =>
|
||||
await queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex);
|
||||
return QueryUtils.queryAllPages(fetchQueries)
|
||||
.then(
|
||||
(results: ViewModels.QueryResults) => {
|
||||
let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => {
|
||||
if (!document) {
|
||||
return undefined;
|
||||
}
|
||||
const { id, resourceId, query, queryName } = document;
|
||||
const parsedQuery: DataModels.Query = {
|
||||
resourceId: resourceId,
|
||||
queryName: queryName,
|
||||
query: query,
|
||||
id: id,
|
||||
};
|
||||
try {
|
||||
this.validateQuery(parsedQuery);
|
||||
return parsedQuery;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
queries = _.reject(queries, (parsedQuery: DataModels.Query) => !parsedQuery);
|
||||
NotificationConsoleUtils.logConsoleInfo("Successfully fetched saved queries");
|
||||
return Promise.resolve(queries);
|
||||
},
|
||||
(error: any) => {
|
||||
handleError(error, "getSavedQueries", "Failed to fetch saved queries");
|
||||
return Promise.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => clearMessage());
|
||||
}
|
||||
|
||||
public async deleteQuery(query: DataModels.Query): Promise<void> {
|
||||
@@ -177,7 +189,7 @@ export class QueriesClient {
|
||||
|
||||
private findQueriesCollection(): ViewModels.Collection {
|
||||
const queriesDatabase: ViewModels.Database = _.find(
|
||||
useDatabases.getState().databases,
|
||||
this.container.databases(),
|
||||
(database: ViewModels.Database) => database.id() === SavedQueries.DatabaseName
|
||||
);
|
||||
if (!queriesDatabase) {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import * as ko from "knockout";
|
||||
|
||||
import { SplitterMetrics } from "./Constants";
|
||||
|
||||
export enum SplitterDirection {
|
||||
Horizontal = "horizontal",
|
||||
Vertical = "vertical",
|
||||
@@ -24,12 +28,14 @@ export class Splitter {
|
||||
public lastX!: number;
|
||||
public lastWidth!: number;
|
||||
|
||||
private isCollapsed: ko.Observable<boolean>;
|
||||
private bounds: SplitterBounds;
|
||||
private direction: SplitterDirection;
|
||||
|
||||
constructor(options: SplitterOptions) {
|
||||
this.splitterId = options.splitterId;
|
||||
this.leftSideId = options.leftId;
|
||||
this.isCollapsed = ko.observable<boolean>(false);
|
||||
this.bounds = options.bounds;
|
||||
this.direction = options.direction;
|
||||
this.initialize();
|
||||
@@ -77,4 +83,23 @@ export class Splitter {
|
||||
};
|
||||
|
||||
private onResizeStop: JQueryUI.ResizableEvent = () => $("iframe").css("pointer-events", "auto");
|
||||
|
||||
public collapseLeft() {
|
||||
this.lastX = $(this.splitter).position().left;
|
||||
this.lastWidth = $(this.leftSide).width();
|
||||
$(this.splitter).css("left", SplitterMetrics.CollapsedPositionLeft);
|
||||
$(this.leftSide).css("width", "");
|
||||
$(this.leftSide).resizable("option", "disabled", true).removeClass("ui-resizable-disabled"); // remove class so splitter is visible
|
||||
$(this.splitter).removeClass("ui-resizable-e");
|
||||
this.isCollapsed(true);
|
||||
}
|
||||
|
||||
public expandLeft() {
|
||||
$(this.splitter).addClass("ui-resizable-e");
|
||||
$(this.leftSide).css("width", this.lastWidth);
|
||||
$(this.splitter).css("left", this.lastX);
|
||||
$(this.splitter).css("left", ""); // this ensures the splitter's position is not fixed and enables movement during resizing
|
||||
$(this.leftSide).resizable("enable");
|
||||
this.isCollapsed(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,14 +120,6 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
|
||||
const armAPIVersion = params.get("armAPIVersion") || "";
|
||||
updateConfigContext({ armAPIVersion });
|
||||
}
|
||||
if (params.has("armEndpoint")) {
|
||||
const ARM_ENDPOINT = params.get("armEndpoint") || "";
|
||||
updateConfigContext({ ARM_ENDPOINT });
|
||||
}
|
||||
if (params.has("aadEndpoint")) {
|
||||
const AAD_ENDPOINT = params.get("aadEndpoint") || "";
|
||||
updateConfigContext({ AAD_ENDPOINT });
|
||||
}
|
||||
if (params.has("platform")) {
|
||||
const platform = params.get("platform");
|
||||
switch (platform) {
|
||||
|
||||
@@ -3,10 +3,11 @@ import {
|
||||
Resource,
|
||||
StoredProcedureDefinition,
|
||||
TriggerDefinition,
|
||||
UserDefinedFunctionDefinition
|
||||
UserDefinedFunctionDefinition,
|
||||
} from "@azure/cosmos";
|
||||
import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/ConsoleData";
|
||||
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { CassandraTableKey, CassandraTableKeys } from "../Explorer/Tables/TableDataClient";
|
||||
import ConflictId from "../Explorer/Tree/ConflictId";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
@@ -89,6 +90,7 @@ export interface Database extends TreeNode {
|
||||
|
||||
selectedSubnodeKind: ko.Observable<CollectionTabKind>;
|
||||
|
||||
selectDatabase(): void;
|
||||
expandDatabase(): Promise<void>;
|
||||
collapseDatabase(): void;
|
||||
|
||||
@@ -274,6 +276,8 @@ export interface TabOptions {
|
||||
tabKind: CollectionTabKind;
|
||||
title: string;
|
||||
tabPath: string;
|
||||
hashLocation: string;
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]) => void;
|
||||
isTabsContentExpanded?: ko.Observable<boolean>;
|
||||
onLoadStartKey?: number;
|
||||
|
||||
@@ -284,7 +288,6 @@ export interface TabOptions {
|
||||
rid?: string;
|
||||
node?: TreeNode;
|
||||
theme?: string;
|
||||
index?: number;
|
||||
}
|
||||
|
||||
export interface DocumentsTabOptions extends TabOptions {
|
||||
@@ -370,7 +373,6 @@ export enum TerminalKind {
|
||||
Default = 0,
|
||||
Mongo = 1,
|
||||
Cassandra = 2,
|
||||
PostgreSQL = 3
|
||||
}
|
||||
|
||||
export interface DataExplorerInputsFrame {
|
||||
|
||||
172
src/Explorer/ContextMenuButtonFactory.ts
Normal file
172
src/Explorer/ContextMenuButtonFactory.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import AddCollectionIcon from "../../images/AddCollection.svg";
|
||||
import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg";
|
||||
import AddStoredProcedureIcon from "../../images/AddStoredProcedure.svg";
|
||||
import AddTriggerIcon from "../../images/AddTrigger.svg";
|
||||
import AddUdfIcon from "../../images/AddUdf.svg";
|
||||
import DeleteCollectionIcon from "../../images/DeleteCollection.svg";
|
||||
import DeleteDatabaseIcon from "../../images/DeleteDatabase.svg";
|
||||
import DeleteSprocIcon from "../../images/DeleteSproc.svg";
|
||||
import DeleteTriggerIcon from "../../images/DeleteTrigger.svg";
|
||||
import DeleteUDFIcon from "../../images/DeleteUDF.svg";
|
||||
import HostedTerminalIcon from "../../images/Hosted-Terminal.svg";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
|
||||
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
|
||||
import Explorer from "./Explorer";
|
||||
import StoredProcedure from "./Tree/StoredProcedure";
|
||||
import Trigger from "./Tree/Trigger";
|
||||
import UserDefinedFunction from "./Tree/UserDefinedFunction";
|
||||
export interface CollectionContextMenuButtonParams {
|
||||
databaseId: string;
|
||||
collectionId: string;
|
||||
}
|
||||
|
||||
export interface DatabaseContextMenuButtonParams {
|
||||
databaseId: string;
|
||||
}
|
||||
/**
|
||||
* New resource tree (in ReactJS)
|
||||
*/
|
||||
export class ResourceTreeContextMenuButtonFactory {
|
||||
public static createDatabaseContextMenu(container: Explorer, databaseId: string): TreeNodeMenuItem[] {
|
||||
const items: TreeNodeMenuItem[] = [
|
||||
{
|
||||
iconSrc: AddCollectionIcon,
|
||||
onClick: () => container.onNewCollectionClicked(databaseId),
|
||||
label: `New ${getCollectionName()}`,
|
||||
},
|
||||
];
|
||||
|
||||
if (userContext.apiType !== "Tables") {
|
||||
items.push({
|
||||
iconSrc: DeleteDatabaseIcon,
|
||||
onClick: () => container.openDeleteDatabaseConfirmationPane(),
|
||||
label: `Delete ${getDatabaseName()}`,
|
||||
styleClass: "deleteDatabaseMenuItem",
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
public static createCollectionContextMenuButton(
|
||||
container: Explorer,
|
||||
selectedCollection: ViewModels.Collection
|
||||
): TreeNodeMenuItem[] {
|
||||
const items: TreeNodeMenuItem[] = [];
|
||||
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
|
||||
items.push({
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null),
|
||||
label: "New SQL Query",
|
||||
});
|
||||
}
|
||||
|
||||
if (userContext.apiType === "Mongo") {
|
||||
items.push({
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null),
|
||||
label: "New Query",
|
||||
});
|
||||
|
||||
items.push({
|
||||
iconSrc: HostedTerminalIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
if (container.isShellEnabled()) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
} else {
|
||||
selectedCollection && selectedCollection.onNewMongoShellClick();
|
||||
}
|
||||
},
|
||||
label: container.isShellEnabled() ? "Open Mongo Shell" : "New Shell",
|
||||
});
|
||||
}
|
||||
|
||||
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
|
||||
items.push({
|
||||
iconSrc: AddStoredProcedureIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null);
|
||||
},
|
||||
label: "New Stored Procedure",
|
||||
});
|
||||
|
||||
items.push({
|
||||
iconSrc: AddUdfIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection, null);
|
||||
},
|
||||
label: "New UDF",
|
||||
});
|
||||
|
||||
items.push({
|
||||
iconSrc: AddTriggerIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, null);
|
||||
},
|
||||
label: "New Trigger",
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
iconSrc: DeleteCollectionIcon,
|
||||
onClick: () => container.openDeleteCollectionConfirmationPane(),
|
||||
label: `Delete ${getCollectionName()}`,
|
||||
styleClass: "deleteCollectionMenuItem",
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
public static createStoreProcedureContextMenuItems(
|
||||
container: Explorer,
|
||||
storedProcedure: StoredProcedure
|
||||
): TreeNodeMenuItem[] {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
iconSrc: DeleteSprocIcon,
|
||||
onClick: () => storedProcedure.delete(),
|
||||
label: "Delete Store Procedure",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public static createTriggerContextMenuItems(container: Explorer, trigger: Trigger): TreeNodeMenuItem[] {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
iconSrc: DeleteTriggerIcon,
|
||||
onClick: () => trigger.delete(),
|
||||
label: "Delete Trigger",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public static createUserDefinedFunctionContextMenuItems(
|
||||
container: Explorer,
|
||||
userDefinedFunction: UserDefinedFunction
|
||||
): TreeNodeMenuItem[] {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
iconSrc: DeleteUDFIcon,
|
||||
onClick: () => userDefinedFunction.delete(),
|
||||
label: "Delete User Defined Function",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
import React from "react";
|
||||
import AddCollectionIcon from "../../images/AddCollection.svg";
|
||||
import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg";
|
||||
import AddStoredProcedureIcon from "../../images/AddStoredProcedure.svg";
|
||||
import AddTriggerIcon from "../../images/AddTrigger.svg";
|
||||
import AddUdfIcon from "../../images/AddUdf.svg";
|
||||
import DeleteCollectionIcon from "../../images/DeleteCollection.svg";
|
||||
import DeleteDatabaseIcon from "../../images/DeleteDatabase.svg";
|
||||
import DeleteSprocIcon from "../../images/DeleteSproc.svg";
|
||||
import DeleteTriggerIcon from "../../images/DeleteTrigger.svg";
|
||||
import DeleteUDFIcon from "../../images/DeleteUDF.svg";
|
||||
import HostedTerminalIcon from "../../images/Hosted-Terminal.svg";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
|
||||
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
|
||||
import Explorer from "./Explorer";
|
||||
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
|
||||
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
|
||||
import StoredProcedure from "./Tree/StoredProcedure";
|
||||
import Trigger from "./Tree/Trigger";
|
||||
import UserDefinedFunction from "./Tree/UserDefinedFunction";
|
||||
import { useSelectedNode } from "./useSelectedNode";
|
||||
|
||||
export interface CollectionContextMenuButtonParams {
|
||||
databaseId: string;
|
||||
collectionId: string;
|
||||
}
|
||||
|
||||
export interface DatabaseContextMenuButtonParams {
|
||||
databaseId: string;
|
||||
}
|
||||
/**
|
||||
* New resource tree (in ReactJS)
|
||||
*/
|
||||
export const createDatabaseContextMenu = (container: Explorer, databaseId: string): TreeNodeMenuItem[] => {
|
||||
const items: TreeNodeMenuItem[] = [
|
||||
{
|
||||
iconSrc: AddCollectionIcon,
|
||||
onClick: () => container.onNewCollectionClicked(databaseId),
|
||||
label: `New ${getCollectionName()}`,
|
||||
},
|
||||
];
|
||||
|
||||
if (userContext.apiType !== "Tables") {
|
||||
items.push({
|
||||
iconSrc: DeleteDatabaseIcon,
|
||||
onClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel("Delete " + getDatabaseName(), <DeleteDatabaseConfirmationPanel explorer={container} />),
|
||||
label: `Delete ${getDatabaseName()}`,
|
||||
styleClass: "deleteDatabaseMenuItem",
|
||||
});
|
||||
}
|
||||
return items;
|
||||
};
|
||||
|
||||
export const createCollectionContextMenuButton = (
|
||||
container: Explorer,
|
||||
selectedCollection: ViewModels.Collection
|
||||
): TreeNodeMenuItem[] => {
|
||||
const items: TreeNodeMenuItem[] = [];
|
||||
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
|
||||
items.push({
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, undefined),
|
||||
label: "New SQL Query",
|
||||
});
|
||||
}
|
||||
|
||||
if (userContext.apiType === "Mongo") {
|
||||
items.push({
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, undefined),
|
||||
label: "New Query",
|
||||
});
|
||||
|
||||
items.push({
|
||||
iconSrc: HostedTerminalIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
if (container.isShellEnabled()) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
} else {
|
||||
selectedCollection && selectedCollection.onNewMongoShellClick();
|
||||
}
|
||||
},
|
||||
label: container.isShellEnabled() ? "Open Mongo Shell" : "New Shell",
|
||||
});
|
||||
}
|
||||
|
||||
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
|
||||
items.push({
|
||||
iconSrc: AddStoredProcedureIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined);
|
||||
},
|
||||
label: "New Stored Procedure",
|
||||
});
|
||||
|
||||
items.push({
|
||||
iconSrc: AddUdfIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection, undefined);
|
||||
},
|
||||
label: "New UDF",
|
||||
});
|
||||
|
||||
items.push({
|
||||
iconSrc: AddTriggerIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, undefined);
|
||||
},
|
||||
label: "New Trigger",
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
iconSrc: DeleteCollectionIcon,
|
||||
onClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel("Delete " + getCollectionName(), <DeleteCollectionConfirmationPane explorer={container} />),
|
||||
label: `Delete ${getCollectionName()}`,
|
||||
styleClass: "deleteCollectionMenuItem",
|
||||
});
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
export const createStoreProcedureContextMenuItems = (
|
||||
container: Explorer,
|
||||
storedProcedure: StoredProcedure
|
||||
): TreeNodeMenuItem[] => {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
iconSrc: DeleteSprocIcon,
|
||||
onClick: () => storedProcedure.delete(),
|
||||
label: "Delete Store Procedure",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const createTriggerContextMenuItems = (container: Explorer, trigger: Trigger): TreeNodeMenuItem[] => {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
iconSrc: DeleteTriggerIcon,
|
||||
onClick: () => trigger.delete(),
|
||||
label: "Delete Trigger",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const createUserDefinedFunctionContextMenuItems = (
|
||||
container: Explorer,
|
||||
userDefinedFunction: UserDefinedFunction
|
||||
): TreeNodeMenuItem[] => {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
iconSrc: DeleteUDFIcon,
|
||||
onClick: () => userDefinedFunction.delete(),
|
||||
label: "Delete User Defined Function",
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -52,7 +52,7 @@ class EditorViewModel extends JsonEditorViewModel {
|
||||
if (EditorViewModel.providerRegistered.indexOf("sql") < 0) {
|
||||
const { SqlCompletionItemProvider } = await import("@azure/cosmos-language-service");
|
||||
const monaco = await loadMonaco();
|
||||
monaco.languages.registerCompletionItemProvider("sql", new SqlCompletionItemProvider());
|
||||
monaco.languages.registerCompletionItemProvider("sql", new SqlCompletionItemProvider() as any); //TODO: Remove any. The upstream cosmos language service needs to be fixed
|
||||
EditorViewModel.providerRegistered.push("sql");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { Spinner, SpinnerSize } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { loadMonaco, monaco } from "../../LazyMonaco";
|
||||
// import "./EditorReact.less";
|
||||
|
||||
interface EditorReactStates {
|
||||
showEditor: boolean;
|
||||
}
|
||||
export interface EditorReactProps {
|
||||
language: string;
|
||||
content: string;
|
||||
@@ -17,26 +12,22 @@ export interface EditorReactProps {
|
||||
theme?: string; // Monaco editor theme
|
||||
}
|
||||
|
||||
export class EditorReact extends React.Component<EditorReactProps, EditorReactStates> {
|
||||
export class EditorReact extends React.Component<EditorReactProps> {
|
||||
private rootNode: HTMLElement;
|
||||
private editor: monaco.editor.IStandaloneCodeEditor;
|
||||
private selectionListener: monaco.IDisposable;
|
||||
|
||||
public constructor(props: EditorReactProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showEditor: false,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.createEditor(this.configureEditor.bind(this));
|
||||
}
|
||||
|
||||
public componentDidUpdate(previous: EditorReactProps) {
|
||||
if (this.props.content !== previous.content) {
|
||||
this.editor.setValue(this.props.content);
|
||||
}
|
||||
public shouldComponentUpdate(): boolean {
|
||||
// Prevents component re-rendering
|
||||
return false;
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
@@ -44,12 +35,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{!this.state.showEditor && <Spinner size={SpinnerSize.large} className="spinner" />}
|
||||
<div className="jsonEditor" ref={(elt: HTMLElement) => this.setRef(elt)} />
|
||||
</React.Fragment>
|
||||
);
|
||||
return <div className="jsonEditor" ref={(elt: HTMLElement) => this.setRef(elt)} />;
|
||||
}
|
||||
|
||||
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
|
||||
@@ -76,7 +62,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||
* Create the monaco editor and attach to DOM
|
||||
*/
|
||||
private async createEditor(createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) {
|
||||
const options: monaco.editor.IEditorConstructionOptions = {
|
||||
const options = {
|
||||
value: this.props.content,
|
||||
language: this.props.language,
|
||||
readOnly: this.props.isReadOnly,
|
||||
@@ -90,12 +76,6 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||
this.rootNode.innerHTML = "";
|
||||
const monaco = await loadMonaco();
|
||||
createCallback(monaco.editor.create(this.rootNode, options));
|
||||
|
||||
if (this.rootNode.innerHTML) {
|
||||
this.setState({
|
||||
showEditor: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private setRef(element: HTMLElement): void {
|
||||
|
||||
20
src/Explorer/Controls/GitHub/GitHubReposComponentAdapter.tsx
Normal file
20
src/Explorer/Controls/GitHub/GitHubReposComponentAdapter.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import ko from "knockout";
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
import { GitHubReposComponent, GitHubReposComponentProps } from "./GitHubReposComponent";
|
||||
|
||||
export class GitHubReposComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Observable<number>;
|
||||
|
||||
constructor(private props: GitHubReposComponentProps) {
|
||||
this.parameters = ko.observable<number>(Date.now());
|
||||
}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
return <GitHubReposComponent {...this.props} />;
|
||||
}
|
||||
|
||||
public triggerRender(): void {
|
||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,7 @@ export class JsonEditorViewModel extends WaitsForTemplateViewModel {
|
||||
protected async createEditor(content: string, createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) {
|
||||
this.registerCompletionItemProvider();
|
||||
this.editorContainer = document.getElementById(this.getEditorId());
|
||||
const options: monaco.editor.IEditorConstructionOptions = {
|
||||
const options = {
|
||||
value: content,
|
||||
language: this.getEditorLanguage(),
|
||||
readOnly: this.params.isReadOnly,
|
||||
|
||||
@@ -55,7 +55,7 @@ export class NotebookViewerComponent
|
||||
databaseAccountName: undefined,
|
||||
defaultExperience: "NotebookViewer",
|
||||
isReadOnly: true,
|
||||
cellEditorType: "codemirror",
|
||||
cellEditorType: "monaco",
|
||||
autoSaveInterval: 365 * 24 * 3600 * 1000, // There is no way to turn off auto-save, set to 1 year
|
||||
contentProvider: contents.JupyterContentProvider, // NotebookViewer only knows how to talk to Jupyter contents API
|
||||
});
|
||||
|
||||
@@ -38,6 +38,8 @@ describe("SettingsComponent", () => {
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
node: undefined,
|
||||
hashLocation: "settings",
|
||||
onUpdateTabsButtons: undefined,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -126,6 +128,7 @@ describe("SettingsComponent", () => {
|
||||
isDatabaseExpanded: undefined,
|
||||
isDatabaseShared: ko.computed(() => true),
|
||||
selectedSubnodeKind: undefined,
|
||||
selectDatabase: undefined,
|
||||
expandDatabase: undefined,
|
||||
collapseDatabase: undefined,
|
||||
loadCollections: undefined,
|
||||
|
||||
@@ -16,9 +16,8 @@ import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/T
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import Explorer from "../../Explorer";
|
||||
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
||||
import "./SettingsComponent.less";
|
||||
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
|
||||
@@ -110,7 +109,6 @@ export interface SettingsComponentState {
|
||||
|
||||
initialNotification: DataModels.Notification;
|
||||
selectedTab: SettingsV2TabTypes;
|
||||
offerLoaded: boolean;
|
||||
}
|
||||
|
||||
export class SettingsComponent extends React.Component<SettingsComponentProps, SettingsComponentState> {
|
||||
@@ -123,6 +121,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private collection: ViewModels.Collection;
|
||||
private database: ViewModels.Database;
|
||||
private offer: DataModels.Offer;
|
||||
private container: Explorer;
|
||||
private changeFeedPolicyVisible: boolean;
|
||||
private isFixedContainer: boolean;
|
||||
private shouldShowIndexingPolicyEditor: boolean;
|
||||
@@ -134,6 +133,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.isCollectionSettingsTab = this.props.settingsTab.tabKind === ViewModels.CollectionTabKind.CollectionSettingsV2;
|
||||
if (this.isCollectionSettingsTab) {
|
||||
this.collection = this.props.settingsTab.collection as ViewModels.Collection;
|
||||
this.container = this.collection?.container;
|
||||
this.offer = this.collection?.offer();
|
||||
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
|
||||
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
||||
@@ -145,6 +145,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
userContext.apiType === "Mongo" && (!this.collection?.partitionKey || this.collection?.partitionKey.systemKey);
|
||||
} else {
|
||||
this.database = this.props.settingsTab.database;
|
||||
this.container = this.database?.container;
|
||||
this.offer = this.database?.offer();
|
||||
}
|
||||
|
||||
@@ -195,7 +196,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
|
||||
initialNotification: undefined,
|
||||
selectedTab: SettingsV2TabTypes.ScaleTab,
|
||||
offerLoaded: !!this.offer,
|
||||
};
|
||||
|
||||
this.saveSettingsButton = {
|
||||
@@ -217,19 +217,18 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
if (this.isCollectionSettingsTab) {
|
||||
this.refreshIndexTransformationProgress();
|
||||
this.loadMongoIndexes();
|
||||
this.loadCollectionOffer();
|
||||
}
|
||||
|
||||
this.setAutoPilotStates();
|
||||
this.setBaseline();
|
||||
if (this.props.settingsTab.isActive()) {
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
this.props.settingsTab.getContainer().onUpdateTabsButtons(this.getTabsButtons());
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(): void {
|
||||
if (this.props.settingsTab.isActive()) {
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
this.props.settingsTab.getContainer().onUpdateTabsButtons(this.getTabsButtons());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,7 +293,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.state.wasAutopilotOriginallySet !== this.state.isAutoPilotSelected;
|
||||
|
||||
public shouldShowKeyspaceSharedThroughputMessage = (): boolean =>
|
||||
userContext.apiType === "Cassandra" && hasDatabaseSharedThroughput(this.collection);
|
||||
this.container && userContext.apiType === "Cassandra" && hasDatabaseSharedThroughput(this.collection);
|
||||
|
||||
public hasConflictResolution = (): boolean =>
|
||||
userContext?.databaseAccount?.properties?.enableMultipleWriteLocations &&
|
||||
@@ -372,34 +371,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
});
|
||||
};
|
||||
|
||||
private async loadCollectionOffer() {
|
||||
try {
|
||||
this.props.settingsTab.isExecuting(true);
|
||||
await this.collection.loadOffer();
|
||||
this.props.settingsTab.tabTitle(this.collection.offer() ? "Settings" : "Scale & Settings");
|
||||
this.setState({ offerLoaded: true });
|
||||
} catch (error) {
|
||||
this.props.settingsTab.isExecutionError(true);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
traceFailure(
|
||||
Action.Tab,
|
||||
{
|
||||
databaseName: this.collection.databaseId,
|
||||
collectionName: this.collection.id(),
|
||||
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.props.settingsTab.tabTitle,
|
||||
error: errorMessage,
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
this.props.settingsTab.onLoadStartKey
|
||||
);
|
||||
logConsoleError(`Error while fetching container settings for container ${this.collection.id()}: ${errorMessage}`);
|
||||
} finally {
|
||||
this.props.settingsTab.isExecuting(false);
|
||||
}
|
||||
}
|
||||
|
||||
private getMongoIndexesToSave = (): MongoIndex[] => {
|
||||
let finalIndexes: MongoIndex[] = [];
|
||||
this.state.currentMongoIndexes?.map((mongoIndex: MongoIndex, index: number) => {
|
||||
@@ -912,6 +883,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
const scaleComponentProps: ScaleComponentProps = {
|
||||
collection: this.collection,
|
||||
database: this.database,
|
||||
container: this.container,
|
||||
isFixedContainer: this.isFixedContainer,
|
||||
onThroughputChange: this.onThroughputChange,
|
||||
throughput: this.state.throughput,
|
||||
@@ -937,12 +909,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.state.offerLoaded) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const subSettingsComponentProps: SubSettingsComponentProps = {
|
||||
collection: this.collection,
|
||||
container: this.container,
|
||||
isAnalyticalStorageEnabled: this.isAnalyticalStorageEnabled,
|
||||
changeFeedPolicyVisible: this.changeFeedPolicyVisible,
|
||||
timeToLive: this.state.timeToLive,
|
||||
@@ -995,6 +964,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
|
||||
const conflictResolutionPolicyComponentProps: ConflictResolutionComponentProps = {
|
||||
collection: this.collection,
|
||||
container: this.container,
|
||||
conflictResolutionPolicyMode: this.state.conflictResolutionPolicyMode,
|
||||
conflictResolutionPolicyModeBaseline: this.state.conflictResolutionPolicyModeBaseline,
|
||||
onConflictResolutionPolicyModeChange: this.onConflictResolutionPolicyModeChange,
|
||||
|
||||
14
src/Explorer/Controls/Settings/SettingsComponentAdapter.tsx
Normal file
14
src/Explorer/Controls/Settings/SettingsComponentAdapter.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import ko from "knockout";
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
import { SettingsComponent, SettingsComponentProps } from "./SettingsComponent";
|
||||
|
||||
export class SettingsComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Computed<boolean>;
|
||||
|
||||
constructor(private props: SettingsComponentProps) {}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
return this.parameters() ? <SettingsComponent {...this.props} /> : <></>;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { ConflictResolutionComponentProps, ConflictResolutionComponent } from "./ConflictResolutionComponent";
|
||||
import { container, collection } from "../TestUtils";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
import { collection } from "../TestUtils";
|
||||
import { ConflictResolutionComponent, ConflictResolutionComponentProps } from "./ConflictResolutionComponent";
|
||||
|
||||
describe("ConflictResolutionComponent", () => {
|
||||
const baseProps: ConflictResolutionComponentProps = {
|
||||
collection: collection,
|
||||
container: container,
|
||||
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode.Custom,
|
||||
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode.Custom,
|
||||
onConflictResolutionPolicyModeChange: () => {
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { ChoiceGroup, IChoiceGroupOption, ITextFieldProps, Stack, TextField } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
import Explorer from "../../../Explorer";
|
||||
import {
|
||||
conflictResolutionCustomToolTip,
|
||||
conflictResolutionLwwTooltip,
|
||||
getChoiceGroupStyles,
|
||||
getTextFieldStyles,
|
||||
conflictResolutionLwwTooltip,
|
||||
conflictResolutionCustomToolTip,
|
||||
subComponentStackProps,
|
||||
getChoiceGroupStyles,
|
||||
} from "../SettingsRenderUtils";
|
||||
import { isDirty } from "../SettingsUtils";
|
||||
import { TextField, ITextFieldProps, Stack, IChoiceGroupOption, ChoiceGroup } from "@fluentui/react";
|
||||
import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
|
||||
import { isDirty } from "../SettingsUtils";
|
||||
|
||||
export interface ConflictResolutionComponentProps {
|
||||
collection: ViewModels.Collection;
|
||||
container: Explorer;
|
||||
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode;
|
||||
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode;
|
||||
onConflictResolutionPolicyModeChange: (newMode: DataModels.ConflictResolutionMode) => void;
|
||||
|
||||
@@ -7,17 +7,20 @@ import * as SharedConstants from "../../../../Shared/Constants";
|
||||
import { updateUserContext } from "../../../../UserContext";
|
||||
import Explorer from "../../../Explorer";
|
||||
import { throughputUnit } from "../SettingsRenderUtils";
|
||||
import { collection } from "../TestUtils";
|
||||
import { collection, container } from "../TestUtils";
|
||||
import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent";
|
||||
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
|
||||
|
||||
describe("ScaleComponent", () => {
|
||||
const nonNationalCloudContainer = new Explorer();
|
||||
nonNationalCloudContainer.isRunningOnNationalCloud = () => false;
|
||||
|
||||
const targetThroughput = 6000;
|
||||
|
||||
const baseProps: ScaleComponentProps = {
|
||||
collection: collection,
|
||||
database: undefined,
|
||||
container: container,
|
||||
isFixedContainer: false,
|
||||
onThroughputChange: () => {
|
||||
return;
|
||||
@@ -108,7 +111,7 @@ describe("ScaleComponent", () => {
|
||||
let scaleComponent = new ScaleComponent(baseProps);
|
||||
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
|
||||
|
||||
let newProps = { ...baseProps };
|
||||
let newProps = { ...baseProps, container: nonNationalCloudContainer };
|
||||
scaleComponent = new ScaleComponent(newProps);
|
||||
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
|
||||
|
||||
@@ -121,7 +124,7 @@ describe("ScaleComponent", () => {
|
||||
let scaleComponent = new ScaleComponent(baseProps);
|
||||
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(true);
|
||||
|
||||
const newProps = { ...baseProps };
|
||||
const newProps = { ...baseProps, container: nonNationalCloudContainer };
|
||||
scaleComponent = new ScaleComponent(newProps);
|
||||
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(true);
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
import * as SharedConstants from "../../../../Shared/Constants";
|
||||
import { userContext } from "../../../../UserContext";
|
||||
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
|
||||
import { isRunningOnNationalCloud } from "../../../../Utils/CloudUtils";
|
||||
import Explorer from "../../../Explorer";
|
||||
import {
|
||||
getTextFieldStyles,
|
||||
getThroughputApplyLongDelayMessage,
|
||||
@@ -23,6 +23,7 @@ import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents
|
||||
export interface ScaleComponentProps {
|
||||
collection: ViewModels.Collection;
|
||||
database: ViewModels.Database;
|
||||
container: Explorer;
|
||||
isFixedContainer: boolean;
|
||||
onThroughputChange: (newThroughput: number) => void;
|
||||
throughput: number;
|
||||
@@ -108,7 +109,11 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
};
|
||||
|
||||
public canThroughputExceedMaximumValue = (): boolean => {
|
||||
return !this.props.isFixedContainer && configContext.platform === Platform.Portal && !isRunningOnNationalCloud();
|
||||
return (
|
||||
!this.props.isFixedContainer &&
|
||||
configContext.platform === Platform.Portal &&
|
||||
!this.props.container.isRunningOnNationalCloud()
|
||||
);
|
||||
};
|
||||
|
||||
public getInitialNotificationElement = (): JSX.Element => {
|
||||
|
||||
@@ -4,12 +4,14 @@ import { DatabaseAccount } from "../../../../Contracts/DataModels";
|
||||
import { updateUserContext } from "../../../../UserContext";
|
||||
import Explorer from "../../../Explorer";
|
||||
import { ChangeFeedPolicyState, GeospatialConfigType, TtlOff, TtlOn, TtlOnNoDefault, TtlType } from "../SettingsUtils";
|
||||
import { collection } from "../TestUtils";
|
||||
import { collection, container } from "../TestUtils";
|
||||
import { SubSettingsComponent, SubSettingsComponentProps } from "./SubSettingsComponent";
|
||||
|
||||
describe("SubSettingsComponent", () => {
|
||||
const baseProps: SubSettingsComponentProps = {
|
||||
collection: collection,
|
||||
container: container,
|
||||
|
||||
timeToLive: TtlType.On,
|
||||
timeToLiveBaseline: TtlType.On,
|
||||
onTtlChange: () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ChoiceGroup, IChoiceGroupOption, Label, Link, MessageBar, Stack, Text,
|
||||
import * as React from "react";
|
||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
import { userContext } from "../../../../UserContext";
|
||||
import Explorer from "../../../Explorer";
|
||||
import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||
import {
|
||||
changeFeedPolicyToolTip,
|
||||
@@ -27,6 +28,8 @@ import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
|
||||
|
||||
export interface SubSettingsComponentProps {
|
||||
collection: ViewModels.Collection;
|
||||
container: Explorer;
|
||||
|
||||
timeToLive: TtlType;
|
||||
timeToLiveBaseline: TtlType;
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ describe("SettingsUtils", () => {
|
||||
isDatabaseExpanded: ko.observable(false),
|
||||
isDatabaseShared: ko.computed(() => true),
|
||||
selectedSubnodeKind: ko.observable(undefined),
|
||||
selectDatabase: undefined,
|
||||
expandDatabase: undefined,
|
||||
collapseDatabase: undefined,
|
||||
loadCollections: undefined,
|
||||
|
||||
@@ -30,11 +30,23 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"container": Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"canSaveQueries": [Function],
|
||||
"collapsedResourceTreeWidth": 36,
|
||||
"commandBarComponentAdapter": CommandBarComponentAdapter {
|
||||
"container": [Circular],
|
||||
"isNotebookTabActive": [Function],
|
||||
"parameters": [Function],
|
||||
"tabsButtons": Array [],
|
||||
},
|
||||
"databases": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isHostedDataExplorerEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isResourceTokenCollectionNodeSelected": [Function],
|
||||
"isSchemaEnabled": [Function],
|
||||
"isServerlessEnabled": [Function],
|
||||
"isShellEnabled": [Function],
|
||||
"isSynapseLinkUpdating": [Function],
|
||||
"isTabsContentExpanded": [Function],
|
||||
@@ -49,16 +61,38 @@ exports[`SettingsComponent renders 1`] = `
|
||||
},
|
||||
"refreshNotebookList": [Function],
|
||||
"resourceTokenCollection": [Function],
|
||||
"resourceTokenCollectionId": [Function],
|
||||
"resourceTokenDatabaseId": [Function],
|
||||
"resourceTokenPartitionKey": [Function],
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"databaseCollectionIdMap": Map {},
|
||||
"koSubsCollectionIdMap": Map {},
|
||||
"koSubsDatabaseIdMap": Map {},
|
||||
"parameters": [Function],
|
||||
},
|
||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||
"container": [Circular],
|
||||
"parameters": [Function],
|
||||
},
|
||||
"selectedDatabaseId": [Function],
|
||||
"selectedNode": [Function],
|
||||
"setInProgressConsoleDataIdToBeDeleted": undefined,
|
||||
"setNotificationConsoleData": undefined,
|
||||
"sparkClusterConnectionInfo": [Function],
|
||||
"splitter": Splitter {
|
||||
"bounds": Object {
|
||||
"max": 400,
|
||||
"min": 240,
|
||||
},
|
||||
"direction": "vertical",
|
||||
"isCollapsed": [Function],
|
||||
"leftSideId": "resourcetree",
|
||||
"onResizeStart": [Function],
|
||||
"onResizeStop": [Function],
|
||||
"splitterId": "h_splitter1",
|
||||
},
|
||||
"tabsManager": TabsManager {
|
||||
"activeTab": [Function],
|
||||
"openedTabs": [Function],
|
||||
@@ -82,6 +116,79 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"usageSizeInKB": [Function],
|
||||
}
|
||||
}
|
||||
container={
|
||||
Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"canSaveQueries": [Function],
|
||||
"collapsedResourceTreeWidth": 36,
|
||||
"commandBarComponentAdapter": CommandBarComponentAdapter {
|
||||
"container": [Circular],
|
||||
"isNotebookTabActive": [Function],
|
||||
"parameters": [Function],
|
||||
"tabsButtons": Array [],
|
||||
},
|
||||
"databases": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isHostedDataExplorerEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isResourceTokenCollectionNodeSelected": [Function],
|
||||
"isSchemaEnabled": [Function],
|
||||
"isServerlessEnabled": [Function],
|
||||
"isShellEnabled": [Function],
|
||||
"isSynapseLinkUpdating": [Function],
|
||||
"isTabsContentExpanded": [Function],
|
||||
"memoryUsageInfo": [Function],
|
||||
"notebookBasePath": [Function],
|
||||
"notebookServerInfo": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
},
|
||||
"refreshNotebookList": [Function],
|
||||
"resourceTokenCollection": [Function],
|
||||
"resourceTokenCollectionId": [Function],
|
||||
"resourceTokenDatabaseId": [Function],
|
||||
"resourceTokenPartitionKey": [Function],
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"databaseCollectionIdMap": Map {},
|
||||
"koSubsCollectionIdMap": Map {},
|
||||
"koSubsDatabaseIdMap": Map {},
|
||||
"parameters": [Function],
|
||||
},
|
||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||
"container": [Circular],
|
||||
"parameters": [Function],
|
||||
},
|
||||
"selectedDatabaseId": [Function],
|
||||
"selectedNode": [Function],
|
||||
"setInProgressConsoleDataIdToBeDeleted": undefined,
|
||||
"setNotificationConsoleData": undefined,
|
||||
"sparkClusterConnectionInfo": [Function],
|
||||
"splitter": Splitter {
|
||||
"bounds": Object {
|
||||
"max": 400,
|
||||
"min": 240,
|
||||
},
|
||||
"direction": "vertical",
|
||||
"isCollapsed": [Function],
|
||||
"leftSideId": "resourcetree",
|
||||
"onResizeStart": [Function],
|
||||
"onResizeStop": [Function],
|
||||
"splitterId": "h_splitter1",
|
||||
},
|
||||
"tabsManager": TabsManager {
|
||||
"activeTab": [Function],
|
||||
"openedTabs": [Function],
|
||||
},
|
||||
}
|
||||
}
|
||||
isAutoPilotSelected={false}
|
||||
isFixedContainer={false}
|
||||
onAutoPilotSelected={[Function]}
|
||||
@@ -116,11 +223,23 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"container": Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"canSaveQueries": [Function],
|
||||
"collapsedResourceTreeWidth": 36,
|
||||
"commandBarComponentAdapter": CommandBarComponentAdapter {
|
||||
"container": [Circular],
|
||||
"isNotebookTabActive": [Function],
|
||||
"parameters": [Function],
|
||||
"tabsButtons": Array [],
|
||||
},
|
||||
"databases": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isHostedDataExplorerEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isResourceTokenCollectionNodeSelected": [Function],
|
||||
"isSchemaEnabled": [Function],
|
||||
"isServerlessEnabled": [Function],
|
||||
"isShellEnabled": [Function],
|
||||
"isSynapseLinkUpdating": [Function],
|
||||
"isTabsContentExpanded": [Function],
|
||||
@@ -135,16 +254,38 @@ exports[`SettingsComponent renders 1`] = `
|
||||
},
|
||||
"refreshNotebookList": [Function],
|
||||
"resourceTokenCollection": [Function],
|
||||
"resourceTokenCollectionId": [Function],
|
||||
"resourceTokenDatabaseId": [Function],
|
||||
"resourceTokenPartitionKey": [Function],
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"databaseCollectionIdMap": Map {},
|
||||
"koSubsCollectionIdMap": Map {},
|
||||
"koSubsDatabaseIdMap": Map {},
|
||||
"parameters": [Function],
|
||||
},
|
||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||
"container": [Circular],
|
||||
"parameters": [Function],
|
||||
},
|
||||
"selectedDatabaseId": [Function],
|
||||
"selectedNode": [Function],
|
||||
"setInProgressConsoleDataIdToBeDeleted": undefined,
|
||||
"setNotificationConsoleData": undefined,
|
||||
"sparkClusterConnectionInfo": [Function],
|
||||
"splitter": Splitter {
|
||||
"bounds": Object {
|
||||
"max": 400,
|
||||
"min": 240,
|
||||
},
|
||||
"direction": "vertical",
|
||||
"isCollapsed": [Function],
|
||||
"leftSideId": "resourcetree",
|
||||
"onResizeStart": [Function],
|
||||
"onResizeStop": [Function],
|
||||
"splitterId": "h_splitter1",
|
||||
},
|
||||
"tabsManager": TabsManager {
|
||||
"activeTab": [Function],
|
||||
"openedTabs": [Function],
|
||||
@@ -168,6 +309,79 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"usageSizeInKB": [Function],
|
||||
}
|
||||
}
|
||||
container={
|
||||
Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"canSaveQueries": [Function],
|
||||
"collapsedResourceTreeWidth": 36,
|
||||
"commandBarComponentAdapter": CommandBarComponentAdapter {
|
||||
"container": [Circular],
|
||||
"isNotebookTabActive": [Function],
|
||||
"parameters": [Function],
|
||||
"tabsButtons": Array [],
|
||||
},
|
||||
"databases": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isHostedDataExplorerEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isResourceTokenCollectionNodeSelected": [Function],
|
||||
"isSchemaEnabled": [Function],
|
||||
"isServerlessEnabled": [Function],
|
||||
"isShellEnabled": [Function],
|
||||
"isSynapseLinkUpdating": [Function],
|
||||
"isTabsContentExpanded": [Function],
|
||||
"memoryUsageInfo": [Function],
|
||||
"notebookBasePath": [Function],
|
||||
"notebookServerInfo": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
},
|
||||
"refreshNotebookList": [Function],
|
||||
"resourceTokenCollection": [Function],
|
||||
"resourceTokenCollectionId": [Function],
|
||||
"resourceTokenDatabaseId": [Function],
|
||||
"resourceTokenPartitionKey": [Function],
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"databaseCollectionIdMap": Map {},
|
||||
"koSubsCollectionIdMap": Map {},
|
||||
"koSubsDatabaseIdMap": Map {},
|
||||
"parameters": [Function],
|
||||
},
|
||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||
"container": [Circular],
|
||||
"parameters": [Function],
|
||||
},
|
||||
"selectedDatabaseId": [Function],
|
||||
"selectedNode": [Function],
|
||||
"setInProgressConsoleDataIdToBeDeleted": undefined,
|
||||
"setNotificationConsoleData": undefined,
|
||||
"sparkClusterConnectionInfo": [Function],
|
||||
"splitter": Splitter {
|
||||
"bounds": Object {
|
||||
"max": 400,
|
||||
"min": 240,
|
||||
},
|
||||
"direction": "vertical",
|
||||
"isCollapsed": [Function],
|
||||
"leftSideId": "resourcetree",
|
||||
"onResizeStart": [Function],
|
||||
"onResizeStop": [Function],
|
||||
"splitterId": "h_splitter1",
|
||||
},
|
||||
"tabsManager": TabsManager {
|
||||
"activeTab": [Function],
|
||||
"openedTabs": [Function],
|
||||
},
|
||||
}
|
||||
}
|
||||
geospatialConfigType="Geometry"
|
||||
geospatialConfigTypeBaseline="Geometry"
|
||||
isAnalyticalStorageEnabled={false}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ThroughputInput } from "./ThroughputInput";
|
||||
const props = {
|
||||
isDatabase: false,
|
||||
showFreeTierExceedThroughputTooltip: true,
|
||||
isSharded: true,
|
||||
isSharded: false,
|
||||
setThroughputValue: () => jest.fn(),
|
||||
setIsAutoscale: () => jest.fn(),
|
||||
onCostAcknowledgeChange: () => jest.fn(),
|
||||
|
||||
@@ -41,16 +41,9 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
throughputHeaderText = AutoPilotUtils.getAutoPilotHeaderText().toLocaleLowerCase();
|
||||
} else {
|
||||
const minRU: string = SharedConstants.CollectionCreation.DefaultCollectionRUs400.toLocaleString();
|
||||
|
||||
let maxRU: string;
|
||||
if (userContext.isTryCosmosDBSubscription) {
|
||||
maxRU = Constants.TryCosmosExperience.maxRU.toLocaleString();
|
||||
} else if (!isSharded) {
|
||||
maxRU = "10000";
|
||||
} else {
|
||||
maxRU = "unlimited";
|
||||
}
|
||||
|
||||
const maxRU: string = userContext.isTryCosmosDBSubscription
|
||||
? Constants.TryCosmosExperience.maxRU.toLocaleString()
|
||||
: "unlimited";
|
||||
throughputHeaderText = `throughput (${minRU} - ${maxRU} RU/s)`;
|
||||
}
|
||||
return `${isDatabase ? "Database" : getCollectionName()} ${throughputHeaderText}`;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
<ThroughputInput
|
||||
isDatabase={false}
|
||||
isSharded={true}
|
||||
isSharded={false}
|
||||
onCostAcknowledgeChange={[Function]}
|
||||
setIsAutoscale={[Function]}
|
||||
setThroughputValue={[Function]}
|
||||
|
||||
@@ -58,7 +58,7 @@ export interface TreeComponentProps {
|
||||
export class TreeComponent extends React.Component<TreeComponentProps> {
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<div style={this.props.style} className={`treeComponent ${this.props.className}`} role="tree">
|
||||
<div style={this.props.style} className={`treeComponent ${this.props.className}`}>
|
||||
<TreeNodeComponent paddingLeft={0} node={this.props.rootNode} generation={0} />
|
||||
</div>
|
||||
);
|
||||
@@ -172,7 +172,6 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
className={`${this.props.node.className || ""} main${generation} nodeItem ${showSelected ? "selected" : ""}`}
|
||||
onClick={(event: React.MouseEvent<HTMLDivElement>) => this.onNodeClick(event, node)}
|
||||
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) => this.onNodeKeyPress(event, node)}
|
||||
role="treeitem"
|
||||
>
|
||||
<div
|
||||
className={`treeNodeHeader ${this.state.isMenuShowing ? "showingMenu" : ""}`}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
exports[`TreeComponent renders a simple tree 1`] = `
|
||||
<div
|
||||
className="treeComponent tree"
|
||||
role="tree"
|
||||
>
|
||||
<TreeNodeComponent
|
||||
generation={0}
|
||||
@@ -38,7 +37,6 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
|
||||
className=" main2 nodeItem "
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="treeitem"
|
||||
>
|
||||
<div
|
||||
className="treeNodeHeader "
|
||||
@@ -139,7 +137,6 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
||||
className="nodeClassname main12 nodeItem "
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="treeitem"
|
||||
>
|
||||
<div
|
||||
className="treeNodeHeader "
|
||||
@@ -288,7 +285,6 @@ exports[`TreeNodeComponent renders loading icon 1`] = `
|
||||
className=" main2 nodeItem "
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="treeitem"
|
||||
>
|
||||
<div
|
||||
className="treeNodeHeader "
|
||||
@@ -360,7 +356,6 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
|
||||
className="nodeClassname main12 nodeItem "
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="treeitem"
|
||||
>
|
||||
<div
|
||||
className="treeNodeHeader "
|
||||
@@ -528,7 +523,6 @@ exports[`TreeNodeComponent renders unsorted children by default 1`] = `
|
||||
className=" main2 nodeItem "
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="treeitem"
|
||||
>
|
||||
<div
|
||||
className="treeNodeHeader "
|
||||
|
||||
@@ -2,22 +2,22 @@ jest.mock("../Graph/GraphExplorerComponent/GremlinClient");
|
||||
jest.mock("../../Common/dataAccess/createCollection");
|
||||
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";
|
||||
import { useDatabases } from "../useDatabases";
|
||||
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
|
||||
|
||||
describe("ContainerSampleGenerator", () => {
|
||||
let explorerStub: Explorer;
|
||||
|
||||
beforeAll(() => {
|
||||
explorerStub = {
|
||||
refreshAllDatabases: () => {},
|
||||
} as Explorer;
|
||||
});
|
||||
const createExplorerStub = (database: ViewModels.Database): Explorer => {
|
||||
const explorerStub = {} as Explorer;
|
||||
explorerStub.databases = ko.observableArray<ViewModels.Database>([database]);
|
||||
explorerStub.findDatabaseWithId = () => database;
|
||||
explorerStub.refreshAllDatabases = () => Q.resolve();
|
||||
return explorerStub;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
(createDocument as jest.Mock).mockResolvedValue(undefined);
|
||||
@@ -59,7 +59,8 @@ describe("ContainerSampleGenerator", () => {
|
||||
loadCollections: () => {},
|
||||
} as ViewModels.Database;
|
||||
database.findCollectionWithId = () => collection;
|
||||
useDatabases.getState().addDatabases([database]);
|
||||
|
||||
const explorerStub = createExplorerStub(database);
|
||||
|
||||
const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub);
|
||||
generator.setData(sampleData);
|
||||
@@ -107,8 +108,8 @@ describe("ContainerSampleGenerator", () => {
|
||||
} as ViewModels.Database;
|
||||
database.findCollectionWithId = () => collection;
|
||||
collection.databaseId = database.id();
|
||||
useDatabases.getState().addDatabases([database]);
|
||||
|
||||
const explorerStub = createExplorerStub(database);
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
@@ -125,6 +126,7 @@ describe("ContainerSampleGenerator", () => {
|
||||
|
||||
it("should not create any sample for Mongo API account", async () => {
|
||||
const experience = "Sample generation not supported for this API Mongo";
|
||||
const explorerStub = createExplorerStub(undefined);
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
@@ -139,6 +141,7 @@ describe("ContainerSampleGenerator", () => {
|
||||
|
||||
it("should not create any sample for Table API account", async () => {
|
||||
const experience = "Sample generation not supported for this API Tables";
|
||||
const explorerStub = createExplorerStub(undefined);
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
@@ -160,6 +163,7 @@ describe("ContainerSampleGenerator", () => {
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
const explorerStub = createExplorerStub(undefined);
|
||||
// Rejects with error that contains experience
|
||||
await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@ import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"
|
||||
import GraphTab from ".././Tabs/GraphTab";
|
||||
import Explorer from "../Explorer";
|
||||
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
|
||||
import { useDatabases } from "../useDatabases";
|
||||
|
||||
interface SampleDataFile extends DataModels.CreateCollectionParams {
|
||||
data: any[];
|
||||
@@ -60,7 +59,7 @@ export class ContainerSampleGenerator {
|
||||
|
||||
await createCollection(createRequest);
|
||||
await this.container.refreshAllDatabases();
|
||||
const database = useDatabases.getState().findDatabaseWithId(this.sampleDataFile.databaseId);
|
||||
const database = this.container.findDatabaseWithId(this.sampleDataFile.databaseId);
|
||||
if (!database) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import * as ko from "knockout";
|
||||
import * as sinon from "sinon";
|
||||
import { Collection, Database } from "../../Contracts/ViewModels";
|
||||
import Explorer from "../Explorer";
|
||||
import { useDatabases } from "../useDatabases";
|
||||
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
|
||||
import { DataSamplesUtil } from "./DataSamplesUtil";
|
||||
|
||||
@@ -17,8 +16,8 @@ describe("DataSampleUtils", () => {
|
||||
collections: ko.observableArray<Collection>([collection]),
|
||||
} as Database;
|
||||
const explorer = {} as Explorer;
|
||||
explorer.databases = ko.observableArray<Database>([database]);
|
||||
explorer.showOkModalDialog = () => {};
|
||||
useDatabases.getState().addDatabases([database]);
|
||||
const dataSamplesUtil = new DataSamplesUtil(explorer);
|
||||
|
||||
const fakeGenerator = sinon.createStubInstance<ContainerSampleGenerator>(ContainerSampleGenerator as any);
|
||||
|
||||
@@ -2,7 +2,6 @@ import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../Explorer";
|
||||
import { useDatabases } from "../useDatabases";
|
||||
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
|
||||
|
||||
export class DataSamplesUtil {
|
||||
@@ -18,7 +17,7 @@ export class DataSamplesUtil {
|
||||
|
||||
const databaseName = generator.getDatabaseId();
|
||||
const containerName = generator.getCollectionId();
|
||||
if (this.hasContainer(databaseName, containerName, useDatabases.getState().databases)) {
|
||||
if (this.hasContainer(databaseName, containerName, this.container.databases())) {
|
||||
const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`;
|
||||
this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
|
||||
logConsoleError(msg);
|
||||
|
||||
43
src/Explorer/Explorer.test.tsx
Normal file
43
src/Explorer/Explorer.test.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
jest.mock("./../Common/dataAccess/deleteDatabase");
|
||||
jest.mock("./../Shared/Telemetry/TelemetryProcessor");
|
||||
import * as ko from "knockout";
|
||||
import { deleteDatabase } from "./../Common/dataAccess/deleteDatabase";
|
||||
import * as ViewModels from "./../Contracts/ViewModels";
|
||||
import Explorer from "./Explorer";
|
||||
|
||||
describe("Explorer.isLastDatabase() and Explorer.isLastNonEmptyDatabase()", () => {
|
||||
let explorer: Explorer;
|
||||
beforeAll(() => {
|
||||
(deleteDatabase as jest.Mock).mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer();
|
||||
});
|
||||
|
||||
it("should be true if only 1 database", () => {
|
||||
const database = {} as ViewModels.Database;
|
||||
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
|
||||
expect(explorer.isLastDatabase()).toBe(true);
|
||||
});
|
||||
|
||||
it("should be false if only 2 databases", () => {
|
||||
const database = {} as ViewModels.Database;
|
||||
const database2 = {} as ViewModels.Database;
|
||||
explorer.databases = ko.observableArray<ViewModels.Database>([database, database2]);
|
||||
expect(explorer.isLastDatabase()).toBe(false);
|
||||
});
|
||||
|
||||
it("should be false if not last empty database", () => {
|
||||
const database = {} as ViewModels.Database;
|
||||
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
|
||||
expect(explorer.isLastNonEmptyDatabase()).toBe(false);
|
||||
});
|
||||
|
||||
it("should be true if last non empty database", () => {
|
||||
const database = {} as ViewModels.Database;
|
||||
database.collections = ko.observableArray<ViewModels.Collection>([{} as ViewModels.Collection]);
|
||||
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
|
||||
expect(explorer.isLastNonEmptyDatabase()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -6,19 +6,22 @@ import _ from "underscore";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
||||
import * as Constants from "../Common/Constants";
|
||||
import { HttpHeaders } from "../Common/Constants";
|
||||
import { ExplorerMetrics } from "../Common/Constants";
|
||||
import { readCollection } from "../Common/dataAccess/readCollection";
|
||||
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
||||
import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility";
|
||||
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { QueriesClient } from "../Common/QueriesClient";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { Splitter, SplitterBounds, SplitterDirection } from "../Common/Splitter";
|
||||
import { configContext, Platform } from "../ConfigContext";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
|
||||
import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager";
|
||||
import { RouteHandler } from "../RouteHandlers/RouteHandler";
|
||||
import { ExplorerSettings } from "../Shared/ExplorerSettings";
|
||||
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||
@@ -32,9 +35,11 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
||||
import * as ComponentRegisterer from "./ComponentRegisterer";
|
||||
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
|
||||
import { DialogProps, TextFieldProps, useDialog } from "./Controls/Dialog";
|
||||
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { ConsoleData } from "./Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import * as FileSystemUtil from "./Notebook/FileSystemUtil";
|
||||
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
||||
@@ -45,15 +50,24 @@ import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
|
||||
import { AddDatabasePanel } from "./Panes/AddDatabasePanel/AddDatabasePanel";
|
||||
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane/BrowseQueriesPane";
|
||||
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
||||
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
|
||||
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
|
||||
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
||||
import { GitHubReposPanel } from "./Panes/GitHubReposPanel/GitHubReposPanel";
|
||||
import { SaveQueryPane } from "./Panes/SaveQueryPane/SaveQueryPane";
|
||||
import { SettingsPane } from "./Panes/SettingsPane/SettingsPane";
|
||||
import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksPanel";
|
||||
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
||||
import { AddTableEntityPanel } from "./Panes/Tables/AddTableEntityPanel";
|
||||
import { EditTableEntityPanel } from "./Panes/Tables/EditTableEntityPanel";
|
||||
import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel/TableQuerySelectPanel";
|
||||
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
|
||||
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
|
||||
import TableListViewModal from "./Tables/DataTable/TableEntityListViewModel";
|
||||
import QueryViewModel from "./Tables/QueryBuilder/QueryViewModel";
|
||||
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
|
||||
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
|
||||
import QueryTablesTab from "./Tabs/QueryTablesTab";
|
||||
import { TabsManager } from "./Tabs/TabsManager";
|
||||
import TerminalTab from "./Tabs/TerminalTab";
|
||||
import Database from "./Tree/Database";
|
||||
@@ -61,30 +75,44 @@ import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
|
||||
import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter";
|
||||
import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken";
|
||||
import StoredProcedure from "./Tree/StoredProcedure";
|
||||
import { useDatabases } from "./useDatabases";
|
||||
import { useSelectedNode } from "./useSelectedNode";
|
||||
|
||||
|
||||
|
||||
BindingHandlersRegisterer.registerBindingHandlers();
|
||||
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
||||
var tmp = ComponentRegisterer;
|
||||
|
||||
export interface ExplorerParams {
|
||||
setNotificationConsoleData: (consoleData: ConsoleData) => void;
|
||||
setInProgressConsoleDataIdToBeDeleted: (id: string) => void;
|
||||
tabsManager: TabsManager;
|
||||
}
|
||||
|
||||
export default class Explorer {
|
||||
public collapsedResourceTreeWidth: number = ExplorerMetrics.CollapsedResourceTreeWidth;
|
||||
|
||||
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
|
||||
public isServerlessEnabled: ko.Computed<boolean>;
|
||||
public isAccountReady: ko.Observable<boolean>;
|
||||
public canSaveQueries: ko.Computed<boolean>;
|
||||
public queriesClient: QueriesClient;
|
||||
public tableDataClient: TableDataClient;
|
||||
public splitter: Splitter;
|
||||
|
||||
private setNotificationConsoleData: (consoleData: ConsoleData) => void;
|
||||
private setInProgressConsoleDataIdToBeDeleted: (id: string) => void;
|
||||
|
||||
// Resource Tree
|
||||
public databases: ko.ObservableArray<ViewModels.Database>;
|
||||
public selectedDatabaseId: ko.Computed<string>;
|
||||
public selectedCollectionId: ko.Computed<string>;
|
||||
public selectedNode: ko.Observable<ViewModels.TreeNode>;
|
||||
private resourceTree: ResourceTreeAdapter;
|
||||
|
||||
// Resource Token
|
||||
public resourceTokenDatabaseId: ko.Observable<string>;
|
||||
public resourceTokenCollectionId: ko.Observable<string>;
|
||||
public resourceTokenCollection: ko.Observable<ViewModels.CollectionBase>;
|
||||
public resourceTokenPartitionKey: ko.Observable<string>;
|
||||
public isResourceTokenCollectionNodeSelected: ko.Computed<boolean>;
|
||||
public resourceTreeForResourceToken: ResourceTreeAdapterForResourceToken;
|
||||
|
||||
// Tabs
|
||||
@@ -92,12 +120,16 @@ export default class Explorer {
|
||||
public tabsManager: TabsManager;
|
||||
|
||||
public gitHubOAuthService: GitHubOAuthService;
|
||||
|
||||
// features
|
||||
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
|
||||
public isSchemaEnabled: ko.Computed<boolean>;
|
||||
|
||||
// Notebooks
|
||||
public isNotebookEnabled: ko.Observable<boolean>;
|
||||
public isNotebooksEnabledForAccount: ko.Observable<boolean>;
|
||||
public notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>;
|
||||
public notebookWorkspaceManager: NotebookWorkspaceManager;
|
||||
public sparkClusterConnectionInfo: ko.Observable<DataModels.SparkClusterConnectionInfo>;
|
||||
public isSynapseLinkUpdating: ko.Observable<boolean>;
|
||||
public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
|
||||
@@ -112,9 +144,15 @@ export default class Explorer {
|
||||
content: string;
|
||||
};
|
||||
|
||||
// React adapters
|
||||
private commandBarComponentAdapter: CommandBarComponentAdapter;
|
||||
|
||||
private static readonly MaxNbDatabasesToAutoExpand = 5;
|
||||
|
||||
constructor(params?: ExplorerParams) {
|
||||
this.setNotificationConsoleData = params?.setNotificationConsoleData;
|
||||
this.setInProgressConsoleDataIdToBeDeleted = params?.setInProgressConsoleDataIdToBeDeleted;
|
||||
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
|
||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||
});
|
||||
@@ -129,11 +167,13 @@ export default class Explorer {
|
||||
userContext.authType === AuthType.ResourceToken
|
||||
? this.refreshDatabaseForResourceToken()
|
||||
: this.refreshAllDatabases(true);
|
||||
RouteHandler.getInstance().initHandler();
|
||||
this.notebookWorkspaceManager = new NotebookWorkspaceManager();
|
||||
await this._refreshNotebooksEnabledStateForAccount();
|
||||
this.isNotebookEnabled(
|
||||
userContext.authType !== AuthType.ResourceToken &&
|
||||
((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) ||
|
||||
userContext.features.enableNotebooks)
|
||||
((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) ||
|
||||
userContext.features.enableNotebooks)
|
||||
);
|
||||
|
||||
this.isShellEnabled(this.isNotebookEnabled() && isPublicInternetAccessAllowed());
|
||||
@@ -154,13 +194,56 @@ export default class Explorer {
|
||||
this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>();
|
||||
|
||||
this.queriesClient = new QueriesClient(this);
|
||||
|
||||
this.resourceTokenDatabaseId = ko.observable<string>();
|
||||
this.resourceTokenCollectionId = ko.observable<string>();
|
||||
this.resourceTokenCollection = ko.observable<ViewModels.CollectionBase>();
|
||||
this.resourceTokenPartitionKey = ko.observable<string>();
|
||||
this.isSchemaEnabled = ko.computed<boolean>(() => userContext.features.enableSchema);
|
||||
|
||||
useSelectedNode.subscribe(() => {
|
||||
this.databases = ko.observableArray<ViewModels.Database>();
|
||||
this.canSaveQueries = ko.computed<boolean>(() => {
|
||||
const savedQueriesDatabase: ViewModels.Database = _.find(
|
||||
this.databases(),
|
||||
(database: ViewModels.Database) => database.id() === Constants.SavedQueries.DatabaseName
|
||||
);
|
||||
if (!savedQueriesDatabase) {
|
||||
return false;
|
||||
}
|
||||
const savedQueriesCollection: ViewModels.Collection =
|
||||
savedQueriesDatabase &&
|
||||
_.find(
|
||||
savedQueriesDatabase.collections(),
|
||||
(collection: ViewModels.Collection) => collection.id() === Constants.SavedQueries.CollectionName
|
||||
);
|
||||
if (!savedQueriesCollection) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
this.selectedNode = ko.observable<ViewModels.TreeNode>();
|
||||
this.selectedNode.subscribe((nodeSelected: ViewModels.TreeNode) => {
|
||||
// Make sure switching tabs restores tabs display
|
||||
this.isTabsContentExpanded(false);
|
||||
});
|
||||
this.isResourceTokenCollectionNodeSelected = ko.computed<boolean>(() => {
|
||||
return (
|
||||
this.selectedNode() &&
|
||||
this.resourceTokenCollection() &&
|
||||
this.selectedNode().id() === this.resourceTokenCollection().id()
|
||||
);
|
||||
});
|
||||
|
||||
const splitterBounds: SplitterBounds = {
|
||||
min: ExplorerMetrics.SplitterMinWidth,
|
||||
max: ExplorerMetrics.SplitterMaxWidth,
|
||||
};
|
||||
this.splitter = new Splitter({
|
||||
splitterId: "h_splitter1",
|
||||
leftId: "resourcetree",
|
||||
bounds: splitterBounds,
|
||||
direction: SplitterDirection.Vertical,
|
||||
});
|
||||
|
||||
this.isFixedCollectionWithSharedThroughputSupported = ko.computed(() => {
|
||||
if (userContext.features.enableFixedCollectionWithSharedThroughput) {
|
||||
@@ -174,11 +257,45 @@ export default class Explorer {
|
||||
return isCapabilityEnabled("EnableMongo");
|
||||
});
|
||||
|
||||
this.isServerlessEnabled = ko.computed(
|
||||
() =>
|
||||
userContext.databaseAccount?.properties?.capabilities?.find(
|
||||
(item) => item.name === Constants.CapabilityNames.EnableServerless
|
||||
) !== undefined
|
||||
);
|
||||
|
||||
this.isHostedDataExplorerEnabled = ko.computed<boolean>(
|
||||
() =>
|
||||
configContext.platform === Platform.Portal &&
|
||||
!this.isRunningOnNationalCloud() &&
|
||||
userContext.apiType !== "Gremlin"
|
||||
);
|
||||
this.selectedDatabaseId = ko.computed<string>(() => {
|
||||
const selectedNode = this.selectedNode();
|
||||
if (!selectedNode) {
|
||||
return "";
|
||||
}
|
||||
|
||||
switch (selectedNode.nodeKind) {
|
||||
case "Collection":
|
||||
return (selectedNode as ViewModels.CollectionBase).databaseId || "";
|
||||
case "Database":
|
||||
return selectedNode.id() || "";
|
||||
case "DocumentId":
|
||||
case "StoredProcedure":
|
||||
case "Trigger":
|
||||
case "UserDefinedFunction":
|
||||
return selectedNode.collection.databaseId || "";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
this.tabsManager = params?.tabsManager ?? new TabsManager();
|
||||
this.tabsManager.openedTabs.subscribe((tabs) => {
|
||||
if (tabs.length === 0) {
|
||||
useSelectedNode.getState().setSelectedNode(undefined);
|
||||
useCommandBar.getState().setContextButtons([]);
|
||||
this.selectedNode(undefined);
|
||||
this.onUpdateTabsButtons([]);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -205,6 +322,8 @@ export default class Explorer {
|
||||
break;
|
||||
}
|
||||
|
||||
this.commandBarComponentAdapter = new CommandBarComponentAdapter(this);
|
||||
|
||||
this._initSettings();
|
||||
|
||||
TelemetryProcessor.traceSuccess(
|
||||
@@ -274,7 +393,6 @@ export default class Explorer {
|
||||
if (configContext.enableSchemaAnalyzer) {
|
||||
userContext.features.enableSchemaAnalyzer = true;
|
||||
}
|
||||
this.isAccountReady(true);
|
||||
}
|
||||
|
||||
public openEnableSynapseLinkDialog(): void {
|
||||
@@ -299,7 +417,7 @@ export default class Explorer {
|
||||
useDialog.getState().closeDialog();
|
||||
|
||||
try {
|
||||
await update(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, {
|
||||
await update(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.id, {
|
||||
properties: {
|
||||
enableAnalyticalStorage: true,
|
||||
},
|
||||
@@ -329,17 +447,45 @@ export default class Explorer {
|
||||
// TODO: return result
|
||||
}
|
||||
|
||||
public refreshDatabaseForResourceToken(): Promise<void> {
|
||||
const databaseId = userContext.parsedResourceToken?.databaseId;
|
||||
const collectionId = userContext.parsedResourceToken?.collectionId;
|
||||
public isDatabaseNodeOrNoneSelected(): boolean {
|
||||
return this.isNoneSelected() || this.isDatabaseNodeSelected();
|
||||
}
|
||||
|
||||
public isDatabaseNodeSelected(): boolean {
|
||||
return (this.selectedNode() && this.selectedNode().nodeKind === "Database") || false;
|
||||
}
|
||||
|
||||
public isNodeKindSelected(nodeKind: string): boolean {
|
||||
return (this.selectedNode() && this.selectedNode().nodeKind === nodeKind) || false;
|
||||
}
|
||||
|
||||
public isNoneSelected(): boolean {
|
||||
return this.selectedNode() == null;
|
||||
}
|
||||
|
||||
public logConsoleData(consoleData: ConsoleData): void {
|
||||
this.setNotificationConsoleData(consoleData);
|
||||
}
|
||||
|
||||
public deleteInProgressConsoleDataWithId(id: string): void {
|
||||
this.setInProgressConsoleDataIdToBeDeleted(id);
|
||||
}
|
||||
|
||||
public refreshDatabaseForResourceToken(): Q.Promise<any> {
|
||||
const databaseId = this.resourceTokenDatabaseId();
|
||||
const collectionId = this.resourceTokenCollectionId();
|
||||
if (!databaseId || !collectionId) {
|
||||
return Promise.reject();
|
||||
return Q.reject();
|
||||
}
|
||||
|
||||
return readCollection(databaseId, collectionId).then((collection: DataModels.Collection) => {
|
||||
const deferred: Q.Deferred<void> = Q.defer();
|
||||
readCollection(databaseId, collectionId).then((collection: DataModels.Collection) => {
|
||||
this.resourceTokenCollection(new ResourceTokenCollection(this, databaseId, collection));
|
||||
useSelectedNode.getState().setSelectedNode(this.resourceTokenCollection());
|
||||
this.selectedNode(this.resourceTokenCollection());
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public refreshAllDatabases(isInitialLoad?: boolean): Q.Promise<any> {
|
||||
@@ -364,9 +510,11 @@ export default class Explorer {
|
||||
},
|
||||
startKey
|
||||
);
|
||||
const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode();
|
||||
const deltaDatabases = this.getDeltaDatabases(databases);
|
||||
this.addDatabasesToList(deltaDatabases.toAdd);
|
||||
this.deleteDatabasesFromList(deltaDatabases.toDelete);
|
||||
this.selectedNode(currentlySelectedNode);
|
||||
this.refreshAndExpandNewDatabases(deltaDatabases.toAdd).then(
|
||||
() => {
|
||||
deferred.resolve();
|
||||
@@ -455,38 +603,37 @@ export default class Explorer {
|
||||
this._isInitializingNotebooks = true;
|
||||
|
||||
await this.ensureNotebookWorkspaceRunning();
|
||||
/*
|
||||
const connectionInfo = await listConnectionInfo(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
databaseAccount.name,
|
||||
"default"
|
||||
);
|
||||
*/
|
||||
let connectionInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
||||
authToken: undefined,
|
||||
notebookServerEndpoint: undefined,
|
||||
};
|
||||
try {
|
||||
connectionInfo = await this.notebookWorkspaceManager.getNotebookConnectionInfoAsync(
|
||||
databaseAccount.id,
|
||||
"default"
|
||||
);
|
||||
} catch (error) {
|
||||
this._isInitializingNotebooks = false;
|
||||
handleError(
|
||||
error,
|
||||
"initNotebooks/getNotebookConnectionInfoAsync",
|
||||
`Failed to get notebook workspace connection info: ${getErrorMessage(error)}`
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
// Overwrite with feature flags
|
||||
if (userContext.features.notebookServerUrl) {
|
||||
connectionInfo.notebookServerEndpoint = userContext.features.notebookServerUrl;
|
||||
}
|
||||
|
||||
const provisionData = {
|
||||
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
|
||||
resourceId: userContext.databaseAccount.id,
|
||||
dbAccountName: userContext.databaseAccount.name,
|
||||
aadToken: userContext.authorizationToken,
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
subscriptionId: userContext.subscriptionId
|
||||
if (userContext.features.notebookServerToken) {
|
||||
connectionInfo.authToken = userContext.features.notebookServerToken;
|
||||
}
|
||||
|
||||
this.notebookServerInfo(connectionInfo);
|
||||
this.notebookServerInfo.valueHasMutated();
|
||||
this.refreshNotebookList();
|
||||
}
|
||||
const response = await window.fetch("http://localhost:443/api/containerpooling/provision", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
[HttpHeaders.contentType]: "application/json",
|
||||
},
|
||||
body: JSON.stringify(provisionData)
|
||||
})
|
||||
var notebookServerInfo = await response.json();
|
||||
|
||||
this.notebookServerInfo({
|
||||
notebookServerEndpoint: userContext.features.notebookServerUrl || notebookServerInfo.notebookServerUrl,
|
||||
authToken: userContext.features.notebookServerToken || notebookServerInfo.notebookServerToken,
|
||||
});
|
||||
this.notebookServerInfo.valueHasMutated();
|
||||
this.refreshNotebookList();
|
||||
|
||||
this._isInitializingNotebooks = false;
|
||||
}
|
||||
@@ -517,20 +664,13 @@ export default class Explorer {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
try {
|
||||
const { value: workspaces } = await listByDatabaseAccount(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name
|
||||
);
|
||||
const workspaces = await this.notebookWorkspaceManager.getNotebookWorkspacesAsync(databaseAccount?.id);
|
||||
return workspaces && workspaces.length > 0 && workspaces.some((workspace) => workspace.name === "default");
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace");
|
||||
return false;
|
||||
}
|
||||
*/
|
||||
return true
|
||||
}
|
||||
|
||||
private async ensureNotebookWorkspaceRunning() {
|
||||
@@ -538,13 +678,10 @@ export default class Explorer {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
let clearMessage;
|
||||
try {
|
||||
const notebookWorkspace = await getWorkspace(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
const notebookWorkspace = await this.notebookWorkspaceManager.getNotebookWorkspaceAsync(
|
||||
userContext.databaseAccount.id,
|
||||
"default"
|
||||
);
|
||||
if (
|
||||
@@ -554,14 +691,13 @@ export default class Explorer {
|
||||
notebookWorkspace.properties.status.toLowerCase() === "stopped"
|
||||
) {
|
||||
clearMessage = NotificationConsoleUtils.logConsoleProgress("Initializing notebook workspace");
|
||||
await start(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, "default");
|
||||
await this.notebookWorkspaceManager.startNotebookWorkspaceAsync(userContext.databaseAccount.id, "default");
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, "Explorer/ensureNotebookWorkspaceRunning", "Failed to initialize notebook workspace");
|
||||
} finally {
|
||||
clearMessage && clearMessage();
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
private _resetNotebookWorkspace = async () => {
|
||||
@@ -583,6 +719,77 @@ export default class Explorer {
|
||||
}
|
||||
};
|
||||
|
||||
public findSelectedDatabase(): ViewModels.Database {
|
||||
if (!this.selectedNode()) {
|
||||
return null;
|
||||
}
|
||||
if (this.selectedNode().nodeKind === "Database") {
|
||||
return _.find(this.databases(), (database: ViewModels.Database) => database.id() === this.selectedNode().id());
|
||||
}
|
||||
return this.findSelectedCollection().database;
|
||||
}
|
||||
|
||||
public findDatabaseWithId(databaseId: string): ViewModels.Database {
|
||||
return _.find(this.databases(), (database: ViewModels.Database) => database.id() === databaseId);
|
||||
}
|
||||
|
||||
public isLastNonEmptyDatabase(): boolean {
|
||||
if (
|
||||
this.isLastDatabase() &&
|
||||
this.databases()[0] &&
|
||||
this.databases()[0].collections &&
|
||||
this.databases()[0].collections().length > 0
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public isLastDatabase(): boolean {
|
||||
if (this.databases().length > 1) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public isSelectedDatabaseShared(): boolean {
|
||||
const database = this.findSelectedDatabase();
|
||||
if (!!database) {
|
||||
return database.offer && !!database.offer();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public configure(inputs: ViewModels.DataExplorerInputsFrame): void {
|
||||
if (inputs != null) {
|
||||
// In development mode, save the iframe message from the portal in session storage.
|
||||
// This allows webpack hot reload to funciton properly
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs));
|
||||
}
|
||||
this.isAccountReady(true);
|
||||
}
|
||||
}
|
||||
|
||||
public findSelectedCollection(): ViewModels.Collection {
|
||||
return (this.selectedNode().nodeKind === "Collection"
|
||||
? this.selectedNode()
|
||||
: this.selectedNode().collection) as ViewModels.Collection;
|
||||
}
|
||||
|
||||
public isRunningOnNationalCloud(): boolean {
|
||||
return (
|
||||
userContext.portalEnv === "blackforest" ||
|
||||
userContext.portalEnv === "fairfax" ||
|
||||
userContext.portalEnv === "mooncake"
|
||||
);
|
||||
}
|
||||
|
||||
public onUpdateTabsButtons(buttons: CommandButtonComponentProps[]): void {
|
||||
this.commandBarComponentAdapter.onUpdateTabsButtons(buttons);
|
||||
}
|
||||
|
||||
private refreshAndExpandNewDatabases(newDatabases: ViewModels.Database[]): Q.Promise<void> {
|
||||
// we reload collections for all databases so the resource tree reflects any collection-level changes
|
||||
// i.e addition of stored procedures, etc.
|
||||
@@ -590,11 +797,10 @@ export default class Explorer {
|
||||
let loadCollectionPromises: Q.Promise<void>[] = [];
|
||||
|
||||
// If the user has a lot of databases, only load expanded databases.
|
||||
const databases = useDatabases.getState().databases;
|
||||
const databasesToLoad =
|
||||
databases.length <= Explorer.MaxNbDatabasesToAutoExpand
|
||||
? databases
|
||||
: databases.filter((db) => db.isDatabaseExpanded() || db.id() === Constants.SavedQueries.DatabaseName);
|
||||
this.databases().length <= Explorer.MaxNbDatabasesToAutoExpand
|
||||
? this.databases()
|
||||
: this.databases().filter((db) => db.isDatabaseExpanded());
|
||||
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.LoadCollections, {
|
||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||
@@ -639,16 +845,37 @@ export default class Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
public findCollection(databaseId: string, collectionId: string): ViewModels.Collection {
|
||||
const database: ViewModels.Database = this.databases().find(
|
||||
(database: ViewModels.Database) => database.id() === databaseId
|
||||
);
|
||||
return database?.collections().find((collection: ViewModels.Collection) => collection.id() === collectionId);
|
||||
}
|
||||
|
||||
public isLastCollection(): boolean {
|
||||
let collectionCount = 0;
|
||||
if (this.databases().length == 0) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < this.databases().length; i++) {
|
||||
const database = this.databases()[i];
|
||||
collectionCount += database.collections().length;
|
||||
if (collectionCount > 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private getDeltaDatabases(
|
||||
updatedDatabaseList: DataModels.Database[]
|
||||
): {
|
||||
toAdd: ViewModels.Database[];
|
||||
toDelete: ViewModels.Database[];
|
||||
} {
|
||||
const databases = useDatabases.getState().databases;
|
||||
const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => {
|
||||
const databaseExists = _.some(
|
||||
databases,
|
||||
this.databases(),
|
||||
(existingDatabase: ViewModels.Database) => existingDatabase.id() === database.id
|
||||
);
|
||||
return !databaseExists;
|
||||
@@ -658,7 +885,7 @@ export default class Explorer {
|
||||
);
|
||||
|
||||
let databasesToDelete: ViewModels.Database[] = [];
|
||||
ko.utils.arrayForEach(databases, (database: ViewModels.Database) => {
|
||||
ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => {
|
||||
const databasePresentInUpdatedList = _.some(
|
||||
updatedDatabaseList,
|
||||
(db: DataModels.Database) => db.id === database.id()
|
||||
@@ -672,12 +899,24 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
private addDatabasesToList(databases: ViewModels.Database[]): void {
|
||||
useDatabases.getState().addDatabases(databases);
|
||||
this.databases(
|
||||
this.databases()
|
||||
.concat(databases)
|
||||
.sort((database1, database2) => database1.id().localeCompare(database2.id()))
|
||||
);
|
||||
}
|
||||
|
||||
private deleteDatabasesFromList(databasesToRemove: ViewModels.Database[]): void {
|
||||
const deleteDatabase = useDatabases.getState().deleteDatabase;
|
||||
databasesToRemove.forEach((database) => deleteDatabase(database));
|
||||
const databasesToKeep: ViewModels.Database[] = [];
|
||||
|
||||
ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => {
|
||||
const shouldRemoveDatabase = _.some(databasesToRemove, (db: ViewModels.Database) => db.id === database.id);
|
||||
if (!shouldRemoveDatabase) {
|
||||
databasesToKeep.push(database);
|
||||
}
|
||||
});
|
||||
|
||||
this.databases(databasesToKeep);
|
||||
}
|
||||
|
||||
public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
|
||||
@@ -839,8 +1078,10 @@ export default class Explorer {
|
||||
tabPath: notebookContentItem.path,
|
||||
collection: null,
|
||||
masterKey: userContext.masterKey || "",
|
||||
hashLocation: "notebooks",
|
||||
isTabsContentExpanded: ko.observable(true),
|
||||
onLoadStartKey: null,
|
||||
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
||||
container: this,
|
||||
notebookContentItem,
|
||||
};
|
||||
@@ -1138,38 +1379,33 @@ export default class Explorer {
|
||||
|
||||
public openNotebookTerminal(kind: ViewModels.TerminalKind) {
|
||||
let title: string;
|
||||
let hashLocation: string;
|
||||
|
||||
switch (kind) {
|
||||
case ViewModels.TerminalKind.Default:
|
||||
title = "Terminal";
|
||||
hashLocation = "terminal";
|
||||
break;
|
||||
|
||||
case ViewModels.TerminalKind.Mongo:
|
||||
title = "Mongo Shell";
|
||||
hashLocation = "mongo-shell";
|
||||
break;
|
||||
|
||||
case ViewModels.TerminalKind.Cassandra:
|
||||
title = "Cassandra Shell";
|
||||
break;
|
||||
|
||||
case ViewModels.TerminalKind.PostgreSQL:
|
||||
title = "PostgreSQL Shell";
|
||||
hashLocation = "cassandra-shell";
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Terminal kind: ${kind} not supported");
|
||||
}
|
||||
|
||||
const terminalTabs: TerminalTab[] = this.tabsManager.getTabs(
|
||||
ViewModels.CollectionTabKind.Terminal,
|
||||
(tab) => tab.tabTitle() === title
|
||||
const terminalTabs: TerminalTab[] = this.tabsManager.getTabs(ViewModels.CollectionTabKind.Terminal, (tab) =>
|
||||
tab.hashLocation().startsWith(hashLocation)
|
||||
) as TerminalTab[];
|
||||
|
||||
let index = 1;
|
||||
if (terminalTabs.length > 0) {
|
||||
index = terminalTabs[terminalTabs.length - 1].index + 1;
|
||||
}
|
||||
|
||||
const index = terminalTabs.length + 1;
|
||||
const newTab = new TerminalTab({
|
||||
account: userContext.databaseAccount,
|
||||
tabKind: ViewModels.CollectionTabKind.Terminal,
|
||||
@@ -1177,11 +1413,12 @@ export default class Explorer {
|
||||
title: `${title} ${index}`,
|
||||
tabPath: `${title} ${index}`,
|
||||
collection: null,
|
||||
hashLocation: `${hashLocation} ${index}`,
|
||||
isTabsContentExpanded: ko.observable(true),
|
||||
onLoadStartKey: null,
|
||||
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
||||
container: this,
|
||||
kind: kind,
|
||||
index: index,
|
||||
});
|
||||
|
||||
this.tabsManager.activateNewTab(newTab);
|
||||
@@ -1194,10 +1431,11 @@ export default class Explorer {
|
||||
isFavorite?: boolean
|
||||
) {
|
||||
const title = "Gallery";
|
||||
const hashLocation = "gallery";
|
||||
const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default;
|
||||
const galleryTab = this.tabsManager
|
||||
.getTabs(ViewModels.CollectionTabKind.Gallery)
|
||||
.find((tab) => tab.tabTitle() == title);
|
||||
.find((tab) => tab.hashLocation() == hashLocation);
|
||||
|
||||
if (galleryTab instanceof GalleryTab) {
|
||||
this.tabsManager.activateTab(galleryTab);
|
||||
@@ -1206,8 +1444,10 @@ export default class Explorer {
|
||||
new GalleryTab(
|
||||
{
|
||||
tabKind: ViewModels.CollectionTabKind.Gallery,
|
||||
title,
|
||||
title: title,
|
||||
tabPath: title,
|
||||
hashLocation: hashLocation,
|
||||
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
||||
onLoadStartKey: null,
|
||||
isTabsContentExpanded: ko.observable(true),
|
||||
},
|
||||
@@ -1227,12 +1467,7 @@ export default class Explorer {
|
||||
|
||||
public onNewCollectionClicked(databaseId?: string): void {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Add Table",
|
||||
<CassandraAddCollectionPane explorer={this} cassandraApiClient={new CassandraAPIDataClient()} />
|
||||
);
|
||||
this.openCassandraAddCollectionPane();
|
||||
} else {
|
||||
this.openAddCollectionPanel(databaseId);
|
||||
}
|
||||
@@ -1243,7 +1478,7 @@ export default class Explorer {
|
||||
if (activeTab) {
|
||||
activeTab.onActivate(); // TODO only update tabs buttons?
|
||||
} else {
|
||||
useCommandBar.getState().setContextButtons([]);
|
||||
this.onUpdateTabsButtons([]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1278,8 +1513,50 @@ export default class Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
public async loadDatabaseOffers(): Promise<void> {
|
||||
await Promise.all(
|
||||
this.databases()?.map(async (database: ViewModels.Database) => {
|
||||
await database.loadOffer();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public isFirstResourceCreated(): boolean {
|
||||
const databases: ViewModels.Database[] = this.databases();
|
||||
|
||||
if (!databases || databases.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return databases.some((database) => {
|
||||
// user has created at least one collection
|
||||
if (database.collections()?.length > 0) {
|
||||
return true;
|
||||
}
|
||||
// user has created a database with shared throughput
|
||||
if (database.offer()) {
|
||||
return true;
|
||||
}
|
||||
// use has created an empty database without shared throughput
|
||||
return false;
|
||||
});
|
||||
}
|
||||
public openDeleteCollectionConfirmationPane(): void {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel("Delete " + getCollectionName(), <DeleteCollectionConfirmationPane explorer={this} />);
|
||||
}
|
||||
|
||||
public openDeleteDatabaseConfirmationPane(): void {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getDatabaseName(),
|
||||
<DeleteDatabaseConfirmationPanel explorer={this} selectedDatabase={this.findSelectedDatabase()} />
|
||||
);
|
||||
}
|
||||
public openUploadItemsPanePane(): void {
|
||||
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane />);
|
||||
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane explorer={this} />);
|
||||
}
|
||||
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
|
||||
useSidePanel
|
||||
@@ -1288,7 +1565,7 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
public async openAddCollectionPanel(databaseId?: string): Promise<void> {
|
||||
await useDatabases.getState().loadDatabaseOffers();
|
||||
await this.loadDatabaseOffers();
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel("New " + getCollectionName(), <AddCollectionPanel explorer={this} databaseId={databaseId} />);
|
||||
@@ -1315,6 +1592,14 @@ export default class Explorer {
|
||||
);
|
||||
}
|
||||
|
||||
public openCassandraAddCollectionPane(): void {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Add Table",
|
||||
<CassandraAddCollectionPane explorer={this} cassandraApiClient={new CassandraAPIDataClient()} />
|
||||
);
|
||||
}
|
||||
public openGitHubReposPanel(header: string, junoClient?: JunoClient): void {
|
||||
useSidePanel
|
||||
.getState()
|
||||
@@ -1328,9 +1613,43 @@ export default class Explorer {
|
||||
);
|
||||
}
|
||||
|
||||
public openAddTableEntityPanel(queryTablesTab: QueryTablesTab, tableEntityListViewModel: TableListViewModal): void {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Add Table Entity",
|
||||
<AddTableEntityPanel
|
||||
tableDataClient={this.tableDataClient}
|
||||
queryTablesTab={queryTablesTab}
|
||||
tableEntityListViewModel={tableEntityListViewModel}
|
||||
cassandraApiClient={new CassandraAPIDataClient()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
public openSetupNotebooksPanel(title: string, description: string): void {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(title, <SetupNoteBooksPanel explorer={this} panelTitle={title} panelDescription={description} />);
|
||||
}
|
||||
|
||||
public openEditTableEntityPanel(queryTablesTab: QueryTablesTab, tableEntityListViewModel: TableListViewModal): void {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Edit Table Entity",
|
||||
<EditTableEntityPanel
|
||||
tableDataClient={this.tableDataClient}
|
||||
queryTablesTab={queryTablesTab}
|
||||
tableEntityListViewModel={tableEntityListViewModel}
|
||||
cassandraApiClient={new CassandraAPIDataClient()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public openTableSelectQueryPanel(queryViewModal: QueryViewModel): void {
|
||||
useSidePanel.getState().openSidePanel("Select Column", <TableQuerySelectPanel queryViewModel={queryViewModal} />);
|
||||
}
|
||||
public openSettingPane(): void {
|
||||
useSidePanel.getState().openSidePanel("Settings", <SettingsPane />);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPag
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as StorageUtility from "../../../Shared/StorageUtility";
|
||||
import { TabComponent } from "../../Controls/Tabs/TabComponent";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/ConsoleData";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import GraphTab from "../../Tabs/GraphTab";
|
||||
import * as D3ForceGraph from "./D3ForceGraph";
|
||||
import { GraphData } from "./GraphData";
|
||||
|
||||
@@ -18,7 +18,7 @@ import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Ut
|
||||
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
||||
import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent";
|
||||
import * as TabComponent from "../../Controls/Tabs/TabComponent";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/ConsoleData";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { IGraphConfig } from "../../Tabs/GraphTab";
|
||||
import { ArraysByKeyCache } from "./ArraysByKeyCache";
|
||||
import * as D3ForceGraph from "./D3ForceGraph";
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { IGraphConfig } from "../../Tabs/GraphTab";
|
||||
import { GraphAccessor, GraphExplorer } from "./GraphExplorer";
|
||||
interface Parameter {
|
||||
onIsNewVertexDisabledChange: (isEnabled: boolean) => void;
|
||||
onGraphAccessorCreated: (instance: GraphAccessor) => void;
|
||||
onIsFilterQueryLoading: (isFilterQueryLoading: boolean) => void;
|
||||
onIsValidQuery: (isValidQuery: boolean) => void;
|
||||
onIsPropertyEditing: (isEditing: boolean) => void;
|
||||
onIsGraphDisplayed: (isDisplayed: boolean) => void;
|
||||
onResetDefaultGraphConfigValues: () => void;
|
||||
|
||||
collectionPartitionKeyProperty: string;
|
||||
graphBackendEndpoint: string;
|
||||
databaseId: string;
|
||||
collectionId: string;
|
||||
masterKey: string;
|
||||
|
||||
onLoadStartKey: number;
|
||||
onLoadStartKeyChange: (newKey: number) => void;
|
||||
resourceId: string;
|
||||
|
||||
igraphConfigUiData: ViewModels.IGraphConfigUiData;
|
||||
igraphConfig: IGraphConfig;
|
||||
setIConfigUiData?: (data: string[]) => void;
|
||||
}
|
||||
|
||||
interface IGraphExplorerProps {
|
||||
isChanged: boolean;
|
||||
}
|
||||
|
||||
interface IGraphExplorerStates {
|
||||
isChangedState: boolean;
|
||||
}
|
||||
|
||||
export interface GraphExplorerAdapter
|
||||
extends ReactAdapter,
|
||||
React.Component<IGraphExplorerProps, IGraphExplorerStates> {}
|
||||
export class GraphExplorerAdapter implements ReactAdapter {
|
||||
public params: Parameter;
|
||||
public parameters = {};
|
||||
public isNewVertexDisabled: boolean;
|
||||
|
||||
public constructor(params: Parameter, props?: IGraphExplorerProps) {
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
return (
|
||||
<GraphExplorer
|
||||
onIsNewVertexDisabledChange={this.params.onIsNewVertexDisabledChange}
|
||||
onGraphAccessorCreated={this.params.onGraphAccessorCreated}
|
||||
onIsFilterQueryLoadingChange={this.params.onIsFilterQueryLoading}
|
||||
onIsValidQueryChange={this.params.onIsValidQuery}
|
||||
onIsPropertyEditing={this.params.onIsPropertyEditing}
|
||||
onIsGraphDisplayed={this.params.onIsGraphDisplayed}
|
||||
onResetDefaultGraphConfigValues={this.params.onResetDefaultGraphConfigValues}
|
||||
collectionPartitionKeyProperty={this.params.collectionPartitionKeyProperty}
|
||||
graphBackendEndpoint={this.params.graphBackendEndpoint}
|
||||
databaseId={this.params.databaseId}
|
||||
collectionId={this.params.collectionId}
|
||||
masterKey={this.params.masterKey}
|
||||
onLoadStartKey={this.params.onLoadStartKey}
|
||||
onLoadStartKeyChange={this.params.onLoadStartKeyChange}
|
||||
resourceId={this.params.resourceId}
|
||||
igraphConfigUiData={this.params.igraphConfigUiData}
|
||||
igraphConfig={this.params.igraphConfig}
|
||||
setIConfigUiData={this.params.setIConfigUiData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import * as sinon from "sinon";
|
||||
import { GraphData, GremlinEdge, GremlinVertex } from "./GraphData";
|
||||
import { GraphExplorer } from "./GraphExplorer";
|
||||
import * as GraphUtil from "./GraphUtil";
|
||||
import { GraphData, GremlinVertex, GremlinEdge } from "./GraphData";
|
||||
import * as sinon from "sinon";
|
||||
import { GraphExplorer } from "./GraphExplorer";
|
||||
window.$ = window.jQuery = require("jquery");
|
||||
|
||||
const OUT_E_MATCHER = "g\\.V\\(.*\\).outE\\(\\).*\\.as\\('e'\\).inV\\(\\)\\.as\\('v'\\)\\.select\\('e', *'v'\\)";
|
||||
const IN_E_MATCHER = "g\\.V\\(.*\\).inE\\(\\).*\\.as\\('e'\\).outV\\(\\)\\.as\\('v'\\)\\.select\\('e', *'v'\\)";
|
||||
|
||||
|
||||
@@ -5,26 +5,21 @@
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import CancelIcon from "../../../../images/cancel.svg";
|
||||
import CheckIcon from "../../../../images/check.svg";
|
||||
import DeleteIcon from "../../../../images/delete.svg";
|
||||
import EditIcon from "../../../../images/edit.svg";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
|
||||
import { GraphHighlightedNodeData, EditedProperties, EditedEdges, PossibleVertex } from "./GraphExplorer";
|
||||
import { CollapsiblePanel } from "../../Controls/CollapsiblePanel/CollapsiblePanel";
|
||||
import { Item } from "../../Controls/InputTypeahead/InputTypeaheadComponent";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/ConsoleData";
|
||||
import * as EditorNeighbors from "./EditorNeighborsComponent";
|
||||
import { EditorNodePropertiesComponent } from "./EditorNodePropertiesComponent";
|
||||
import {
|
||||
EditedEdges,
|
||||
EditedProperties,
|
||||
GraphExplorer,
|
||||
GraphHighlightedNodeData,
|
||||
PossibleVertex,
|
||||
} from "./GraphExplorer";
|
||||
import { ReadOnlyNeighborsComponent } from "./ReadOnlyNeighborsComponent";
|
||||
import { ReadOnlyNodePropertiesComponent } from "./ReadOnlyNodePropertiesComponent";
|
||||
import { EditorNodePropertiesComponent } from "./EditorNodePropertiesComponent";
|
||||
import { ReadOnlyNeighborsComponent } from "./ReadOnlyNeighborsComponent";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { Item } from "../../Controls/InputTypeahead/InputTypeaheadComponent";
|
||||
import * as EditorNeighbors from "./EditorNeighborsComponent";
|
||||
import EditIcon from "../../../../images/edit.svg";
|
||||
import DeleteIcon from "../../../../images/delete.svg";
|
||||
import CheckIcon from "../../../../images/check.svg";
|
||||
import CancelIcon from "../../../../images/cancel.svg";
|
||||
import { GraphExplorer } from "./GraphExplorer";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
|
||||
|
||||
export enum Mode {
|
||||
READONLY_PROP,
|
||||
|
||||
@@ -3,71 +3,100 @@
|
||||
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
|
||||
* and update any knockout observables passed from the parent.
|
||||
*/
|
||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||
import { CommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||
import * as ko from "knockout";
|
||||
import * as React from "react";
|
||||
import create, { UseStore } from "zustand";
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
import { StyleConstants } from "../../../Common/Constants";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
|
||||
import * as CommandBarUtil from "./CommandBarUtil";
|
||||
|
||||
interface Props {
|
||||
container: Explorer;
|
||||
export class CommandBarComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Observable<number>;
|
||||
public container: Explorer;
|
||||
private tabsButtons: CommandButtonComponentProps[];
|
||||
private isNotebookTabActive: ko.Computed<boolean>;
|
||||
|
||||
constructor(container: Explorer) {
|
||||
this.container = container;
|
||||
this.tabsButtons = [];
|
||||
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.isDatabaseNodeOrNoneSelected,
|
||||
container.isDatabaseNodeSelected,
|
||||
container.isNoneSelected,
|
||||
container.isResourceTokenCollectionNodeSelected,
|
||||
container.isHostedDataExplorerEnabled,
|
||||
container.isSynapseLinkUpdating,
|
||||
userContext?.databaseAccount,
|
||||
this.isNotebookTabActive,
|
||||
container.isServerlessEnabled,
|
||||
];
|
||||
|
||||
ko.computed(() => ko.toJSON(toWatch)).subscribe(() => this.triggerRender());
|
||||
this.parameters = ko.observable(Date.now());
|
||||
}
|
||||
|
||||
public onUpdateTabsButtons(buttons: CommandButtonComponentProps[]): void {
|
||||
this.tabsButtons = buttons;
|
||||
this.triggerRender();
|
||||
}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
const backgroundColor = StyleConstants.BaseLight;
|
||||
|
||||
const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(this.container);
|
||||
const contextButtons = (this.tabsButtons || []).concat(
|
||||
CommandBarComponentButtonFactory.createContextCommandBarButtons(this.container)
|
||||
);
|
||||
const controlButtons = CommandBarComponentButtonFactory.createControlCommandBarButtons(this.container);
|
||||
|
||||
const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, backgroundColor);
|
||||
if (this.tabsButtons && this.tabsButtons.length > 0) {
|
||||
uiFabricStaticButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||
}
|
||||
|
||||
const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(contextButtons, backgroundColor);
|
||||
|
||||
if (uiFabricTabsButtons.length > 0) {
|
||||
uiFabricStaticButtons.push(CommandBarUtil.createDivider("commandBarDivider"));
|
||||
}
|
||||
|
||||
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
|
||||
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||
|
||||
if (this.isNotebookTabActive()) {
|
||||
uiFabricControlButtons.unshift(
|
||||
CommandBarUtil.createMemoryTracker("memoryTracker", this.container.memoryUsageInfo)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="commandBarContainer">
|
||||
<CommandBar
|
||||
ariaLabel="Use left and right arrow keys to navigate between commands"
|
||||
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
|
||||
farItems={uiFabricControlButtons}
|
||||
styles={{
|
||||
root: { backgroundColor: backgroundColor },
|
||||
}}
|
||||
overflowButtonProps={{ ariaLabel: "More commands" }}
|
||||
/>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
private triggerRender() {
|
||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||
}
|
||||
}
|
||||
|
||||
export interface CommandBarStore {
|
||||
contextButtons: CommandButtonComponentProps[];
|
||||
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => void;
|
||||
}
|
||||
|
||||
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
|
||||
contextButtons: [],
|
||||
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
|
||||
}));
|
||||
|
||||
export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||
const selectedNodeState = useSelectedNode();
|
||||
const buttons = useCommandBar((state) => state.contextButtons);
|
||||
const backgroundColor = StyleConstants.BaseLight;
|
||||
|
||||
const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(container, selectedNodeState);
|
||||
const contextButtons = (buttons || []).concat(
|
||||
CommandBarComponentButtonFactory.createContextCommandBarButtons(container, selectedNodeState)
|
||||
);
|
||||
const controlButtons = CommandBarComponentButtonFactory.createControlCommandBarButtons(container);
|
||||
|
||||
const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, backgroundColor);
|
||||
if (buttons && buttons.length > 0) {
|
||||
uiFabricStaticButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||
}
|
||||
|
||||
const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(contextButtons, backgroundColor);
|
||||
|
||||
if (uiFabricTabsButtons.length > 0) {
|
||||
uiFabricStaticButtons.push(CommandBarUtil.createDivider("commandBarDivider"));
|
||||
}
|
||||
|
||||
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
|
||||
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||
|
||||
if (container.tabsManager.activeTab()?.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
|
||||
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker", container.memoryUsageInfo));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="commandBarContainer">
|
||||
<FluentCommandBar
|
||||
ariaLabel="Use left and right arrow keys to navigate between commands"
|
||||
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
|
||||
farItems={uiFabricControlButtons}
|
||||
styles={{
|
||||
root: { backgroundColor: backgroundColor },
|
||||
}}
|
||||
overflowButtonProps={{ ariaLabel: "More commands" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
import * as ko from "knockout";
|
||||
import { AuthType } from "../../../AuthType";
|
||||
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
||||
import { CollectionBase } from "../../../Contracts/ViewModels";
|
||||
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
|
||||
import { updateUserContext } from "../../../UserContext";
|
||||
import Explorer from "../../Explorer";
|
||||
import NotebookManager from "../../Notebook/NotebookManager";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
|
||||
|
||||
describe("CommandBarComponentButtonFactory tests", () => {
|
||||
let mockExplorer: Explorer;
|
||||
|
||||
afterEach(() => useSelectedNode.getState().setSelectedNode(undefined));
|
||||
|
||||
describe("Enable Azure Synapse Link Button", () => {
|
||||
const enableAzureSynapseLinkBtnLabel = "Enable Azure Synapse Link";
|
||||
const selectedNodeState = useSelectedNode.getState();
|
||||
|
||||
beforeAll(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
@@ -28,12 +23,17 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
mockExplorer.isNotebookEnabled = ko.observable(false);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
|
||||
mockExplorer.isRunningOnNationalCloud = () => false;
|
||||
});
|
||||
|
||||
it("Account is not serverless - button should be visible", () => {
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const enableAzureSynapseLinkBtn = buttons.find(
|
||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
|
||||
);
|
||||
@@ -41,14 +41,9 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
|
||||
it("Account is serverless - button should be hidden", () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableServerless" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const enableAzureSynapseLinkBtn = buttons.find(
|
||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
|
||||
);
|
||||
@@ -58,12 +53,10 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
|
||||
describe("Enable notebook button", () => {
|
||||
const enableNotebookBtnLabel = "Enable Notebooks (Preview)";
|
||||
const selectedNodeState = useSelectedNode.getState();
|
||||
|
||||
beforeAll(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
updateUserContext({
|
||||
portalEnv: "prod",
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableTable" }],
|
||||
@@ -71,19 +64,18 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
});
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
|
||||
afterEach(() => {
|
||||
updateUserContext({
|
||||
portalEnv: "prod",
|
||||
});
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
|
||||
});
|
||||
|
||||
it("Notebooks is already enabled - button should be hidden", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
expect(enableNotebookBtn).toBeUndefined();
|
||||
});
|
||||
@@ -91,11 +83,9 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
it("Account is running on one of the national clouds - button should be hidden", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(false);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
|
||||
updateUserContext({
|
||||
portalEnv: "mooncake",
|
||||
});
|
||||
mockExplorer.isRunningOnNationalCloud = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
expect(enableNotebookBtn).toBeUndefined();
|
||||
});
|
||||
@@ -103,8 +93,9 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
it("Notebooks is not enabled but is available - button should be shown and enabled", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(false);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
expect(enableNotebookBtn).toBeDefined();
|
||||
expect(enableNotebookBtn.disabled).toBe(false);
|
||||
@@ -114,8 +105,9 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(false);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
|
||||
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
expect(enableNotebookBtn).toBeDefined();
|
||||
expect(enableNotebookBtn.disabled).toBe(true);
|
||||
@@ -127,7 +119,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
|
||||
describe("Open Mongo Shell button", () => {
|
||||
const openMongoShellBtnLabel = "Open Mongo Shell";
|
||||
const selectedNodeState = useSelectedNode.getState();
|
||||
|
||||
beforeAll(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
@@ -139,6 +130,9 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isShellEnabled = ko.observable(true);
|
||||
});
|
||||
|
||||
@@ -154,7 +148,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
mockExplorer.isNotebookEnabled = ko.observable(false);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
|
||||
|
||||
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
|
||||
mockExplorer.isShellEnabled = ko.observable(true);
|
||||
});
|
||||
|
||||
@@ -162,23 +156,21 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
updateUserContext({
|
||||
apiType: "SQL",
|
||||
});
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Running on a national cloud - button should be hidden", () => {
|
||||
updateUserContext({
|
||||
portalEnv: "mooncake",
|
||||
});
|
||||
mockExplorer.isRunningOnNationalCloud = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled and is unavailable - button should be hidden", () => {
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeUndefined();
|
||||
});
|
||||
@@ -186,7 +178,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
it("Notebooks is not enabled and is available - button should be hidden", () => {
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeUndefined();
|
||||
});
|
||||
@@ -194,7 +186,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeDefined();
|
||||
expect(openMongoShellBtn.disabled).toBe(false);
|
||||
@@ -205,7 +197,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeDefined();
|
||||
expect(openMongoShellBtn.disabled).toBe(false);
|
||||
@@ -217,7 +209,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
mockExplorer.isShellEnabled = ko.observable(false);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeUndefined();
|
||||
});
|
||||
@@ -225,7 +217,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
|
||||
describe("Open Cassandra Shell button", () => {
|
||||
const openCassandraShellBtnLabel = "Open Cassandra Shell";
|
||||
const selectedNodeState = useSelectedNode.getState();
|
||||
|
||||
beforeAll(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
@@ -237,6 +228,9 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -249,6 +243,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
mockExplorer.isNotebookEnabled = ko.observable(false);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
|
||||
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
|
||||
});
|
||||
|
||||
it("Cassandra Api not available - button should be hidden", () => {
|
||||
@@ -260,23 +255,21 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
console.log(mockExplorer);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Running on a national cloud - button should be hidden", () => {
|
||||
updateUserContext({
|
||||
portalEnv: "mooncake",
|
||||
});
|
||||
mockExplorer.isRunningOnNationalCloud = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeUndefined();
|
||||
});
|
||||
@@ -284,7 +277,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
it("Notebooks is not enabled and is available - button should be shown and enabled", () => {
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeUndefined();
|
||||
});
|
||||
@@ -292,7 +285,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeDefined();
|
||||
expect(openCassandraShellBtn.disabled).toBe(false);
|
||||
@@ -303,7 +296,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeDefined();
|
||||
expect(openCassandraShellBtn.disabled).toBe(false);
|
||||
@@ -314,7 +307,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
describe("GitHub buttons", () => {
|
||||
const connectToGitHubBtnLabel = "Connect to GitHub";
|
||||
const manageGitHubSettingsBtnLabel = "Manage GitHub settings";
|
||||
const selectedNodeState = useSelectedNode.getState();
|
||||
|
||||
beforeAll(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
@@ -327,10 +319,12 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
|
||||
|
||||
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
|
||||
mockExplorer.notebookManager = new NotebookManager();
|
||||
mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined);
|
||||
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -344,7 +338,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
it("Notebooks is enabled and GitHubOAuthService is not logged in - connect to github button should be visible", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
|
||||
expect(connectToGitHubBtn).toBeDefined();
|
||||
});
|
||||
@@ -353,7 +347,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
mockExplorer.notebookManager.gitHubOAuthService.isLoggedIn = jest.fn().mockReturnValue(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const manageGitHubSettingsBtn = buttons.find(
|
||||
(button) => button.commandButtonLabel === manageGitHubSettingsBtnLabel
|
||||
);
|
||||
@@ -361,7 +355,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled - connect to github and manage github settings buttons should be hidden", () => {
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
|
||||
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
|
||||
expect(connectToGitHubBtn).toBeUndefined();
|
||||
@@ -374,13 +368,11 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
|
||||
describe("Resource token", () => {
|
||||
const mockCollection = { id: ko.observable("test") } as CollectionBase;
|
||||
useSelectedNode.getState().setSelectedNode(mockCollection);
|
||||
const selectedNodeState = useSelectedNode.getState();
|
||||
beforeAll(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
mockExplorer.resourceTokenCollection = ko.observable(mockCollection);
|
||||
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true);
|
||||
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
|
||||
updateUserContext({
|
||||
authType: AuthType.ResourceToken,
|
||||
});
|
||||
@@ -392,7 +384,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
kind: "DocumentDB",
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
expect(buttons.length).toBe(2);
|
||||
expect(buttons[0].commandButtonLabel).toBe("New SQL Query");
|
||||
expect(buttons[0].disabled).toBe(false);
|
||||
|
||||
@@ -24,23 +24,16 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
|
||||
import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
|
||||
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
import { OpenFullScreen } from "../../OpenFullScreen";
|
||||
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
|
||||
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
|
||||
import { SelectedNodeState } from "../../useSelectedNode";
|
||||
|
||||
let counter = 0;
|
||||
|
||||
export function createStaticCommandBarButtons(
|
||||
container: Explorer,
|
||||
selectedNodeState: SelectedNodeState
|
||||
): CommandButtonComponentProps[] {
|
||||
export function createStaticCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||
if (userContext.authType === AuthType.ResourceToken) {
|
||||
return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState);
|
||||
return createStaticCommandBarButtonsForResourceToken(container);
|
||||
}
|
||||
|
||||
const newCollectionBtn = createNewCollectionGroup(container);
|
||||
@@ -74,36 +67,35 @@ export function createStaticCommandBarButtons(
|
||||
buttons.push(createOpenTerminalButton(container));
|
||||
|
||||
buttons.push(createNotebookWorkspaceResetButton(container));
|
||||
|
||||
buttons.push(createDivider());
|
||||
buttons.push(createOpenPostgreSQLTerminalButton(container));
|
||||
|
||||
if (userContext.apiType === "Mongo" &&
|
||||
container.isShellEnabled() &&
|
||||
selectedNodeState.isDatabaseNodeOrNoneSelected()){
|
||||
if (
|
||||
(userContext.apiType === "Mongo" && container.isShellEnabled() && container.isDatabaseNodeOrNoneSelected()) ||
|
||||
userContext.apiType === "Cassandra"
|
||||
) {
|
||||
buttons.push(createDivider());
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
buttons.push(createOpenCassandraTerminalButton(container));
|
||||
} else {
|
||||
buttons.push(createOpenMongoTerminalButton(container));
|
||||
}
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
buttons.push(createOpenCassandraTerminalButton(container));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!isRunningOnNationalCloud()) {
|
||||
if (!container.isRunningOnNationalCloud()) {
|
||||
buttons.push(createEnableNotebooksButton(container));
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
|
||||
if (!container.isDatabaseNodeOrNoneSelected()) {
|
||||
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
||||
|
||||
if (isQuerySupported) {
|
||||
buttons.push(createDivider());
|
||||
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
|
||||
const newSqlQueryBtn = createNewSQLQueryButton(container);
|
||||
buttons.push(newSqlQueryBtn);
|
||||
}
|
||||
|
||||
if (isQuerySupported && selectedNodeState.findSelectedCollection()) {
|
||||
if (isQuerySupported && container.selectedNode() && container.findSelectedCollection()) {
|
||||
const openQueryBtn = createOpenQueryButton(container);
|
||||
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
|
||||
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)];
|
||||
buttons.push(openQueryBtn);
|
||||
}
|
||||
|
||||
@@ -113,16 +105,16 @@ export function createStaticCommandBarButtons(
|
||||
iconSrc: AddStoredProcedureIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
|
||||
disabled: container.isDatabaseNodeOrNoneSelected(),
|
||||
};
|
||||
|
||||
newStoredProcedureBtn.children = createScriptCommandButtons(selectedNodeState);
|
||||
newStoredProcedureBtn.children = createScriptCommandButtons(container);
|
||||
buttons.push(newStoredProcedureBtn);
|
||||
}
|
||||
}
|
||||
@@ -130,19 +122,16 @@ export function createStaticCommandBarButtons(
|
||||
return buttons;
|
||||
}
|
||||
|
||||
export function createContextCommandBarButtons(
|
||||
container: Explorer,
|
||||
selectedNodeState: SelectedNodeState
|
||||
): CommandButtonComponentProps[] {
|
||||
export function createContextCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
|
||||
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
|
||||
if (!container.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
|
||||
const label = container.isShellEnabled() ? "Open Mongo Shell" : "New Shell";
|
||||
const newMongoShellBtn: CommandButtonComponentProps = {
|
||||
iconSrc: HostedTerminalIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
if (container.isShellEnabled()) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
} else {
|
||||
@@ -152,7 +141,7 @@ export function createContextCommandBarButtons(
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo",
|
||||
disabled: container.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo",
|
||||
};
|
||||
buttons.push(newMongoShellBtn);
|
||||
}
|
||||
@@ -165,7 +154,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
|
||||
{
|
||||
iconSrc: SettingsIcon,
|
||||
iconAlt: "Settings",
|
||||
onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", <SettingsPane />),
|
||||
onCommandClick: container.openSettingPane,
|
||||
commandButtonLabel: undefined,
|
||||
ariaLabel: "Settings",
|
||||
tooltipText: "Settings",
|
||||
@@ -174,10 +163,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
|
||||
},
|
||||
];
|
||||
|
||||
const showOpenFullScreen =
|
||||
configContext.platform === Platform.Portal && !isRunningOnNationalCloud() && userContext.apiType !== "Gremlin";
|
||||
|
||||
if (showOpenFullScreen) {
|
||||
if (container.isHostedDataExplorerEnabled()) {
|
||||
const label = "Open Full Screen";
|
||||
const fullScreenButton: CommandButtonComponentProps = {
|
||||
iconSrc: OpenInTabIcon,
|
||||
@@ -189,7 +175,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
|
||||
ariaLabel: label,
|
||||
tooltipText: label,
|
||||
hasPopup: false,
|
||||
disabled: !showOpenFullScreen,
|
||||
disabled: !container.isHostedDataExplorerEnabled(),
|
||||
className: "OpenFullScreen",
|
||||
};
|
||||
buttons.push(fullScreenButton);
|
||||
@@ -248,7 +234,7 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isServerlessAccount()) {
|
||||
if (container.isServerlessEnabled()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -287,20 +273,20 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
|
||||
};
|
||||
}
|
||||
|
||||
function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandButtonComponentProps {
|
||||
function createNewSQLQueryButton(container: Explorer): CommandButtonComponentProps {
|
||||
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
|
||||
const label = "New SQL Query";
|
||||
return {
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
|
||||
disabled: container.isDatabaseNodeOrNoneSelected(),
|
||||
};
|
||||
} else if (userContext.apiType === "Mongo") {
|
||||
const label = "New Query";
|
||||
@@ -308,24 +294,23 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
|
||||
disabled: container.isDatabaseNodeOrNoneSelected(),
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function createScriptCommandButtons(selectedNodeState: SelectedNodeState): CommandButtonComponentProps[] {
|
||||
export function createScriptCommandButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
|
||||
const shouldEnableScriptsCommands: boolean =
|
||||
!selectedNodeState.isDatabaseNodeOrNoneSelected() && areScriptsSupported();
|
||||
const shouldEnableScriptsCommands: boolean = !container.isDatabaseNodeOrNoneSelected() && areScriptsSupported();
|
||||
|
||||
if (shouldEnableScriptsCommands) {
|
||||
const label = "New Stored Procedure";
|
||||
@@ -333,13 +318,13 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
||||
iconSrc: AddStoredProcedureIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
|
||||
disabled: container.isDatabaseNodeOrNoneSelected(),
|
||||
};
|
||||
buttons.push(newStoredProcedureBtn);
|
||||
}
|
||||
@@ -350,13 +335,13 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
||||
iconSrc: AddUdfIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
|
||||
disabled: container.isDatabaseNodeOrNoneSelected(),
|
||||
};
|
||||
buttons.push(newUserDefinedFunctionBtn);
|
||||
}
|
||||
@@ -367,13 +352,13 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
||||
iconSrc: AddTriggerIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
|
||||
disabled: container.isDatabaseNodeOrNoneSelected(),
|
||||
};
|
||||
buttons.push(newTriggerBtn);
|
||||
}
|
||||
@@ -420,12 +405,12 @@ function createOpenQueryButton(container: Explorer): CommandButtonComponentProps
|
||||
};
|
||||
}
|
||||
|
||||
function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
|
||||
function createOpenQueryFromDiskButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Open Query From Disk";
|
||||
return {
|
||||
iconSrc: OpenQueryFromDiskIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane />),
|
||||
onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane explorer={container} />),
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
@@ -467,19 +452,6 @@ function createOpenTerminalButton(container: Explorer): CommandButtonComponentPr
|
||||
};
|
||||
}
|
||||
|
||||
function createOpenPostgreSQLTerminalButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Open PostgreSQL Shell";
|
||||
return {
|
||||
iconSrc: HostedTerminalIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.PostgreSQL),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: label,
|
||||
};
|
||||
}
|
||||
|
||||
function createOpenMongoTerminalButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Open Mongo Shell";
|
||||
const tooltip =
|
||||
@@ -559,25 +531,19 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
|
||||
};
|
||||
}
|
||||
|
||||
function createStaticCommandBarButtonsForResourceToken(
|
||||
container: Explorer,
|
||||
selectedNodeState: SelectedNodeState
|
||||
): CommandButtonComponentProps[] {
|
||||
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
|
||||
function createStaticCommandBarButtonsForResourceToken(container: Explorer): CommandButtonComponentProps[] {
|
||||
const newSqlQueryBtn = createNewSQLQueryButton(container);
|
||||
const openQueryBtn = createOpenQueryButton(container);
|
||||
|
||||
const isResourceTokenCollectionNodeSelected: boolean =
|
||||
container.resourceTokenCollection() &&
|
||||
container.resourceTokenCollection().id() === selectedNodeState.selectedNode?.id();
|
||||
newSqlQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
|
||||
newSqlQueryBtn.disabled = !container.isResourceTokenCollectionNodeSelected();
|
||||
newSqlQueryBtn.onCommandClick = () => {
|
||||
const resourceTokenCollection: ViewModels.CollectionBase = container.resourceTokenCollection();
|
||||
resourceTokenCollection && resourceTokenCollection.onNewQueryClick(resourceTokenCollection, undefined);
|
||||
};
|
||||
|
||||
openQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
|
||||
openQueryBtn.disabled = !container.isResourceTokenCollectionNodeSelected();
|
||||
if (!openQueryBtn.disabled) {
|
||||
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
|
||||
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)];
|
||||
}
|
||||
|
||||
return [newSqlQueryBtn, openQueryBtn];
|
||||
|
||||
33
src/Explorer/Menus/NavBar/ControlBarComponent.tsx
Normal file
33
src/Explorer/Menus/NavBar/ControlBarComponent.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* React component for control bar
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import {
|
||||
CommandButtonComponent,
|
||||
CommandButtonComponentProps,
|
||||
} from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
|
||||
export interface ControlBarComponentProps {
|
||||
buttons: CommandButtonComponentProps[];
|
||||
}
|
||||
|
||||
export class ControlBarComponent extends React.Component<ControlBarComponentProps> {
|
||||
private static renderButtons(commandButtonOptions: CommandButtonComponentProps[]): JSX.Element[] {
|
||||
return commandButtonOptions.map(
|
||||
(btn: CommandButtonComponentProps, index: number): JSX.Element => {
|
||||
// Remove label
|
||||
btn.commandButtonLabel = undefined;
|
||||
return CommandButtonComponent.renderButton(btn, `${index}`);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
if (!this.props.buttons || this.props.buttons.length < 1) {
|
||||
return <React.Fragment />;
|
||||
}
|
||||
|
||||
return <React.Fragment>{ControlBarComponent.renderButtons(this.props.buttons)}</React.Fragment>;
|
||||
}
|
||||
}
|
||||
28
src/Explorer/Menus/NavBar/ControlBarComponentAdapter.tsx
Normal file
28
src/Explorer/Menus/NavBar/ControlBarComponentAdapter.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* This adapter is responsible to render the React component
|
||||
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
|
||||
* and update any knockout observables passed from the parent.
|
||||
*/
|
||||
|
||||
import * as ko from "knockout";
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
import { ControlBarComponent } from "./ControlBarComponent";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
|
||||
export class ControlBarComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Observable<number>;
|
||||
|
||||
constructor(private buttons: ko.ObservableArray<CommandButtonComponentProps>) {
|
||||
this.buttons.subscribe(() => this.forceRender());
|
||||
this.parameters = ko.observable<number>(Date.now());
|
||||
}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
return <ControlBarComponent buttons={this.buttons()} />;
|
||||
}
|
||||
|
||||
public forceRender(): void {
|
||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Interface for the data/content that will be recorded
|
||||
*/
|
||||
|
||||
export interface ConsoleData {
|
||||
type: ConsoleDataType;
|
||||
date: string;
|
||||
message: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export enum ConsoleDataType {
|
||||
Info = 0,
|
||||
Error = 1,
|
||||
InProgress = 2,
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { ConsoleDataType } from "./ConsoleData";
|
||||
import { NotificationConsoleComponent, NotificationConsoleComponentProps } from "./NotificationConsoleComponent";
|
||||
import {
|
||||
ConsoleDataType,
|
||||
NotificationConsoleComponent,
|
||||
NotificationConsoleComponentProps,
|
||||
} from "./NotificationConsoleComponent";
|
||||
|
||||
describe("NotificationConsoleComponent", () => {
|
||||
const createBlankProps = (): NotificationConsoleComponentProps => {
|
||||
|
||||
@@ -17,7 +17,25 @@ import ChevronUpIcon from "../../../../images/QueryBuilder/CollapseChevronUp_16x
|
||||
import { ClientDefaults, KeyCodes } from "../../../Common/Constants";
|
||||
import { useNotificationConsole } from "../../../hooks/useNotificationConsole";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { ConsoleData, ConsoleDataType } from "./ConsoleData";
|
||||
|
||||
/**
|
||||
* Log levels
|
||||
*/
|
||||
export enum ConsoleDataType {
|
||||
Info = 0,
|
||||
Error = 1,
|
||||
InProgress = 2,
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for the data/content that will be recorded
|
||||
*/
|
||||
export interface ConsoleData {
|
||||
type: ConsoleDataType;
|
||||
date: string;
|
||||
message: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface NotificationConsoleComponentProps {
|
||||
isConsoleExpanded: boolean;
|
||||
@@ -305,13 +323,14 @@ const PrPreview = (props: { pr: string }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const NotificationConsole: React.FC = () => {
|
||||
export const NotificationConsole: React.FC<
|
||||
Pick<NotificationConsoleComponentProps, "consoleData" | "inProgressConsoleDataIdToBeDeleted">
|
||||
> = ({
|
||||
consoleData,
|
||||
inProgressConsoleDataIdToBeDeleted,
|
||||
}: Pick<NotificationConsoleComponentProps, "consoleData" | "inProgressConsoleDataIdToBeDeleted">) => {
|
||||
const setIsExpanded = useNotificationConsole((state) => state.setIsExpanded);
|
||||
const isExpanded = useNotificationConsole((state) => state.isExpanded);
|
||||
const consoleData = useNotificationConsole((state) => state.consoleData);
|
||||
const inProgressConsoleDataIdToBeDeleted = useNotificationConsole(
|
||||
(state) => state.inProgressConsoleDataIdToBeDeleted
|
||||
);
|
||||
// TODO Refactor NotificationConsoleComponent into a functional component and remove this wrapper
|
||||
// This component only exists so we can use hooks and pass them down to a non-functional component
|
||||
return (
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
makeStateRecord,
|
||||
makeTransformsRecord,
|
||||
} from "@nteract/core";
|
||||
import { configOption, defineConfigOption } from "@nteract/mythic-configuration";
|
||||
import { configOption, createConfigCollection, defineConfigOption } from "@nteract/mythic-configuration";
|
||||
import { Media } from "@nteract/outputs";
|
||||
import TransformVDOM from "@nteract/transform-vdom";
|
||||
import * as Immutable from "immutable";
|
||||
@@ -242,27 +242,22 @@ export class NotebookClientV2 {
|
||||
);
|
||||
|
||||
// Additional configuration
|
||||
this.store.dispatch(configOption("editorType").action(params.cellEditorType ?? "codemirror"));
|
||||
this.store.dispatch(configOption("editorType").action(params.cellEditorType ?? "monaco"));
|
||||
this.store.dispatch(
|
||||
configOption("autoSaveInterval").action(params.autoSaveInterval ?? Constants.Notebook.autoSaveIntervalMs)
|
||||
);
|
||||
this.store.dispatch(configOption("codeMirror.lineNumbers").action(true));
|
||||
|
||||
const readOnlyConfigOption = configOption("codeMirror.readOnly");
|
||||
const readOnlyValue = params.isReadOnly ? "nocursor" : undefined;
|
||||
if (!readOnlyConfigOption) {
|
||||
defineConfigOption({
|
||||
label: "Read-only",
|
||||
key: "codeMirror.readOnly",
|
||||
values: [
|
||||
{ label: "Read-Only", value: "nocursor" },
|
||||
{ label: "Not read-only", value: undefined },
|
||||
],
|
||||
defaultValue: readOnlyValue,
|
||||
});
|
||||
} else {
|
||||
this.store.dispatch(readOnlyConfigOption.action(readOnlyValue));
|
||||
}
|
||||
createConfigCollection({
|
||||
key: "monaco",
|
||||
});
|
||||
defineConfigOption({
|
||||
label: "Show Line numbers",
|
||||
key: "monaco.lineNumbers",
|
||||
values: [
|
||||
{ label: "Yes", value: true },
|
||||
{ label: "No", value: false },
|
||||
],
|
||||
defaultValue: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
.notebookComponentContainer {
|
||||
text-transform: none;
|
||||
line-height: 1.28581;
|
||||
letter-spacing: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #182026;
|
||||
text-transform:none;
|
||||
line-height:1.28581;
|
||||
letter-spacing:0;
|
||||
font-size:14px;
|
||||
font-weight:400;
|
||||
color:#182026;
|
||||
height: 100%;
|
||||
|
||||
.hotKeys {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
KernelRef,
|
||||
RemoteKernelProps,
|
||||
selectors,
|
||||
ServerConfig as JupyterServerConfig
|
||||
ServerConfig as JupyterServerConfig,
|
||||
} from "@nteract/core";
|
||||
import { Channels, childOf, createMessage, JupyterMessage, message, ofMessageType } from "@nteract/messaging";
|
||||
import { RecordOf } from "immutable";
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
timeout
|
||||
timeout,
|
||||
} from "rxjs/operators";
|
||||
import { webSocket } from "rxjs/webSocket";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
@@ -107,7 +107,7 @@ const formWebSocketURL = (serverConfig: NotebookServiceConfig, kernelId: string,
|
||||
const q = params.toString();
|
||||
const suffix = q !== "" ? `?${q}` : "";
|
||||
|
||||
const url = (serverConfig.endpoint.slice(0, -1) || "") + `api/kernels/${kernelId}/channels${suffix}`;
|
||||
const url = (serverConfig.endpoint || "") + `api/kernels/${kernelId}/channels${suffix}`;
|
||||
|
||||
return url.replace(/^http(s)?/, "ws$1");
|
||||
};
|
||||
|
||||
@@ -53,7 +53,7 @@ export class NotebookContainerClient {
|
||||
|
||||
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
|
||||
try {
|
||||
const response = await fetch(`${notebookServerEndpoint}api/metrics/memory`, {
|
||||
const response = await fetch(`${notebookServerEndpoint}/api/metrics/memory`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: authToken,
|
||||
@@ -130,23 +130,19 @@ export class NotebookContainerClient {
|
||||
}
|
||||
|
||||
private async recreateNotebookWorkspaceAsync(): Promise<void> {
|
||||
const explorer = window.dataExplorer;
|
||||
const { databaseAccount } = userContext;
|
||||
if (!databaseAccount?.id) {
|
||||
throw new Error("DataExplorer not initialized");
|
||||
}
|
||||
/*
|
||||
|
||||
const notebookWorkspaceManager = explorer.notebookWorkspaceManager;
|
||||
try {
|
||||
await destroy(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, "default");
|
||||
await createOrUpdate(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
"default"
|
||||
);
|
||||
await notebookWorkspaceManager.deleteNotebookWorkspaceAsync(databaseAccount?.id, "default");
|
||||
await notebookWorkspaceManager.createNotebookWorkspaceAsync(databaseAccount?.id, "default");
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "NotebookContainerClient/recreateNotebookWorkspaceAsync");
|
||||
return Promise.reject(error);
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export class NotebookContentClient {
|
||||
private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>,
|
||||
public notebookBasePath: ko.Observable<string>,
|
||||
private contentProvider: IContentProvider
|
||||
) { }
|
||||
) {}
|
||||
|
||||
/**
|
||||
* This updates the item and points all the children's parent to this item
|
||||
@@ -25,9 +25,6 @@ export class NotebookContentClient {
|
||||
});
|
||||
}
|
||||
|
||||
private sleep = (milliseconds: number) => {
|
||||
return new Promise(resolve => setTimeout(resolve, milliseconds))
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param parent parent folder
|
||||
@@ -38,7 +35,6 @@ export class NotebookContentClient {
|
||||
}
|
||||
|
||||
const type = "notebook";
|
||||
|
||||
return this.contentProvider
|
||||
.create<"notebook">(this.getServerConfig(), parent.path, { type })
|
||||
.toPromise()
|
||||
|
||||
@@ -1,68 +1,56 @@
|
||||
.NotebookReadOnlyRender {
|
||||
.nteract-cell-container {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.nteract-cell-container {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.nteract-cell {
|
||||
padding: 0.5px;
|
||||
border: 1px solid #ffffff;
|
||||
border-left: 3px solid #ffffff;
|
||||
}
|
||||
.nteract-cell {
|
||||
padding: 0.5px;
|
||||
border: 1px solid #ffffff;
|
||||
border-left: 3px solid #ffffff;
|
||||
}
|
||||
|
||||
.CodeMirror-scroll {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
.CodeMirror-scroll {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.CodeMirror-lines {
|
||||
cursor: default;
|
||||
}
|
||||
.CodeMirror-lines {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
height: inherit;
|
||||
}
|
||||
.nteract-cell:hover {
|
||||
border: 1px solid #0078d4;
|
||||
border-left: 3px solid #0078d4;
|
||||
|
||||
.CodeMirror-scroll,
|
||||
.CodeMirror-linenumber,
|
||||
.CodeMirror-gutters {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.CodeMirror-scroll {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.nteract-cell:hover {
|
||||
border: 1px solid #0078d4;
|
||||
border-left: 3px solid #0078d4;
|
||||
.nteract-cell-outputs {
|
||||
border-top: 1px solid #d7d7d7;
|
||||
}
|
||||
|
||||
.CodeMirror-scroll,
|
||||
.CodeMirror-linenumber,
|
||||
.CodeMirror-gutters {
|
||||
background-color: #ffffff;
|
||||
.nteract-md-cell {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.nteract-cell-outputs {
|
||||
border-top: 1px solid #d7d7d7;
|
||||
padding: 10px;
|
||||
border-top: 1px solid #ffffff;
|
||||
|
||||
pre {
|
||||
background-color: #ffffff;
|
||||
border: none;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.nteract-md-cell {
|
||||
background-color: #ffffff;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.nteract-cell-outputs {
|
||||
padding: 10px;
|
||||
border-top: 1px solid #ffffff;
|
||||
|
||||
pre {
|
||||
background-color: #ffffff;
|
||||
border: none;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
.nteract-cell:hover.nteract-md-cell {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.nteract-md-cell {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.nteract-cell:hover.nteract-md-cell {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { actions, ContentRef } from "@nteract/core";
|
||||
import { Cells, CodeCell, RawCell } from "@nteract/stateful-components";
|
||||
import CodeMirrorEditor from "@nteract/stateful-components/lib/inputs/connected-editors/codemirror";
|
||||
import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
|
||||
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
|
||||
import Prompt, { PassedPromptProps } from "@nteract/stateful-components/lib/inputs/prompt";
|
||||
import * as React from "react";
|
||||
@@ -67,8 +67,8 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
|
||||
? () => <SandboxOutputs id={id} contentRef={contentRef} />
|
||||
: undefined,
|
||||
editor: {
|
||||
codemirror: (props: PassedEditorProps) =>
|
||||
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} editorType="codemirror" />,
|
||||
monaco: (props: PassedEditorProps) =>
|
||||
this.props.hideInputs ? <></> : <MonacoEditor readOnly={true} {...props} editorType={"monaco"} />,
|
||||
},
|
||||
}}
|
||||
</CodeCell>
|
||||
@@ -84,8 +84,8 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
|
||||
<RawCell id={id} contentRef={contentRef} cell_type="raw">
|
||||
{{
|
||||
editor: {
|
||||
codemirror: (props: PassedEditorProps) =>
|
||||
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} editorType="codemirror" />,
|
||||
monaco: (props: PassedEditorProps) =>
|
||||
this.props.hideInputs ? <></> : <MonacoEditor {...props} readOnly={true} editorType={"monaco"} />,
|
||||
},
|
||||
}}
|
||||
</RawCell>
|
||||
|
||||
@@ -3,122 +3,110 @@
|
||||
@HighlightColor: #0078d4;
|
||||
|
||||
.NotebookRendererContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.NotebookRenderer {
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
|
||||
.nteract-cells {
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
.nteract-cell-container {
|
||||
margin-bottom: 10px;
|
||||
|
||||
.nteract-cell {
|
||||
padding: 0.5px;
|
||||
border: 1px solid #ffffff;
|
||||
border-left: 3px solid #ffffff;
|
||||
|
||||
.CellContextMenuButton {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
margin: 0px 0px 0px -100%;
|
||||
float: right;
|
||||
visibility: hidden;
|
||||
}
|
||||
.nteract-cells {
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
.CodeMirror-scroll {
|
||||
overflow: hidden !important;
|
||||
.nteract-cell-container {
|
||||
margin-bottom: 10px;
|
||||
|
||||
.nteract-cell {
|
||||
padding: 0.5px;
|
||||
border: 1px solid #ffffff;
|
||||
border-left: 3px solid #ffffff;
|
||||
|
||||
.CellContextMenuButton {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
margin: 0px 0px 0px -100%;
|
||||
float: right;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror-scroll, .CodeMirror-linenumber, .CodeMirror-gutters {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.nteract-cell:hover {
|
||||
border: 1px solid @HoverColor;
|
||||
border-left: 3px solid @HoverColor;
|
||||
|
||||
.CellContextMenuButton {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror-scroll,
|
||||
.CodeMirror-linenumber,
|
||||
.CodeMirror-gutters {
|
||||
background-color: #f5f5f5;
|
||||
.nteract-cell-container.selected {
|
||||
.nteract-cell {
|
||||
border: 1px solid @HighlightColor;
|
||||
border-left: 3px solid @HighlightColor;
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
height: inherit;
|
||||
}
|
||||
// White background when hovered or selected
|
||||
.nteract-cell:hover, .nteract-cell-container.selected .nteract-cell {
|
||||
.CodeMirror-scroll, .CodeMirror-linenumber, .CodeMirror-gutters {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.nteract-cell:hover {
|
||||
border: 1px solid @HoverColor;
|
||||
border-left: 3px solid @HoverColor;
|
||||
.CodeMirror-linenumber {
|
||||
color: #015CDA;
|
||||
}
|
||||
|
||||
.CellContextMenuButton {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
.nteract-cell-outputs {
|
||||
border-top: 1px solid @HoverColor;
|
||||
}
|
||||
|
||||
.nteract-cell-container.selected {
|
||||
.nteract-cell {
|
||||
border: 1px solid @HighlightColor;
|
||||
border-left: 3px solid @HighlightColor;
|
||||
}
|
||||
}
|
||||
|
||||
// White background when hovered or selected
|
||||
.nteract-cell:hover,
|
||||
.nteract-cell-container.selected .nteract-cell {
|
||||
.CodeMirror-scroll,
|
||||
.CodeMirror-linenumber,
|
||||
.CodeMirror-gutters {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.CodeMirror-linenumber {
|
||||
color: #015cda;
|
||||
.nteract-md-cell {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.nteract-cell-outputs {
|
||||
border-top: 1px solid @HoverColor;
|
||||
padding: 10px;
|
||||
border-top: 1px solid #ffffff;
|
||||
|
||||
pre {
|
||||
background-color: #ffffff;
|
||||
border: none;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.nteract-md-cell {
|
||||
background-color: #ffffff;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.nteract-cell-outputs {
|
||||
padding: 10px;
|
||||
border-top: 1px solid #ffffff;
|
||||
|
||||
pre {
|
||||
background-color: #ffffff;
|
||||
border: none;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
.nteract-cell:hover.nteract-md-cell {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.nteract-md-cell {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.nteract-cell:hover.nteract-md-cell {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.nteract-md-cell .ntreact-cell-source {
|
||||
width: 100%;
|
||||
}
|
||||
.nteract-md-cell .ntreact-cell-source {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Undo tree.less
|
||||
.expanded::before {
|
||||
content: "";
|
||||
content: '';
|
||||
}
|
||||
|
||||
.monaco-editor .monaco-list .main {
|
||||
background-color: transparent;
|
||||
}
|
||||
background-color: transparent;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { CellId } from "@nteract/commutable";
|
||||
import { CellType } from "@nteract/commutable/src";
|
||||
import { actions, ContentRef, selectors } from "@nteract/core";
|
||||
import { Cells, CodeCell, RawCell } from "@nteract/stateful-components";
|
||||
import CodeMirrorEditor from "@nteract/stateful-components/lib/inputs/connected-editors/codemirror";
|
||||
import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
|
||||
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
|
||||
import * as React from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
@@ -120,9 +120,7 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
|
||||
<CodeCell id={id} contentRef={contentRef} cell_type="code">
|
||||
{{
|
||||
editor: {
|
||||
codemirror: (props: PassedEditorProps) => (
|
||||
<CodeMirrorEditor {...props} editorType="codemirror" />
|
||||
),
|
||||
monaco: (props: PassedEditorProps) => <MonacoEditor {...props} editorType={"monaco"} />,
|
||||
},
|
||||
prompt: ({ id, contentRef }: { id: CellId; contentRef: ContentRef }) => (
|
||||
<Prompt id={id} contentRef={contentRef} isHovered={false}>
|
||||
@@ -144,9 +142,7 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
|
||||
<MarkdownCell id={id} contentRef={contentRef} cell_type="markdown">
|
||||
{{
|
||||
editor: {
|
||||
codemirror: (props: PassedEditorProps) => (
|
||||
<CodeMirrorEditor {...props} editorType="codemirror" />
|
||||
),
|
||||
monaco: (props: PassedEditorProps) => <MonacoEditor {...props} editorType={"monaco"} />,
|
||||
},
|
||||
toolbar: () => <CellToolbar id={id} contentRef={contentRef} />,
|
||||
}}
|
||||
@@ -161,9 +157,7 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
|
||||
<RawCell id={id} contentRef={contentRef} cell_type="raw">
|
||||
{{
|
||||
editor: {
|
||||
codemirror: (props: PassedEditorProps) => (
|
||||
<CodeMirrorEditor {...props} editorType="codemirror" />
|
||||
),
|
||||
monaco: (props: PassedEditorProps) => <MonacoEditor {...props} editorType={"monaco"} />,
|
||||
},
|
||||
toolbar: () => <CellToolbar id={id} contentRef={contentRef} />,
|
||||
}}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
jest.mock("./NotebookComponent/store");
|
||||
jest.mock("@nteract/core");
|
||||
import { defineConfigOption } from "@nteract/mythic-configuration";
|
||||
import { NotebookClientV2 } from "./NotebookClientV2";
|
||||
import configureStore from "./NotebookComponent/store";
|
||||
import { defineConfigOption } from "@nteract/mythic-configuration";
|
||||
|
||||
describe("auto start kernel", () => {
|
||||
it("configure autoStartKernelOnNotebookOpen properly depending whether notebook is/is not read-only", async () => {
|
||||
@@ -24,12 +24,6 @@ describe("auto start kernel", () => {
|
||||
defaultValue: 1234,
|
||||
});
|
||||
|
||||
defineConfigOption({
|
||||
label: "Line numbers",
|
||||
key: "codeMirror.lineNumbers",
|
||||
defaultValue: true,
|
||||
});
|
||||
|
||||
[true, false].forEach((isReadOnly) => {
|
||||
new NotebookClientV2({
|
||||
connectionInfo: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as ko from "knockout";
|
||||
import { ActionContracts } from "../../Contracts/ExplorerContracts";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import Explorer from "../Explorer";
|
||||
import { ActionContracts } from "../Contracts/ExplorerContracts";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import Explorer from "./Explorer";
|
||||
import { handleOpenAction } from "./OpenActions";
|
||||
|
||||
describe("OpenActions", () => {
|
||||
@@ -9,6 +9,7 @@ describe("OpenActions", () => {
|
||||
let explorer: Explorer;
|
||||
let database: ViewModels.Database;
|
||||
let collection: ViewModels.Collection;
|
||||
let databases: ViewModels.Database[];
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = {} as Explorer;
|
||||
@@ -18,6 +19,7 @@ describe("OpenActions", () => {
|
||||
id: ko.observable("db"),
|
||||
collections: ko.observableArray<ViewModels.Collection>([]),
|
||||
} as ViewModels.Database;
|
||||
databases = [database];
|
||||
collection = {
|
||||
id: ko.observable("coll"),
|
||||
} as ViewModels.Collection;
|
||||
@@ -66,7 +68,7 @@ describe("OpenActions", () => {
|
||||
paneKind: "AddCollection",
|
||||
};
|
||||
|
||||
handleOpenAction(action, [], explorer);
|
||||
const actionHandled = handleOpenAction(action, [], explorer);
|
||||
expect(explorer.onNewCollectionClicked).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -76,7 +78,7 @@ describe("OpenActions", () => {
|
||||
paneKind: ActionContracts.PaneKind.AddCollection,
|
||||
};
|
||||
|
||||
handleOpenAction(action, [], explorer);
|
||||
const actionHandled = handleOpenAction(action, [], explorer);
|
||||
expect(explorer.onNewCollectionClicked).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,38 +1,39 @@
|
||||
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
|
||||
import React from "react";
|
||||
import { ActionContracts } from "../../Contracts/ExplorerContracts";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { useSidePanel } from "../../hooks/useSidePanel";
|
||||
import Explorer from "../Explorer";
|
||||
import { CassandraAddCollectionPane } from "../Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
||||
import { SettingsPane } from "../Panes/SettingsPane/SettingsPane";
|
||||
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
|
||||
|
||||
function generateQueryText(action: ActionContracts.OpenQueryTab, partitionKeyProperty: string): string {
|
||||
if (!action.query) {
|
||||
return "SELECT * FROM c";
|
||||
} else if (action.query.text) {
|
||||
return action.query.text;
|
||||
} else if (!!action.query.partitionKeys && action.query.partitionKeys.length > 0) {
|
||||
let query = "SELECT * FROM c WHERE";
|
||||
for (let i = 0; i < action.query.partitionKeys.length; i++) {
|
||||
const partitionKey = action.query.partitionKeys[i];
|
||||
if (!partitionKey) {
|
||||
// null partition key case
|
||||
query = query.concat(` c.${partitionKeyProperty} = ${action.query.partitionKeys[i]}`);
|
||||
} else if (typeof partitionKey !== "string") {
|
||||
// Undefined partition key case
|
||||
query = query.concat(` NOT IS_DEFINED(c.${partitionKeyProperty})`);
|
||||
} else {
|
||||
query = query.concat(` c.${partitionKeyProperty} = "${action.query.partitionKeys[i]}"`);
|
||||
}
|
||||
if (i !== action.query.partitionKeys.length - 1) {
|
||||
query = query.concat(" OR");
|
||||
}
|
||||
}
|
||||
return query;
|
||||
import { ActionContracts } from "../Contracts/ExplorerContracts";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import Explorer from "./Explorer";
|
||||
|
||||
export function handleOpenAction(
|
||||
action: ActionContracts.DataExplorerAction,
|
||||
databases: ViewModels.Database[],
|
||||
explorer: Explorer
|
||||
): boolean {
|
||||
if (
|
||||
action.actionType === ActionContracts.ActionType.OpenCollectionTab ||
|
||||
(<any>action).actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenCollectionTab]
|
||||
) {
|
||||
openCollectionTab(<ActionContracts.OpenCollectionTab>action, databases);
|
||||
return true;
|
||||
}
|
||||
return "SELECT * FROM c";
|
||||
|
||||
if (
|
||||
action.actionType === ActionContracts.ActionType.OpenPane ||
|
||||
(<any>action).actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenPane]
|
||||
) {
|
||||
openPane(<ActionContracts.OpenPane>action, explorer);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
action.actionType === ActionContracts.ActionType.OpenSampleNotebook ||
|
||||
(<any>action).actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenSampleNotebook]
|
||||
) {
|
||||
openFile(<ActionContracts.OpenSampleNotebook>action, explorer);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function openCollectionTab(
|
||||
@@ -64,7 +65,7 @@ function openCollectionTab(
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLDocuments]
|
||||
(<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLDocuments]
|
||||
) {
|
||||
collection.onDocumentDBDocumentsClick();
|
||||
break;
|
||||
@@ -72,7 +73,7 @@ function openCollectionTab(
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.MongoDocuments ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoDocuments]
|
||||
(<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoDocuments]
|
||||
) {
|
||||
collection.onMongoDBDocumentsClick();
|
||||
break;
|
||||
@@ -80,7 +81,7 @@ function openCollectionTab(
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.SchemaAnalyzer ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer]
|
||||
(<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer]
|
||||
) {
|
||||
collection.onSchemaAnalyzerClick();
|
||||
break;
|
||||
@@ -88,7 +89,7 @@ function openCollectionTab(
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.TableEntities ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities]
|
||||
(<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities]
|
||||
) {
|
||||
collection.onTableEntitiesClick();
|
||||
break;
|
||||
@@ -96,7 +97,7 @@ function openCollectionTab(
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.Graph ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.Graph]
|
||||
(<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.Graph]
|
||||
) {
|
||||
collection.onGraphDocumentsClick();
|
||||
break;
|
||||
@@ -104,19 +105,19 @@ function openCollectionTab(
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.SQLQuery ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
|
||||
(<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
|
||||
) {
|
||||
collection.onNewQueryClick(
|
||||
collection,
|
||||
undefined,
|
||||
generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperty)
|
||||
null,
|
||||
generateQueryText(<ActionContracts.OpenQueryTab>action, collection.partitionKeyProperty)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.ScaleSettings ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.ScaleSettings]
|
||||
(<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.ScaleSettings]
|
||||
) {
|
||||
collection.onSettingsClick();
|
||||
break;
|
||||
@@ -137,59 +138,49 @@ function openCollectionTab(
|
||||
function openPane(action: ActionContracts.OpenPane, explorer: Explorer) {
|
||||
if (
|
||||
action.paneKind === ActionContracts.PaneKind.AddCollection ||
|
||||
action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.AddCollection]
|
||||
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.AddCollection]
|
||||
) {
|
||||
explorer.onNewCollectionClicked();
|
||||
} else if (
|
||||
action.paneKind === ActionContracts.PaneKind.CassandraAddCollection ||
|
||||
action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.CassandraAddCollection]
|
||||
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.CassandraAddCollection]
|
||||
) {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Add Table",
|
||||
<CassandraAddCollectionPane explorer={explorer} cassandraApiClient={new CassandraAPIDataClient()} />
|
||||
);
|
||||
explorer.openCassandraAddCollectionPane();
|
||||
} else if (
|
||||
action.paneKind === ActionContracts.PaneKind.GlobalSettings ||
|
||||
action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings]
|
||||
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings]
|
||||
) {
|
||||
useSidePanel.getState().openSidePanel("Settings", <SettingsPane />);
|
||||
explorer.openSettingPane();
|
||||
}
|
||||
}
|
||||
|
||||
export function handleOpenAction(
|
||||
action: ActionContracts.DataExplorerAction,
|
||||
databases: ViewModels.Database[],
|
||||
explorer: Explorer
|
||||
): boolean {
|
||||
if (
|
||||
action.actionType === ActionContracts.ActionType.OpenCollectionTab ||
|
||||
action.actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenCollectionTab]
|
||||
) {
|
||||
openCollectionTab(action as ActionContracts.OpenCollectionTab, databases);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
action.actionType === ActionContracts.ActionType.OpenPane ||
|
||||
action.actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenPane]
|
||||
) {
|
||||
openPane(action as ActionContracts.OpenPane, explorer);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
action.actionType === ActionContracts.ActionType.OpenSampleNotebook ||
|
||||
action.actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenSampleNotebook]
|
||||
) {
|
||||
openFile(action as ActionContracts.OpenSampleNotebook, explorer);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function openFile(action: ActionContracts.OpenSampleNotebook, explorer: Explorer) {
|
||||
explorer.handleOpenFileAction(decodeURIComponent(action.path));
|
||||
}
|
||||
|
||||
function generateQueryText(action: ActionContracts.OpenQueryTab, partitionKeyProperty: string): string {
|
||||
if (!action.query) {
|
||||
return "SELECT * FROM c";
|
||||
} else if (!!action.query.text) {
|
||||
return action.query.text;
|
||||
} else if (!!action.query.partitionKeys && action.query.partitionKeys.length > 0) {
|
||||
let query = "SELECT * FROM c WHERE";
|
||||
for (let i = 0; i < action.query.partitionKeys.length; i++) {
|
||||
let partitionKey = action.query.partitionKeys[i];
|
||||
if (!partitionKey) {
|
||||
// null partition key case
|
||||
query = query.concat(` c.${partitionKeyProperty} = ${action.query.partitionKeys[i]}`);
|
||||
} else if (typeof partitionKey !== "string") {
|
||||
// Undefined partition key case
|
||||
query = query.concat(` NOT IS_DEFINED(c.${partitionKeyProperty})`);
|
||||
} else {
|
||||
query = query.concat(` c.${partitionKeyProperty} = "${action.query.partitionKeys[i]}"`);
|
||||
}
|
||||
if (i !== action.query.partitionKeys.length - 1) {
|
||||
query = query.concat(" OR");
|
||||
}
|
||||
}
|
||||
return query;
|
||||
}
|
||||
return "SELECT * FROM c";
|
||||
}
|
||||
@@ -31,7 +31,6 @@ import { getUpsellMessage } from "../../Utils/PricingUtils";
|
||||
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
|
||||
import Explorer from "../Explorer";
|
||||
import { useDatabases } from "../useDatabases";
|
||||
import { PanelFooterComponent } from "./PanelFooterComponent";
|
||||
import { PanelInfoErrorComponent } from "./PanelInfoErrorComponent";
|
||||
import { PanelLoadingScreen } from "./PanelLoadingScreen";
|
||||
@@ -126,8 +125,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const isFirstResourceCreated = useDatabases.getState().isFirstResourceCreated();
|
||||
|
||||
return (
|
||||
<form className="panelFormWrapper" onSubmit={this.submit.bind(this)}>
|
||||
{this.state.errorMessage && (
|
||||
@@ -140,7 +137,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
{!this.state.errorMessage && this.isFreeTierAccount() && (
|
||||
<PanelInfoErrorComponent
|
||||
message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)}
|
||||
message={getUpsellMessage(userContext.portalEnv, true, this.props.explorer.isFirstResourceCreated(), true)}
|
||||
messageType="info"
|
||||
showErrorDetails={false}
|
||||
link={Constants.Urls.freeTierInformation}
|
||||
@@ -243,7 +240,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
{!isServerlessAccount() && this.state.isSharedThroughputChecked && (
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
|
||||
showFreeTierExceedThroughputTooltip={
|
||||
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
|
||||
}
|
||||
isDatabase={true}
|
||||
isSharded={this.state.isSharded}
|
||||
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
|
||||
@@ -470,7 +469,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
{this.shouldShowCollectionThroughputInput() && (
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
|
||||
showFreeTierExceedThroughputTooltip={
|
||||
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
|
||||
}
|
||||
isDatabase={false}
|
||||
isSharded={this.state.isSharded}
|
||||
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
|
||||
@@ -679,7 +680,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
}
|
||||
|
||||
private getDatabaseOptions(): IDropdownOption[] {
|
||||
return useDatabases.getState().databases?.map((database) => ({
|
||||
return this.props.explorer?.databases()?.map((database) => ({
|
||||
key: database.id(),
|
||||
text: database.id(),
|
||||
}));
|
||||
@@ -771,9 +772,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectedDatabase = useDatabases
|
||||
.getState()
|
||||
.databases?.find((database) => database.id() === this.state.selectedDatabaseId);
|
||||
const selectedDatabase = this.props.explorer
|
||||
.databases()
|
||||
?.find((database) => database.id() === this.state.selectedDatabaseId);
|
||||
return !!selectedDatabase?.offer();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,7 @@ import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
|
||||
import { getUpsellMessage } from "../../../Utils/PricingUtils";
|
||||
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
||||
import Explorer from "../../Explorer";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
|
||||
import { getTextFieldStyles } from "../PanelStyles";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
|
||||
export interface AddDatabasePaneProps {
|
||||
@@ -173,12 +171,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||
<RightPaneForm {...props}>
|
||||
{!formErrors && isFreeTierAccount && (
|
||||
<PanelInfoErrorComponent
|
||||
message={getUpsellMessage(
|
||||
userContext.portalEnv,
|
||||
true,
|
||||
useDatabases.getState().isFirstResourceCreated(),
|
||||
true
|
||||
)}
|
||||
message={getUpsellMessage(userContext.portalEnv, true, container.isFirstResourceCreated(), true)}
|
||||
messageType="info"
|
||||
showErrorDetails={false}
|
||||
link={Constants.Urls.freeTierInformation}
|
||||
@@ -186,12 +179,10 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||
/>
|
||||
)}
|
||||
<div className="panelMainContent">
|
||||
<Stack>
|
||||
<div>
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
{databaseIdLabel}
|
||||
</Text>
|
||||
<span className="mandatoryStar">*</span>
|
||||
<Text variant="small">{databaseIdLabel}</Text>
|
||||
<InfoTooltip>{databaseIdTooltipText}</InfoTooltip>
|
||||
</Stack>
|
||||
|
||||
@@ -208,37 +199,36 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||
value={databaseId}
|
||||
onChange={handleonChangeDBId}
|
||||
autoFocus
|
||||
styles={getTextFieldStyles()}
|
||||
style={{ fontSize: 12 }}
|
||||
styles={{ root: { width: 300 } }}
|
||||
/>
|
||||
|
||||
{!isServerlessAccount() && (
|
||||
<Stack horizontal>
|
||||
<Checkbox
|
||||
title="Provision shared throughput"
|
||||
styles={{
|
||||
text: { fontSize: 12 },
|
||||
checkbox: { width: 12, height: 12 },
|
||||
label: { padding: 0, alignItems: "center" },
|
||||
}}
|
||||
label="Provision throughput"
|
||||
checked={databaseCreateNewShared}
|
||||
onChange={() => setDatabaseCreateNewShared(!databaseCreateNewShared)}
|
||||
/>
|
||||
<InfoTooltip>{databaseLevelThroughputTooltipText}</InfoTooltip>
|
||||
</Stack>
|
||||
<Stack horizontal>
|
||||
<Checkbox
|
||||
title="Provision shared throughput"
|
||||
styles={{
|
||||
text: { fontSize: 12 },
|
||||
checkbox: { width: 12, height: 12 },
|
||||
label: { padding: 0, alignItems: "center" },
|
||||
}}
|
||||
label="Provision throughput"
|
||||
checked={databaseCreateNewShared}
|
||||
onChange={() => setDatabaseCreateNewShared(!databaseCreateNewShared)}
|
||||
/>
|
||||
<InfoTooltip>{databaseLevelThroughputTooltipText}</InfoTooltip>
|
||||
</Stack>
|
||||
|
||||
{!isServerlessAccount() && databaseCreateNewShared && (
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !container?.isFirstResourceCreated()}
|
||||
isDatabase={true}
|
||||
isSharded={databaseCreateNewShared}
|
||||
setThroughputValue={(newThroughput: number) => (throughput = newThroughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (isAutoscaleSelected = isAutoscale)}
|
||||
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{!isServerlessAccount() && databaseCreateNewShared && (
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
|
||||
isDatabase={true}
|
||||
isSharded={databaseCreateNewShared}
|
||||
setThroughputValue={(newThroughput: number) => (throughput = newThroughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (isAutoscaleSelected = isAutoscale)}
|
||||
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</RightPaneForm>
|
||||
);
|
||||
|
||||
@@ -10,17 +10,16 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
||||
<div
|
||||
className="panelMainContent"
|
||||
>
|
||||
<Stack>
|
||||
<div>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<span
|
||||
className="mandatoryStar"
|
||||
>
|
||||
*
|
||||
*
|
||||
</span>
|
||||
<Text
|
||||
className="panelTextBold"
|
||||
variant="small"
|
||||
>
|
||||
Database id
|
||||
@@ -39,16 +38,13 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
||||
pattern="[^/?#\\\\\\\\]*[^/?# \\\\\\\\]"
|
||||
placeholder="Type a new database id"
|
||||
size={40}
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 12,
|
||||
}
|
||||
}
|
||||
styles={
|
||||
Object {
|
||||
"field": Object {
|
||||
"fontSize": 12,
|
||||
"selectors": Object {
|
||||
"::placeholder": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
"root": Object {
|
||||
"width": 300,
|
||||
},
|
||||
@@ -86,14 +82,14 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
||||
Provisioned throughput at the database level will be shared across all collections within the database.
|
||||
</InfoTooltip>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<ThroughputInput
|
||||
isDatabase={true}
|
||||
isSharded={true}
|
||||
onCostAcknowledgeChange={[Function]}
|
||||
setIsAutoscale={[Function]}
|
||||
setThroughputValue={[Function]}
|
||||
/>
|
||||
<ThroughputInput
|
||||
isDatabase={true}
|
||||
isSharded={true}
|
||||
onCostAcknowledgeChange={[Function]}
|
||||
setIsAutoscale={[Function]}
|
||||
setThroughputValue={[Function]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</RightPaneForm>
|
||||
`;
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { mount } from "enzyme";
|
||||
import * as ko from "knockout";
|
||||
import React from "react";
|
||||
import { SavedQueries } from "../../../Common/Constants";
|
||||
import { QueriesClient } from "../../../Common/QueriesClient";
|
||||
import { Query } from "../../../Contracts/DataModels";
|
||||
import { Collection, Database } from "../../../Contracts/ViewModels";
|
||||
import Explorer from "../../Explorer";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
import { BrowseQueriesPane } from "./BrowseQueriesPane";
|
||||
|
||||
describe("Browse queries panel", () => {
|
||||
const fakeExplorer = {} as Explorer;
|
||||
fakeExplorer.canSaveQueries = ko.computed<boolean>(() => true);
|
||||
const fakeClientQuery = {} as QueriesClient;
|
||||
const fakeQueryData = [] as Query[];
|
||||
fakeClientQuery.getQueries = async () => fakeQueryData;
|
||||
@@ -19,16 +17,6 @@ describe("Browse queries panel", () => {
|
||||
explorer: fakeExplorer,
|
||||
closePanel: (): void => undefined,
|
||||
};
|
||||
useDatabases.getState().addDatabases([
|
||||
{
|
||||
id: ko.observable(SavedQueries.DatabaseName),
|
||||
collections: ko.observableArray([
|
||||
{
|
||||
id: ko.observable(SavedQueries.CollectionName),
|
||||
} as Collection,
|
||||
]),
|
||||
} as Database,
|
||||
]);
|
||||
|
||||
it("Should render Default properly", () => {
|
||||
const wrapper = mount(<BrowseQueriesPane {...props} />);
|
||||
|
||||
@@ -12,9 +12,7 @@ import {
|
||||
QueriesGridComponentProps,
|
||||
} from "../../Controls/QueriesGridReactComponent/QueriesGridComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
import { NewQueryTab } from "../../Tabs/QueryTab/QueryTab";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
import QueryTab from "../../Tabs/QueryTab";
|
||||
|
||||
interface BrowseQueriesPaneProps {
|
||||
explorer: Explorer;
|
||||
@@ -25,7 +23,7 @@ export const BrowseQueriesPane: FunctionComponent<BrowseQueriesPaneProps> = ({
|
||||
}: BrowseQueriesPaneProps): JSX.Element => {
|
||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||
const loadSavedQuery = (savedQuery: Query): void => {
|
||||
const selectedCollection: Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
const selectedCollection: Collection = explorer && explorer.findSelectedCollection();
|
||||
if (!selectedCollection) {
|
||||
// should never get into this state because this pane is only accessible through the query tab
|
||||
logError("No collection was selected", "BrowseQueriesPane.loadSavedQuery");
|
||||
@@ -33,13 +31,13 @@ export const BrowseQueriesPane: FunctionComponent<BrowseQueriesPaneProps> = ({
|
||||
} else if (userContext.apiType === "Mongo") {
|
||||
selectedCollection.onNewMongoQueryClick(selectedCollection, undefined);
|
||||
} else {
|
||||
selectedCollection.onNewQueryClick(selectedCollection, undefined, savedQuery.query);
|
||||
selectedCollection.onNewQueryClick(selectedCollection, undefined);
|
||||
}
|
||||
|
||||
const queryTab = explorer && (explorer.tabsManager.activeTab() as NewQueryTab);
|
||||
const queryTab = explorer.tabsManager.activeTab() as QueryTab;
|
||||
queryTab.tabTitle(savedQuery.queryName);
|
||||
queryTab.tabPath(`${selectedCollection.databaseId}>${selectedCollection.id()}>${savedQuery.queryName}`);
|
||||
|
||||
queryTab.initialEditorContent(savedQuery.query);
|
||||
queryTab.sqlQueryEditorContent(savedQuery.query);
|
||||
trace(Action.LoadSavedQuery, ActionModifiers.Mark, {
|
||||
dataExplorerArea: Areas.ContextualPane,
|
||||
queryName: savedQuery.queryName,
|
||||
@@ -47,13 +45,12 @@ export const BrowseQueriesPane: FunctionComponent<BrowseQueriesPaneProps> = ({
|
||||
});
|
||||
closeSidePanel();
|
||||
};
|
||||
const isSaveQueryEnabled = useDatabases((state) => state.isSaveQueryEnabled);
|
||||
|
||||
const props: QueriesGridComponentProps = {
|
||||
queriesClient: explorer.queriesClient,
|
||||
onQuerySelect: loadSavedQuery,
|
||||
containerVisible: true,
|
||||
saveQueryEnabled: isSaveQueryEnabled(),
|
||||
saveQueryEnabled: explorer.canSaveQueries(),
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,6 +5,7 @@ exports[`Browse queries panel Should render Default properly 1`] = `
|
||||
closePanel={[Function]}
|
||||
explorer={
|
||||
Object {
|
||||
"canSaveQueries": [Function],
|
||||
"queriesClient": Object {
|
||||
"getQueries": [Function],
|
||||
},
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import Explorer from "../../Explorer";
|
||||
import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
|
||||
import { CassandraAddCollectionPane } from "./CassandraAddCollectionPane";
|
||||
const props = {
|
||||
explorer: new Explorer(),
|
||||
closePanel: (): void => undefined,
|
||||
cassandraApiClient: new CassandraAPIDataClient(),
|
||||
};
|
||||
|
||||
describe("Cassandra add collection pane test", () => {
|
||||
const props = {
|
||||
explorer: new Explorer(),
|
||||
closePanel: (): void => undefined,
|
||||
cassandraApiClient: new CassandraAPIDataClient(),
|
||||
};
|
||||
|
||||
describe("CassandraAddCollectionPane Pane", () => {
|
||||
beforeEach(() => render(<CassandraAddCollectionPane {...props} />));
|
||||
|
||||
it("should render default properly", () => {
|
||||
expect(screen.getByRole("radio", { name: "Create new keyspace", checked: true })).toBeDefined();
|
||||
expect(screen.getByRole("checkbox", { name: "Provision shared throughput", checked: false })).toBeDefined();
|
||||
it("should render Default properly", () => {
|
||||
const wrapper = shallow(<CassandraAddCollectionPane {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it("click on is Create new keyspace", () => {
|
||||
fireEvent.click(screen.getByLabelText("Create new keyspace"));
|
||||
expect(screen.getByLabelText("Provision keyspace throughput")).toBeDefined();
|
||||
});
|
||||
it("click on Use existing", () => {
|
||||
fireEvent.click(screen.getByLabelText("Use existing keyspace"));
|
||||
});
|
||||
|
||||
it("click on use existing", () => {
|
||||
fireEvent.click(screen.getByRole("radio", { name: "Use existing keyspace" }));
|
||||
expect(screen.getByRole("combobox", { name: "Choose existing keyspace id" })).toBeDefined();
|
||||
});
|
||||
|
||||
it("enter Keyspace name ", () => {
|
||||
fireEvent.change(screen.getByRole("textbox", { name: "Keyspace id" }), { target: { value: "table1" } });
|
||||
expect(screen.getByText("CREATE TABLE table1.")).toBeDefined();
|
||||
it("Enter Keyspace name ", () => {
|
||||
fireEvent.change(screen.getByLabelText("Keyspace id"), { target: { value: "unittest1" } });
|
||||
expect(screen.getByLabelText("CREATE TABLE unittest1.")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { Checkbox, Dropdown, IDropdownOption, Link, Stack, Text, TextField } from "@fluentui/react";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import { Label, Stack, TextField } from "@fluentui/react";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import * as _ from "underscore";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import * as AddCollectionUtility from "../../../Shared/AddCollectionUtility";
|
||||
import * as SharedConstants from "../../../Shared/Constants";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
|
||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
||||
import Explorer from "../../Explorer";
|
||||
import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
import { getTextFieldStyles } from "../PanelStyles";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
|
||||
export interface CassandraAddCollectionPaneProps {
|
||||
@@ -25,73 +27,183 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
explorer: container,
|
||||
cassandraApiClient,
|
||||
}: CassandraAddCollectionPaneProps) => {
|
||||
let newKeySpaceThroughput: number;
|
||||
let isNewKeySpaceAutoscale: boolean;
|
||||
let tableThroughput: number;
|
||||
let isTableAutoscale: boolean;
|
||||
let isCostAcknowledged: boolean;
|
||||
|
||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||
const [newKeyspaceId, setNewKeyspaceId] = useState<string>("");
|
||||
const [existingKeyspaceId, setExistingKeyspaceId] = useState<string>("");
|
||||
const throughputDefaults = userContext.collectionCreationDefaults.throughput;
|
||||
const [createTableQuery, setCreateTableQuery] = useState<string>("CREATE TABLE ");
|
||||
const [keyspaceId, setKeyspaceId] = useState<string>("");
|
||||
const [tableId, setTableId] = useState<string>("");
|
||||
const [throughput, setThroughput] = useState<number>(
|
||||
AddCollectionUtility.getMaxThroughput(userContext.collectionCreationDefaults, container)
|
||||
);
|
||||
|
||||
const [isAutoPilotSelected, setIsAutoPilotSelected] = useState<boolean>(userContext.features.autoscaleDefault);
|
||||
|
||||
const [isSharedAutoPilotSelected, setIsSharedAutoPilotSelected] = useState<boolean>(
|
||||
userContext.features.autoscaleDefault
|
||||
);
|
||||
|
||||
const [userTableQuery, setUserTableQuery] = useState<string>(
|
||||
"(userid int, name text, email text, PRIMARY KEY (userid))"
|
||||
);
|
||||
const [isKeyspaceShared, setIsKeyspaceShared] = useState<boolean>(false);
|
||||
|
||||
const [keyspaceHasSharedOffer, setKeyspaceHasSharedOffer] = useState<boolean>(false);
|
||||
const [keyspaceIds, setKeyspaceIds] = useState<string[]>([]);
|
||||
const [keyspaceThroughput, setKeyspaceThroughput] = useState<number>(throughputDefaults.shared);
|
||||
const [keyspaceCreateNew, setKeyspaceCreateNew] = useState<boolean>(true);
|
||||
const [dedicateTableThroughput, setDedicateTableThroughput] = useState<boolean>(false);
|
||||
const [isExecuting, setIsExecuting] = useState<boolean>();
|
||||
const [formError, setFormError] = useState<string>("");
|
||||
const [throughputSpendAck, setThroughputSpendAck] = useState<boolean>(false);
|
||||
const [sharedThroughputSpendAck, setSharedThroughputSpendAck] = useState<boolean>(false);
|
||||
|
||||
const { minAutoPilotThroughput: selectedAutoPilotThroughput } = AutoPilotUtils;
|
||||
const { minAutoPilotThroughput: sharedAutoPilotThroughput } = AutoPilotUtils;
|
||||
|
||||
const _getAutoPilot = (): DataModels.AutoPilotCreationSettings => {
|
||||
if (keyspaceCreateNew && keyspaceHasSharedOffer && isSharedAutoPilotSelected && sharedAutoPilotThroughput) {
|
||||
return {
|
||||
maxThroughput: sharedAutoPilotThroughput * 1,
|
||||
};
|
||||
}
|
||||
|
||||
if (selectedAutoPilotThroughput) {
|
||||
return {
|
||||
maxThroughput: selectedAutoPilotThroughput * 1,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
|
||||
|
||||
const canConfigureThroughput = !container.isServerlessEnabled();
|
||||
|
||||
const keyspaceOffers = new Map();
|
||||
const [isExecuting, setIsExecuting] = useState<boolean>();
|
||||
const [formErrors, setFormErrors] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
if (keyspaceIds.indexOf(keyspaceId) >= 0) {
|
||||
setKeyspaceHasSharedOffer(keyspaceOffers.has(keyspaceId));
|
||||
}
|
||||
setCreateTableQuery(`CREATE TABLE ${keyspaceId}.`);
|
||||
}, [keyspaceId]);
|
||||
|
||||
const addCollectionPaneOpenMessage = {
|
||||
collection: {
|
||||
id: tableId,
|
||||
storage: Constants.BackendDefaults.multiPartitionStorageInGb,
|
||||
offerThroughput: newKeySpaceThroughput || tableThroughput,
|
||||
offerThroughput: throughput,
|
||||
partitionKey: "",
|
||||
databaseId: keyspaceCreateNew ? newKeyspaceId : existingKeyspaceId,
|
||||
databaseId: keyspaceId,
|
||||
},
|
||||
subscriptionType: userContext.subscriptionType,
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
storage: "u",
|
||||
throughput: newKeySpaceThroughput || tableThroughput,
|
||||
throughput,
|
||||
flight: userContext.addCollectionFlight,
|
||||
},
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
const throughput = keyspaceCreateNew ? newKeySpaceThroughput : tableThroughput;
|
||||
const keyspaceId = keyspaceCreateNew ? newKeyspaceId : existingKeyspaceId;
|
||||
|
||||
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
|
||||
const errorMessage =
|
||||
isNewKeySpaceAutoscale || isTableAutoscale
|
||||
? "Please acknowledge the estimated monthly spend."
|
||||
: "Please acknowledge the estimated daily spend.";
|
||||
setFormError(errorMessage);
|
||||
return;
|
||||
useEffect(() => {
|
||||
if (!container.isServerlessEnabled()) {
|
||||
setIsAutoPilotSelected(userContext.features.autoscaleDefault);
|
||||
}
|
||||
|
||||
TelemetryProcessor.trace(Action.CreateCollection, ActionModifiers.Open, addCollectionPaneOpenMessage);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (container) {
|
||||
const newKeyspaceIds: ViewModels.Database[] = container.databases();
|
||||
const cachedKeyspaceIdsList = _.map(newKeyspaceIds, (keyspace: ViewModels.Database) => {
|
||||
if (keyspace && keyspace.offer && !!keyspace.offer()) {
|
||||
keyspaceOffers.set(keyspace.id(), keyspace.offer());
|
||||
}
|
||||
return keyspace.id();
|
||||
});
|
||||
setKeyspaceIds(cachedKeyspaceIdsList);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const _isValid = () => {
|
||||
const sharedAutoscaleThroughput = sharedAutoPilotThroughput * 1;
|
||||
if (
|
||||
isSharedAutoPilotSelected &&
|
||||
sharedAutoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
|
||||
!sharedThroughputSpendAck
|
||||
) {
|
||||
setFormErrors(`Please acknowledge the estimated monthly spend.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const dedicatedAutoscaleThroughput = selectedAutoPilotThroughput * 1;
|
||||
if (
|
||||
isAutoPilotSelected &&
|
||||
dedicatedAutoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
|
||||
!throughputSpendAck
|
||||
) {
|
||||
setFormErrors(`Please acknowledge the estimated monthly spend.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((keyspaceCreateNew && keyspaceHasSharedOffer && isSharedAutoPilotSelected) || isAutoPilotSelected) {
|
||||
const autoPilot = _getAutoPilot();
|
||||
if (
|
||||
!autoPilot ||
|
||||
!autoPilot.maxThroughput ||
|
||||
!AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput)
|
||||
) {
|
||||
setFormErrors(
|
||||
`Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !throughputSpendAck) {
|
||||
setFormErrors(`Please acknowledge the estimated daily spend.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
keyspaceHasSharedOffer &&
|
||||
keyspaceCreateNew &&
|
||||
keyspaceThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
|
||||
!sharedThroughputSpendAck
|
||||
) {
|
||||
setFormErrors("Please acknowledge the estimated daily spend");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!_isValid()) {
|
||||
return;
|
||||
}
|
||||
setIsExecuting(true);
|
||||
const autoPilotCommand = `cosmosdb_autoscale_max_throughput`;
|
||||
|
||||
const toCreateKeyspace: boolean = keyspaceCreateNew;
|
||||
const useAutoPilotForKeyspace: boolean = isSharedAutoPilotSelected && !!sharedAutoPilotThroughput;
|
||||
const createKeyspaceQueryPrefix = `CREATE KEYSPACE ${keyspaceId.trim()} WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 }`;
|
||||
const createKeyspaceQuery: string = isKeyspaceShared
|
||||
? isNewKeySpaceAutoscale
|
||||
? `${createKeyspaceQueryPrefix} AND ${autoPilotCommand}=${newKeySpaceThroughput};`
|
||||
: `${createKeyspaceQueryPrefix} AND cosmosdb_provisioned_throughput=${newKeySpaceThroughput};`
|
||||
const createKeyspaceQuery: string = keyspaceHasSharedOffer
|
||||
? useAutoPilotForKeyspace
|
||||
? `${createKeyspaceQueryPrefix} AND ${autoPilotCommand}=${keyspaceThroughput};`
|
||||
: `${createKeyspaceQueryPrefix} AND cosmosdb_provisioned_throughput=${keyspaceThroughput};`
|
||||
: `${createKeyspaceQueryPrefix};`;
|
||||
let tableQuery: string;
|
||||
const createTableQueryPrefix = `CREATE TABLE ${keyspaceId}.${tableId.trim()} ${userTableQuery}`;
|
||||
const createTableQueryPrefix = `${createTableQuery}${tableId.trim()} ${userTableQuery}`;
|
||||
|
||||
if (tableThroughput) {
|
||||
if (isTableAutoscale) {
|
||||
tableQuery = `${createTableQueryPrefix} WITH ${autoPilotCommand}=${tableThroughput};`;
|
||||
if (canConfigureThroughput && (dedicateTableThroughput || !keyspaceHasSharedOffer)) {
|
||||
if (isAutoPilotSelected && selectedAutoPilotThroughput) {
|
||||
tableQuery = `${createTableQueryPrefix} WITH ${autoPilotCommand}=${throughput};`;
|
||||
} else {
|
||||
tableQuery = `${createTableQueryPrefix} WITH cosmosdb_provisioned_throughput=${tableThroughput};`;
|
||||
tableQuery = `${createTableQueryPrefix} WITH cosmosdb_provisioned_throughput=${throughput};`;
|
||||
}
|
||||
} else {
|
||||
tableQuery = `${createTableQueryPrefix};`;
|
||||
@@ -103,15 +215,15 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
...addCollectionPaneOpenMessage.collection,
|
||||
hasDedicatedThroughput: dedicateTableThroughput,
|
||||
},
|
||||
isKeyspaceShared,
|
||||
keyspaceCreateNew,
|
||||
keyspaceHasSharedOffer,
|
||||
toCreateKeyspace,
|
||||
createKeyspaceQuery,
|
||||
createTableQuery: tableQuery,
|
||||
};
|
||||
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, addCollectionPaneStartMessage);
|
||||
try {
|
||||
if (keyspaceCreateNew) {
|
||||
if (toCreateKeyspace) {
|
||||
await cassandraApiClient.createTableAndKeyspace(
|
||||
userContext?.databaseAccount?.properties?.cassandraEndpoint,
|
||||
userContext?.databaseAccount?.id,
|
||||
@@ -134,7 +246,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
TelemetryProcessor.traceSuccess(Action.CreateCollection, addCollectionPaneStartMessage, startKey);
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
setFormError(errorMessage);
|
||||
setFormErrors(errorMessage);
|
||||
setIsExecuting(false);
|
||||
const addCollectionPaneFailedMessage = {
|
||||
...addCollectionPaneStartMessage,
|
||||
@@ -144,160 +256,129 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
TelemetryProcessor.traceFailure(Action.CreateCollection, addCollectionPaneFailedMessage, startKey);
|
||||
}
|
||||
};
|
||||
|
||||
const props: RightPaneFormProps = {
|
||||
formError,
|
||||
isExecuting,
|
||||
submitButtonText: "OK",
|
||||
onSubmit,
|
||||
const handleOnChangeKeyspaceType = (ev: React.FormEvent<HTMLInputElement>, mode: string): void => {
|
||||
setKeyspaceCreateNew(mode === "Create new");
|
||||
};
|
||||
|
||||
const props: RightPaneFormProps = {
|
||||
formError: formErrors,
|
||||
isExecuting,
|
||||
submitButtonText: "Apply",
|
||||
onSubmit,
|
||||
};
|
||||
return (
|
||||
<RightPaneForm {...props}>
|
||||
<div className="panelMainContent">
|
||||
<Stack>
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
<div className="paneMainContent">
|
||||
<div className="seconddivpadding">
|
||||
<p>
|
||||
<Label required>
|
||||
Keyspace name <InfoTooltip>Select an existing keyspace or enter a new keyspace id.</InfoTooltip>
|
||||
</Text>
|
||||
</Stack>
|
||||
</Label>
|
||||
</p>
|
||||
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
className="throughputInputRadioBtn"
|
||||
aria-label="Create new keyspace"
|
||||
checked={keyspaceCreateNew}
|
||||
type="radio"
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
onChange={() => {
|
||||
setKeyspaceCreateNew(true);
|
||||
setIsKeyspaceShared(false);
|
||||
setExistingKeyspaceId("");
|
||||
}}
|
||||
onChange={(e) => handleOnChangeKeyspaceType(e, "Create new")}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Create new</span>
|
||||
<span className="throughputInputRadioBtnLabel">Create new</span>
|
||||
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
className="throughputInputRadioBtn"
|
||||
aria-label="Use existing keyspace"
|
||||
checked={!keyspaceCreateNew}
|
||||
type="radio"
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
onChange={() => {
|
||||
setKeyspaceCreateNew(false);
|
||||
setIsKeyspaceShared(false);
|
||||
}}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Use existing</span>
|
||||
</Stack>
|
||||
|
||||
{keyspaceCreateNew && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<TextField
|
||||
aria-required="true"
|
||||
autoComplete="off"
|
||||
styles={getTextFieldStyles()}
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||
placeholder="Type a new keyspace id"
|
||||
size={40}
|
||||
value={newKeyspaceId}
|
||||
onChange={(e, newValue) => setNewKeyspaceId(newValue)}
|
||||
ariaLabel="Keyspace id"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
{!isServerlessAccount() && (
|
||||
<Stack horizontal>
|
||||
<Checkbox
|
||||
label="Provision shared throughput"
|
||||
checked={isKeyspaceShared}
|
||||
styles={{
|
||||
text: { fontSize: 12 },
|
||||
checkbox: { width: 12, height: 12 },
|
||||
label: { padding: 0, alignItems: "center" },
|
||||
}}
|
||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => setIsKeyspaceShared(isChecked)}
|
||||
/>
|
||||
<InfoTooltip>
|
||||
Provisioned throughput at the keyspace level will be shared across unlimited number of tables within
|
||||
the keyspace
|
||||
</InfoTooltip>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{!keyspaceCreateNew && (
|
||||
<Dropdown
|
||||
ariaLabel="Choose existing keyspace id"
|
||||
styles={{ root: { width: 300 }, title: { fontSize: 12 }, dropdownItem: { fontSize: 12 } }}
|
||||
placeholder="Choose existing keyspace id"
|
||||
defaultSelectedKey={existingKeyspaceId}
|
||||
options={useDatabases.getState().databases?.map((keyspace) => ({
|
||||
key: keyspace.id(),
|
||||
text: keyspace.id(),
|
||||
data: {
|
||||
isShared: !!keyspace.offer(),
|
||||
},
|
||||
}))}
|
||||
onChange={(event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) => {
|
||||
setExistingKeyspaceId(option.key as string);
|
||||
setIsKeyspaceShared(option.data.isShared);
|
||||
}}
|
||||
responsiveMode={999}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isServerlessAccount() && keyspaceCreateNew && isKeyspaceShared && (
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={
|
||||
isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()
|
||||
}
|
||||
isDatabase
|
||||
isSharded
|
||||
setThroughputValue={(throughput: number) => (newKeySpaceThroughput = throughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (isNewKeySpaceAutoscale = isAutoscale)}
|
||||
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Stack>
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Enter CQL command to create the table.{" "}
|
||||
<Link href="https://aka.ms/cassandra-create-table" target="_blank">
|
||||
Learn More
|
||||
</Link>
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<Text variant="small" style={{ marginRight: 4 }}>
|
||||
{`CREATE TABLE ${keyspaceCreateNew ? newKeyspaceId : existingKeyspaceId}.`}
|
||||
</Text>
|
||||
<TextField
|
||||
underlined
|
||||
styles={getTextFieldStyles({ fontSize: 12, width: 150 })}
|
||||
aria-required="true"
|
||||
ariaLabel="addCollection-tableId"
|
||||
autoComplete="off"
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||
placeholder="Enter table Id"
|
||||
size={20}
|
||||
value={tableId}
|
||||
onChange={(e, newValue) => setTableId(newValue)}
|
||||
onChange={(e) => handleOnChangeKeyspaceType(e, "Use existing")}
|
||||
/>
|
||||
<span className="throughputInputRadioBtnLabel">Use existing</span>
|
||||
</Stack>
|
||||
|
||||
<TextField
|
||||
styles={getTextFieldStyles()}
|
||||
aria-required="true"
|
||||
autoComplete="off"
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||
list={keyspaceCreateNew ? "" : "keyspacesList"}
|
||||
placeholder={keyspaceCreateNew ? "Type a new keyspace id" : "Choose existing keyspace id"}
|
||||
size={40}
|
||||
data-test="addCollection-keyspaceId"
|
||||
value={keyspaceId}
|
||||
onChange={(e, newValue) => setKeyspaceId(newValue)}
|
||||
ariaLabel="Keyspace id"
|
||||
autoFocus
|
||||
/>
|
||||
<datalist id="keyspacesList">
|
||||
{keyspaceIds?.map((id: string, index: number) => (
|
||||
<option key={index}>{id}</option>
|
||||
))}
|
||||
</datalist>
|
||||
{canConfigureThroughput && keyspaceCreateNew && (
|
||||
<div className="databaseProvision">
|
||||
<input
|
||||
tabIndex={0}
|
||||
type="checkbox"
|
||||
id="keyspaceSharedThroughput"
|
||||
title="Provision shared throughput"
|
||||
checked={keyspaceHasSharedOffer}
|
||||
onChange={(e) => setKeyspaceHasSharedOffer(e.target.checked)}
|
||||
/>
|
||||
<span className="databaseProvisionText" aria-label="Provision keyspace throughput">
|
||||
Provision keyspace throughput
|
||||
</span>
|
||||
<InfoTooltip>
|
||||
Provisioned throughput at the keyspace level will be shared across unlimited number of tables within the
|
||||
keyspace
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
)}
|
||||
{canConfigureThroughput && keyspaceCreateNew && keyspaceHasSharedOffer && (
|
||||
<div>
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !container.isFirstResourceCreated()}
|
||||
isDatabase
|
||||
isSharded
|
||||
setThroughputValue={(throughput: number) => setKeyspaceThroughput(throughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => setIsSharedAutoPilotSelected(isAutoscale)}
|
||||
onCostAcknowledgeChange={(isAcknowledge: boolean) => {
|
||||
setSharedThroughputSpendAck(isAcknowledge);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="seconddivpadding">
|
||||
<p>
|
||||
<Label required>
|
||||
Enter CQL command to create the table.
|
||||
<a href="https://aka.ms/cassandra-create-table" target="_blank" rel="noreferrer">
|
||||
Learn More
|
||||
</a>
|
||||
</Label>
|
||||
</p>
|
||||
<div aria-label={createTableQuery} style={{ float: "left", paddingTop: "3px", paddingRight: "3px" }}>
|
||||
{createTableQuery}
|
||||
</div>
|
||||
<TextField
|
||||
aria-required="true"
|
||||
ariaLabel="addCollection-tableId"
|
||||
autoComplete="off"
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||
placeholder="Enter tableId"
|
||||
size={20}
|
||||
className="textfontclr"
|
||||
value={tableId}
|
||||
onChange={(e, newValue) => setTableId(newValue)}
|
||||
style={{ marginBottom: "5px" }}
|
||||
/>
|
||||
<TextField
|
||||
multiline
|
||||
id="editor-area"
|
||||
rows={5}
|
||||
@@ -305,10 +386,10 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
value={userTableQuery}
|
||||
onChange={(e, newValue) => setUserTableQuery(newValue)}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
{!isServerlessAccount() && isKeyspaceShared && !keyspaceCreateNew && (
|
||||
<Stack>
|
||||
{canConfigureThroughput && keyspaceHasSharedOffer && !keyspaceCreateNew && (
|
||||
<div className="seconddivpadding">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="tableSharedThroughput"
|
||||
@@ -323,17 +404,21 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
does not count towards the throughput you provisioned for the keyspace. This throughput amount will be
|
||||
billed in addition to the throughput amount you provisioned at the keyspace level.
|
||||
</InfoTooltip>
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
{!isServerlessAccount() && (!isKeyspaceShared || dedicateTableThroughput) && (
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
|
||||
isDatabase={false}
|
||||
isSharded={false}
|
||||
setThroughputValue={(throughput: number) => (tableThroughput = throughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (isTableAutoscale = isAutoscale)}
|
||||
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
|
||||
/>
|
||||
{canConfigureThroughput && (!keyspaceHasSharedOffer || dedicateTableThroughput) && (
|
||||
<div>
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !container.isFirstResourceCreated()}
|
||||
isDatabase={false}
|
||||
isSharded={false}
|
||||
setThroughputValue={(throughput: number) => setThroughput(throughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => setIsAutoPilotSelected(isAutoscale)}
|
||||
onCostAcknowledgeChange={(isAcknowledge: boolean) => {
|
||||
setThroughputSpendAck(isAcknowledge);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</RightPaneForm>
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CassandraAddCollectionPane Pane should render Default properly 1`] = `
|
||||
<RightPaneForm
|
||||
formError=""
|
||||
onSubmit={[Function]}
|
||||
submitButtonText="Apply"
|
||||
>
|
||||
<div
|
||||
className="paneMainContent"
|
||||
>
|
||||
<div
|
||||
className="seconddivpadding"
|
||||
>
|
||||
<p>
|
||||
<StyledLabelBase
|
||||
required={true}
|
||||
>
|
||||
Keyspace name
|
||||
<InfoTooltip>
|
||||
Select an existing keyspace or enter a new keyspace id.
|
||||
</InfoTooltip>
|
||||
</StyledLabelBase>
|
||||
</p>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<input
|
||||
aria-label="Create new keyspace"
|
||||
checked={true}
|
||||
className="throughputInputRadioBtn"
|
||||
onChange={[Function]}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
type="radio"
|
||||
/>
|
||||
<span
|
||||
className="throughputInputRadioBtnLabel"
|
||||
>
|
||||
Create new
|
||||
</span>
|
||||
<input
|
||||
aria-label="Use existing keyspace"
|
||||
checked={false}
|
||||
className="throughputInputRadioBtn"
|
||||
onChange={[Function]}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
type="radio"
|
||||
/>
|
||||
<span
|
||||
className="throughputInputRadioBtnLabel"
|
||||
>
|
||||
Use existing
|
||||
</span>
|
||||
</Stack>
|
||||
<StyledTextFieldBase
|
||||
aria-required="true"
|
||||
ariaLabel="Keyspace id"
|
||||
autoComplete="off"
|
||||
autoFocus={true}
|
||||
data-test="addCollection-keyspaceId"
|
||||
list=""
|
||||
onChange={[Function]}
|
||||
pattern="[^/?#\\\\\\\\]*[^/?# \\\\\\\\]"
|
||||
placeholder="Type a new keyspace id"
|
||||
size={40}
|
||||
title="May not end with space nor contain characters '\\\\' '/' '#' '?'"
|
||||
value=""
|
||||
/>
|
||||
<datalist
|
||||
id="keyspacesList"
|
||||
/>
|
||||
<div
|
||||
className="databaseProvision"
|
||||
>
|
||||
<input
|
||||
checked={false}
|
||||
id="keyspaceSharedThroughput"
|
||||
onChange={[Function]}
|
||||
tabIndex={0}
|
||||
title="Provision shared throughput"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span
|
||||
aria-label="Provision keyspace throughput"
|
||||
className="databaseProvisionText"
|
||||
>
|
||||
Provision keyspace throughput
|
||||
</span>
|
||||
<InfoTooltip>
|
||||
Provisioned throughput at the keyspace level will be shared across unlimited number of tables within the keyspace
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="seconddivpadding"
|
||||
>
|
||||
<p>
|
||||
<StyledLabelBase
|
||||
required={true}
|
||||
>
|
||||
Enter CQL command to create the table.
|
||||
<a
|
||||
href="https://aka.ms/cassandra-create-table"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Learn More
|
||||
</a>
|
||||
</StyledLabelBase>
|
||||
</p>
|
||||
<div
|
||||
aria-label="CREATE TABLE "
|
||||
style={
|
||||
Object {
|
||||
"float": "left",
|
||||
"paddingRight": "3px",
|
||||
"paddingTop": "3px",
|
||||
}
|
||||
}
|
||||
>
|
||||
CREATE TABLE
|
||||
</div>
|
||||
<StyledTextFieldBase
|
||||
aria-required="true"
|
||||
ariaLabel="addCollection-tableId"
|
||||
autoComplete="off"
|
||||
className="textfontclr"
|
||||
onChange={[Function]}
|
||||
pattern="[^/?#\\\\\\\\]*[^/?# \\\\\\\\]"
|
||||
placeholder="Enter tableId"
|
||||
size={20}
|
||||
style={
|
||||
Object {
|
||||
"marginBottom": "5px",
|
||||
}
|
||||
}
|
||||
title="May not end with space nor contain characters '\\\\' '/' '#' '?'"
|
||||
value=""
|
||||
/>
|
||||
<StyledTextFieldBase
|
||||
aria-label="Table Schema"
|
||||
id="editor-area"
|
||||
multiline={true}
|
||||
onChange={[Function]}
|
||||
rows={5}
|
||||
value="(userid int, name text, email text, PRIMARY KEY (userid))"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ThroughputInput
|
||||
isDatabase={false}
|
||||
isSharded={false}
|
||||
onCostAcknowledgeChange={[Function]}
|
||||
setIsAutoscale={[Function]}
|
||||
setThroughputValue={[Function]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</RightPaneForm>
|
||||
`;
|
||||
@@ -1,53 +1,54 @@
|
||||
jest.mock("../../../Common/dataAccess/deleteCollection");
|
||||
jest.mock("../../../Shared/Telemetry/TelemetryProcessor");
|
||||
import { mount, shallow } from "enzyme";
|
||||
import { mount, ReactWrapper, shallow } from "enzyme";
|
||||
import * as ko from "knockout";
|
||||
import React from "react";
|
||||
import { deleteCollection } from "../../../Common/dataAccess/deleteCollection";
|
||||
import DeleteFeedback from "../../../Common/DeleteFeedback";
|
||||
import { ApiKind, DatabaseAccount } from "../../../Contracts/DataModels";
|
||||
import { Collection, Database } from "../../../Contracts/ViewModels";
|
||||
import { Collection, Database, TreeNode } from "../../../Contracts/ViewModels";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { updateUserContext } from "../../../UserContext";
|
||||
import Explorer from "../../Explorer";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
import { DeleteCollectionConfirmationPane } from "./DeleteCollectionConfirmationPane";
|
||||
|
||||
describe("Delete Collection Confirmation Pane", () => {
|
||||
describe("useDatabases.isLastCollection()", () => {
|
||||
beforeAll(() => useDatabases.getState().clearDatabases());
|
||||
afterEach(() => useDatabases.getState().clearDatabases());
|
||||
describe("Explorer.isLastCollection()", () => {
|
||||
let explorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer();
|
||||
});
|
||||
|
||||
it("should be true if 1 database and 1 collection", () => {
|
||||
const database = { id: ko.observable("testDB") } as Database;
|
||||
database.collections = ko.observableArray<Collection>([{ id: ko.observable("testCollection") } as Collection]);
|
||||
useDatabases.getState().addDatabases([database]);
|
||||
expect(useDatabases.getState().isLastCollection()).toBe(true);
|
||||
const database = {} as Database;
|
||||
database.collections = ko.observableArray<Collection>([{} as Collection]);
|
||||
explorer.databases = ko.observableArray<Database>([database]);
|
||||
expect(explorer.isLastCollection()).toBe(true);
|
||||
});
|
||||
|
||||
it("should be false if if 1 database and 2 collection", () => {
|
||||
const database = { id: ko.observable("testDB") } as Database;
|
||||
database.collections = ko.observableArray<Collection>([
|
||||
{ id: ko.observable("coll1") } as Collection,
|
||||
{ id: ko.observable("coll2") } as Collection,
|
||||
]);
|
||||
useDatabases.getState().addDatabases([database]);
|
||||
expect(useDatabases.getState().isLastCollection()).toBe(false);
|
||||
const database = {} as Database;
|
||||
database.collections = ko.observableArray<Collection>([{} as Collection, {} as Collection]);
|
||||
explorer.databases = ko.observableArray<Database>([database]);
|
||||
expect(explorer.isLastCollection()).toBe(false);
|
||||
});
|
||||
|
||||
it("should be false if 2 database and 1 collection each", () => {
|
||||
const database = { id: ko.observable("testDB") } as Database;
|
||||
database.collections = ko.observableArray<Collection>([{ id: ko.observable("coll1") } as Collection]);
|
||||
const database2 = { id: ko.observable("testDB2") } as Database;
|
||||
database2.collections = ko.observableArray<Collection>([{ id: ko.observable("coll2") } as Collection]);
|
||||
useDatabases.getState().addDatabases([database, database2]);
|
||||
expect(useDatabases.getState().isLastCollection()).toBe(false);
|
||||
const database = {} as Database;
|
||||
database.collections = ko.observableArray<Collection>([{} as Collection]);
|
||||
const database2 = {} as Database;
|
||||
database2.collections = ko.observableArray<Collection>([{} as Collection]);
|
||||
explorer.databases = ko.observableArray<Database>([database, database2]);
|
||||
expect(explorer.isLastCollection()).toBe(false);
|
||||
});
|
||||
|
||||
it("should be false if 0 databases", () => {
|
||||
expect(useDatabases.getState().isLastCollection()).toBe(false);
|
||||
const database = {} as Database;
|
||||
explorer.databases = ko.observableArray<Database>();
|
||||
database.collections = ko.observableArray<Collection>();
|
||||
expect(explorer.isLastCollection()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,39 +56,46 @@ describe("Delete Collection Confirmation Pane", () => {
|
||||
it("should return true if last collection and database does not have shared throughput else false", () => {
|
||||
const fakeExplorer = new Explorer();
|
||||
fakeExplorer.refreshAllDatabases = () => undefined;
|
||||
fakeExplorer.isLastCollection = () => true;
|
||||
fakeExplorer.isSelectedDatabaseShared = () => false;
|
||||
|
||||
const wrapper = shallow(<DeleteCollectionConfirmationPane explorer={fakeExplorer} />);
|
||||
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
|
||||
|
||||
const database = { id: ko.observable("testDB") } as Database;
|
||||
database.collections = ko.observableArray<Collection>([{ id: ko.observable("testCollection") } as Collection]);
|
||||
database.nodeKind = "Database";
|
||||
database.isDatabaseShared = ko.computed(() => false);
|
||||
useDatabases.getState().addDatabases([database]);
|
||||
useSelectedNode.getState().setSelectedNode(database);
|
||||
wrapper.setProps({ explorer: fakeExplorer });
|
||||
const props = {
|
||||
explorer: fakeExplorer,
|
||||
closePanel: (): void => undefined,
|
||||
collectionName: "container",
|
||||
};
|
||||
const wrapper = shallow(<DeleteCollectionConfirmationPane {...props} />);
|
||||
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true);
|
||||
|
||||
database.isDatabaseShared = ko.computed(() => true);
|
||||
wrapper.setProps({ explorer: fakeExplorer });
|
||||
props.explorer.isLastCollection = () => true;
|
||||
props.explorer.isSelectedDatabaseShared = () => true;
|
||||
wrapper.setProps(props);
|
||||
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
|
||||
|
||||
props.explorer.isLastCollection = () => false;
|
||||
props.explorer.isSelectedDatabaseShared = () => false;
|
||||
wrapper.setProps(props);
|
||||
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("submit()", () => {
|
||||
let wrapper: ReactWrapper;
|
||||
const selectedCollectionId = "testCol";
|
||||
const databaseId = "testDatabase";
|
||||
const fakeExplorer = {} as Explorer;
|
||||
fakeExplorer.findSelectedCollection = () => {
|
||||
return {
|
||||
id: ko.observable<string>(selectedCollectionId),
|
||||
databaseId,
|
||||
rid: "test",
|
||||
} as Collection;
|
||||
};
|
||||
fakeExplorer.selectedCollectionId = ko.computed<string>(() => selectedCollectionId);
|
||||
fakeExplorer.selectedNode = ko.observable<TreeNode>();
|
||||
fakeExplorer.refreshAllDatabases = () => undefined;
|
||||
const database = { id: ko.observable(databaseId) } as Database;
|
||||
const collection = {
|
||||
id: ko.observable(selectedCollectionId),
|
||||
nodeKind: "Collection",
|
||||
database,
|
||||
databaseId,
|
||||
} as Collection;
|
||||
database.collections = ko.observableArray<Collection>([collection]);
|
||||
database.isDatabaseShared = ko.computed(() => false);
|
||||
fakeExplorer.isLastCollection = () => true;
|
||||
fakeExplorer.isSelectedDatabaseShared = () => false;
|
||||
|
||||
beforeAll(() => {
|
||||
updateUserContext({
|
||||
@@ -105,17 +113,15 @@ describe("Delete Collection Confirmation Pane", () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
useDatabases.getState().addDatabases([database]);
|
||||
useSelectedNode.getState().setSelectedNode(collection);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
useDatabases.getState().clearDatabases();
|
||||
useSelectedNode.getState().setSelectedNode(undefined);
|
||||
const props = {
|
||||
explorer: fakeExplorer,
|
||||
closePanel: (): void => undefined,
|
||||
collectionName: "container",
|
||||
};
|
||||
wrapper = mount(<DeleteCollectionConfirmationPane {...props} />);
|
||||
});
|
||||
|
||||
it("should call delete collection", () => {
|
||||
const wrapper = mount(<DeleteCollectionConfirmationPane explorer={fakeExplorer} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
|
||||
@@ -132,7 +138,6 @@ describe("Delete Collection Confirmation Pane", () => {
|
||||
});
|
||||
|
||||
it("should record feedback", async () => {
|
||||
const wrapper = mount(<DeleteCollectionConfirmationPane explorer={fakeExplorer} />);
|
||||
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
|
||||
wrapper
|
||||
.find("#confirmCollectionId")
|
||||
|
||||
@@ -13,10 +13,7 @@ import { userContext } from "../../../UserContext";
|
||||
import { getCollectionName } from "../../../Utils/APITypeUtils";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
|
||||
export interface DeleteCollectionConfirmationPaneProps {
|
||||
explorer: Explorer;
|
||||
}
|
||||
@@ -30,14 +27,13 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
const [formError, setFormError] = useState<string>("");
|
||||
const [isExecuting, setIsExecuting] = useState(false);
|
||||
|
||||
const shouldRecordFeedback = (): boolean =>
|
||||
useDatabases.getState().isLastCollection() &&
|
||||
!useSelectedNode.getState().findSelectedDatabase()?.isDatabaseShared();
|
||||
|
||||
const shouldRecordFeedback = (): boolean => {
|
||||
return explorer.isLastCollection() && !explorer.isSelectedDatabaseShared();
|
||||
};
|
||||
const collectionName = getCollectionName().toLocaleLowerCase();
|
||||
const paneTitle = "Delete " + collectionName;
|
||||
const onSubmit = async (): Promise<void> => {
|
||||
const collection = useSelectedNode.getState().findSelectedCollection();
|
||||
const collection = explorer.findSelectedCollection();
|
||||
if (!collection || inputCollectionName !== collection.id()) {
|
||||
const errorMessage = "Input " + collectionName + " name does not match the selected " + collectionName;
|
||||
setFormError(errorMessage);
|
||||
@@ -62,7 +58,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
await deleteCollection(collection.databaseId, collection.id());
|
||||
|
||||
setIsExecuting(false);
|
||||
useSelectedNode.getState().setSelectedNode(collection.database);
|
||||
explorer.selectedNode(collection.database);
|
||||
explorer.tabsManager?.closeTabsByComparator(
|
||||
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
|
||||
);
|
||||
|
||||
@@ -2,9 +2,16 @@
|
||||
|
||||
exports[`Delete Collection Confirmation Pane submit() should call delete collection 1`] = `
|
||||
<DeleteCollectionConfirmationPane
|
||||
closePanel={[Function]}
|
||||
collectionName="container"
|
||||
explorer={
|
||||
Object {
|
||||
"findSelectedCollection": [Function],
|
||||
"isLastCollection": [Function],
|
||||
"isSelectedDatabaseShared": [Function],
|
||||
"refreshAllDatabases": [Function],
|
||||
"selectedCollectionId": [Function],
|
||||
"selectedNode": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -36,7 +43,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
variant="small"
|
||||
>
|
||||
<span
|
||||
className="css-53"
|
||||
className="css-102"
|
||||
>
|
||||
Confirm by typing the
|
||||
container
|
||||
@@ -340,18 +347,18 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
value=""
|
||||
>
|
||||
<div
|
||||
className="ms-TextField root-55"
|
||||
className="ms-TextField root-104"
|
||||
>
|
||||
<div
|
||||
className="ms-TextField-wrapper"
|
||||
>
|
||||
<div
|
||||
className="ms-TextField-fieldGroup fieldGroup-56"
|
||||
className="ms-TextField-fieldGroup fieldGroup-105"
|
||||
>
|
||||
<input
|
||||
aria-invalid={false}
|
||||
autoFocus={true}
|
||||
className="ms-TextField-field field-57"
|
||||
className="ms-TextField-field field-106"
|
||||
id="confirmCollectionId"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
@@ -366,6 +373,355 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
</TextFieldBase>
|
||||
</StyledTextFieldBase>
|
||||
</div>
|
||||
<div
|
||||
className="deleteCollectionFeedback"
|
||||
>
|
||||
<Text
|
||||
block={true}
|
||||
variant="small"
|
||||
>
|
||||
<span
|
||||
className="css-115"
|
||||
>
|
||||
Help us improve Azure Cosmos DB!
|
||||
</span>
|
||||
</Text>
|
||||
<Text
|
||||
block={true}
|
||||
variant="small"
|
||||
>
|
||||
<span
|
||||
className="css-115"
|
||||
>
|
||||
What is the reason why you are deleting this
|
||||
container
|
||||
?
|
||||
</span>
|
||||
</Text>
|
||||
<StyledTextFieldBase
|
||||
id="deleteCollectionFeedbackInput"
|
||||
multiline={true}
|
||||
onChange={[Function]}
|
||||
rows={3}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"width": 300,
|
||||
},
|
||||
}
|
||||
}
|
||||
value=""
|
||||
>
|
||||
<TextFieldBase
|
||||
deferredValidationTime={200}
|
||||
id="deleteCollectionFeedbackInput"
|
||||
multiline={true}
|
||||
onChange={[Function]}
|
||||
resizable={true}
|
||||
rows={3}
|
||||
styles={[Function]}
|
||||
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",
|
||||
},
|
||||
}
|
||||
}
|
||||
validateOnLoad={true}
|
||||
value=""
|
||||
>
|
||||
<div
|
||||
className="ms-TextField ms-TextField--multiline root-104"
|
||||
>
|
||||
<div
|
||||
className="ms-TextField-wrapper"
|
||||
>
|
||||
<div
|
||||
className="ms-TextField-fieldGroup fieldGroup-116"
|
||||
>
|
||||
<textarea
|
||||
aria-invalid={false}
|
||||
className="ms-TextField-field field-117"
|
||||
id="deleteCollectionFeedbackInput"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onInput={[Function]}
|
||||
rows={3}
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TextFieldBase>
|
||||
</StyledTextFieldBase>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PanelFooterComponent
|
||||
@@ -2078,7 +2434,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
>
|
||||
<button
|
||||
aria-label="OK"
|
||||
className="ms-Button ms-Button--primary root-66"
|
||||
className="ms-Button ms-Button--primary root-119"
|
||||
data-is-focusable={true}
|
||||
id="sidePanelOkButton"
|
||||
onClick={[Function]}
|
||||
@@ -2090,16 +2446,16 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
className="ms-Button-flexContainer flexContainer-67"
|
||||
className="ms-Button-flexContainer flexContainer-120"
|
||||
data-automationid="splitbuttonprimary"
|
||||
>
|
||||
<span
|
||||
className="ms-Button-textContainer textContainer-68"
|
||||
className="ms-Button-textContainer textContainer-121"
|
||||
>
|
||||
<span
|
||||
className="ms-Button-label label-70"
|
||||
id="id__3"
|
||||
key="id__3"
|
||||
className="ms-Button-label label-123"
|
||||
id="id__6"
|
||||
key="id__6"
|
||||
>
|
||||
OK
|
||||
</span>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
jest.mock("../../Common/dataAccess/deleteDatabase");
|
||||
jest.mock("../../Shared/Telemetry/TelemetryProcessor");
|
||||
import { mount, shallow } from "enzyme";
|
||||
import { mount, ReactWrapper, shallow } from "enzyme";
|
||||
import * as ko from "knockout";
|
||||
import React from "react";
|
||||
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
|
||||
@@ -11,104 +11,128 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { updateUserContext } from "../../UserContext";
|
||||
import Explorer from "../Explorer";
|
||||
import { TabsManager } from "../Tabs/TabsManager";
|
||||
import { useDatabases } from "../useDatabases";
|
||||
import { useSelectedNode } from "../useSelectedNode";
|
||||
import { DeleteDatabaseConfirmationPanel } from "./DeleteDatabaseConfirmationPanel";
|
||||
|
||||
describe("Delete Database Confirmation Pane", () => {
|
||||
const selectedDatabaseId = "testDatabase";
|
||||
let fakeExplorer: Explorer;
|
||||
let database: Database;
|
||||
describe("shouldRecordFeedback()", () => {
|
||||
it("should return true if last non empty database or is last database that has shared throughput, else false", () => {
|
||||
const fakeExplorer = new Explorer();
|
||||
fakeExplorer.refreshAllDatabases = () => undefined;
|
||||
fakeExplorer.isLastCollection = () => true;
|
||||
fakeExplorer.isSelectedDatabaseShared = () => false;
|
||||
|
||||
beforeAll(() => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
name: "testDatabaseAccountName",
|
||||
properties: {
|
||||
cassandraEndpoint: "testEndpoint",
|
||||
},
|
||||
id: "testDatabaseAccountId",
|
||||
} as DatabaseAccount,
|
||||
apiType: "SQL",
|
||||
const database = {} as Database;
|
||||
database.collections = ko.observableArray<Collection>([{} as Collection]);
|
||||
database.id = ko.observable<string>("testDatabse");
|
||||
|
||||
const props = {
|
||||
explorer: fakeExplorer,
|
||||
closePanel: (): void => undefined,
|
||||
openNotificationConsole: (): void => undefined,
|
||||
selectedDatabase: database,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<DeleteDatabaseConfirmationPanel {...props} />);
|
||||
props.explorer.isLastNonEmptyDatabase = () => true;
|
||||
wrapper.setProps(props);
|
||||
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(true);
|
||||
|
||||
props.explorer.isLastNonEmptyDatabase = () => false;
|
||||
props.explorer.isLastDatabase = () => false;
|
||||
wrapper.setProps(props);
|
||||
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false);
|
||||
|
||||
props.explorer.isLastNonEmptyDatabase = () => false;
|
||||
props.explorer.isLastDatabase = () => true;
|
||||
props.explorer.isSelectedDatabaseShared = () => false;
|
||||
wrapper.setProps(props);
|
||||
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false);
|
||||
});
|
||||
(deleteDatabase as jest.Mock).mockResolvedValue(undefined);
|
||||
(TelemetryProcessor.trace as jest.Mock).mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fakeExplorer = {} as Explorer;
|
||||
describe("submit()", () => {
|
||||
const selectedDatabaseId = "testDatabse";
|
||||
const fakeExplorer = new Explorer();
|
||||
fakeExplorer.refreshAllDatabases = () => undefined;
|
||||
fakeExplorer.tabsManager = new TabsManager();
|
||||
fakeExplorer.isLastCollection = () => true;
|
||||
fakeExplorer.isSelectedDatabaseShared = () => false;
|
||||
|
||||
database = {} as Database;
|
||||
database.collections = ko.observableArray<Collection>([{ id: ko.observable("testCollection") } as Collection]);
|
||||
database.id = ko.observable<string>(selectedDatabaseId);
|
||||
database.nodeKind = "Database";
|
||||
|
||||
useDatabases.getState().addDatabases([database]);
|
||||
useSelectedNode.getState().setSelectedNode(database);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
useDatabases.getState().clearDatabases();
|
||||
useSelectedNode.getState().setSelectedNode(undefined);
|
||||
});
|
||||
|
||||
it("shouldRecordFeedback() should return true if last non empty database or is last database that has shared throughput", () => {
|
||||
const wrapper = shallow(<DeleteDatabaseConfirmationPanel explorer={fakeExplorer} />);
|
||||
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(true);
|
||||
|
||||
useDatabases.getState().addDatabases([database]);
|
||||
wrapper.setProps({ explorer: fakeExplorer });
|
||||
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false);
|
||||
useDatabases.getState().clearDatabases();
|
||||
});
|
||||
|
||||
it("Should call delete database", () => {
|
||||
const wrapper = mount(<DeleteDatabaseConfirmationPanel explorer={fakeExplorer} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
|
||||
|
||||
wrapper
|
||||
.find("#confirmDatabaseId")
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: selectedDatabaseId } });
|
||||
expect(wrapper.exists("button")).toBe(true);
|
||||
wrapper.find("button").hostNodes().simulate("submit");
|
||||
expect(deleteDatabase).toHaveBeenCalledWith(selectedDatabaseId);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it("should record feedback", async () => {
|
||||
const wrapper = mount(<DeleteDatabaseConfirmationPanel explorer={fakeExplorer} />);
|
||||
expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
|
||||
wrapper
|
||||
.find("#confirmDatabaseId")
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: selectedDatabaseId } });
|
||||
|
||||
expect(wrapper.exists("#deleteDatabaseFeedbackInput")).toBe(true);
|
||||
const feedbackText = "Test delete Database feedback text";
|
||||
wrapper
|
||||
.find("#deleteDatabaseFeedbackInput")
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: feedbackText } });
|
||||
|
||||
expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
|
||||
wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit");
|
||||
expect(deleteDatabase).toHaveBeenCalledWith(selectedDatabaseId);
|
||||
|
||||
const deleteFeedback = new DeleteFeedback(
|
||||
"testDatabaseAccountId",
|
||||
"testDatabaseAccountName",
|
||||
ApiKind.SQL,
|
||||
feedbackText
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(TelemetryProcessor.trace).toHaveBeenCalledWith(Action.DeleteDatabase, ActionModifiers.Mark, {
|
||||
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
|
||||
let wrapper: ReactWrapper;
|
||||
beforeAll(() => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
name: "testDatabaseAccountName",
|
||||
properties: {
|
||||
cassandraEndpoint: "testEndpoint",
|
||||
},
|
||||
id: "testDatabaseAccountId",
|
||||
} as DatabaseAccount,
|
||||
apiType: "SQL",
|
||||
});
|
||||
(deleteDatabase as jest.Mock).mockResolvedValue(undefined);
|
||||
(TelemetryProcessor.trace as jest.Mock).mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const database = {} as Database;
|
||||
database.collections = ko.observableArray<Collection>([{} as Collection]);
|
||||
database.id = ko.observable<string>(selectedDatabaseId);
|
||||
|
||||
const props = {
|
||||
explorer: fakeExplorer,
|
||||
closePanel: (): void => undefined,
|
||||
openNotificationConsole: (): void => undefined,
|
||||
selectedDatabase: database,
|
||||
};
|
||||
|
||||
wrapper = mount(<DeleteDatabaseConfirmationPanel {...props} />);
|
||||
props.explorer.isLastNonEmptyDatabase = () => true;
|
||||
wrapper.setProps(props);
|
||||
});
|
||||
|
||||
it("Should call delete database", () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
|
||||
|
||||
wrapper
|
||||
.find("#confirmDatabaseId")
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: selectedDatabaseId } });
|
||||
expect(wrapper.exists("button")).toBe(true);
|
||||
wrapper.find("button").hostNodes().simulate("submit");
|
||||
expect(deleteDatabase).toHaveBeenCalledWith(selectedDatabaseId);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it("should record feedback", async () => {
|
||||
expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
|
||||
wrapper
|
||||
.find("#confirmDatabaseId")
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: selectedDatabaseId } });
|
||||
|
||||
expect(wrapper.exists("#deleteDatabaseFeedbackInput")).toBe(true);
|
||||
const feedbackText = "Test delete Database feedback text";
|
||||
wrapper
|
||||
.find("#deleteDatabaseFeedbackInput")
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: feedbackText } });
|
||||
|
||||
expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
|
||||
wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit");
|
||||
expect(deleteDatabase).toHaveBeenCalledWith(selectedDatabaseId);
|
||||
|
||||
const deleteFeedback = new DeleteFeedback(
|
||||
"testDatabaseAccountId",
|
||||
"testDatabaseAccountName",
|
||||
ApiKind.SQL,
|
||||
feedbackText
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(TelemetryProcessor.trace).toHaveBeenCalledWith(Action.DeleteDatabase, ActionModifiers.Mark, {
|
||||
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
|
||||
});
|
||||
wrapper.unmount();
|
||||
});
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,26 +13,24 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../Explorer";
|
||||
import { useDatabases } from "../useDatabases";
|
||||
import { useSelectedNode } from "../useSelectedNode";
|
||||
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent";
|
||||
import { RightPaneForm, RightPaneFormProps } from "./RightPaneForm/RightPaneForm";
|
||||
|
||||
interface DeleteDatabaseConfirmationPanelProps {
|
||||
explorer: Explorer;
|
||||
selectedDatabase: Database;
|
||||
}
|
||||
|
||||
export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseConfirmationPanelProps> = ({
|
||||
explorer,
|
||||
selectedDatabase,
|
||||
}: DeleteDatabaseConfirmationPanelProps): JSX.Element => {
|
||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||
const isLastNonEmptyDatabase = useDatabases((state) => state.isLastNonEmptyDatabase);
|
||||
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
|
||||
|
||||
const [formError, setFormError] = useState<string>("");
|
||||
const [databaseInput, setDatabaseInput] = useState<string>("");
|
||||
const [databaseFeedbackInput, setDatabaseFeedbackInput] = useState<string>("");
|
||||
const selectedDatabase: Database = useSelectedNode.getState().findSelectedDatabase();
|
||||
|
||||
const submit = async (): Promise<void> => {
|
||||
if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) {
|
||||
@@ -54,7 +52,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
closeSidePanel();
|
||||
explorer.refreshAllDatabases();
|
||||
explorer.tabsManager.closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id());
|
||||
useSelectedNode.getState().setSelectedNode(undefined);
|
||||
explorer.selectedNode(undefined);
|
||||
selectedDatabase
|
||||
.collections()
|
||||
.forEach((collection: Collection) =>
|
||||
@@ -72,7 +70,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
startKey
|
||||
);
|
||||
|
||||
if (isLastNonEmptyDatabase()) {
|
||||
if (shouldRecordFeedback()) {
|
||||
const deleteFeedback = new DeleteFeedback(
|
||||
userContext?.databaseAccount.id,
|
||||
userContext?.databaseAccount.name,
|
||||
@@ -102,6 +100,10 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
}
|
||||
};
|
||||
|
||||
const shouldRecordFeedback = (): boolean => {
|
||||
return explorer.isLastNonEmptyDatabase() || (explorer.isLastDatabase() && explorer.isSelectedDatabaseShared());
|
||||
};
|
||||
|
||||
const props: RightPaneFormProps = {
|
||||
formError,
|
||||
isExecuting: isLoading,
|
||||
@@ -132,7 +134,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{isLastNonEmptyDatabase() && (
|
||||
{shouldRecordFeedback() && (
|
||||
<div className="deleteDatabaseFeedback">
|
||||
<Text variant="small" block>
|
||||
Help us improve Azure Cosmos DB!
|
||||
|
||||
@@ -19,11 +19,23 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
|
||||
"container": Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"canSaveQueries": [Function],
|
||||
"collapsedResourceTreeWidth": 36,
|
||||
"commandBarComponentAdapter": CommandBarComponentAdapter {
|
||||
"container": [Circular],
|
||||
"isNotebookTabActive": [Function],
|
||||
"parameters": [Function],
|
||||
"tabsButtons": Array [],
|
||||
},
|
||||
"databases": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isHostedDataExplorerEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isResourceTokenCollectionNodeSelected": [Function],
|
||||
"isSchemaEnabled": [Function],
|
||||
"isServerlessEnabled": [Function],
|
||||
"isShellEnabled": [Function],
|
||||
"isSynapseLinkUpdating": [Function],
|
||||
"isTabsContentExpanded": [Function],
|
||||
@@ -38,16 +50,38 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
|
||||
},
|
||||
"refreshNotebookList": [Function],
|
||||
"resourceTokenCollection": [Function],
|
||||
"resourceTokenCollectionId": [Function],
|
||||
"resourceTokenDatabaseId": [Function],
|
||||
"resourceTokenPartitionKey": [Function],
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"databaseCollectionIdMap": Map {},
|
||||
"koSubsCollectionIdMap": Map {},
|
||||
"koSubsDatabaseIdMap": Map {},
|
||||
"parameters": [Function],
|
||||
},
|
||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||
"container": [Circular],
|
||||
"parameters": [Function],
|
||||
},
|
||||
"selectedDatabaseId": [Function],
|
||||
"selectedNode": [Function],
|
||||
"setInProgressConsoleDataIdToBeDeleted": undefined,
|
||||
"setNotificationConsoleData": undefined,
|
||||
"sparkClusterConnectionInfo": [Function],
|
||||
"splitter": Splitter {
|
||||
"bounds": Object {
|
||||
"max": 400,
|
||||
"min": 240,
|
||||
},
|
||||
"direction": "vertical",
|
||||
"isCollapsed": [Function],
|
||||
"leftSideId": "resourcetree",
|
||||
"onResizeStart": [Function],
|
||||
"onResizeStop": [Function],
|
||||
"splitterId": "h_splitter1",
|
||||
},
|
||||
"tabsManager": TabsManager {
|
||||
"activeTab": [Function],
|
||||
"openedTabs": [Function],
|
||||
|
||||
@@ -19,8 +19,7 @@ export const GraphStylingPanel: FunctionComponent<GraphStylingProps> = ({
|
||||
|
||||
const buttonLabel = "Ok";
|
||||
|
||||
const submit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const submit = () => {
|
||||
closeSidePanel();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import Explorer from "../../Explorer";
|
||||
import { LoadQueryPane } from "./LoadQueryPane";
|
||||
|
||||
describe("Load Query Pane", () => {
|
||||
it("should render Default properly", () => {
|
||||
const wrapper = shallow(<LoadQueryPane />);
|
||||
const fakeExplorer = {} as Explorer;
|
||||
const props = {
|
||||
explorer: fakeExplorer,
|
||||
closePanel: (): void => undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<LoadQueryPane {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,10 +7,15 @@ import { Collection } from "../../../Contracts/ViewModels";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
import Explorer from "../../Explorer";
|
||||
import QueryTab from "../../Tabs/QueryTab";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
|
||||
export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
|
||||
interface LoadQueryPaneProps {
|
||||
explorer: Explorer;
|
||||
}
|
||||
|
||||
export const LoadQueryPane: FunctionComponent<LoadQueryPaneProps> = ({ explorer }: LoadQueryPaneProps): JSX.Element => {
|
||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
|
||||
const [formError, setFormError] = useState<string>("");
|
||||
@@ -54,20 +59,21 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
|
||||
};
|
||||
|
||||
const loadQueryFromFile = async (file: File): Promise<void> => {
|
||||
const selectedCollection: Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
const selectedCollection: Collection = explorer?.findSelectedCollection();
|
||||
if (!selectedCollection) {
|
||||
logError("No collection was selected", "LoadQueryPane.loadQueryFromFile");
|
||||
} else if (userContext.apiType === "Mongo") {
|
||||
selectedCollection.onNewMongoQueryClick(selectedCollection, undefined);
|
||||
} else {
|
||||
selectedCollection.onNewQueryClick(selectedCollection, undefined);
|
||||
}
|
||||
const reader = new FileReader();
|
||||
let fileData: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
reader.onload = (evt: any): void => {
|
||||
fileData = evt.target.result;
|
||||
|
||||
if (!selectedCollection) {
|
||||
logError("No collection was selected", "LoadQueryPane.loadQueryFromFile");
|
||||
} else if (userContext.apiType === "Mongo") {
|
||||
selectedCollection.onNewMongoQueryClick(selectedCollection, undefined);
|
||||
} else {
|
||||
selectedCollection.onNewQueryClick(selectedCollection, undefined, fileData);
|
||||
}
|
||||
const fileData: string = evt.target.result;
|
||||
const queryTab = explorer.tabsManager.activeTab() as QueryTab;
|
||||
queryTab.initialEditorContent(fileData);
|
||||
queryTab.sqlQueryEditorContent(fileData);
|
||||
};
|
||||
|
||||
reader.onerror = (): void => {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { ITextFieldStyles } from "@fluentui/react";
|
||||
|
||||
interface TextFieldStylesProps {
|
||||
fontSize: number | string;
|
||||
width: number | string;
|
||||
}
|
||||
|
||||
export const getTextFieldStyles = (params?: TextFieldStylesProps): Partial<ITextFieldStyles> => ({
|
||||
field: {
|
||||
fontSize: params?.fontSize || 12,
|
||||
selectors: {
|
||||
"::placeholder": {
|
||||
fontSize: params?.fontSize || 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
root: {
|
||||
width: params?.width || 300,
|
||||
},
|
||||
});
|
||||
@@ -1,38 +1,32 @@
|
||||
import { shallow } from "enzyme";
|
||||
import * as ko from "knockout";
|
||||
import React from "react";
|
||||
import { SavedQueries } from "../../../Common/Constants";
|
||||
import { Collection, Database } from "../../../Contracts/ViewModels";
|
||||
import Explorer from "../../Explorer";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
import { SaveQueryPane } from "./SaveQueryPane";
|
||||
|
||||
describe("Save Query Pane", () => {
|
||||
const fakeExplorer = {} as Explorer;
|
||||
fakeExplorer.canSaveQueries = ko.computed<boolean>(() => true);
|
||||
|
||||
const props = {
|
||||
explorer: fakeExplorer,
|
||||
closePanel: (): void => undefined,
|
||||
};
|
||||
|
||||
it("should render Default properly", () => {
|
||||
const wrapper = shallow(<SaveQueryPane {...props} />);
|
||||
expect(wrapper.exists("#saveQueryInput")).toBe(false);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
const wrapper = shallow(<SaveQueryPane {...props} />);
|
||||
|
||||
it("should return true if can save Queries else false", () => {
|
||||
useDatabases.getState().addDatabases([
|
||||
{
|
||||
id: ko.observable(SavedQueries.DatabaseName),
|
||||
collections: ko.observableArray([
|
||||
{
|
||||
id: ko.observable(SavedQueries.CollectionName),
|
||||
} as Collection,
|
||||
]),
|
||||
} as Database,
|
||||
]);
|
||||
const wrapper = shallow(<SaveQueryPane {...props} />);
|
||||
fakeExplorer.canSaveQueries = ko.computed<boolean>(() => true);
|
||||
wrapper.setProps(props);
|
||||
expect(wrapper.exists("#saveQueryInput")).toBe(true);
|
||||
|
||||
fakeExplorer.canSaveQueries = ko.computed<boolean>(() => false);
|
||||
wrapper.setProps(props);
|
||||
expect(wrapper.exists("#saveQueryInput")).toBe(false);
|
||||
});
|
||||
|
||||
it("should render Default properly", () => {
|
||||
const wrapper = shallow(<SaveQueryPane {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,8 +9,7 @@ import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import { NewQueryTab } from "../../Tabs/QueryTab/QueryTab";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
import QueryTab from "../../Tabs/QueryTab";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
|
||||
interface SaveQueryPaneProps {
|
||||
@@ -25,18 +24,17 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({ explorer
|
||||
|
||||
const setupSaveQueriesText = `For compliance reasons, we save queries in a container in your Azure Cosmos account, in a separate database called “${SavedQueries.DatabaseName}”. To proceed, we need to create a container in your account, estimated additional cost is $0.77 daily.`;
|
||||
const title = "Save Query";
|
||||
const isSaveQueryEnabled = useDatabases((state) => state.isSaveQueryEnabled);
|
||||
const { canSaveQueries } = explorer;
|
||||
|
||||
const submit = async (): Promise<void> => {
|
||||
setFormError("");
|
||||
if (!isSaveQueryEnabled()) {
|
||||
if (!canSaveQueries()) {
|
||||
setFormError("Cannot save query");
|
||||
logConsoleError("Failed to save query: account not setup to save queries");
|
||||
}
|
||||
|
||||
const queryTab = explorer && (explorer.tabsManager.activeTab() as NewQueryTab);
|
||||
const query: string = queryTab && queryTab.iTabAccessor.onSaveClickEvent();
|
||||
|
||||
const queryTab = explorer && (explorer.tabsManager.activeTab() as QueryTab);
|
||||
const query: string = queryTab && queryTab.sqlQueryEditorContent();
|
||||
if (!queryName || queryName.length === 0) {
|
||||
setFormError("No query name specified");
|
||||
logConsoleError("Could not save query -- No query name specified. Please specify a query name.");
|
||||
@@ -130,16 +128,16 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({ explorer
|
||||
const props: RightPaneFormProps = {
|
||||
formError: formError,
|
||||
isExecuting: isLoading,
|
||||
submitButtonText: isSaveQueryEnabled() ? "Save" : "Complete setup",
|
||||
submitButtonText: canSaveQueries() ? "Save" : "Complete setup",
|
||||
onSubmit: () => {
|
||||
isSaveQueryEnabled() ? submit() : setupQueries();
|
||||
canSaveQueries() ? submit() : setupQueries();
|
||||
},
|
||||
};
|
||||
return (
|
||||
<RightPaneForm {...props}>
|
||||
<div className="panelFormWrapper">
|
||||
<div className="panelMainContent">
|
||||
{!isSaveQueryEnabled() ? (
|
||||
{!canSaveQueries() ? (
|
||||
<Text variant="small">{setupSaveQueriesText}</Text>
|
||||
) : (
|
||||
<TextField
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { createOrUpdate } from "../../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
|
||||
@@ -57,10 +56,8 @@ export const SetupNoteBooksPanel: FunctionComponent<SetupNoteBooksPanelProps> =
|
||||
|
||||
try {
|
||||
setLoadingTrue();
|
||||
await createOrUpdate(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
await explorer.notebookWorkspaceManager.createNotebookWorkspaceAsync(
|
||||
userContext.databaseAccount && userContext.databaseAccount.id,
|
||||
"default"
|
||||
);
|
||||
explorer.isAccountReady.valueHasMutated(); // re-trigger init notebooks
|
||||
|
||||
@@ -9,11 +9,23 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
||||
Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"canSaveQueries": [Function],
|
||||
"collapsedResourceTreeWidth": 36,
|
||||
"commandBarComponentAdapter": CommandBarComponentAdapter {
|
||||
"container": [Circular],
|
||||
"isNotebookTabActive": [Function],
|
||||
"parameters": [Function],
|
||||
"tabsButtons": Array [],
|
||||
},
|
||||
"databases": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isHostedDataExplorerEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isResourceTokenCollectionNodeSelected": [Function],
|
||||
"isSchemaEnabled": [Function],
|
||||
"isServerlessEnabled": [Function],
|
||||
"isShellEnabled": [Function],
|
||||
"isSynapseLinkUpdating": [Function],
|
||||
"isTabsContentExpanded": [Function],
|
||||
@@ -28,16 +40,38 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
||||
},
|
||||
"refreshNotebookList": [Function],
|
||||
"resourceTokenCollection": [Function],
|
||||
"resourceTokenCollectionId": [Function],
|
||||
"resourceTokenDatabaseId": [Function],
|
||||
"resourceTokenPartitionKey": [Function],
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"databaseCollectionIdMap": Map {},
|
||||
"koSubsCollectionIdMap": Map {},
|
||||
"koSubsDatabaseIdMap": Map {},
|
||||
"parameters": [Function],
|
||||
},
|
||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||
"container": [Circular],
|
||||
"parameters": [Function],
|
||||
},
|
||||
"selectedDatabaseId": [Function],
|
||||
"selectedNode": [Function],
|
||||
"setInProgressConsoleDataIdToBeDeleted": undefined,
|
||||
"setNotificationConsoleData": undefined,
|
||||
"sparkClusterConnectionInfo": [Function],
|
||||
"splitter": Splitter {
|
||||
"bounds": Object {
|
||||
"max": 400,
|
||||
"min": 240,
|
||||
},
|
||||
"direction": "vertical",
|
||||
"isCollapsed": [Function],
|
||||
"leftSideId": "resourcetree",
|
||||
"onResizeStart": [Function],
|
||||
"onResizeStop": [Function],
|
||||
"splitterId": "h_splitter1",
|
||||
},
|
||||
"tabsManager": TabsManager {
|
||||
"activeTab": [Function],
|
||||
"openedTabs": [Function],
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import Explorer from "../../Explorer";
|
||||
import { UploadItemsPane } from "./UploadItemsPane";
|
||||
|
||||
const props = {
|
||||
explorer: new Explorer(),
|
||||
};
|
||||
describe("Upload Items Pane", () => {
|
||||
it("should render Default properly", () => {
|
||||
const wrapper = shallow(<UploadItemsPane />);
|
||||
const wrapper = shallow(<UploadItemsPane {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,11 +3,15 @@ import React, { ChangeEvent, FunctionComponent, useState } from "react";
|
||||
import { Upload } from "../../../Common/Upload/Upload";
|
||||
import { UploadDetailsRecord } from "../../../Contracts/ViewModels";
|
||||
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import { getErrorMessage } from "../../Tables/Utilities";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
|
||||
export const UploadItemsPane: FunctionComponent = () => {
|
||||
export interface UploadItemsPaneProps {
|
||||
explorer: Explorer;
|
||||
}
|
||||
|
||||
export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ explorer }: UploadItemsPaneProps) => {
|
||||
const [files, setFiles] = useState<FileList>();
|
||||
const [uploadFileData, setUploadFileData] = useState<UploadDetailsRecord[]>([]);
|
||||
const [formError, setFormError] = useState<string>("");
|
||||
@@ -21,7 +25,7 @@ export const UploadItemsPane: FunctionComponent = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedCollection = useSelectedNode.getState().findSelectedCollection();
|
||||
const selectedCollection = explorer.findSelectedCollection();
|
||||
setIsExecuting(true);
|
||||
|
||||
selectedCollection
|
||||
|
||||
@@ -1,16 +1,92 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
exports[`Delete Database Confirmation Pane submit() Should call delete database 1`] = `
|
||||
<DeleteDatabaseConfirmationPanel
|
||||
closePanel={[Function]}
|
||||
explorer={
|
||||
Object {
|
||||
Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"canSaveQueries": [Function],
|
||||
"collapsedResourceTreeWidth": 36,
|
||||
"commandBarComponentAdapter": CommandBarComponentAdapter {
|
||||
"container": [Circular],
|
||||
"isNotebookTabActive": [Function],
|
||||
"parameters": [Function],
|
||||
"tabsButtons": Array [],
|
||||
},
|
||||
"databases": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isHostedDataExplorerEnabled": [Function],
|
||||
"isLastCollection": [Function],
|
||||
"isLastNonEmptyDatabase": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isResourceTokenCollectionNodeSelected": [Function],
|
||||
"isSchemaEnabled": [Function],
|
||||
"isSelectedDatabaseShared": [Function],
|
||||
"isServerlessEnabled": [Function],
|
||||
"isShellEnabled": [Function],
|
||||
"isSynapseLinkUpdating": [Function],
|
||||
"isTabsContentExpanded": [Function],
|
||||
"memoryUsageInfo": [Function],
|
||||
"notebookBasePath": [Function],
|
||||
"notebookServerInfo": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
},
|
||||
"refreshAllDatabases": [Function],
|
||||
"refreshNotebookList": [Function],
|
||||
"resourceTokenCollection": [Function],
|
||||
"resourceTokenCollectionId": [Function],
|
||||
"resourceTokenDatabaseId": [Function],
|
||||
"resourceTokenPartitionKey": [Function],
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"databaseCollectionIdMap": Map {},
|
||||
"koSubsCollectionIdMap": Map {},
|
||||
"koSubsDatabaseIdMap": Map {},
|
||||
"parameters": [Function],
|
||||
},
|
||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||
"container": [Circular],
|
||||
"parameters": [Function],
|
||||
},
|
||||
"selectedDatabaseId": [Function],
|
||||
"selectedNode": [Function],
|
||||
"setInProgressConsoleDataIdToBeDeleted": undefined,
|
||||
"setNotificationConsoleData": undefined,
|
||||
"sparkClusterConnectionInfo": [Function],
|
||||
"splitter": Splitter {
|
||||
"bounds": Object {
|
||||
"max": 400,
|
||||
"min": 240,
|
||||
},
|
||||
"direction": "vertical",
|
||||
"isCollapsed": [Function],
|
||||
"leftSideId": "resourcetree",
|
||||
"onResizeStart": [Function],
|
||||
"onResizeStop": [Function],
|
||||
"splitterId": "h_splitter1",
|
||||
},
|
||||
"tabsManager": TabsManager {
|
||||
"activeTab": [Function],
|
||||
"openedTabs": [Function],
|
||||
},
|
||||
}
|
||||
}
|
||||
openNotificationConsole={[Function]}
|
||||
selectedDatabase={
|
||||
Object {
|
||||
"collections": [Function],
|
||||
"id": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<RightPaneForm
|
||||
formError=""
|
||||
@@ -33,7 +109,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
verticalAlign="center"
|
||||
>
|
||||
<div
|
||||
className="ms-Stack panelInfoErrorContainer css-53"
|
||||
className="ms-Stack panelInfoErrorContainer css-102"
|
||||
>
|
||||
<StyledIconBase
|
||||
aria-label="warning"
|
||||
@@ -322,7 +398,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
>
|
||||
<i
|
||||
aria-label="warning"
|
||||
className="panelWarningIcon root-55"
|
||||
className="panelWarningIcon root-104"
|
||||
data-icon-name="WarningSolid"
|
||||
role="img"
|
||||
>
|
||||
@@ -341,7 +417,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
>
|
||||
<span
|
||||
aria-label="message"
|
||||
className="panelWarningErrorMessage css-56"
|
||||
className="panelWarningErrorMessage css-105"
|
||||
>
|
||||
Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.
|
||||
</span>
|
||||
@@ -365,7 +441,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
variant="small"
|
||||
>
|
||||
<span
|
||||
className="css-56"
|
||||
className="css-105"
|
||||
>
|
||||
Confirm by typing the database id
|
||||
</span>
|
||||
@@ -665,18 +741,18 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
validateOnLoad={true}
|
||||
>
|
||||
<div
|
||||
className="ms-TextField root-58"
|
||||
className="ms-TextField root-107"
|
||||
>
|
||||
<div
|
||||
className="ms-TextField-wrapper"
|
||||
>
|
||||
<div
|
||||
className="ms-TextField-fieldGroup fieldGroup-59"
|
||||
className="ms-TextField-fieldGroup fieldGroup-108"
|
||||
>
|
||||
<input
|
||||
aria-invalid={false}
|
||||
autoFocus={true}
|
||||
className="ms-TextField-field field-60"
|
||||
className="ms-TextField-field field-109"
|
||||
id="confirmDatabaseId"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
@@ -699,7 +775,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
variant="small"
|
||||
>
|
||||
<span
|
||||
className="css-69"
|
||||
className="css-126"
|
||||
>
|
||||
Help us improve Azure Cosmos DB!
|
||||
</span>
|
||||
@@ -709,7 +785,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
variant="small"
|
||||
>
|
||||
<span
|
||||
className="css-69"
|
||||
className="css-126"
|
||||
>
|
||||
What is the reason why you are deleting this database?
|
||||
</span>
|
||||
@@ -1011,17 +1087,17 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
validateOnLoad={true}
|
||||
>
|
||||
<div
|
||||
className="ms-TextField ms-TextField--multiline root-58"
|
||||
className="ms-TextField ms-TextField--multiline root-107"
|
||||
>
|
||||
<div
|
||||
className="ms-TextField-wrapper"
|
||||
>
|
||||
<div
|
||||
className="ms-TextField-fieldGroup fieldGroup-70"
|
||||
className="ms-TextField-fieldGroup fieldGroup-127"
|
||||
>
|
||||
<textarea
|
||||
aria-invalid={false}
|
||||
className="ms-TextField-field field-71"
|
||||
className="ms-TextField-field field-128"
|
||||
id="deleteDatabaseFeedbackInput"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
@@ -2747,7 +2823,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
>
|
||||
<button
|
||||
aria-label="OK"
|
||||
className="ms-Button ms-Button--primary root-73"
|
||||
className="ms-Button ms-Button--primary root-118"
|
||||
data-is-focusable={true}
|
||||
id="sidePanelOkButton"
|
||||
onClick={[Function]}
|
||||
@@ -2759,16 +2835,16 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
className="ms-Button-flexContainer flexContainer-74"
|
||||
className="ms-Button-flexContainer flexContainer-119"
|
||||
data-automationid="splitbuttonprimary"
|
||||
>
|
||||
<span
|
||||
className="ms-Button-textContainer textContainer-75"
|
||||
className="ms-Button-textContainer textContainer-120"
|
||||
>
|
||||
<span
|
||||
className="ms-Button-label label-77"
|
||||
id="id__6"
|
||||
key="id__6"
|
||||
className="ms-Button-label label-122"
|
||||
id="id__3"
|
||||
key="id__3"
|
||||
>
|
||||
OK
|
||||
</span>
|
||||
|
||||
@@ -7,6 +7,7 @@ jest.mock("../Explorer");
|
||||
|
||||
const createExplorer = () => {
|
||||
const mock = new Explorer();
|
||||
mock.selectedNode = ko.observable();
|
||||
mock.isNotebookEnabled = ko.observable(false);
|
||||
mock.tabsManager = new TabsManager();
|
||||
return mock as jest.Mocked<Explorer>;
|
||||
|
||||
@@ -22,8 +22,6 @@ import { FeaturePanelLauncher } from "../Controls/FeaturePanel/FeaturePanelLaunc
|
||||
import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
|
||||
import Explorer from "../Explorer";
|
||||
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
|
||||
import { useDatabases } from "../useDatabases";
|
||||
import { useSelectedNode } from "../useSelectedNode";
|
||||
|
||||
export interface SplashScreenItem {
|
||||
iconSrc: string;
|
||||
@@ -61,7 +59,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
|
||||
public componentDidMount() {
|
||||
this.subscriptions.push(
|
||||
{ dispose: useSelectedNode.subscribe(() => this.setState({})) },
|
||||
this.container.selectedNode.subscribe(() => this.setState({})),
|
||||
this.container.isNotebookEnabled.subscribe(() => this.setState({}))
|
||||
);
|
||||
}
|
||||
@@ -229,12 +227,12 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
return items;
|
||||
}
|
||||
|
||||
if (!useSelectedNode.getState().isDatabaseNodeOrNoneSelected()) {
|
||||
if (!this.container.isDatabaseNodeOrNoneSelected()) {
|
||||
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
|
||||
items.push({
|
||||
iconSrc: NewQueryIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null);
|
||||
},
|
||||
title: "New SQL Query",
|
||||
@@ -244,7 +242,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
items.push({
|
||||
iconSrc: NewQueryIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null);
|
||||
},
|
||||
title: "New Query",
|
||||
@@ -267,14 +265,20 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
title: "New Stored Procedure",
|
||||
description: null,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/* Scale & Settings */
|
||||
const isShared = useSelectedNode.getState().findSelectedDatabase()?.isDatabaseShared();
|
||||
let isShared = false;
|
||||
if (this.container.isDatabaseNodeSelected()) {
|
||||
isShared = this.container.findSelectedDatabase().isDatabaseShared();
|
||||
} else if (this.container.isNodeKindSelected("Collection")) {
|
||||
const database: ViewModels.Database = this.container.findSelectedCollection().getDatabase();
|
||||
isShared = database && database.isDatabaseShared();
|
||||
}
|
||||
|
||||
const label = isShared ? "Settings" : "Scale & Settings";
|
||||
items.push({
|
||||
@@ -282,7 +286,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
title: label,
|
||||
description: null,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onSettingsClick();
|
||||
},
|
||||
});
|
||||
@@ -304,8 +308,8 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
title: collectionId,
|
||||
description: "Data",
|
||||
onClick: () => {
|
||||
const collection = useDatabases.getState().findCollection(databaseId, collectionId);
|
||||
collection?.openTab();
|
||||
const collection = this.container.findCollection(databaseId, collectionId);
|
||||
collection && collection.openTab();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -792,7 +792,7 @@ export default class QueryBuilderViewModel {
|
||||
return null;
|
||||
}
|
||||
|
||||
public checkIfClauseChanged(): void {
|
||||
this._queryViewModel.checkIfBuilderChanged();
|
||||
public checkIfClauseChanged(clause: QueryClauseViewModel): void {
|
||||
this._queryViewModel.checkIfBuilderChanged(clause);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ export default class QueryClauseViewModel {
|
||||
);
|
||||
|
||||
this.and_or.subscribe((value) => {
|
||||
this._queryBuilderViewModel.checkIfClauseChanged();
|
||||
this._queryBuilderViewModel.checkIfClauseChanged(this);
|
||||
});
|
||||
this.field.subscribe((value) => {
|
||||
this.changeField();
|
||||
@@ -103,13 +103,13 @@ export default class QueryClauseViewModel {
|
||||
// }
|
||||
});
|
||||
this.customTimeValue.subscribe((value) => {
|
||||
this._queryBuilderViewModel.checkIfClauseChanged();
|
||||
this._queryBuilderViewModel.checkIfClauseChanged(this);
|
||||
});
|
||||
this.value.subscribe((value) => {
|
||||
this._queryBuilderViewModel.checkIfClauseChanged();
|
||||
this._queryBuilderViewModel.checkIfClauseChanged(this);
|
||||
});
|
||||
this.operator.subscribe((value) => {
|
||||
this._queryBuilderViewModel.checkIfClauseChanged();
|
||||
this._queryBuilderViewModel.checkIfClauseChanged(this);
|
||||
});
|
||||
this._groupCheckSubscription = this.checkedForGrouping.subscribe((value) => {
|
||||
this._queryBuilderViewModel.updateCanGroupClauses();
|
||||
@@ -184,7 +184,7 @@ export default class QueryClauseViewModel {
|
||||
this.type(QueryBuilderConstants.TableType.String);
|
||||
}
|
||||
}
|
||||
this._queryBuilderViewModel.checkIfClauseChanged();
|
||||
this._queryBuilderViewModel.checkIfClauseChanged(this);
|
||||
}
|
||||
|
||||
private resetFromTimestamp(): void {
|
||||
@@ -216,7 +216,7 @@ export default class QueryClauseViewModel {
|
||||
this.timeValue("");
|
||||
this.customTimeValue("");
|
||||
}
|
||||
this._queryBuilderViewModel.checkIfClauseChanged();
|
||||
this._queryBuilderViewModel.checkIfClauseChanged(this);
|
||||
}
|
||||
|
||||
// private customTimestampDialog(): Promise<any> {
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import * as ko from "knockout";
|
||||
import React from "react";
|
||||
import * as _ from "underscore";
|
||||
import { KeyCodes } from "../../../Common/Constants";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { TableQuerySelectPanel } from "../../Panes/Tables/TableQuerySelectPanel/TableQuerySelectPanel";
|
||||
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
||||
import { getQuotedCqlIdentifier } from "../CqlUtilities";
|
||||
import * as DataTableUtilities from "../DataTable/DataTableUtilities";
|
||||
import TableEntityListViewModel from "../DataTable/TableEntityListViewModel";
|
||||
import QueryBuilderViewModel from "./QueryBuilderViewModel";
|
||||
import QueryClauseViewModel from "./QueryClauseViewModel";
|
||||
|
||||
export default class QueryViewModel {
|
||||
public readonly topValueLimitMessage: string = "Please input a number between 0 and 1000.";
|
||||
public topValueLimitMessage: string = "Please input a number between 0 and 1000.";
|
||||
public queryBuilderViewModel = ko.observable<QueryBuilderViewModel>();
|
||||
public isHelperActive = ko.observable<boolean>(true);
|
||||
public isEditorActive = ko.observable<boolean>(false);
|
||||
@@ -51,7 +49,7 @@ export default class QueryViewModel {
|
||||
this.queryTextIsReadOnly = ko.computed<boolean>(() => {
|
||||
return userContext.apiType !== "Cassandra";
|
||||
});
|
||||
const initialOptions = this._tableEntityListViewModel.headers;
|
||||
let initialOptions = this._tableEntityListViewModel.headers;
|
||||
this.columnOptions = ko.observableArray<string>(initialOptions);
|
||||
this.focusTopResult = ko.observable<boolean>(false);
|
||||
this.focusExpandIcon = ko.observable<boolean>(false);
|
||||
@@ -65,12 +63,12 @@ export default class QueryViewModel {
|
||||
this.topValue() !== this.unchangedSaveTop()
|
||||
);
|
||||
|
||||
this.queryBuilderViewModel().clauseArray.subscribe(() => {
|
||||
this.queryBuilderViewModel().clauseArray.subscribe((value) => {
|
||||
this.setFilter();
|
||||
});
|
||||
|
||||
this.isExceedingLimit = ko.computed<boolean>(() => {
|
||||
const currentTopValue: number = this.topValue();
|
||||
var currentTopValue: number = this.topValue();
|
||||
return currentTopValue < 0 || currentTopValue > 1000;
|
||||
});
|
||||
|
||||
@@ -113,7 +111,7 @@ export default class QueryViewModel {
|
||||
DataTableUtilities.forceRecalculateTableSize(); // Fix for 261924, forces the resize event so DataTableBindingManager will redo the calculation on table size.
|
||||
};
|
||||
|
||||
public ontoggleAdvancedOptionsKeyDown = (source: string, event: KeyboardEvent): boolean => {
|
||||
public ontoggleAdvancedOptionsKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||
this.toggleAdvancedOptions();
|
||||
event.stopPropagation();
|
||||
@@ -127,29 +125,31 @@ export default class QueryViewModel {
|
||||
};
|
||||
|
||||
private setFilter = (): string => {
|
||||
const queryString = this.isEditorActive()
|
||||
var queryString = this.isEditorActive()
|
||||
? this.queryText()
|
||||
: userContext.apiType === "Cassandra"
|
||||
? this.queryBuilderViewModel().getCqlFilterFromClauses()
|
||||
: this.queryBuilderViewModel().getODataFilterFromClauses();
|
||||
const filter = queryString;
|
||||
var filter = queryString;
|
||||
this.queryText(filter);
|
||||
return this.queryText();
|
||||
};
|
||||
|
||||
private setSqlFilter = (): string => {
|
||||
return this.queryBuilderViewModel().getSqlFilterFromClauses();
|
||||
var filter = this.queryBuilderViewModel().getSqlFilterFromClauses();
|
||||
return filter;
|
||||
};
|
||||
|
||||
private setCqlFilter = (): string => {
|
||||
return this.queryBuilderViewModel().getCqlFilterFromClauses();
|
||||
var filter = this.queryBuilderViewModel().getCqlFilterFromClauses();
|
||||
return filter;
|
||||
};
|
||||
|
||||
public isHelperEnabled = ko
|
||||
.computed<boolean>(() => {
|
||||
return (
|
||||
this.queryText() === this.unchangedText() ||
|
||||
this.queryText() === undefined ||
|
||||
this.queryText() === null ||
|
||||
this.queryText() === "" ||
|
||||
this.isHelperActive()
|
||||
);
|
||||
@@ -159,13 +159,13 @@ export default class QueryViewModel {
|
||||
});
|
||||
|
||||
public runQuery = (): DataTables.DataTable => {
|
||||
let filter = this.setFilter();
|
||||
var filter = this.setFilter();
|
||||
if (filter && userContext.apiType !== "Cassandra") {
|
||||
filter = filter.replace(/"/g, "'");
|
||||
}
|
||||
const top = this.topValue();
|
||||
const selectOptions = this._getSelectedResults();
|
||||
const select = selectOptions;
|
||||
var top = this.topValue();
|
||||
var selectOptions = this._getSelectedResults();
|
||||
var select = selectOptions;
|
||||
this._tableEntityListViewModel.tableQuery.filter = filter;
|
||||
this._tableEntityListViewModel.tableQuery.top = top;
|
||||
this._tableEntityListViewModel.tableQuery.select = select;
|
||||
@@ -177,16 +177,16 @@ export default class QueryViewModel {
|
||||
};
|
||||
|
||||
public clearQuery = (): DataTables.DataTable => {
|
||||
this.queryText();
|
||||
this.topValue();
|
||||
this.selectText();
|
||||
this.queryText(null);
|
||||
this.topValue(null);
|
||||
this.selectText(null);
|
||||
this.selectMessage("");
|
||||
// clears the queryBuilder and adds a new blank clause
|
||||
this.queryBuilderViewModel().queryClauses.removeAll();
|
||||
this.queryBuilderViewModel().addNewClause();
|
||||
this._tableEntityListViewModel.tableQuery.filter = undefined;
|
||||
this._tableEntityListViewModel.tableQuery.top = undefined;
|
||||
this._tableEntityListViewModel.tableQuery.select = undefined;
|
||||
this._tableEntityListViewModel.tableQuery.filter = null;
|
||||
this._tableEntityListViewModel.tableQuery.top = null;
|
||||
this._tableEntityListViewModel.tableQuery.select = null;
|
||||
this._tableEntityListViewModel.oDataQuery("");
|
||||
this._tableEntityListViewModel.sqlQuery("SELECT * FROM c");
|
||||
this._tableEntityListViewModel.cqlQuery(
|
||||
@@ -197,11 +197,12 @@ export default class QueryViewModel {
|
||||
return this._tableEntityListViewModel.reloadTable(false);
|
||||
};
|
||||
|
||||
public selectQueryOptions() {
|
||||
useSidePanel.getState().openSidePanel("Select Column", <TableQuerySelectPanel queryViewModel={this} />);
|
||||
public selectQueryOptions(): Promise<any> {
|
||||
this.queryTablesTab.container.openTableSelectQueryPanel(this);
|
||||
return null;
|
||||
}
|
||||
|
||||
public onselectQueryOptionsKeyDown = (source: string, event: KeyboardEvent): boolean => {
|
||||
public onselectQueryOptionsKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||
this.selectQueryOptions();
|
||||
event.stopPropagation();
|
||||
@@ -211,7 +212,7 @@ export default class QueryViewModel {
|
||||
};
|
||||
|
||||
public getSelectMessage(): void {
|
||||
if (_.isEmpty(this.selectText()) || this.selectText() === undefined) {
|
||||
if (_.isEmpty(this.selectText()) || this.selectText() === null) {
|
||||
this.selectMessage("");
|
||||
} else {
|
||||
this.selectMessage(`${this.selectText().length} of ${this.columnOptions().length} columns selected.`);
|
||||
@@ -219,7 +220,7 @@ export default class QueryViewModel {
|
||||
}
|
||||
|
||||
public isSelected = ko.computed<boolean>(() => {
|
||||
return !(_.isEmpty(this.selectText()) || this.selectText() === undefined);
|
||||
return !(_.isEmpty(this.selectText()) || this.selectText() === null);
|
||||
});
|
||||
|
||||
private setCheckToSave(): void {
|
||||
@@ -229,7 +230,7 @@ export default class QueryViewModel {
|
||||
this.isSaveEnabled(false);
|
||||
}
|
||||
|
||||
public checkIfBuilderChanged(): void {
|
||||
public checkIfBuilderChanged(clause: QueryClauseViewModel): void {
|
||||
this.setFilter();
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import * as ko from "knockout";
|
||||
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { updateUserContext } from "../../UserContext";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../Explorer";
|
||||
import DocumentId from "../Tree/DocumentId";
|
||||
import DocumentsTab from "./DocumentsTab";
|
||||
@@ -15,6 +16,8 @@ describe("Documents tab", () => {
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
|
||||
});
|
||||
|
||||
expect(documentsTab.buildQuery("")).toContain("select");
|
||||
@@ -89,6 +92,8 @@ describe("Documents tab", () => {
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
|
||||
});
|
||||
|
||||
expect(documentsTab.showPartitionKey).toBe(false);
|
||||
@@ -102,6 +107,8 @@ describe("Documents tab", () => {
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
|
||||
});
|
||||
|
||||
expect(documentsTab.showPartitionKey).toBe(false);
|
||||
@@ -115,6 +122,8 @@ describe("Documents tab", () => {
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
|
||||
});
|
||||
|
||||
expect(documentsTab.showPartitionKey).toBe(true);
|
||||
@@ -131,6 +140,8 @@ describe("Documents tab", () => {
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
|
||||
});
|
||||
|
||||
expect(documentsTab.showPartitionKey).toBe(false);
|
||||
@@ -144,6 +155,8 @@ describe("Documents tab", () => {
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
|
||||
});
|
||||
|
||||
expect(documentsTab.showPartitionKey).toBe(true);
|
||||
|
||||
@@ -28,7 +28,6 @@ import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandBu
|
||||
import Explorer from "../Explorer";
|
||||
import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList";
|
||||
import DocumentId from "../Tree/DocumentId";
|
||||
import { useSelectedNode } from "../useSelectedNode";
|
||||
import template from "./DocumentsTab.html";
|
||||
import TabsBase from "./TabsBase";
|
||||
|
||||
@@ -912,13 +911,13 @@ export default class DocumentsTab extends TabsBase {
|
||||
iconSrc: UploadIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && container.openUploadItemsPanePane();
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: useSelectedNode.getState().isDatabaseNodeOrNoneSelected(),
|
||||
disabled: container.isDatabaseNodeOrNoneSelected(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,7 +281,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
||||
}
|
||||
|
||||
/** Renders a Javascript object to be displayed inside Monaco Editor */
|
||||
public renderObjectForEditor(value: any, replacer: any, space: string | number): string {
|
||||
protected renderObjectForEditor(value: any, replacer: any, space: string | number): string {
|
||||
return MongoUtility.tojson(value, null, false);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user