Merge branch 'master'
This commit is contained in:
commit
42b7d8fe09
|
@ -44,7 +44,6 @@ 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
|
||||
|
@ -106,17 +105,10 @@ src/Explorer/Notebook/NotebookContentClient.ts
|
|||
src/Explorer/Notebook/NotebookContentItem.ts
|
||||
src/Explorer/Notebook/NotebookUtil.ts
|
||||
src/Explorer/OpenActionsStubs.ts
|
||||
src/Explorer/Panes/AddDatabasePane.ts
|
||||
src/Explorer/Panes/AddDatabasePane.test.ts
|
||||
src/Explorer/Panes/BrowseQueriesPane.ts
|
||||
src/Explorer/Panes/RenewAdHocAccessPane.ts
|
||||
src/Explorer/Panes/SetupNotebooksPane.ts
|
||||
src/Explorer/Panes/SwitchDirectoryPane.ts
|
||||
src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts
|
||||
src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts
|
||||
src/Explorer/Panes/Tables/Validators/EntityPropertyValueValidator.ts
|
||||
src/Explorer/SplashScreen/SplashScreen.test.ts
|
||||
src/Explorer/Tables/Constants.ts
|
||||
src/Explorer/Tables/DataTable/CacheBase.ts
|
||||
src/Explorer/Tables/DataTable/DataTableBindingManager.ts
|
||||
src/Explorer/Tables/DataTable/DataTableBuilder.ts
|
||||
|
@ -133,7 +125,6 @@ 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
|
||||
|
@ -143,14 +134,8 @@ 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/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/TabComponents.ts
|
||||
src/Explorer/Tabs/TabsBase.ts
|
||||
src/Explorer/Tabs/TriggerTab.ts
|
||||
|
@ -159,7 +144,6 @@ src/Explorer/Tree/AccessibleVerticalList.ts
|
|||
src/Explorer/Tree/Collection.test.ts
|
||||
src/Explorer/Tree/Collection.ts
|
||||
src/Explorer/Tree/ConflictId.ts
|
||||
src/Explorer/Tree/Database.ts
|
||||
src/Explorer/Tree/DocumentId.ts
|
||||
src/Explorer/Tree/ObjectId.ts
|
||||
src/Explorer/Tree/ResourceTokenCollection.ts
|
||||
|
@ -174,38 +158,12 @@ src/GitHub/GitHubConnector.ts
|
|||
src/GitHub/GitHubContentProvider.test.ts
|
||||
src/GitHub/GitHubContentProvider.ts
|
||||
src/GitHub/GitHubOAuthService.ts
|
||||
src/HostedExplorer.ts
|
||||
src/Index.ts
|
||||
src/Juno/JunoClient.test.ts
|
||||
src/Juno/JunoClient.ts
|
||||
src/Main.ts
|
||||
src/NotebookWorkspaceManager/NotebookWorkspaceManager.ts
|
||||
src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts
|
||||
src/Platform/Emulator/DataAccessUtility.ts
|
||||
src/Platform/Emulator/ExplorerFactory.ts
|
||||
src/Platform/Emulator/Main.ts
|
||||
src/Platform/Emulator/NotificationsClient.ts
|
||||
src/Platform/Hosted/ArmResourceUtils.ts
|
||||
src/Platform/Hosted/Authorization.ts
|
||||
src/Platform/Hosted/DataAccessUtility.ts
|
||||
src/Platform/Hosted/ExplorerFactory.ts
|
||||
src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts
|
||||
src/Platform/Hosted/Main.ts
|
||||
src/Platform/Hosted/Maint.test.ts
|
||||
src/Platform/Hosted/NotificationsClient.ts
|
||||
src/Platform/Portal/DataAccessUtility.ts
|
||||
src/Platform/Portal/ExplorerFactory.ts
|
||||
src/Platform/Portal/Main.ts
|
||||
src/Platform/Portal/NotificationsClient.ts
|
||||
src/PlatformType.ts
|
||||
src/ReactDevTools.ts
|
||||
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
|
||||
|
@ -248,15 +206,7 @@ src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx
|
|||
src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.tsx
|
||||
src/NotebookViewer/NotebookViewer.tsx
|
||||
src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx
|
||||
src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx
|
||||
src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponentAdapter.tsx
|
||||
src/Explorer/Controls/ResizeSensorReactComponent/ResizeSensorComponent.tsx
|
||||
src/Explorer/Controls/Spark/ClusterSettingsComponent.tsx
|
||||
src/Explorer/Controls/Spark/ClusterSettingsComponentAdapter.tsx
|
||||
src/Explorer/Controls/Tabs/TabComponent.tsx
|
||||
src/Explorer/Controls/TreeComponent/TreeComponent.test.tsx
|
||||
src/Explorer/Controls/TreeComponent/TreeComponent.tsx
|
||||
src/Explorer/Graph/GraphExplorerComponent/EditorNeighborsComponent.tsx
|
||||
src/Explorer/Graph/GraphExplorerComponent/EditorNodePropertiesComponent.test.tsx
|
||||
src/Explorer/Graph/GraphExplorerComponent/EditorNodePropertiesComponent.tsx
|
||||
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx
|
||||
|
@ -304,8 +254,5 @@ src/Explorer/Tabs/NotebookViewerTab.tsx
|
|||
src/Explorer/Tabs/TerminalTab.tsx
|
||||
src/Explorer/Tree/ResourceTreeAdapter.tsx
|
||||
src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx
|
||||
src/GalleryViewer/Cards/GalleryCardComponent.tsx
|
||||
src/GalleryViewer/GalleryViewer.tsx
|
||||
src/GalleryViewer/GalleryViewerComponent.tsx
|
||||
__mocks__/monaco-editor.ts
|
||||
src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx
|
|
@ -724,45 +724,24 @@ execute-sproc-params-pane {
|
|||
|
||||
.results-container,
|
||||
.errors-container {
|
||||
padding: @MediumSpace 0px 0px @MediumSpace;
|
||||
height: 100%;
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
overflow: hidden;
|
||||
|
||||
.toggles {
|
||||
height: @ToggleHeight;
|
||||
width: @ToggleWidth;
|
||||
margin-left: @MediumSpace;
|
||||
|
||||
&:focus {
|
||||
.focus();
|
||||
}
|
||||
|
||||
.tab {
|
||||
margin-right: @MediumSpace;
|
||||
}
|
||||
|
||||
.toggleSwitch {
|
||||
.toggleSwitch();
|
||||
}
|
||||
|
||||
.selectedToggle {
|
||||
.selectedToggle();
|
||||
}
|
||||
|
||||
.unselectedToggle {
|
||||
.unselectedToggle();
|
||||
}
|
||||
}
|
||||
|
||||
.enterInputParameters {
|
||||
padding: @LargeSpace @MediumSpace;
|
||||
}
|
||||
|
||||
div[role="tabpanel"] {
|
||||
height: 100%;
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.errors-container {
|
||||
padding-left: (2 * @MediumSpace);
|
||||
padding: @MediumSpace 0px 0px @MediumSpace;
|
||||
.errors-header {
|
||||
font-weight: 700;
|
||||
font-size: @DefaultFontSize;
|
||||
|
@ -3160,4 +3139,11 @@ settings-pane {
|
|||
padding: 20px;
|
||||
padding-left: 10px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
}
|
||||
.spinner {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
background: white;
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
@ -201,3 +201,11 @@
|
|||
.migration:disabled {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.trigger-field {
|
||||
width: 40%;
|
||||
margin-top: 10px
|
||||
}
|
||||
.trigger-form {
|
||||
padding: 10px 30px 10px 30px;
|
||||
}
|
||||
|
|
|
@ -3709,14 +3709,84 @@
|
|||
}
|
||||
},
|
||||
"@nteract/editor": {
|
||||
"version": "10.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@nteract/editor/-/editor-10.1.2.tgz",
|
||||
"integrity": "sha512-Wtj0kJUSoBZsWUh82JGt6miqYS0jt0k+3SD3cnW9socayxp2KB0Qbqhh2NtrF9ysxVHWnQT8iUarJjpGIdNyng==",
|
||||
"version": "10.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@nteract/editor/-/editor-10.1.12.tgz",
|
||||
"integrity": "sha512-bsUrCctukjWdpKNWQOQmhfxMCQ/SBVIO6+RkazI4y4dVeeP3KMP8nxfhzIbzTMNSkyynps/deZFjpDWqRhG+Dg==",
|
||||
"requires": {
|
||||
"@nteract/messaging": "^7.0.10",
|
||||
"@nteract/outputs": "^3.0.9",
|
||||
"codemirror": "5.57.0",
|
||||
"@nteract/messaging": "^7.0.19",
|
||||
"@nteract/outputs": "^3.0.11",
|
||||
"codemirror": "5.61.1",
|
||||
"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": {
|
||||
|
@ -8067,9 +8137,9 @@
|
|||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
|
||||
},
|
||||
"codemirror": {
|
||||
"version": "5.57.0",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.57.0.tgz",
|
||||
"integrity": "sha512-WGc6UL7Hqt+8a6ZAsj/f1ApQl3NPvHY/UQSzG6fB6l4BjExgVdhFaxd7mRTw1UCiYe/6q86zHP+kfvBQcZGvUg=="
|
||||
"version": "5.61.1",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.61.1.tgz",
|
||||
"integrity": "sha512-+D1NZjAucuzE93vJGbAaXzvoBHwp9nJZWWWF9utjv25+5AZUiah6CIlfb4ikG4MoDsFsCG8niiJH5++OO2LgIQ=="
|
||||
},
|
||||
"collapse-white-space": {
|
||||
"version": "1.0.6",
|
||||
|
@ -17699,12 +17769,6 @@
|
|||
"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",
|
||||
|
@ -18508,9 +18572,9 @@
|
|||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"lodash-es": {
|
||||
"version": "4.17.20",
|
||||
|
@ -18737,9 +18801,9 @@
|
|||
"integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg=="
|
||||
},
|
||||
"marked": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-2.0.3.tgz",
|
||||
"integrity": "sha512-5otztIIcJfPc2qGTN8cVtOJEjNJZ0jwa46INMagrYfk0EvqtRuEHLsEe0LrFS0/q+ZRKT0+kXK7P2T1AN5lWRA==",
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-2.0.6.tgz",
|
||||
"integrity": "sha512-S2mYj0FzTQa0dLddssqwRVW4EOJOVJ355Xm2Vcbm+LU7GQRGWvwbO5K87OaPSOux2AwTSgtPPaXmc8sDPrhn2A==",
|
||||
"dev": true
|
||||
},
|
||||
"martinez-polygon-clipping": {
|
||||
|
@ -24381,12 +24445,6 @@
|
|||
"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",
|
||||
|
@ -24402,9 +24460,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz",
|
||||
"integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==",
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.4.tgz",
|
||||
"integrity": "sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==",
|
||||
"dev": true
|
||||
},
|
||||
"typestyle": {
|
||||
|
|
|
@ -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.2",
|
||||
"@nteract/editor": "10.1.12",
|
||||
"@nteract/fixtures": "2.3.0",
|
||||
"@nteract/iron-icons": "1.0.0",
|
||||
"@nteract/jupyter-widgets": "2.0.0",
|
||||
|
@ -174,7 +174,7 @@
|
|||
"tslint": "5.11.0",
|
||||
"tslint-microsoft-contrib": "6.0.0",
|
||||
"typedoc": "0.20.36",
|
||||
"typescript": "4.2.4",
|
||||
"typescript": "4.3.4",
|
||||
"url-loader": "1.1.1",
|
||||
"wait-on": "4.0.2",
|
||||
"webpack": "4.46.0",
|
||||
|
|
|
@ -4,6 +4,7 @@ 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 { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
|
||||
|
@ -176,7 +177,7 @@ export class QueriesClient {
|
|||
|
||||
private findQueriesCollection(): ViewModels.Collection {
|
||||
const queriesDatabase: ViewModels.Database = _.find(
|
||||
this.container.databases(),
|
||||
useDatabases.getState().databases,
|
||||
(database: ViewModels.Database) => database.id() === SavedQueries.DatabaseName
|
||||
);
|
||||
if (!queriesDatabase) {
|
||||
|
|
|
@ -120,6 +120,14 @@ 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) {
|
||||
|
|
|
@ -89,7 +89,6 @@ export interface Database extends TreeNode {
|
|||
|
||||
selectedSubnodeKind: ko.Observable<CollectionTabKind>;
|
||||
|
||||
selectDatabase(): void;
|
||||
expandDatabase(): Promise<void>;
|
||||
collapseDatabase(): void;
|
||||
|
||||
|
@ -275,7 +274,6 @@ export interface TabOptions {
|
|||
tabKind: CollectionTabKind;
|
||||
title: string;
|
||||
tabPath: string;
|
||||
hashLocation: string;
|
||||
isTabsContentExpanded?: ko.Observable<boolean>;
|
||||
onLoadStartKey?: number;
|
||||
|
||||
|
|
|
@ -1,172 +0,0 @@
|
|||
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",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
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 { useNotebook } from "./Notebook/useNotebook";
|
||||
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 refreshDatabases={() => container.refreshAllDatabases()} />
|
||||
),
|
||||
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 (useNotebook.getState().isShellEnabled) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
} else {
|
||||
selectedCollection && selectedCollection.onNewMongoShellClick();
|
||||
}
|
||||
},
|
||||
label: useNotebook.getState().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 refreshDatabases={() => container.refreshAllDatabases()} />
|
||||
),
|
||||
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",
|
||||
},
|
||||
];
|
||||
};
|
|
@ -1,6 +1,11 @@
|
|||
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;
|
||||
|
@ -13,13 +18,16 @@ export interface EditorReactProps {
|
|||
editorKey?: string;
|
||||
}
|
||||
|
||||
export class EditorReact extends React.Component<EditorReactProps> {
|
||||
export class EditorReact extends React.Component<EditorReactProps, EditorReactStates> {
|
||||
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 {
|
||||
|
@ -42,7 +50,12 @@ export class EditorReact extends React.Component<EditorReactProps> {
|
|||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return <div className="jsonEditor" ref={(elt: HTMLElement) => this.setRef(elt)} />;
|
||||
return (
|
||||
<React.Fragment>
|
||||
{!this.state.showEditor && <Spinner size={SpinnerSize.large} className="spinner" />}
|
||||
<div className="jsonEditor" ref={(elt: HTMLElement) => this.setRef(elt)} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
|
||||
|
@ -83,6 +96,12 @@ export class EditorReact extends React.Component<EditorReactProps> {
|
|||
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 {
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
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()));
|
||||
}
|
||||
}
|
|
@ -55,7 +55,7 @@ export class NotebookViewerComponent
|
|||
databaseAccountName: undefined,
|
||||
defaultExperience: "NotebookViewer",
|
||||
isReadOnly: true,
|
||||
cellEditorType: "monaco",
|
||||
cellEditorType: "codemirror",
|
||||
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
|
||||
});
|
||||
|
|
|
@ -30,7 +30,7 @@ import * as DataModels from "../../../Contracts/DataModels";
|
|||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
|
||||
const title: string = "Open Saved Queries";
|
||||
const title = "Open Saved Queries";
|
||||
|
||||
export interface QueriesGridComponentProps {
|
||||
queriesClient: QueriesClient;
|
||||
|
@ -196,9 +196,9 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
|
|||
{
|
||||
key: "Action",
|
||||
name: "Action",
|
||||
fieldName: null,
|
||||
fieldName: undefined,
|
||||
minWidth: 70,
|
||||
onRender: (query: Query, index: number, column: IColumn) => {
|
||||
onRender: (query: Query) => {
|
||||
const buttonProps: IButtonProps = {
|
||||
iconProps: {
|
||||
iconName: "More",
|
||||
|
@ -214,19 +214,15 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
|
|||
{
|
||||
key: "Open",
|
||||
text: "Open query",
|
||||
onClick: (event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>, menuItem: any) => {
|
||||
onClick: () => {
|
||||
this.props.onQuerySelect(query);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "Delete",
|
||||
text: "Delete query",
|
||||
onClick: async (
|
||||
event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
|
||||
menuItem: any
|
||||
) => {
|
||||
onClick: async () => {
|
||||
if (window.confirm("Are you sure you want to delete this query?")) {
|
||||
const container = window.dataExplorer;
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, {
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
paneTitle: title,
|
||||
|
|
|
@ -38,7 +38,6 @@ describe("SettingsComponent", () => {
|
|||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
node: undefined,
|
||||
hashLocation: "settings",
|
||||
}),
|
||||
};
|
||||
|
||||
|
@ -127,7 +126,6 @@ describe("SettingsComponent", () => {
|
|||
isDatabaseExpanded: undefined,
|
||||
isDatabaseShared: ko.computed(() => true),
|
||||
selectedSubnodeKind: undefined,
|
||||
selectDatabase: undefined,
|
||||
expandDatabase: undefined,
|
||||
collapseDatabase: undefined,
|
||||
loadCollections: undefined,
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
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} /> : <></>;
|
||||
}
|
||||
}
|
|
@ -36,7 +36,6 @@ describe("SettingsUtils", () => {
|
|||
isDatabaseExpanded: ko.observable(false),
|
||||
isDatabaseShared: ko.computed(() => true),
|
||||
selectedSubnodeKind: ko.observable(undefined),
|
||||
selectDatabase: undefined,
|
||||
expandDatabase: undefined,
|
||||
collapseDatabase: undefined,
|
||||
loadCollections: undefined,
|
||||
|
|
|
@ -30,21 +30,8 @@ exports[`SettingsComponent renders 1`] = `
|
|||
"container": Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"canSaveQueries": [Function],
|
||||
"databases": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isHostedDataExplorerEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isResourceTokenCollectionNodeSelected": [Function],
|
||||
"isSchemaEnabled": [Function],
|
||||
"isShellEnabled": [Function],
|
||||
"isSynapseLinkUpdating": [Function],
|
||||
"isTabsContentExpanded": [Function],
|
||||
"memoryUsageInfo": [Function],
|
||||
"notebookBasePath": [Function],
|
||||
"notebookServerInfo": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"provideFeedbackEmail": [Function],
|
||||
|
@ -52,26 +39,15 @@ exports[`SettingsComponent renders 1`] = `
|
|||
"container": [Circular],
|
||||
},
|
||||
"refreshNotebookList": [Function],
|
||||
"resourceTokenCollection": [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],
|
||||
"sparkClusterConnectionInfo": [Function],
|
||||
"tabsManager": TabsManager {
|
||||
"activeTab": [Function],
|
||||
"openedTabs": [Function],
|
||||
},
|
||||
},
|
||||
"databaseId": "test",
|
||||
"defaultTtl": [Function],
|
||||
|
@ -125,21 +101,8 @@ exports[`SettingsComponent renders 1`] = `
|
|||
"container": Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"canSaveQueries": [Function],
|
||||
"databases": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isHostedDataExplorerEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isResourceTokenCollectionNodeSelected": [Function],
|
||||
"isSchemaEnabled": [Function],
|
||||
"isShellEnabled": [Function],
|
||||
"isSynapseLinkUpdating": [Function],
|
||||
"isTabsContentExpanded": [Function],
|
||||
"memoryUsageInfo": [Function],
|
||||
"notebookBasePath": [Function],
|
||||
"notebookServerInfo": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"provideFeedbackEmail": [Function],
|
||||
|
@ -147,26 +110,15 @@ exports[`SettingsComponent renders 1`] = `
|
|||
"container": [Circular],
|
||||
},
|
||||
"refreshNotebookList": [Function],
|
||||
"resourceTokenCollection": [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],
|
||||
"sparkClusterConnectionInfo": [Function],
|
||||
"tabsManager": TabsManager {
|
||||
"activeTab": [Function],
|
||||
"openedTabs": [Function],
|
||||
},
|
||||
},
|
||||
"databaseId": "test",
|
||||
"defaultTtl": [Function],
|
||||
|
|
|
@ -58,7 +58,7 @@ export class TabComponent extends React.Component<TabComponentProps> {
|
|||
as="span"
|
||||
className={className}
|
||||
role="presentation"
|
||||
onActivated={(e) => this.setActiveTab(index)}
|
||||
onActivated={() => this.setActiveTab(index)}
|
||||
aria-label={`Select tab: ${tab.title}`}
|
||||
>
|
||||
{tab.title}
|
||||
|
|
|
@ -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}`}>
|
||||
<div style={this.props.style} className={`treeComponent ${this.props.className}`} role="tree">
|
||||
<TreeNodeComponent paddingLeft={0} node={this.props.rootNode} generation={0} />
|
||||
</div>
|
||||
);
|
||||
|
@ -172,6 +172,7 @@ 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,6 +3,7 @@
|
|||
exports[`TreeComponent renders a simple tree 1`] = `
|
||||
<div
|
||||
className="treeComponent tree"
|
||||
role="tree"
|
||||
>
|
||||
<TreeNodeComponent
|
||||
generation={0}
|
||||
|
@ -37,6 +38,7 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
|
|||
className=" main2 nodeItem "
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="treeitem"
|
||||
>
|
||||
<div
|
||||
className="treeNodeHeader "
|
||||
|
@ -137,6 +139,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
|||
className="nodeClassname main12 nodeItem "
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="treeitem"
|
||||
>
|
||||
<div
|
||||
className="treeNodeHeader "
|
||||
|
@ -285,6 +288,7 @@ exports[`TreeNodeComponent renders loading icon 1`] = `
|
|||
className=" main2 nodeItem "
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="treeitem"
|
||||
>
|
||||
<div
|
||||
className="treeNodeHeader "
|
||||
|
@ -356,6 +360,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
|
|||
className="nodeClassname main12 nodeItem "
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="treeitem"
|
||||
>
|
||||
<div
|
||||
className="treeNodeHeader "
|
||||
|
@ -523,6 +528,7 @@ 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", () => {
|
||||
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;
|
||||
};
|
||||
let explorerStub: Explorer;
|
||||
|
||||
beforeAll(() => {
|
||||
explorerStub = {
|
||||
refreshAllDatabases: () => {},
|
||||
} as Explorer;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
(createDocument as jest.Mock).mockResolvedValue(undefined);
|
||||
|
@ -59,8 +59,7 @@ describe("ContainerSampleGenerator", () => {
|
|||
loadCollections: () => {},
|
||||
} as ViewModels.Database;
|
||||
database.findCollectionWithId = () => collection;
|
||||
|
||||
const explorerStub = createExplorerStub(database);
|
||||
useDatabases.getState().addDatabases([database]);
|
||||
|
||||
const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub);
|
||||
generator.setData(sampleData);
|
||||
|
@ -108,8 +107,8 @@ describe("ContainerSampleGenerator", () => {
|
|||
} as ViewModels.Database;
|
||||
database.findCollectionWithId = () => collection;
|
||||
collection.databaseId = database.id();
|
||||
useDatabases.getState().addDatabases([database]);
|
||||
|
||||
const explorerStub = createExplorerStub(database);
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
|
@ -126,7 +125,6 @@ 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: {
|
||||
|
@ -141,7 +139,6 @@ 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: {
|
||||
|
@ -163,7 +160,6 @@ describe("ContainerSampleGenerator", () => {
|
|||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
const explorerStub = createExplorerStub(undefined);
|
||||
// Rejects with error that contains experience
|
||||
await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@ 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[];
|
||||
|
@ -59,7 +60,7 @@ export class ContainerSampleGenerator {
|
|||
|
||||
await createCollection(createRequest);
|
||||
await this.container.refreshAllDatabases();
|
||||
const database = this.container.findDatabaseWithId(this.sampleDataFile.databaseId);
|
||||
const database = useDatabases.getState().findDatabaseWithId(this.sampleDataFile.databaseId);
|
||||
if (!database) {
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ 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";
|
||||
|
||||
|
@ -16,8 +17,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,6 +2,7 @@ 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 {
|
||||
|
@ -17,7 +18,7 @@ export class DataSamplesUtil {
|
|||
|
||||
const databaseName = generator.getDatabaseId();
|
||||
const containerName = generator.getCollectionId();
|
||||
if (this.hasContainer(databaseName, containerName, this.container.databases())) {
|
||||
if (this.hasContainer(databaseName, containerName, useDatabases.getState().databases)) {
|
||||
const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`;
|
||||
this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
|
||||
logConsoleError(msg);
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
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);
|
||||
});
|
||||
});
|
File diff suppressed because it is too large
Load Diff
|
@ -4,12 +4,12 @@
|
|||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import { NeighborVertexBasicInfo, EditedEdges, GraphNewEdgeData, PossibleVertex } from "./GraphExplorer";
|
||||
import * as GraphUtil from "./GraphUtil";
|
||||
import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent";
|
||||
import DeleteIcon from "../../../../images/delete.svg";
|
||||
import AddPropertyIcon from "../../../../images/Add-property.svg";
|
||||
import DeleteIcon from "../../../../images/delete.svg";
|
||||
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
|
||||
import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent";
|
||||
import { EditedEdges, GraphNewEdgeData, NeighborVertexBasicInfo, PossibleVertex } from "./GraphExplorer";
|
||||
import * as GraphUtil from "./GraphUtil";
|
||||
|
||||
export interface EditorNeighborsComponentProps {
|
||||
isSource: boolean;
|
||||
|
@ -83,11 +83,11 @@ export class EditorNeighborsComponent extends React.Component<EditorNeighborsCom
|
|||
}
|
||||
|
||||
private removeCurrentNeighborEdge(index: number): void {
|
||||
let sources = this.props.editedNeighbors.currentNeighbors;
|
||||
let id = sources[index].edgeId;
|
||||
const sources = this.props.editedNeighbors.currentNeighbors;
|
||||
const id = sources[index].edgeId;
|
||||
sources.splice(index, 1);
|
||||
|
||||
let droppedIds = this.props.editedNeighbors.droppedIds;
|
||||
const droppedIds = this.props.editedNeighbors.droppedIds;
|
||||
droppedIds.push(id);
|
||||
this.onUpdateEdges();
|
||||
}
|
||||
|
@ -215,7 +215,7 @@ export class EditorNeighborsComponent extends React.Component<EditorNeighborsCom
|
|||
</td>
|
||||
<td className="actionCol">
|
||||
<span className="rightPaneTrashIcon rightPaneBtns">
|
||||
<img src={DeleteIcon} alt="Delete" onClick={(e) => this.removeAddedEdgeToNeighbor(index)} />
|
||||
<img src={DeleteIcon} alt="Delete" onClick={() => this.removeAddedEdgeToNeighbor(index)} />
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -8,9 +8,10 @@ import * as React from "react";
|
|||
import create, { UseStore } from "zustand";
|
||||
import { StyleConstants } from "../../../Common/Constants";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { useObservable } from "../../../hooks/useObservable";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
|
||||
import * as CommandBarUtil from "./CommandBarUtil";
|
||||
|
||||
|
@ -29,13 +30,13 @@ export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
|
|||
}));
|
||||
|
||||
export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||
useObservable(container.selectedNode);
|
||||
const selectedNodeState = useSelectedNode();
|
||||
const buttons = useCommandBar((state) => state.contextButtons);
|
||||
const backgroundColor = StyleConstants.BaseLight;
|
||||
|
||||
const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(container);
|
||||
const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(container, selectedNodeState);
|
||||
const contextButtons = (buttons || []).concat(
|
||||
CommandBarComponentButtonFactory.createContextCommandBarButtons(container)
|
||||
CommandBarComponentButtonFactory.createContextCommandBarButtons(container, selectedNodeState)
|
||||
);
|
||||
const controlButtons = CommandBarComponentButtonFactory.createControlCommandBarButtons(container);
|
||||
|
||||
|
@ -53,8 +54,8 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||
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));
|
||||
if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
|
||||
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker"));
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
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 { useNotebook } from "../../Notebook/useNotebook";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
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;
|
||||
|
@ -22,15 +29,10 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
mockExplorer.isNotebookEnabled = ko.observable(false);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
|
||||
});
|
||||
|
||||
it("Account is not serverless - button should be visible", () => {
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableAzureSynapseLinkBtn = buttons.find(
|
||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
|
||||
);
|
||||
|
@ -45,7 +47,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableAzureSynapseLinkBtn = buttons.find(
|
||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
|
||||
);
|
||||
|
@ -55,6 +57,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||
|
||||
describe("Enable notebook button", () => {
|
||||
const enableNotebookBtnLabel = "Enable Notebooks (Preview)";
|
||||
const selectedNodeState = useSelectedNode.getState();
|
||||
|
||||
beforeAll(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
|
@ -66,44 +69,39 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
updateUserContext({
|
||||
portalEnv: "prod",
|
||||
});
|
||||
useNotebook.getState().setIsNotebookEnabled(false);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
|
||||
});
|
||||
|
||||
it("Notebooks is already enabled - button should be hidden", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
expect(enableNotebookBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
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",
|
||||
});
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
expect(enableNotebookBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled but is available - button should be shown and enabled", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(false);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
expect(enableNotebookBtn).toBeDefined();
|
||||
expect(enableNotebookBtn.disabled).toBe(false);
|
||||
|
@ -111,10 +109,7 @@ 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);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
expect(enableNotebookBtn).toBeDefined();
|
||||
expect(enableNotebookBtn.disabled).toBe(true);
|
||||
|
@ -126,6 +121,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||
|
||||
describe("Open Mongo Shell button", () => {
|
||||
const openMongoShellBtnLabel = "Open Mongo Shell";
|
||||
const selectedNodeState = useSelectedNode.getState();
|
||||
|
||||
beforeAll(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
|
@ -136,34 +132,32 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
|
||||
mockExplorer.isShellEnabled = ko.observable(true);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
updateUserContext({
|
||||
apiType: "SQL",
|
||||
});
|
||||
useNotebook.getState().setIsShellEnabled(false);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
updateUserContext({
|
||||
apiType: "Mongo",
|
||||
});
|
||||
mockExplorer.isNotebookEnabled = ko.observable(false);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
|
||||
useNotebook.getState().setIsShellEnabled(true);
|
||||
});
|
||||
|
||||
mockExplorer.isShellEnabled = ko.observable(true);
|
||||
afterEach(() => {
|
||||
useNotebook.getState().setIsNotebookEnabled(false);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
|
||||
});
|
||||
|
||||
it("Mongo Api not available - button should be hidden", () => {
|
||||
updateUserContext({
|
||||
apiType: "SQL",
|
||||
});
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeUndefined();
|
||||
});
|
||||
|
@ -173,29 +167,29 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||
portalEnv: "mooncake",
|
||||
});
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
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);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled and is available - button should be hidden", () => {
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeDefined();
|
||||
expect(openMongoShellBtn.disabled).toBe(false);
|
||||
|
@ -203,10 +197,10 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||
});
|
||||
|
||||
it("Notebooks is enabled and is available - button should be shown and enabled", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeDefined();
|
||||
expect(openMongoShellBtn.disabled).toBe(false);
|
||||
|
@ -214,11 +208,11 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||
});
|
||||
|
||||
it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
mockExplorer.isShellEnabled = ko.observable(false);
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
useNotebook.getState().setIsShellEnabled(false);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeUndefined();
|
||||
});
|
||||
|
@ -226,6 +220,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||
|
||||
describe("Open Cassandra Shell button", () => {
|
||||
const openCassandraShellBtnLabel = "Open Cassandra Shell";
|
||||
const selectedNodeState = useSelectedNode.getState();
|
||||
|
||||
beforeAll(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
|
@ -236,9 +231,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -249,8 +241,11 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
mockExplorer.isNotebookEnabled = ko.observable(false);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
useNotebook.getState().setIsNotebookEnabled(false);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
|
||||
});
|
||||
|
||||
it("Cassandra Api not available - button should be hidden", () => {
|
||||
|
@ -262,7 +257,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||
} as DatabaseAccount,
|
||||
});
|
||||
console.log(mockExplorer);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeUndefined();
|
||||
});
|
||||
|
@ -272,29 +267,29 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||
portalEnv: "mooncake",
|
||||
});
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
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);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled and is available - button should be shown and enabled", () => {
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeDefined();
|
||||
expect(openCassandraShellBtn.disabled).toBe(false);
|
||||
|
@ -302,10 +297,10 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||
});
|
||||
|
||||
it("Notebooks is enabled and is available - button should be shown and enabled", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeDefined();
|
||||
expect(openCassandraShellBtn.disabled).toBe(false);
|
||||
|
@ -316,6 +311,7 @@ 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,35 +323,28 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
|
||||
|
||||
mockExplorer.notebookManager = new NotebookManager();
|
||||
mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
useNotebook.getState().setIsNotebookEnabled(false);
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and GitHubOAuthService is not logged in - connect to github button should be visible", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
|
||||
expect(connectToGitHubBtn).toBeDefined();
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and GitHubOAuthService is logged in - manage github settings button should be visible", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
mockExplorer.notebookManager.gitHubOAuthService.isLoggedIn = jest.fn().mockReturnValue(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const manageGitHubSettingsBtn = buttons.find(
|
||||
(button) => button.commandButtonLabel === manageGitHubSettingsBtnLabel
|
||||
);
|
||||
|
@ -363,7 +352,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);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
|
||||
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
|
||||
expect(connectToGitHubBtn).toBeUndefined();
|
||||
|
@ -376,10 +365,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||
});
|
||||
|
||||
describe("Resource token", () => {
|
||||
const mockCollection = { id: ko.observable("test") } as CollectionBase;
|
||||
useSelectedNode.getState().setSelectedNode(mockCollection);
|
||||
useDatabases.setState({ resourceTokenCollection: mockCollection });
|
||||
const selectedNodeState = useSelectedNode.getState();
|
||||
|
||||
beforeAll(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true);
|
||||
|
||||
updateUserContext({
|
||||
authType: AuthType.ResourceToken,
|
||||
|
@ -392,7 +384,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||
kind: "DocumentDB",
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
expect(buttons.length).toBe(2);
|
||||
expect(buttons[0].commandButtonLabel).toBe("New SQL Query");
|
||||
expect(buttons[0].disabled).toBe(false);
|
||||
|
|
|
@ -28,15 +28,21 @@ import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
|
|||
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
import { useNotebook } from "../../Notebook/useNotebook";
|
||||
import { OpenFullScreen } from "../../OpenFullScreen";
|
||||
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
|
||||
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
import { SelectedNodeState } from "../../useSelectedNode";
|
||||
|
||||
let counter = 0;
|
||||
|
||||
export function createStaticCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||
export function createStaticCommandBarButtons(
|
||||
container: Explorer,
|
||||
selectedNodeState: SelectedNodeState
|
||||
): CommandButtonComponentProps[] {
|
||||
if (userContext.authType === AuthType.ResourceToken) {
|
||||
return createStaticCommandBarButtonsForResourceToken(container);
|
||||
return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState);
|
||||
}
|
||||
|
||||
const newCollectionBtn = createNewCollectionGroup(container);
|
||||
|
@ -58,7 +64,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
|
|||
|
||||
buttons.push(createDivider());
|
||||
|
||||
if (container.isNotebookEnabled()) {
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
const newNotebookButton = createNewNotebookButton(container);
|
||||
newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)];
|
||||
buttons.push(newNotebookButton);
|
||||
|
@ -71,7 +77,9 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
|
|||
|
||||
buttons.push(createNotebookWorkspaceResetButton(container));
|
||||
if (
|
||||
(userContext.apiType === "Mongo" && container.isShellEnabled() && container.isDatabaseNodeOrNoneSelected()) ||
|
||||
(userContext.apiType === "Mongo" &&
|
||||
useNotebook.getState().isShellEnabled &&
|
||||
selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
|
||||
userContext.apiType === "Cassandra"
|
||||
) {
|
||||
buttons.push(createDivider());
|
||||
|
@ -87,18 +95,18 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
|
|||
}
|
||||
}
|
||||
|
||||
if (!container.isDatabaseNodeOrNoneSelected()) {
|
||||
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
|
||||
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
||||
|
||||
if (isQuerySupported) {
|
||||
buttons.push(createDivider());
|
||||
const newSqlQueryBtn = createNewSQLQueryButton(container);
|
||||
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
|
||||
buttons.push(newSqlQueryBtn);
|
||||
}
|
||||
|
||||
if (isQuerySupported && container.selectedNode() && container.findSelectedCollection()) {
|
||||
if (isQuerySupported && selectedNodeState.findSelectedCollection()) {
|
||||
const openQueryBtn = createOpenQueryButton(container);
|
||||
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)];
|
||||
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
|
||||
buttons.push(openQueryBtn);
|
||||
}
|
||||
|
||||
|
@ -108,16 +116,16 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
|
|||
iconSrc: AddStoredProcedureIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: container.isDatabaseNodeOrNoneSelected(),
|
||||
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
|
||||
};
|
||||
|
||||
newStoredProcedureBtn.children = createScriptCommandButtons(container);
|
||||
newStoredProcedureBtn.children = createScriptCommandButtons(selectedNodeState);
|
||||
buttons.push(newStoredProcedureBtn);
|
||||
}
|
||||
}
|
||||
|
@ -125,17 +133,20 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
|
|||
return buttons;
|
||||
}
|
||||
|
||||
export function createContextCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||
export function createContextCommandBarButtons(
|
||||
container: Explorer,
|
||||
selectedNodeState: SelectedNodeState
|
||||
): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
|
||||
if (!container.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
|
||||
const label = container.isShellEnabled() ? "Open Mongo Shell" : "New Shell";
|
||||
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
|
||||
const label = useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell";
|
||||
const newMongoShellBtn: CommandButtonComponentProps = {
|
||||
iconSrc: HostedTerminalIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
if (container.isShellEnabled()) {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
if (useNotebook.getState().isShellEnabled) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
} else {
|
||||
selectedCollection && selectedCollection.onNewMongoShellClick();
|
||||
|
@ -144,7 +155,7 @@ export function createContextCommandBarButtons(container: Explorer): CommandButt
|
|||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: container.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo",
|
||||
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo",
|
||||
};
|
||||
buttons.push(newMongoShellBtn);
|
||||
}
|
||||
|
@ -166,7 +177,10 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
|
|||
},
|
||||
];
|
||||
|
||||
if (container.isHostedDataExplorerEnabled()) {
|
||||
const showOpenFullScreen =
|
||||
configContext.platform === Platform.Portal && !isRunningOnNationalCloud() && userContext.apiType !== "Gremlin";
|
||||
|
||||
if (showOpenFullScreen) {
|
||||
const label = "Open Full Screen";
|
||||
const fullScreenButton: CommandButtonComponentProps = {
|
||||
iconSrc: OpenInTabIcon,
|
||||
|
@ -178,7 +192,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
|
|||
ariaLabel: label,
|
||||
tooltipText: label,
|
||||
hasPopup: false,
|
||||
disabled: !container.isHostedDataExplorerEnabled(),
|
||||
disabled: !showOpenFullScreen,
|
||||
className: "OpenFullScreen",
|
||||
};
|
||||
buttons.push(fullScreenButton);
|
||||
|
@ -257,7 +271,7 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
|
|||
onCommandClick: () => container.openEnableSynapseLinkDialog(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: container.isSynapseLinkUpdating(),
|
||||
disabled: useNotebook.getState().isSynapseLinkUpdating,
|
||||
ariaLabel: label,
|
||||
};
|
||||
}
|
||||
|
@ -276,20 +290,20 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
|
|||
};
|
||||
}
|
||||
|
||||
function createNewSQLQueryButton(container: Explorer): CommandButtonComponentProps {
|
||||
function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandButtonComponentProps {
|
||||
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
|
||||
const label = "New SQL Query";
|
||||
return {
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: container.isDatabaseNodeOrNoneSelected(),
|
||||
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
|
||||
};
|
||||
} else if (userContext.apiType === "Mongo") {
|
||||
const label = "New Query";
|
||||
|
@ -297,23 +311,24 @@ function createNewSQLQueryButton(container: Explorer): CommandButtonComponentPro
|
|||
iconSrc: AddSqlQueryIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: container.isDatabaseNodeOrNoneSelected(),
|
||||
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function createScriptCommandButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||
export function createScriptCommandButtons(selectedNodeState: SelectedNodeState): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
|
||||
const shouldEnableScriptsCommands: boolean = !container.isDatabaseNodeOrNoneSelected() && areScriptsSupported();
|
||||
const shouldEnableScriptsCommands: boolean =
|
||||
!selectedNodeState.isDatabaseNodeOrNoneSelected() && areScriptsSupported();
|
||||
|
||||
if (shouldEnableScriptsCommands) {
|
||||
const label = "New Stored Procedure";
|
||||
|
@ -321,13 +336,13 @@ export function createScriptCommandButtons(container: Explorer): CommandButtonCo
|
|||
iconSrc: AddStoredProcedureIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: container.isDatabaseNodeOrNoneSelected(),
|
||||
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
|
||||
};
|
||||
buttons.push(newStoredProcedureBtn);
|
||||
}
|
||||
|
@ -338,13 +353,13 @@ export function createScriptCommandButtons(container: Explorer): CommandButtonCo
|
|||
iconSrc: AddUdfIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: container.isDatabaseNodeOrNoneSelected(),
|
||||
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
|
||||
};
|
||||
buttons.push(newUserDefinedFunctionBtn);
|
||||
}
|
||||
|
@ -355,13 +370,13 @@ export function createScriptCommandButtons(container: Explorer): CommandButtonCo
|
|||
iconSrc: AddTriggerIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: container.isDatabaseNodeOrNoneSelected(),
|
||||
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
|
||||
};
|
||||
buttons.push(newTriggerBtn);
|
||||
}
|
||||
|
@ -408,12 +423,12 @@ function createOpenQueryButton(container: Explorer): CommandButtonComponentProps
|
|||
};
|
||||
}
|
||||
|
||||
function createOpenQueryFromDiskButton(container: Explorer): CommandButtonComponentProps {
|
||||
function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
|
||||
const label = "Open Query From Disk";
|
||||
return {
|
||||
iconSrc: OpenQueryFromDiskIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane explorer={container} />),
|
||||
onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane />),
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
|
@ -436,9 +451,9 @@ function createEnableNotebooksButton(container: Explorer): CommandButtonComponen
|
|||
onCommandClick: () => container.openSetupNotebooksPanel(label, description),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !container.isNotebooksEnabledForAccount(),
|
||||
disabled: !useNotebook.getState().isNotebooksEnabledForAccount,
|
||||
ariaLabel: label,
|
||||
tooltipText: container.isNotebooksEnabledForAccount() ? "" : tooltip,
|
||||
tooltipText: useNotebook.getState().isNotebooksEnabledForAccount ? "" : tooltip,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -462,12 +477,13 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
|
|||
const title = "Set up workspace";
|
||||
const description =
|
||||
"Looks like you have not created a workspace for this account. To proceed and start using features including mongo shell and notebook, we will need to create a default workspace in this account.";
|
||||
const disableButton = !container.isNotebooksEnabledForAccount() && !container.isNotebookEnabled();
|
||||
const disableButton =
|
||||
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
|
||||
return {
|
||||
iconSrc: HostedTerminalIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
if (container.isNotebookEnabled()) {
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
} else {
|
||||
container.openSetupNotebooksPanel(title, description);
|
||||
|
@ -488,12 +504,13 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
|
|||
const title = "Set up workspace";
|
||||
const description =
|
||||
"Looks like you have not created a workspace for this account. To proceed and start using features including cassandra shell and notebook, we will need to create a default workspace in this account.";
|
||||
const disableButton = !container.isNotebooksEnabledForAccount() && !container.isNotebookEnabled();
|
||||
const disableButton =
|
||||
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
|
||||
return {
|
||||
iconSrc: HostedTerminalIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
if (container.isNotebookEnabled()) {
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
||||
} else {
|
||||
container.openSetupNotebooksPanel(title, description);
|
||||
|
@ -534,19 +551,25 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
|
|||
};
|
||||
}
|
||||
|
||||
function createStaticCommandBarButtonsForResourceToken(container: Explorer): CommandButtonComponentProps[] {
|
||||
const newSqlQueryBtn = createNewSQLQueryButton(container);
|
||||
function createStaticCommandBarButtonsForResourceToken(
|
||||
container: Explorer,
|
||||
selectedNodeState: SelectedNodeState
|
||||
): CommandButtonComponentProps[] {
|
||||
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
|
||||
const openQueryBtn = createOpenQueryButton(container);
|
||||
|
||||
newSqlQueryBtn.disabled = !container.isResourceTokenCollectionNodeSelected();
|
||||
const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
|
||||
const isResourceTokenCollectionNodeSelected: boolean =
|
||||
resourceTokenCollection?.id() === selectedNodeState.selectedNode?.id();
|
||||
newSqlQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
|
||||
newSqlQueryBtn.onCommandClick = () => {
|
||||
const resourceTokenCollection: ViewModels.CollectionBase = container.resourceTokenCollection();
|
||||
const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
|
||||
resourceTokenCollection && resourceTokenCollection.onNewQueryClick(resourceTokenCollection, undefined);
|
||||
};
|
||||
|
||||
openQueryBtn.disabled = !container.isResourceTokenCollectionNodeSelected();
|
||||
openQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
|
||||
if (!openQueryBtn.disabled) {
|
||||
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)];
|
||||
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
|
||||
}
|
||||
|
||||
return [newSqlQueryBtn, openQueryBtn];
|
||||
|
|
|
@ -6,16 +6,14 @@ import {
|
|||
IDropdownOption,
|
||||
IDropdownStyles,
|
||||
} from "@fluentui/react";
|
||||
import { Observable } from "knockout";
|
||||
import * as React from "react";
|
||||
import _ from "underscore";
|
||||
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
|
||||
import { StyleConstants } from "../../../Common/Constants";
|
||||
import { MemoryUsageInfo } from "../../../Contracts/DataModels";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import { MemoryTrackerComponent } from "./MemoryTrackerComponent";
|
||||
import { MemoryTracker } from "./MemoryTrackerComponent";
|
||||
|
||||
/**
|
||||
* Convert our NavbarButtonConfig to UI Fabric buttons
|
||||
|
@ -185,12 +183,9 @@ export const createDivider = (key: string): ICommandBarItemProps => {
|
|||
};
|
||||
};
|
||||
|
||||
export const createMemoryTracker = (
|
||||
key: string,
|
||||
memoryUsageInfo: Observable<MemoryUsageInfo>
|
||||
): ICommandBarItemProps => {
|
||||
export const createMemoryTracker = (key: string): ICommandBarItemProps => {
|
||||
return {
|
||||
key,
|
||||
onRender: () => <MemoryTrackerComponent memoryUsageInfo={memoryUsageInfo} />,
|
||||
onRender: () => <MemoryTracker />,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,48 +1,29 @@
|
|||
import { ProgressIndicator, Spinner, SpinnerSize, Stack } from "@fluentui/react";
|
||||
import { Observable, Subscription } from "knockout";
|
||||
import * as React from "react";
|
||||
import { MemoryUsageInfo } from "../../../Contracts/DataModels";
|
||||
|
||||
interface MemoryTrackerProps {
|
||||
memoryUsageInfo: Observable<MemoryUsageInfo>;
|
||||
}
|
||||
|
||||
export class MemoryTrackerComponent extends React.Component<MemoryTrackerProps> {
|
||||
private memoryUsageInfoSubscription: Subscription;
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.memoryUsageInfoSubscription = this.props.memoryUsageInfo.subscribe(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.memoryUsageInfoSubscription && this.memoryUsageInfoSubscription.dispose();
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const memoryUsageInfo: MemoryUsageInfo = this.props.memoryUsageInfo();
|
||||
if (!memoryUsageInfo) {
|
||||
return (
|
||||
<Stack className="memoryTrackerContainer" horizontal>
|
||||
<span>Memory</span>
|
||||
<Spinner size={SpinnerSize.medium} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const totalGB = memoryUsageInfo.totalKB / 1048576;
|
||||
const usedGB = totalGB - memoryUsageInfo.freeKB / 1048576;
|
||||
import { useNotebook } from "../../Notebook/useNotebook";
|
||||
|
||||
export const MemoryTracker: React.FC = (): JSX.Element => {
|
||||
const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo);
|
||||
if (!memoryUsageInfo) {
|
||||
return (
|
||||
<Stack className="memoryTrackerContainer" horizontal>
|
||||
<span>Memory</span>
|
||||
<ProgressIndicator
|
||||
className={usedGB / totalGB > 0.8 ? "lowMemory" : ""}
|
||||
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
|
||||
percentComplete={usedGB / totalGB}
|
||||
/>
|
||||
<Spinner size={SpinnerSize.medium} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const totalGB = memoryUsageInfo.totalKB / 1048576;
|
||||
const usedGB = totalGB - memoryUsageInfo.freeKB / 1048576;
|
||||
|
||||
return (
|
||||
<Stack className="memoryTrackerContainer" horizontal>
|
||||
<span>Memory</span>
|
||||
<ProgressIndicator
|
||||
className={usedGB / totalGB > 0.8 ? "lowMemory" : ""}
|
||||
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
|
||||
percentComplete={usedGB / totalGB}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
makeStateRecord,
|
||||
makeTransformsRecord,
|
||||
} from "@nteract/core";
|
||||
import { configOption, createConfigCollection, defineConfigOption } from "@nteract/mythic-configuration";
|
||||
import { configOption, defineConfigOption } from "@nteract/mythic-configuration";
|
||||
import { Media } from "@nteract/outputs";
|
||||
import TransformVDOM from "@nteract/transform-vdom";
|
||||
import * as Immutable from "immutable";
|
||||
|
@ -242,22 +242,27 @@ export class NotebookClientV2 {
|
|||
);
|
||||
|
||||
// Additional configuration
|
||||
this.store.dispatch(configOption("editorType").action(params.cellEditorType ?? "monaco"));
|
||||
this.store.dispatch(configOption("editorType").action(params.cellEditorType ?? "codemirror"));
|
||||
this.store.dispatch(
|
||||
configOption("autoSaveInterval").action(params.autoSaveInterval ?? Constants.Notebook.autoSaveIntervalMs)
|
||||
);
|
||||
createConfigCollection({
|
||||
key: "monaco",
|
||||
});
|
||||
defineConfigOption({
|
||||
label: "Show Line numbers",
|
||||
key: "monaco.lineNumbers",
|
||||
values: [
|
||||
{ label: "Yes", value: true },
|
||||
{ label: "No", value: false },
|
||||
],
|
||||
defaultValue: true,
|
||||
});
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
import { webSocket } from "rxjs/webSocket";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { Areas } from "../../../Common/Constants";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
|
||||
|
@ -776,9 +777,11 @@ const closeUnsupportedMimetypesEpic = (
|
|||
if (explorer && !TextFile.handles(mimetype)) {
|
||||
const filepath = action.payload.filepath;
|
||||
// Close tab and show error message
|
||||
explorer.tabsManager.closeTabsByComparator(
|
||||
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
|
||||
);
|
||||
useTabs
|
||||
.getState()
|
||||
.closeTabsByComparator(
|
||||
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
|
||||
);
|
||||
const msg = `${filepath} cannot be rendered. Please download the file, in order to view it outside of Data Explorer.`;
|
||||
explorer.showOkModalDialog("File cannot be rendered", msg);
|
||||
logConsoleError(msg);
|
||||
|
@ -804,9 +807,11 @@ const closeContentFailedToFetchEpic = (
|
|||
if (explorer) {
|
||||
const filepath = action.payload.filepath;
|
||||
// Close tab and show error message
|
||||
explorer.tabsManager.closeTabsByComparator(
|
||||
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
|
||||
);
|
||||
useTabs
|
||||
.getState()
|
||||
.closeTabsByComparator(
|
||||
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
|
||||
);
|
||||
const msg = `Failed to load file: ${filepath}.`;
|
||||
explorer.showOkModalDialog("Failure to load", msg);
|
||||
logConsoleError(msg);
|
||||
|
|
|
@ -8,25 +8,26 @@ import * as DataModels from "../../Contracts/DataModels";
|
|||
import { userContext } from "../../UserContext";
|
||||
import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { useNotebook } from "./useNotebook";
|
||||
|
||||
export class NotebookContainerClient {
|
||||
private clearReconnectionAttemptMessage? = () => {};
|
||||
private isResettingWorkspace: boolean;
|
||||
|
||||
constructor(
|
||||
private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>,
|
||||
private onConnectionLost: () => void,
|
||||
private onMemoryUsageInfoUpdate: (update: DataModels.MemoryUsageInfo) => void
|
||||
) {
|
||||
if (notebookServerInfo() && notebookServerInfo().notebookServerEndpoint) {
|
||||
constructor(private onConnectionLost: () => void) {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (notebookServerInfo?.notebookServerEndpoint) {
|
||||
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
||||
} else {
|
||||
const subscription = notebookServerInfo.subscribe((newServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => {
|
||||
if (newServerInfo && newServerInfo.notebookServerEndpoint) {
|
||||
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
||||
}
|
||||
subscription.dispose();
|
||||
});
|
||||
const unsub = useNotebook.subscribe(
|
||||
(newServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => {
|
||||
if (newServerInfo?.notebookServerEndpoint) {
|
||||
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
||||
}
|
||||
unsub();
|
||||
},
|
||||
(state) => state.notebookServerInfo
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,13 +37,14 @@ export class NotebookContainerClient {
|
|||
private scheduleHeartbeat(delayMs: number): void {
|
||||
setTimeout(() => {
|
||||
this.getMemoryUsage()
|
||||
.then((memoryUsageInfo) => this.onMemoryUsageInfoUpdate(memoryUsageInfo))
|
||||
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo))
|
||||
.finally(() => this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs));
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
private async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
|
||||
if (!this.notebookServerInfo() || !this.notebookServerInfo().notebookServerEndpoint) {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
|
||||
const error = "No server endpoint detected";
|
||||
Logger.logError(error, "NotebookContainerClient/getMemoryUsage");
|
||||
return Promise.reject(error);
|
||||
|
@ -98,7 +100,8 @@ export class NotebookContainerClient {
|
|||
}
|
||||
|
||||
private async _resetWorkspace(): Promise<void> {
|
||||
if (!this.notebookServerInfo() || !this.notebookServerInfo().notebookServerEndpoint) {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
|
||||
const error = "No server endpoint detected";
|
||||
Logger.logError(error, "NotebookContainerClient/resetWorkspace");
|
||||
return Promise.reject(error);
|
||||
|
@ -117,15 +120,11 @@ export class NotebookContainerClient {
|
|||
}
|
||||
|
||||
private getNotebookServerConfig(): { notebookServerEndpoint: string; authToken: string } {
|
||||
let authToken: string,
|
||||
notebookServerEndpoint = this.notebookServerInfo().notebookServerEndpoint,
|
||||
token = this.notebookServerInfo().authToken;
|
||||
if (token) {
|
||||
authToken = `Token ${token}`;
|
||||
}
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
const authToken: string = notebookServerInfo.authToken ? `Token ${notebookServerInfo.authToken}` : undefined;
|
||||
|
||||
return {
|
||||
notebookServerEndpoint,
|
||||
notebookServerEndpoint: notebookServerInfo.notebookServerEndpoint,
|
||||
authToken,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
import { stringifyNotebook } from "@nteract/commutable";
|
||||
import { FileType, IContent, IContentProvider, IEmptyContent, ServerConfig } from "@nteract/core";
|
||||
import { AjaxResponse } from "rxjs/ajax";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as StringUtils from "../../Utils/StringUtils";
|
||||
import * as FileSystemUtil from "./FileSystemUtil";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
||||
import { NotebookUtil } from "./NotebookUtil";
|
||||
import { useNotebook } from "./useNotebook";
|
||||
|
||||
export class NotebookContentClient {
|
||||
constructor(
|
||||
private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>,
|
||||
public notebookBasePath: ko.Observable<string>,
|
||||
private contentProvider: IContentProvider
|
||||
) {}
|
||||
constructor(private contentProvider: IContentProvider) {}
|
||||
|
||||
/**
|
||||
* This updates the item and points all the children's parent to this item
|
||||
|
@ -271,9 +267,10 @@ export class NotebookContentClient {
|
|||
}
|
||||
|
||||
private getServerConfig(): ServerConfig {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
return {
|
||||
endpoint: this.notebookServerInfo().notebookServerEndpoint,
|
||||
token: this.notebookServerInfo().authToken,
|
||||
endpoint: notebookServerInfo.notebookServerEndpoint,
|
||||
token: notebookServerInfo.authToken,
|
||||
crossDomain: true,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,13 +4,11 @@
|
|||
|
||||
import { ImmutableNotebook } from "@nteract/commutable";
|
||||
import type { IContentProvider } from "@nteract/core";
|
||||
import ko from "knockout";
|
||||
import React from "react";
|
||||
import { contents } from "rx-jupyter";
|
||||
import { Areas, HttpStatusCodes } from "../../Common/Constants";
|
||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { MemoryUsageInfo } from "../../Contracts/DataModels";
|
||||
import { GitHubClient } from "../../GitHub/GitHubClient";
|
||||
import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider";
|
||||
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
|
||||
|
@ -37,7 +35,6 @@ export type { NotebookPaneContent };
|
|||
|
||||
export interface NotebookManagerOptions {
|
||||
container: Explorer;
|
||||
notebookBasePath: ko.Observable<string>;
|
||||
resourceTree: ResourceTreeAdapter;
|
||||
refreshCommandBarButtons: () => void;
|
||||
refreshNotebookList: () => void;
|
||||
|
@ -81,17 +78,11 @@ export default class NotebookManager {
|
|||
contents.JupyterContentProvider
|
||||
);
|
||||
|
||||
this.notebookClient = new NotebookContainerClient(
|
||||
this.params.container.notebookServerInfo,
|
||||
() => this.params.container.initNotebooks(userContext?.databaseAccount),
|
||||
(update: MemoryUsageInfo) => this.params.container.memoryUsageInfo(update)
|
||||
this.notebookClient = new NotebookContainerClient(() =>
|
||||
this.params.container.initNotebooks(userContext?.databaseAccount)
|
||||
);
|
||||
|
||||
this.notebookContentClient = new NotebookContentClient(
|
||||
this.params.container.notebookServerInfo,
|
||||
this.params.notebookBasePath,
|
||||
this.notebookContentProvider
|
||||
);
|
||||
this.notebookContentClient = new NotebookContentClient(this.notebookContentProvider);
|
||||
|
||||
this.gitHubOAuthService.getTokenObservable().subscribe((token) => {
|
||||
this.gitHubClient.setToken(token?.access_token);
|
||||
|
|
|
@ -1,56 +1,68 @@
|
|||
.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 {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.CodeMirror-scroll {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.CodeMirror-lines {
|
||||
cursor: default;
|
||||
}
|
||||
.CodeMirror-lines {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.nteract-cell:hover {
|
||||
border: 1px solid #0078d4;
|
||||
border-left: 3px solid #0078d4;
|
||||
.CodeMirror {
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.CodeMirror-scroll {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
.CodeMirror-scroll,
|
||||
.CodeMirror-linenumber,
|
||||
.CodeMirror-gutters {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.nteract-cell-outputs {
|
||||
border-top: 1px solid #d7d7d7;
|
||||
}
|
||||
.nteract-cell:hover {
|
||||
border: 1px solid #0078d4;
|
||||
border-left: 3px solid #0078d4;
|
||||
|
||||
.nteract-md-cell {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
.CodeMirror-scroll,
|
||||
.CodeMirror-linenumber,
|
||||
.CodeMirror-gutters {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.nteract-cell-outputs {
|
||||
padding: 10px;
|
||||
border-top: 1px solid #ffffff;
|
||||
|
||||
pre {
|
||||
background-color: #ffffff;
|
||||
border: none;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
border-top: 1px solid #d7d7d7;
|
||||
}
|
||||
|
||||
.nteract-md-cell {
|
||||
background-color: #f5f5f5;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.nteract-cell:hover.nteract-md-cell {
|
||||
background-color: #ffffff;
|
||||
.nteract-cell-outputs {
|
||||
padding: 10px;
|
||||
border-top: 1px solid #ffffff;
|
||||
|
||||
pre {
|
||||
background-color: #ffffff;
|
||||
border: none;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.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 MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
|
||||
import CodeMirrorEditor from "@nteract/stateful-components/lib/inputs/connected-editors/codemirror";
|
||||
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: {
|
||||
monaco: (props: PassedEditorProps) =>
|
||||
this.props.hideInputs ? <></> : <MonacoEditor readOnly={true} {...props} editorType={"monaco"} />,
|
||||
codemirror: (props: PassedEditorProps) =>
|
||||
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} editorType="codemirror" />,
|
||||
},
|
||||
}}
|
||||
</CodeCell>
|
||||
|
@ -84,8 +84,8 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
|
|||
<RawCell id={id} contentRef={contentRef} cell_type="raw">
|
||||
{{
|
||||
editor: {
|
||||
monaco: (props: PassedEditorProps) =>
|
||||
this.props.hideInputs ? <></> : <MonacoEditor {...props} readOnly={true} editorType={"monaco"} />,
|
||||
codemirror: (props: PassedEditorProps) =>
|
||||
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} editorType="codemirror" />,
|
||||
},
|
||||
}}
|
||||
</RawCell>
|
||||
|
|
|
@ -3,110 +3,122 @@
|
|||
@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-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-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 {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.nteract-cell-container.selected {
|
||||
.nteract-cell {
|
||||
border: 1px solid @HighlightColor;
|
||||
border-left: 3px solid @HighlightColor;
|
||||
}
|
||||
.CodeMirror-scroll,
|
||||
.CodeMirror-linenumber,
|
||||
.CodeMirror-gutters {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.CodeMirror-linenumber {
|
||||
color: #015CDA;
|
||||
}
|
||||
.nteract-cell:hover {
|
||||
border: 1px solid @HoverColor;
|
||||
border-left: 3px solid @HoverColor;
|
||||
|
||||
.nteract-cell-outputs {
|
||||
border-top: 1px solid @HoverColor;
|
||||
}
|
||||
.CellContextMenuButton {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nteract-md-cell {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
.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-cell-outputs {
|
||||
padding: 10px;
|
||||
border-top: 1px solid #ffffff;
|
||||
|
||||
pre {
|
||||
background-color: #ffffff;
|
||||
border: none;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
border-top: 1px solid @HoverColor;
|
||||
}
|
||||
|
||||
.nteract-md-cell {
|
||||
background-color: #f5f5f5;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.nteract-cell:hover.nteract-md-cell {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
.nteract-cell-outputs {
|
||||
padding: 10px;
|
||||
border-top: 1px solid #ffffff;
|
||||
|
||||
.nteract-md-cell .ntreact-cell-source {
|
||||
width: 100%;
|
||||
pre {
|
||||
background-color: #ffffff;
|
||||
border: none;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.nteract-md-cell {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.nteract-cell:hover.nteract-md-cell {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.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 MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
|
||||
import CodeMirrorEditor from "@nteract/stateful-components/lib/inputs/connected-editors/codemirror";
|
||||
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
|
||||
import * as React from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
|
@ -120,7 +120,9 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
|
|||
<CodeCell id={id} contentRef={contentRef} cell_type="code">
|
||||
{{
|
||||
editor: {
|
||||
monaco: (props: PassedEditorProps) => <MonacoEditor {...props} editorType={"monaco"} />,
|
||||
codemirror: (props: PassedEditorProps) => (
|
||||
<CodeMirrorEditor {...props} editorType="codemirror" />
|
||||
),
|
||||
},
|
||||
prompt: ({ id, contentRef }: { id: CellId; contentRef: ContentRef }) => (
|
||||
<Prompt id={id} contentRef={contentRef} isHovered={false}>
|
||||
|
@ -142,7 +144,9 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
|
|||
<MarkdownCell id={id} contentRef={contentRef} cell_type="markdown">
|
||||
{{
|
||||
editor: {
|
||||
monaco: (props: PassedEditorProps) => <MonacoEditor {...props} editorType={"monaco"} />,
|
||||
codemirror: (props: PassedEditorProps) => (
|
||||
<CodeMirrorEditor {...props} editorType="codemirror" />
|
||||
),
|
||||
},
|
||||
toolbar: () => <CellToolbar id={id} contentRef={contentRef} />,
|
||||
}}
|
||||
|
@ -157,7 +161,9 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
|
|||
<RawCell id={id} contentRef={contentRef} cell_type="raw">
|
||||
{{
|
||||
editor: {
|
||||
monaco: (props: PassedEditorProps) => <MonacoEditor {...props} editorType={"monaco"} />,
|
||||
codemirror: (props: PassedEditorProps) => (
|
||||
<CodeMirrorEditor {...props} editorType="codemirror" />
|
||||
),
|
||||
},
|
||||
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,6 +24,12 @@ describe("auto start kernel", () => {
|
|||
defaultValue: 1234,
|
||||
});
|
||||
|
||||
defineConfigOption({
|
||||
label: "Line numbers",
|
||||
key: "codeMirror.lineNumbers",
|
||||
defaultValue: true,
|
||||
});
|
||||
|
||||
[true, false].forEach((isReadOnly) => {
|
||||
new NotebookClientV2({
|
||||
connectionInfo: {
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
import create, { UseStore } from "zustand";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||
|
||||
interface NotebookState {
|
||||
isNotebookEnabled: boolean;
|
||||
isNotebooksEnabledForAccount: boolean;
|
||||
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
||||
sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo;
|
||||
isSynapseLinkUpdating: boolean;
|
||||
memoryUsageInfo: DataModels.MemoryUsageInfo;
|
||||
isShellEnabled: boolean;
|
||||
notebookBasePath: string;
|
||||
isInitializingNotebooks: boolean;
|
||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
|
||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
|
||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
|
||||
setSparkClusterConnectionInfo: (sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo) => void;
|
||||
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => void;
|
||||
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => void;
|
||||
setIsShellEnabled: (isShellEnabled: boolean) => void;
|
||||
setNotebookBasePath: (notebookBasePath: string) => void;
|
||||
refreshNotebooksEnabledStateForAccount: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useNotebook: UseStore<NotebookState> = create((set) => ({
|
||||
isNotebookEnabled: false,
|
||||
isNotebooksEnabledForAccount: false,
|
||||
notebookServerInfo: {
|
||||
notebookServerEndpoint: undefined,
|
||||
authToken: undefined,
|
||||
},
|
||||
sparkClusterConnectionInfo: {
|
||||
userName: undefined,
|
||||
password: undefined,
|
||||
endpoints: [],
|
||||
},
|
||||
isSynapseLinkUpdating: false,
|
||||
memoryUsageInfo: undefined,
|
||||
isShellEnabled: false,
|
||||
notebookBasePath: Constants.Notebook.defaultBasePath,
|
||||
isInitializingNotebooks: false,
|
||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
|
||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
|
||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
|
||||
set({ notebookServerInfo }),
|
||||
setSparkClusterConnectionInfo: (sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo) =>
|
||||
set({ sparkClusterConnectionInfo }),
|
||||
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => set({ isSynapseLinkUpdating }),
|
||||
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }),
|
||||
setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }),
|
||||
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
|
||||
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
|
||||
const { databaseAccount, authType } = userContext;
|
||||
if (
|
||||
authType === AuthType.EncryptedToken ||
|
||||
authType === AuthType.ResourceToken ||
|
||||
authType === AuthType.MasterKey
|
||||
) {
|
||||
set({ isNotebooksEnabledForAccount: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const firstWriteLocation =
|
||||
databaseAccount?.properties?.writeLocations &&
|
||||
databaseAccount?.properties?.writeLocations[0]?.locationName.toLowerCase();
|
||||
const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
try {
|
||||
const response = await fetch(disallowedLocationsUri, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
resourceTypes: [Constants.ArmResourceTypes.notebookWorkspaces],
|
||||
}),
|
||||
headers: {
|
||||
[authorizationHeader.header]: authorizationHeader.token,
|
||||
[Constants.HttpHeaders.contentType]: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch disallowed locations");
|
||||
}
|
||||
|
||||
const disallowedLocations: string[] = await response.json();
|
||||
if (!disallowedLocations) {
|
||||
Logger.logInfo("No disallowed locations found", "Explorer/isNotebooksEnabledForAccount");
|
||||
set({ isNotebooksEnabledForAccount: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// firstWriteLocation should not be disallowed
|
||||
const isAccountInAllowedLocation = firstWriteLocation && disallowedLocations.indexOf(firstWriteLocation) === -1;
|
||||
set({ isNotebooksEnabledForAccount: isAccountInAllowedLocation });
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount");
|
||||
set({ isNotebooksEnabledForAccount: false });
|
||||
}
|
||||
},
|
||||
}));
|
|
@ -31,6 +31,7 @@ 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";
|
||||
|
@ -125,6 +126,8 @@ 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 && (
|
||||
|
@ -137,7 +140,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||
|
||||
{!this.state.errorMessage && this.isFreeTierAccount() && (
|
||||
<PanelInfoErrorComponent
|
||||
message={getUpsellMessage(userContext.portalEnv, true, this.props.explorer.isFirstResourceCreated(), true)}
|
||||
message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)}
|
||||
messageType="info"
|
||||
showErrorDetails={false}
|
||||
link={Constants.Urls.freeTierInformation}
|
||||
|
@ -240,9 +243,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||
|
||||
{!isServerlessAccount() && this.state.isSharedThroughputChecked && (
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={
|
||||
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
|
||||
}
|
||||
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
|
||||
isDatabase={true}
|
||||
isSharded={this.state.isSharded}
|
||||
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
|
||||
|
@ -469,9 +470,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||
|
||||
{this.shouldShowCollectionThroughputInput() && (
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={
|
||||
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
|
||||
}
|
||||
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
|
||||
isDatabase={false}
|
||||
isSharded={this.state.isSharded}
|
||||
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
|
||||
|
@ -680,7 +679,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||
}
|
||||
|
||||
private getDatabaseOptions(): IDropdownOption[] {
|
||||
return this.props.explorer?.databases()?.map((database) => ({
|
||||
return useDatabases.getState().databases?.map((database) => ({
|
||||
key: database.id(),
|
||||
text: database.id(),
|
||||
}));
|
||||
|
@ -772,9 +771,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||
return false;
|
||||
}
|
||||
|
||||
const selectedDatabase = this.props.explorer
|
||||
.databases()
|
||||
?.find((database) => database.id() === this.state.selectedDatabaseId);
|
||||
const selectedDatabase = useDatabases
|
||||
.getState()
|
||||
.databases?.find((database) => database.id() === this.state.selectedDatabaseId);
|
||||
return !!selectedDatabase?.offer();
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +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";
|
||||
|
@ -172,7 +173,12 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
|||
<RightPaneForm {...props}>
|
||||
{!formErrors && isFreeTierAccount && (
|
||||
<PanelInfoErrorComponent
|
||||
message={getUpsellMessage(userContext.portalEnv, true, container.isFirstResourceCreated(), true)}
|
||||
message={getUpsellMessage(
|
||||
userContext.portalEnv,
|
||||
true,
|
||||
useDatabases.getState().isFirstResourceCreated(),
|
||||
true
|
||||
)}
|
||||
messageType="info"
|
||||
showErrorDetails={false}
|
||||
link={Constants.Urls.freeTierInformation}
|
||||
|
@ -225,7 +231,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
|||
|
||||
{!isServerlessAccount() && databaseCreateNewShared && (
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !container?.isFirstResourceCreated()}
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
|
||||
isDatabase={true}
|
||||
isSharded={databaseCreateNewShared}
|
||||
setThroughputValue={(newThroughput: number) => (throughput = newThroughput)}
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
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;
|
||||
|
@ -17,6 +19,16 @@ 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} />);
|
||||
|
|
|
@ -4,6 +4,7 @@ import { logError } from "../../../Common/Logger";
|
|||
import { Query } from "../../../Contracts/DataModels";
|
||||
import { Collection } from "../../../Contracts/ViewModels";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { trace } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../../UserContext";
|
||||
|
@ -12,7 +13,9 @@ import {
|
|||
QueriesGridComponentProps,
|
||||
} from "../../Controls/QueriesGridReactComponent/QueriesGridComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
import QueryTab from "../../Tabs/QueryTab";
|
||||
import { NewQueryTab } from "../../Tabs/QueryTab/QueryTab";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
|
||||
interface BrowseQueriesPaneProps {
|
||||
explorer: Explorer;
|
||||
|
@ -23,7 +26,7 @@ export const BrowseQueriesPane: FunctionComponent<BrowseQueriesPaneProps> = ({
|
|||
}: BrowseQueriesPaneProps): JSX.Element => {
|
||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||
const loadSavedQuery = (savedQuery: Query): void => {
|
||||
const selectedCollection: Collection = explorer && explorer.findSelectedCollection();
|
||||
const selectedCollection: Collection = useSelectedNode.getState().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");
|
||||
|
@ -31,13 +34,13 @@ export const BrowseQueriesPane: FunctionComponent<BrowseQueriesPaneProps> = ({
|
|||
} else if (userContext.apiType === "Mongo") {
|
||||
selectedCollection.onNewMongoQueryClick(selectedCollection, undefined);
|
||||
} else {
|
||||
selectedCollection.onNewQueryClick(selectedCollection, undefined);
|
||||
selectedCollection.onNewQueryClick(selectedCollection, undefined, savedQuery.query);
|
||||
}
|
||||
const queryTab = explorer.tabsManager.activeTab() as QueryTab;
|
||||
|
||||
const queryTab = useTabs.getState().activeTab as NewQueryTab;
|
||||
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,
|
||||
|
@ -45,12 +48,13 @@ export const BrowseQueriesPane: FunctionComponent<BrowseQueriesPaneProps> = ({
|
|||
});
|
||||
closeSidePanel();
|
||||
};
|
||||
const isSaveQueryEnabled = useDatabases((state) => state.isSaveQueryEnabled);
|
||||
|
||||
const props: QueriesGridComponentProps = {
|
||||
queriesClient: explorer.queriesClient,
|
||||
onQuerySelect: loadSavedQuery,
|
||||
containerVisible: true,
|
||||
saveQueryEnabled: explorer.canSaveQueries(),
|
||||
saveQueryEnabled: isSaveQueryEnabled(),
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -5,7 +5,6 @@ exports[`Browse queries panel Should render Default properly 1`] = `
|
|||
closePanel={[Function]}
|
||||
explorer={
|
||||
Object {
|
||||
"canSaveQueries": [Function],
|
||||
"queriesClient": Object {
|
||||
"getQueries": [Function],
|
||||
},
|
||||
|
|
|
@ -12,6 +12,7 @@ import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
|
|||
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";
|
||||
|
||||
|
@ -236,7 +237,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
|||
styles={{ root: { width: 300 }, title: { fontSize: 12 }, dropdownItem: { fontSize: 12 } }}
|
||||
placeholder="Choose existing keyspace id"
|
||||
defaultSelectedKey={existingKeyspaceId}
|
||||
options={container?.databases()?.map((keyspace) => ({
|
||||
options={useDatabases.getState().databases?.map((keyspace) => ({
|
||||
key: keyspace.id(),
|
||||
text: keyspace.id(),
|
||||
data: {
|
||||
|
@ -253,7 +254,9 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
|||
|
||||
{!isServerlessAccount() && keyspaceCreateNew && isKeyspaceShared && (
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !container.isFirstResourceCreated()}
|
||||
showFreeTierExceedThroughputTooltip={
|
||||
isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()
|
||||
}
|
||||
isDatabase
|
||||
isSharded
|
||||
setThroughputValue={(throughput: number) => (newKeySpaceThroughput = throughput)}
|
||||
|
@ -324,7 +327,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
|||
)}
|
||||
{!isServerlessAccount() && (!isKeyspaceShared || dedicateTableThroughput) && (
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !container.isFirstResourceCreated()}
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
|
||||
isDatabase={false}
|
||||
isSharded={false}
|
||||
setThroughputValue={(throughput: number) => (tableThroughput = throughput)}
|
||||
|
|
|
@ -9,6 +9,7 @@ import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
|||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem";
|
||||
import { useNotebook } from "../../Notebook/useNotebook";
|
||||
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
|
||||
|
@ -101,7 +102,7 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
|
|||
case "MyNotebooks":
|
||||
parent = {
|
||||
name: ResourceTreeAdapter.MyNotebooksTitle,
|
||||
path: container.getNotebookBasePath(),
|
||||
path: useNotebook.getState().notebookBasePath,
|
||||
type: NotebookContentItemType.Directory,
|
||||
};
|
||||
break;
|
||||
|
|
|
@ -1,101 +1,87 @@
|
|||
jest.mock("../../../Common/dataAccess/deleteCollection");
|
||||
jest.mock("../../../Shared/Telemetry/TelemetryProcessor");
|
||||
import { mount, ReactWrapper, shallow } from "enzyme";
|
||||
import { mount, 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, TreeNode } from "../../../Contracts/ViewModels";
|
||||
import { Collection, Database } 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("Explorer.isLastCollection()", () => {
|
||||
let explorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer();
|
||||
});
|
||||
describe("useDatabases.isLastCollection()", () => {
|
||||
beforeAll(() => useDatabases.getState().clearDatabases());
|
||||
afterEach(() => useDatabases.getState().clearDatabases());
|
||||
|
||||
it("should be true if 1 database and 1 collection", () => {
|
||||
const database = {} as Database;
|
||||
database.collections = ko.observableArray<Collection>([{} as Collection]);
|
||||
explorer.databases = ko.observableArray<Database>([database]);
|
||||
expect(explorer.isLastCollection()).toBe(true);
|
||||
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);
|
||||
});
|
||||
|
||||
it("should be false if if 1 database and 2 collection", () => {
|
||||
const database = {} as Database;
|
||||
database.collections = ko.observableArray<Collection>([{} as Collection, {} as Collection]);
|
||||
explorer.databases = ko.observableArray<Database>([database]);
|
||||
expect(explorer.isLastCollection()).toBe(false);
|
||||
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);
|
||||
});
|
||||
|
||||
it("should be false if 2 database and 1 collection each", () => {
|
||||
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);
|
||||
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);
|
||||
});
|
||||
|
||||
it("should be false if 0 databases", () => {
|
||||
const database = {} as Database;
|
||||
explorer.databases = ko.observableArray<Database>();
|
||||
database.collections = ko.observableArray<Collection>();
|
||||
expect(explorer.isLastCollection()).toBe(false);
|
||||
expect(useDatabases.getState().isLastCollection()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldRecordFeedback()", () => {
|
||||
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 props = {
|
||||
explorer: fakeExplorer,
|
||||
closePanel: (): void => undefined,
|
||||
collectionName: "container",
|
||||
};
|
||||
const wrapper = shallow(<DeleteCollectionConfirmationPane {...props} />);
|
||||
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true);
|
||||
|
||||
props.explorer.isLastCollection = () => true;
|
||||
props.explorer.isSelectedDatabaseShared = () => true;
|
||||
wrapper.setProps(props);
|
||||
const wrapper = shallow(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />);
|
||||
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
|
||||
|
||||
props.explorer.isLastCollection = () => false;
|
||||
props.explorer.isSelectedDatabaseShared = () => false;
|
||||
wrapper.setProps(props);
|
||||
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({});
|
||||
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true);
|
||||
|
||||
database.isDatabaseShared = ko.computed(() => true);
|
||||
wrapper.setProps({});
|
||||
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;
|
||||
fakeExplorer.isLastCollection = () => true;
|
||||
fakeExplorer.isSelectedDatabaseShared = () => false;
|
||||
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);
|
||||
|
||||
beforeAll(() => {
|
||||
updateUserContext({
|
||||
|
@ -113,15 +99,17 @@ describe("Delete Collection Confirmation Pane", () => {
|
|||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const props = {
|
||||
explorer: fakeExplorer,
|
||||
closePanel: (): void => undefined,
|
||||
collectionName: "container",
|
||||
};
|
||||
wrapper = mount(<DeleteCollectionConfirmationPane {...props} />);
|
||||
useDatabases.getState().addDatabases([database]);
|
||||
useSelectedNode.getState().setSelectedNode(collection);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
useDatabases.getState().clearDatabases();
|
||||
useSelectedNode.getState().setSelectedNode(undefined);
|
||||
});
|
||||
|
||||
it("should call delete collection", () => {
|
||||
const wrapper = mount(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
|
||||
|
@ -138,6 +126,7 @@ describe("Delete Collection Confirmation Pane", () => {
|
|||
});
|
||||
|
||||
it("should record feedback", async () => {
|
||||
const wrapper = mount(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />);
|
||||
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
|
||||
wrapper
|
||||
.find("#confirmCollectionId")
|
||||
|
|
|
@ -6,20 +6,23 @@ import DeleteFeedback from "../../../Common/DeleteFeedback";
|
|||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||
import { Collection } from "../../../Contracts/ViewModels";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import { DefaultExperienceUtility } from "../../../Shared/DefaultExperienceUtility";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
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;
|
||||
refreshDatabases: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectionConfirmationPaneProps> = ({
|
||||
explorer,
|
||||
refreshDatabases,
|
||||
}: DeleteCollectionConfirmationPaneProps) => {
|
||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||
const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>("");
|
||||
|
@ -27,13 +30,13 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
|||
const [formError, setFormError] = useState<string>("");
|
||||
const [isExecuting, setIsExecuting] = useState(false);
|
||||
|
||||
const shouldRecordFeedback = (): boolean => {
|
||||
return explorer.isLastCollection() && !explorer.isSelectedDatabaseShared();
|
||||
};
|
||||
const shouldRecordFeedback = (): boolean =>
|
||||
useDatabases.getState().isLastCollection() && !useDatabases.getState().findSelectedDatabase()?.isDatabaseShared();
|
||||
|
||||
const collectionName = getCollectionName().toLocaleLowerCase();
|
||||
const paneTitle = "Delete " + collectionName;
|
||||
const onSubmit = async (): Promise<void> => {
|
||||
const collection = explorer.findSelectedCollection();
|
||||
const collection = useSelectedNode.getState().findSelectedCollection();
|
||||
if (!collection || inputCollectionName !== collection.id()) {
|
||||
const errorMessage = "Input " + collectionName + " name does not match the selected " + collectionName;
|
||||
setFormError(errorMessage);
|
||||
|
@ -58,11 +61,13 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
|||
await deleteCollection(collection.databaseId, collection.id());
|
||||
|
||||
setIsExecuting(false);
|
||||
explorer.selectedNode(collection.database);
|
||||
explorer.tabsManager?.closeTabsByComparator(
|
||||
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
|
||||
);
|
||||
explorer.refreshAllDatabases();
|
||||
useSelectedNode.getState().setSelectedNode(collection.database);
|
||||
useTabs
|
||||
.getState()
|
||||
.closeTabsByComparator(
|
||||
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
|
||||
);
|
||||
refreshDatabases();
|
||||
|
||||
TelemetryProcessor.traceSuccess(Action.DeleteCollection, paneInfo, startKey);
|
||||
|
||||
|
|
|
@ -2,18 +2,7 @@
|
|||
|
||||
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],
|
||||
}
|
||||
}
|
||||
refreshDatabases={[Function]}
|
||||
>
|
||||
<RightPaneForm
|
||||
formError=""
|
||||
|
@ -373,355 +362,6 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||
</TextFieldBase>
|
||||
</StyledTextFieldBase>
|
||||
</div>
|
||||
<div
|
||||
className="deleteCollectionFeedback"
|
||||
>
|
||||
<Text
|
||||
block={true}
|
||||
variant="small"
|
||||
>
|
||||
<span
|
||||
className="css-66"
|
||||
>
|
||||
Help us improve Azure Cosmos DB!
|
||||
</span>
|
||||
</Text>
|
||||
<Text
|
||||
block={true}
|
||||
variant="small"
|
||||
>
|
||||
<span
|
||||
className="css-66"
|
||||
>
|
||||
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-55"
|
||||
>
|
||||
<div
|
||||
className="ms-TextField-wrapper"
|
||||
>
|
||||
<div
|
||||
className="ms-TextField-fieldGroup fieldGroup-67"
|
||||
>
|
||||
<textarea
|
||||
aria-invalid={false}
|
||||
className="ms-TextField-field field-68"
|
||||
id="deleteCollectionFeedbackInput"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onInput={[Function]}
|
||||
rows={3}
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TextFieldBase>
|
||||
</StyledTextFieldBase>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PanelFooterComponent
|
||||
|
@ -2434,7 +2074,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||
>
|
||||
<button
|
||||
aria-label="OK"
|
||||
className="ms-Button ms-Button--primary root-70"
|
||||
className="ms-Button ms-Button--primary root-66"
|
||||
data-is-focusable={true}
|
||||
id="sidePanelOkButton"
|
||||
onClick={[Function]}
|
||||
|
@ -2446,16 +2086,16 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||
type="submit"
|
||||
>
|
||||
<span
|
||||
className="ms-Button-flexContainer flexContainer-71"
|
||||
className="ms-Button-flexContainer flexContainer-67"
|
||||
data-automationid="splitbuttonprimary"
|
||||
>
|
||||
<span
|
||||
className="ms-Button-textContainer textContainer-72"
|
||||
className="ms-Button-textContainer textContainer-68"
|
||||
>
|
||||
<span
|
||||
className="ms-Button-label label-74"
|
||||
id="id__6"
|
||||
key="id__6"
|
||||
className="ms-Button-label label-70"
|
||||
id="id__3"
|
||||
key="id__3"
|
||||
>
|
||||
OK
|
||||
</span>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
jest.mock("../../Common/dataAccess/deleteDatabase");
|
||||
jest.mock("../../Shared/Telemetry/TelemetryProcessor");
|
||||
import { mount, ReactWrapper, shallow } from "enzyme";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import * as ko from "knockout";
|
||||
import React from "react";
|
||||
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
|
||||
|
@ -10,129 +10,98 @@ import { Collection, Database } 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 { DeleteDatabaseConfirmationPanel } from "./DeleteDatabaseConfirmationPanel";
|
||||
|
||||
describe("Delete Database Confirmation Pane", () => {
|
||||
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;
|
||||
const selectedDatabaseId = "testDatabase";
|
||||
let database: Database;
|
||||
|
||||
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);
|
||||
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);
|
||||
});
|
||||
|
||||
describe("submit()", () => {
|
||||
const selectedDatabaseId = "testDatabse";
|
||||
const fakeExplorer = new Explorer();
|
||||
fakeExplorer.refreshAllDatabases = () => undefined;
|
||||
fakeExplorer.isLastCollection = () => true;
|
||||
fakeExplorer.isSelectedDatabaseShared = () => false;
|
||||
beforeEach(() => {
|
||||
database = {} as Database;
|
||||
database.collections = ko.observableArray<Collection>([{ id: ko.observable("testCollection") } as Collection]);
|
||||
database.id = ko.observable<string>(selectedDatabaseId);
|
||||
database.nodeKind = "Database";
|
||||
|
||||
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();
|
||||
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 refreshDatabases={() => undefined} />);
|
||||
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(true);
|
||||
|
||||
useDatabases.getState().addDatabases([database]);
|
||||
wrapper.setProps({});
|
||||
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false);
|
||||
useDatabases.getState().clearDatabases();
|
||||
});
|
||||
|
||||
it("Should call delete database", () => {
|
||||
const wrapper = mount(<DeleteDatabaseConfirmationPanel refreshDatabases={() => undefined} />);
|
||||
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 refreshDatabases={() => undefined} />);
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,30 +7,32 @@ import DeleteFeedback from "../../Common/DeleteFeedback";
|
|||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import { Collection, Database } from "../../Contracts/ViewModels";
|
||||
import { useSidePanel } from "../../hooks/useSidePanel";
|
||||
import { useTabs } from "../../hooks/useTabs";
|
||||
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
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;
|
||||
refreshDatabases: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseConfirmationPanelProps> = ({
|
||||
explorer,
|
||||
selectedDatabase,
|
||||
refreshDatabases,
|
||||
}: 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 = useDatabases.getState().findSelectedDatabase();
|
||||
|
||||
const submit = async (): Promise<void> => {
|
||||
if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) {
|
||||
|
@ -50,15 +52,18 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
|||
try {
|
||||
await deleteDatabase(selectedDatabase.id());
|
||||
closeSidePanel();
|
||||
explorer.refreshAllDatabases();
|
||||
explorer.tabsManager.closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id());
|
||||
explorer.selectedNode(undefined);
|
||||
refreshDatabases();
|
||||
useTabs.getState().closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id());
|
||||
useSelectedNode.getState().setSelectedNode(undefined);
|
||||
selectedDatabase
|
||||
.collections()
|
||||
.forEach((collection: Collection) =>
|
||||
explorer.tabsManager.closeTabsByComparator(
|
||||
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
|
||||
)
|
||||
useTabs
|
||||
.getState()
|
||||
.closeTabsByComparator(
|
||||
(tab) =>
|
||||
tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
|
||||
)
|
||||
);
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.DeleteDatabase,
|
||||
|
@ -70,7 +75,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
|||
startKey
|
||||
);
|
||||
|
||||
if (shouldRecordFeedback()) {
|
||||
if (isLastNonEmptyDatabase()) {
|
||||
const deleteFeedback = new DeleteFeedback(
|
||||
userContext?.databaseAccount.id,
|
||||
userContext?.databaseAccount.name,
|
||||
|
@ -100,10 +105,6 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
|||
}
|
||||
};
|
||||
|
||||
const shouldRecordFeedback = (): boolean => {
|
||||
return explorer.isLastNonEmptyDatabase() || (explorer.isLastDatabase() && explorer.isSelectedDatabaseShared());
|
||||
};
|
||||
|
||||
const props: RightPaneFormProps = {
|
||||
formError,
|
||||
isExecuting: isLoading,
|
||||
|
@ -134,7 +135,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
{shouldRecordFeedback() && (
|
||||
{isLastNonEmptyDatabase() && (
|
||||
<div className="deleteDatabaseFeedback">
|
||||
<Text variant="small" block>
|
||||
Help us improve Azure Cosmos DB!
|
||||
|
|
|
@ -19,21 +19,8 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
|
|||
"container": Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"canSaveQueries": [Function],
|
||||
"databases": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isHostedDataExplorerEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isResourceTokenCollectionNodeSelected": [Function],
|
||||
"isSchemaEnabled": [Function],
|
||||
"isShellEnabled": [Function],
|
||||
"isSynapseLinkUpdating": [Function],
|
||||
"isTabsContentExpanded": [Function],
|
||||
"memoryUsageInfo": [Function],
|
||||
"notebookBasePath": [Function],
|
||||
"notebookServerInfo": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"provideFeedbackEmail": [Function],
|
||||
|
@ -41,26 +28,15 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
|
|||
"container": [Circular],
|
||||
},
|
||||
"refreshNotebookList": [Function],
|
||||
"resourceTokenCollection": [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],
|
||||
"sparkClusterConnectionInfo": [Function],
|
||||
"tabsManager": TabsManager {
|
||||
"activeTab": [Function],
|
||||
"openedTabs": [Function],
|
||||
},
|
||||
},
|
||||
"getRepo": [Function],
|
||||
"pinRepo": [Function],
|
||||
|
|
|
@ -1,17 +1,10 @@
|
|||
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 fakeExplorer = {} as Explorer;
|
||||
const props = {
|
||||
explorer: fakeExplorer,
|
||||
closePanel: (): void => undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<LoadQueryPane {...props} />);
|
||||
const wrapper = shallow(<LoadQueryPane />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,15 +7,10 @@ import { Collection } from "../../../Contracts/ViewModels";
|
|||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import QueryTab from "../../Tabs/QueryTab";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
|
||||
interface LoadQueryPaneProps {
|
||||
explorer: Explorer;
|
||||
}
|
||||
|
||||
export const LoadQueryPane: FunctionComponent<LoadQueryPaneProps> = ({ explorer }: LoadQueryPaneProps): JSX.Element => {
|
||||
export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
|
||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
|
||||
const [formError, setFormError] = useState<string>("");
|
||||
|
@ -59,21 +54,20 @@ export const LoadQueryPane: FunctionComponent<LoadQueryPaneProps> = ({ explorer
|
|||
};
|
||||
|
||||
const loadQueryFromFile = async (file: File): Promise<void> => {
|
||||
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 selectedCollection: Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
const reader = new FileReader();
|
||||
let fileData: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
reader.onload = (evt: any): void => {
|
||||
const fileData: string = evt.target.result;
|
||||
const queryTab = explorer.tabsManager.activeTab() as QueryTab;
|
||||
queryTab.initialEditorContent(fileData);
|
||||
queryTab.sqlQueryEditorContent(fileData);
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = (): void => {
|
||||
|
|
|
@ -1,32 +1,38 @@
|
|||
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,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<SaveQueryPane {...props} />);
|
||||
|
||||
it("should return true if can save Queries else false", () => {
|
||||
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.exists("#saveQueryInput")).toBe(false);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
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} />);
|
||||
expect(wrapper.exists("#saveQueryInput")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,11 +5,13 @@ import { Areas, SavedQueries } from "../../../Common/Constants";
|
|||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||
import { Query } from "../../../Contracts/DataModels";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import QueryTab from "../../Tabs/QueryTab";
|
||||
import { NewQueryTab } from "../../Tabs/QueryTab/QueryTab";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
|
||||
interface SaveQueryPaneProps {
|
||||
|
@ -24,17 +26,18 @@ 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 { canSaveQueries } = explorer;
|
||||
const isSaveQueryEnabled = useDatabases((state) => state.isSaveQueryEnabled);
|
||||
|
||||
const submit = async (): Promise<void> => {
|
||||
setFormError("");
|
||||
if (!canSaveQueries()) {
|
||||
if (!isSaveQueryEnabled()) {
|
||||
setFormError("Cannot save query");
|
||||
logConsoleError("Failed to save query: account not setup to save queries");
|
||||
}
|
||||
|
||||
const queryTab = explorer && (explorer.tabsManager.activeTab() as QueryTab);
|
||||
const query: string = queryTab && queryTab.sqlQueryEditorContent();
|
||||
const queryTab = useTabs.getState().activeTab as NewQueryTab;
|
||||
const query: string = queryTab && queryTab.iTabAccessor.onSaveClickEvent();
|
||||
|
||||
if (!queryName || queryName.length === 0) {
|
||||
setFormError("No query name specified");
|
||||
logConsoleError("Could not save query -- No query name specified. Please specify a query name.");
|
||||
|
@ -128,16 +131,16 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({ explorer
|
|||
const props: RightPaneFormProps = {
|
||||
formError: formError,
|
||||
isExecuting: isLoading,
|
||||
submitButtonText: canSaveQueries() ? "Save" : "Complete setup",
|
||||
submitButtonText: isSaveQueryEnabled() ? "Save" : "Complete setup",
|
||||
onSubmit: () => {
|
||||
canSaveQueries() ? submit() : setupQueries();
|
||||
isSaveQueryEnabled() ? submit() : setupQueries();
|
||||
},
|
||||
};
|
||||
return (
|
||||
<RightPaneForm {...props}>
|
||||
<div className="panelFormWrapper">
|
||||
<div className="panelMainContent">
|
||||
{!canSaveQueries() ? (
|
||||
{!isSaveQueryEnabled() ? (
|
||||
<Text variant="small">{setupSaveQueriesText}</Text>
|
||||
) : (
|
||||
<TextField
|
||||
|
|
|
@ -63,7 +63,7 @@ export const SetupNoteBooksPanel: FunctionComponent<SetupNoteBooksPanelProps> =
|
|||
userContext.databaseAccount.name,
|
||||
"default"
|
||||
);
|
||||
explorer.isAccountReady.valueHasMutated(); // re-trigger init notebooks
|
||||
explorer.refreshExplorer();
|
||||
|
||||
closeSidePanel();
|
||||
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import { TextField } from "@fluentui/react";
|
||||
import React, { FormEvent, FunctionComponent, useState } from "react";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
|
||||
import { NotebookContentItem } from "../../Notebook/NotebookContentItem";
|
||||
import NotebookV2Tab from "../../Tabs/NotebookV2Tab";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
|
||||
export interface StringInputPanelProps {
|
||||
explorer: Explorer;
|
||||
closePanel: () => void;
|
||||
errorMessage: string;
|
||||
inProgressMessage: string;
|
||||
|
@ -23,7 +22,6 @@ export interface StringInputPanelProps {
|
|||
}
|
||||
|
||||
export const StringInputPane: FunctionComponent<StringInputPanelProps> = ({
|
||||
explorer: container,
|
||||
closePanel,
|
||||
errorMessage,
|
||||
inProgressMessage,
|
||||
|
@ -55,10 +53,12 @@ export const StringInputPane: FunctionComponent<StringInputPanelProps> = ({
|
|||
logConsoleInfo(`${successMessage}: ${stringInput}`);
|
||||
const originalPath = notebookFile.path;
|
||||
|
||||
const notebookTabs = container.tabsManager.getTabs(
|
||||
ViewModels.CollectionTabKind.NotebookV2,
|
||||
(tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath)
|
||||
);
|
||||
const notebookTabs = useTabs
|
||||
.getState()
|
||||
.getTabs(
|
||||
ViewModels.CollectionTabKind.NotebookV2,
|
||||
(tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath)
|
||||
);
|
||||
notebookTabs.forEach((tab) => {
|
||||
tab.tabTitle(newNotebookFile.name);
|
||||
tab.tabPath(newNotebookFile.path);
|
||||
|
|
|
@ -9,21 +9,8 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
|||
Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"canSaveQueries": [Function],
|
||||
"databases": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isHostedDataExplorerEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isResourceTokenCollectionNodeSelected": [Function],
|
||||
"isSchemaEnabled": [Function],
|
||||
"isShellEnabled": [Function],
|
||||
"isSynapseLinkUpdating": [Function],
|
||||
"isTabsContentExpanded": [Function],
|
||||
"memoryUsageInfo": [Function],
|
||||
"notebookBasePath": [Function],
|
||||
"notebookServerInfo": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"provideFeedbackEmail": [Function],
|
||||
|
@ -31,26 +18,15 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
|||
"container": [Circular],
|
||||
},
|
||||
"refreshNotebookList": [Function],
|
||||
"resourceTokenCollection": [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],
|
||||
"sparkClusterConnectionInfo": [Function],
|
||||
"tabsManager": TabsManager {
|
||||
"activeTab": [Function],
|
||||
"openedTabs": [Function],
|
||||
},
|
||||
}
|
||||
}
|
||||
inProgressMessage="Creating directory "
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
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 {...props} />);
|
||||
const wrapper = shallow(<UploadItemsPane />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,15 +3,11 @@ 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 interface UploadItemsPaneProps {
|
||||
explorer: Explorer;
|
||||
}
|
||||
|
||||
export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ explorer }: UploadItemsPaneProps) => {
|
||||
export const UploadItemsPane: FunctionComponent = () => {
|
||||
const [files, setFiles] = useState<FileList>();
|
||||
const [uploadFileData, setUploadFileData] = useState<UploadDetailsRecord[]>([]);
|
||||
const [formError, setFormError] = useState<string>("");
|
||||
|
@ -25,7 +21,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ explo
|
|||
return;
|
||||
}
|
||||
|
||||
const selectedCollection = explorer.findSelectedCollection();
|
||||
const selectedCollection = useSelectedNode.getState().findSelectedCollection();
|
||||
setIsExecuting(true);
|
||||
|
||||
selectedCollection
|
||||
|
|
|
@ -1,67 +1,8 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Delete Database Confirmation Pane submit() Should call delete database 1`] = `
|
||||
exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
<DeleteDatabaseConfirmationPanel
|
||||
closePanel={[Function]}
|
||||
explorer={
|
||||
Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"canSaveQueries": [Function],
|
||||
"databases": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isHostedDataExplorerEnabled": [Function],
|
||||
"isLastCollection": [Function],
|
||||
"isLastNonEmptyDatabase": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isResourceTokenCollectionNodeSelected": [Function],
|
||||
"isSchemaEnabled": [Function],
|
||||
"isSelectedDatabaseShared": [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],
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"databaseCollectionIdMap": Map {},
|
||||
"koSubsCollectionIdMap": Map {},
|
||||
"koSubsDatabaseIdMap": Map {},
|
||||
"parameters": [Function],
|
||||
},
|
||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||
"container": [Circular],
|
||||
"parameters": [Function],
|
||||
},
|
||||
"selectedDatabaseId": [Function],
|
||||
"selectedNode": [Function],
|
||||
"sparkClusterConnectionInfo": [Function],
|
||||
"tabsManager": TabsManager {
|
||||
"activeTab": [Function],
|
||||
"openedTabs": [Function],
|
||||
},
|
||||
}
|
||||
}
|
||||
openNotificationConsole={[Function]}
|
||||
selectedDatabase={
|
||||
Object {
|
||||
"collections": [Function],
|
||||
"id": [Function],
|
||||
}
|
||||
}
|
||||
refreshDatabases={[Function]}
|
||||
>
|
||||
<RightPaneForm
|
||||
formError=""
|
||||
|
@ -750,7 +691,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
|
|||
variant="small"
|
||||
>
|
||||
<span
|
||||
className="css-77"
|
||||
className="css-69"
|
||||
>
|
||||
Help us improve Azure Cosmos DB!
|
||||
</span>
|
||||
|
@ -760,7 +701,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
|
|||
variant="small"
|
||||
>
|
||||
<span
|
||||
className="css-77"
|
||||
className="css-69"
|
||||
>
|
||||
What is the reason why you are deleting this database?
|
||||
</span>
|
||||
|
@ -1068,11 +1009,11 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
|
|||
className="ms-TextField-wrapper"
|
||||
>
|
||||
<div
|
||||
className="ms-TextField-fieldGroup fieldGroup-78"
|
||||
className="ms-TextField-fieldGroup fieldGroup-70"
|
||||
>
|
||||
<textarea
|
||||
aria-invalid={false}
|
||||
className="ms-TextField-field field-79"
|
||||
className="ms-TextField-field field-71"
|
||||
id="deleteDatabaseFeedbackInput"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
|
@ -2798,7 +2739,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
|
|||
>
|
||||
<button
|
||||
aria-label="OK"
|
||||
className="ms-Button ms-Button--primary root-69"
|
||||
className="ms-Button ms-Button--primary root-73"
|
||||
data-is-focusable={true}
|
||||
id="sidePanelOkButton"
|
||||
onClick={[Function]}
|
||||
|
@ -2810,16 +2751,16 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
|
|||
type="submit"
|
||||
>
|
||||
<span
|
||||
className="ms-Button-flexContainer flexContainer-70"
|
||||
className="ms-Button-flexContainer flexContainer-74"
|
||||
data-automationid="splitbuttonprimary"
|
||||
>
|
||||
<span
|
||||
className="ms-Button-textContainer textContainer-71"
|
||||
className="ms-Button-textContainer textContainer-75"
|
||||
>
|
||||
<span
|
||||
className="ms-Button-label label-73"
|
||||
id="id__3"
|
||||
key="id__3"
|
||||
className="ms-Button-label label-77"
|
||||
id="id__6"
|
||||
key="id__6"
|
||||
>
|
||||
OK
|
||||
</span>
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
import * as ko from "knockout";
|
||||
import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
|
||||
import Explorer from "../Explorer";
|
||||
import { TabsManager } from "../Tabs/TabsManager";
|
||||
import { SplashScreen } from "./SplashScreen";
|
||||
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,6 +22,9 @@ import { FeaturePanelLauncher } from "../Controls/FeaturePanel/FeaturePanelLaunc
|
|||
import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
|
||||
import Explorer from "../Explorer";
|
||||
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
|
||||
import { useNotebook } from "../Notebook/useNotebook";
|
||||
import { useDatabases } from "../useDatabases";
|
||||
import { useSelectedNode } from "../useSelectedNode";
|
||||
|
||||
export interface SplashScreenItem {
|
||||
iconSrc: string;
|
||||
|
@ -59,8 +62,13 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||
|
||||
public componentDidMount() {
|
||||
this.subscriptions.push(
|
||||
this.container.selectedNode.subscribe(() => this.setState({})),
|
||||
this.container.isNotebookEnabled.subscribe(() => this.setState({}))
|
||||
{
|
||||
dispose: useNotebook.subscribe(
|
||||
() => this.setState({}),
|
||||
(state) => state.isNotebookEnabled
|
||||
),
|
||||
},
|
||||
{ dispose: useSelectedNode.subscribe(() => this.setState({})) }
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -208,7 +216,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||
});
|
||||
}
|
||||
|
||||
if (this.container.isNotebookEnabled()) {
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
heroes.push({
|
||||
iconSrc: NewNotebookIcon,
|
||||
title: "New Notebook",
|
||||
|
@ -227,12 +235,12 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||
return items;
|
||||
}
|
||||
|
||||
if (!this.container.isDatabaseNodeOrNoneSelected()) {
|
||||
if (!useSelectedNode.getState().isDatabaseNodeOrNoneSelected()) {
|
||||
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
|
||||
items.push({
|
||||
iconSrc: NewQueryIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null);
|
||||
},
|
||||
title: "New SQL Query",
|
||||
|
@ -242,7 +250,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||
items.push({
|
||||
iconSrc: NewQueryIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null);
|
||||
},
|
||||
title: "New Query",
|
||||
|
@ -265,20 +273,14 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||
title: "New Stored Procedure",
|
||||
description: null,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/* Scale & Settings */
|
||||
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 isShared = useDatabases.getState().findSelectedDatabase()?.isDatabaseShared();
|
||||
|
||||
const label = isShared ? "Settings" : "Scale & Settings";
|
||||
items.push({
|
||||
|
@ -286,7 +288,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||
title: label,
|
||||
description: null,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onSettingsClick();
|
||||
},
|
||||
});
|
||||
|
@ -308,8 +310,8 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||
title: collectionId,
|
||||
description: "Data",
|
||||
onClick: () => {
|
||||
const collection = this.container.findCollection(databaseId, collectionId);
|
||||
collection && collection.openTab();
|
||||
const collection = useDatabases.getState().findCollection(databaseId, collectionId);
|
||||
collection?.openTab();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export var TableType = {
|
||||
export const TableType = {
|
||||
String: "String",
|
||||
Boolean: "Boolean",
|
||||
Binary: "Binary",
|
||||
|
@ -9,7 +9,7 @@ export var TableType = {
|
|||
Int64: "Int64",
|
||||
};
|
||||
|
||||
export var CassandraType = {
|
||||
export const CassandraType = {
|
||||
Ascii: "Ascii",
|
||||
Bigint: "Bigint",
|
||||
Blob: "Blob",
|
||||
|
@ -27,12 +27,12 @@ export var CassandraType = {
|
|||
Tinyint: "Tinyint",
|
||||
};
|
||||
|
||||
export var ClauseRule = {
|
||||
export const ClauseRule = {
|
||||
And: "And",
|
||||
Or: "Or",
|
||||
};
|
||||
|
||||
export var Operator = {
|
||||
export const Operator = {
|
||||
EqualTo: "==",
|
||||
GreaterThan: ">",
|
||||
GreaterThanOrEqualTo: ">=",
|
||||
|
@ -42,7 +42,7 @@ export var Operator = {
|
|||
Equal: "=",
|
||||
};
|
||||
|
||||
export var ODataOperator = {
|
||||
export const ODataOperator = {
|
||||
EqualTo: "eq",
|
||||
GreaterThan: "gt",
|
||||
GreaterThanOrEqualTo: "ge",
|
||||
|
@ -51,7 +51,7 @@ export var ODataOperator = {
|
|||
NotEqualTo: "ne",
|
||||
};
|
||||
|
||||
export var timeOptions = {
|
||||
export const timeOptions = {
|
||||
lastHour: "Last hour",
|
||||
last24Hours: "Last 24 hours",
|
||||
last7Days: "Last 7 days",
|
||||
|
@ -62,7 +62,7 @@ export var timeOptions = {
|
|||
custom: "Custom...",
|
||||
};
|
||||
|
||||
export var htmlSelectors = {
|
||||
export const htmlSelectors = {
|
||||
dataTableSelector: "#storageTable",
|
||||
dataTableAllRowsSelector: "#storageTable tbody tr",
|
||||
dataTableHeadRowSelector: ".dataTable thead tr",
|
||||
|
@ -84,9 +84,9 @@ export var htmlSelectors = {
|
|||
selectAllDropdownSelector: "#select-all-dropdown",
|
||||
};
|
||||
|
||||
export var defaultHeader = " ";
|
||||
export const defaultHeader = " ";
|
||||
|
||||
export var EntityKeyNames = {
|
||||
export const EntityKeyNames = {
|
||||
PartitionKey: "PartitionKey",
|
||||
RowKey: "RowKey",
|
||||
Timestamp: "Timestamp",
|
||||
|
@ -94,7 +94,7 @@ export var EntityKeyNames = {
|
|||
Etag: "etag",
|
||||
};
|
||||
|
||||
export var htmlAttributeNames = {
|
||||
export const htmlAttributeNames = {
|
||||
dataTableNameAttr: "name_attr",
|
||||
dataTableContentTypeAttr: "contentType_attr",
|
||||
dataTableSnapshotAttr: "snapshot_attr",
|
||||
|
@ -103,14 +103,14 @@ export var htmlAttributeNames = {
|
|||
dataTableHeaderIndex: "data-column-index",
|
||||
};
|
||||
|
||||
export var cssColors = {
|
||||
export const cssColors = {
|
||||
commonControlsButtonActive: "#B4C7DC" /* A darker shade of [{common-controls-button-hover-background}] */,
|
||||
};
|
||||
|
||||
export var clauseGroupColors = ["#ffe1ff", "#fffacd", "#f0ffff", "#ffefd5", "#f0fff0"];
|
||||
export var transparentColor = "transparent";
|
||||
export const clauseGroupColors = ["#ffe1ff", "#fffacd", "#f0ffff", "#ffefd5", "#f0fff0"];
|
||||
export const transparentColor = "transparent";
|
||||
|
||||
export var keyCodes = {
|
||||
export const keyCodes = {
|
||||
RightClick: 3,
|
||||
Enter: 13,
|
||||
Esc: 27,
|
||||
|
@ -163,7 +163,7 @@ export var keyCodes = {
|
|||
Dash: 189,
|
||||
};
|
||||
|
||||
export var InputType = {
|
||||
export const InputType = {
|
||||
Text: "text",
|
||||
// Chrome doesn't support datetime, instead, datetime-local is supported.
|
||||
DateTime: "datetime-local",
|
||||
|
|
|
@ -792,7 +792,7 @@ export default class QueryBuilderViewModel {
|
|||
return null;
|
||||
}
|
||||
|
||||
public checkIfClauseChanged(clause: QueryClauseViewModel): void {
|
||||
this._queryViewModel.checkIfBuilderChanged(clause);
|
||||
public checkIfClauseChanged(): void {
|
||||
this._queryViewModel.checkIfBuilderChanged();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -89,7 +89,7 @@ export default class QueryClauseViewModel {
|
|||
);
|
||||
|
||||
this.and_or.subscribe((value) => {
|
||||
this._queryBuilderViewModel.checkIfClauseChanged(this);
|
||||
this._queryBuilderViewModel.checkIfClauseChanged();
|
||||
});
|
||||
this.field.subscribe((value) => {
|
||||
this.changeField();
|
||||
|
@ -103,13 +103,13 @@ export default class QueryClauseViewModel {
|
|||
// }
|
||||
});
|
||||
this.customTimeValue.subscribe((value) => {
|
||||
this._queryBuilderViewModel.checkIfClauseChanged(this);
|
||||
this._queryBuilderViewModel.checkIfClauseChanged();
|
||||
});
|
||||
this.value.subscribe((value) => {
|
||||
this._queryBuilderViewModel.checkIfClauseChanged(this);
|
||||
this._queryBuilderViewModel.checkIfClauseChanged();
|
||||
});
|
||||
this.operator.subscribe((value) => {
|
||||
this._queryBuilderViewModel.checkIfClauseChanged(this);
|
||||
this._queryBuilderViewModel.checkIfClauseChanged();
|
||||
});
|
||||
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);
|
||||
this._queryBuilderViewModel.checkIfClauseChanged();
|
||||
}
|
||||
|
||||
private resetFromTimestamp(): void {
|
||||
|
@ -216,7 +216,7 @@ export default class QueryClauseViewModel {
|
|||
this.timeValue("");
|
||||
this.customTimeValue("");
|
||||
}
|
||||
this._queryBuilderViewModel.checkIfClauseChanged(this);
|
||||
this._queryBuilderViewModel.checkIfClauseChanged();
|
||||
}
|
||||
|
||||
// private customTimestampDialog(): Promise<any> {
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
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 topValueLimitMessage: string = "Please input a number between 0 and 1000.";
|
||||
public readonly 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);
|
||||
|
@ -49,7 +51,7 @@ export default class QueryViewModel {
|
|||
this.queryTextIsReadOnly = ko.computed<boolean>(() => {
|
||||
return userContext.apiType !== "Cassandra";
|
||||
});
|
||||
let initialOptions = this._tableEntityListViewModel.headers;
|
||||
const initialOptions = this._tableEntityListViewModel.headers;
|
||||
this.columnOptions = ko.observableArray<string>(initialOptions);
|
||||
this.focusTopResult = ko.observable<boolean>(false);
|
||||
this.focusExpandIcon = ko.observable<boolean>(false);
|
||||
|
@ -63,12 +65,12 @@ export default class QueryViewModel {
|
|||
this.topValue() !== this.unchangedSaveTop()
|
||||
);
|
||||
|
||||
this.queryBuilderViewModel().clauseArray.subscribe((value) => {
|
||||
this.queryBuilderViewModel().clauseArray.subscribe(() => {
|
||||
this.setFilter();
|
||||
});
|
||||
|
||||
this.isExceedingLimit = ko.computed<boolean>(() => {
|
||||
var currentTopValue: number = this.topValue();
|
||||
const currentTopValue: number = this.topValue();
|
||||
return currentTopValue < 0 || currentTopValue > 1000;
|
||||
});
|
||||
|
||||
|
@ -111,7 +113,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: any, event: KeyboardEvent): boolean => {
|
||||
public ontoggleAdvancedOptionsKeyDown = (source: string, event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||
this.toggleAdvancedOptions();
|
||||
event.stopPropagation();
|
||||
|
@ -125,31 +127,29 @@ export default class QueryViewModel {
|
|||
};
|
||||
|
||||
private setFilter = (): string => {
|
||||
var queryString = this.isEditorActive()
|
||||
const queryString = this.isEditorActive()
|
||||
? this.queryText()
|
||||
: userContext.apiType === "Cassandra"
|
||||
? this.queryBuilderViewModel().getCqlFilterFromClauses()
|
||||
: this.queryBuilderViewModel().getODataFilterFromClauses();
|
||||
var filter = queryString;
|
||||
const filter = queryString;
|
||||
this.queryText(filter);
|
||||
return this.queryText();
|
||||
};
|
||||
|
||||
private setSqlFilter = (): string => {
|
||||
var filter = this.queryBuilderViewModel().getSqlFilterFromClauses();
|
||||
return filter;
|
||||
return this.queryBuilderViewModel().getSqlFilterFromClauses();
|
||||
};
|
||||
|
||||
private setCqlFilter = (): string => {
|
||||
var filter = this.queryBuilderViewModel().getCqlFilterFromClauses();
|
||||
return filter;
|
||||
return this.queryBuilderViewModel().getCqlFilterFromClauses();
|
||||
};
|
||||
|
||||
public isHelperEnabled = ko
|
||||
.computed<boolean>(() => {
|
||||
return (
|
||||
this.queryText() === this.unchangedText() ||
|
||||
this.queryText() === null ||
|
||||
this.queryText() === undefined ||
|
||||
this.queryText() === "" ||
|
||||
this.isHelperActive()
|
||||
);
|
||||
|
@ -159,13 +159,13 @@ export default class QueryViewModel {
|
|||
});
|
||||
|
||||
public runQuery = (): DataTables.DataTable => {
|
||||
var filter = this.setFilter();
|
||||
let filter = this.setFilter();
|
||||
if (filter && userContext.apiType !== "Cassandra") {
|
||||
filter = filter.replace(/"/g, "'");
|
||||
}
|
||||
var top = this.topValue();
|
||||
var selectOptions = this._getSelectedResults();
|
||||
var select = selectOptions;
|
||||
const top = this.topValue();
|
||||
const selectOptions = this._getSelectedResults();
|
||||
const 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(null);
|
||||
this.topValue(null);
|
||||
this.selectText(null);
|
||||
this.queryText();
|
||||
this.topValue();
|
||||
this.selectText();
|
||||
this.selectMessage("");
|
||||
// clears the queryBuilder and adds a new blank clause
|
||||
this.queryBuilderViewModel().queryClauses.removeAll();
|
||||
this.queryBuilderViewModel().addNewClause();
|
||||
this._tableEntityListViewModel.tableQuery.filter = null;
|
||||
this._tableEntityListViewModel.tableQuery.top = null;
|
||||
this._tableEntityListViewModel.tableQuery.select = null;
|
||||
this._tableEntityListViewModel.tableQuery.filter = undefined;
|
||||
this._tableEntityListViewModel.tableQuery.top = undefined;
|
||||
this._tableEntityListViewModel.tableQuery.select = undefined;
|
||||
this._tableEntityListViewModel.oDataQuery("");
|
||||
this._tableEntityListViewModel.sqlQuery("SELECT * FROM c");
|
||||
this._tableEntityListViewModel.cqlQuery(
|
||||
|
@ -197,12 +197,11 @@ export default class QueryViewModel {
|
|||
return this._tableEntityListViewModel.reloadTable(false);
|
||||
};
|
||||
|
||||
public selectQueryOptions(): Promise<any> {
|
||||
this.queryTablesTab.container.openTableSelectQueryPanel(this);
|
||||
return null;
|
||||
public selectQueryOptions() {
|
||||
useSidePanel.getState().openSidePanel("Select Column", <TableQuerySelectPanel queryViewModel={this} />);
|
||||
}
|
||||
|
||||
public onselectQueryOptionsKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
public onselectQueryOptionsKeyDown = (source: string, event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||
this.selectQueryOptions();
|
||||
event.stopPropagation();
|
||||
|
@ -212,7 +211,7 @@ export default class QueryViewModel {
|
|||
};
|
||||
|
||||
public getSelectMessage(): void {
|
||||
if (_.isEmpty(this.selectText()) || this.selectText() === null) {
|
||||
if (_.isEmpty(this.selectText()) || this.selectText() === undefined) {
|
||||
this.selectMessage("");
|
||||
} else {
|
||||
this.selectMessage(`${this.selectText().length} of ${this.columnOptions().length} columns selected.`);
|
||||
|
@ -220,7 +219,7 @@ export default class QueryViewModel {
|
|||
}
|
||||
|
||||
public isSelected = ko.computed<boolean>(() => {
|
||||
return !(_.isEmpty(this.selectText()) || this.selectText() === null);
|
||||
return !(_.isEmpty(this.selectText()) || this.selectText() === undefined);
|
||||
});
|
||||
|
||||
private setCheckToSave(): void {
|
||||
|
@ -230,7 +229,7 @@ export default class QueryViewModel {
|
|||
this.isSaveEnabled(false);
|
||||
}
|
||||
|
||||
public checkIfBuilderChanged(clause: QueryClauseViewModel): void {
|
||||
public checkIfBuilderChanged(): void {
|
||||
this.setFilter();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
import * as ko from "knockout";
|
||||
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { updateUserContext } from "../../UserContext";
|
||||
import Explorer from "../Explorer";
|
||||
import DocumentId from "../Tree/DocumentId";
|
||||
import DocumentsTab from "./DocumentsTab";
|
||||
|
||||
describe("Documents tab", () => {
|
||||
describe("buildQuery", () => {
|
||||
it("should generate the right select query for SQL API", () => {
|
||||
const documentsTab = new DocumentsTab({
|
||||
partitionKey: null,
|
||||
documentIds: ko.observableArray<DocumentId>(),
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
});
|
||||
|
||||
expect(documentsTab.buildQuery("")).toContain("select");
|
||||
});
|
||||
});
|
||||
|
||||
describe("showPartitionKey", () => {
|
||||
const explorer = new Explorer();
|
||||
const mongoExplorer = new Explorer();
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableGremlin" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
const collectionWithoutPartitionKey = <ViewModels.Collection>(<unknown>{
|
||||
id: ko.observable<string>("foo"),
|
||||
database: {
|
||||
id: ko.observable<string>("foo"),
|
||||
},
|
||||
container: explorer,
|
||||
});
|
||||
|
||||
const collectionWithSystemPartitionKey = <ViewModels.Collection>(<unknown>{
|
||||
id: ko.observable<string>("foo"),
|
||||
database: {
|
||||
id: ko.observable<string>("foo"),
|
||||
},
|
||||
partitionKey: {
|
||||
paths: ["/foo"],
|
||||
kind: "Hash",
|
||||
version: 2,
|
||||
systemKey: true,
|
||||
},
|
||||
container: explorer,
|
||||
});
|
||||
|
||||
const collectionWithNonSystemPartitionKey = <ViewModels.Collection>(<unknown>{
|
||||
id: ko.observable<string>("foo"),
|
||||
database: {
|
||||
id: ko.observable<string>("foo"),
|
||||
},
|
||||
partitionKey: {
|
||||
paths: ["/foo"],
|
||||
kind: "Hash",
|
||||
version: 2,
|
||||
systemKey: false,
|
||||
},
|
||||
container: explorer,
|
||||
});
|
||||
|
||||
const mongoCollectionWithSystemPartitionKey = <ViewModels.Collection>(<unknown>{
|
||||
id: ko.observable<string>("foo"),
|
||||
database: {
|
||||
id: ko.observable<string>("foo"),
|
||||
},
|
||||
partitionKey: {
|
||||
paths: ["/foo"],
|
||||
kind: "Hash",
|
||||
version: 2,
|
||||
systemKey: true,
|
||||
},
|
||||
container: mongoExplorer,
|
||||
});
|
||||
|
||||
it("should be false for null or undefined collection", () => {
|
||||
const documentsTab = new DocumentsTab({
|
||||
partitionKey: null,
|
||||
documentIds: ko.observableArray<DocumentId>(),
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
});
|
||||
|
||||
expect(documentsTab.showPartitionKey).toBe(false);
|
||||
});
|
||||
|
||||
it("should be false for null or undefined partitionKey", () => {
|
||||
const documentsTab = new DocumentsTab({
|
||||
collection: collectionWithoutPartitionKey,
|
||||
partitionKey: null,
|
||||
documentIds: ko.observableArray<DocumentId>(),
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
});
|
||||
|
||||
expect(documentsTab.showPartitionKey).toBe(false);
|
||||
});
|
||||
|
||||
it("should be true for non-Mongo accounts with system partitionKey", () => {
|
||||
const documentsTab = new DocumentsTab({
|
||||
collection: collectionWithSystemPartitionKey,
|
||||
partitionKey: null,
|
||||
documentIds: ko.observableArray<DocumentId>(),
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
});
|
||||
|
||||
expect(documentsTab.showPartitionKey).toBe(true);
|
||||
});
|
||||
|
||||
it("should be false for Mongo accounts with system partitionKey", () => {
|
||||
updateUserContext({
|
||||
apiType: "Mongo",
|
||||
});
|
||||
const documentsTab = new DocumentsTab({
|
||||
collection: mongoCollectionWithSystemPartitionKey,
|
||||
partitionKey: null,
|
||||
documentIds: ko.observableArray<DocumentId>(),
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
});
|
||||
|
||||
expect(documentsTab.showPartitionKey).toBe(false);
|
||||
});
|
||||
|
||||
it("should be true for non-system partitionKey", () => {
|
||||
const documentsTab = new DocumentsTab({
|
||||
collection: collectionWithNonSystemPartitionKey,
|
||||
partitionKey: null,
|
||||
documentIds: ko.observableArray<DocumentId>(),
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
});
|
||||
|
||||
expect(documentsTab.showPartitionKey).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,922 @@
|
|||
import { extractPartitionKey, ItemDefinition, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import * as ko from "knockout";
|
||||
import Q from "q";
|
||||
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
|
||||
import DiscardIcon from "../../../images/discard.svg";
|
||||
import NewDocumentIcon from "../../../images/NewDocument.svg";
|
||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||
import UploadIcon from "../../../images/Upload_16x16.svg";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { DocumentsGridMetrics, KeyCodes } from "../../Common/Constants";
|
||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
|
||||
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
|
||||
import { readDocument } from "../../Common/dataAccess/readDocument";
|
||||
import { updateDocument } from "../../Common/dataAccess/updateDocument";
|
||||
import editable from "../../Common/EditableUtility";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import * as HeadersUtility from "../../Common/HeadersUtility";
|
||||
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
||||
import * as QueryUtils from "../../Utils/QueryUtils";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../Explorer";
|
||||
import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList";
|
||||
import DocumentId from "../Tree/DocumentId";
|
||||
import { useSelectedNode } from "../useSelectedNode";
|
||||
import TabsBase from "./TabsBase";
|
||||
|
||||
export default class DocumentsTab extends TabsBase {
|
||||
public selectedDocumentId: ko.Observable<DocumentId>;
|
||||
public selectedDocumentContent: ViewModels.Editable<string>;
|
||||
public initialDocumentContent: ko.Observable<string>;
|
||||
public documentContentsGridId: string;
|
||||
public documentContentsContainerId: string;
|
||||
public filterContent: ko.Observable<string>;
|
||||
public appliedFilter: ko.Observable<string>;
|
||||
public lastFilterContents: ko.ObservableArray<string>;
|
||||
public isFilterExpanded: ko.Observable<boolean>;
|
||||
public isFilterCreated: ko.Observable<boolean>;
|
||||
public applyFilterButton: ViewModels.Button;
|
||||
public isEditorDirty: ko.Computed<boolean>;
|
||||
public editorState: ko.Observable<ViewModels.DocumentExplorerState>;
|
||||
public newDocumentButton: ViewModels.Button;
|
||||
public saveNewDocumentButton: ViewModels.Button;
|
||||
public saveExisitingDocumentButton: ViewModels.Button;
|
||||
public discardNewDocumentChangesButton: ViewModels.Button;
|
||||
public discardExisitingDocumentChangesButton: ViewModels.Button;
|
||||
public deleteExisitingDocumentButton: ViewModels.Button;
|
||||
public displayedError: ko.Observable<string>;
|
||||
public accessibleDocumentList: AccessibleVerticalList;
|
||||
public dataContentsGridScrollHeight: ko.Observable<string>;
|
||||
public isPreferredApiMongoDB: boolean;
|
||||
public shouldShowEditor: ko.Computed<boolean>;
|
||||
public splitter: Splitter;
|
||||
public showPartitionKey: boolean;
|
||||
public idHeader: string;
|
||||
|
||||
// TODO need to refactor
|
||||
public partitionKey: DataModels.PartitionKey;
|
||||
public partitionKeyPropertyHeader: string;
|
||||
public partitionKeyProperty: string;
|
||||
public documentIds: ko.ObservableArray<DocumentId>;
|
||||
|
||||
private _documentsIterator: QueryIterator<ItemDefinition & Resource>;
|
||||
private _resourceTokenPartitionKey: string;
|
||||
|
||||
constructor(options: ViewModels.DocumentsTabOptions) {
|
||||
super(options);
|
||||
this.isPreferredApiMongoDB = userContext.apiType === "Mongo" || options.isPreferredApiMongoDB;
|
||||
|
||||
this.idHeader = this.isPreferredApiMongoDB ? "_id" : "id";
|
||||
|
||||
this.documentContentsGridId = `documentContentsGrid${this.tabId}`;
|
||||
this.documentContentsContainerId = `documentContentsContainer${this.tabId}`;
|
||||
this.editorState = ko.observable<ViewModels.DocumentExplorerState>(
|
||||
ViewModels.DocumentExplorerState.noDocumentSelected
|
||||
);
|
||||
this.selectedDocumentId = ko.observable<DocumentId>();
|
||||
this.selectedDocumentContent = editable.observable<string>("");
|
||||
this.initialDocumentContent = ko.observable<string>("");
|
||||
this.partitionKey = options.partitionKey || (this.collection && this.collection.partitionKey);
|
||||
this._resourceTokenPartitionKey = options.resourceTokenPartitionKey;
|
||||
this.documentIds = options.documentIds;
|
||||
|
||||
this.partitionKeyPropertyHeader =
|
||||
(this.collection && this.collection.partitionKeyPropertyHeader) || this._getPartitionKeyPropertyHeader();
|
||||
this.partitionKeyProperty = !!this.partitionKeyPropertyHeader
|
||||
? this.partitionKeyPropertyHeader.replace(/[/]+/g, ".").substr(1).replace(/[']+/g, "")
|
||||
: null;
|
||||
|
||||
this.isFilterExpanded = ko.observable<boolean>(false);
|
||||
this.isFilterCreated = ko.observable<boolean>(true);
|
||||
this.filterContent = ko.observable<string>("");
|
||||
this.appliedFilter = ko.observable<string>("");
|
||||
this.displayedError = ko.observable<string>("");
|
||||
this.lastFilterContents = ko.observableArray<string>([
|
||||
'WHERE c.id = "foo"',
|
||||
"ORDER BY c._ts DESC",
|
||||
'WHERE c.id = "foo" ORDER BY c._ts DESC',
|
||||
]);
|
||||
|
||||
this.dataContentsGridScrollHeight = ko.observable<string>(null);
|
||||
|
||||
// initialize splitter only after template has been loaded so dom elements are accessible
|
||||
super.onTemplateReady((isTemplateReady: boolean) => {
|
||||
if (isTemplateReady) {
|
||||
const tabContainer: HTMLElement = document.getElementById("content");
|
||||
const splitterBounds: SplitterBounds = {
|
||||
min: Constants.DocumentsGridMetrics.DocumentEditorMinWidthRatio * tabContainer.clientWidth,
|
||||
max: Constants.DocumentsGridMetrics.DocumentEditorMaxWidthRatio * tabContainer.clientWidth,
|
||||
};
|
||||
this.splitter = new Splitter({
|
||||
splitterId: "h_splitter2",
|
||||
leftId: this.documentContentsContainerId,
|
||||
bounds: splitterBounds,
|
||||
direction: SplitterDirection.Vertical,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.accessibleDocumentList = new AccessibleVerticalList(this.documentIds());
|
||||
this.accessibleDocumentList.setOnSelect(
|
||||
(selectedDocument: DocumentId) => selectedDocument && selectedDocument.click()
|
||||
);
|
||||
this.selectedDocumentId.subscribe((newSelectedDocumentId: DocumentId) =>
|
||||
this.accessibleDocumentList.updateCurrentItem(newSelectedDocumentId)
|
||||
);
|
||||
this.documentIds.subscribe((newDocuments: DocumentId[]) => {
|
||||
this.accessibleDocumentList.updateItemList(newDocuments);
|
||||
if (newDocuments.length > 0) {
|
||||
this.dataContentsGridScrollHeight(
|
||||
newDocuments.length * DocumentsGridMetrics.IndividualRowHeight + DocumentsGridMetrics.BufferHeight + "px"
|
||||
);
|
||||
} else {
|
||||
this.dataContentsGridScrollHeight(
|
||||
DocumentsGridMetrics.IndividualRowHeight + DocumentsGridMetrics.BufferHeight + "px"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.isEditorDirty = ko.computed<boolean>(() => {
|
||||
switch (this.editorState()) {
|
||||
case ViewModels.DocumentExplorerState.noDocumentSelected:
|
||||
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
|
||||
return false;
|
||||
|
||||
case ViewModels.DocumentExplorerState.newDocumentValid:
|
||||
case ViewModels.DocumentExplorerState.newDocumentInvalid:
|
||||
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
|
||||
return true;
|
||||
|
||||
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
|
||||
return (
|
||||
this.selectedDocumentContent.getEditableOriginalValue() !==
|
||||
this.selectedDocumentContent.getEditableCurrentValue()
|
||||
);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
this.newDocumentButton = {
|
||||
enabled: ko.computed<boolean>(() => {
|
||||
switch (this.editorState()) {
|
||||
case ViewModels.DocumentExplorerState.noDocumentSelected:
|
||||
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
|
||||
visible: ko.computed<boolean>(() => {
|
||||
return true;
|
||||
}),
|
||||
};
|
||||
|
||||
this.saveNewDocumentButton = {
|
||||
enabled: ko.computed<boolean>(() => {
|
||||
switch (this.editorState()) {
|
||||
case ViewModels.DocumentExplorerState.newDocumentValid:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
|
||||
visible: ko.computed<boolean>(() => {
|
||||
switch (this.editorState()) {
|
||||
case ViewModels.DocumentExplorerState.newDocumentValid:
|
||||
case ViewModels.DocumentExplorerState.newDocumentInvalid:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
};
|
||||
|
||||
this.discardNewDocumentChangesButton = {
|
||||
enabled: ko.computed<boolean>(() => {
|
||||
switch (this.editorState()) {
|
||||
case ViewModels.DocumentExplorerState.newDocumentValid:
|
||||
case ViewModels.DocumentExplorerState.newDocumentInvalid:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
|
||||
visible: ko.computed<boolean>(() => {
|
||||
switch (this.editorState()) {
|
||||
case ViewModels.DocumentExplorerState.newDocumentValid:
|
||||
case ViewModels.DocumentExplorerState.newDocumentInvalid:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
};
|
||||
|
||||
this.saveExisitingDocumentButton = {
|
||||
enabled: ko.computed<boolean>(() => {
|
||||
switch (this.editorState()) {
|
||||
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
|
||||
visible: ko.computed<boolean>(() => {
|
||||
switch (this.editorState()) {
|
||||
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
|
||||
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
|
||||
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
};
|
||||
|
||||
this.discardExisitingDocumentChangesButton = {
|
||||
enabled: ko.computed<boolean>(() => {
|
||||
switch (this.editorState()) {
|
||||
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
|
||||
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
|
||||
visible: ko.computed<boolean>(() => {
|
||||
switch (this.editorState()) {
|
||||
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
|
||||
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
|
||||
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
};
|
||||
|
||||
this.deleteExisitingDocumentButton = {
|
||||
enabled: ko.computed<boolean>(() => {
|
||||
switch (this.editorState()) {
|
||||
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
|
||||
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
|
||||
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
|
||||
visible: ko.computed<boolean>(() => {
|
||||
switch (this.editorState()) {
|
||||
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
|
||||
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
|
||||
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
};
|
||||
|
||||
this.applyFilterButton = {
|
||||
enabled: ko.computed<boolean>(() => {
|
||||
return true;
|
||||
}),
|
||||
|
||||
visible: ko.computed<boolean>(() => {
|
||||
return true;
|
||||
}),
|
||||
};
|
||||
this.buildCommandBarOptions();
|
||||
this.shouldShowEditor = ko.computed<boolean>(() => {
|
||||
const documentHasContent: boolean =
|
||||
this.selectedDocumentContent() != null && this.selectedDocumentContent().length > 0;
|
||||
const isNewDocument: boolean =
|
||||
this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid ||
|
||||
this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid;
|
||||
|
||||
return documentHasContent || isNewDocument;
|
||||
});
|
||||
this.selectedDocumentContent.subscribe((newContent: string) => this._onEditorContentChange(newContent));
|
||||
|
||||
this.showPartitionKey = this._shouldShowPartitionKey();
|
||||
}
|
||||
|
||||
private _shouldShowPartitionKey(): boolean {
|
||||
if (!this.collection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.collection.partitionKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.collection.partitionKey.systemKey && this.isPreferredApiMongoDB) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public onShowFilterClick(): Q.Promise<any> {
|
||||
this.isFilterCreated(true);
|
||||
this.isFilterExpanded(true);
|
||||
|
||||
$(".filterDocExpanded").addClass("active");
|
||||
$("#content").addClass("active");
|
||||
$(".querydropdown").focus();
|
||||
|
||||
return Q();
|
||||
}
|
||||
|
||||
public onHideFilterClick(): Q.Promise<any> {
|
||||
this.isFilterExpanded(false);
|
||||
|
||||
$(".filterDocExpanded").removeClass("active");
|
||||
$("#content").removeClass("active");
|
||||
$(".queryButton").focus();
|
||||
return Q();
|
||||
}
|
||||
|
||||
public onCloseButtonKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||
this.onHideFilterClick();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
public async refreshDocumentsGrid(): Promise<void> {
|
||||
// clear documents grid
|
||||
this.documentIds([]);
|
||||
|
||||
try {
|
||||
// reset iterator
|
||||
this._documentsIterator = this.createIterator();
|
||||
// load documents
|
||||
await this.loadNextPage();
|
||||
// collapse filter
|
||||
this.appliedFilter(this.filterContent());
|
||||
this.isFilterExpanded(false);
|
||||
document.getElementById("errorStatusIcon")?.focus();
|
||||
} catch (error) {
|
||||
window.alert(getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||
this.refreshDocumentsGrid();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
public onDocumentIdClick(clickedDocumentId: DocumentId): Q.Promise<any> {
|
||||
if (this.editorState() !== ViewModels.DocumentExplorerState.noDocumentSelected) {
|
||||
return Q();
|
||||
}
|
||||
|
||||
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
|
||||
|
||||
return Q();
|
||||
}
|
||||
|
||||
public onNewDocumentClick = (): Q.Promise<any> => {
|
||||
if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) {
|
||||
return Q();
|
||||
}
|
||||
this.selectedDocumentId(null);
|
||||
|
||||
const defaultDocument: string = this.renderObjectForEditor({ id: "replace_with_new_document_id" }, null, 4);
|
||||
this.initialDocumentContent(defaultDocument);
|
||||
this.selectedDocumentContent.setBaseline(defaultDocument);
|
||||
this.editorState(ViewModels.DocumentExplorerState.newDocumentValid);
|
||||
|
||||
return Q();
|
||||
};
|
||||
|
||||
public onSaveNewDocumentClick = (): Promise<any> => {
|
||||
this.isExecutionError(false);
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, {
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
});
|
||||
const document = JSON.parse(this.selectedDocumentContent());
|
||||
this.isExecuting(true);
|
||||
return createDocument(this.collection, document)
|
||||
.then(
|
||||
(savedDocument: any) => {
|
||||
const value: string = this.renderObjectForEditor(savedDocument || {}, null, 4);
|
||||
this.selectedDocumentContent.setBaseline(value);
|
||||
this.initialDocumentContent(value);
|
||||
const partitionKeyValueArray = extractPartitionKey(
|
||||
savedDocument,
|
||||
this.partitionKey as PartitionKeyDefinition
|
||||
);
|
||||
const partitionKeyValue = partitionKeyValueArray && partitionKeyValueArray[0];
|
||||
let id = new DocumentId(this, savedDocument, partitionKeyValue);
|
||||
let ids = this.documentIds();
|
||||
ids.push(id);
|
||||
|
||||
this.selectedDocumentId(id);
|
||||
this.documentIds(ids);
|
||||
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.CreateDocument,
|
||||
{
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
},
|
||||
(error) => {
|
||||
this.isExecutionError(true);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
window.alert(errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.CreateDocument,
|
||||
{
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
error: errorMessage,
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
}
|
||||
)
|
||||
.finally(() => this.isExecuting(false));
|
||||
};
|
||||
|
||||
public onRevertNewDocumentClick = (): Q.Promise<any> => {
|
||||
this.initialDocumentContent("");
|
||||
this.selectedDocumentContent("");
|
||||
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
|
||||
|
||||
return Q();
|
||||
};
|
||||
|
||||
public onSaveExisitingDocumentClick = (): Promise<any> => {
|
||||
const selectedDocumentId = this.selectedDocumentId();
|
||||
const documentContent = JSON.parse(this.selectedDocumentContent());
|
||||
|
||||
const partitionKeyValueArray = extractPartitionKey(documentContent, this.partitionKey as PartitionKeyDefinition);
|
||||
const partitionKeyValue = partitionKeyValueArray && partitionKeyValueArray[0];
|
||||
|
||||
selectedDocumentId.partitionKeyValue = partitionKeyValue;
|
||||
|
||||
this.isExecutionError(false);
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.UpdateDocument, {
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
});
|
||||
this.isExecuting(true);
|
||||
return updateDocument(this.collection, selectedDocumentId, documentContent)
|
||||
.then(
|
||||
(updatedDocument: any) => {
|
||||
const value: string = this.renderObjectForEditor(updatedDocument || {}, null, 4);
|
||||
this.selectedDocumentContent.setBaseline(value);
|
||||
this.initialDocumentContent(value);
|
||||
this.documentIds().forEach((documentId: DocumentId) => {
|
||||
if (documentId.rid === updatedDocument._rid) {
|
||||
documentId.id(updatedDocument.id);
|
||||
}
|
||||
});
|
||||
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.UpdateDocument,
|
||||
{
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
},
|
||||
(error) => {
|
||||
this.isExecutionError(true);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
window.alert(errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.UpdateDocument,
|
||||
{
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
error: errorMessage,
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
}
|
||||
)
|
||||
.finally(() => this.isExecuting(false));
|
||||
};
|
||||
|
||||
public onRevertExisitingDocumentClick = (): Q.Promise<any> => {
|
||||
this.selectedDocumentContent.setBaseline(this.initialDocumentContent());
|
||||
this.initialDocumentContent.valueHasMutated();
|
||||
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
|
||||
|
||||
return Q();
|
||||
};
|
||||
|
||||
public onDeleteExisitingDocumentClick = async (): Promise<void> => {
|
||||
const selectedDocumentId = this.selectedDocumentId();
|
||||
const msg = !this.isPreferredApiMongoDB
|
||||
? "Are you sure you want to delete the selected item ?"
|
||||
: "Are you sure you want to delete the selected document ?";
|
||||
|
||||
if (window.confirm(msg)) {
|
||||
await this._deleteDocument(selectedDocumentId);
|
||||
}
|
||||
};
|
||||
|
||||
public onValidDocumentEdit(): Q.Promise<any> {
|
||||
if (
|
||||
this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid ||
|
||||
this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid
|
||||
) {
|
||||
this.editorState(ViewModels.DocumentExplorerState.newDocumentValid);
|
||||
return Q();
|
||||
}
|
||||
|
||||
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid);
|
||||
return Q();
|
||||
}
|
||||
|
||||
public onInvalidDocumentEdit(): Q.Promise<any> {
|
||||
if (
|
||||
this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid ||
|
||||
this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid
|
||||
) {
|
||||
this.editorState(ViewModels.DocumentExplorerState.newDocumentInvalid);
|
||||
return Q();
|
||||
}
|
||||
|
||||
if (
|
||||
this.editorState() === ViewModels.DocumentExplorerState.exisitingDocumentNoEdits ||
|
||||
this.editorState() === ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid
|
||||
) {
|
||||
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid);
|
||||
return Q();
|
||||
}
|
||||
|
||||
return Q();
|
||||
}
|
||||
|
||||
public onTabClick(): void {
|
||||
super.onTabClick();
|
||||
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
|
||||
}
|
||||
|
||||
public async onActivate(): Promise<void> {
|
||||
super.onActivate();
|
||||
|
||||
if (!this._documentsIterator) {
|
||||
try {
|
||||
this._documentsIterator = this.createIterator();
|
||||
await this.loadNextPage();
|
||||
} catch (error) {
|
||||
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.Tab,
|
||||
{
|
||||
databaseName: this.collection.databaseId,
|
||||
collectionName: this.collection.id(),
|
||||
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
this.onLoadStartKey
|
||||
);
|
||||
this.onLoadStartKey = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _isIgnoreDirtyEditor = (): boolean => {
|
||||
var msg: string = "Changes will be lost. Do you want to continue?";
|
||||
return window.confirm(msg);
|
||||
};
|
||||
|
||||
protected __deleteDocument(documentId: DocumentId): Promise<void> {
|
||||
return deleteDocument(this.collection, documentId);
|
||||
}
|
||||
|
||||
private _deleteDocument(selectedDocumentId: DocumentId): Promise<void> {
|
||||
this.isExecutionError(false);
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, {
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
});
|
||||
this.isExecuting(true);
|
||||
return this.__deleteDocument(selectedDocumentId)
|
||||
.then(
|
||||
() => {
|
||||
this.documentIds.remove((documentId: DocumentId) => documentId.rid === selectedDocumentId.rid);
|
||||
this.selectedDocumentContent("");
|
||||
this.selectedDocumentId(null);
|
||||
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.DeleteDocument,
|
||||
{
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
},
|
||||
(error) => {
|
||||
this.isExecutionError(true);
|
||||
console.error(error);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.DeleteDocument,
|
||||
{
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
}
|
||||
)
|
||||
.finally(() => this.isExecuting(false));
|
||||
}
|
||||
|
||||
public createIterator(): QueryIterator<ItemDefinition & Resource> {
|
||||
let filters = this.lastFilterContents();
|
||||
const filter: string = this.filterContent().trim();
|
||||
const query: string = this.buildQuery(filter);
|
||||
let options: any = {};
|
||||
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
|
||||
|
||||
if (this._resourceTokenPartitionKey) {
|
||||
options.partitionKey = this._resourceTokenPartitionKey;
|
||||
}
|
||||
|
||||
return queryDocuments(this.collection.databaseId, this.collection.id(), query, options);
|
||||
}
|
||||
|
||||
public async selectDocument(documentId: DocumentId): Promise<void> {
|
||||
this.selectedDocumentId(documentId);
|
||||
const content = await readDocument(this.collection, documentId);
|
||||
this.initDocumentEditor(documentId, content);
|
||||
}
|
||||
|
||||
public loadNextPage(): Q.Promise<any> {
|
||||
this.isExecuting(true);
|
||||
this.isExecutionError(false);
|
||||
return this._loadNextPageInternal()
|
||||
.then(
|
||||
(documentsIdsResponse = []) => {
|
||||
const currentDocuments = this.documentIds();
|
||||
const currentDocumentsRids = currentDocuments.map((currentDocument) => currentDocument.rid);
|
||||
const nextDocumentIds = documentsIdsResponse
|
||||
// filter documents already loaded in observable
|
||||
.filter((d: any) => {
|
||||
return currentDocumentsRids.indexOf(d._rid) < 0;
|
||||
})
|
||||
// map raw response to view model
|
||||
.map((rawDocument: any) => {
|
||||
const partitionKeyValue = rawDocument._partitionKeyValue;
|
||||
return new DocumentId(this, rawDocument, partitionKeyValue);
|
||||
});
|
||||
|
||||
const merged = currentDocuments.concat(nextDocumentIds);
|
||||
this.documentIds(merged);
|
||||
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.Tab,
|
||||
{
|
||||
databaseName: this.collection.databaseId,
|
||||
collectionName: this.collection.id(),
|
||||
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
},
|
||||
this.onLoadStartKey
|
||||
);
|
||||
this.onLoadStartKey = null;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
this.isExecutionError(true);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
logConsoleError(errorMessage);
|
||||
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.Tab,
|
||||
{
|
||||
databaseName: this.collection.databaseId,
|
||||
collectionName: this.collection.id(),
|
||||
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
error: errorMessage,
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
this.onLoadStartKey
|
||||
);
|
||||
this.onLoadStartKey = null;
|
||||
}
|
||||
}
|
||||
)
|
||||
.finally(() => this.isExecuting(false));
|
||||
}
|
||||
|
||||
public onLoadMoreKeyInput = (source: any, event: KeyboardEvent): void => {
|
||||
if (event.key === " " || event.key === "Enter") {
|
||||
const focusElement = document.getElementById(this.documentContentsGridId);
|
||||
this.loadNextPage();
|
||||
focusElement && focusElement.focus();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
protected _loadNextPageInternal(): Q.Promise<DataModels.DocumentId[]> {
|
||||
return Q(this._documentsIterator.fetchNext().then((response) => response.resources));
|
||||
}
|
||||
|
||||
protected _onEditorContentChange(newContent: string) {
|
||||
try {
|
||||
let parsed: any = JSON.parse(newContent);
|
||||
this.onValidDocumentEdit();
|
||||
} catch (e) {
|
||||
this.onInvalidDocumentEdit();
|
||||
}
|
||||
}
|
||||
|
||||
public initDocumentEditor(documentId: DocumentId, documentContent: any): Q.Promise<any> {
|
||||
if (documentId) {
|
||||
const content: string = this.renderObjectForEditor(documentContent, null, 4);
|
||||
this.selectedDocumentContent.setBaseline(content);
|
||||
this.initialDocumentContent(content);
|
||||
const newState = documentId
|
||||
? ViewModels.DocumentExplorerState.exisitingDocumentNoEdits
|
||||
: ViewModels.DocumentExplorerState.newDocumentValid;
|
||||
this.editorState(newState);
|
||||
}
|
||||
|
||||
return Q();
|
||||
}
|
||||
|
||||
public buildQuery(filter: string): string {
|
||||
return QueryUtils.buildDocumentsQuery(filter, this.partitionKeyProperty, this.partitionKey);
|
||||
}
|
||||
|
||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
const label = !this.isPreferredApiMongoDB ? "New Item" : "New Document";
|
||||
if (this.newDocumentButton.visible()) {
|
||||
buttons.push({
|
||||
iconSrc: NewDocumentIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: this.onNewDocumentClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.newDocumentButton.enabled(),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.saveNewDocumentButton.visible()) {
|
||||
const label = "Save";
|
||||
buttons.push({
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: this.onSaveNewDocumentClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.saveNewDocumentButton.enabled(),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.discardNewDocumentChangesButton.visible()) {
|
||||
const label = "Discard";
|
||||
buttons.push({
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: this.onRevertNewDocumentClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.discardNewDocumentChangesButton.enabled(),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.saveExisitingDocumentButton.visible()) {
|
||||
const label = "Update";
|
||||
buttons.push({
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: this.onSaveExisitingDocumentClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.saveExisitingDocumentButton.enabled(),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.discardExisitingDocumentChangesButton.visible()) {
|
||||
const label = "Discard";
|
||||
buttons.push({
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: this.onRevertExisitingDocumentClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.discardExisitingDocumentChangesButton.enabled(),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.deleteExisitingDocumentButton.visible()) {
|
||||
const label = "Delete";
|
||||
buttons.push({
|
||||
iconSrc: DeleteDocumentIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: this.onDeleteExisitingDocumentClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.deleteExisitingDocumentButton.enabled(),
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.isPreferredApiMongoDB) {
|
||||
buttons.push(DocumentsTab._createUploadButton(this.collection.container));
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
protected buildCommandBarOptions(): void {
|
||||
ko.computed(() =>
|
||||
ko.toJSON([
|
||||
this.newDocumentButton.visible,
|
||||
this.newDocumentButton.enabled,
|
||||
this.saveNewDocumentButton.visible,
|
||||
this.saveNewDocumentButton.enabled,
|
||||
this.discardNewDocumentChangesButton.visible,
|
||||
this.discardNewDocumentChangesButton.enabled,
|
||||
this.saveExisitingDocumentButton.visible,
|
||||
this.saveExisitingDocumentButton.enabled,
|
||||
this.discardExisitingDocumentChangesButton.visible,
|
||||
this.discardExisitingDocumentChangesButton.enabled,
|
||||
this.deleteExisitingDocumentButton.visible,
|
||||
this.deleteExisitingDocumentButton.enabled,
|
||||
])
|
||||
).subscribe(() => this.updateNavbarWithTabsButtons());
|
||||
this.updateNavbarWithTabsButtons();
|
||||
}
|
||||
|
||||
private _getPartitionKeyPropertyHeader(): string {
|
||||
return (
|
||||
(this.partitionKey &&
|
||||
this.partitionKey.paths &&
|
||||
this.partitionKey.paths.length > 0 &&
|
||||
this.partitionKey.paths[0]) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
public static _createUploadButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Upload Item";
|
||||
return {
|
||||
iconSrc: UploadIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && container.openUploadItemsPanePane();
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: useSelectedNode.getState().isDatabaseNodeOrNoneSelected(),
|
||||
};
|
||||
}
|
||||
}
|
|
@ -41,6 +41,7 @@ import { EditorReact } from "../Controls/Editor/EditorReact";
|
|||
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import DocumentId from "../Tree/DocumentId";
|
||||
import ObjectId from "../Tree/ObjectId";
|
||||
import { useSelectedNode } from "../useSelectedNode";
|
||||
import DocumentsTab from "./DocumentsTab";
|
||||
import {
|
||||
formatDocumentContent,
|
||||
|
@ -450,13 +451,13 @@ export default class DocumentsTabContent extends React.Component<DocumentsTab, I
|
|||
iconSrc: UploadIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = collection.container.findSelectedCollection();
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && collection.container.openUploadItemsPanePane();
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: collection.container.isDatabaseNodeOrNoneSelected(),
|
||||
disabled: useSelectedNode.getState().isDatabaseNodeOrNoneSelected(),
|
||||
});
|
||||
}
|
||||
return buttons;
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import Q from "q";
|
||||
import MongoUtility from "../../Common/MongoUtility";
|
||||
import QueryTab from "./QueryTab";
|
||||
import * as HeadersUtility from "../../Common/HeadersUtility";
|
||||
import { queryIterator } from "../../Common/MongoProxyClient";
|
||||
import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
|
||||
|
||||
export default class MongoQueryTab extends QueryTab {
|
||||
public collection: ViewModels.Collection;
|
||||
|
||||
constructor(options: ViewModels.QueryTabOptions) {
|
||||
options.queryText = ""; // override sql query editor content for now so we only display mongo related help items
|
||||
super(options);
|
||||
this.isPreferredApiMongoDB = true;
|
||||
this.monacoSettings = new ViewModels.MonacoEditorSettings("plaintext", false);
|
||||
}
|
||||
/** Renders a Javascript object to be displayed inside Monaco Editor */
|
||||
protected renderObjectForEditor(value: any, replacer: any, space: string | number): string {
|
||||
return MongoUtility.tojson(value, null, false);
|
||||
}
|
||||
|
||||
protected _initIterator(): Q.Promise<MinimalQueryIterator> {
|
||||
let options: any = {};
|
||||
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
|
||||
this._iterator = queryIterator(this.collection.databaseId, this.collection, this.sqlStatementToExecute());
|
||||
return Q(this._iterator);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import React from "react";
|
||||
import MongoUtility from "../../../Common/MongoUtility";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import Explorer from "../../Explorer";
|
||||
import { NewQueryTab } from "../QueryTab/QueryTab";
|
||||
import QueryTabComponent, { IQueryTabComponentProps, ITabAccessor } from "../QueryTab/QueryTabComponent";
|
||||
|
||||
export interface IMongoQueryTabProps {
|
||||
container: Explorer;
|
||||
viewModelcollection?: ViewModels.Collection;
|
||||
}
|
||||
|
||||
export class NewMongoQueryTab extends NewQueryTab {
|
||||
public collection: ViewModels.Collection;
|
||||
public iMongoQueryTabComponentProps: IQueryTabComponentProps;
|
||||
public queryText: string;
|
||||
|
||||
constructor(options: ViewModels.QueryTabOptions, private mongoQueryTabProps: IMongoQueryTabProps) {
|
||||
super(options, mongoQueryTabProps);
|
||||
this.queryText = "";
|
||||
this.iMongoQueryTabComponentProps = {
|
||||
collection: options.collection,
|
||||
isExecutionError: this.isExecutionError(),
|
||||
tabId: this.tabId,
|
||||
tabsBaseInstance: this,
|
||||
queryText: this.queryText,
|
||||
partitionKey: this.partitionKey,
|
||||
container: this.mongoQueryTabProps.container,
|
||||
onTabAccessor: (instance: ITabAccessor): void => {
|
||||
this.iTabAccessor = instance;
|
||||
},
|
||||
isPreferredApiMongoDB: true,
|
||||
monacoEditorSetting: "plaintext",
|
||||
viewModelcollection: this.mongoQueryTabProps.viewModelcollection,
|
||||
};
|
||||
}
|
||||
|
||||
/** Renders a Javascript object to be displayed inside Monaco Editor */
|
||||
//eslint-disable-next-line
|
||||
public renderObjectForEditor(value: any, replacer: any, space: string | number): string {
|
||||
return MongoUtility.tojson(value, undefined, false);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return <QueryTabComponent {...this.iMongoQueryTabComponentProps} />;
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
<iframe
|
||||
name="explorer"
|
||||
class="iframe"
|
||||
style="width: 100%; height: 100%; border: 0px; padding: 0px; margin: 0px; overflow: hidden"
|
||||
data-bind="
|
||||
attr: {
|
||||
src: url,
|
||||
id: tabId
|
||||
},
|
||||
event:{
|
||||
load: setContentFocus(event)
|
||||
}"
|
||||
title="Mongo Shell"
|
||||
role="tabpanel"
|
||||
></iframe>
|
|
@ -0,0 +1,40 @@
|
|||
import React from "react";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import type { TabOptions } from "../../../Contracts/ViewModels";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import Explorer from "../../Explorer";
|
||||
import TabsBase from "../TabsBase";
|
||||
import MongoShellTabComponent, { IMongoShellTabAccessor, IMongoShellTabComponentProps } from "./MongoShellTabComponent";
|
||||
|
||||
export interface IMongoShellTabProps {
|
||||
container: Explorer;
|
||||
}
|
||||
|
||||
export class NewMongoShellTab extends TabsBase {
|
||||
public queryText: string;
|
||||
public currentQuery: string;
|
||||
public partitionKey: DataModels.PartitionKey;
|
||||
public iMongoShellTabComponentProps: IMongoShellTabComponentProps;
|
||||
public iMongoShellTabAccessor: IMongoShellTabAccessor;
|
||||
|
||||
constructor(options: TabOptions, private props: IMongoShellTabProps) {
|
||||
super(options);
|
||||
this.iMongoShellTabComponentProps = {
|
||||
collection: this.collection,
|
||||
tabsBaseInstance: this,
|
||||
container: this.props.container,
|
||||
onMongoShellTabAccessor: (instance: IMongoShellTabAccessor) => {
|
||||
this.iMongoShellTabAccessor = instance;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return <MongoShellTabComponent {...this.iMongoShellTabComponentProps} />;
|
||||
}
|
||||
|
||||
public onTabClick(): void {
|
||||
useTabs.getState().activateTab(this);
|
||||
this.iMongoShellTabAccessor.onTabClickEvent();
|
||||
}
|
||||
}
|
|
@ -1,67 +1,101 @@
|
|||
import * as ko from "knockout";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { configContext, Platform } from "../../ConfigContext";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { isInvalidParentFrameOrigin, isReadyMessage } from "../../Utils/MessageValidation";
|
||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../Explorer";
|
||||
import template from "./MongoShellTab.html";
|
||||
import TabsBase from "./TabsBase";
|
||||
import React, { Component } from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { configContext, Platform } from "../../../ConfigContext";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { isInvalidParentFrameOrigin, isReadyMessage } from "../../../Utils/MessageValidation";
|
||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import TabsBase from "../TabsBase";
|
||||
|
||||
export default class MongoShellTab extends TabsBase {
|
||||
public readonly html = template;
|
||||
public url: ko.Computed<string>;
|
||||
private _container: Explorer;
|
||||
//eslint-disable-next-line
|
||||
class MessageType {
|
||||
static IframeReady = "iframeready";
|
||||
static Notification = "notification";
|
||||
static Log = "log";
|
||||
}
|
||||
|
||||
//eslint-disable-next-line
|
||||
class LogType {
|
||||
static Information = "information";
|
||||
static Warning = "warning";
|
||||
static Verbose = "verbose";
|
||||
static InProgress = "inprogress";
|
||||
static StartTrace = "start";
|
||||
static SuccessTrace = "success";
|
||||
static FailureTrace = "failure";
|
||||
}
|
||||
|
||||
export interface IMongoShellTabAccessor {
|
||||
onTabClickEvent: () => void;
|
||||
}
|
||||
|
||||
export interface IMongoShellTabComponentStates {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface IMongoShellTabComponentProps {
|
||||
collection: ViewModels.CollectionBase;
|
||||
tabsBaseInstance: TabsBase;
|
||||
container: Explorer;
|
||||
onMongoShellTabAccessor: (instance: IMongoShellTabAccessor) => void;
|
||||
}
|
||||
|
||||
export default class MongoShellTabComponent extends Component<
|
||||
IMongoShellTabComponentProps,
|
||||
IMongoShellTabComponentStates
|
||||
> {
|
||||
private _runtimeEndpoint: string;
|
||||
private _logTraces: Map<string, number>;
|
||||
|
||||
constructor(options: ViewModels.TabOptions) {
|
||||
super(options);
|
||||
constructor(props: IMongoShellTabComponentProps) {
|
||||
super(props);
|
||||
this._logTraces = new Map();
|
||||
this._container = options.collection.container;
|
||||
this.url = ko.computed<string>(() => {
|
||||
const { databaseAccount: account } = userContext;
|
||||
const resourceId = account?.id;
|
||||
const accountName = account?.name;
|
||||
const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint;
|
||||
|
||||
this._runtimeEndpoint = configContext.platform === Platform.Hosted ? configContext.BACKEND_ENDPOINT : "";
|
||||
const extensionEndpoint: string = configContext.BACKEND_ENDPOINT || this._runtimeEndpoint || "";
|
||||
let baseUrl = "/content/mongoshell/dist/";
|
||||
if (userContext.portalEnv === "localhost") {
|
||||
baseUrl = "/content/mongoshell/";
|
||||
}
|
||||
this.state = {
|
||||
url: this.getURL(),
|
||||
};
|
||||
|
||||
return `${extensionEndpoint}${baseUrl}index.html?resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`;
|
||||
props.onMongoShellTabAccessor({
|
||||
onTabClickEvent: this.onTabClick.bind(this),
|
||||
});
|
||||
|
||||
window.addEventListener("message", this.handleMessage.bind(this), false);
|
||||
}
|
||||
|
||||
public setContentFocus(event: any): any {
|
||||
// TODO: Work around cross origin security issue in Hosted Data Explorer by using Shell <-> Data Explorer messaging (253527)
|
||||
// if(event.type === "load" && window.dataExplorerPlatform != PlatformType.Hosted) {
|
||||
// let activeShell = event.target.contentWindow && event.target.contentWindow.mongo && event.target.contentWindow.mongo.shells && event.target.contentWindow.mongo.shells[0];
|
||||
// activeShell && setTimeout(function(){
|
||||
// activeShell.focus();
|
||||
// },2000);
|
||||
// }
|
||||
public getURL(): string {
|
||||
const { databaseAccount: account } = userContext;
|
||||
const resourceId = account?.id;
|
||||
const accountName = account?.name;
|
||||
const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint;
|
||||
|
||||
this._runtimeEndpoint = configContext.platform === Platform.Hosted ? configContext.BACKEND_ENDPOINT : "";
|
||||
const extensionEndpoint: string = configContext.BACKEND_ENDPOINT || this._runtimeEndpoint || "";
|
||||
let baseUrl = "/content/mongoshell/dist/";
|
||||
if (userContext.portalEnv === "localhost") {
|
||||
baseUrl = "/content/mongoshell/";
|
||||
}
|
||||
|
||||
return `${extensionEndpoint}${baseUrl}index.html?resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`;
|
||||
}
|
||||
|
||||
//eslint-disable-next-line
|
||||
public setContentFocus(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void {}
|
||||
|
||||
public onTabClick(): void {
|
||||
super.onTabClick();
|
||||
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
|
||||
this.props.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
|
||||
}
|
||||
|
||||
public handleMessage(event: MessageEvent) {
|
||||
public handleMessage(event: MessageEvent): void {
|
||||
if (isInvalidParentFrameOrigin(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shellIframe: HTMLIFrameElement = <HTMLIFrameElement>document.getElementById(this.tabId);
|
||||
const shellIframe: HTMLIFrameElement = document.getElementById(
|
||||
this.props.tabsBaseInstance.tabId
|
||||
) as HTMLIFrameElement;
|
||||
|
||||
if (!shellIframe) {
|
||||
return;
|
||||
|
@ -73,9 +107,9 @@ export default class MongoShellTab extends TabsBase {
|
|||
return;
|
||||
}
|
||||
|
||||
if (event.data.eventType == MessageType.IframeReady) {
|
||||
if (event.data.eventType === MessageType.IframeReady) {
|
||||
this.handleReadyMessage(event, shellIframe);
|
||||
} else if (event.data.eventType == MessageType.Notification) {
|
||||
} else if (event.data.eventType === MessageType.Notification) {
|
||||
this.handleNotificationMessage(event, shellIframe);
|
||||
} else {
|
||||
this.handleLogMessage(event, shellIframe);
|
||||
|
@ -98,8 +132,8 @@ export default class MongoShellTab extends TabsBase {
|
|||
documentEndpoint.length -
|
||||
(Constants.MongoDBAccounts.protocol.length + 2 + Constants.MongoDBAccounts.defaultPort.length)
|
||||
) + Constants.MongoDBAccounts.defaultPort.toString();
|
||||
const databaseId = this.collection.databaseId;
|
||||
const collectionId = this.collection.id();
|
||||
const databaseId = this.props.collection.databaseId;
|
||||
const collectionId = this.props.collection.id();
|
||||
const apiEndpoint = configContext.BACKEND_ENDPOINT;
|
||||
const encryptedAuthToken: string = userContext.accessToken;
|
||||
|
||||
|
@ -121,6 +155,7 @@ export default class MongoShellTab extends TabsBase {
|
|||
);
|
||||
}
|
||||
|
||||
//eslint-disable-next-line
|
||||
private handleLogMessage(event: MessageEvent, shellIframe: HTMLIFrameElement) {
|
||||
if (!("logType" in event.data.data) || typeof event.data.data["logType"] !== "string") {
|
||||
return;
|
||||
|
@ -144,6 +179,7 @@ export default class MongoShellTab extends TabsBase {
|
|||
TelemetryProcessor.trace(Action.MongoShell, ActionModifiers.Mark, dataToLog);
|
||||
break;
|
||||
case LogType.StartTrace:
|
||||
//eslint-disable-next-line
|
||||
const telemetryTraceId: number = TelemetryProcessor.traceStart(Action.MongoShell, dataToLog);
|
||||
this._logTraces.set(shellTraceId, telemetryTraceId);
|
||||
break;
|
||||
|
@ -168,6 +204,7 @@ export default class MongoShellTab extends TabsBase {
|
|||
}
|
||||
}
|
||||
|
||||
//eslint-disable-next-line
|
||||
private handleNotificationMessage(event: MessageEvent, shellIframe: HTMLIFrameElement) {
|
||||
if (!("logType" in event.data.data) || typeof event.data.data["logType"] !== "string") {
|
||||
return;
|
||||
|
@ -188,20 +225,19 @@ export default class MongoShellTab extends TabsBase {
|
|||
return logConsoleProgress(dataToLog);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MessageType {
|
||||
static IframeReady: string = "iframeready";
|
||||
static Notification: string = "notification";
|
||||
static Log: string = "log";
|
||||
}
|
||||
|
||||
class LogType {
|
||||
static Information: string = "information";
|
||||
static Warning: string = "warning";
|
||||
static Verbose: string = "verbose";
|
||||
static InProgress: string = "inprogress";
|
||||
static StartTrace: string = "start";
|
||||
static SuccessTrace: string = "success";
|
||||
static FailureTrace: string = "failure";
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<iframe
|
||||
name="explorer"
|
||||
className="iframe"
|
||||
style={{ width: "100%", height: "100%", border: 0, padding: 0, margin: 0, overflow: "hidden" }}
|
||||
src={this.state.url}
|
||||
id={this.props.tabsBaseInstance.tabId}
|
||||
onLoad={(event) => this.setContentFocus(event)}
|
||||
title="Mongo Shell"
|
||||
role="tabpanel"
|
||||
></iframe>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
|||
import { userContext } from "../../UserContext";
|
||||
import Explorer from "../Explorer";
|
||||
import { NotebookClientV2 } from "../Notebook/NotebookClientV2";
|
||||
import { useNotebook } from "../Notebook/useNotebook";
|
||||
import TabsBase from "./TabsBase";
|
||||
|
||||
export interface NotebookTabBaseOptions extends ViewModels.TabOptions {
|
||||
|
@ -28,7 +29,7 @@ export default class NotebookTabBase extends TabsBase {
|
|||
|
||||
if (!NotebookTabBase.clientManager) {
|
||||
NotebookTabBase.clientManager = new NotebookClientV2({
|
||||
connectionInfo: this.container.notebookServerInfo(),
|
||||
connectionInfo: useNotebook.getState().notebookServerInfo,
|
||||
databaseAccountName: userContext?.databaseAccount?.name,
|
||||
defaultExperience: userContext.apiType,
|
||||
contentProvider: this.container.notebookManager?.notebookContentProvider,
|
||||
|
|
|
@ -23,6 +23,7 @@ import * as CdbActions from "../Notebook/NotebookComponent/actions";
|
|||
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
|
||||
import { CdbAppState, SnapshotRequest } from "../Notebook/NotebookComponent/types";
|
||||
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
||||
import { useNotebook } from "../Notebook/useNotebook";
|
||||
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
|
||||
|
||||
export interface NotebookTabOptions extends NotebookTabBaseOptions {
|
||||
|
@ -39,10 +40,13 @@ export default class NotebookTabV2 extends NotebookTabBase {
|
|||
|
||||
this.container = options.container;
|
||||
this.notebookPath = ko.observable(options.notebookContentItem.path);
|
||||
this.container.notebookServerInfo.subscribe(() => logConsoleInfo("New notebook server info received."));
|
||||
useNotebook.subscribe(
|
||||
() => logConsoleInfo("New notebook server info received."),
|
||||
(state) => state.notebookServerInfo
|
||||
);
|
||||
this.notebookComponentAdapter = new NotebookComponentAdapter({
|
||||
contentItem: options.notebookContentItem,
|
||||
notebooksBasePath: this.container.getNotebookBasePath(),
|
||||
notebooksBasePath: useNotebook.getState().notebookBasePath,
|
||||
notebookClient: NotebookTabBase.clientManager,
|
||||
onUpdateKernelInfo: this.onKernelUpdate,
|
||||
});
|
||||
|
@ -359,8 +363,8 @@ export default class NotebookTabV2 extends NotebookTabBase {
|
|||
};
|
||||
|
||||
private async configureServiceEndpoints(kernelName: string) {
|
||||
const notebookConnectionInfo = this.container && this.container.notebookServerInfo();
|
||||
const sparkClusterConnectionInfo = this.container && this.container.sparkClusterConnectionInfo();
|
||||
const notebookConnectionInfo = useNotebook.getState().notebookServerInfo;
|
||||
const sparkClusterConnectionInfo = useNotebook.getState().sparkClusterConnectionInfo;
|
||||
await NotebookConfigurationUtils.configureServiceEndpoints(
|
||||
this.notebookPath(),
|
||||
notebookConnectionInfo,
|
||||
|
|
|
@ -1,335 +0,0 @@
|
|||
<div class="tab-pane" data-bind="attr:{id: tabId}" role="tabpanel">
|
||||
<div class="tabPaneContentContainer">
|
||||
<div class="mongoQueryHelper" data-bind="visible: isPreferredApiMongoDB && sqlQueryEditorContent().length === 0">
|
||||
Start by writing a Mongo query, for example: <strong>{'id':'foo'}</strong> or <strong>{ }</strong> to get all the
|
||||
documents.
|
||||
</div>
|
||||
<div class="warningErrorContainer" aria-live="assertive" data-bind="visible: maybeSubQuery">
|
||||
<div class="warningErrorContent">
|
||||
<span><img class="paneErrorIcon" src="/info_color.svg" alt="Error" /></span>
|
||||
<span class="warningErrorDetailsLinkContainer">
|
||||
We have detected you may be using a subquery. Non-correlated subqueries are not currently supported.
|
||||
<a href="https://docs.microsoft.com/en-us/azure/cosmos-db/sql-query-subquery"
|
||||
>Please see Cosmos sub query documentation for further information</a
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="queryEditorWithSplitter" data-bind="attr: { id: queryEditorId }">
|
||||
<editor
|
||||
class="queryEditor"
|
||||
data-bind="css: { mongoQueryEditor: isPreferredApiMongoDB }"
|
||||
params="{
|
||||
content: initialEditorContent,
|
||||
contentType: monacoSettings.language,
|
||||
isReadOnly: monacoSettings.readOnly,
|
||||
lineNumbers: 'on',
|
||||
ariaLabel: 'Editing Query',
|
||||
updatedContent: sqlQueryEditorContent,
|
||||
selectedContent: selectedContent
|
||||
}"
|
||||
></editor>
|
||||
<!-- Splitter - Start -->
|
||||
<div class="splitter ui-resizable-handle ui-resizable-s" data-bind="attr: { id: splitterId }">
|
||||
<img class="queryEditorHorizontalSplitter" src="/HorizontalSplitter.svg" alt="Splitter" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Splitter - End -->
|
||||
|
||||
<!-- Script for results metadata that is common to all APIs -->
|
||||
<script type="text/html" id="result-metadata-template">
|
||||
<span>
|
||||
<span data-bind="text: showingDocumentsDisplayText"></span>
|
||||
</span>
|
||||
<span class="queryResultDivider" data-bind="visible: fetchNextPageButton.enabled"> | </span>
|
||||
<span class="queryResultNextEnable" data-bind="visible: fetchNextPageButton.enabled">
|
||||
<a data-bind="click: onFetchNextPageClick">
|
||||
<span>Load more</span>
|
||||
<img class="queryResultnextImg" src="/Query-Editor-Next.svg" alt="Fetch next page" />
|
||||
</a>
|
||||
</span>
|
||||
</script>
|
||||
|
||||
<!-- Query Errors Tab - Start-->
|
||||
<div class="active queryErrorsHeaderContainer" data-bind="visible: !!error()">
|
||||
<span class="queryErrors" data-toggle="tab" data-bind="attr: { href: '#queryerrors' + tabId }">Errors</span>
|
||||
</div>
|
||||
<!-- Query Errors Tab - End -->
|
||||
|
||||
<!-- Query Results & Errors Content Container - Start-->
|
||||
<div class="queryResultErrorContentContainer">
|
||||
<div
|
||||
class="queryEditorWatermark"
|
||||
data-bind="visible: allResultsMetadata().length === 0 && !error() && !queryResults() && !isExecuting()"
|
||||
>
|
||||
<p><img src="/RunQuery.png" alt="Execute Query Watermark" /></p>
|
||||
<p class="queryEditorWatermarkText">Execute a query to see the results</p>
|
||||
</div>
|
||||
<div
|
||||
class="queryResultsErrorsContent"
|
||||
data-bind="visible: allResultsMetadata().length > 0 || !!error() || queryResults()"
|
||||
>
|
||||
<div class="togglesWithMetadata" data-bind="visible: !error()">
|
||||
<div
|
||||
class="toggles"
|
||||
aria-label="Successful execution"
|
||||
id="execute-query-toggles"
|
||||
data-bind="event: { keydown: onToggleKeyDown }"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="tab">
|
||||
<input type="radio" class="radio" value="result" />
|
||||
<span
|
||||
class="toggleSwitch"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
data-bind="click: toggleResult, css:{ selectedToggle: isResultToggled(), unselectedToggle: !isResultToggled() }"
|
||||
aria-label="Results"
|
||||
>Results</span
|
||||
>
|
||||
</div>
|
||||
<div class="tab">
|
||||
<input type="radio" class="radio" value="logs" />
|
||||
<span
|
||||
class="toggleSwitch"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
data-bind="click: toggleMetrics, css:{ selectedToggle: isMetricsToggled(), unselectedToggle: !isMetricsToggled() }"
|
||||
aria-label="Query stats"
|
||||
>Query Stats</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="result-metadata"
|
||||
data-bind="template: { name: 'result-metadata-template' }, visible: isResultToggled()"
|
||||
></div>
|
||||
</div>
|
||||
<json-editor
|
||||
params="{ content: queryResults, isReadOnly: true, ariaLabel: 'Query results' }"
|
||||
data-bind="visible: queryResults() && queryResults().length > 0 && isResultToggled() && allResultsMetadata().length > 0 && !error()"
|
||||
>
|
||||
</json-editor>
|
||||
<div
|
||||
class="queryMetricsSummaryContainer"
|
||||
data-bind="visible: isMetricsToggled() && allResultsMetadata().length > 0 && !error()"
|
||||
>
|
||||
<table class="queryMetricsSummary">
|
||||
<caption>
|
||||
Query Statistics
|
||||
</caption>
|
||||
<thead class="queryMetricsSummaryHead">
|
||||
<tr class="queryMetricsSummaryHeader queryMetricsSummaryTuple">
|
||||
<th title="METRIC" scope="col">METRIC</th>
|
||||
<th title="VALUE" scope="col">VALUE</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="queryMetricsSummaryBody" data-bind="with: aggregatedQueryMetrics">
|
||||
<tr class="queryMetricsSummaryTuple">
|
||||
<td title="Request Charge">Request Charge</td>
|
||||
<td>
|
||||
<span
|
||||
data-bind="text: $parent.requestChargeDisplayText, attr: { title: $parent.requestChargeDisplayText }"
|
||||
></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="queryMetricsSummaryTuple">
|
||||
<td title="Showing Results">Showing Results</td>
|
||||
<td>
|
||||
<span
|
||||
data-bind="text: $parent.showingDocumentsDisplayText, attr: { title: $parent.showingDocumentsDisplayText }"
|
||||
></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
|
||||
<td>
|
||||
<span title="Retrieved document count">Retrieved document count</span>
|
||||
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
|
||||
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
|
||||
<span class="queryMetricTooltipText">Total number of retrieved documents</span>
|
||||
</span>
|
||||
</td>
|
||||
<td><span data-bind="text: retrievedDocumentCount, attr: { title: retrievedDocumentCount }"></span></td>
|
||||
</tr>
|
||||
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
|
||||
<td>
|
||||
<span title="Retrieved document size">Retrieved document size</span>
|
||||
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
|
||||
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
|
||||
<span class="queryMetricTooltipText">Total size of retrieved documents in bytes</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span data-bind="text: retrievedDocumentSize, attr: { title: retrievedDocumentSize }"></span>
|
||||
<span>bytes</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
|
||||
<td>
|
||||
<span title="Output document count">Output document count</span>
|
||||
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
|
||||
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
|
||||
<span class="queryMetricTooltipText">Number of output documents</span>
|
||||
</span>
|
||||
</td>
|
||||
<td><span data-bind="text: outputDocumentCount, attr: { title: outputDocumentCount }"></span></td>
|
||||
</tr>
|
||||
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
|
||||
<td>
|
||||
<span title="Output document size">Output document size</span>
|
||||
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
|
||||
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
|
||||
<span class="queryMetricTooltipText">Total size of output documents in bytes</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span data-bind="text: outputDocumentSize, attr: { title: outputDocumentSize }"></span>
|
||||
<span>bytes</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
|
||||
<td>
|
||||
<span title="Index hit document count">Index hit document count</span>
|
||||
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
|
||||
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
|
||||
<span class="queryMetricTooltipText">Total number of documents matched by the filter</span>
|
||||
</span>
|
||||
</td>
|
||||
<td><span data-bind="text: indexHitDocumentCount, attr: { title: indexHitDocumentCount }"></span></td>
|
||||
</tr>
|
||||
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
|
||||
<td>
|
||||
<span title="Index lookup time">Index lookup time</span>
|
||||
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
|
||||
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
|
||||
<span class="queryMetricTooltipText">Time spent in physical index layer</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span data-bind="text: indexLookupTime, attr: { title: indexLookupTime }"></span> <span>ms</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
|
||||
<td>
|
||||
<span title="Document load time">Document load time</span>
|
||||
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
|
||||
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
|
||||
<span class="queryMetricTooltipText">Time spent in loading documents</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span data-bind="text: documentLoadTime, attr: { title: documentLoadTime }"></span> <span>ms</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
|
||||
<td>
|
||||
<span title="Query engine execution time">Query engine execution time</span>
|
||||
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
|
||||
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
|
||||
<span class="queryMetricTooltipText queryEngineExeTimeInfo"
|
||||
>Time spent by the query engine to execute the query expression (excludes other execution times
|
||||
like load documents or write results)</span
|
||||
>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
data-bind="text: runtimeExecutionTimes.queryEngineExecutionTime, attr: { title: runtimeExecutionTimes.queryEngineExecutionTime }"
|
||||
></span>
|
||||
<span>ms</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
|
||||
<td>
|
||||
<span title="System function execution time">System function execution time</span>
|
||||
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
|
||||
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
|
||||
<span class="queryMetricTooltipText">Total time spent executing system (built-in) functions</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
data-bind="text: runtimeExecutionTimes.systemFunctionExecutionTime, attr: { title: runtimeExecutionTimes.systemFunctionExecutionTime }"
|
||||
></span>
|
||||
<span>ms</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
|
||||
<td>
|
||||
<span title="User defined function execution time">User defined function execution time</span>
|
||||
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
|
||||
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
|
||||
<span class="queryMetricTooltipText">Total time spent executing user-defined functions</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
data-bind="text: runtimeExecutionTimes.userDefinedFunctionExecutionTime, attr: { title: runtimeExecutionTimes.userDefinedFunctionExecutionTime }"
|
||||
></span>
|
||||
<span>ms</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
|
||||
<td>
|
||||
<span title="Document write time">Document write time</span>
|
||||
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
|
||||
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
|
||||
<span class="queryMetricTooltipText">Time spent to write query result set to response buffer</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span data-bind="text: documentWriteTime, attr: { title: documentWriteTime }"></span> <span>ms</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.roundTrips() != null">
|
||||
<td title="Round Trips">Round Trips</td>
|
||||
<td><span data-bind="text: $parent.roundTrips, attr: { title: $parent.roundTrips }"></span></td>
|
||||
</tr>
|
||||
<!-- TODO: Report activity id for mongo queries -->
|
||||
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.activityId() != null">
|
||||
<td title="Activity id">Activity id</td>
|
||||
<td></td>
|
||||
<td><span data-bind="text: $parent.activityId, attr: { title: $parent.activityId }"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="downloadMetricsLinkContainer" data-bind="visible: $parent.isQueryMetricsEnabled">
|
||||
<a
|
||||
id="downloadMetricsLink"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
data-bind="event: { click: onDownloadQueryMetricsCsvClick, keypress: onDownloadQueryMetricsCsvKeyPress }"
|
||||
>
|
||||
<img class="downloadCsvImg" src="/DownloadQuery.svg" alt="download query metrics csv" />
|
||||
<span>Per-partition query metrics (CSV)</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Query Errors Content - Start-->
|
||||
<div
|
||||
class="tab-pane active"
|
||||
data-bind="
|
||||
id: {
|
||||
href: 'queryerrors' + tabId
|
||||
},
|
||||
visible: !!error()"
|
||||
>
|
||||
<div class="errorContent">
|
||||
<span class="errorMessage" data-bind="text: error"></span>
|
||||
<span class="errorDetailsLink">
|
||||
<a
|
||||
data-bind="click: $parent.onErrorDetailsClick, event: { keypress: $parent.onErrorDetailsKeyPress }"
|
||||
id="error-display"
|
||||
tabindex="0"
|
||||
aria-label="Error details link"
|
||||
>More details</a
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Query Errors Content - End-->
|
||||
</div>
|
||||
</div>
|
||||
<!-- Results & Errors Content Container - End-->
|
||||
</div>
|
||||
</div>
|
|
@ -1,311 +0,0 @@
|
|||
@import "../../../less/Common/Constants";
|
||||
@import "../../../less/Common/TabCommon";
|
||||
|
||||
@MongoQueryEditorHeight: 50px;
|
||||
@ResultsTextFontWeight: 600;
|
||||
@ToggleHeight: 30px;
|
||||
@ToggleWidth: 250px;
|
||||
@QueryEngineExeInfo: 305px;
|
||||
|
||||
.tab-pane {
|
||||
.tabContentContainer();
|
||||
|
||||
.tabPaneContentContainer {
|
||||
.tabContentContainer();
|
||||
|
||||
.mongoQueryHelper {
|
||||
margin:@DefaultSpace 0px 0px 44px;
|
||||
position: absolute;
|
||||
top: 115px; //this is to avoid the jump of query editor
|
||||
}
|
||||
|
||||
.queryEditorWithSplitter {
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin-left: @SmallSpace;
|
||||
|
||||
.queryEditor {
|
||||
.flex-display();
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin-top: @SmallSpace;
|
||||
|
||||
.jsonEditor {
|
||||
border: none;
|
||||
margin-top: @SmallSpace;
|
||||
}
|
||||
}
|
||||
|
||||
.queryEditor.mongoQueryEditor {
|
||||
margin-top: 32px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.queryEditorHorizontalSplitter {
|
||||
margin: auto;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.queryErrorsHeaderContainer {
|
||||
padding: 24px @LargeSpace 0px @MediumSpace;
|
||||
|
||||
.queryErrors {
|
||||
font-size: @mediumFontSize;
|
||||
list-style-type: none;
|
||||
color: @BaseDark;
|
||||
font-weight: bold;
|
||||
margin-left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.queryResultErrorContentContainer {
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
font-size: @DefaultFontSize;
|
||||
padding: @DefaultSpace;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.queryEditorWatermark {
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
height: 25vh; // this is to align the water mark in center of the layout.
|
||||
|
||||
p {
|
||||
margin-bottom: @LargeSpace;
|
||||
color: @BaseHigh;
|
||||
}
|
||||
|
||||
.queryEditorWatermarkText {
|
||||
color: @BaseHigh;
|
||||
font-size: @DefaultFontSize;
|
||||
font-family: @DataExplorerFont;
|
||||
}
|
||||
}
|
||||
|
||||
.queryResultsErrorsContent {
|
||||
height: 100%;
|
||||
margin-left: @MediumSpace;
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
|
||||
|
||||
.togglesWithMetadata {
|
||||
margin-top: @MediumSpace;
|
||||
|
||||
.toggles {
|
||||
height: @ToggleHeight;
|
||||
width: @ToggleWidth;
|
||||
margin-left: @MediumSpace;
|
||||
|
||||
&:focus {
|
||||
.focus();
|
||||
}
|
||||
|
||||
.tab {
|
||||
margin-right: @MediumSpace;
|
||||
}
|
||||
|
||||
.toggleSwitch {
|
||||
.toggleSwitch();
|
||||
}
|
||||
|
||||
.selectedToggle {
|
||||
.selectedToggle();
|
||||
}
|
||||
|
||||
.unselectedToggle {
|
||||
.unselectedToggle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.result-metadata {
|
||||
padding: @LargeSpace @SmallSpace @MediumSpace @MediumSpace;
|
||||
|
||||
.queryResultDivider {
|
||||
margin-left: @SmallSpace;
|
||||
margin-right: @SmallSpace;
|
||||
}
|
||||
|
||||
.queryResultNextEnable {
|
||||
color: @AccentMediumHigh;
|
||||
font-size: @mediumFontSize;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
height: @ImgHeight;
|
||||
width: @ImgWidth;
|
||||
margin-left: @SmallSpace;
|
||||
}
|
||||
}
|
||||
|
||||
.queryResultNextDisable {
|
||||
color: @BaseMediumHigh;
|
||||
opacity: 0.5;
|
||||
font-size: @mediumFontSize;
|
||||
|
||||
img {
|
||||
height: @ImgHeight;
|
||||
width: @ImgWidth;
|
||||
margin-left: @SmallSpace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-pane.active {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.errorContent {
|
||||
.flex-display();
|
||||
width: 60%;
|
||||
white-space: nowrap;
|
||||
font-size: @mediumFontSize;
|
||||
padding: 0px @MediumSpace 0px @MediumSpace;
|
||||
|
||||
.errorMessage {
|
||||
padding: @SmallSpace;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.errorDetailsLink {
|
||||
cursor: pointer;
|
||||
padding: @SmallSpace;
|
||||
}
|
||||
|
||||
.queryMetricsSummaryContainer {
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
overflow: hidden;
|
||||
|
||||
.queryMetricsSummary {
|
||||
margin: @LargeSpace @LargeSpace 0px @DefaultSpace;
|
||||
table-layout: fixed;
|
||||
display: block;
|
||||
height: auto;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
caption {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.queryMetricsSummaryHead {
|
||||
.flex-display();
|
||||
}
|
||||
|
||||
.queryMetricsSummaryHeader.queryMetricsSummaryTuple {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.queryMetricsSummaryBody {
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
}
|
||||
|
||||
.queryMetricsSummaryTuple {
|
||||
border-bottom: 1px solid @BaseMedium;
|
||||
height: 32px;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
.flex-display();
|
||||
th, td {
|
||||
padding: @DefaultSpace;
|
||||
|
||||
&:nth-child(1) {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
flex: 0 0 50%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
flex: 0 0 50%;
|
||||
}
|
||||
|
||||
.queryMetricInfoTooltip {
|
||||
.infoTooltip();
|
||||
|
||||
&:hover .queryMetricTooltipText {
|
||||
.tooltipVisible();
|
||||
}
|
||||
|
||||
&:focus .queryMetricTooltipText {
|
||||
.tooltipVisible();
|
||||
}
|
||||
|
||||
.queryMetricTooltipText {
|
||||
top: -50px;
|
||||
width: auto;
|
||||
white-space: nowrap;
|
||||
left: 6px;
|
||||
visibility: hidden;
|
||||
background-color: @BaseHigh;
|
||||
color: @BaseLight;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
padding: @MediumSpace;
|
||||
|
||||
&::after {
|
||||
border-width: (2 * @MediumSpace) (2 * @MediumSpace) 0px 0px;
|
||||
bottom: -14px;
|
||||
.tooltipTextAfter();
|
||||
}
|
||||
}
|
||||
|
||||
.queryEngineExeTimeInfo {
|
||||
width: @QueryEngineExeInfo;
|
||||
top: -85px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.downloadMetricsLinkContainer {
|
||||
margin: 24px 0px 24px @MediumSpace;
|
||||
|
||||
#downloadMetricsLink {
|
||||
color: @BaseHigh;
|
||||
padding: @DefaultSpace;
|
||||
font-size: @mediumFontSize;
|
||||
border: @ButtonBorderWidth solid @BaseLight;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
.hover();
|
||||
}
|
||||
|
||||
&:active {
|
||||
border: @ButtonBorderWidth dashed @AccentMedium;
|
||||
.active();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json-editor {
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: @SmallSpace 0px @SmallSpace @MediumSpace;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
import * as ko from "knockout";
|
||||
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { updateUserContext } from "../../UserContext";
|
||||
import Explorer from "../Explorer";
|
||||
import QueryTab from "./QueryTab";
|
||||
|
||||
describe("Query Tab", () => {
|
||||
function getNewQueryTabForContainer(container: Explorer): QueryTab {
|
||||
const database = {
|
||||
container: container,
|
||||
id: ko.observable<string>("test"),
|
||||
isDatabaseShared: () => false,
|
||||
} as ViewModels.Database;
|
||||
const collection = {
|
||||
container: container,
|
||||
databaseId: "test",
|
||||
id: ko.observable<string>("test"),
|
||||
} as ViewModels.Collection;
|
||||
|
||||
return new QueryTab({
|
||||
tabKind: ViewModels.CollectionTabKind.Query,
|
||||
collection: collection,
|
||||
database: database,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
});
|
||||
}
|
||||
|
||||
describe("shouldSetSystemPartitionKeyContainerPartitionKeyValueUndefined", () => {
|
||||
const collection = {
|
||||
id: ko.observable<string>("withoutsystempk"),
|
||||
partitionKey: {
|
||||
systemKey: true,
|
||||
},
|
||||
} as ViewModels.Collection;
|
||||
|
||||
it("no container with system pk, should not set partition key option", () => {
|
||||
const iteratorOptions = QueryTab.getIteratorOptions(collection);
|
||||
expect(iteratorOptions.initialHeaders).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isQueryMetricsEnabled()", () => {
|
||||
let explorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer();
|
||||
});
|
||||
|
||||
it("should be true for accounts using SQL API", () => {
|
||||
updateUserContext({});
|
||||
const queryTab = getNewQueryTabForContainer(explorer);
|
||||
expect(queryTab.isQueryMetricsEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
it("should be false for accounts using other APIs", () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableGremlin" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
const queryTab = getNewQueryTabForContainer(explorer);
|
||||
expect(queryTab.isQueryMetricsEnabled()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Save Queries command button", () => {
|
||||
let explorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer();
|
||||
});
|
||||
|
||||
it("should be visible when using a supported API", () => {
|
||||
updateUserContext({});
|
||||
const queryTab = getNewQueryTabForContainer(explorer);
|
||||
expect(queryTab.saveQueryButton.visible()).toBe(true);
|
||||
});
|
||||
|
||||
it("should not be visible when using an unsupported API", () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableMongo" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
const queryTab = getNewQueryTabForContainer(explorer);
|
||||
expect(queryTab.saveQueryButton.visible()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,594 +0,0 @@
|
|||
import * as ko from "knockout";
|
||||
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
|
||||
import SaveQueryIcon from "../../../images/save-cosmos.svg";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
|
||||
import { queryDocumentsPage } from "../../Common/dataAccess/queryDocumentsPage";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import * as HeadersUtility from "../../Common/HeadersUtility";
|
||||
import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
|
||||
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { useNotificationConsole } from "../../hooks/useNotificationConsole";
|
||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import * as QueryUtils from "../../Utils/QueryUtils";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import template from "./QueryTab.html";
|
||||
import TabsBase from "./TabsBase";
|
||||
|
||||
enum ToggleState {
|
||||
Result,
|
||||
QueryMetrics,
|
||||
}
|
||||
|
||||
export default class QueryTab extends TabsBase implements ViewModels.WaitsForTemplate {
|
||||
public readonly html = template;
|
||||
public queryEditorId: string;
|
||||
public executeQueryButton: ViewModels.Button;
|
||||
public fetchNextPageButton: ViewModels.Button;
|
||||
public saveQueryButton: ViewModels.Button;
|
||||
public initialEditorContent: ko.Observable<string>;
|
||||
public maybeSubQuery: ko.Computed<boolean>;
|
||||
public sqlQueryEditorContent: ko.Observable<string>;
|
||||
public selectedContent: ko.Observable<string>;
|
||||
public sqlStatementToExecute: ko.Observable<string>;
|
||||
public queryResults: ko.Observable<string>;
|
||||
public error: ko.Observable<string>;
|
||||
public statusMessge: ko.Observable<string>;
|
||||
public statusIcon: ko.Observable<string>;
|
||||
public allResultsMetadata: ko.ObservableArray<ViewModels.QueryResultsMetadata>;
|
||||
public showingDocumentsDisplayText: ko.Observable<string>;
|
||||
public requestChargeDisplayText: ko.Observable<string>;
|
||||
public isTemplateReady: ko.Observable<boolean>;
|
||||
public splitterId: string;
|
||||
public splitter: Splitter;
|
||||
public isPreferredApiMongoDB: boolean;
|
||||
|
||||
public queryMetrics: ko.Observable<Map<string, DataModels.QueryMetrics>>;
|
||||
public aggregatedQueryMetrics: ko.Observable<DataModels.QueryMetrics>;
|
||||
public activityId: ko.Observable<string>;
|
||||
public roundTrips: ko.Observable<number>;
|
||||
public toggleState: ko.Observable<ToggleState>;
|
||||
public isQueryMetricsEnabled: ko.Computed<boolean>;
|
||||
|
||||
protected monacoSettings: ViewModels.MonacoEditorSettings;
|
||||
private _executeQueryButtonTitle: ko.Observable<string>;
|
||||
protected _iterator: MinimalQueryIterator;
|
||||
private _isSaveQueriesEnabled: ko.Computed<boolean>;
|
||||
private _resourceTokenPartitionKey: string;
|
||||
|
||||
_partitionKey: DataModels.PartitionKey;
|
||||
|
||||
constructor(options: ViewModels.QueryTabOptions) {
|
||||
super(options);
|
||||
this.queryEditorId = `queryeditor${this.tabId}`;
|
||||
this.showingDocumentsDisplayText = ko.observable<string>();
|
||||
this.requestChargeDisplayText = ko.observable<string>();
|
||||
const defaultQueryText = options.queryText != void 0 ? options.queryText : "SELECT * FROM c";
|
||||
this.initialEditorContent = ko.observable<string>(defaultQueryText);
|
||||
this.sqlQueryEditorContent = ko.observable<string>(defaultQueryText);
|
||||
this._executeQueryButtonTitle = ko.observable<string>("Execute Query");
|
||||
this.selectedContent = ko.observable<string>();
|
||||
this.selectedContent.subscribe((selectedContent: string) => {
|
||||
if (!selectedContent.trim()) {
|
||||
this._executeQueryButtonTitle("Execute Query");
|
||||
} else {
|
||||
this._executeQueryButtonTitle("Execute Selection");
|
||||
}
|
||||
});
|
||||
this.sqlStatementToExecute = ko.observable<string>("");
|
||||
this.queryResults = ko.observable<string>("");
|
||||
this.statusMessge = ko.observable<string>();
|
||||
this.statusIcon = ko.observable<string>();
|
||||
this.allResultsMetadata = ko.observableArray<ViewModels.QueryResultsMetadata>([]);
|
||||
this.error = ko.observable<string>();
|
||||
this._partitionKey = options.partitionKey;
|
||||
this._resourceTokenPartitionKey = options.resourceTokenPartitionKey;
|
||||
this.splitterId = this.tabId + "_splitter";
|
||||
this.isPreferredApiMongoDB = false;
|
||||
this.aggregatedQueryMetrics = ko.observable<DataModels.QueryMetrics>();
|
||||
this._resetAggregateQueryMetrics();
|
||||
this.queryMetrics = ko.observable<Map<string, DataModels.QueryMetrics>>(new Map());
|
||||
this.queryMetrics.subscribe((metrics) => this.aggregatedQueryMetrics(this._aggregateQueryMetrics(metrics)));
|
||||
this.isQueryMetricsEnabled = ko.computed<boolean>(() => {
|
||||
return userContext.apiType === "SQL" || false;
|
||||
});
|
||||
this.activityId = ko.observable<string>();
|
||||
this.roundTrips = ko.observable<number>();
|
||||
this.toggleState = ko.observable<ToggleState>(ToggleState.Result);
|
||||
|
||||
this.monacoSettings = new ViewModels.MonacoEditorSettings("sql", false);
|
||||
|
||||
this.executeQueryButton = {
|
||||
enabled: ko.computed<boolean>(() => {
|
||||
return !!this.sqlQueryEditorContent() && this.sqlQueryEditorContent().length > 0;
|
||||
}),
|
||||
|
||||
visible: ko.computed<boolean>(() => {
|
||||
return true;
|
||||
}),
|
||||
};
|
||||
|
||||
this._isSaveQueriesEnabled = ko.computed<boolean>(() => {
|
||||
const container = this.collection && this.collection.container;
|
||||
return userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
||||
});
|
||||
|
||||
this.maybeSubQuery = ko.computed<boolean>(function () {
|
||||
const sql = this.sqlQueryEditorContent();
|
||||
return sql && /.*\(.*SELECT.*\)/i.test(sql);
|
||||
}, this);
|
||||
|
||||
this.saveQueryButton = {
|
||||
enabled: this._isSaveQueriesEnabled,
|
||||
visible: this._isSaveQueriesEnabled,
|
||||
};
|
||||
|
||||
super.onTemplateReady((isTemplateReady: boolean) => {
|
||||
if (isTemplateReady) {
|
||||
const splitterBounds: SplitterBounds = {
|
||||
min: Constants.Queries.QueryEditorMinHeightRatio * window.innerHeight,
|
||||
max: $("#" + this.tabId).height() - Constants.Queries.QueryEditorMaxHeightRatio * window.innerHeight,
|
||||
};
|
||||
this.splitter = new Splitter({
|
||||
splitterId: this.splitterId,
|
||||
leftId: this.queryEditorId,
|
||||
bounds: splitterBounds,
|
||||
direction: SplitterDirection.Horizontal,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.fetchNextPageButton = {
|
||||
enabled: ko.computed<boolean>(() => {
|
||||
const allResultsMetadata = this.allResultsMetadata() || [];
|
||||
const numberOfResultsMetadata = allResultsMetadata.length;
|
||||
|
||||
if (numberOfResultsMetadata === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (allResultsMetadata[numberOfResultsMetadata - 1].hasMoreResults) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
|
||||
visible: ko.computed<boolean>(() => {
|
||||
return true;
|
||||
}),
|
||||
};
|
||||
|
||||
this._buildCommandBarOptions();
|
||||
}
|
||||
|
||||
public onTabClick(): void {
|
||||
super.onTabClick();
|
||||
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Query);
|
||||
}
|
||||
|
||||
public onExecuteQueryClick = async (): Promise<void> => {
|
||||
const sqlStatement: string = this.selectedContent() || this.sqlQueryEditorContent();
|
||||
this.sqlStatementToExecute(sqlStatement);
|
||||
this.allResultsMetadata([]);
|
||||
this.queryResults("");
|
||||
this._iterator = undefined;
|
||||
|
||||
await this._executeQueryDocumentsPage(0);
|
||||
};
|
||||
|
||||
public onSaveQueryClick = (): void => {
|
||||
this.collection && this.collection.container && this.collection.container.openSaveQueryPanel();
|
||||
};
|
||||
|
||||
public onSavedQueriesClick = (): void => {
|
||||
this.collection && this.collection.container && this.collection.container.openBrowseQueriesPanel();
|
||||
};
|
||||
|
||||
public async onFetchNextPageClick(): Promise<void> {
|
||||
const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || [];
|
||||
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
|
||||
const firstResultIndex: number = (metadata && Number(metadata.firstItemIndex)) || 1;
|
||||
const itemCount: number = (metadata && Number(metadata.itemCount)) || 0;
|
||||
|
||||
await this._executeQueryDocumentsPage(firstResultIndex + itemCount - 1);
|
||||
}
|
||||
|
||||
public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => {
|
||||
useNotificationConsole.getState().expandConsole();
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
public onErrorDetailsKeyPress = (src: any, event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
|
||||
this.onErrorDetailsClick(src, null);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
public toggleResult(): void {
|
||||
this.toggleState(ToggleState.Result);
|
||||
this.queryResults.valueHasMutated(); // needed to refresh the json-editor component
|
||||
}
|
||||
|
||||
public toggleMetrics(): void {
|
||||
this.toggleState(ToggleState.QueryMetrics);
|
||||
}
|
||||
|
||||
public onToggleKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === Constants.KeyCodes.LeftArrow) {
|
||||
this.toggleResult();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
} else if (event.keyCode === Constants.KeyCodes.RightArrow) {
|
||||
this.toggleMetrics();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
public togglesOnFocus(): void {
|
||||
const focusElement = document.getElementById("execute-query-toggles");
|
||||
focusElement && focusElement.focus();
|
||||
}
|
||||
|
||||
public isResultToggled(): boolean {
|
||||
return this.toggleState() === ToggleState.Result;
|
||||
}
|
||||
|
||||
public isMetricsToggled(): boolean {
|
||||
return this.toggleState() === ToggleState.QueryMetrics;
|
||||
}
|
||||
|
||||
public onDownloadQueryMetricsCsvClick = (source: any, event: MouseEvent): boolean => {
|
||||
this._downloadQueryMetricsCsvData();
|
||||
return false;
|
||||
};
|
||||
|
||||
public onDownloadQueryMetricsCsvKeyPress = (source: any, event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === Constants.KeyCodes.Space || Constants.KeyCodes.Enter) {
|
||||
this._downloadQueryMetricsCsvData();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
private async _executeQueryDocumentsPage(firstItemIndex: number): Promise<any> {
|
||||
this.error("");
|
||||
this.roundTrips(undefined);
|
||||
if (this._iterator === undefined) {
|
||||
this._initIterator();
|
||||
}
|
||||
|
||||
await this._queryDocumentsPage(firstItemIndex);
|
||||
}
|
||||
|
||||
// TODO: Position and enable spinner when request is in progress
|
||||
private async _queryDocumentsPage(firstItemIndex: number): Promise<void> {
|
||||
this.isExecutionError(false);
|
||||
this._resetAggregateQueryMetrics();
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.ExecuteQuery, {
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
});
|
||||
let options: any = {};
|
||||
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
|
||||
|
||||
const queryDocuments = async (firstItemIndex: number) =>
|
||||
await queryDocumentsPage(this.collection && this.collection.id(), this._iterator, firstItemIndex);
|
||||
this.isExecuting(true);
|
||||
|
||||
try {
|
||||
const queryResults: ViewModels.QueryResults = await QueryUtils.queryPagesUntilContentPresent(
|
||||
firstItemIndex,
|
||||
queryDocuments
|
||||
);
|
||||
const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || [];
|
||||
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
|
||||
const resultsMetadata: ViewModels.QueryResultsMetadata = {
|
||||
hasMoreResults: queryResults.hasMoreResults,
|
||||
itemCount: queryResults.itemCount,
|
||||
firstItemIndex: queryResults.firstItemIndex,
|
||||
lastItemIndex: queryResults.lastItemIndex,
|
||||
};
|
||||
this.allResultsMetadata.push(resultsMetadata);
|
||||
this.activityId(queryResults.activityId);
|
||||
this.roundTrips(queryResults.roundTrips);
|
||||
|
||||
this._updateQueryMetricsMap(queryResults.headers[Constants.HttpHeaders.queryMetrics]);
|
||||
|
||||
if (queryResults.itemCount == 0 && metadata != null && metadata.itemCount >= 0) {
|
||||
// we let users query for the next page because the SDK sometimes specifies there are more elements
|
||||
// even though there aren't any so we should not update the prior query results.
|
||||
return;
|
||||
}
|
||||
|
||||
const documents: any[] = queryResults.documents;
|
||||
const results = this.renderObjectForEditor(documents, null, 4);
|
||||
|
||||
const resultsDisplay: string =
|
||||
queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`;
|
||||
this.showingDocumentsDisplayText(resultsDisplay);
|
||||
this.requestChargeDisplayText(`${queryResults.requestCharge} RUs`);
|
||||
this.queryResults(results);
|
||||
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.ExecuteQuery,
|
||||
{
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
} catch (error) {
|
||||
this.isExecutionError(true);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
this.error(errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.ExecuteQuery,
|
||||
{
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
error: errorMessage,
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
document.getElementById("error-display").focus();
|
||||
} finally {
|
||||
this.isExecuting(false);
|
||||
this.togglesOnFocus();
|
||||
}
|
||||
}
|
||||
|
||||
private _updateQueryMetricsMap(metricsMap: { [partitionKeyRange: string]: DataModels.QueryMetrics }): void {
|
||||
if (!metricsMap) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(metricsMap).forEach((key: string) => {
|
||||
this.queryMetrics().set(key, metricsMap[key]);
|
||||
});
|
||||
this.queryMetrics.valueHasMutated();
|
||||
}
|
||||
|
||||
private _aggregateQueryMetrics(metricsMap: Map<string, DataModels.QueryMetrics>): DataModels.QueryMetrics {
|
||||
if (!metricsMap) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const aggregatedMetrics: DataModels.QueryMetrics = this.aggregatedQueryMetrics();
|
||||
metricsMap.forEach((queryMetrics) => {
|
||||
if (queryMetrics) {
|
||||
aggregatedMetrics.documentLoadTime =
|
||||
queryMetrics.documentLoadTime &&
|
||||
this._normalize(queryMetrics.documentLoadTime.totalMilliseconds()) +
|
||||
this._normalize(aggregatedMetrics.documentLoadTime);
|
||||
aggregatedMetrics.documentWriteTime =
|
||||
queryMetrics.documentWriteTime &&
|
||||
this._normalize(queryMetrics.documentWriteTime.totalMilliseconds()) +
|
||||
this._normalize(aggregatedMetrics.documentWriteTime);
|
||||
aggregatedMetrics.indexHitDocumentCount =
|
||||
queryMetrics.indexHitDocumentCount &&
|
||||
this._normalize(queryMetrics.indexHitDocumentCount) +
|
||||
this._normalize(aggregatedMetrics.indexHitDocumentCount);
|
||||
aggregatedMetrics.outputDocumentCount =
|
||||
queryMetrics.outputDocumentCount &&
|
||||
this._normalize(queryMetrics.outputDocumentCount) + this._normalize(aggregatedMetrics.outputDocumentCount);
|
||||
aggregatedMetrics.outputDocumentSize =
|
||||
queryMetrics.outputDocumentSize &&
|
||||
this._normalize(queryMetrics.outputDocumentSize) + this._normalize(aggregatedMetrics.outputDocumentSize);
|
||||
aggregatedMetrics.indexLookupTime =
|
||||
queryMetrics.indexLookupTime &&
|
||||
this._normalize(queryMetrics.indexLookupTime.totalMilliseconds()) +
|
||||
this._normalize(aggregatedMetrics.indexLookupTime);
|
||||
aggregatedMetrics.retrievedDocumentCount =
|
||||
queryMetrics.retrievedDocumentCount &&
|
||||
this._normalize(queryMetrics.retrievedDocumentCount) +
|
||||
this._normalize(aggregatedMetrics.retrievedDocumentCount);
|
||||
aggregatedMetrics.retrievedDocumentSize =
|
||||
queryMetrics.retrievedDocumentSize &&
|
||||
this._normalize(queryMetrics.retrievedDocumentSize) +
|
||||
this._normalize(aggregatedMetrics.retrievedDocumentSize);
|
||||
aggregatedMetrics.vmExecutionTime =
|
||||
queryMetrics.vmExecutionTime &&
|
||||
this._normalize(queryMetrics.vmExecutionTime.totalMilliseconds()) +
|
||||
this._normalize(aggregatedMetrics.vmExecutionTime);
|
||||
aggregatedMetrics.totalQueryExecutionTime =
|
||||
queryMetrics.totalQueryExecutionTime &&
|
||||
this._normalize(queryMetrics.totalQueryExecutionTime.totalMilliseconds()) +
|
||||
this._normalize(aggregatedMetrics.totalQueryExecutionTime);
|
||||
|
||||
aggregatedMetrics.runtimeExecutionTimes.queryEngineExecutionTime =
|
||||
aggregatedMetrics.runtimeExecutionTimes &&
|
||||
this._normalize(queryMetrics.runtimeExecutionTimes.queryEngineExecutionTime.totalMilliseconds()) +
|
||||
this._normalize(aggregatedMetrics.runtimeExecutionTimes.queryEngineExecutionTime);
|
||||
aggregatedMetrics.runtimeExecutionTimes.systemFunctionExecutionTime =
|
||||
aggregatedMetrics.runtimeExecutionTimes &&
|
||||
this._normalize(queryMetrics.runtimeExecutionTimes.systemFunctionExecutionTime.totalMilliseconds()) +
|
||||
this._normalize(aggregatedMetrics.runtimeExecutionTimes.systemFunctionExecutionTime);
|
||||
aggregatedMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime =
|
||||
aggregatedMetrics.runtimeExecutionTimes &&
|
||||
this._normalize(queryMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime.totalMilliseconds()) +
|
||||
this._normalize(aggregatedMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime);
|
||||
}
|
||||
});
|
||||
|
||||
return aggregatedMetrics;
|
||||
}
|
||||
|
||||
public _downloadQueryMetricsCsvData(): void {
|
||||
const csvData: string = this._generateQueryMetricsCsvData();
|
||||
if (!csvData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (navigator.msSaveBlob) {
|
||||
// for IE and Edge
|
||||
navigator.msSaveBlob(
|
||||
new Blob([csvData], { type: "data:text/csv;charset=utf-8" }),
|
||||
"PerPartitionQueryMetrics.csv"
|
||||
);
|
||||
} else {
|
||||
const downloadLink: HTMLAnchorElement = document.createElement("a");
|
||||
downloadLink.href = "data:text/csv;charset=utf-8," + encodeURI(csvData);
|
||||
downloadLink.target = "_self";
|
||||
downloadLink.download = "QueryMetricsPerPartition.csv";
|
||||
|
||||
// for some reason, FF displays the download prompt only when
|
||||
// the link is added to the dom so we add and remove it
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
downloadLink.remove();
|
||||
}
|
||||
}
|
||||
|
||||
protected _initIterator(): void {
|
||||
const options: any = QueryTab.getIteratorOptions(this.collection);
|
||||
if (this._resourceTokenPartitionKey) {
|
||||
options.partitionKey = this._resourceTokenPartitionKey;
|
||||
}
|
||||
|
||||
this._iterator = queryDocuments(
|
||||
this.collection.databaseId,
|
||||
this.collection.id(),
|
||||
this.sqlStatementToExecute(),
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
public static getIteratorOptions(container: ViewModels.CollectionBase): any {
|
||||
let options: any = {};
|
||||
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
|
||||
return options;
|
||||
}
|
||||
|
||||
private _normalize(value: number): number {
|
||||
if (!value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private _resetAggregateQueryMetrics(): void {
|
||||
this.aggregatedQueryMetrics({
|
||||
clientSideMetrics: {},
|
||||
documentLoadTime: undefined,
|
||||
documentWriteTime: undefined,
|
||||
indexHitDocumentCount: undefined,
|
||||
outputDocumentCount: undefined,
|
||||
outputDocumentSize: undefined,
|
||||
indexLookupTime: undefined,
|
||||
retrievedDocumentCount: undefined,
|
||||
retrievedDocumentSize: undefined,
|
||||
vmExecutionTime: undefined,
|
||||
queryPreparationTimes: undefined,
|
||||
runtimeExecutionTimes: {
|
||||
queryEngineExecutionTime: undefined,
|
||||
systemFunctionExecutionTime: undefined,
|
||||
userDefinedFunctionExecutionTime: undefined,
|
||||
},
|
||||
totalQueryExecutionTime: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private _generateQueryMetricsCsvData(): string {
|
||||
if (!this.queryMetrics()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const queryMetrics = this.queryMetrics();
|
||||
let csvData: string = "";
|
||||
const columnHeaders: string =
|
||||
[
|
||||
"Partition key range id",
|
||||
"Retrieved document count",
|
||||
"Retrieved document size (in bytes)",
|
||||
"Output document count",
|
||||
"Output document size (in bytes)",
|
||||
"Index hit document count",
|
||||
"Index lookup time (ms)",
|
||||
"Document load time (ms)",
|
||||
"Query engine execution time (ms)",
|
||||
"System function execution time (ms)",
|
||||
"User defined function execution time (ms)",
|
||||
"Document write time (ms)",
|
||||
].join(",") + "\n";
|
||||
csvData = csvData + columnHeaders;
|
||||
queryMetrics.forEach((queryMetric, partitionKeyRangeId) => {
|
||||
const partitionKeyRangeData: string =
|
||||
[
|
||||
partitionKeyRangeId,
|
||||
queryMetric.retrievedDocumentCount,
|
||||
queryMetric.retrievedDocumentSize,
|
||||
queryMetric.outputDocumentCount,
|
||||
queryMetric.outputDocumentSize,
|
||||
queryMetric.indexHitDocumentCount,
|
||||
queryMetric.indexLookupTime && queryMetric.indexLookupTime.totalMilliseconds(),
|
||||
queryMetric.documentLoadTime && queryMetric.documentLoadTime.totalMilliseconds(),
|
||||
queryMetric.runtimeExecutionTimes &&
|
||||
queryMetric.runtimeExecutionTimes.queryEngineExecutionTime &&
|
||||
queryMetric.runtimeExecutionTimes.queryEngineExecutionTime.totalMilliseconds(),
|
||||
queryMetric.runtimeExecutionTimes &&
|
||||
queryMetric.runtimeExecutionTimes.systemFunctionExecutionTime &&
|
||||
queryMetric.runtimeExecutionTimes.systemFunctionExecutionTime.totalMilliseconds(),
|
||||
queryMetric.runtimeExecutionTimes &&
|
||||
queryMetric.runtimeExecutionTimes.userDefinedFunctionExecutionTime &&
|
||||
queryMetric.runtimeExecutionTimes.userDefinedFunctionExecutionTime.totalMilliseconds(),
|
||||
queryMetric.documentWriteTime && queryMetric.documentWriteTime.totalMilliseconds(),
|
||||
].join(",") + "\n";
|
||||
csvData = csvData + partitionKeyRangeData;
|
||||
});
|
||||
|
||||
return csvData;
|
||||
}
|
||||
|
||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
if (this.executeQueryButton.visible()) {
|
||||
const label = this._executeQueryButtonTitle();
|
||||
buttons.push({
|
||||
iconSrc: ExecuteQueryIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: this.onExecuteQueryClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.executeQueryButton.enabled(),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.saveQueryButton.visible()) {
|
||||
const label = "Save Query";
|
||||
buttons.push({
|
||||
iconSrc: SaveQueryIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: this.onSaveQueryClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.saveQueryButton.enabled(),
|
||||
});
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
private _buildCommandBarOptions(): void {
|
||||
ko.computed(() =>
|
||||
ko.toJSON([this.executeQueryButton.visible, this.executeQueryButton.enabled, this._executeQueryButtonTitle])
|
||||
).subscribe(() => this.updateNavbarWithTabsButtons());
|
||||
this.updateNavbarWithTabsButtons();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import React from "react";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import type { QueryTabOptions } from "../../../Contracts/ViewModels";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import Explorer from "../../Explorer";
|
||||
import { IQueryTabComponentProps, ITabAccessor } from "../../Tabs/QueryTab/QueryTabComponent";
|
||||
import TabsBase from "../TabsBase";
|
||||
import QueryTabComponent from "./QueryTabComponent";
|
||||
|
||||
export interface IQueryTabProps {
|
||||
container: Explorer;
|
||||
}
|
||||
|
||||
export class NewQueryTab extends TabsBase {
|
||||
public queryText: string;
|
||||
public currentQuery: string;
|
||||
public partitionKey: DataModels.PartitionKey;
|
||||
public iQueryTabComponentProps: IQueryTabComponentProps;
|
||||
public iTabAccessor: ITabAccessor;
|
||||
|
||||
constructor(options: QueryTabOptions, private props: IQueryTabProps) {
|
||||
super(options);
|
||||
this.partitionKey = options.partitionKey;
|
||||
this.iQueryTabComponentProps = {
|
||||
collection: this.collection,
|
||||
isExecutionError: this.isExecutionError(),
|
||||
tabId: this.tabId,
|
||||
tabsBaseInstance: this,
|
||||
queryText: options.queryText,
|
||||
partitionKey: this.partitionKey,
|
||||
container: this.props.container,
|
||||
onTabAccessor: (instance: ITabAccessor): void => {
|
||||
this.iTabAccessor = instance;
|
||||
},
|
||||
isPreferredApiMongoDB: false,
|
||||
};
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return <QueryTabComponent {...this.iQueryTabComponentProps} />;
|
||||
}
|
||||
|
||||
public onTabClick(): void {
|
||||
useTabs.getState().activateTab(this);
|
||||
this.iTabAccessor.onTabClickEvent();
|
||||
}
|
||||
|
||||
public onCloseTabButtonClick(): void {
|
||||
useTabs.getState().closeTab(this);
|
||||
if (this.iTabAccessor) {
|
||||
this.iTabAccessor.onCloseClickEvent(true);
|
||||
}
|
||||
}
|
||||
|
||||
public getContainer(): Explorer {
|
||||
return this.props.container;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,285 @@
|
|||
@import "../../../../less/Common/Constants.less";
|
||||
@import "../../../../less/Common/TabCommon.less";
|
||||
|
||||
@MongoQueryEditorHeight: 50px;
|
||||
@ResultsTextFontWeight: 600;
|
||||
@ToggleHeight: 30px;
|
||||
@ToggleWidth: 250px;
|
||||
@QueryEngineExeInfo: 305px;
|
||||
|
||||
.tab-pane {
|
||||
.tabContentContainer();
|
||||
|
||||
.tabPaneContentContainer {
|
||||
position: relative;
|
||||
.tabContentContainer();
|
||||
|
||||
.mongoQueryHelper {
|
||||
margin: @DefaultSpace 0px 0px 44px;
|
||||
}
|
||||
|
||||
.splitter-layout {
|
||||
.layout-pane-primary {
|
||||
overflow: hidden !important;
|
||||
.queryEditor {
|
||||
.flex-display();
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin-top: @SmallSpace;
|
||||
|
||||
.jsonEditor {
|
||||
border: none;
|
||||
margin-top: @SmallSpace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.queryEditor.mongoQueryEditor {
|
||||
margin-top: 32px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.queryEditorHorizontalSplitter {
|
||||
margin: auto;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.queryErrorsHeaderContainer {
|
||||
padding: 24px @LargeSpace 0px @MediumSpace;
|
||||
|
||||
.queryErrors {
|
||||
font-size: @mediumFontSize;
|
||||
list-style-type: none;
|
||||
color: @BaseDark;
|
||||
font-weight: bold;
|
||||
margin-left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.queryResultErrorContentContainer {
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
font-size: @DefaultFontSize;
|
||||
padding: @DefaultSpace;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.queryEditorWatermark {
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
height: 25vh; // this is to align the water mark in center of the layout.
|
||||
|
||||
p {
|
||||
margin-bottom: @LargeSpace;
|
||||
color: @BaseHigh;
|
||||
}
|
||||
|
||||
.queryEditorWatermarkText {
|
||||
color: @BaseHigh;
|
||||
font-size: @DefaultFontSize;
|
||||
font-family: @DataExplorerFont;
|
||||
}
|
||||
}
|
||||
|
||||
.queryResultsErrorsContent {
|
||||
height: 100%;
|
||||
margin-left: @MediumSpace;
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
|
||||
div[role="tabpanel"] {
|
||||
height: 100%;
|
||||
div:nth-child(1) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.result-metadata {
|
||||
padding: @LargeSpace @SmallSpace @MediumSpace @MediumSpace;
|
||||
height: auto !important;
|
||||
.queryResultDivider {
|
||||
margin-left: @SmallSpace;
|
||||
margin-right: @SmallSpace;
|
||||
}
|
||||
|
||||
.queryResultNextEnable {
|
||||
color: @AccentMediumHigh;
|
||||
font-size: @mediumFontSize;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
height: @ImgHeight;
|
||||
width: @ImgWidth;
|
||||
margin-left: @SmallSpace;
|
||||
}
|
||||
}
|
||||
|
||||
.queryResultNextDisable {
|
||||
color: @BaseMediumHigh;
|
||||
opacity: 0.5;
|
||||
font-size: @mediumFontSize;
|
||||
|
||||
img {
|
||||
height: @ImgHeight;
|
||||
width: @ImgWidth;
|
||||
margin-left: @SmallSpace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-pane.active {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.errorContent {
|
||||
.flex-display();
|
||||
width: 60%;
|
||||
white-space: nowrap;
|
||||
font-size: @mediumFontSize;
|
||||
padding: 0px @MediumSpace 0px @MediumSpace;
|
||||
|
||||
.errorMessage {
|
||||
padding: @SmallSpace;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.errorDetailsLink {
|
||||
cursor: pointer;
|
||||
padding: @SmallSpace;
|
||||
}
|
||||
|
||||
.queryMetricsSummaryContainer {
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
.queryMetricsSummary {
|
||||
margin: @LargeSpace @LargeSpace 0px @DefaultSpace;
|
||||
table-layout: fixed;
|
||||
display: block;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
caption {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.queryMetricsSummaryHead {
|
||||
.flex-display();
|
||||
}
|
||||
|
||||
.queryMetricsSummaryHeader.queryMetricsSummaryTuple {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.queryMetricsSummaryBody {
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
}
|
||||
|
||||
.queryMetricsSummaryTuple {
|
||||
border-bottom: 1px solid @BaseMedium;
|
||||
height: 32px;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
.flex-display();
|
||||
th,
|
||||
td {
|
||||
padding: @DefaultSpace;
|
||||
|
||||
&:nth-child(1) {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
flex: 0 0 50%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
flex: 0 0 50%;
|
||||
}
|
||||
|
||||
.queryMetricInfoTooltip {
|
||||
.infoTooltip();
|
||||
|
||||
&:hover .queryMetricTooltipText {
|
||||
.tooltipVisible();
|
||||
}
|
||||
|
||||
&:focus .queryMetricTooltipText {
|
||||
.tooltipVisible();
|
||||
}
|
||||
|
||||
.queryMetricTooltipText {
|
||||
top: -50px;
|
||||
width: auto;
|
||||
white-space: nowrap;
|
||||
left: 6px;
|
||||
visibility: hidden;
|
||||
background-color: @BaseHigh;
|
||||
color: @BaseLight;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
padding: @MediumSpace;
|
||||
|
||||
&::after {
|
||||
border-width: (2 * @MediumSpace) (2 * @MediumSpace) 0px 0px;
|
||||
bottom: -14px;
|
||||
.tooltipTextAfter();
|
||||
}
|
||||
}
|
||||
|
||||
.queryEngineExeTimeInfo {
|
||||
width: @QueryEngineExeInfo;
|
||||
top: -85px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.downloadMetricsLinkContainer {
|
||||
margin: 24px 0px 50px @MediumSpace;
|
||||
position: sticky;
|
||||
#downloadMetricsLink {
|
||||
color: @BaseHigh;
|
||||
padding: @DefaultSpace;
|
||||
font-size: @mediumFontSize;
|
||||
border: @ButtonBorderWidth solid @BaseLight;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
.hover();
|
||||
}
|
||||
|
||||
&:active {
|
||||
border: @ButtonBorderWidth dashed @AccentMedium;
|
||||
.active();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json-editor {
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: @SmallSpace 0px @SmallSpace @MediumSpace;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,5 @@
|
|||
import * as ko from "knockout";
|
||||
import Q from "q";
|
||||
import React from "react";
|
||||
import AddEntityIcon from "../../../images/AddEntity.svg";
|
||||
import DeleteEntitiesIcon from "../../../images/DeleteEntities.svg";
|
||||
import EditEntityIcon from "../../../images/Edit-entity.svg";
|
||||
|
@ -7,13 +7,16 @@ import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
|
|||
import QueryBuilderIcon from "../../../images/Query-Builder.svg";
|
||||
import QueryTextIcon from "../../../images/Query-Text.svg";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { useSidePanel } from "../../hooks/useSidePanel";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../Explorer";
|
||||
import { AddTableEntityPanel } from "../Panes/Tables/AddTableEntityPanel";
|
||||
import { EditTableEntityPanel } from "../Panes/Tables/EditTableEntityPanel";
|
||||
import TableCommands from "../Tables/DataTable/TableCommands";
|
||||
import TableEntityListViewModel from "../Tables/DataTable/TableEntityListViewModel";
|
||||
import QueryViewModel from "../Tables/QueryBuilder/QueryViewModel";
|
||||
import { TableDataClient } from "../Tables/TableDataClient";
|
||||
import { CassandraAPIDataClient, TableDataClient } from "../Tables/TableDataClient";
|
||||
import template from "./QueryTablesTab.html";
|
||||
import TabsBase from "./TabsBase";
|
||||
|
||||
|
@ -130,34 +133,36 @@ export default class QueryTablesTab extends TabsBase {
|
|||
this.buildCommandBarOptions();
|
||||
}
|
||||
|
||||
public onExecuteQueryClick = (): Q.Promise<any> => {
|
||||
this.queryViewModel().runQuery();
|
||||
return null;
|
||||
public onAddEntityClick = (): void => {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Add Table Entity",
|
||||
<AddTableEntityPanel
|
||||
tableDataClient={this.tableDataClient}
|
||||
queryTablesTab={this}
|
||||
tableEntityListViewModel={this.tableEntityListViewModel()}
|
||||
cassandraApiClient={new CassandraAPIDataClient()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
public onQueryBuilderClick = (): Q.Promise<any> => {
|
||||
this.queryViewModel().selectHelper();
|
||||
return null;
|
||||
public onEditEntityClick = (): void => {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Edit Table Entity",
|
||||
<EditTableEntityPanel
|
||||
tableDataClient={this.tableDataClient}
|
||||
queryTablesTab={this}
|
||||
tableEntityListViewModel={this.tableEntityListViewModel()}
|
||||
cassandraApiClient={new CassandraAPIDataClient()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
public onQueryTextClick = (): Q.Promise<any> => {
|
||||
this.queryViewModel().selectEditor();
|
||||
return null;
|
||||
};
|
||||
|
||||
public onAddEntityClick = (): Q.Promise<any> => {
|
||||
this.container.openAddTableEntityPanel(this, this.tableEntityListViewModel());
|
||||
return null;
|
||||
};
|
||||
|
||||
public onEditEntityClick = (): Q.Promise<any> => {
|
||||
this.container.openEditTableEntityPanel(this, this.tableEntityListViewModel());
|
||||
return null;
|
||||
};
|
||||
|
||||
public onDeleteEntityClick = (): Q.Promise<any> => {
|
||||
public onDeleteEntityClick = (): void => {
|
||||
this.tableCommands.deleteEntitiesCommand(this.tableEntityListViewModel());
|
||||
return null;
|
||||
};
|
||||
|
||||
public onActivate(): void {
|
||||
|
@ -166,7 +171,7 @@ export default class QueryTablesTab extends TabsBase {
|
|||
!!this.tableEntityListViewModel() &&
|
||||
!!this.tableEntityListViewModel().table &&
|
||||
this.tableEntityListViewModel().table.columns;
|
||||
if (!!columns) {
|
||||
if (columns) {
|
||||
columns.adjust();
|
||||
$(window).resize();
|
||||
}
|
||||
|
@ -179,7 +184,7 @@ export default class QueryTablesTab extends TabsBase {
|
|||
buttons.push({
|
||||
iconSrc: QueryBuilderIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: this.onQueryBuilderClick,
|
||||
onCommandClick: () => this.queryViewModel().selectHelper(),
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
|
@ -193,7 +198,7 @@ export default class QueryTablesTab extends TabsBase {
|
|||
buttons.push({
|
||||
iconSrc: QueryTextIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: this.onQueryTextClick,
|
||||
onCommandClick: () => this.queryViewModel().selectEditor(),
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
|
@ -207,7 +212,7 @@ export default class QueryTablesTab extends TabsBase {
|
|||
buttons.push({
|
||||
iconSrc: ExecuteQueryIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: this.onExecuteQueryClick,
|
||||
onCommandClick: () => this.queryViewModel().runQuery(),
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
|
@ -1,139 +1,23 @@
|
|||
import ko from "knockout";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import React from "react";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import { traceFailure } from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
||||
import { SettingsComponentProps } from "../Controls/Settings/SettingsComponent";
|
||||
import { SettingsComponentAdapter } from "../Controls/Settings/SettingsComponentAdapter";
|
||||
import { SettingsComponent } from "../Controls/Settings/SettingsComponent";
|
||||
import TabsBase from "./TabsBase";
|
||||
|
||||
export class SettingsTabV2 extends TabsBase {
|
||||
public readonly html = '<div style="height: 100%" data-bind="react:settingsComponentAdapter"></div>';
|
||||
public settingsComponentAdapter: SettingsComponentAdapter;
|
||||
|
||||
constructor(options: ViewModels.TabOptions) {
|
||||
super(options);
|
||||
const props: SettingsComponentProps = {
|
||||
settingsTab: this,
|
||||
};
|
||||
this.settingsComponentAdapter = new SettingsComponentAdapter(props);
|
||||
public render(): JSX.Element {
|
||||
return <SettingsComponent settingsTab={this} />;
|
||||
}
|
||||
}
|
||||
|
||||
export class CollectionSettingsTabV2 extends SettingsTabV2 {
|
||||
private notificationRead: ko.Observable<boolean>;
|
||||
private notification: DataModels.Notification;
|
||||
private offerRead: ko.Observable<boolean>;
|
||||
|
||||
constructor(options: ViewModels.TabOptions) {
|
||||
super(options);
|
||||
|
||||
this.tabId = "SettingsV2-" + this.tabId;
|
||||
this.notificationRead = ko.observable(false);
|
||||
this.offerRead = ko.observable(false);
|
||||
this.settingsComponentAdapter.parameters = ko.computed<boolean>(() => {
|
||||
if (this.notificationRead() && this.offerRead()) {
|
||||
this.pendingNotification(this.notification);
|
||||
this.notification = undefined;
|
||||
this.offerRead(false);
|
||||
this.notificationRead(false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public async onActivate(): Promise<void> {
|
||||
try {
|
||||
this.isExecuting(true);
|
||||
|
||||
const collection: ViewModels.Collection = this.collection as ViewModels.Collection;
|
||||
await collection.loadOffer();
|
||||
// passed in options and set by parent as "Settings" by default
|
||||
this.tabTitle(collection.offer() ? "Settings" : "Scale & Settings");
|
||||
|
||||
const data: DataModels.Notification = await collection.getPendingThroughputSplitNotification();
|
||||
this.notification = data;
|
||||
this.notificationRead(true);
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
this.notification = undefined;
|
||||
this.notificationRead(true);
|
||||
traceFailure(
|
||||
Action.Tab,
|
||||
{
|
||||
databaseName: this.collection.databaseId,
|
||||
collectionName: this.collection.id(),
|
||||
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle,
|
||||
error: errorMessage,
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
this.onLoadStartKey
|
||||
);
|
||||
logConsoleError(`Error while fetching container settings for container ${this.collection.id()}: ${errorMessage}`);
|
||||
throw error;
|
||||
} finally {
|
||||
this.offerRead(true);
|
||||
this.isExecuting(false);
|
||||
}
|
||||
|
||||
public onActivate(): void {
|
||||
super.onActivate();
|
||||
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.CollectionSettingsV2);
|
||||
}
|
||||
}
|
||||
|
||||
export class DatabaseSettingsTabV2 extends SettingsTabV2 {
|
||||
private notificationRead: ko.Observable<boolean>;
|
||||
private notification: DataModels.Notification;
|
||||
|
||||
constructor(options: ViewModels.TabOptions) {
|
||||
super(options);
|
||||
this.tabId = "DatabaseSettingsV2-" + this.tabId;
|
||||
this.notificationRead = ko.observable(false);
|
||||
this.settingsComponentAdapter.parameters = ko.computed<boolean>(() => {
|
||||
if (this.notificationRead()) {
|
||||
this.pendingNotification(this.notification);
|
||||
this.notification = undefined;
|
||||
this.notificationRead(false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public async onActivate(): Promise<void> {
|
||||
try {
|
||||
this.isExecuting(true);
|
||||
|
||||
const data: DataModels.Notification = await this.database.getPendingThroughputSplitNotification();
|
||||
this.notification = data;
|
||||
this.notificationRead(true);
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
this.notification = undefined;
|
||||
this.notificationRead(true);
|
||||
traceFailure(
|
||||
Action.Tab,
|
||||
{
|
||||
databaseName: this.database.id(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle,
|
||||
error: errorMessage,
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
this.onLoadStartKey
|
||||
);
|
||||
logConsoleError(`Error while fetching database settings for database ${this.database.id()}: ${errorMessage}`);
|
||||
throw error;
|
||||
} finally {
|
||||
this.isExecuting(false);
|
||||
}
|
||||
|
||||
public onActivate(): void {
|
||||
super.onActivate();
|
||||
this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettingsV2);
|
||||
}
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
<div class="tab-pane flexContainer stored-procedure-tab" data-bind="attr:{ id: tabId }" role="tabpanel">
|
||||
<!-- Stored Procedure Tab Form - Start -->
|
||||
<div class="storedTabForm flexContainer">
|
||||
<div class="formTitleFirst">Stored Procedure Id</div>
|
||||
<span class="formTitleTextbox">
|
||||
<input
|
||||
class="formTree"
|
||||
type="text"
|
||||
required
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||
aria-label="Stored procedure id"
|
||||
placeholder="Enter the new stored procedure id"
|
||||
size="40"
|
||||
data-bind="
|
||||
textInput: id"
|
||||
/>
|
||||
</span>
|
||||
<div class="spUdfTriggerHeader">Stored Procedure Body</div>
|
||||
<editor
|
||||
params="{
|
||||
content: originalSprocBody,
|
||||
contentType: 'javascript',
|
||||
isReadOnly: false,
|
||||
ariaLabel: 'Stored procedure body',
|
||||
lineNumbers: 'on',
|
||||
updatedContent: editorContent,
|
||||
theme: _theme
|
||||
}"
|
||||
data-bind="attr: { id: editorId }"
|
||||
></editor>
|
||||
<!-- Results & Errors Content - Start-->
|
||||
<div class="results-container" data-bind="visible: hasResults">
|
||||
<div
|
||||
class="toggles"
|
||||
id="execute-storedproc-toggles"
|
||||
aria-label="Successful execution of stored procedure"
|
||||
data-bind="event: { keydown: onToggleKeyDown }"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="tab">
|
||||
<input type="radio" class="radio" value="result" />
|
||||
<span
|
||||
class="toggleSwitch"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
data-bind="click: toggleResult, css:{ selectedToggle: isResultToggled(), unselectedToggle: !isResultToggled() }"
|
||||
aria-label="Result"
|
||||
>Result</span
|
||||
>
|
||||
</div>
|
||||
<div class="tab">
|
||||
<input type="radio" class="radio" value="logs" />
|
||||
<span
|
||||
class="toggleSwitch"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
data-bind="click: toggleLogs, css:{ selectedToggle: isLogsToggled(), unselectedToggle: !isLogsToggled() }"
|
||||
aria-label="console.log"
|
||||
>console.log</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<json-editor
|
||||
params="{ content: resultsData, isReadOnly: true, ariaLabel: 'Execute stored procedure result' }"
|
||||
data-bind="attr: { id: executeResultsEditorId }, visible: hasResults() && isResultToggled()"
|
||||
>
|
||||
</json-editor>
|
||||
<json-editor
|
||||
params="{ content: logsData, isReadOnly: true, ariaLabel: 'Execute stored procedure logs' }"
|
||||
data-bind="attr: { id: executeLogsEditorId }, visible: hasResults() && isLogsToggled()"
|
||||
></json-editor>
|
||||
</div>
|
||||
<div class="errors-container" data-bind="visible: hasErrors">
|
||||
<div class="errors-header">Errors:</div>
|
||||
<div class="errorContent">
|
||||
<span class="errorMessage" data-bind="text: error"></span>
|
||||
<span class="errorDetailsLink">
|
||||
<a
|
||||
data-bind="click: $data.onErrorDetailsClick, event: { keypress: $data.onErrorDetailsKeyPress }"
|
||||
aria-label="Error details link"
|
||||
>More details</a
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Results & Errors Content - End-->
|
||||
</div>
|
||||
</div>
|
|
@ -1,287 +0,0 @@
|
|||
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||
import * as ko from "knockout";
|
||||
import Q from "q";
|
||||
import * as _ from "underscore";
|
||||
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { createStoredProcedure } from "../../Common/dataAccess/createStoredProcedure";
|
||||
import { updateStoredProcedure } from "../../Common/dataAccess/updateStoredProcedure";
|
||||
import editable from "../../Common/EditableUtility";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { useNotificationConsole } from "../../hooks/useNotificationConsole";
|
||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import StoredProcedure from "../Tree/StoredProcedure";
|
||||
import ScriptTabBase from "./ScriptTabBase";
|
||||
import template from "./StoredProcedureTab.html";
|
||||
|
||||
enum ToggleState {
|
||||
Result = "result",
|
||||
Logs = "logs",
|
||||
}
|
||||
|
||||
export default class StoredProcedureTab extends ScriptTabBase {
|
||||
public readonly html = template;
|
||||
public collection: ViewModels.Collection;
|
||||
public node: StoredProcedure;
|
||||
public executeResultsEditorId: string;
|
||||
public executeLogsEditorId: string;
|
||||
public toggleState: ko.Observable<ToggleState>;
|
||||
public originalSprocBody: ViewModels.Editable<string>;
|
||||
public resultsData: ko.Observable<string>;
|
||||
public logsData: ko.Observable<string>;
|
||||
public error: ko.Observable<string>;
|
||||
public hasResults: ko.Observable<boolean>;
|
||||
public hasErrors: ko.Observable<boolean>;
|
||||
|
||||
constructor(options: ViewModels.ScriptTabOption) {
|
||||
super(options);
|
||||
super.onActivate.bind(this);
|
||||
|
||||
this.executeResultsEditorId = `executestoredprocedureresults${this.tabId}`;
|
||||
this.executeLogsEditorId = `executestoredprocedurelogs${this.tabId}`;
|
||||
this.toggleState = ko.observable<ToggleState>(ToggleState.Result);
|
||||
this.originalSprocBody = editable.observable<string>(this.editorContent());
|
||||
this.resultsData = ko.observable<string>();
|
||||
this.logsData = ko.observable<string>();
|
||||
this.error = ko.observable<string>();
|
||||
this.hasResults = ko.observable<boolean>(false);
|
||||
this.hasErrors = ko.observable<boolean>(false);
|
||||
this.error.subscribe((error: string) => {
|
||||
this.hasErrors(error != null);
|
||||
this.hasResults(error == null);
|
||||
});
|
||||
|
||||
this.ariaLabel("Stored Procedure Body");
|
||||
this.buildCommandBarOptions();
|
||||
}
|
||||
|
||||
public onSaveClick = (): Promise<StoredProcedureDefinition & Resource> => {
|
||||
return this._createStoredProcedure({
|
||||
id: this.id(),
|
||||
body: this.editorContent(),
|
||||
});
|
||||
};
|
||||
|
||||
public onDiscard = (): Q.Promise<any> => {
|
||||
this.setBaselines();
|
||||
const original = this.editorContent.getEditableOriginalValue();
|
||||
this.originalSprocBody(original);
|
||||
this.originalSprocBody.valueHasMutated(); // trigger a re-render of the editor
|
||||
|
||||
return Q();
|
||||
};
|
||||
|
||||
public onUpdateClick = (): Promise<any> => {
|
||||
const data = this._getResource();
|
||||
|
||||
this.isExecutionError(false);
|
||||
this.isExecuting(true);
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.UpdateStoredProcedure, {
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
});
|
||||
return updateStoredProcedure(this.collection.databaseId, this.collection.id(), data)
|
||||
.then(
|
||||
(updatedResource) => {
|
||||
this.resource(updatedResource);
|
||||
this.tabTitle(updatedResource.id);
|
||||
this.node.id(updatedResource.id);
|
||||
this.node.body(updatedResource.body as string);
|
||||
this.setBaselines();
|
||||
|
||||
const editorModel = this.editor() && this.editor().getModel();
|
||||
editorModel && editorModel.setValue(updatedResource.body as string);
|
||||
this.editorContent.setBaseline(updatedResource.body as string);
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.UpdateStoredProcedure,
|
||||
{
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
},
|
||||
(error: any) => {
|
||||
this.isExecutionError(true);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.UpdateStoredProcedure,
|
||||
{
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
}
|
||||
)
|
||||
.finally(() => this.isExecuting(false));
|
||||
};
|
||||
|
||||
public onExecuteSprocsResult(result: any, logsData: any): void {
|
||||
const resultData: string = this.renderObjectForEditor(_.omit(result, "scriptLogs").result, null, 4);
|
||||
const scriptLogs: string = (result.scriptLogs && decodeURIComponent(result.scriptLogs)) || "";
|
||||
const logs: string = this.renderObjectForEditor(scriptLogs, null, 4);
|
||||
this.error(null);
|
||||
this.resultsData(resultData);
|
||||
this.logsData(logs);
|
||||
}
|
||||
|
||||
public onExecuteSprocsError(error: string): void {
|
||||
this.isExecutionError(true);
|
||||
console.error(error);
|
||||
this.error(error);
|
||||
}
|
||||
|
||||
public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => {
|
||||
useNotificationConsole.getState().expandConsole();
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
public onErrorDetailsKeyPress = (src: any, event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
|
||||
this.onErrorDetailsClick(src, null);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
public toggleResult(): void {
|
||||
this.toggleState(ToggleState.Result);
|
||||
this.resultsData.valueHasMutated(); // needed to refresh the json-editor component
|
||||
}
|
||||
|
||||
public toggleLogs(): void {
|
||||
this.toggleState(ToggleState.Logs);
|
||||
this.logsData.valueHasMutated(); // needed to refresh the json-editor component
|
||||
}
|
||||
|
||||
public onToggleKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === Constants.KeyCodes.LeftArrow) {
|
||||
this.toggleResult();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
} else if (event.keyCode === Constants.KeyCodes.RightArrow) {
|
||||
this.toggleLogs();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
public isResultToggled(): boolean {
|
||||
return this.toggleState() === ToggleState.Result;
|
||||
}
|
||||
|
||||
public isLogsToggled(): boolean {
|
||||
return this.toggleState() === ToggleState.Logs;
|
||||
}
|
||||
|
||||
protected updateSelectedNode(): void {
|
||||
if (this.collection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const database: ViewModels.Database = this.collection.getDatabase();
|
||||
if (!database.isDatabaseExpanded()) {
|
||||
this.collection.container.selectedNode(database);
|
||||
} else if (!this.collection.isCollectionExpanded() || !this.collection.isStoredProceduresExpanded()) {
|
||||
this.collection.container.selectedNode(this.collection);
|
||||
} else {
|
||||
this.collection.container.selectedNode(this.node);
|
||||
}
|
||||
}
|
||||
|
||||
protected buildCommandBarOptions(): void {
|
||||
ko.computed(() => ko.toJSON([this.isNew, this.formIsDirty])).subscribe(() => this.updateNavbarWithTabsButtons());
|
||||
super.buildCommandBarOptions();
|
||||
}
|
||||
|
||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||
const label = "Execute";
|
||||
return super.getTabsButtons().concat({
|
||||
iconSrc: ExecuteQueryIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
this.collection && this.collection.container.openExecuteSprocParamsPanel(this.node);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: this.isNew() || this.formIsDirty(),
|
||||
});
|
||||
}
|
||||
|
||||
private _getResource() {
|
||||
return {
|
||||
id: this.id(),
|
||||
body: this.editorContent(),
|
||||
};
|
||||
}
|
||||
|
||||
private _createStoredProcedure(resource: StoredProcedureDefinition): Promise<StoredProcedureDefinition & Resource> {
|
||||
this.isExecutionError(false);
|
||||
this.isExecuting(true);
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.CreateStoredProcedure, {
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
});
|
||||
|
||||
return createStoredProcedure(this.collection.databaseId, this.collection.id(), resource)
|
||||
.then(
|
||||
(createdResource) => {
|
||||
this.tabTitle(createdResource.id);
|
||||
this.isNew(false);
|
||||
this.resource(createdResource);
|
||||
this.hashLocation(
|
||||
`${Constants.HashRoutePrefixes.collectionsWithIds(
|
||||
this.collection.databaseId,
|
||||
this.collection.id()
|
||||
)}/sprocs/${createdResource.id}`
|
||||
);
|
||||
this.setBaselines();
|
||||
|
||||
const editorModel = this.editor() && this.editor().getModel();
|
||||
editorModel && editorModel.setValue(createdResource.body as string);
|
||||
this.editorContent.setBaseline(createdResource.body as string);
|
||||
this.node = this.collection.createStoredProcedureNode(createdResource);
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.CreateStoredProcedure,
|
||||
{
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
this.editorState(ViewModels.ScriptEditorState.exisitingNoEdits);
|
||||
return createdResource;
|
||||
},
|
||||
(createError) => {
|
||||
this.isExecutionError(true);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.CreateStoredProcedure,
|
||||
{
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
error: getErrorMessage(createError),
|
||||
errorStack: getErrorStack(createError),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
return Promise.reject(createError);
|
||||
}
|
||||
)
|
||||
.finally(() => this.isExecuting(false));
|
||||
}
|
||||
|
||||
public onDelete(): Q.Promise<any> {
|
||||
// TODO
|
||||
return Q();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
import React from "react";
|
||||
import { ExecuteSprocResult } from "../../../Common/dataAccess/executeStoredProcedure";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import Explorer from "../../Explorer";
|
||||
import StoredProcedure from "../../Tree/StoredProcedure";
|
||||
import ScriptTabBase from "../ScriptTabBase";
|
||||
import StoredProcedureTabComponent, {
|
||||
IStoredProcTabComponentProps,
|
||||
IStorProcTabComponentAccessor,
|
||||
} from "./StoredProcedureTabComponent";
|
||||
|
||||
export interface IStoredProcTabProps {
|
||||
container: Explorer;
|
||||
collection: ViewModels.Collection;
|
||||
}
|
||||
|
||||
export class NewStoredProcedureTab extends ScriptTabBase {
|
||||
public queryText: string;
|
||||
public currentQuery: string;
|
||||
public partitionKey: DataModels.PartitionKey;
|
||||
public iStoredProcTabComponentProps: IStoredProcTabComponentProps;
|
||||
public iStoreProcAccessor: IStorProcTabComponentAccessor;
|
||||
public node: StoredProcedure;
|
||||
public onSaveClick: () => void;
|
||||
public onUpdateClick: () => Promise<void>;
|
||||
|
||||
constructor(options: ViewModels.ScriptTabOption, private props: IStoredProcTabProps) {
|
||||
super(options);
|
||||
this.partitionKey = options.partitionKey;
|
||||
|
||||
this.iStoredProcTabComponentProps = {
|
||||
resource: options.resource,
|
||||
isNew: options.isNew,
|
||||
tabKind: options.tabKind,
|
||||
title: options.title,
|
||||
tabPath: options.tabPath,
|
||||
collectionBase: options.collection,
|
||||
node: options.node,
|
||||
scriptTabBaseInstance: this,
|
||||
collection: props.collection,
|
||||
iStorProcTabComponentAccessor: (instance: IStorProcTabComponentAccessor) => {
|
||||
this.iStoreProcAccessor = instance;
|
||||
},
|
||||
container: props.container,
|
||||
};
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return <StoredProcedureTabComponent {...this.iStoredProcTabComponentProps} />;
|
||||
}
|
||||
|
||||
public onTabClick(): void {
|
||||
useTabs.getState().activateTab(this);
|
||||
this.iStoreProcAccessor.onTabClickEvent();
|
||||
}
|
||||
|
||||
public onCloseTabButtonClick(): void {
|
||||
useTabs.getState().closeTab(this);
|
||||
}
|
||||
|
||||
public onExecuteSprocsResult(result: ExecuteSprocResult): void {
|
||||
this.iStoreProcAccessor.onExecuteSprocsResultEvent(result);
|
||||
}
|
||||
|
||||
public onExecuteSprocsError(error: string): void {
|
||||
this.iStoreProcAccessor.onExecuteSprocsErrorEvent(error);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,597 @@
|
|||
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||
import { Pivot, PivotItem } from "@fluentui/react";
|
||||
import React from "react";
|
||||
import DiscardIcon from "../../../../images/discard.svg";
|
||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||
import SaveIcon from "../../../../images/save-cosmos.svg";
|
||||
import { NormalizedEventKey } from "../../../Common/Constants";
|
||||
import { createStoredProcedure } from "../../../Common/dataAccess/createStoredProcedure";
|
||||
import { ExecuteSprocResult } from "../../../Common/dataAccess/executeStoredProcedure";
|
||||
import { updateStoredProcedure } from "../../../Common/dataAccess/updateStoredProcedure";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { useNotificationConsole } from "../../../hooks/useNotificationConsole";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
||||
import Explorer from "../../Explorer";
|
||||
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import StoredProcedure from "../../Tree/StoredProcedure";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
import ScriptTabBase from "../ScriptTabBase";
|
||||
|
||||
export interface IStorProcTabComponentAccessor {
|
||||
onExecuteSprocsResultEvent: (result: ExecuteSprocResult) => void;
|
||||
onExecuteSprocsErrorEvent: (error: string) => void;
|
||||
onTabClickEvent: () => void;
|
||||
}
|
||||
|
||||
export interface Button {
|
||||
visible: boolean;
|
||||
enabled: boolean;
|
||||
isSelected?: boolean;
|
||||
}
|
||||
|
||||
interface IStoredProcTabComponentStates {
|
||||
hasResults: boolean;
|
||||
hasErrors: boolean;
|
||||
error: string;
|
||||
resultData: string;
|
||||
logsData: string;
|
||||
originalSprocBody: string;
|
||||
initialEditorContent: string;
|
||||
sProcEditorContent: string;
|
||||
id: string;
|
||||
executeButton: Button;
|
||||
saveButton: Button;
|
||||
updateButton: Button;
|
||||
discardButton: Button;
|
||||
}
|
||||
|
||||
export interface IStoredProcTabComponentProps {
|
||||
resource: StoredProcedureDefinition;
|
||||
isNew: boolean;
|
||||
tabKind: ViewModels.CollectionTabKind;
|
||||
title: string;
|
||||
tabPath: string;
|
||||
collectionBase: ViewModels.CollectionBase;
|
||||
//eslint-disable-next-line
|
||||
node?: any;
|
||||
scriptTabBaseInstance: ScriptTabBase;
|
||||
collection: ViewModels.Collection;
|
||||
iStorProcTabComponentAccessor: (instance: IStorProcTabComponentAccessor) => void;
|
||||
container: Explorer;
|
||||
}
|
||||
|
||||
export default class StoredProcedureTabComponent extends React.Component<
|
||||
IStoredProcTabComponentProps,
|
||||
IStoredProcTabComponentStates
|
||||
> {
|
||||
public node: StoredProcedure;
|
||||
public executeResultsEditorId: string;
|
||||
public executeLogsEditorId: string;
|
||||
public collection: ViewModels.Collection;
|
||||
|
||||
constructor(
|
||||
public storedProcTabCompProps: IStoredProcTabComponentProps,
|
||||
private storedProcTabCompStates: IStoredProcTabComponentStates
|
||||
) {
|
||||
super(storedProcTabCompProps);
|
||||
this.state = {
|
||||
error: "",
|
||||
hasErrors: false,
|
||||
hasResults: false,
|
||||
resultData: "",
|
||||
logsData: "",
|
||||
originalSprocBody: this.props.resource.body.toString(),
|
||||
initialEditorContent: this.props.resource.body.toString(),
|
||||
sProcEditorContent: this.props.resource.body.toString(),
|
||||
id: this.props.resource.id,
|
||||
executeButton: {
|
||||
enabled: !this.props.scriptTabBaseInstance.isNew(),
|
||||
visible: true,
|
||||
},
|
||||
saveButton: {
|
||||
enabled: (() => {
|
||||
if (!this.props.scriptTabBaseInstance.formIsValid()) {
|
||||
return false;
|
||||
}
|
||||
if (!this.props.scriptTabBaseInstance.formIsDirty()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})(),
|
||||
visible: this.props.scriptTabBaseInstance.isNew(),
|
||||
},
|
||||
updateButton: {
|
||||
enabled: (() => {
|
||||
if (!this.props.scriptTabBaseInstance.formIsValid()) {
|
||||
return false;
|
||||
}
|
||||
if (!this.props.scriptTabBaseInstance.formIsDirty()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})(),
|
||||
visible: !this.props.scriptTabBaseInstance.isNew(),
|
||||
},
|
||||
discardButton: {
|
||||
enabled: (() => {
|
||||
if (!this.props.scriptTabBaseInstance.formIsValid()) {
|
||||
return false;
|
||||
}
|
||||
if (!this.props.scriptTabBaseInstance.formIsDirty()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})(),
|
||||
visible: true,
|
||||
},
|
||||
};
|
||||
|
||||
this.collection = this.props.collection;
|
||||
this.executeResultsEditorId = `executestoredprocedureresults${this.props.scriptTabBaseInstance.tabId}`;
|
||||
this.executeLogsEditorId = `executestoredprocedurelogs${this.props.scriptTabBaseInstance.tabId}`;
|
||||
this.props.scriptTabBaseInstance.ariaLabel("Stored Procedure Body");
|
||||
|
||||
this.props.iStorProcTabComponentAccessor({
|
||||
onExecuteSprocsResultEvent: this.onExecuteSprocsResult.bind(this),
|
||||
onExecuteSprocsErrorEvent: this.onExecuteSprocsError.bind(this),
|
||||
onTabClickEvent: this.onTabClick.bind(this),
|
||||
});
|
||||
|
||||
this.node = this.props.node;
|
||||
|
||||
this.buildCommandBarOptions();
|
||||
}
|
||||
|
||||
public onTabClick(): void {
|
||||
if (useTabs.getState().openedTabs.length > 0) {
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
}
|
||||
}
|
||||
|
||||
public onSaveClick = (): Promise<StoredProcedureDefinition & Resource> => {
|
||||
return this._createStoredProcedure({
|
||||
id: this.state.id,
|
||||
body: this.state.sProcEditorContent,
|
||||
});
|
||||
};
|
||||
|
||||
public onDiscard = (): Promise<unknown> => {
|
||||
const onDiscardPromise = new Promise(() => {
|
||||
this.props.scriptTabBaseInstance.setBaselines();
|
||||
const original = this.props.scriptTabBaseInstance.editorContent.getEditableOriginalValue();
|
||||
if (this.state.updateButton.visible) {
|
||||
this.setState({
|
||||
updateButton: {
|
||||
enabled: false,
|
||||
visible: true,
|
||||
},
|
||||
sProcEditorContent: original,
|
||||
discardButton: {
|
||||
enabled: false,
|
||||
visible: true,
|
||||
},
|
||||
executeButton: {
|
||||
enabled: true,
|
||||
visible: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
saveButton: {
|
||||
enabled: false,
|
||||
visible: true,
|
||||
},
|
||||
sProcEditorContent: original,
|
||||
discardButton: {
|
||||
enabled: false,
|
||||
visible: true,
|
||||
},
|
||||
executeButton: {
|
||||
enabled: false,
|
||||
visible: true,
|
||||
},
|
||||
id: "",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
}, 100);
|
||||
|
||||
return onDiscardPromise;
|
||||
};
|
||||
|
||||
public onUpdateClick = (): Promise<void> => {
|
||||
const data = this._getResource();
|
||||
|
||||
this.props.scriptTabBaseInstance.isExecutionError(false);
|
||||
this.props.scriptTabBaseInstance.isExecuting(true);
|
||||
|
||||
return updateStoredProcedure(
|
||||
this.props.scriptTabBaseInstance.collection.databaseId,
|
||||
this.props.scriptTabBaseInstance.collection.id(),
|
||||
data
|
||||
)
|
||||
.then(
|
||||
(updatedResource) => {
|
||||
this.props.scriptTabBaseInstance.resource(updatedResource);
|
||||
this.props.scriptTabBaseInstance.tabTitle(updatedResource.id);
|
||||
this.node.id(updatedResource.id);
|
||||
this.node.body(updatedResource.body as string);
|
||||
this.props.scriptTabBaseInstance.setBaselines();
|
||||
|
||||
const editorModel =
|
||||
this.props.scriptTabBaseInstance.editor() && this.props.scriptTabBaseInstance.editor().getModel();
|
||||
editorModel && editorModel.setValue(updatedResource.body as string);
|
||||
this.props.scriptTabBaseInstance.editorContent.setBaseline(updatedResource.body as string);
|
||||
this.setState({
|
||||
discardButton: {
|
||||
enabled: false,
|
||||
visible: true,
|
||||
},
|
||||
updateButton: {
|
||||
enabled: false,
|
||||
visible: true,
|
||||
},
|
||||
executeButton: {
|
||||
enabled: true,
|
||||
visible: true,
|
||||
},
|
||||
});
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
},
|
||||
() => {
|
||||
this.props.scriptTabBaseInstance.isExecutionError(true);
|
||||
}
|
||||
)
|
||||
.finally(() => this.props.scriptTabBaseInstance.isExecuting(false));
|
||||
};
|
||||
|
||||
public onExecuteSprocsResult(result: ExecuteSprocResult): void {
|
||||
const resultData: string = this.props.scriptTabBaseInstance.renderObjectForEditor(result.result, undefined, 4);
|
||||
const scriptLogs: string = (result.scriptLogs && decodeURIComponent(result.scriptLogs)) || "";
|
||||
const logs: string = this.props.scriptTabBaseInstance.renderObjectForEditor(scriptLogs, undefined, 4);
|
||||
|
||||
this.setState({
|
||||
hasResults: false,
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
error: undefined,
|
||||
resultData: resultData,
|
||||
logsData: logs,
|
||||
hasResults: resultData ? true : false,
|
||||
hasErrors: false,
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
public onExecuteSprocsError(error: string): void {
|
||||
this.props.scriptTabBaseInstance.isExecutionError(true);
|
||||
console.error(error);
|
||||
this.setState({
|
||||
error: error,
|
||||
hasErrors: true,
|
||||
hasResults: false,
|
||||
});
|
||||
}
|
||||
|
||||
public onErrorDetailsClick = (): boolean => {
|
||||
useNotificationConsole.getState().expandConsole();
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
public onErrorDetailsKeyPress = (event: React.KeyboardEvent<HTMLAnchorElement>): boolean => {
|
||||
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
|
||||
this.onErrorDetailsClick();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
protected updateSelectedNode(): void {
|
||||
if (this.props.collectionBase === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const database: ViewModels.Database = this.props.collectionBase.getDatabase();
|
||||
const setSelectedNode = useSelectedNode.getState().setSelectedNode;
|
||||
if (!database.isDatabaseExpanded()) {
|
||||
setSelectedNode(database);
|
||||
} else if (!this.props.collectionBase.isCollectionExpanded() || !this.collection.isStoredProceduresExpanded()) {
|
||||
setSelectedNode(this.props.collectionBase);
|
||||
} else {
|
||||
setSelectedNode(this.node);
|
||||
}
|
||||
}
|
||||
|
||||
protected buildCommandBarOptions(): void {
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
}
|
||||
|
||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
const label = "Save";
|
||||
if (this.state.saveButton.visible) {
|
||||
buttons.push({
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: this.onSaveClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.state.saveButton.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.state.updateButton.visible) {
|
||||
const label = "Update";
|
||||
buttons.push({
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: this.onUpdateClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.state.updateButton.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.state.discardButton.visible) {
|
||||
const label = "Discard";
|
||||
buttons.push({
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: this.onDiscard,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.state.discardButton.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.state.executeButton.visible) {
|
||||
const label = "Execute";
|
||||
buttons.push({
|
||||
iconSrc: ExecuteQueryIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
this.collection.container.openExecuteSprocParamsPanel(this.node);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.state.executeButton.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
private _getResource() {
|
||||
return {
|
||||
id: this.state.id,
|
||||
body: this.state.sProcEditorContent,
|
||||
};
|
||||
}
|
||||
|
||||
private _createStoredProcedure(resource: StoredProcedureDefinition): Promise<StoredProcedureDefinition & Resource> {
|
||||
this.props.scriptTabBaseInstance.isExecutionError(false);
|
||||
this.props.scriptTabBaseInstance.isExecuting(true);
|
||||
|
||||
return createStoredProcedure(this.props.collectionBase.databaseId, this.props.collectionBase.id(), resource)
|
||||
.then(
|
||||
(createdResource) => {
|
||||
this.props.scriptTabBaseInstance.tabTitle(createdResource.id);
|
||||
this.props.scriptTabBaseInstance.isNew(false);
|
||||
this.props.scriptTabBaseInstance.resource(createdResource);
|
||||
this.props.scriptTabBaseInstance.setBaselines();
|
||||
|
||||
const editorModel =
|
||||
this.props.scriptTabBaseInstance.editor() && this.props.scriptTabBaseInstance.editor().getModel();
|
||||
editorModel && editorModel.setValue(createdResource.body as string);
|
||||
this.props.scriptTabBaseInstance.editorContent.setBaseline(createdResource.body as string);
|
||||
this.node = this.collection.createStoredProcedureNode(createdResource);
|
||||
this.props.scriptTabBaseInstance.node = this.node;
|
||||
useTabs.getState().updateTab(this.props.scriptTabBaseInstance);
|
||||
this.props.scriptTabBaseInstance.editorState(ViewModels.ScriptEditorState.exisitingNoEdits);
|
||||
|
||||
this.setState({
|
||||
executeButton: {
|
||||
enabled: false,
|
||||
visible: true,
|
||||
},
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
executeButton: {
|
||||
enabled: true,
|
||||
visible: true,
|
||||
},
|
||||
updateButton: {
|
||||
enabled: false,
|
||||
visible: true,
|
||||
},
|
||||
saveButton: {
|
||||
enabled: false,
|
||||
visible: false,
|
||||
},
|
||||
discardButton: {
|
||||
enabled: false,
|
||||
visible: true,
|
||||
},
|
||||
sProcEditorContent: this.state.sProcEditorContent,
|
||||
});
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
}, 100);
|
||||
|
||||
return createdResource;
|
||||
},
|
||||
(createError) => {
|
||||
this.props.scriptTabBaseInstance.isExecutionError(true);
|
||||
|
||||
return Promise.reject(createError);
|
||||
}
|
||||
)
|
||||
.finally(() => this.props.scriptTabBaseInstance.isExecuting(false));
|
||||
}
|
||||
|
||||
public onDelete(): Promise<unknown> {
|
||||
const isDeleted = false;
|
||||
const onDeletePromise = new Promise((resolve) => {
|
||||
resolve(isDeleted);
|
||||
});
|
||||
return onDeletePromise;
|
||||
}
|
||||
|
||||
public handleIdOnChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||
if (this.state.saveButton.visible) {
|
||||
this.setState({
|
||||
id: event.target.value,
|
||||
saveButton: {
|
||||
enabled: true,
|
||||
visible: this.props.scriptTabBaseInstance.isNew(),
|
||||
},
|
||||
discardButton: {
|
||||
enabled: true,
|
||||
visible: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
setTimeout(() => {
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
public onChangeContent(newConent: string): void {
|
||||
if (this.state.updateButton.visible) {
|
||||
this.setState({
|
||||
updateButton: {
|
||||
enabled: true,
|
||||
visible: true,
|
||||
},
|
||||
discardButton: {
|
||||
enabled: true,
|
||||
visible: true,
|
||||
},
|
||||
executeButton: {
|
||||
enabled: false,
|
||||
visible: true,
|
||||
},
|
||||
sProcEditorContent: newConent,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
saveButton: {
|
||||
enabled: false,
|
||||
visible: this.props.scriptTabBaseInstance.isNew(),
|
||||
},
|
||||
executeButton: {
|
||||
enabled: false,
|
||||
visible: true,
|
||||
},
|
||||
discardButton: {
|
||||
enabled: true,
|
||||
visible: true,
|
||||
},
|
||||
sProcEditorContent: newConent,
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
}, 100);
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<div className="tab-pane flexContainer stored-procedure-tab" role="tabpanel">
|
||||
<div className="storedTabForm flexContainer">
|
||||
<div className="formTitleFirst">Stored Procedure Id</div>
|
||||
<span className="formTitleTextbox">
|
||||
<input
|
||||
className="formTree"
|
||||
type="text"
|
||||
required
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||
aria-label="Stored procedure id"
|
||||
placeholder="Enter the new stored procedure id"
|
||||
size={40}
|
||||
value={this.state.id}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => this.handleIdOnChange(event)}
|
||||
/>
|
||||
</span>
|
||||
<div className="spUdfTriggerHeader">Stored Procedure Body</div>
|
||||
<EditorReact
|
||||
language={"javascript"}
|
||||
content={this.state.sProcEditorContent}
|
||||
isReadOnly={false}
|
||||
ariaLabel={"Stored procedure body"}
|
||||
lineNumbers={"on"}
|
||||
theme={"_theme"}
|
||||
onContentChanged={(newContent: string) => this.onChangeContent(newContent)}
|
||||
/>
|
||||
{this.state.hasResults && (
|
||||
<div className="results-container">
|
||||
<Pivot aria-label="Successful execution of stored procedure" style={{ height: "100%" }}>
|
||||
<PivotItem
|
||||
headerText="Result"
|
||||
headerButtonProps={{
|
||||
"data-order": 1,
|
||||
"data-title": "Result",
|
||||
}}
|
||||
style={{ height: "100%" }}
|
||||
>
|
||||
<EditorReact
|
||||
language={"javascript"}
|
||||
content={this.state.resultData}
|
||||
isReadOnly={true}
|
||||
ariaLabel={"Execute stored procedure result"}
|
||||
/>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText="console.log"
|
||||
headerButtonProps={{
|
||||
"data-order": 2,
|
||||
"data-title": "console.log",
|
||||
}}
|
||||
style={{ height: "100%" }}
|
||||
>
|
||||
<EditorReact
|
||||
language={"javascript"}
|
||||
content={this.state.logsData}
|
||||
isReadOnly={true}
|
||||
ariaLabel={"Execute stored procedure logs"}
|
||||
/>
|
||||
</PivotItem>
|
||||
</Pivot>
|
||||
</div>
|
||||
)}
|
||||
{this.state.hasErrors && (
|
||||
<div className="errors-container">
|
||||
<div className="errors-header">Errors:</div>
|
||||
<div className="errorContent">
|
||||
<span className="errorMessage">{this.state.error}</span>
|
||||
<span className="errorDetailsLink">
|
||||
<a
|
||||
aria-label="Error details link"
|
||||
onClick={() => this.onErrorDetailsClick()}
|
||||
onKeyPress={(event: React.KeyboardEvent<HTMLAnchorElement>) => this.onErrorDetailsKeyPress(event)}
|
||||
>
|
||||
More details
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -3,28 +3,32 @@ import React, { useEffect, useRef, useState } from "react";
|
|||
import loadingIcon from "../../../images/circular_loader_black_16x16.gif";
|
||||
import errorIcon from "../../../images/close-black.svg";
|
||||
import { useObservable } from "../../hooks/useObservable";
|
||||
import { useTabs } from "../../hooks/useTabs";
|
||||
import TabsBase from "./TabsBase";
|
||||
|
||||
type Tab = TabsBase | (TabsBase & { render: () => JSX.Element });
|
||||
|
||||
export const Tabs = ({ tabs, activeTab }: { tabs: readonly Tab[]; activeTab: Tab }): JSX.Element => (
|
||||
<div className="tabsManagerContainer">
|
||||
<div id="content" className="flexContainer hideOverflows">
|
||||
<div className="nav-tabs-margin">
|
||||
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
|
||||
{tabs.map((tab) => (
|
||||
<TabNav key={tab.tabId} tab={tab} active={activeTab === tab} />
|
||||
export const Tabs = (): JSX.Element => {
|
||||
const { openedTabs, activeTab } = useTabs();
|
||||
return (
|
||||
<div className="tabsManagerContainer">
|
||||
<div id="content" className="flexContainer hideOverflows">
|
||||
<div className="nav-tabs-margin">
|
||||
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
|
||||
{openedTabs.map((tab) => (
|
||||
<TabNav key={tab.tabId} tab={tab} active={activeTab === tab} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="tabPanesContainer">
|
||||
{openedTabs.map((tab) => (
|
||||
<TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="tabPanesContainer">
|
||||
{tabs.map((tab) => (
|
||||
<TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
|
||||
const [hovering, setHovering] = useState(false);
|
||||
|
|
|
@ -4,15 +4,14 @@ import * as ThemeUtility from "../../Common/ThemeUtility";
|
|||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { useNotificationConsole } from "../../hooks/useNotificationConsole";
|
||||
import { RouteHandler } from "../../RouteHandlers/RouteHandler";
|
||||
import { useTabs } from "../../hooks/useTabs";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../Explorer";
|
||||
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { useSelectedNode } from "../useSelectedNode";
|
||||
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
|
||||
import { TabsManager } from "./TabsManager";
|
||||
|
||||
// TODO: Use specific actions for logging telemetry data
|
||||
export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
private static id = 0;
|
||||
|
@ -26,11 +25,9 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
|||
public tabKind: ViewModels.CollectionTabKind;
|
||||
public tabTitle: ko.Observable<string>;
|
||||
public tabPath: ko.Observable<string>;
|
||||
public hashLocation: ko.Observable<string>;
|
||||
public isExecutionError = ko.observable(false);
|
||||
public isExecuting = ko.observable(false);
|
||||
public pendingNotification?: ko.Observable<DataModels.Notification>;
|
||||
public manager?: TabsManager;
|
||||
protected _theme: string;
|
||||
public onLoadStartKey: number;
|
||||
|
||||
|
@ -50,8 +47,6 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
|||
ko.observable<string>(`${this.collection.databaseId}>${this.collection.id()}>${this.tabTitle()}`));
|
||||
this.pendingNotification = ko.observable<DataModels.Notification>(undefined);
|
||||
this.onLoadStartKey = options.onLoadStartKey;
|
||||
this.hashLocation = ko.observable<string>(options.hashLocation || "");
|
||||
this.hashLocation.subscribe((newLocation: string) => this.updateGlobalHash(newLocation));
|
||||
this.closeTabButton = {
|
||||
enabled: ko.computed<boolean>(() => {
|
||||
return true;
|
||||
|
@ -64,7 +59,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
|||
}
|
||||
|
||||
public onCloseTabButtonClick(): void {
|
||||
this.manager?.closeTab(this);
|
||||
useTabs.getState().closeTab(this);
|
||||
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {
|
||||
tabName: this.constructor.name,
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
|
@ -74,17 +69,18 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
|||
}
|
||||
|
||||
public onTabClick(): void {
|
||||
this.manager?.activateTab(this);
|
||||
useTabs.getState().activateTab(this);
|
||||
}
|
||||
|
||||
protected updateSelectedNode(): void {
|
||||
const relatedDatabase = (this.collection && this.collection.getDatabase()) || this.database;
|
||||
const setSelectedNode = useSelectedNode.getState().setSelectedNode;
|
||||
if (relatedDatabase && !relatedDatabase.isDatabaseExpanded()) {
|
||||
this.getContainer().selectedNode(relatedDatabase);
|
||||
setSelectedNode(relatedDatabase);
|
||||
} else if (this.collection && !this.collection.isCollectionExpanded()) {
|
||||
this.getContainer().selectedNode(this.collection);
|
||||
setSelectedNode(this.collection);
|
||||
} else {
|
||||
this.getContainer().selectedNode(this.node);
|
||||
setSelectedNode(this.node);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,14 +104,13 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
|||
|
||||
/** @deprecated this is no longer observable, bind to comparisons with manager.activeTab() instead */
|
||||
public isActive() {
|
||||
return this === this.manager?.activeTab();
|
||||
return this === useTabs.getState().activeTab;
|
||||
}
|
||||
|
||||
public onActivate(): void {
|
||||
this.updateSelectedNode();
|
||||
this.collection?.selectedSubnodeKind(this.tabKind);
|
||||
this.database?.selectedSubnodeKind(this.tabKind);
|
||||
this.updateGlobalHash(this.hashLocation());
|
||||
this.updateNavbarWithTabsButtons();
|
||||
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Open, {
|
||||
tabName: this.constructor.name,
|
||||
|
@ -149,14 +144,10 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
|||
}
|
||||
|
||||
/** Renders a Javascript object to be displayed inside Monaco Editor */
|
||||
protected renderObjectForEditor(value: any, replacer: any, space: string | number): string {
|
||||
public renderObjectForEditor(value: any, replacer: any, space: string | number): string {
|
||||
return JSON.stringify(value, replacer, space);
|
||||
}
|
||||
|
||||
private updateGlobalHash(newHash: string): void {
|
||||
RouteHandler.getInstance().updateRouteHashLocation(newHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return buttons that are displayed in the navbar
|
||||
*/
|
||||
|
@ -164,7 +155,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
|||
return [];
|
||||
}
|
||||
|
||||
protected updateNavbarWithTabsButtons = (): void => {
|
||||
public updateNavbarWithTabsButtons = (): void => {
|
||||
if (this.isActive()) {
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
}
|
||||
|
|
|
@ -1,131 +0,0 @@
|
|||
import * as ko from "knockout";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { updateUserContext } from "../../UserContext";
|
||||
import Explorer from "../Explorer";
|
||||
import DocumentId from "../Tree/DocumentId";
|
||||
import DocumentsTab from "./DocumentsTab";
|
||||
import QueryTab from "./QueryTab";
|
||||
import { TabsManager } from "./TabsManager";
|
||||
|
||||
describe("Tabs manager tests", () => {
|
||||
let tabsManager: TabsManager;
|
||||
let explorer: Explorer;
|
||||
let database: ViewModels.Database;
|
||||
let collection: ViewModels.Collection;
|
||||
let queryTab: QueryTab;
|
||||
let documentsTab: DocumentsTab;
|
||||
|
||||
beforeAll(() => {
|
||||
explorer = new Explorer();
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
id: "test",
|
||||
name: "test",
|
||||
location: "",
|
||||
type: "",
|
||||
kind: "",
|
||||
properties: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
database = {
|
||||
container: explorer,
|
||||
id: ko.observable<string>("test"),
|
||||
isDatabaseShared: () => false,
|
||||
} as ViewModels.Database;
|
||||
database.isDatabaseExpanded = ko.observable<boolean>(true);
|
||||
database.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
|
||||
|
||||
collection = {
|
||||
container: explorer,
|
||||
databaseId: "test",
|
||||
id: ko.observable<string>("test"),
|
||||
} as ViewModels.Collection;
|
||||
collection.getDatabase = (): ViewModels.Database => database;
|
||||
collection.isCollectionExpanded = ko.observable<boolean>(true);
|
||||
collection.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
|
||||
|
||||
queryTab = new QueryTab({
|
||||
tabKind: ViewModels.CollectionTabKind.Query,
|
||||
collection,
|
||||
database,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
});
|
||||
|
||||
documentsTab = new DocumentsTab({
|
||||
partitionKey: undefined,
|
||||
documentIds: ko.observableArray<DocumentId>(),
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
collection,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
});
|
||||
|
||||
// make sure tabs have different tabId
|
||||
queryTab.tabId = "1";
|
||||
documentsTab.tabId = "2";
|
||||
});
|
||||
|
||||
beforeEach(() => (tabsManager = new TabsManager()));
|
||||
|
||||
it("open new tabs", () => {
|
||||
tabsManager.activateNewTab(queryTab);
|
||||
expect(tabsManager.openedTabs().length).toBe(1);
|
||||
expect(tabsManager.openedTabs()[0]).toEqual(queryTab);
|
||||
expect(tabsManager.activeTab()).toEqual(queryTab);
|
||||
expect(queryTab.isActive()).toBe(true);
|
||||
|
||||
tabsManager.activateNewTab(documentsTab);
|
||||
expect(tabsManager.openedTabs().length).toBe(2);
|
||||
expect(tabsManager.openedTabs()[1]).toEqual(documentsTab);
|
||||
expect(tabsManager.activeTab()).toEqual(documentsTab);
|
||||
expect(queryTab.isActive()).toBe(false);
|
||||
expect(documentsTab.isActive()).toBe(true);
|
||||
});
|
||||
|
||||
it("open existing tabs", () => {
|
||||
tabsManager.activateNewTab(queryTab);
|
||||
tabsManager.activateNewTab(documentsTab);
|
||||
tabsManager.activateTab(queryTab);
|
||||
expect(tabsManager.openedTabs().length).toBe(2);
|
||||
expect(tabsManager.activeTab()).toEqual(queryTab);
|
||||
expect(queryTab.isActive()).toBe(true);
|
||||
expect(documentsTab.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it("get tabs", () => {
|
||||
tabsManager.activateNewTab(queryTab);
|
||||
tabsManager.activateNewTab(documentsTab);
|
||||
|
||||
const queryTabs = tabsManager.getTabs(ViewModels.CollectionTabKind.Query);
|
||||
expect(queryTabs.length).toBe(1);
|
||||
expect(queryTabs[0]).toEqual(queryTab);
|
||||
|
||||
const documentsTabs = tabsManager.getTabs(
|
||||
ViewModels.CollectionTabKind.Documents,
|
||||
(tab) => tab.tabId === documentsTab.tabId
|
||||
);
|
||||
expect(documentsTabs.length).toBe(1);
|
||||
expect(documentsTabs[0]).toEqual(documentsTab);
|
||||
});
|
||||
|
||||
it("close tabs", () => {
|
||||
tabsManager.activateNewTab(queryTab);
|
||||
tabsManager.activateNewTab(documentsTab);
|
||||
|
||||
tabsManager.closeTab(documentsTab);
|
||||
expect(tabsManager.openedTabs().length).toBe(1);
|
||||
expect(tabsManager.openedTabs()[0]).toEqual(queryTab);
|
||||
expect(tabsManager.activeTab()).toEqual(queryTab);
|
||||
expect(queryTab.isActive()).toBe(true);
|
||||
expect(documentsTab.isActive()).toBe(false);
|
||||
|
||||
tabsManager.closeTabsByComparator((tab) => tab.tabId === queryTab.tabId);
|
||||
expect(tabsManager.openedTabs().length).toBe(0);
|
||||
expect(tabsManager.activeTab()).toEqual(undefined);
|
||||
expect(queryTab.isActive()).toBe(false);
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue