diff --git a/.eslintignore b/.eslintignore index b4c56fa2d..f4064b9e5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -105,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 @@ -141,11 +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/ScriptTabBase.ts -# src/Explorer/Tabs/StoredProcedureTab.ts src/Explorer/Tabs/TabComponents.ts src/Explorer/Tabs/TabsBase.ts src/Explorer/Tabs/TriggerTab.ts @@ -168,35 +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/Shared/Constants.ts src/Shared/DefaultExperienceUtility.test.ts src/Shared/DefaultExperienceUtility.ts @@ -287,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/src/Explorer/ContextMenuButtonFactory.tsx b/src/Explorer/ContextMenuButtonFactory.tsx index 232427ccc..ece39b07d 100644 --- a/src/Explorer/ContextMenuButtonFactory.tsx +++ b/src/Explorer/ContextMenuButtonFactory.tsx @@ -50,7 +50,10 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin onClick: () => useSidePanel .getState() - .openSidePanel("Delete " + getDatabaseName(), ), + .openSidePanel( + "Delete " + getDatabaseName(), + container.refreshAllDatabases()} /> + ), label: `Delete ${getDatabaseName()}`, styleClass: "deleteDatabaseMenuItem", }); @@ -126,7 +129,10 @@ export const createCollectionContextMenuButton = ( onClick: () => useSidePanel .getState() - .openSidePanel("Delete " + getCollectionName(), ), + .openSidePanel( + "Delete " + getCollectionName(), + container.refreshAllDatabases()} /> + ), label: `Delete ${getCollectionName()}`, styleClass: "deleteCollectionMenuItem", }); diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index 457e2bb35..cb8ecbfaf 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -31,7 +31,6 @@ exports[`SettingsComponent renders 1`] = ` "_isInitializingNotebooks": false, "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], - "isSchemaEnabled": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], @@ -49,10 +48,6 @@ exports[`SettingsComponent renders 1`] = ` "container": [Circular], "parameters": [Function], }, - "tabsManager": TabsManager { - "activeTab": [Function], - "openedTabs": [Function], - }, }, "databaseId": "test", "defaultTtl": [Function], @@ -107,7 +102,6 @@ exports[`SettingsComponent renders 1`] = ` "_isInitializingNotebooks": false, "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], - "isSchemaEnabled": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], @@ -125,10 +119,6 @@ exports[`SettingsComponent renders 1`] = ` "container": [Circular], "parameters": [Function], }, - "tabsManager": TabsManager { - "activeTab": [Function], - "openedTabs": [Function], - }, }, "databaseId": "test", "defaultTtl": [Function], diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 6f04d5bbd..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"; @@ -17,6 +16,7 @@ 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 { ExplorerSettings } from "../Shared/ExplorerSettings"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; @@ -59,7 +59,7 @@ import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane"; import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; -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"; @@ -73,10 +73,6 @@ 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 queriesClient: QueriesClient; @@ -90,10 +86,8 @@ export default class Explorer { // Tabs public isTabsContentExpanded: ko.Observable; - public tabsManager: TabsManager; public gitHubOAuthService: GitHubOAuthService; - public isSchemaEnabled: ko.Computed; // Notebooks public notebookManager?: NotebookManager; @@ -106,7 +100,7 @@ export default class Explorer { private static readonly MaxNbDatabasesToAutoExpand = 5; - constructor(params?: ExplorerParams) { + constructor() { const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { dataExplorerArea: Constants.Areas.ResourceTree, }); @@ -117,7 +111,6 @@ export default class Explorer { ); this.queriesClient = new QueriesClient(this); - this.isSchemaEnabled = ko.computed(() => userContext.features.enableSchema); useSelectedNode.subscribe(() => { // Make sure switching tabs restores tabs display @@ -136,13 +129,15 @@ export default class Explorer { return isCapabilityEnabled("EnableMongo"); }); - this.tabsManager = params?.tabsManager ?? new TabsManager(); - this.tabsManager.openedTabs.subscribe((tabs) => { - if (tabs.length === 0) { - useSelectedNode.getState().setSelectedNode(undefined); - useCommandBar.getState().setContextButtons([]); - } - }); + useTabs.subscribe( + (openedTabs: TabsBase[]) => { + if (openedTabs.length === 0) { + useSelectedNode.getState().setSelectedNode(undefined); + useCommandBar.getState().setContextButtons([]); + } + }, + (state) => state.openedTabs + ); this.isTabsContentExpanded = ko.observable(false); @@ -283,96 +278,56 @@ export default class Explorer { // TODO: return result } - 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) => { - const resourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection); - useDatabases.setState({ resourceTokenCollection }); - useSelectedNode.getState().setSelectedNode(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 deltaDatabases = this.getDeltaDatabases(databases); - this.addDatabasesToList(deltaDatabases.toAdd); - this.deleteDatabasesFromList(deltaDatabases.toDelete); - 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 => { @@ -513,69 +468,13 @@ export default class Explorer { } }; - 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 databases = useDatabases.getState().databases; - const databasesToLoad = - databases.length <= Explorer.MaxNbDatabasesToAutoExpand - ? databases - : 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(); - } - } - private getDeltaDatabases( - updatedDatabaseList: DataModels.Database[] + updatedDatabaseList: DataModels.Database[], + databases: ViewModels.Database[] ): { toAdd: ViewModels.Database[]; toDelete: ViewModels.Database[]; } { - const databases = useDatabases.getState().databases; const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => { const databaseExists = _.some( databases, @@ -587,8 +486,8 @@ export default class Explorer { (newDatabase: DataModels.Database) => new Database(this, newDatabase) ); - let databasesToDelete: ViewModels.Database[] = []; - ko.utils.arrayForEach(databases, (database: ViewModels.Database) => { + const databasesToDelete: ViewModels.Database[] = []; + databases.forEach((database: ViewModels.Database) => { const databasePresentInUpdatedList = _.some( updatedDatabaseList, (db: DataModels.Database) => db.id === database.id() @@ -601,13 +500,58 @@ export default class Explorer { return { toAdd: databasesToAdd, toDelete: databasesToDelete }; } - private addDatabasesToList(databases: ViewModels.Database[]): void { - useDatabases.getState().addDatabases(databases); + 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. + + // 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); + + const startKey: number = TelemetryProcessor.traceStart(Action.LoadCollections, { + dataExplorerArea: Constants.Areas.ResourceTree, + }); + + 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 deleteDatabasesFromList(databasesToRemove: ViewModels.Database[]): void { - const deleteDatabase = useDatabases.getState().deleteDatabase; - databasesToRemove.forEach((database) => deleteDatabase(database)); + private _initSettings() { + if (!ExplorerSettings.hasSettingsDefined()) { + ExplorerSettings.createDefaultSettings(); + } } public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise { @@ -750,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, @@ -778,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; @@ -796,19 +742,17 @@ export default class Explorer { } // 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(); @@ -839,7 +783,6 @@ export default class Explorer { useSidePanel.getState().openSidePanel( "Create new directory", { useSidePanel.getState().closeSidePanel(); this.resourceTree.triggerRender(); @@ -927,12 +870,11 @@ export default class Explorer { } // 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(); @@ -1034,10 +976,9 @@ export default class Explorer { throw new Error("Terminal kind: ${kind} not supported"); } - const terminalTabs: TerminalTab[] = this.tabsManager.getTabs( - ViewModels.CollectionTabKind.Terminal, - (tab) => tab.tabTitle() === title - ) as TerminalTab[]; + const terminalTabs: TerminalTab[] = useTabs + .getState() + .getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => tab.tabTitle() === title) as TerminalTab[]; let index = 1; if (terminalTabs.length > 0) { @@ -1058,7 +999,7 @@ export default class Explorer { index: index, }); - this.tabsManager.activateNewTab(newTab); + useTabs.getState().activateNewTab(newTab); } public async openGallery( @@ -1069,14 +1010,15 @@ export default class Explorer { ) { const title = "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.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, @@ -1116,7 +1058,7 @@ export default class Explorer { } private refreshCommandBarButtons(): void { - const activeTab = this.tabsManager.activeTab(); + const activeTab = useTabs.getState().activeTab; if (activeTab) { activeTab.onActivate(); // TODO only update tabs buttons? } else { @@ -1208,7 +1150,7 @@ export default class Explorer { public async refreshExplorer(): Promise { userContext.authType === AuthType.ResourceToken ? this.refreshDatabaseForResourceToken() - : this.refreshAllDatabases(true); + : this.refreshAllDatabases(); await useNotebook.getState().refreshNotebooksEnabledStateForAccount(); const isNotebookEnabled: boolean = userContext.authType !== AuthType.ResourceToken && diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index 675ba6800..9755075a8 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -8,6 +8,7 @@ import * as React from "react"; import create, { UseStore } from "zustand"; import { StyleConstants } from "../../../Common/Constants"; import * as ViewModels from "../../../Contracts/ViewModels"; +import { useTabs } from "../../../hooks/useTabs"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import Explorer from "../../Explorer"; import { useSelectedNode } from "../../useSelectedNode"; @@ -53,7 +54,7 @@ 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) { + if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) { uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker")); } 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/Panes/BrowseQueriesPane/BrowseQueriesPane.tsx b/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.tsx index ce23077be..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"; @@ -36,7 +37,7 @@ export const BrowseQueriesPane: FunctionComponent = ({ selectedCollection.onNewQueryClick(selectedCollection, undefined, savedQuery.query); } - const queryTab = explorer && (explorer.tabsManager.activeTab() as NewQueryTab); + const queryTab = useTabs.getState().activeTab as NewQueryTab; queryTab.tabTitle(savedQuery.queryName); queryTab.tabPath(`${selectedCollection.databaseId}>${selectedCollection.id()}>${savedQuery.queryName}`); diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.test.tsx b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.test.tsx index 1ad1ddf12..26a98c485 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.test.tsx +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.test.tsx @@ -10,7 +10,6 @@ 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"; @@ -53,10 +52,7 @@ describe("Delete Collection Confirmation Pane", () => { 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; - - const wrapper = shallow(); + const wrapper = shallow( undefined} />); expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false); const database = { id: ko.observable("testDB") } as Database; @@ -65,11 +61,11 @@ describe("Delete Collection Confirmation Pane", () => { database.isDatabaseShared = ko.computed(() => false); useDatabases.getState().addDatabases([database]); useSelectedNode.getState().setSelectedNode(database); - wrapper.setProps({ explorer: fakeExplorer }); + wrapper.setProps({}); expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true); database.isDatabaseShared = ko.computed(() => true); - wrapper.setProps({ explorer: fakeExplorer }); + wrapper.setProps({}); expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false); }); }); @@ -77,8 +73,6 @@ describe("Delete Collection Confirmation Pane", () => { describe("submit()", () => { const selectedCollectionId = "testCol"; const databaseId = "testDatabase"; - const fakeExplorer = {} as Explorer; - fakeExplorer.refreshAllDatabases = () => undefined; const database = { id: ko.observable(databaseId) } as Database; const collection = { id: ko.observable(selectedCollectionId), @@ -115,7 +109,7 @@ describe("Delete Collection Confirmation Pane", () => { }); it("should call delete collection", () => { - const wrapper = mount(); + const wrapper = mount( undefined} />); expect(wrapper).toMatchSnapshot(); expect(wrapper.exists("#confirmCollectionId")).toBe(true); @@ -132,7 +126,7 @@ describe("Delete Collection Confirmation Pane", () => { }); it("should record feedback", async () => { - const wrapper = mount(); + 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 6effe0b01..b09ac3eae 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx @@ -6,23 +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(""); @@ -31,8 +31,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent - useDatabases.getState().isLastCollection() && - !useSelectedNode.getState().findSelectedDatabase()?.isDatabaseShared(); + useDatabases.getState().isLastCollection() && !useDatabases.getState().findSelectedDatabase()?.isDatabaseShared(); const collectionName = getCollectionName().toLocaleLowerCase(); const paneTitle = "Delete " + collectionName; @@ -63,10 +62,12 @@ export const DeleteCollectionConfirmationPane: FunctionComponent tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId - ); - explorer.refreshAllDatabases(); + 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 6b3104666..923557265 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap @@ -2,11 +2,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collection 1`] = ` { const selectedDatabaseId = "testDatabase"; - let fakeExplorer: Explorer; let database: Database; beforeAll(() => { @@ -37,10 +34,6 @@ describe("Delete Database Confirmation Pane", () => { }); beforeEach(() => { - fakeExplorer = {} as Explorer; - fakeExplorer.refreshAllDatabases = () => undefined; - fakeExplorer.tabsManager = new TabsManager(); - database = {} as Database; database.collections = ko.observableArray([{ id: ko.observable("testCollection") } as Collection]); database.id = ko.observable(selectedDatabaseId); @@ -56,17 +49,17 @@ describe("Delete Database Confirmation Pane", () => { }); it("shouldRecordFeedback() should return true if last non empty database or is last database that has shared throughput", () => { - const wrapper = shallow(); + const wrapper = shallow( undefined} />); expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(true); useDatabases.getState().addDatabases([database]); - wrapper.setProps({ explorer: fakeExplorer }); + wrapper.setProps({}); expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false); useDatabases.getState().clearDatabases(); }); it("Should call delete database", () => { - const wrapper = mount(); + const wrapper = mount( undefined} />); expect(wrapper).toMatchSnapshot(); expect(wrapper.exists("#confirmDatabaseId")).toBe(true); @@ -81,7 +74,7 @@ describe("Delete Database Confirmation Pane", () => { }); it("should record feedback", async () => { - const wrapper = mount(); + const wrapper = mount( undefined} />); expect(wrapper.exists("#confirmDatabaseId")).toBe(true); wrapper .find("#confirmDatabaseId") diff --git a/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.tsx b/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.tsx index 3fafdcafd..c07368e91 100644 --- a/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.tsx +++ b/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.tsx @@ -7,23 +7,23 @@ import DeleteFeedback from "../../Common/DeleteFeedback"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { Collection, Database } from "../../Contracts/ViewModels"; import { useSidePanel } from "../../hooks/useSidePanel"; +import { useTabs } from "../../hooks/useTabs"; import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { logConsoleError } from "../../Utils/NotificationConsoleUtils"; -import Explorer from "../Explorer"; import { useDatabases } from "../useDatabases"; import { useSelectedNode } from "../useSelectedNode"; import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent"; import { RightPaneForm, RightPaneFormProps } from "./RightPaneForm/RightPaneForm"; interface DeleteDatabaseConfirmationPanelProps { - explorer: Explorer; + refreshDatabases: () => Promise; } export const DeleteDatabaseConfirmationPanel: FunctionComponent = ({ - explorer, + refreshDatabases, }: DeleteDatabaseConfirmationPanelProps): JSX.Element => { const closeSidePanel = useSidePanel((state) => state.closeSidePanel); const isLastNonEmptyDatabase = useDatabases((state) => state.isLastNonEmptyDatabase); @@ -32,7 +32,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent(""); const [databaseInput, setDatabaseInput] = useState(""); const [databaseFeedbackInput, setDatabaseFeedbackInput] = useState(""); - const selectedDatabase: Database = useSelectedNode.getState().findSelectedDatabase(); + const selectedDatabase: Database = useDatabases.getState().findSelectedDatabase(); const submit = async (): Promise => { if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) { @@ -52,15 +52,18 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent tab.node?.id() === selectedDatabase.id()); + refreshDatabases(); + useTabs.getState().closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id()); useSelectedNode.getState().setSelectedNode(undefined); selectedDatabase .collections() .forEach((collection: Collection) => - explorer.tabsManager.closeTabsByComparator( - (tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId - ) + useTabs + .getState() + .closeTabsByComparator( + (tab) => + tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId + ) ); TelemetryProcessor.traceSuccess( Action.DeleteDatabase, diff --git a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap index 8522a1cfd..9d1b8cc0c 100644 --- a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap +++ b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap @@ -20,7 +20,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` "_isInitializingNotebooks": false, "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], - "isSchemaEnabled": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], @@ -38,10 +37,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` "container": [Circular], "parameters": [Function], }, - "tabsManager": TabsManager { - "activeTab": [Function], - "openedTabs": [Function], - }, }, "getRepo": [Function], "pinRepo": [Function], diff --git a/src/Explorer/Panes/SaveQueryPane/SaveQueryPane.tsx b/src/Explorer/Panes/SaveQueryPane/SaveQueryPane.tsx index f852b09c2..6d16876b3 100644 --- a/src/Explorer/Panes/SaveQueryPane/SaveQueryPane.tsx +++ b/src/Explorer/Panes/SaveQueryPane/SaveQueryPane.tsx @@ -5,6 +5,7 @@ import { Areas, SavedQueries } from "../../../Common/Constants"; import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import { Query } from "../../../Contracts/DataModels"; import { useSidePanel } from "../../../hooks/useSidePanel"; +import { useTabs } from "../../../hooks/useTabs"; import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor"; import { logConsoleError } from "../../../Utils/NotificationConsoleUtils"; @@ -34,7 +35,7 @@ export const SaveQueryPane: FunctionComponent = ({ explorer logConsoleError("Failed to save query: account not setup to save queries"); } - const queryTab = explorer && (explorer.tabsManager.activeTab() as NewQueryTab); + const queryTab = useTabs.getState().activeTab as NewQueryTab; const query: string = queryTab && queryTab.iTabAccessor.onSaveClickEvent(); if (!queryName || queryName.length === 0) { diff --git a/src/Explorer/Panes/StringInputPane/StringInputPane.tsx b/src/Explorer/Panes/StringInputPane/StringInputPane.tsx index c449b6cb1..8c8ce3b9e 100644 --- a/src/Explorer/Panes/StringInputPane/StringInputPane.tsx +++ b/src/Explorer/Panes/StringInputPane/StringInputPane.tsx @@ -1,15 +1,14 @@ import { TextField } from "@fluentui/react"; import React, { FormEvent, FunctionComponent, useState } from "react"; import * as ViewModels from "../../../Contracts/ViewModels"; +import { useTabs } from "../../../hooks/useTabs"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils"; -import Explorer from "../../Explorer"; import * as FileSystemUtil from "../../Notebook/FileSystemUtil"; import { NotebookContentItem } from "../../Notebook/NotebookContentItem"; import NotebookV2Tab from "../../Tabs/NotebookV2Tab"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; export interface StringInputPanelProps { - explorer: Explorer; closePanel: () => void; errorMessage: string; inProgressMessage: string; @@ -23,7 +22,6 @@ export interface StringInputPanelProps { } export const StringInputPane: FunctionComponent = ({ - explorer: container, closePanel, errorMessage, inProgressMessage, @@ -55,10 +53,12 @@ export const StringInputPane: FunctionComponent = ({ logConsoleInfo(`${successMessage}: ${stringInput}`); const originalPath = notebookFile.path; - const notebookTabs = container.tabsManager.getTabs( - ViewModels.CollectionTabKind.NotebookV2, - (tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath) - ); + const notebookTabs = useTabs + .getState() + .getTabs( + ViewModels.CollectionTabKind.NotebookV2, + (tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath) + ); notebookTabs.forEach((tab) => { tab.tabTitle(newNotebookFile.name); tab.tabPath(newNotebookFile.path); diff --git a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap index 9648eff21..27f1f941d 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -10,7 +10,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "_isInitializingNotebooks": false, "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], - "isSchemaEnabled": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], @@ -28,10 +27,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "container": [Circular], "parameters": [Function], }, - "tabsManager": TabsManager { - "activeTab": [Function], - "openedTabs": [Function], - }, } } inProgressMessage="Creating directory " diff --git a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap index 710c5db24..ca0868a35 100644 --- a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap +++ b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap @@ -2,15 +2,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = ` { const mock = new Explorer(); - mock.tabsManager = new TabsManager(); return mock as jest.Mocked; }; diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index fedc1c1eb..d05ea16c2 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -280,7 +280,7 @@ export class SplashScreen extends React.Component { } /* Scale & Settings */ - const isShared = useSelectedNode.getState().findSelectedDatabase()?.isDatabaseShared(); + const isShared = useDatabases.getState().findSelectedDatabase()?.isDatabaseShared(); const label = isShared ? "Settings" : "Scale & Settings"; items.push({ diff --git a/src/Explorer/Tables/Constants.ts b/src/Explorer/Tables/Constants.ts index 5d5d60fee..9bb313b7a 100644 --- a/src/Explorer/Tables/Constants.ts +++ b/src/Explorer/Tables/Constants.ts @@ -1,4 +1,4 @@ -export var TableType = { +export const TableType = { String: "String", Boolean: "Boolean", Binary: "Binary", @@ -9,7 +9,7 @@ export var TableType = { Int64: "Int64", }; -export var CassandraType = { +export const CassandraType = { Ascii: "Ascii", Bigint: "Bigint", Blob: "Blob", @@ -27,12 +27,12 @@ export var CassandraType = { Tinyint: "Tinyint", }; -export var ClauseRule = { +export const ClauseRule = { And: "And", Or: "Or", }; -export var Operator = { +export const Operator = { EqualTo: "==", GreaterThan: ">", GreaterThanOrEqualTo: ">=", @@ -42,7 +42,7 @@ export var Operator = { Equal: "=", }; -export var ODataOperator = { +export const ODataOperator = { EqualTo: "eq", GreaterThan: "gt", GreaterThanOrEqualTo: "ge", @@ -51,7 +51,7 @@ export var ODataOperator = { NotEqualTo: "ne", }; -export var timeOptions = { +export const timeOptions = { lastHour: "Last hour", last24Hours: "Last 24 hours", last7Days: "Last 7 days", @@ -62,7 +62,7 @@ export var timeOptions = { custom: "Custom...", }; -export var htmlSelectors = { +export const htmlSelectors = { dataTableSelector: "#storageTable", dataTableAllRowsSelector: "#storageTable tbody tr", dataTableHeadRowSelector: ".dataTable thead tr", @@ -84,9 +84,9 @@ export var htmlSelectors = { selectAllDropdownSelector: "#select-all-dropdown", }; -export var defaultHeader = " "; +export const defaultHeader = " "; -export var EntityKeyNames = { +export const EntityKeyNames = { PartitionKey: "PartitionKey", RowKey: "RowKey", Timestamp: "Timestamp", @@ -94,7 +94,7 @@ export var EntityKeyNames = { Etag: "etag", }; -export var htmlAttributeNames = { +export const htmlAttributeNames = { dataTableNameAttr: "name_attr", dataTableContentTypeAttr: "contentType_attr", dataTableSnapshotAttr: "snapshot_attr", @@ -103,14 +103,14 @@ export var htmlAttributeNames = { dataTableHeaderIndex: "data-column-index", }; -export var cssColors = { +export const cssColors = { commonControlsButtonActive: "#B4C7DC" /* A darker shade of [{common-controls-button-hover-background}] */, }; -export var clauseGroupColors = ["#ffe1ff", "#fffacd", "#f0ffff", "#ffefd5", "#f0fff0"]; -export var transparentColor = "transparent"; +export const clauseGroupColors = ["#ffe1ff", "#fffacd", "#f0ffff", "#ffefd5", "#f0fff0"]; +export const transparentColor = "transparent"; -export var keyCodes = { +export const keyCodes = { RightClick: 3, Enter: 13, Esc: 27, @@ -163,7 +163,7 @@ export var keyCodes = { Dash: 189, }; -export var InputType = { +export const InputType = { Text: "text", // Chrome doesn't support datetime, instead, datetime-local is supported. DateTime: "datetime-local", diff --git a/src/Explorer/Tabs/MongoShellTab/MongoShellTab.tsx b/src/Explorer/Tabs/MongoShellTab/MongoShellTab.tsx index edec5ba6b..d40965849 100644 --- a/src/Explorer/Tabs/MongoShellTab/MongoShellTab.tsx +++ b/src/Explorer/Tabs/MongoShellTab/MongoShellTab.tsx @@ -1,6 +1,7 @@ import React from "react"; import * as DataModels from "../../../Contracts/DataModels"; import type { TabOptions } from "../../../Contracts/ViewModels"; +import { useTabs } from "../../../hooks/useTabs"; import Explorer from "../../Explorer"; import TabsBase from "../TabsBase"; import MongoShellTabComponent, { IMongoShellTabAccessor, IMongoShellTabComponentProps } from "./MongoShellTabComponent"; @@ -33,7 +34,7 @@ export class NewMongoShellTab extends TabsBase { } public onTabClick(): void { - this.manager?.activateTab(this); + useTabs.getState().activateTab(this); this.iMongoShellTabAccessor.onTabClickEvent(); } } diff --git a/src/Explorer/Tabs/QueryTab/QueryTab.tsx b/src/Explorer/Tabs/QueryTab/QueryTab.tsx index 6a847c92f..11d609c87 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTab.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTab.tsx @@ -1,6 +1,7 @@ import React from "react"; import * as DataModels from "../../../Contracts/DataModels"; import type { QueryTabOptions } from "../../../Contracts/ViewModels"; +import { useTabs } from "../../../hooks/useTabs"; import Explorer from "../../Explorer"; import { IQueryTabComponentProps, ITabAccessor } from "../../Tabs/QueryTab/QueryTabComponent"; import TabsBase from "../TabsBase"; @@ -40,12 +41,12 @@ export class NewQueryTab extends TabsBase { } public onTabClick(): void { - this.manager?.activateTab(this); + useTabs.getState().activateTab(this); this.iTabAccessor.onTabClickEvent(); } public onCloseTabButtonClick(): void { - this.manager?.closeTab(this); + useTabs.getState().closeTab(this); if (this.iTabAccessor) { this.iTabAccessor.onCloseClickEvent(true); } diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index 73f5bf8b6..e8744441e 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -29,7 +29,6 @@ import { EditorReact } from "../../Controls/Editor/EditorReact"; import Explorer from "../../Explorer"; import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter"; import TabsBase from "../TabsBase"; -import { TabsManager } from "../TabsManager"; import "./QueryTabComponent.less"; enum ToggleState { @@ -65,7 +64,6 @@ export interface IQueryTabComponentProps { partitionKey: DataModels.PartitionKey; container: Explorer; activeTab?: TabsBase; - tabManager?: TabsManager; onTabAccessor: (instance: ITabAccessor) => void; isPreferredApiMongoDB?: boolean; monacoEditorSetting?: string; diff --git a/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTab.tsx b/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTab.tsx index 488944a05..b7b0f1673 100644 --- a/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTab.tsx +++ b/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTab.tsx @@ -2,6 +2,7 @@ import React from "react"; import { ExecuteSprocResult } from "../../../Common/dataAccess/executeStoredProcedure"; import * as DataModels from "../../../Contracts/DataModels"; import * as ViewModels from "../../../Contracts/ViewModels"; +import { useTabs } from "../../../hooks/useTabs"; import Explorer from "../../Explorer"; import StoredProcedure from "../../Tree/StoredProcedure"; import ScriptTabBase from "../ScriptTabBase"; @@ -51,12 +52,12 @@ export class NewStoredProcedureTab extends ScriptTabBase { } public onTabClick(): void { - this.manager?.activateTab(this); + useTabs.getState().activateTab(this); this.iStoreProcAccessor.onTabClickEvent(); } public onCloseTabButtonClick(): void { - this.manager?.closeTab(this); + useTabs.getState().closeTab(this); } public onExecuteSprocsResult(result: ExecuteSprocResult): void { diff --git a/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx b/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx index 71423e60e..b0d156b44 100644 --- a/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx +++ b/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx @@ -10,6 +10,7 @@ import { ExecuteSprocResult } from "../../../Common/dataAccess/executeStoredProc import { updateStoredProcedure } from "../../../Common/dataAccess/updateStoredProcedure"; import * as ViewModels from "../../../Contracts/ViewModels"; import { useNotificationConsole } from "../../../hooks/useNotificationConsole"; +import { useTabs } from "../../../hooks/useTabs"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { EditorReact } from "../../Controls/Editor/EditorReact"; import Explorer from "../../Explorer"; @@ -144,7 +145,7 @@ export default class StoredProcedureTabComponent extends React.Component< } public onTabClick(): void { - if (this.props.container.tabsManager.openedTabs().length > 0) { + if (useTabs.getState().openedTabs.length > 0) { useCommandBar.getState().setContextButtons(this.getTabsButtons()); } } @@ -396,10 +397,8 @@ export default class StoredProcedureTabComponent extends React.Component< editorModel && editorModel.setValue(createdResource.body as string); this.props.scriptTabBaseInstance.editorContent.setBaseline(createdResource.body as string); this.node = this.collection.createStoredProcedureNode(createdResource); - this.props.container.tabsManager.openedTabs()[ - this.props.container.tabsManager.openedTabs().length - 1 - ].node = this.node; - + this.props.scriptTabBaseInstance.node = this.node; + useTabs.getState().updateTab(this.props.scriptTabBaseInstance); this.props.scriptTabBaseInstance.editorState(ViewModels.ScriptEditorState.exisitingNoEdits); this.setState({ diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx index 9c4de6477..caf13ef7d 100644 --- a/src/Explorer/Tabs/Tabs.tsx +++ b/src/Explorer/Tabs/Tabs.tsx @@ -3,28 +3,32 @@ import React, { useEffect, useRef, useState } from "react"; import loadingIcon from "../../../images/circular_loader_black_16x16.gif"; import errorIcon from "../../../images/close-black.svg"; import { useObservable } from "../../hooks/useObservable"; +import { useTabs } from "../../hooks/useTabs"; import TabsBase from "./TabsBase"; type Tab = TabsBase | (TabsBase & { render: () => JSX.Element }); -export const Tabs = ({ tabs, activeTab }: { tabs: readonly Tab[]; activeTab: Tab }): JSX.Element => ( -
-
-
- -
-
- {tabs.map((tab) => ( - - ))} +
- -); + ); +}; function TabNav({ tab, active }: { tab: Tab; active: boolean }) { const [hovering, setHovering] = useState(false); diff --git a/src/Explorer/Tabs/TabsBase.ts b/src/Explorer/Tabs/TabsBase.ts index ccedf002b..308bb451f 100644 --- a/src/Explorer/Tabs/TabsBase.ts +++ b/src/Explorer/Tabs/TabsBase.ts @@ -4,6 +4,7 @@ import * as ThemeUtility from "../../Common/ThemeUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; import { useNotificationConsole } from "../../hooks/useNotificationConsole"; +import { useTabs } from "../../hooks/useTabs"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; @@ -11,7 +12,6 @@ import Explorer from "../Explorer"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { useSelectedNode } from "../useSelectedNode"; import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel"; -import { TabsManager } from "./TabsManager"; // TODO: Use specific actions for logging telemetry data export default class TabsBase extends WaitsForTemplateViewModel { private static id = 0; @@ -28,7 +28,6 @@ export default class TabsBase extends WaitsForTemplateViewModel { public isExecutionError = ko.observable(false); public isExecuting = ko.observable(false); public pendingNotification?: ko.Observable; - public manager?: TabsManager; protected _theme: string; public onLoadStartKey: number; @@ -60,7 +59,7 @@ export default class TabsBase extends WaitsForTemplateViewModel { } public onCloseTabButtonClick(): void { - this.manager?.closeTab(this); + useTabs.getState().closeTab(this); TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, { tabName: this.constructor.name, dataExplorerArea: Constants.Areas.Tab, @@ -70,7 +69,7 @@ export default class TabsBase extends WaitsForTemplateViewModel { } public onTabClick(): void { - this.manager?.activateTab(this); + useTabs.getState().activateTab(this); } protected updateSelectedNode(): void { @@ -105,7 +104,7 @@ export default class TabsBase extends WaitsForTemplateViewModel { /** @deprecated this is no longer observable, bind to comparisons with manager.activeTab() instead */ public isActive() { - return this === this.manager?.activeTab(); + return this === useTabs.getState().activeTab; } public onActivate(): void { diff --git a/src/Explorer/Tabs/TabsManager.ts b/src/Explorer/Tabs/TabsManager.ts deleted file mode 100644 index 95221a685..000000000 --- a/src/Explorer/Tabs/TabsManager.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as ko from "knockout"; -import * as ViewModels from "../../Contracts/ViewModels"; -import TabsBase from "./TabsBase"; - -export class TabsManager { - public openedTabs = ko.observableArray([]); - public activeTab = ko.observable(); - - public activateNewTab(tab: TabsBase): void { - this.openedTabs.push(tab); - this.activateTab(tab); - } - - public activateTab(tab: TabsBase): void { - if (this.openedTabs().includes(tab)) { - tab.manager = this; - this.activeTab(tab); - tab.onActivate(); - } - } - - public getTabs(tabKind: ViewModels.CollectionTabKind, comparator?: (tab: TabsBase) => boolean): TabsBase[] { - return this.openedTabs().filter((tab) => tab.tabKind === tabKind && (!comparator || comparator(tab))); - } - - public refreshActiveTab(comparator: (tab: TabsBase) => boolean): void { - // ensures that the tab selects/highlights the right node based on resource tree expand/collapse state - this.activeTab() && comparator(this.activeTab()) && this.activeTab().onActivate(); - } - - public closeTabsByComparator(comparator: (tab: TabsBase) => boolean): void { - this.openedTabs() - .filter(comparator) - .forEach((tab) => tab.onCloseTabButtonClick()); - } - - public closeTab(tab: TabsBase): void { - const tabIndex = this.openedTabs().indexOf(tab); - if (tabIndex !== -1) { - this.openedTabs.remove(tab); - tab.manager = undefined; - - if (this.openedTabs().length === 0) { - this.activeTab(undefined); - } - - if (tab === this.activeTab()) { - const tabToTheRight = this.openedTabs()[tabIndex]; - const lastOpenTab = this.openedTabs()[this.openedTabs().length - 1]; - this.activateTab(tabToTheRight ?? lastOpenTab); - } - } - } -} diff --git a/src/Explorer/Tabs/TabsManager.test.ts b/src/Explorer/Tabs/useTabs.test.ts similarity index 57% rename from src/Explorer/Tabs/TabsManager.test.ts rename to src/Explorer/Tabs/useTabs.test.ts index 4106eba3c..8d8148c69 100644 --- a/src/Explorer/Tabs/TabsManager.test.ts +++ b/src/Explorer/Tabs/useTabs.test.ts @@ -1,23 +1,19 @@ import * as ko from "knockout"; import * as ViewModels from "../../Contracts/ViewModels"; +import { useTabs } from "../../hooks/useTabs"; import { updateUserContext } from "../../UserContext"; -import Explorer from "../Explorer"; +import { container } from "../Controls/Settings/TestUtils"; import DocumentId from "../Tree/DocumentId"; -import { container } from "./../Controls/Settings/TestUtils"; import DocumentsTab from "./DocumentsTab"; import { NewQueryTab } from "./QueryTab/QueryTab"; -import { TabsManager } from "./TabsManager"; -describe("Tabs manager tests", () => { - let tabsManager: TabsManager; - let explorer: Explorer; +describe("useTabs tests", () => { let database: ViewModels.Database; let collection: ViewModels.Collection; let queryTab: NewQueryTab; let documentsTab: DocumentsTab; beforeEach(() => { - explorer = new Explorer(); updateUserContext({ databaseAccount: { id: "test", @@ -30,7 +26,6 @@ describe("Tabs manager tests", () => { }); database = { - container: explorer, id: ko.observable("test"), isDatabaseShared: () => false, } as ViewModels.Database; @@ -38,7 +33,6 @@ describe("Tabs manager tests", () => { database.selectedSubnodeKind = ko.observable(); collection = { - container: explorer, databaseId: "test", id: ko.observable("test"), } as ViewModels.Collection; @@ -76,63 +70,70 @@ describe("Tabs manager tests", () => { documentsTab.tabId = "2"; }); - beforeEach(() => (tabsManager = new TabsManager())); + beforeEach(() => useTabs.setState({ openedTabs: [], activeTab: undefined })); it("open new tabs", () => { - tabsManager.activateNewTab(queryTab); - expect(tabsManager.openedTabs().length).toBe(1); - expect(tabsManager.openedTabs()[0]).toEqual(queryTab); - expect(tabsManager.activeTab()).toEqual(queryTab); + const { activateNewTab } = useTabs.getState(); + activateNewTab(queryTab); + let tabsState = useTabs.getState(); + expect(tabsState.openedTabs.length).toBe(1); + expect(tabsState.openedTabs[0]).toEqual(queryTab); + expect(tabsState.activeTab).toEqual(queryTab); expect(queryTab.isActive()).toBe(true); - tabsManager.activateNewTab(documentsTab); - expect(tabsManager.openedTabs().length).toBe(2); - expect(tabsManager.openedTabs()[1]).toEqual(documentsTab); - expect(tabsManager.activeTab()).toEqual(documentsTab); + activateNewTab(documentsTab); + tabsState = useTabs.getState(); + expect(tabsState.openedTabs.length).toBe(2); + expect(tabsState.openedTabs[1]).toEqual(documentsTab); + expect(tabsState.activeTab).toEqual(documentsTab); expect(queryTab.isActive()).toBe(false); expect(documentsTab.isActive()).toBe(true); }); it("open existing tabs", () => { - tabsManager.activateNewTab(queryTab); - tabsManager.activateNewTab(documentsTab); - tabsManager.activateTab(queryTab); - expect(tabsManager.openedTabs().length).toBe(2); - expect(tabsManager.activeTab()).toEqual(queryTab); + const { activateNewTab, activateTab } = useTabs.getState(); + activateNewTab(queryTab); + activateNewTab(documentsTab); + activateTab(queryTab); + + const { openedTabs, activeTab } = useTabs.getState(); + expect(openedTabs.length).toBe(2); + expect(activeTab).toEqual(queryTab); expect(queryTab.isActive()).toBe(true); expect(documentsTab.isActive()).toBe(false); }); it("get tabs", () => { - tabsManager.activateNewTab(queryTab); - tabsManager.activateNewTab(documentsTab); + const { activateNewTab, getTabs } = useTabs.getState(); + activateNewTab(queryTab); + activateNewTab(documentsTab); - const queryTabs = tabsManager.getTabs(ViewModels.CollectionTabKind.Query); + const queryTabs = getTabs(ViewModels.CollectionTabKind.Query); expect(queryTabs.length).toBe(1); expect(queryTabs[0]).toEqual(queryTab); - const documentsTabs = tabsManager.getTabs( - ViewModels.CollectionTabKind.Documents, - (tab) => tab.tabId === documentsTab.tabId - ); + const documentsTabs = getTabs(ViewModels.CollectionTabKind.Documents, (tab) => tab.tabId === documentsTab.tabId); expect(documentsTabs.length).toBe(1); expect(documentsTabs[0]).toEqual(documentsTab); }); it("close tabs", () => { - tabsManager.activateNewTab(queryTab); - tabsManager.activateNewTab(documentsTab); + const { activateNewTab, closeTab, closeTabsByComparator } = useTabs.getState(); + activateNewTab(queryTab); + activateNewTab(documentsTab); + closeTab(documentsTab); - tabsManager.closeTab(documentsTab); - expect(tabsManager.openedTabs().length).toBe(1); - expect(tabsManager.openedTabs()[0]).toEqual(queryTab); - expect(tabsManager.activeTab()).toEqual(queryTab); + let tabsState = useTabs.getState(); + expect(tabsState.openedTabs.length).toBe(1); + expect(tabsState.openedTabs[0]).toEqual(queryTab); + expect(tabsState.activeTab).toEqual(queryTab); expect(queryTab.isActive()).toBe(true); expect(documentsTab.isActive()).toBe(false); - tabsManager.closeTabsByComparator((tab) => tab.tabId === queryTab.tabId); - expect(tabsManager.openedTabs().length).toBe(0); - expect(tabsManager.activeTab()).toEqual(undefined); + closeTabsByComparator((tab) => tab.tabId === queryTab.tabId); + tabsState = useTabs.getState(); + expect(tabsState.openedTabs.length).toBe(0); + expect(tabsState.activeTab).toEqual(undefined); expect(queryTab.isActive()).toBe(false); }); }); diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 7a9fcb22f..d2ceb8deb 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -15,6 +15,7 @@ import { fetchPortalNotifications } from "../../Common/PortalNotifications"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; import { UploadDetailsRecord } from "../../Contracts/ViewModels"; +import { useTabs } from "../../hooks/useTabs"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; @@ -239,9 +240,11 @@ export default class Collection implements ViewModels.Collection { this.expandCollection(); } useCommandBar.getState().setContextButtons([]); - this.container.tabsManager.refreshActiveTab( - (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() - ); + useTabs + .getState() + .refreshActiveTab( + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + ); } public collapseCollection() { @@ -288,14 +291,16 @@ export default class Collection implements ViewModels.Collection { dataExplorerArea: Constants.Areas.ResourceTree, }); - const documentsTabs: DocumentsTab[] = this.container.tabsManager.getTabs( - ViewModels.CollectionTabKind.Documents, - (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() - ) as DocumentsTab[]; + const documentsTabs: DocumentsTab[] = useTabs + .getState() + .getTabs( + ViewModels.CollectionTabKind.Documents, + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + ) as DocumentsTab[]; let documentsTab: DocumentsTab = documentsTabs && documentsTabs[0]; if (documentsTab) { - this.container.tabsManager.activateTab(documentsTab); + useTabs.getState().activateTab(documentsTab); } else { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { databaseName: this.databaseId, @@ -317,7 +322,7 @@ export default class Collection implements ViewModels.Collection { onLoadStartKey: startKey, }); - this.container.tabsManager.activateNewTab(documentsTab); + useTabs.getState().activateNewTab(documentsTab); } } @@ -333,14 +338,16 @@ export default class Collection implements ViewModels.Collection { dataExplorerArea: Constants.Areas.ResourceTree, }); - const conflictsTabs: ConflictsTab[] = this.container.tabsManager.getTabs( - ViewModels.CollectionTabKind.Conflicts, - (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() - ) as ConflictsTab[]; + const conflictsTabs: ConflictsTab[] = useTabs + .getState() + .getTabs( + ViewModels.CollectionTabKind.Conflicts, + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + ) as ConflictsTab[]; let conflictsTab: ConflictsTab = conflictsTabs && conflictsTabs[0]; if (conflictsTab) { - this.container.tabsManager.activateTab(conflictsTab); + useTabs.getState().activateTab(conflictsTab); } else { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { databaseName: this.databaseId, @@ -362,7 +369,7 @@ export default class Collection implements ViewModels.Collection { onLoadStartKey: startKey, }); - this.container.tabsManager.activateNewTab(conflictsTab); + useTabs.getState().activateNewTab(conflictsTab); } } @@ -384,14 +391,16 @@ export default class Collection implements ViewModels.Collection { }); } - const queryTablesTabs: QueryTablesTab[] = this.container.tabsManager.getTabs( - ViewModels.CollectionTabKind.QueryTables, - (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() - ) as QueryTablesTab[]; + const queryTablesTabs: QueryTablesTab[] = useTabs + .getState() + .getTabs( + ViewModels.CollectionTabKind.QueryTables, + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + ) as QueryTablesTab[]; let queryTablesTab: QueryTablesTab = queryTablesTabs && queryTablesTabs[0]; if (queryTablesTab) { - this.container.tabsManager.activateTab(queryTablesTab); + useTabs.getState().activateTab(queryTablesTab); } else { this.documentIds([]); let title = `Entities`; @@ -415,7 +424,7 @@ export default class Collection implements ViewModels.Collection { onLoadStartKey: startKey, }); - this.container.tabsManager.activateNewTab(queryTablesTab); + useTabs.getState().activateNewTab(queryTablesTab); } } @@ -431,14 +440,16 @@ export default class Collection implements ViewModels.Collection { dataExplorerArea: Constants.Areas.ResourceTree, }); - const graphTabs: GraphTab[] = this.container.tabsManager.getTabs( - ViewModels.CollectionTabKind.Graph, - (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() - ) as GraphTab[]; + const graphTabs: GraphTab[] = useTabs + .getState() + .getTabs( + ViewModels.CollectionTabKind.Graph, + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + ) as GraphTab[]; let graphTab: GraphTab = graphTabs && graphTabs[0]; if (graphTab) { - this.container.tabsManager.activateTab(graphTab); + useTabs.getState().activateTab(graphTab); } else { this.documentIds([]); const title = "Graph"; @@ -466,7 +477,7 @@ export default class Collection implements ViewModels.Collection { onLoadStartKey: startKey, }); - this.container.tabsManager.activateNewTab(graphTab); + useTabs.getState().activateNewTab(graphTab); } } @@ -482,14 +493,16 @@ export default class Collection implements ViewModels.Collection { dataExplorerArea: Constants.Areas.ResourceTree, }); - const mongoDocumentsTabs: MongoDocumentsTab[] = this.container.tabsManager.getTabs( - ViewModels.CollectionTabKind.Documents, - (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() - ) as MongoDocumentsTab[]; + const mongoDocumentsTabs: MongoDocumentsTab[] = useTabs + .getState() + .getTabs( + ViewModels.CollectionTabKind.Documents, + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + ) as MongoDocumentsTab[]; let mongoDocumentsTab: MongoDocumentsTab = mongoDocumentsTabs && mongoDocumentsTabs[0]; if (mongoDocumentsTab) { - this.container.tabsManager.activateTab(mongoDocumentsTab); + useTabs.getState().activateTab(mongoDocumentsTab); } else { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { databaseName: this.databaseId, @@ -510,7 +523,7 @@ export default class Collection implements ViewModels.Collection { node: this, onLoadStartKey: startKey, }); - this.container.tabsManager.activateNewTab(mongoDocumentsTab); + useTabs.getState().activateNewTab(mongoDocumentsTab); } }; @@ -525,13 +538,13 @@ export default class Collection implements ViewModels.Collection { dataExplorerArea: Constants.Areas.ResourceTree, }); - for (const tab of this.container.tabsManager.openedTabs()) { + for (const tab of useTabs.getState().openedTabs) { if ( tab instanceof SchemaAnalyzerTab && tab.collection?.databaseId === this.databaseId && tab.collection?.id() === this.id() ) { - return this.container.tabsManager.activateTab(tab); + return useTabs.getState().activateTab(tab); } } @@ -542,7 +555,7 @@ export default class Collection implements ViewModels.Collection { tabTitle: "Schema", }); this.documentIds([]); - this.container.tabsManager.activateNewTab( + useTabs.getState().activateNewTab( new SchemaAnalyzerTab({ account: userContext.databaseAccount, masterKey: userContext.masterKey || "", @@ -571,12 +584,9 @@ export default class Collection implements ViewModels.Collection { }); const tabTitle = !this.offer() ? "Settings" : "Scale & Settings"; - const matchingTabs = this.container.tabsManager.getTabs( - ViewModels.CollectionTabKind.CollectionSettingsV2, - (tab) => { - return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id(); - } - ); + const matchingTabs = useTabs.getState().getTabs(ViewModels.CollectionTabKind.CollectionSettingsV2, (tab) => { + return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id(); + }); const traceStartData = { databaseName: this.databaseId, @@ -608,15 +618,15 @@ export default class Collection implements ViewModels.Collection { settingsTabOptions.onLoadStartKey = startKey; settingsTabOptions.tabKind = ViewModels.CollectionTabKind.CollectionSettingsV2; settingsTabV2 = new CollectionSettingsTabV2(settingsTabOptions); - this.container.tabsManager.activateNewTab(settingsTabV2); + useTabs.getState().activateNewTab(settingsTabV2); } else { - this.container.tabsManager.activateTab(settingsTabV2); + useTabs.getState().activateTab(settingsTabV2); } }; public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) { const collection: ViewModels.Collection = source.collection || source; - const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Query).length + 1; + const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1; const title = "Query " + id; const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { databaseName: this.databaseId, @@ -626,7 +636,7 @@ export default class Collection implements ViewModels.Collection { tabTitle: title, }); - this.container.tabsManager.activateNewTab( + useTabs.getState().activateNewTab( new NewQueryTab( { tabKind: ViewModels.CollectionTabKind.Query, @@ -645,7 +655,7 @@ export default class Collection implements ViewModels.Collection { public onNewMongoQueryClick(source: any, event: MouseEvent, queryText?: string) { const collection: ViewModels.Collection = source.collection || source; - const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Query).length + 1; + const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1; const title = "Query " + id; const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { @@ -672,11 +682,11 @@ export default class Collection implements ViewModels.Collection { } ); - this.container.tabsManager.activateNewTab(newMongoQueryTab); + useTabs.getState().activateNewTab(newMongoQueryTab); } public onNewGraphClick() { - const id: number = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Graph).length + 1; + const id: number = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Graph).length + 1; const title: string = "Graph Query " + id; const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { @@ -702,13 +712,11 @@ export default class Collection implements ViewModels.Collection { onLoadStartKey: startKey, }); - this.container.tabsManager.activateNewTab(graphTab); + useTabs.getState().activateNewTab(graphTab); } public onNewMongoShellClick() { - const mongoShellTabs = this.container.tabsManager.getTabs( - ViewModels.CollectionTabKind.MongoShell - ) as NewMongoShellTab[]; + const mongoShellTabs = useTabs.getState().getTabs(ViewModels.CollectionTabKind.MongoShell) as NewMongoShellTab[]; let index = 1; if (mongoShellTabs.length > 0) { @@ -729,7 +737,7 @@ export default class Collection implements ViewModels.Collection { } ); - this.container.tabsManager.activateNewTab(mongoShellTab); + useTabs.getState().activateNewTab(mongoShellTab); } public onNewStoredProcedureClick(source: ViewModels.Collection, event: MouseEvent) { @@ -787,9 +795,11 @@ export default class Collection implements ViewModels.Collection { } else { this.expandStoredProcedures(); } - this.container.tabsManager.refreshActiveTab( - (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() - ); + useTabs + .getState() + .refreshActiveTab( + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + ); } public expandStoredProcedures() { @@ -846,9 +856,11 @@ export default class Collection implements ViewModels.Collection { } else { this.expandUserDefinedFunctions(); } - this.container.tabsManager.refreshActiveTab( - (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() - ); + useTabs + .getState() + .refreshActiveTab( + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + ); } public expandUserDefinedFunctions() { @@ -905,9 +917,11 @@ export default class Collection implements ViewModels.Collection { } else { this.expandTriggers(); } - this.container.tabsManager.refreshActiveTab( - (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() - ); + useTabs + .getState() + .refreshActiveTab( + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + ); } public expandTriggers() { diff --git a/src/Explorer/Tree/Database.test.tsx b/src/Explorer/Tree/Database.test.tsx index 87c75f8c5..47e4b665a 100644 --- a/src/Explorer/Tree/Database.test.tsx +++ b/src/Explorer/Tree/Database.test.tsx @@ -1,7 +1,7 @@ -import * as ko from "knockout"; import { HttpStatusCodes } from "../../Common/Constants"; import * as DataModels from "../../Contracts/DataModels"; import { JunoClient } from "../../Juno/JunoClient"; +import { Features } from "../../Platform/Hosted/extractFeatures"; import { updateUserContext, userContext } from "../../UserContext"; import Explorer from "../Explorer"; import Database from "./Database"; @@ -35,7 +35,6 @@ describe("Add Schema", () => { collection.analyticalStorageTtl = undefined; const database = new Database(createMockContainer(), collection); database.container = createMockContainer(); - database.container.isSchemaEnabled = ko.computed(() => false); database.junoClient = new JunoClient(); database.junoClient.requestSchema = jest.fn(); @@ -52,7 +51,11 @@ describe("Add Schema", () => { const database = new Database(createMockContainer(), collection); database.container = createMockContainer(); - database.container.isSchemaEnabled = ko.computed(() => true); + updateUserContext({ + features: { + enableSchema: true, + } as Features, + }); database.junoClient = new JunoClient(); database.junoClient.requestSchema = jest.fn(); diff --git a/src/Explorer/Tree/Database.tsx b/src/Explorer/Tree/Database.tsx index cfa7decf0..5d4f83fc7 100644 --- a/src/Explorer/Tree/Database.tsx +++ b/src/Explorer/Tree/Database.tsx @@ -11,6 +11,7 @@ import { fetchPortalNotifications } from "../../Common/PortalNotifications"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; import { useSidePanel } from "../../hooks/useSidePanel"; +import { useTabs } from "../../hooks/useTabs"; import { IJunoResponse, JunoClient } from "../../Juno/JunoClient"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; @@ -67,7 +68,7 @@ export default class Database implements ViewModels.Database { const pendingNotificationsPromise: Promise = this.getPendingThroughputSplitNotification(); const tabKind = ViewModels.CollectionTabKind.DatabaseSettingsV2; - const matchingTabs = this.container.tabsManager.getTabs(tabKind, (tab) => tab.node?.id() === this.id()); + const matchingTabs = useTabs.getState().getTabs(tabKind, (tab) => tab.node?.id() === this.id()); let settingsTab = matchingTabs?.[0] as DatabaseSettingsTabV2; if (!settingsTab) { @@ -91,7 +92,7 @@ export default class Database implements ViewModels.Database { }; settingsTab = new DatabaseSettingsTabV2(tabOptions); settingsTab.pendingNotification(pendingNotification); - this.container.tabsManager.activateNewTab(settingsTab); + useTabs.getState().activateNewTab(settingsTab); }, (error) => { const errorMessage = getErrorMessage(error); @@ -116,11 +117,11 @@ export default class Database implements ViewModels.Database { pendingNotificationsPromise.then( (pendingNotification: DataModels.Notification) => { settingsTab.pendingNotification(pendingNotification); - this.container.tabsManager.activateTab(settingsTab); + useTabs.getState().activateTab(settingsTab); }, () => { settingsTab.pendingNotification(undefined); - this.container.tabsManager.activateTab(settingsTab); + useTabs.getState().activateTab(settingsTab); } ); } @@ -312,7 +313,7 @@ export default class Database implements ViewModels.Database { let checkForSchema: NodeJS.Timeout; interval = interval || 5000; - if (collection.analyticalStorageTtl !== undefined && this.container.isSchemaEnabled()) { + if (collection.analyticalStorageTtl !== undefined && userContext.features.enableSchema) { collection.requestSchema = () => { this.junoClient.requestSchema({ id: undefined, diff --git a/src/Explorer/Tree/ResourceTokenCollection.ts b/src/Explorer/Tree/ResourceTokenCollection.ts index cd4e82d03..04daa74a3 100644 --- a/src/Explorer/Tree/ResourceTokenCollection.ts +++ b/src/Explorer/Tree/ResourceTokenCollection.ts @@ -2,6 +2,7 @@ import * as ko from "knockout"; import * as Constants from "../../Common/Constants"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; +import { useTabs } from "../../hooks/useTabs"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; @@ -77,7 +78,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) { const collection: ViewModels.Collection = source.collection || source; - const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Query).length + 1; + const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1; const title = "Query " + id; const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { databaseName: this.databaseId, @@ -87,7 +88,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas tabTitle: title, }); - this.container.tabsManager.activateNewTab( + useTabs.getState().activateNewTab( new NewQueryTab( { tabKind: ViewModels.CollectionTabKind.Query, @@ -115,16 +116,18 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas dataExplorerArea: Constants.Areas.ResourceTree, }); - const documentsTabs: DocumentsTab[] = this.container.tabsManager.getTabs( - ViewModels.CollectionTabKind.Documents, - (tab: TabsBase) => - tab.collection?.id() === this.id() && - (tab.collection as ViewModels.CollectionBase).databaseId === this.databaseId - ) as DocumentsTab[]; + const documentsTabs: DocumentsTab[] = useTabs + .getState() + .getTabs( + ViewModels.CollectionTabKind.Documents, + (tab: TabsBase) => + tab.collection?.id() === this.id() && + (tab.collection as ViewModels.CollectionBase).databaseId === this.databaseId + ) as DocumentsTab[]; let documentsTab: DocumentsTab = documentsTabs && documentsTabs[0]; if (documentsTab) { - this.container.tabsManager.activateTab(documentsTab); + useTabs.getState().activateTab(documentsTab); } else { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { databaseName: this.databaseId, @@ -146,7 +149,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas onLoadStartKey: startKey, }); - this.container.tabsManager.activateNewTab(documentsTab); + useTabs.getState().activateNewTab(documentsTab); } } diff --git a/src/Explorer/Tree/ResourceTreeAdapter.test.ts b/src/Explorer/Tree/ResourceTreeAdapter.test.ts index 5a51a026a..05bebd913 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.test.ts +++ b/src/Explorer/Tree/ResourceTreeAdapter.test.ts @@ -1,28 +1,34 @@ import * as ko from "knockout"; import * as ViewModels from "../../Contracts/ViewModels"; +import { useTabs } from "../../hooks/useTabs"; import TabsBase from "../Tabs/TabsBase"; import { useSelectedNode } from "../useSelectedNode"; -describe("useSelectedNode.getState()", () => { +describe("useSelectedNode", () => { const mockTab = { tabKind: ViewModels.CollectionTabKind.Documents, } as TabsBase; // TODO isDataNodeSelected needs a better design and refactor, but for now, we protect some of the code paths describe("isDataNodeSelected", () => { - afterEach(() => useSelectedNode.getState().setSelectedNode(undefined)); + afterEach(() => { + useSelectedNode.getState().setSelectedNode(undefined); + useTabs.setState({ activeTab: undefined }); + }); it("it should not select if no selected node", () => { - const isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected(mockTab, "foo", "bar", undefined); + useTabs.setState({ activeTab: mockTab }); + const isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected("foo", "bar", undefined); expect(isDataNodeSelected).toBeFalsy(); }); it("it should not select incorrect subnodekinds", () => { + useTabs.setState({ activeTab: mockTab }); useSelectedNode.getState().setSelectedNode({ nodeKind: "nodeKind", rid: "rid", id: ko.observable("id"), }); - const isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected(mockTab, "foo", "bar", undefined); + const isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected("foo", "bar", undefined); expect(isDataNodeSelected).toBeFalsy(); }); @@ -32,11 +38,12 @@ describe("useSelectedNode.getState()", () => { rid: "rid", id: ko.observable("id"), }); - const isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected(undefined, "foo", "bar", undefined); + const isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected("foo", "bar", undefined); expect(isDataNodeSelected).toBeFalsy(); }); it("should select if correct database node regardless of subnodekinds", () => { + useTabs.setState({ activeTab: mockTab }); const subNodeKind = ViewModels.CollectionTabKind.Documents; useSelectedNode.getState().setSelectedNode({ nodeKind: "Database", @@ -46,7 +53,7 @@ describe("useSelectedNode.getState()", () => { } as ViewModels.TreeNode); const isDataNodeSelected = useSelectedNode .getState() - .isDataNodeSelected(mockTab, "dbid", undefined, [ViewModels.CollectionTabKind.Documents]); + .isDataNodeSelected("dbid", undefined, [ViewModels.CollectionTabKind.Documents]); expect(isDataNodeSelected).toBeTruthy(); }); @@ -55,6 +62,7 @@ describe("useSelectedNode.getState()", () => { let activeTab = { tabKind: subNodeKind, } as TabsBase; + useTabs.setState({ activeTab }); useSelectedNode.getState().setSelectedNode({ nodeKind: "Collection", rid: "collrid", @@ -62,15 +70,14 @@ describe("useSelectedNode.getState()", () => { id: ko.observable("collid"), selectedSubnodeKind: ko.observable(subNodeKind), } as ViewModels.TreeNode); - let isDataNodeSelected = useSelectedNode - .getState() - .isDataNodeSelected(activeTab, "dbid", "collid", [subNodeKind]); + let isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected("dbid", "collid", [subNodeKind]); expect(isDataNodeSelected).toBeTruthy(); subNodeKind = ViewModels.CollectionTabKind.Graph; activeTab = { tabKind: subNodeKind, } as TabsBase; + useTabs.setState({ activeTab }); useSelectedNode.getState().setSelectedNode({ nodeKind: "Collection", rid: "collrid", @@ -78,7 +85,7 @@ describe("useSelectedNode.getState()", () => { id: ko.observable("collid"), selectedSubnodeKind: ko.observable(subNodeKind), } as ViewModels.TreeNode); - isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected(activeTab, "dbid", "collid", [subNodeKind]); + isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected("dbid", "collid", [subNodeKind]); expect(isDataNodeSelected).toBeTruthy(); }); @@ -93,9 +100,10 @@ describe("useSelectedNode.getState()", () => { const activeTab = { tabKind: ViewModels.CollectionTabKind.Documents, } as TabsBase; + useTabs.setState({ activeTab }); const isDataNodeSelected = useSelectedNode .getState() - .isDataNodeSelected(activeTab, "dbid", "collid", [ViewModels.CollectionTabKind.Settings]); + .isDataNodeSelected("dbid", "collid", [ViewModels.CollectionTabKind.Settings]); expect(isDataNodeSelected).toBeFalsy(); }); }); diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index a499857c2..936046567 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -16,6 +16,7 @@ import { Areas } from "../../Common/Constants"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; +import { useTabs } from "../../hooks/useTabs"; import { IPinnedRepo } from "../../Juno/JunoClient"; import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; @@ -57,7 +58,10 @@ export class ResourceTreeAdapter implements ReactAdapter { this.parameters = ko.observable(Date.now()); useSelectedNode.subscribe(() => this.triggerRender()); - this.container.tabsManager.activeTab.subscribe((newValue: TabsBase) => this.triggerRender()); + useTabs.subscribe( + () => this.triggerRender(), + (state) => state.activeTab + ); useNotebook.subscribe( () => this.triggerRender(), (state) => state.isNotebookEnabled @@ -188,8 +192,7 @@ export class ResourceTreeAdapter implements ReactAdapter { isExpanded: false, className: "databaseHeader", children: [], - isSelected: () => - useSelectedNode.getState().isDataNodeSelected(this.container.tabsManager.activeTab(), database.id()), + isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()), contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(this.container, database.id()), onClick: async (isExpanded) => { // Rewritten version of expandCollapseDatabase(): @@ -204,7 +207,7 @@ export class ResourceTreeAdapter implements ReactAdapter { databaseNode.isLoading = false; useSelectedNode.getState().setSelectedNode(database); useCommandBar.getState().setContextButtons([]); - this.container.tabsManager.refreshActiveTab((tab: TabsBase) => tab.collection?.databaseId === database.id()); + useTabs.getState().refreshActiveTab((tab: TabsBase) => tab.collection?.databaseId === database.id()); }, onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(database), }; @@ -215,9 +218,7 @@ export class ResourceTreeAdapter implements ReactAdapter { isSelected: () => useSelectedNode .getState() - .isDataNodeSelected(this.container.tabsManager.activeTab(), database.id(), undefined, [ - ViewModels.CollectionTabKind.DatabaseSettings, - ]), + .isDataNodeSelected(database.id(), undefined, [ViewModels.CollectionTabKind.DatabaseSettings]), onClick: database.onSettingsClick.bind(database), }); } @@ -265,7 +266,7 @@ export class ResourceTreeAdapter implements ReactAdapter { isSelected: () => useSelectedNode .getState() - .isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id(), [ + .isDataNodeSelected(collection.databaseId, collection.id(), [ ViewModels.CollectionTabKind.Documents, ViewModels.CollectionTabKind.Graph, ]), @@ -283,9 +284,7 @@ export class ResourceTreeAdapter implements ReactAdapter { isSelected: () => useSelectedNode .getState() - .isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id(), [ - ViewModels.CollectionTabKind.SchemaAnalyzer, - ]), + .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.SchemaAnalyzer]), }); } @@ -296,9 +295,7 @@ export class ResourceTreeAdapter implements ReactAdapter { isSelected: () => useSelectedNode .getState() - .isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id(), [ - ViewModels.CollectionTabKind.Settings, - ]), + .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Settings]), }); } @@ -326,9 +323,7 @@ export class ResourceTreeAdapter implements ReactAdapter { isSelected: () => useSelectedNode .getState() - .isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id(), [ - ViewModels.CollectionTabKind.Conflicts, - ]), + .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Conflicts]), }); } @@ -343,10 +338,12 @@ export class ResourceTreeAdapter implements ReactAdapter { // Rewritten version of expandCollapseCollection useSelectedNode.getState().setSelectedNode(collection); useCommandBar.getState().setContextButtons([]); - this.container.tabsManager.refreshActiveTab( - (tab: TabsBase) => - tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId - ); + useTabs + .getState() + .refreshActiveTab( + (tab: TabsBase) => + tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId + ); }, onExpanded: () => { if (ResourceTreeAdapter.showScriptNodes(this.container)) { @@ -355,10 +352,7 @@ export class ResourceTreeAdapter implements ReactAdapter { collection.loadTriggers(); } }, - isSelected: () => - useSelectedNode - .getState() - .isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id()), + isSelected: () => useSelectedNode.getState().isDataNodeSelected(collection.databaseId, collection.id()), onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(collection), }; } @@ -372,17 +366,19 @@ export class ResourceTreeAdapter implements ReactAdapter { isSelected: () => useSelectedNode .getState() - .isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id(), [ + .isDataNodeSelected(collection.databaseId, collection.id(), [ ViewModels.CollectionTabKind.StoredProcedures, ]), contextMenu: ResourceTreeContextMenuButtonFactory.createStoreProcedureContextMenuItems(this.container, sp), })), onClick: () => { collection.selectedSubnodeKind(ViewModels.CollectionTabKind.StoredProcedures); - this.container.tabsManager.refreshActiveTab( - (tab: TabsBase) => - tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId - ); + useTabs + .getState() + .refreshActiveTab( + (tab: TabsBase) => + tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId + ); }, }; } @@ -396,7 +392,7 @@ export class ResourceTreeAdapter implements ReactAdapter { isSelected: () => useSelectedNode .getState() - .isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id(), [ + .isDataNodeSelected(collection.databaseId, collection.id(), [ ViewModels.CollectionTabKind.UserDefinedFunctions, ]), contextMenu: ResourceTreeContextMenuButtonFactory.createUserDefinedFunctionContextMenuItems( @@ -406,10 +402,12 @@ export class ResourceTreeAdapter implements ReactAdapter { })), onClick: () => { collection.selectedSubnodeKind(ViewModels.CollectionTabKind.UserDefinedFunctions); - this.container.tabsManager.refreshActiveTab( - (tab: TabsBase) => - tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId - ); + useTabs + .getState() + .refreshActiveTab( + (tab: TabsBase) => + tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId + ); }, }; } @@ -423,17 +421,17 @@ export class ResourceTreeAdapter implements ReactAdapter { isSelected: () => useSelectedNode .getState() - .isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id(), [ - ViewModels.CollectionTabKind.Triggers, - ]), + .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Triggers]), contextMenu: ResourceTreeContextMenuButtonFactory.createTriggerContextMenuItems(this.container, trigger), })), onClick: () => { collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Triggers); - this.container.tabsManager.refreshActiveTab( - (tab: TabsBase) => - tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId - ); + useTabs + .getState() + .refreshActiveTab( + (tab: TabsBase) => + tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId + ); }, }; } @@ -452,9 +450,7 @@ export class ResourceTreeAdapter implements ReactAdapter { children: this.getSchemaNodes(collection.schema.fields), onClick: () => { collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Schema); - this.container.tabsManager.refreshActiveTab( - (tab: TabsBase) => tab.collection && tab.collection.rid === collection.rid - ); + useTabs.getState().refreshActiveTab((tab: TabsBase) => tab.collection && tab.collection.rid === collection.rid); }, }; } @@ -584,7 +580,7 @@ export class ResourceTreeAdapter implements ReactAdapter { className: "notebookHeader galleryHeader", onClick: () => this.container.openGallery(), isSelected: () => { - const activeTab = this.container.tabsManager.activeTab(); + const activeTab = useTabs.getState().activeTab; return activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.Gallery; }, }; @@ -678,7 +674,7 @@ export class ResourceTreeAdapter implements ReactAdapter { className: "notebookHeader", onClick: () => onFileClick(item), isSelected: () => { - const activeTab = this.container.tabsManager.activeTab(); + const activeTab = useTabs.getState().activeTab; return ( activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && @@ -833,7 +829,7 @@ export class ResourceTreeAdapter implements ReactAdapter { } }, isSelected: () => { - const activeTab = this.container.tabsManager.activeTab(); + const activeTab = useTabs.getState().activeTab; return ( activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && diff --git a/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx b/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx index 59a867a83..49529f0d7 100644 --- a/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx @@ -3,6 +3,7 @@ import * as React from "react"; import CollectionIcon from "../../../images/tree-collection.svg"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; import * as ViewModels from "../../Contracts/ViewModels"; +import { useTabs } from "../../hooks/useTabs"; import { userContext } from "../../UserContext"; import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent"; import Explorer from "../Explorer"; @@ -24,7 +25,10 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter { (state) => state.resourceTokenCollection ); useSelectedNode.subscribe(() => this.triggerRender()); - this.container.tabsManager && this.container.tabsManager.activeTab.subscribe(() => this.triggerRender()); + useTabs.subscribe( + () => this.triggerRender(), + (state) => state.activeTab + ); this.triggerRender(); } @@ -55,9 +59,7 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter { isSelected: () => useSelectedNode .getState() - .isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id(), [ - ViewModels.CollectionTabKind.Documents, - ]), + .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Documents]), }); const collectionNode: TreeNode = { @@ -70,14 +72,13 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter { // Rewritten version of expandCollapseCollection useSelectedNode.getState().setSelectedNode(collection); useCommandBar.getState().setContextButtons([]); - this.container.tabsManager.refreshActiveTab( - (tab) => tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId - ); - }, - isSelected: () => - useSelectedNode + useTabs .getState() - .isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id()), + .refreshActiveTab( + (tab) => tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId + ); + }, + isSelected: () => useSelectedNode.getState().isDataNodeSelected(collection.databaseId, collection.id()), }; return { diff --git a/src/Explorer/Tree/StoredProcedure.ts b/src/Explorer/Tree/StoredProcedure.ts index 903e5d088..8e6f77ece 100644 --- a/src/Explorer/Tree/StoredProcedure.ts +++ b/src/Explorer/Tree/StoredProcedure.ts @@ -4,6 +4,7 @@ import * as Constants from "../../Common/Constants"; import { deleteStoredProcedure } from "../../Common/dataAccess/deleteStoredProcedure"; import { executeStoredProcedure } from "../../Common/dataAccess/executeStoredProcedure"; import * as ViewModels from "../../Contracts/ViewModels"; +import { useTabs } from "../../hooks/useTabs"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; @@ -62,7 +63,7 @@ export default class StoredProcedure { } public static create(source: ViewModels.Collection, event: MouseEvent) { - const id = source.container.tabsManager.getTabs(ViewModels.CollectionTabKind.StoredProcedures).length + 1; + const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.StoredProcedures).length + 1; const storedProcedure = { id: "", body: sampleStoredProcedureBody, @@ -84,7 +85,7 @@ export default class StoredProcedure { } ); - source.container.tabsManager.activateNewTab(storedProcedureTab); + useTabs.getState().activateNewTab(storedProcedureTab); } public select() { @@ -99,14 +100,16 @@ export default class StoredProcedure { public open = () => { this.select(); - const storedProcedureTabs: NewStoredProcedureTab[] = this.container.tabsManager.getTabs( - ViewModels.CollectionTabKind.StoredProcedures, - (tab: TabsBase) => tab.node && tab.node.rid === this.rid - ) as NewStoredProcedureTab[]; + const storedProcedureTabs: NewStoredProcedureTab[] = useTabs + .getState() + .getTabs( + ViewModels.CollectionTabKind.StoredProcedures, + (tab: TabsBase) => tab.node && tab.node.rid === this.rid + ) as NewStoredProcedureTab[]; let storedProcedureTab: NewStoredProcedureTab = storedProcedureTabs && storedProcedureTabs[0]; if (storedProcedureTab) { - this.container.tabsManager.activateTab(storedProcedureTab); + useTabs.getState().activateTab(storedProcedureTab); } else { const storedProcedureData = { _rid: this.rid, @@ -131,7 +134,7 @@ export default class StoredProcedure { } ); - this.container.tabsManager.activateNewTab(storedProcedureTab); + useTabs.getState().activateNewTab(storedProcedureTab); } }; public delete() { @@ -141,7 +144,7 @@ export default class StoredProcedure { deleteStoredProcedure(this.collection.databaseId, this.collection.id(), this.id()).then( () => { - this.container.tabsManager.closeTabsByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid); + useTabs.getState().closeTabsByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid); this.collection.children.remove(this); }, (reason) => {} @@ -149,10 +152,12 @@ export default class StoredProcedure { } public execute(params: string[], partitionKeyValue?: string): void { - const sprocTabs: NewStoredProcedureTab[] = this.container.tabsManager.getTabs( - ViewModels.CollectionTabKind.StoredProcedures, - (tab: TabsBase) => tab.node && tab.node.rid === this.rid - ) as NewStoredProcedureTab[]; + const sprocTabs: NewStoredProcedureTab[] = useTabs + .getState() + .getTabs( + ViewModels.CollectionTabKind.StoredProcedures, + (tab: TabsBase) => tab.node && tab.node.rid === this.rid + ) as NewStoredProcedureTab[]; const sprocTab: NewStoredProcedureTab = sprocTabs && sprocTabs.length > 0 && sprocTabs[0]; sprocTab.isExecuting(true); this.container && diff --git a/src/Explorer/Tree/Trigger.ts b/src/Explorer/Tree/Trigger.ts index 2aa44a17e..d90428b65 100644 --- a/src/Explorer/Tree/Trigger.ts +++ b/src/Explorer/Tree/Trigger.ts @@ -3,6 +3,7 @@ import * as ko from "knockout"; import * as Constants from "../../Common/Constants"; import { deleteTrigger } from "../../Common/dataAccess/deleteTrigger"; import * as ViewModels from "../../Contracts/ViewModels"; +import { useTabs } from "../../hooks/useTabs"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import Explorer from "../Explorer"; @@ -42,7 +43,7 @@ export default class Trigger { } public static create(source: ViewModels.Collection, event: MouseEvent) { - const id = source.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Triggers).length + 1; + const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Triggers).length + 1; const trigger = { id: "", body: "function trigger(){}", @@ -60,20 +61,19 @@ export default class Trigger { node: source, }); - source.container.tabsManager.activateNewTab(triggerTab); + useTabs.getState().activateNewTab(triggerTab); } public open = () => { this.select(); - const triggerTabs: TriggerTab[] = this.container.tabsManager.getTabs( - ViewModels.CollectionTabKind.Triggers, - (tab) => tab.node && tab.node.rid === this.rid - ) as TriggerTab[]; + const triggerTabs: TriggerTab[] = useTabs + .getState() + .getTabs(ViewModels.CollectionTabKind.Triggers, (tab) => tab.node && tab.node.rid === this.rid) as TriggerTab[]; let triggerTab: TriggerTab = triggerTabs && triggerTabs[0]; if (triggerTab) { - this.container.tabsManager.activateTab(triggerTab); + useTabs.getState().activateTab(triggerTab); } else { const triggerData = { _rid: this.rid, @@ -94,7 +94,7 @@ export default class Trigger { node: this, }); - this.container.tabsManager.activateNewTab(triggerTab); + useTabs.getState().activateNewTab(triggerTab); } }; @@ -105,7 +105,7 @@ export default class Trigger { deleteTrigger(this.collection.databaseId, this.collection.id(), this.id()).then( () => { - this.container.tabsManager.closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); + useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); this.collection.children.remove(this); }, (reason) => {} diff --git a/src/Explorer/Tree/UserDefinedFunction.ts b/src/Explorer/Tree/UserDefinedFunction.ts index ef4a5e180..2d2b64889 100644 --- a/src/Explorer/Tree/UserDefinedFunction.ts +++ b/src/Explorer/Tree/UserDefinedFunction.ts @@ -3,6 +3,7 @@ import * as ko from "knockout"; import * as Constants from "../../Common/Constants"; import { deleteUserDefinedFunction } from "../../Common/dataAccess/deleteUserDefinedFunction"; import * as ViewModels from "../../Contracts/ViewModels"; +import { useTabs } from "../../hooks/useTabs"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import Explorer from "../Explorer"; @@ -30,7 +31,7 @@ export default class UserDefinedFunction { } public static create(source: ViewModels.Collection, event: MouseEvent) { - const id = source.container.tabsManager.getTabs(ViewModels.CollectionTabKind.UserDefinedFunctions).length + 1; + const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.UserDefinedFunctions).length + 1; const userDefinedFunction = { id: "", body: "function userDefinedFunction(){}", @@ -46,20 +47,22 @@ export default class UserDefinedFunction { node: source, }); - source.container.tabsManager.activateNewTab(userDefinedFunctionTab); + useTabs.getState().activateNewTab(userDefinedFunctionTab); } public open = () => { this.select(); - const userDefinedFunctionTabs: UserDefinedFunctionTab[] = this.container.tabsManager.getTabs( - ViewModels.CollectionTabKind.UserDefinedFunctions, - (tab) => tab.node?.rid === this.rid - ) as UserDefinedFunctionTab[]; + const userDefinedFunctionTabs: UserDefinedFunctionTab[] = useTabs + .getState() + .getTabs( + ViewModels.CollectionTabKind.UserDefinedFunctions, + (tab) => tab.node?.rid === this.rid + ) as UserDefinedFunctionTab[]; let userDefinedFunctionTab: UserDefinedFunctionTab = userDefinedFunctionTabs && userDefinedFunctionTabs[0]; if (userDefinedFunctionTab) { - this.container.tabsManager.activateTab(userDefinedFunctionTab); + useTabs.getState().activateTab(userDefinedFunctionTab); } else { const userDefinedFunctionData = { _rid: this.rid, @@ -78,7 +81,7 @@ export default class UserDefinedFunction { node: this, }); - this.container.tabsManager.activateNewTab(userDefinedFunctionTab); + useTabs.getState().activateNewTab(userDefinedFunctionTab); } }; @@ -98,7 +101,7 @@ export default class UserDefinedFunction { deleteUserDefinedFunction(this.collection.databaseId, this.collection.id(), this.id()).then( () => { - this.container.tabsManager.closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); + useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); this.collection.children.remove(this); }, (reason) => {} diff --git a/src/Explorer/useDatabases.ts b/src/Explorer/useDatabases.ts index db16e5766..0a1f3ae45 100644 --- a/src/Explorer/useDatabases.ts +++ b/src/Explorer/useDatabases.ts @@ -2,6 +2,7 @@ import _ from "underscore"; import create, { UseStore } from "zustand"; import * as Constants from "../Common/Constants"; import * as ViewModels from "../Contracts/ViewModels"; +import { useSelectedNode } from "./useSelectedNode"; interface DatabasesState { databases: ViewModels.Database[]; @@ -17,6 +18,7 @@ interface DatabasesState { isLastCollection: () => boolean; loadDatabaseOffers: () => Promise; isFirstResourceCreated: () => boolean; + findSelectedDatabase: () => ViewModels.Database; } export const useDatabases: UseStore = create((set, get) => ({ @@ -112,4 +114,19 @@ export const useDatabases: UseStore = create((set, get) => ({ return false; }); }, + findSelectedDatabase: (): ViewModels.Database => { + const selectedNode = useSelectedNode.getState().selectedNode; + if (!selectedNode) { + return undefined; + } + if (selectedNode.nodeKind === "Database") { + return _.find(get().databases, (database: ViewModels.Database) => database.id() === selectedNode.id()); + } + + if (selectedNode.nodeKind === "Collection") { + return selectedNode.database; + } + + return selectedNode.collection?.database; + }, })); diff --git a/src/Explorer/useSelectedNode.ts b/src/Explorer/useSelectedNode.ts index 2e29a588e..15f953641 100644 --- a/src/Explorer/useSelectedNode.ts +++ b/src/Explorer/useSelectedNode.ts @@ -1,17 +1,13 @@ -import _ from "underscore"; import create, { UseStore } from "zustand"; import * as ViewModels from "../Contracts/ViewModels"; -import TabsBase from "./Tabs/TabsBase"; -import { useDatabases } from "./useDatabases"; +import { useTabs } from "../hooks/useTabs"; export interface SelectedNodeState { selectedNode: ViewModels.TreeNode; setSelectedNode: (node: ViewModels.TreeNode) => void; isDatabaseNodeOrNoneSelected: () => boolean; - findSelectedDatabase: () => ViewModels.Database; findSelectedCollection: () => ViewModels.Collection; isDataNodeSelected: ( - activeTab: TabsBase, databaseId: string, collectionId?: string, subnodeKinds?: ViewModels.CollectionTabKind[] @@ -25,30 +21,11 @@ export const useSelectedNode: UseStore = create((set, get) => const selectedNode = get().selectedNode; return !selectedNode || selectedNode.nodeKind === "Database"; }, - findSelectedDatabase: (): ViewModels.Database => { - const selectedNode = get().selectedNode; - if (!selectedNode) { - return undefined; - } - if (selectedNode.nodeKind === "Database") { - return _.find( - useDatabases.getState().databases, - (database: ViewModels.Database) => database.id() === selectedNode.id() - ); - } - - if (selectedNode.nodeKind === "Collection") { - return selectedNode.database; - } - - return selectedNode.collection?.database; - }, findSelectedCollection: (): ViewModels.Collection => { const selectedNode = get().selectedNode; return (selectedNode.nodeKind === "Collection" ? selectedNode : selectedNode.collection) as ViewModels.Collection; }, isDataNodeSelected: ( - activeTab: TabsBase, databaseId: string, collectionId?: string, subnodeKinds?: ViewModels.CollectionTabKind[] @@ -70,6 +47,7 @@ export const useSelectedNode: UseStore = create((set, get) => return true; } + const activeTab = useTabs.getState().activeTab; const selectedSubnodeKind = collectionId ? (selectedNode as ViewModels.Collection).selectedSubnodeKind() : (selectedNode as ViewModels.Database).selectedSubnodeKind(); diff --git a/src/GalleryViewer/GalleryViewer.tsx b/src/GalleryViewer/GalleryViewer.tsx index 07fcb733e..e630ee7b2 100644 --- a/src/GalleryViewer/GalleryViewer.tsx +++ b/src/GalleryViewer/GalleryViewer.tsx @@ -42,10 +42,10 @@ const onInit = async () => { practices, and how to get started with Azure Cosmos DB. - If you'd like to run or edit the notebook in your own Azure Cosmos DB account,{" "} + If {`you'd`} like to run or edit the notebook in your own Azure Cosmos DB account,{" "} sign in and select an account with{" "} notebooks enabled. From there, you can download the sample to your - account. If you don't have an account yet, you can{" "} + account. If you {`don't`} have an account yet, you can{" "} create one from the Azure portal. diff --git a/src/Main.tsx b/src/Main.tsx index 566317825..c85a39f10 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -34,7 +34,6 @@ import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less"; import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less"; import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less"; import "./Explorer/Controls/TreeComponent/treeComponent.less"; -import { ExplorerParams } from "./Explorer/Explorer"; import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less"; import "./Explorer/Menus/CommandBar/CommandBarComponent.less"; import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter"; @@ -56,14 +55,10 @@ initializeIcons(); const App: React.FunctionComponent = () => { const [isLeftPaneExpanded, setIsLeftPaneExpanded] = useState(true); - const { tabs, activeTab, tabsManager } = useTabs(); - - const explorerParams: ExplorerParams = { - tabsManager, - }; + const openedTabs = useTabs((state) => state.openedTabs); const config = useConfig(); - const explorer = useKnockoutExplorer(config?.platform, explorerParams); + const explorer = useKnockoutExplorer(config?.platform); const toggleLeftPaneExpanded = () => { setIsLeftPaneExpanded(!isLeftPaneExpanded); @@ -100,8 +95,8 @@ const App: React.FunctionComponent = () => { {/* Collections Tree - End */} - {tabs.length === 0 && } - + {openedTabs.length === 0 && } + {/* Collections Tree and Tabs - End */}
(); useEffect(() => { const effect = async () => { if (platform) { if (platform === Platform.Hosted) { - const explorer = await configureHosted(explorerParams); + const explorer = await configureHosted(); setExplorer(explorer); } else if (platform === Platform.Emulator) { - const explorer = configureEmulator(explorerParams); + const explorer = configureEmulator(); setExplorer(explorer); } else if (platform === Platform.Portal) { - const explorer = await configurePortal(explorerParams); + const explorer = await configurePortal(); setExplorer(explorer); } } @@ -67,21 +67,21 @@ export function useKnockoutExplorer(platform: Platform, explorerParams: Explorer return explorer; } -async function configureHosted(explorerParams: ExplorerParams): Promise { +async function configureHosted(): Promise { const win = (window as unknown) as HostedExplorerChildFrame; if (win.hostedConfig.authType === AuthType.EncryptedToken) { - return configureHostedWithEncryptedToken(win.hostedConfig, explorerParams); + return configureHostedWithEncryptedToken(win.hostedConfig); } else if (win.hostedConfig.authType === AuthType.ResourceToken) { - return configureHostedWithResourceToken(win.hostedConfig, explorerParams); + return configureHostedWithResourceToken(win.hostedConfig); } else if (win.hostedConfig.authType === AuthType.ConnectionString) { - return configureHostedWithConnectionString(win.hostedConfig, explorerParams); + return configureHostedWithConnectionString(win.hostedConfig); } else if (win.hostedConfig.authType === AuthType.AAD) { - return configureHostedWithAAD(win.hostedConfig, explorerParams); + return configureHostedWithAAD(win.hostedConfig); } throw new Error(`Unknown hosted config: ${win.hostedConfig}`); } -async function configureHostedWithAAD(config: AAD, explorerParams: ExplorerParams): Promise { +async function configureHostedWithAAD(config: AAD): Promise { // TODO: Refactor. updateUserContext needs to be called twice because listKeys below depends on userContext.authorizationToken updateUserContext({ authType: AuthType.AAD, @@ -120,11 +120,11 @@ async function configureHostedWithAAD(config: AAD, explorerParams: ExplorerParam databaseAccount: config.databaseAccount, masterKey: keys.primaryMasterKey, }); - const explorer = new Explorer(explorerParams); + const explorer = new Explorer(); return explorer; } -function configureHostedWithConnectionString(config: ConnectionString, explorerParams: ExplorerParams): Explorer { +function configureHostedWithConnectionString(config: ConnectionString): Explorer { const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind); const databaseAccount = { id: "", @@ -142,11 +142,11 @@ function configureHostedWithConnectionString(config: ConnectionString, explorerP databaseAccount, masterKey: config.masterKey, }); - const explorer = new Explorer(explorerParams); + const explorer = new Explorer(); return explorer; } -function configureHostedWithResourceToken(config: ResourceToken, explorerParams: ExplorerParams): Explorer { +function configureHostedWithResourceToken(config: ResourceToken): Explorer { const parsedResourceToken = parseResourceTokenConnectionString(config.resourceToken); const databaseAccount = { id: "", @@ -167,11 +167,11 @@ function configureHostedWithResourceToken(config: ResourceToken, explorerParams: partitionKey: parsedResourceToken.partitionKey, }, }); - const explorer = new Explorer(explorerParams); + const explorer = new Explorer(); return explorer; } -function configureHostedWithEncryptedToken(config: EncryptedToken, explorerParams: ExplorerParams): Explorer { +function configureHostedWithEncryptedToken(config: EncryptedToken): Explorer { const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind); updateUserContext({ authType: AuthType.EncryptedToken, @@ -185,20 +185,20 @@ function configureHostedWithEncryptedToken(config: EncryptedToken, explorerParam properties: getDatabaseAccountPropertiesFromMetadata(config.encryptedTokenMetadata), }, }); - const explorer = new Explorer(explorerParams); + const explorer = new Explorer(); return explorer; } -function configureEmulator(explorerParams: ExplorerParams): Explorer { +function configureEmulator(): Explorer { updateUserContext({ databaseAccount: emulatorAccount, authType: AuthType.MasterKey, }); - const explorer = new Explorer(explorerParams); + const explorer = new Explorer(); return explorer; } -async function configurePortal(explorerParams: ExplorerParams): Promise { +async function configurePortal(): Promise { updateUserContext({ authType: AuthType.AAD, }); @@ -214,7 +214,7 @@ async function configurePortal(explorerParams: ExplorerParams): Promise new TabsManager()); - const tabs = useObservable(tabsManager.openedTabs); - const activeTab = useObservable(tabsManager.activeTab); - - return { tabs, activeTab, tabsManager }; + activateTab: (tab: TabsBase) => void; + activateNewTab: (tab: TabsBase) => void; + updateTab: (tab: TabsBase) => void; + getTabs: (tabKind: ViewModels.CollectionTabKind, comparator?: (tab: TabsBase) => boolean) => TabsBase[]; + refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void; + closeTabsByComparator: (comparator: (tab: TabsBase) => boolean) => void; + closeTab: (tab: TabsBase) => void; } + +export const useTabs: UseStore = create((set, get) => ({ + openedTabs: [], + activeTab: undefined, + activateTab: (tab: TabsBase): void => { + if (get().openedTabs.some((openedTab) => openedTab.tabId === tab.tabId)) { + set({ activeTab: tab }); + tab.onActivate(); + } + }, + activateNewTab: (tab: TabsBase): void => { + set((state) => ({ openedTabs: [...state.openedTabs, tab], activeTab: tab })); + tab.onActivate(); + }, + updateTab: (tab: TabsBase) => { + if (get().activeTab?.tabId === tab.tabId) { + set({ activeTab: tab }); + } + + set((state) => ({ + openedTabs: state.openedTabs.map((openedTab) => { + if (openedTab.tabId === tab.tabId) { + return tab; + } + return openedTab; + }), + })); + }, + getTabs: (tabKind: ViewModels.CollectionTabKind, comparator?: (tab: TabsBase) => boolean): TabsBase[] => + get().openedTabs.filter((tab) => tab.tabKind === tabKind && (!comparator || comparator(tab))), + refreshActiveTab: (comparator: (tab: TabsBase) => boolean): void => { + // ensures that the tab selects/highlights the right node based on resource tree expand/collapse state + const activeTab = get().activeTab; + activeTab && comparator(activeTab) && activeTab.onActivate(); + }, + closeTabsByComparator: (comparator: (tab: TabsBase) => boolean): void => + get() + .openedTabs.filter(comparator) + .forEach((tab) => tab.onCloseTabButtonClick()), + closeTab: (tab: TabsBase): void => { + let tabIndex: number; + const { activeTab, openedTabs } = get(); + const updatedTabs = openedTabs.filter((openedTab, index) => { + if (tab.tabId === openedTab.tabId) { + tabIndex = index; + return false; + } + return true; + }); + if (updatedTabs.length === 0) { + set({ activeTab: undefined }); + } + + if (tab.tabId === activeTab.tabId && tabIndex !== -1) { + const tabToTheRight = updatedTabs[tabIndex]; + const lastOpenTab = updatedTabs[updatedTabs.length - 1]; + set({ activeTab: tabToTheRight || lastOpenTab }); + } + + set({ openedTabs: updatedTabs }); + }, +}));