diff --git a/.eslintignore b/.eslintignore index a880fcc44..f4064b9e5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -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 \ No newline at end of file diff --git a/less/documentDB.less b/less/documentDB.less index 3188c7899..904eb947c 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -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; -} \ No newline at end of file +} +.spinner { + width: 100%; + position: absolute; + z-index: 1; + background: white; + height: 100%; +} diff --git a/less/forms.less b/less/forms.less index 71510b156..c36a75ee2 100644 --- a/less/forms.less +++ b/less/forms.less @@ -201,3 +201,11 @@ .migration:disabled { background-color: #ccc; } + +.trigger-field { + width: 40%; + margin-top: 10px +} +.trigger-form { + padding: 10px 30px 10px 30px; +} diff --git a/package-lock.json b/package-lock.json index 7ecf322c0..997269344 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index afaaed826..886b54c8f 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "@nteract/data-explorer": "8.0.3", "@nteract/directory-listing": "2.0.6", "@nteract/dropdown-menu": "1.0.1", - "@nteract/editor": "10.1.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", diff --git a/src/Common/QueriesClient.ts b/src/Common/QueriesClient.ts index dfb31a2f6..534d12879 100644 --- a/src/Common/QueriesClient.ts +++ b/src/Common/QueriesClient.ts @@ -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) { diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index 5f64cbe81..e83a4e9ec 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -120,6 +120,14 @@ export async function initializeConfiguration(): Promise { 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) { diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index c851ea8aa..210d34075 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -89,7 +89,6 @@ export interface Database extends TreeNode { selectedSubnodeKind: ko.Observable; - selectDatabase(): void; expandDatabase(): Promise; collapseDatabase(): void; @@ -275,7 +274,6 @@ export interface TabOptions { tabKind: CollectionTabKind; title: string; tabPath: string; - hashLocation: string; isTabsContentExpanded?: ko.Observable; onLoadStartKey?: number; diff --git a/src/Explorer/ContextMenuButtonFactory.ts b/src/Explorer/ContextMenuButtonFactory.ts deleted file mode 100644 index 42eac8d44..000000000 --- a/src/Explorer/ContextMenuButtonFactory.ts +++ /dev/null @@ -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", - }, - ]; - } -} diff --git a/src/Explorer/ContextMenuButtonFactory.tsx b/src/Explorer/ContextMenuButtonFactory.tsx new file mode 100644 index 000000000..ece39b07d --- /dev/null +++ b/src/Explorer/ContextMenuButtonFactory.tsx @@ -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(), + 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(), + 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", + }, + ]; +}; diff --git a/src/Explorer/Controls/Editor/EditorReact.tsx b/src/Explorer/Controls/Editor/EditorReact.tsx index c4eaabb80..111db73ee 100644 --- a/src/Explorer/Controls/Editor/EditorReact.tsx +++ b/src/Explorer/Controls/Editor/EditorReact.tsx @@ -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 { +export class EditorReact extends React.Component { 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 { } public render(): JSX.Element { - return
this.setRef(elt)} />; + return ( + + {!this.state.showEditor && } +
this.setRef(elt)} /> + + ); } protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) { @@ -83,6 +96,12 @@ export class EditorReact extends React.Component { 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 { diff --git a/src/Explorer/Controls/GitHub/GitHubReposComponentAdapter.tsx b/src/Explorer/Controls/GitHub/GitHubReposComponentAdapter.tsx deleted file mode 100644 index aa8cabde5..000000000 --- a/src/Explorer/Controls/GitHub/GitHubReposComponentAdapter.tsx +++ /dev/null @@ -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; - - constructor(private props: GitHubReposComponentProps) { - this.parameters = ko.observable(Date.now()); - } - - public renderComponent(): JSX.Element { - return ; - } - - public triggerRender(): void { - window.requestAnimationFrame(() => this.parameters(Date.now())); - } -} diff --git a/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx b/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx index 805317627..3bbc6a538 100644 --- a/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx +++ b/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx @@ -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 }); diff --git a/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx b/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx index a32c9bcd3..3c3ef24b7 100644 --- a/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx +++ b/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx @@ -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 { + onRender: (query: Query) => { const buttonProps: IButtonProps = { iconProps: { iconName: "More", @@ -214,19 +214,15 @@ export class QueriesGridComponent extends React.Component | React.KeyboardEvent, menuItem: any) => { + onClick: () => { this.props.onQuerySelect(query); }, }, { key: "Delete", text: "Delete query", - onClick: async ( - event: React.MouseEvent | React.KeyboardEvent, - 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, diff --git a/src/Explorer/Controls/Settings/SettingsComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsComponent.test.tsx index e4cee955c..d14fe1cc4 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.test.tsx @@ -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, diff --git a/src/Explorer/Controls/Settings/SettingsComponentAdapter.tsx b/src/Explorer/Controls/Settings/SettingsComponentAdapter.tsx deleted file mode 100644 index 6fd2cff07..000000000 --- a/src/Explorer/Controls/Settings/SettingsComponentAdapter.tsx +++ /dev/null @@ -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; - - constructor(private props: SettingsComponentProps) {} - - public renderComponent(): JSX.Element { - return this.parameters() ? : <>; - } -} diff --git a/src/Explorer/Controls/Settings/SettingsUtils.test.tsx b/src/Explorer/Controls/Settings/SettingsUtils.test.tsx index 81c025c1c..1a7e4d79b 100644 --- a/src/Explorer/Controls/Settings/SettingsUtils.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsUtils.test.tsx @@ -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, diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index 8e49f2ccf..cb8ecbfaf 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -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], diff --git a/src/Explorer/Controls/Tabs/TabComponent.tsx b/src/Explorer/Controls/Tabs/TabComponent.tsx index f1fd76bf8..7936321d0 100644 --- a/src/Explorer/Controls/Tabs/TabComponent.tsx +++ b/src/Explorer/Controls/Tabs/TabComponent.tsx @@ -58,7 +58,7 @@ export class TabComponent extends React.Component { as="span" className={className} role="presentation" - onActivated={(e) => this.setActiveTab(index)} + onActivated={() => this.setActiveTab(index)} aria-label={`Select tab: ${tab.title}`} > {tab.title} diff --git a/src/Explorer/Controls/TreeComponent/TreeComponent.tsx b/src/Explorer/Controls/TreeComponent/TreeComponent.tsx index dd7127daa..6c5ceb817 100644 --- a/src/Explorer/Controls/TreeComponent/TreeComponent.tsx +++ b/src/Explorer/Controls/TreeComponent/TreeComponent.tsx @@ -58,7 +58,7 @@ export interface TreeComponentProps { export class TreeComponent extends React.Component { public render(): JSX.Element { return ( -
+
); @@ -172,6 +172,7 @@ export class TreeNodeComponent extends React.Component) => this.onNodeClick(event, node)} onKeyPress={(event: React.KeyboardEvent) => this.onNodeKeyPress(event, node)} + role="treeitem" >
{ - const createExplorerStub = (database: ViewModels.Database): Explorer => { - const explorerStub = {} as Explorer; - explorerStub.databases = ko.observableArray([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); }); diff --git a/src/Explorer/DataSamples/ContainerSampleGenerator.ts b/src/Explorer/DataSamples/ContainerSampleGenerator.ts index dd9a4adb9..44906151d 100644 --- a/src/Explorer/DataSamples/ContainerSampleGenerator.ts +++ b/src/Explorer/DataSamples/ContainerSampleGenerator.ts @@ -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; } diff --git a/src/Explorer/DataSamples/DataSamplesUtil.test.ts b/src/Explorer/DataSamples/DataSamplesUtil.test.ts index 9a35158f5..f8ff6f8e5 100644 --- a/src/Explorer/DataSamples/DataSamplesUtil.test.ts +++ b/src/Explorer/DataSamples/DataSamplesUtil.test.ts @@ -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]), } as Database; const explorer = {} as Explorer; - explorer.databases = ko.observableArray([database]); explorer.showOkModalDialog = () => {}; + useDatabases.getState().addDatabases([database]); const dataSamplesUtil = new DataSamplesUtil(explorer); const fakeGenerator = sinon.createStubInstance(ContainerSampleGenerator as any); diff --git a/src/Explorer/DataSamples/DataSamplesUtil.ts b/src/Explorer/DataSamples/DataSamplesUtil.ts index 63b35cfff..4007608c0 100644 --- a/src/Explorer/DataSamples/DataSamplesUtil.ts +++ b/src/Explorer/DataSamples/DataSamplesUtil.ts @@ -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); diff --git a/src/Explorer/Explorer.test.tsx b/src/Explorer/Explorer.test.tsx deleted file mode 100644 index ef595252a..000000000 --- a/src/Explorer/Explorer.test.tsx +++ /dev/null @@ -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([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([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([database]); - expect(explorer.isLastNonEmptyDatabase()).toBe(false); - }); - - it("should be true if last non empty database", () => { - const database = {} as ViewModels.Database; - database.collections = ko.observableArray([{} as ViewModels.Collection]); - explorer.databases = ko.observableArray([database]); - expect(explorer.isLastNonEmptyDatabase()).toBe(true); - }); -}); diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 73c37510a..008651899 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -1,6 +1,5 @@ import { IChoiceGroupProps } from "@fluentui/react"; import * as ko from "knockout"; -import Q from "q"; import React from "react"; import _ from "underscore"; import { AuthType } from "../AuthType"; @@ -12,13 +11,13 @@ import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility" import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils"; import * as Logger from "../Common/Logger"; import { QueriesClient } from "../Common/QueriesClient"; -import { configContext, Platform } from "../ConfigContext"; +import { configContext } from "../ConfigContext"; import * as DataModels from "../Contracts/DataModels"; import * as ViewModels from "../Contracts/ViewModels"; import { GitHubOAuthService } from "../GitHub/GitHubOAuthService"; import { useSidePanel } from "../hooks/useSidePanel"; +import { useTabs } from "../hooks/useTabs"; import { IGalleryItem, JunoClient } from "../Juno/JunoClient"; -import { RouteHandler } from "../RouteHandlers/RouteHandler"; import { ExplorerSettings } from "../Shared/ExplorerSettings"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; @@ -31,10 +30,8 @@ import { listConnectionInfo, start, } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces"; -import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; import { stringToBlob } from "../Utils/BlobUtils"; import { isCapabilityEnabled } from "../Utils/CapabilityUtils"; -import { isRunningOnNationalCloud } from "../Utils/CloudUtils"; import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; @@ -48,85 +45,54 @@ import { NotebookContentItem, NotebookContentItemType } from "./Notebook/Noteboo import type NotebookManager from "./Notebook/NotebookManager"; import type { NotebookPaneContent } from "./Notebook/NotebookManager"; import { NotebookUtil } from "./Notebook/NotebookUtil"; +import { useNotebook } from "./Notebook/useNotebook"; import { AddCollectionPanel } from "./Panes/AddCollectionPanel"; import { AddDatabasePanel } from "./Panes/AddDatabasePanel/AddDatabasePanel"; import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane/BrowseQueriesPane"; import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane"; -import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane"; -import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel"; import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane"; import { GitHubReposPanel } from "./Panes/GitHubReposPanel/GitHubReposPanel"; import { SaveQueryPane } from "./Panes/SaveQueryPane/SaveQueryPane"; import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksPanel"; import { StringInputPane } from "./Panes/StringInputPane/StringInputPane"; -import { AddTableEntityPanel } from "./Panes/Tables/AddTableEntityPanel"; -import { EditTableEntityPanel } from "./Panes/Tables/EditTableEntityPanel"; -import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel/TableQuerySelectPanel"; import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane"; import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane"; -import TableListViewModal from "./Tables/DataTable/TableEntityListViewModel"; -import QueryViewModel from "./Tables/QueryBuilder/QueryViewModel"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; -import QueryTablesTab from "./Tabs/QueryTablesTab"; -import { TabsManager } from "./Tabs/TabsManager"; +import TabsBase from "./Tabs/TabsBase"; import TerminalTab from "./Tabs/TerminalTab"; import Database from "./Tree/Database"; import ResourceTokenCollection from "./Tree/ResourceTokenCollection"; import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter"; import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken"; import StoredProcedure from "./Tree/StoredProcedure"; +import { useDatabases } from "./useDatabases"; +import { useSelectedNode } from "./useSelectedNode"; BindingHandlersRegisterer.registerBindingHandlers(); // Hold a reference to ComponentRegisterer to prevent transpiler to ignore import var tmp = ComponentRegisterer; -export interface ExplorerParams { - tabsManager: TabsManager; -} - export default class Explorer { public isFixedCollectionWithSharedThroughputSupported: ko.Computed; - public isAccountReady: ko.Observable; - public canSaveQueries: ko.Computed; public queriesClient: QueriesClient; public tableDataClient: TableDataClient; // Resource Tree - public databases: ko.ObservableArray; - public selectedDatabaseId: ko.Computed; - public selectedCollectionId: ko.Computed; - public selectedNode: ko.Observable; private resourceTree: ResourceTreeAdapter; // Resource Token - public resourceTokenCollection: ko.Observable; - public isResourceTokenCollectionNodeSelected: ko.Computed; public resourceTreeForResourceToken: ResourceTreeAdapterForResourceToken; // Tabs public isTabsContentExpanded: ko.Observable; - public tabsManager: TabsManager; public gitHubOAuthService: GitHubOAuthService; - // features - public isHostedDataExplorerEnabled: ko.Computed; - public isSchemaEnabled: ko.Computed; - // Notebooks - public isNotebookEnabled: ko.Observable; - public isNotebooksEnabledForAccount: ko.Observable; - public notebookServerInfo: ko.Observable; - public sparkClusterConnectionInfo: ko.Observable; - public isSynapseLinkUpdating: ko.Observable; - public memoryUsageInfo: ko.Observable; public notebookManager?: NotebookManager; - public isShellEnabled: ko.Observable; - private _isInitializingNotebooks: boolean; - private notebookBasePath: ko.Observable; private notebookToImport: { name: string; content: string; @@ -134,82 +100,22 @@ export default class Explorer { private static readonly MaxNbDatabasesToAutoExpand = 5; - constructor(params?: ExplorerParams) { + constructor() { const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { dataExplorerArea: Constants.Areas.ResourceTree, }); - this.isAccountReady = ko.observable(false); this._isInitializingNotebooks = false; - this.isShellEnabled = ko.observable(false); - this.isNotebooksEnabledForAccount = ko.observable(false); - this.isNotebooksEnabledForAccount.subscribe((isEnabledForAccount: boolean) => this.refreshCommandBarButtons()); - this.isSynapseLinkUpdating = ko.observable(false); - this.isAccountReady.subscribe(async (isAccountReady: boolean) => { - if (isAccountReady) { - userContext.authType === AuthType.ResourceToken - ? this.refreshDatabaseForResourceToken() - : this.refreshAllDatabases(true); - RouteHandler.getInstance().initHandler(); - await this._refreshNotebooksEnabledStateForAccount(); - this.isNotebookEnabled( - userContext.authType !== AuthType.ResourceToken && - ((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) || - userContext.features.enableNotebooks) - ); - - this.isShellEnabled(this.isNotebookEnabled() && isPublicInternetAccessAllowed()); - - TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, { - isNotebookEnabled: this.isNotebookEnabled(), - dataExplorerArea: Constants.Areas.Notebook, - }); - - if (this.isNotebookEnabled()) { - await this.initNotebooks(userContext.databaseAccount); - } else if (this.notebookToImport) { - // if notebooks is not enabled but the user is trying to do a quickstart setup with notebooks, open the SetupNotebooksPane - this._openSetupNotebooksPaneForQuickstart(); - } - } - }); - this.memoryUsageInfo = ko.observable(); + useNotebook.subscribe( + () => this.refreshCommandBarButtons(), + (state) => state.isNotebooksEnabledForAccount + ); this.queriesClient = new QueriesClient(this); - this.resourceTokenCollection = ko.observable(); - this.isSchemaEnabled = ko.computed(() => userContext.features.enableSchema); - this.databases = ko.observableArray(); - this.canSaveQueries = ko.computed(() => { - const savedQueriesDatabase: ViewModels.Database = _.find( - this.databases(), - (database: ViewModels.Database) => database.id() === Constants.SavedQueries.DatabaseName - ); - if (!savedQueriesDatabase) { - return false; - } - const savedQueriesCollection: ViewModels.Collection = - savedQueriesDatabase && - _.find( - savedQueriesDatabase.collections(), - (collection: ViewModels.Collection) => collection.id() === Constants.SavedQueries.CollectionName - ); - if (!savedQueriesCollection) { - return false; - } - return true; - }); - this.selectedNode = ko.observable(); - this.selectedNode.subscribe((nodeSelected: ViewModels.TreeNode) => { + useSelectedNode.subscribe(() => { // Make sure switching tabs restores tabs display this.isTabsContentExpanded(false); }); - this.isResourceTokenCollectionNodeSelected = ko.computed(() => { - return ( - this.selectedNode() && - this.resourceTokenCollection() && - this.selectedNode().id() === this.resourceTokenCollection().id() - ); - }); this.isFixedCollectionWithSharedThroughputSupported = ko.computed(() => { if (userContext.features.enableFixedCollectionWithSharedThroughput) { @@ -223,38 +129,15 @@ export default class Explorer { return isCapabilityEnabled("EnableMongo"); }); - this.isHostedDataExplorerEnabled = ko.computed( - () => - configContext.platform === Platform.Portal && !isRunningOnNationalCloud() && userContext.apiType !== "Gremlin" + useTabs.subscribe( + (openedTabs: TabsBase[]) => { + if (openedTabs.length === 0) { + useSelectedNode.getState().setSelectedNode(undefined); + useCommandBar.getState().setContextButtons([]); + } + }, + (state) => state.openedTabs ); - this.selectedDatabaseId = ko.computed(() => { - const selectedNode = this.selectedNode(); - if (!selectedNode) { - return ""; - } - - switch (selectedNode.nodeKind) { - case "Collection": - return (selectedNode as ViewModels.CollectionBase).databaseId || ""; - case "Database": - return selectedNode.id() || ""; - case "DocumentId": - case "StoredProcedure": - case "Trigger": - case "UserDefinedFunction": - return selectedNode.collection.databaseId || ""; - default: - return ""; - } - }); - - this.tabsManager = params?.tabsManager ?? new TabsManager(); - this.tabsManager.openedTabs.subscribe((tabs) => { - if (tabs.length === 0) { - this.selectedNode(undefined); - useCommandBar.getState().setContextButtons([]); - } - }); this.isTabsContentExpanded = ko.observable(false); @@ -287,53 +170,44 @@ export default class Explorer { startKey ); - this.isNotebookEnabled = ko.observable(false); - this.isNotebookEnabled.subscribe(async () => { - if (!this.notebookManager) { - const NotebookManager = await ( - await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager") - ).default; - this.notebookManager = new NotebookManager(); - this.notebookManager.initialize({ - container: this, - notebookBasePath: this.notebookBasePath, - resourceTree: this.resourceTree, - refreshCommandBarButtons: () => this.refreshCommandBarButtons(), - refreshNotebookList: () => this.refreshNotebookList(), - }); - } + useNotebook.subscribe( + async () => { + if (!this.notebookManager) { + const NotebookManager = await ( + await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager") + ).default; + this.notebookManager = new NotebookManager(); + this.notebookManager.initialize({ + container: this, + resourceTree: this.resourceTree, + refreshCommandBarButtons: () => this.refreshCommandBarButtons(), + refreshNotebookList: () => this.refreshNotebookList(), + }); + } - this.refreshCommandBarButtons(); - this.refreshNotebookList(); - }); + this.refreshCommandBarButtons(); + this.refreshNotebookList(); + }, + (state) => state.isNotebookEnabled + ); this.resourceTree = new ResourceTreeAdapter(this); this.resourceTreeForResourceToken = new ResourceTreeAdapterForResourceToken(this); - this.notebookServerInfo = ko.observable({ - notebookServerEndpoint: undefined, - authToken: undefined, - }); - this.notebookBasePath = ko.observable(Constants.Notebook.defaultBasePath); - this.sparkClusterConnectionInfo = ko.observable({ - userName: undefined, - password: undefined, - endpoints: [], - }); // Override notebook server parameters from URL parameters if (userContext.features.notebookServerUrl && userContext.features.notebookServerToken) { - this.notebookServerInfo({ + useNotebook.getState().setNotebookServerInfo({ notebookServerEndpoint: userContext.features.notebookServerUrl, authToken: userContext.features.notebookServerToken, }); } if (userContext.features.notebookBasePath) { - this.notebookBasePath(userContext.features.notebookBasePath); + useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath); } if (userContext.features.livyEndpoint) { - this.sparkClusterConnectionInfo({ + useNotebook.getState().setSparkClusterConnectionInfo({ userName: undefined, password: undefined, endpoints: [ @@ -348,7 +222,8 @@ export default class Explorer { if (configContext.enableSchemaAnalyzer) { userContext.features.enableSchemaAnalyzer = true; } - this.isAccountReady(true); + + this.refreshExplorer(); } public openEnableSynapseLinkDialog(): void { @@ -369,7 +244,7 @@ export default class Explorer { const clearInProgressMessage = logConsoleProgress( "Enabling Azure Synapse Link for this account. This may take a few minutes before you can enable analytical store for this account." ); - this.isSynapseLinkUpdating(true); + useNotebook.getState().setIsSynapseLinkUpdating(true); useDialog.getState().closeDialog(); try { @@ -388,7 +263,7 @@ export default class Explorer { logConsoleError(`Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`); TelemetryProcessor.traceFailure(Action.EnableAzureSynapseLink, {}, startTime); } finally { - this.isSynapseLinkUpdating(false); + useNotebook.getState().setIsSynapseLinkUpdating(false); } }, @@ -403,113 +278,56 @@ export default class Explorer { // TODO: return result } - public isDatabaseNodeOrNoneSelected(): boolean { - return this.isNoneSelected() || this.isDatabaseNodeSelected(); - } - - public isDatabaseNodeSelected(): boolean { - return (this.selectedNode() && this.selectedNode().nodeKind === "Database") || false; - } - - public isNodeKindSelected(nodeKind: string): boolean { - return (this.selectedNode() && this.selectedNode().nodeKind === nodeKind) || false; - } - - public isNoneSelected(): boolean { - return this.selectedNode() == null; - } - - public refreshDatabaseForResourceToken(): Promise { + public async refreshDatabaseForResourceToken(): Promise { const databaseId = userContext.parsedResourceToken?.databaseId; const collectionId = userContext.parsedResourceToken?.collectionId; if (!databaseId || !collectionId) { - return Promise.reject(); + return; } - return readCollection(databaseId, collectionId).then((collection: DataModels.Collection) => { - this.resourceTokenCollection(new ResourceTokenCollection(this, databaseId, collection)); - this.selectedNode(this.resourceTokenCollection()); - }); + const collection: DataModels.Collection = await readCollection(databaseId, collectionId); + const resourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection); + useDatabases.setState({ resourceTokenCollection }); + useSelectedNode.getState().setSelectedNode(resourceTokenCollection); } - public refreshAllDatabases(isInitialLoad?: boolean): Q.Promise { + public async refreshAllDatabases(): Promise { const startKey: number = TelemetryProcessor.traceStart(Action.LoadDatabases, { dataExplorerArea: Constants.Areas.ResourceTree, }); - let resourceTreeStartKey: number = null; - if (isInitialLoad) { - resourceTreeStartKey = TelemetryProcessor.traceStart(Action.LoadResourceTree, { - dataExplorerArea: Constants.Areas.ResourceTree, - }); + + try { + const databases: DataModels.Database[] = await readDatabases(); + TelemetryProcessor.traceSuccess( + Action.LoadDatabases, + { + dataExplorerArea: Constants.Areas.ResourceTree, + }, + startKey + ); + const currentDatabases = useDatabases.getState().databases; + const deltaDatabases = this.getDeltaDatabases(databases, currentDatabases); + let updatedDatabases = currentDatabases.filter( + (database) => !deltaDatabases.toDelete.some((deletedDatabase) => deletedDatabase.id() === database.id()) + ); + updatedDatabases = [...updatedDatabases, ...deltaDatabases.toAdd].sort((db1, db2) => + db1.id().localeCompare(db2.id()) + ); + useDatabases.setState({ databases: updatedDatabases }); + await this.refreshAndExpandNewDatabases(deltaDatabases.toAdd, currentDatabases); + } catch (error) { + const errorMessage = getErrorMessage(error); + TelemetryProcessor.traceFailure( + Action.LoadDatabases, + { + dataExplorerArea: Constants.Areas.ResourceTree, + error: errorMessage, + errorStack: getErrorStack(error), + }, + startKey + ); + logConsoleError(`Error while refreshing databases: ${errorMessage}`); } - - // TODO: Refactor - const deferred: Q.Deferred = Q.defer(); - readDatabases().then( - (databases: DataModels.Database[]) => { - TelemetryProcessor.traceSuccess( - Action.LoadDatabases, - { - dataExplorerArea: Constants.Areas.ResourceTree, - }, - startKey - ); - const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode(); - const deltaDatabases = this.getDeltaDatabases(databases); - this.addDatabasesToList(deltaDatabases.toAdd); - this.deleteDatabasesFromList(deltaDatabases.toDelete); - this.selectedNode(currentlySelectedNode); - this.refreshAndExpandNewDatabases(deltaDatabases.toAdd).then( - () => { - deferred.resolve(); - }, - (reason) => { - deferred.reject(reason); - } - ); - }, - (error) => { - deferred.reject(error); - const errorMessage = getErrorMessage(error); - TelemetryProcessor.traceFailure( - Action.LoadDatabases, - { - dataExplorerArea: Constants.Areas.ResourceTree, - error: errorMessage, - errorStack: getErrorStack(error), - }, - startKey - ); - logConsoleError(`Error while refreshing databases: ${errorMessage}`); - } - ); - - return deferred.promise.then( - () => { - if (resourceTreeStartKey != null) { - TelemetryProcessor.traceSuccess( - Action.LoadResourceTree, - { - dataExplorerArea: Constants.Areas.ResourceTree, - }, - resourceTreeStartKey - ); - } - }, - (error) => { - if (resourceTreeStartKey != null) { - TelemetryProcessor.traceFailure( - Action.LoadResourceTree, - { - dataExplorerArea: Constants.Areas.ResourceTree, - error: getErrorMessage(error), - errorStack: getErrorStack(error), - }, - resourceTreeStartKey - ); - } - } - ); } public onRefreshDatabasesKeyPress = (source: any, event: KeyboardEvent): boolean => { @@ -554,18 +372,17 @@ export default class Explorer { "default" ); - this.notebookServerInfo({ + useNotebook.getState().setNotebookServerInfo({ notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint, authToken: userContext.features.notebookServerToken || connectionInfo.authToken, }); - this.notebookServerInfo.valueHasMutated(); this.refreshNotebookList(); this._isInitializingNotebooks = false; } public resetNotebookWorkspace() { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookClient) { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) { handleError( "Attempt to reset notebook workspace, but notebook is not enabled", "Explorer/resetNotebookWorkspace" @@ -651,140 +468,16 @@ export default class Explorer { } }; - public findSelectedDatabase(): ViewModels.Database { - if (!this.selectedNode()) { - return null; - } - if (this.selectedNode().nodeKind === "Database") { - return _.find(this.databases(), (database: ViewModels.Database) => database.id() === this.selectedNode().id()); - } - return this.findSelectedCollection().database; - } - - public findDatabaseWithId(databaseId: string): ViewModels.Database { - return _.find(this.databases(), (database: ViewModels.Database) => database.id() === databaseId); - } - - public isLastNonEmptyDatabase(): boolean { - if ( - this.isLastDatabase() && - this.databases()[0] && - this.databases()[0].collections && - this.databases()[0].collections().length > 0 - ) { - return true; - } - return false; - } - - public isLastDatabase(): boolean { - if (this.databases().length > 1) { - return false; - } - return true; - } - - public isSelectedDatabaseShared(): boolean { - const database = this.findSelectedDatabase(); - if (!!database) { - return database.offer && !!database.offer(); - } - - return false; - } - - public findSelectedCollection(): ViewModels.Collection { - return (this.selectedNode().nodeKind === "Collection" - ? this.selectedNode() - : this.selectedNode().collection) as ViewModels.Collection; - } - - private refreshAndExpandNewDatabases(newDatabases: ViewModels.Database[]): Q.Promise { - // we reload collections for all databases so the resource tree reflects any collection-level changes - // i.e addition of stored procedures, etc. - const deferred: Q.Deferred = Q.defer(); - let loadCollectionPromises: Q.Promise[] = []; - - // If the user has a lot of databases, only load expanded databases. - const databasesToLoad = - this.databases().length <= Explorer.MaxNbDatabasesToAutoExpand - ? this.databases() - : this.databases().filter((db) => db.isDatabaseExpanded() || db.id() === Constants.SavedQueries.DatabaseName); - - const startKey: number = TelemetryProcessor.traceStart(Action.LoadCollections, { - dataExplorerArea: Constants.Areas.ResourceTree, - }); - databasesToLoad.forEach(async (database: ViewModels.Database) => { - await database.loadCollections(); - const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.id() === database.id()); - if (isNewDatabase) { - database.expandDatabase(); - } - this.tabsManager.refreshActiveTab((tab) => tab.collection && tab.collection.getDatabase().id() === database.id()); - }); - - Q.all(loadCollectionPromises).done( - () => { - deferred.resolve(); - TelemetryProcessor.traceSuccess( - Action.LoadCollections, - { dataExplorerArea: Constants.Areas.ResourceTree }, - startKey - ); - }, - (error: any) => { - deferred.reject(error); - TelemetryProcessor.traceFailure( - Action.LoadCollections, - { - dataExplorerArea: Constants.Areas.ResourceTree, - error: getErrorMessage(error), - errorStack: getErrorStack(error), - }, - startKey - ); - } - ); - return deferred.promise; - } - - private _initSettings() { - if (!ExplorerSettings.hasSettingsDefined()) { - ExplorerSettings.createDefaultSettings(); - } - } - - public findCollection(databaseId: string, collectionId: string): ViewModels.Collection { - const database: ViewModels.Database = this.databases().find( - (database: ViewModels.Database) => database.id() === databaseId - ); - return database?.collections().find((collection: ViewModels.Collection) => collection.id() === collectionId); - } - - public isLastCollection(): boolean { - let collectionCount = 0; - if (this.databases().length == 0) { - return false; - } - for (let i = 0; i < this.databases().length; i++) { - const database = this.databases()[i]; - collectionCount += database.collections().length; - if (collectionCount > 1) { - return false; - } - } - return true; - } - private getDeltaDatabases( - updatedDatabaseList: DataModels.Database[] + updatedDatabaseList: DataModels.Database[], + databases: ViewModels.Database[] ): { toAdd: ViewModels.Database[]; toDelete: ViewModels.Database[]; } { const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => { const databaseExists = _.some( - this.databases(), + databases, (existingDatabase: ViewModels.Database) => existingDatabase.id() === database.id ); return !databaseExists; @@ -793,8 +486,8 @@ export default class Explorer { (newDatabase: DataModels.Database) => new Database(this, newDatabase) ); - let databasesToDelete: ViewModels.Database[] = []; - ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => { + const databasesToDelete: ViewModels.Database[] = []; + databases.forEach((database: ViewModels.Database) => { const databasePresentInUpdatedList = _.some( updatedDatabaseList, (db: DataModels.Database) => db.id === database.id() @@ -807,29 +500,62 @@ export default class Explorer { return { toAdd: databasesToAdd, toDelete: databasesToDelete }; } - private addDatabasesToList(databases: ViewModels.Database[]): void { - this.databases( - this.databases() - .concat(databases) - .sort((database1, database2) => database1.id().localeCompare(database2.id())) - ); - } + private async refreshAndExpandNewDatabases( + newDatabases: ViewModels.Database[], + databases: ViewModels.Database[] + ): Promise { + // we reload collections for all databases so the resource tree reflects any collection-level changes + // i.e addition of stored procedures, etc. - private deleteDatabasesFromList(databasesToRemove: ViewModels.Database[]): void { - const databasesToKeep: ViewModels.Database[] = []; + // If the user has a lot of databases, only load expanded databases. + const databasesToLoad = + databases.length <= Explorer.MaxNbDatabasesToAutoExpand + ? databases + : databases.filter((db) => db.isDatabaseExpanded() || db.id() === Constants.SavedQueries.DatabaseName); - ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => { - const shouldRemoveDatabase = _.some(databasesToRemove, (db: ViewModels.Database) => db.id === database.id); - if (!shouldRemoveDatabase) { - databasesToKeep.push(database); - } + const startKey: number = TelemetryProcessor.traceStart(Action.LoadCollections, { + dataExplorerArea: Constants.Areas.ResourceTree, }); - this.databases(databasesToKeep); + try { + await Promise.all( + databasesToLoad.map(async (database: ViewModels.Database) => { + await database.loadCollections(); + const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.id() === database.id()); + if (isNewDatabase) { + database.expandDatabase(); + } + useTabs + .getState() + .refreshActiveTab((tab) => tab.collection && tab.collection.getDatabase().id() === database.id()); + TelemetryProcessor.traceSuccess( + Action.LoadCollections, + { dataExplorerArea: Constants.Areas.ResourceTree }, + startKey + ); + }) + ); + } catch (error) { + TelemetryProcessor.traceFailure( + Action.LoadCollections, + { + dataExplorerArea: Constants.Areas.ResourceTree, + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }, + startKey + ); + } + } + + private _initSettings() { + if (!ExplorerSettings.hasSettingsDefined()) { + ExplorerSettings.createDefaultSettings(); + } } public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to upload notebook, but notebook is not enabled"; handleError(error, "Explorer/uploadFile"); throw new Error(error); @@ -847,7 +573,7 @@ export default class Explorer { const item = NotebookUtil.createNotebookContentItem(name, path, "file"); const parent = this.resourceTree.myNotebooksContentRoot; - if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) { + if (parent && parent.children && useNotebook.getState().isNotebookEnabled && this.notebookManager?.notebookClient) { const existingItem = _.find(parent.children, (node) => node.name === name); if (existingItem) { return this.openNotebook(existingItem); @@ -864,7 +590,7 @@ export default class Explorer { public async importAndOpenContent(name: string, content: string): Promise { const parent = this.resourceTree.myNotebooksContentRoot; - if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) { + if (parent && parent.children && useNotebook.getState().isNotebookEnabled && this.notebookManager?.notebookClient) { if (this.notebookToImport && this.notebookToImport.name === name && this.notebookToImport.content === content) { this.notebookToImport = undefined; // we don't want to try opening this notebook again } @@ -968,16 +694,18 @@ export default class Explorer { throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`); } - const notebookTabs = this.tabsManager.getTabs( - ViewModels.CollectionTabKind.NotebookV2, - (tab) => - (tab as NotebookV2Tab).notebookPath && - FileSystemUtil.isPathEqual((tab as NotebookV2Tab).notebookPath(), notebookContentItem.path) - ) as NotebookV2Tab[]; + const notebookTabs = useTabs + .getState() + .getTabs( + ViewModels.CollectionTabKind.NotebookV2, + (tab) => + (tab as NotebookV2Tab).notebookPath && + FileSystemUtil.isPathEqual((tab as NotebookV2Tab).notebookPath(), notebookContentItem.path) + ) as NotebookV2Tab[]; let notebookTab = notebookTabs && notebookTabs[0]; if (notebookTab) { - this.tabsManager.activateTab(notebookTab); + useTabs.getState().activateTab(notebookTab); } else { const options: NotebookTabOptions = { account: userContext.databaseAccount, @@ -987,7 +715,6 @@ export default class Explorer { tabPath: notebookContentItem.path, collection: null, masterKey: userContext.masterKey || "", - hashLocation: "notebooks", isTabsContentExpanded: ko.observable(true), onLoadStartKey: null, container: this, @@ -997,7 +724,7 @@ export default class Explorer { try { const NotebookTabV2 = await import(/* webpackChunkName: "NotebookV2Tab" */ "./Tabs/NotebookV2Tab"); notebookTab = new NotebookTabV2.default(options); - this.tabsManager.activateNewTab(notebookTab); + useTabs.getState().activateNewTab(notebookTab); } catch (reason) { console.error("Import NotebookV2Tab failed!", reason); return false; @@ -1008,26 +735,24 @@ export default class Explorer { } public renameNotebook(notebookFile: NotebookContentItem): void { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to rename notebook, but notebook is not enabled"; handleError(error, "Explorer/renameNotebook"); throw new Error(error); } // Don't delete if tab is open to avoid accidental deletion - const openedNotebookTabs = this.tabsManager.getTabs( - ViewModels.CollectionTabKind.NotebookV2, - (tab: NotebookV2Tab) => { + const openedNotebookTabs = useTabs + .getState() + .getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab: NotebookV2Tab) => { return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path); - } - ); + }); if (openedNotebookTabs.length > 0) { this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again."); } else { useSidePanel.getState().openSidePanel( "Rename Notebook", { useSidePanel.getState().closeSidePanel(); this.resourceTree.triggerRender(); @@ -1049,7 +774,7 @@ export default class Explorer { } public onCreateDirectory(parent: NotebookContentItem): void { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to create notebook directory, but notebook is not enabled"; handleError(error, "Explorer/onCreateDirectory"); throw new Error(error); @@ -1058,7 +783,6 @@ export default class Explorer { useSidePanel.getState().openSidePanel( "Create new directory", { useSidePanel.getState().closeSidePanel(); this.resourceTree.triggerRender(); @@ -1079,7 +803,7 @@ export default class Explorer { } public readFile(notebookFile: NotebookContentItem): Promise { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to read file, but notebook is not enabled"; handleError(error, "Explorer/downloadFile"); throw new Error(error); @@ -1089,7 +813,7 @@ export default class Explorer { } public downloadFile(notebookFile: NotebookContentItem): Promise { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to download file, but notebook is not enabled"; handleError(error, "Explorer/downloadFile"); throw new Error(error); @@ -1126,56 +850,8 @@ export default class Explorer { ); } - private async _refreshNotebooksEnabledStateForAccount(): Promise { - const { databaseAccount, authType } = userContext; - if ( - authType === AuthType.EncryptedToken || - authType === AuthType.ResourceToken || - authType === AuthType.MasterKey - ) { - this.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"); - this.isNotebooksEnabledForAccount(true); - return; - } - - // firstWriteLocation should not be disallowed - const isAccountInAllowedLocation = firstWriteLocation && disallowedLocations.indexOf(firstWriteLocation) === -1; - this.isNotebooksEnabledForAccount(isAccountInAllowedLocation); - } catch (error) { - Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount"); - this.isNotebooksEnabledForAccount(false); - } - } - private refreshNotebookList = async (): Promise => { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { return; } @@ -1187,19 +863,18 @@ export default class Explorer { }; public deleteNotebookFile(item: NotebookContentItem): Promise { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to delete notebook file, but notebook is not enabled"; handleError(error, "Explorer/deleteNotebookFile"); throw new Error(error); } // Don't delete if tab is open to avoid accidental deletion - const openedNotebookTabs = this.tabsManager.getTabs( - ViewModels.CollectionTabKind.NotebookV2, - (tab: NotebookV2Tab) => { + const openedNotebookTabs = useTabs + .getState() + .getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab: NotebookV2Tab) => { return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path); - } - ); + }); if (openedNotebookTabs.length > 0) { this.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again."); return Promise.reject(); @@ -1228,7 +903,7 @@ export default class Explorer { * This creates a new notebook file, then opens the notebook */ public onNewNotebookClicked(parent?: NotebookContentItem): void { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to create new notebook, but notebook is not enabled"; handleError(error, "Explorer/onNewNotebookClicked"); throw new Error(error); @@ -1272,7 +947,7 @@ export default class Explorer { } public refreshContentItem(item: NotebookContentItem): Promise { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to refresh notebook list, but notebook is not enabled"; handleError(error, "Explorer/refreshContentItem"); return Promise.reject(new Error(error)); @@ -1281,37 +956,29 @@ export default class Explorer { return this.notebookManager?.notebookContentClient.updateItemChildren(item); } - public getNotebookBasePath(): string { - return this.notebookBasePath(); - } - public openNotebookTerminal(kind: ViewModels.TerminalKind) { let title: string; - let hashLocation: string; switch (kind) { case ViewModels.TerminalKind.Default: title = "Terminal"; - hashLocation = "terminal"; break; case ViewModels.TerminalKind.Mongo: title = "Mongo Shell"; - hashLocation = "mongo-shell"; break; case ViewModels.TerminalKind.Cassandra: title = "Cassandra Shell"; - hashLocation = "cassandra-shell"; break; default: throw new Error("Terminal kind: ${kind} not supported"); } - const terminalTabs: TerminalTab[] = this.tabsManager.getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => - tab.hashLocation().startsWith(hashLocation) - ) as TerminalTab[]; + const terminalTabs: TerminalTab[] = useTabs + .getState() + .getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => tab.tabTitle() === title) as TerminalTab[]; let index = 1; if (terminalTabs.length > 0) { @@ -1325,7 +992,6 @@ export default class Explorer { title: `${title} ${index}`, tabPath: `${title} ${index}`, collection: null, - hashLocation: `${hashLocation} ${index}`, isTabsContentExpanded: ko.observable(true), onLoadStartKey: null, container: this, @@ -1333,7 +999,7 @@ export default class Explorer { index: index, }); - this.tabsManager.activateNewTab(newTab); + useTabs.getState().activateNewTab(newTab); } public async openGallery( @@ -1343,22 +1009,21 @@ export default class Explorer { isFavorite?: boolean ) { const title = "Gallery"; - const hashLocation = "gallery"; const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default; - const galleryTab = this.tabsManager + const galleryTab = useTabs + .getState() .getTabs(ViewModels.CollectionTabKind.Gallery) - .find((tab) => tab.hashLocation() == hashLocation); + .find((tab) => tab.tabTitle() == title); if (galleryTab instanceof GalleryTab) { - this.tabsManager.activateTab(galleryTab); + useTabs.getState().activateTab(galleryTab); } else { - this.tabsManager.activateNewTab( + useTabs.getState().activateNewTab( new GalleryTab( { tabKind: ViewModels.CollectionTabKind.Gallery, - title: title, + title, tabPath: title, - hashLocation: hashLocation, onLoadStartKey: null, isTabsContentExpanded: ko.observable(true), }, @@ -1376,7 +1041,7 @@ export default class Explorer { } } - public onNewCollectionClicked(databaseId?: string): void { + public async onNewCollectionClicked(databaseId?: string): Promise { if (userContext.apiType === "Cassandra") { useSidePanel .getState() @@ -1385,12 +1050,15 @@ export default class Explorer { ); } else { - this.openAddCollectionPanel(databaseId); + await useDatabases.getState().loadDatabaseOffers(); + useSidePanel + .getState() + .openSidePanel("New " + getCollectionName(), ); } } private refreshCommandBarButtons(): void { - const activeTab = this.tabsManager.activeTab(); + const activeTab = useTabs.getState().activeTab; if (activeTab) { activeTab.onActivate(); // TODO only update tabs buttons? } else { @@ -1407,7 +1075,7 @@ export default class Explorer { } public async handleOpenFileAction(path: string): Promise { - if (this.isAccountReady() && !(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) { + if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) { this._openSetupNotebooksPaneForQuickstart(); } @@ -1429,50 +1097,8 @@ export default class Explorer { } } - public async loadDatabaseOffers(): Promise { - await Promise.all( - this.databases()?.map(async (database: ViewModels.Database) => { - await database.loadOffer(); - }) - ); - } - - public isFirstResourceCreated(): boolean { - const databases: ViewModels.Database[] = this.databases(); - - if (!databases || databases.length === 0) { - return false; - } - - return databases.some((database) => { - // user has created at least one collection - if (database.collections()?.length > 0) { - return true; - } - // user has created a database with shared throughput - if (database.offer()) { - return true; - } - // use has created an empty database without shared throughput - return false; - }); - } - public openDeleteCollectionConfirmationPane(): void { - useSidePanel - .getState() - .openSidePanel("Delete " + getCollectionName(), ); - } - - public openDeleteDatabaseConfirmationPane(): void { - useSidePanel - .getState() - .openSidePanel( - "Delete " + getDatabaseName(), - - ); - } public openUploadItemsPanePane(): void { - useSidePanel.getState().openSidePanel("Upload " + getUploadName(), ); + useSidePanel.getState().openSidePanel("Upload " + getUploadName(), ); } public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void { useSidePanel @@ -1480,12 +1106,6 @@ export default class Explorer { .openSidePanel("Input parameters", ); } - public async openAddCollectionPanel(databaseId?: string): Promise { - await this.loadDatabaseOffers(); - useSidePanel - .getState() - .openSidePanel("New " + getCollectionName(), ); - } public openAddDatabasePane(): void { useSidePanel.getState().openSidePanel("New " + getDatabaseName(), ); } @@ -1521,40 +1141,34 @@ export default class Explorer { ); } - public openAddTableEntityPanel(queryTablesTab: QueryTablesTab, tableEntityListViewModel: TableListViewModal): void { - useSidePanel - .getState() - .openSidePanel( - "Add Table Entity", - - ); - } public openSetupNotebooksPanel(title: string, description: string): void { useSidePanel .getState() .openSidePanel(title, ); } - public openEditTableEntityPanel(queryTablesTab: QueryTablesTab, tableEntityListViewModel: TableListViewModal): void { - useSidePanel - .getState() - .openSidePanel( - "Edit Table Entity", - - ); - } + public async refreshExplorer(): Promise { + userContext.authType === AuthType.ResourceToken + ? this.refreshDatabaseForResourceToken() + : this.refreshAllDatabases(); + await useNotebook.getState().refreshNotebooksEnabledStateForAccount(); + const isNotebookEnabled: boolean = + userContext.authType !== AuthType.ResourceToken && + ((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) || + userContext.features.enableNotebooks); + useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled); + useNotebook.getState().setIsShellEnabled(isNotebookEnabled && isPublicInternetAccessAllowed()); - public openTableSelectQueryPanel(queryViewModal: QueryViewModel): void { - useSidePanel.getState().openSidePanel("Select Column", ); + TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, { + isNotebookEnabled, + dataExplorerArea: Constants.Areas.Notebook, + }); + + if (isNotebookEnabled) { + await this.initNotebooks(userContext.databaseAccount); + } else if (this.notebookToImport) { + // if notebooks is not enabled but the user is trying to do a quickstart setup with notebooks, open the SetupNotebooksPane + this._openSetupNotebooksPaneForQuickstart(); + } } } diff --git a/src/Explorer/Graph/GraphExplorerComponent/EditorNeighborsComponent.tsx b/src/Explorer/Graph/GraphExplorerComponent/EditorNeighborsComponent.tsx index 5b4470054..c77fc2852 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/EditorNeighborsComponent.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/EditorNeighborsComponent.tsx @@ -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 - Delete this.removeAddedEdgeToNeighbor(index)} /> + Delete this.removeAddedEdgeToNeighbor(index)} /> diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index efaa5a3dd..9755075a8 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -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 = create((set) => ({ })); export const CommandBar: React.FC = ({ 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 = ({ 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 ( diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts index b0e2feeec..a8dba6874 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts @@ -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); diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 6ff55ef28..52c70d99b 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -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", ), + onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", ), 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]; diff --git a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx index 2f17a1370..6403b4ed9 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx @@ -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 -): ICommandBarItemProps => { +export const createMemoryTracker = (key: string): ICommandBarItemProps => { return { key, - onRender: () => , + onRender: () => , }; }; diff --git a/src/Explorer/Menus/CommandBar/MemoryTrackerComponent.tsx b/src/Explorer/Menus/CommandBar/MemoryTrackerComponent.tsx index 4317fed4e..ac0621897 100644 --- a/src/Explorer/Menus/CommandBar/MemoryTrackerComponent.tsx +++ b/src/Explorer/Menus/CommandBar/MemoryTrackerComponent.tsx @@ -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; -} - -export class MemoryTrackerComponent extends React.Component { - 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 ( - - Memory - - - ); - } - - 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 ( Memory - 0.8 ? "lowMemory" : ""} - description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"} - percentComplete={usedGB / totalGB} - /> + ); } -} + + const totalGB = memoryUsageInfo.totalKB / 1048576; + const usedGB = totalGB - memoryUsageInfo.freeKB / 1048576; + + return ( + + Memory + 0.8 ? "lowMemory" : ""} + description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"} + percentComplete={usedGB / totalGB} + /> + + ); +}; diff --git a/src/Explorer/Notebook/NotebookClientV2.ts b/src/Explorer/Notebook/NotebookClientV2.ts index e7c1fa289..86318a166 100644 --- a/src/Explorer/Notebook/NotebookClientV2.ts +++ b/src/Explorer/Notebook/NotebookClientV2.ts @@ -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)); + } } /** diff --git a/src/Explorer/Notebook/NotebookComponent/NotebookComponent.less b/src/Explorer/Notebook/NotebookComponent/NotebookComponent.less index 36a819b88..1bd303e1d 100644 --- a/src/Explorer/Notebook/NotebookComponent/NotebookComponent.less +++ b/src/Explorer/Notebook/NotebookComponent/NotebookComponent.less @@ -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 { diff --git a/src/Explorer/Notebook/NotebookComponent/epics.ts b/src/Explorer/Notebook/NotebookComponent/epics.ts index 33eac5fac..718028dab 100644 --- a/src/Explorer/Notebook/NotebookComponent/epics.ts +++ b/src/Explorer/Notebook/NotebookComponent/epics.ts @@ -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); diff --git a/src/Explorer/Notebook/NotebookContainerClient.ts b/src/Explorer/Notebook/NotebookContainerClient.ts index 3764360ef..0e50106a7 100644 --- a/src/Explorer/Notebook/NotebookContainerClient.ts +++ b/src/Explorer/Notebook/NotebookContainerClient.ts @@ -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, - 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 { - 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 { - 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, }; } diff --git a/src/Explorer/Notebook/NotebookContentClient.ts b/src/Explorer/Notebook/NotebookContentClient.ts index e7f14b112..a4a9958d0 100644 --- a/src/Explorer/Notebook/NotebookContentClient.ts +++ b/src/Explorer/Notebook/NotebookContentClient.ts @@ -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, - public notebookBasePath: ko.Observable, - 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, }; } diff --git a/src/Explorer/Notebook/NotebookManager.tsx b/src/Explorer/Notebook/NotebookManager.tsx index d2fb3bcb0..9aca162f1 100644 --- a/src/Explorer/Notebook/NotebookManager.tsx +++ b/src/Explorer/Notebook/NotebookManager.tsx @@ -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; 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); diff --git a/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.less b/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.less index 541062c6f..d124cd0f2 100644 --- a/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.less +++ b/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.less @@ -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; + } } diff --git a/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx b/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx index 7fcdd2579..f912acd4a 100644 --- a/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx +++ b/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx @@ -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 { ? () => : undefined, editor: { - monaco: (props: PassedEditorProps) => - this.props.hideInputs ? <> : , + codemirror: (props: PassedEditorProps) => + this.props.hideInputs ? <> : , }, }} @@ -84,8 +84,8 @@ class NotebookReadOnlyRenderer extends React.Component { {{ editor: { - monaco: (props: PassedEditorProps) => - this.props.hideInputs ? <> : , + codemirror: (props: PassedEditorProps) => + this.props.hideInputs ? <> : , }, }} diff --git a/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.less b/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.less index c680f8f56..ed77b9fb7 100644 --- a/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.less +++ b/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.less @@ -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; -} \ No newline at end of file + background-color: transparent; +} diff --git a/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx b/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx index db3f8d5ad..792cd5e0a 100644 --- a/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx +++ b/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx @@ -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 { {{ editor: { - monaco: (props: PassedEditorProps) => , + codemirror: (props: PassedEditorProps) => ( + + ), }, prompt: ({ id, contentRef }: { id: CellId; contentRef: ContentRef }) => ( @@ -142,7 +144,9 @@ class BaseNotebookRenderer extends React.Component { {{ editor: { - monaco: (props: PassedEditorProps) => , + codemirror: (props: PassedEditorProps) => ( + + ), }, toolbar: () => , }} @@ -157,7 +161,9 @@ class BaseNotebookRenderer extends React.Component { {{ editor: { - monaco: (props: PassedEditorProps) => , + codemirror: (props: PassedEditorProps) => ( + + ), }, toolbar: () => , }} diff --git a/src/Explorer/Notebook/notebookClientV2.test.ts b/src/Explorer/Notebook/notebookClientV2.test.ts index bb0b6bef7..4313932b5 100644 --- a/src/Explorer/Notebook/notebookClientV2.test.ts +++ b/src/Explorer/Notebook/notebookClientV2.test.ts @@ -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: { diff --git a/src/Explorer/Notebook/useNotebook.ts b/src/Explorer/Notebook/useNotebook.ts new file mode 100644 index 000000000..ecf927565 --- /dev/null +++ b/src/Explorer/Notebook/useNotebook.ts @@ -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; +} + +export const useNotebook: UseStore = 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 => { + 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 }); + } + }, +})); diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel.tsx index aa98e4113..a946c1091 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel.tsx @@ -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 {this.state.errorMessage && ( @@ -137,7 +140,7 @@ export class AddCollectionPanel extends React.Component (this.newDatabaseThroughput = throughput)} @@ -469,9 +470,7 @@ export class AddCollectionPanel extends React.Component (this.collectionThroughput = throughput)} @@ -680,7 +679,7 @@ export class AddCollectionPanel extends React.Component ({ + return useDatabases.getState().databases?.map((database) => ({ key: database.id(), text: database.id(), })); @@ -772,9 +771,9 @@ export class AddCollectionPanel extends React.Component database.id() === this.state.selectedDatabaseId); + const selectedDatabase = useDatabases + .getState() + .databases?.find((database) => database.id() === this.state.selectedDatabaseId); return !!selectedDatabase?.offer(); } diff --git a/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx b/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx index 9bd37368b..3ee59bd30 100644 --- a/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx +++ b/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx @@ -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 = ({ {!formErrors && isFreeTierAccount && ( = ({ {!isServerlessAccount() && databaseCreateNewShared && ( (throughput = newThroughput)} diff --git a/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.test.tsx b/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.test.tsx index e052a6616..834bbed18 100644 --- a/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.test.tsx +++ b/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.test.tsx @@ -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(() => 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(); diff --git a/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.tsx b/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.tsx index dead62b56..cac39b636 100644 --- a/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.tsx +++ b/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.tsx @@ -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): 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 = ({ } 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 = ({ }); closeSidePanel(); }; + const isSaveQueryEnabled = useDatabases((state) => state.isSaveQueryEnabled); const props: QueriesGridComponentProps = { queriesClient: explorer.queriesClient, onQuerySelect: loadSavedQuery, containerVisible: true, - saveQueryEnabled: explorer.canSaveQueries(), + saveQueryEnabled: isSaveQueryEnabled(), }; return ( diff --git a/src/Explorer/Panes/BrowseQueriesPane/__snapshots__/BrowseQueriesPane.test.tsx.snap b/src/Explorer/Panes/BrowseQueriesPane/__snapshots__/BrowseQueriesPane.test.tsx.snap index eed894482..5fcb72a63 100644 --- a/src/Explorer/Panes/BrowseQueriesPane/__snapshots__/BrowseQueriesPane.test.tsx.snap +++ b/src/Explorer/Panes/BrowseQueriesPane/__snapshots__/BrowseQueriesPane.test.tsx.snap @@ -5,7 +5,6 @@ exports[`Browse queries panel Should render Default properly 1`] = ` closePanel={[Function]} explorer={ Object { - "canSaveQueries": [Function], "queriesClient": Object { "getQueries": [Function], }, diff --git a/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx index 3f829c191..7d0703336 100644 --- a/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx +++ b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx @@ -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 ({ + options={useDatabases.getState().databases?.map((keyspace) => ({ key: keyspace.id(), text: keyspace.id(), data: { @@ -253,7 +254,9 @@ export const CassandraAddCollectionPane: FunctionComponent (newKeySpaceThroughput = throughput)} @@ -324,7 +327,7 @@ export const CassandraAddCollectionPane: FunctionComponent (tableThroughput = throughput)} diff --git a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx index 3a982da82..60cb731b8 100644 --- a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx +++ b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx @@ -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 = ({ case "MyNotebooks": parent = { name: ResourceTreeAdapter.MyNotebooksTitle, - path: container.getNotebookBasePath(), + path: useNotebook.getState().notebookBasePath, type: NotebookContentItemType.Directory, }; break; diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.test.tsx b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.test.tsx index c75f998e3..26a98c485 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.test.tsx +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.test.tsx @@ -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([{} as Collection]); - explorer.databases = ko.observableArray([database]); - expect(explorer.isLastCollection()).toBe(true); + const database = { id: ko.observable("testDB") } as Database; + database.collections = ko.observableArray([{ 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([{} as Collection, {} as Collection]); - explorer.databases = ko.observableArray([database]); - expect(explorer.isLastCollection()).toBe(false); + const database = { id: ko.observable("testDB") } as Database; + database.collections = ko.observableArray([ + { 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([{} as Collection]); - const database2 = {} as Database; - database2.collections = ko.observableArray([{} as Collection]); - explorer.databases = ko.observableArray([database, database2]); - expect(explorer.isLastCollection()).toBe(false); + const database = { id: ko.observable("testDB") } as Database; + database.collections = ko.observableArray([{ id: ko.observable("coll1") } as Collection]); + const database2 = { id: ko.observable("testDB2") } as Database; + database2.collections = ko.observableArray([{ 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.collections = ko.observableArray(); - 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(); - expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true); - - props.explorer.isLastCollection = () => true; - props.explorer.isSelectedDatabaseShared = () => true; - wrapper.setProps(props); + const wrapper = shallow( 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([{ 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(selectedCollectionId), - databaseId, - rid: "test", - } as Collection; - }; - fakeExplorer.selectedCollectionId = ko.computed(() => selectedCollectionId); - fakeExplorer.selectedNode = ko.observable(); - 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]); + 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(); + useDatabases.getState().addDatabases([database]); + useSelectedNode.getState().setSelectedNode(collection); + }); + + afterEach(() => { + useDatabases.getState().clearDatabases(); + useSelectedNode.getState().setSelectedNode(undefined); }); it("should call delete collection", () => { + const wrapper = mount( 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( undefined} />); expect(wrapper.exists("#confirmCollectionId")).toBe(true); wrapper .find("#confirmCollectionId") diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx index 8fad674bd..b09ac3eae 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx @@ -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; } export const DeleteCollectionConfirmationPane: FunctionComponent = ({ - explorer, + refreshDatabases, }: DeleteCollectionConfirmationPaneProps) => { const closeSidePanel = useSidePanel((state) => state.closeSidePanel); const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState(""); @@ -27,13 +30,13 @@ export const DeleteCollectionConfirmationPane: FunctionComponent(""); 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 => { - 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 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); diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap b/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap index 59e62296b..923557265 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap @@ -2,18 +2,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collection 1`] = `
-
- - - Help us improve Azure Cosmos DB! - - - - - What is the reason why you are deleting this - container - ? - - - - -
-
-
-