Move tabs manager to zustand (#915)

This commit is contained in:
victor-meng 2021-07-08 21:32:22 -07:00 committed by GitHub
parent f4eef1b61b
commit f8ab0a82e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 609 additions and 663 deletions

View File

@ -50,7 +50,10 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
onClick: () => onClick: () =>
useSidePanel useSidePanel
.getState() .getState()
.openSidePanel("Delete " + getDatabaseName(), <DeleteDatabaseConfirmationPanel explorer={container} />), .openSidePanel(
"Delete " + getDatabaseName(),
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />
),
label: `Delete ${getDatabaseName()}`, label: `Delete ${getDatabaseName()}`,
styleClass: "deleteDatabaseMenuItem", styleClass: "deleteDatabaseMenuItem",
}); });
@ -126,7 +129,10 @@ export const createCollectionContextMenuButton = (
onClick: () => onClick: () =>
useSidePanel useSidePanel
.getState() .getState()
.openSidePanel("Delete " + getCollectionName(), <DeleteCollectionConfirmationPane explorer={container} />), .openSidePanel(
"Delete " + getCollectionName(),
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />
),
label: `Delete ${getCollectionName()}`, label: `Delete ${getCollectionName()}`,
styleClass: "deleteCollectionMenuItem", styleClass: "deleteCollectionMenuItem",
}); });

View File

@ -31,7 +31,6 @@ exports[`SettingsComponent renders 1`] = `
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isSchemaEnabled": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
@ -49,10 +48,6 @@ exports[`SettingsComponent renders 1`] = `
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
}, },
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
},
}, },
"databaseId": "test", "databaseId": "test",
"defaultTtl": [Function], "defaultTtl": [Function],
@ -107,7 +102,6 @@ exports[`SettingsComponent renders 1`] = `
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isSchemaEnabled": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
@ -125,10 +119,6 @@ exports[`SettingsComponent renders 1`] = `
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
}, },
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
},
}, },
"databaseId": "test", "databaseId": "test",
"defaultTtl": [Function], "defaultTtl": [Function],

View File

@ -1,6 +1,5 @@
import { IChoiceGroupProps } from "@fluentui/react"; import { IChoiceGroupProps } from "@fluentui/react";
import * as ko from "knockout"; import * as ko from "knockout";
import Q from "q";
import React from "react"; import React from "react";
import _ from "underscore"; import _ from "underscore";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
@ -17,6 +16,7 @@ import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService"; import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
import { useSidePanel } from "../hooks/useSidePanel"; import { useSidePanel } from "../hooks/useSidePanel";
import { useTabs } from "../hooks/useTabs";
import { IGalleryItem, JunoClient } from "../Juno/JunoClient"; import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
import { ExplorerSettings } from "../Shared/ExplorerSettings"; import { ExplorerSettings } from "../Shared/ExplorerSettings";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
@ -59,7 +59,7 @@ import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane"; import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import { TabsManager } from "./Tabs/TabsManager"; import TabsBase from "./Tabs/TabsBase";
import TerminalTab from "./Tabs/TerminalTab"; import TerminalTab from "./Tabs/TerminalTab";
import Database from "./Tree/Database"; import Database from "./Tree/Database";
import ResourceTokenCollection from "./Tree/ResourceTokenCollection"; import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
@ -73,10 +73,6 @@ BindingHandlersRegisterer.registerBindingHandlers();
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import // Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
var tmp = ComponentRegisterer; var tmp = ComponentRegisterer;
export interface ExplorerParams {
tabsManager: TabsManager;
}
export default class Explorer { export default class Explorer {
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>; public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
public queriesClient: QueriesClient; public queriesClient: QueriesClient;
@ -90,10 +86,8 @@ export default class Explorer {
// Tabs // Tabs
public isTabsContentExpanded: ko.Observable<boolean>; public isTabsContentExpanded: ko.Observable<boolean>;
public tabsManager: TabsManager;
public gitHubOAuthService: GitHubOAuthService; public gitHubOAuthService: GitHubOAuthService;
public isSchemaEnabled: ko.Computed<boolean>;
// Notebooks // Notebooks
public notebookManager?: NotebookManager; public notebookManager?: NotebookManager;
@ -106,7 +100,7 @@ export default class Explorer {
private static readonly MaxNbDatabasesToAutoExpand = 5; private static readonly MaxNbDatabasesToAutoExpand = 5;
constructor(params?: ExplorerParams) { constructor() {
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
@ -117,7 +111,6 @@ export default class Explorer {
); );
this.queriesClient = new QueriesClient(this); this.queriesClient = new QueriesClient(this);
this.isSchemaEnabled = ko.computed<boolean>(() => userContext.features.enableSchema);
useSelectedNode.subscribe(() => { useSelectedNode.subscribe(() => {
// Make sure switching tabs restores tabs display // Make sure switching tabs restores tabs display
@ -136,13 +129,15 @@ export default class Explorer {
return isCapabilityEnabled("EnableMongo"); return isCapabilityEnabled("EnableMongo");
}); });
this.tabsManager = params?.tabsManager ?? new TabsManager(); useTabs.subscribe(
this.tabsManager.openedTabs.subscribe((tabs) => { (openedTabs: TabsBase[]) => {
if (tabs.length === 0) { if (openedTabs.length === 0) {
useSelectedNode.getState().setSelectedNode(undefined); useSelectedNode.getState().setSelectedNode(undefined);
useCommandBar.getState().setContextButtons([]); useCommandBar.getState().setContextButtons([]);
} }
}); },
(state) => state.openedTabs
);
this.isTabsContentExpanded = ko.observable(false); this.isTabsContentExpanded = ko.observable(false);
@ -283,35 +278,26 @@ export default class Explorer {
// TODO: return result // TODO: return result
} }
public refreshDatabaseForResourceToken(): Promise<void> { public async refreshDatabaseForResourceToken(): Promise<void> {
const databaseId = userContext.parsedResourceToken?.databaseId; const databaseId = userContext.parsedResourceToken?.databaseId;
const collectionId = userContext.parsedResourceToken?.collectionId; const collectionId = userContext.parsedResourceToken?.collectionId;
if (!databaseId || !collectionId) { if (!databaseId || !collectionId) {
return Promise.reject(); return;
} }
return readCollection(databaseId, collectionId).then((collection: DataModels.Collection) => { const collection: DataModels.Collection = await readCollection(databaseId, collectionId);
const resourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection); const resourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection);
useDatabases.setState({ resourceTokenCollection }); useDatabases.setState({ resourceTokenCollection });
useSelectedNode.getState().setSelectedNode(resourceTokenCollection); useSelectedNode.getState().setSelectedNode(resourceTokenCollection);
});
} }
public refreshAllDatabases(isInitialLoad?: boolean): Q.Promise<any> { public async refreshAllDatabases(): Promise<void> {
const startKey: number = TelemetryProcessor.traceStart(Action.LoadDatabases, { const startKey: number = TelemetryProcessor.traceStart(Action.LoadDatabases, {
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
let resourceTreeStartKey: number = null;
if (isInitialLoad) {
resourceTreeStartKey = TelemetryProcessor.traceStart(Action.LoadResourceTree, {
dataExplorerArea: Constants.Areas.ResourceTree,
});
}
// TODO: Refactor try {
const deferred: Q.Deferred<any> = Q.defer(); const databases: DataModels.Database[] = await readDatabases();
readDatabases().then(
(databases: DataModels.Database[]) => {
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.LoadDatabases, Action.LoadDatabases,
{ {
@ -319,20 +305,17 @@ export default class Explorer {
}, },
startKey startKey
); );
const deltaDatabases = this.getDeltaDatabases(databases); const currentDatabases = useDatabases.getState().databases;
this.addDatabasesToList(deltaDatabases.toAdd); const deltaDatabases = this.getDeltaDatabases(databases, currentDatabases);
this.deleteDatabasesFromList(deltaDatabases.toDelete); let updatedDatabases = currentDatabases.filter(
this.refreshAndExpandNewDatabases(deltaDatabases.toAdd).then( (database) => !deltaDatabases.toDelete.some((deletedDatabase) => deletedDatabase.id() === database.id())
() => {
deferred.resolve();
},
(reason) => {
deferred.reject(reason);
}
); );
}, updatedDatabases = [...updatedDatabases, ...deltaDatabases.toAdd].sort((db1, db2) =>
(error) => { db1.id().localeCompare(db2.id())
deferred.reject(error); );
useDatabases.setState({ databases: updatedDatabases });
await this.refreshAndExpandNewDatabases(deltaDatabases.toAdd, currentDatabases);
} catch (error) {
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.LoadDatabases, Action.LoadDatabases,
@ -345,34 +328,6 @@ export default class Explorer {
); );
logConsoleError(`Error while refreshing databases: ${errorMessage}`); 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 => { public onRefreshDatabasesKeyPress = (source: any, event: KeyboardEvent): boolean => {
@ -513,69 +468,13 @@ export default class Explorer {
} }
}; };
private refreshAndExpandNewDatabases(newDatabases: ViewModels.Database[]): Q.Promise<void> {
// we reload collections for all databases so the resource tree reflects any collection-level changes
// i.e addition of stored procedures, etc.
const deferred: Q.Deferred<void> = Q.defer<void>();
let loadCollectionPromises: Q.Promise<void>[] = [];
// If the user has a lot of databases, only load expanded databases.
const databases = useDatabases.getState().databases;
const databasesToLoad =
databases.length <= Explorer.MaxNbDatabasesToAutoExpand
? databases
: databases.filter((db) => db.isDatabaseExpanded() || db.id() === Constants.SavedQueries.DatabaseName);
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( private getDeltaDatabases(
updatedDatabaseList: DataModels.Database[] updatedDatabaseList: DataModels.Database[],
databases: ViewModels.Database[]
): { ): {
toAdd: ViewModels.Database[]; toAdd: ViewModels.Database[];
toDelete: ViewModels.Database[]; toDelete: ViewModels.Database[];
} { } {
const databases = useDatabases.getState().databases;
const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => { const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => {
const databaseExists = _.some( const databaseExists = _.some(
databases, databases,
@ -587,8 +486,8 @@ export default class Explorer {
(newDatabase: DataModels.Database) => new Database(this, newDatabase) (newDatabase: DataModels.Database) => new Database(this, newDatabase)
); );
let databasesToDelete: ViewModels.Database[] = []; const databasesToDelete: ViewModels.Database[] = [];
ko.utils.arrayForEach(databases, (database: ViewModels.Database) => { databases.forEach((database: ViewModels.Database) => {
const databasePresentInUpdatedList = _.some( const databasePresentInUpdatedList = _.some(
updatedDatabaseList, updatedDatabaseList,
(db: DataModels.Database) => db.id === database.id() (db: DataModels.Database) => db.id === database.id()
@ -601,13 +500,58 @@ export default class Explorer {
return { toAdd: databasesToAdd, toDelete: databasesToDelete }; return { toAdd: databasesToAdd, toDelete: databasesToDelete };
} }
private addDatabasesToList(databases: ViewModels.Database[]): void { private async refreshAndExpandNewDatabases(
useDatabases.getState().addDatabases(databases); newDatabases: ViewModels.Database[],
databases: ViewModels.Database[]
): Promise<void> {
// 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 { private _initSettings() {
const deleteDatabase = useDatabases.getState().deleteDatabase; if (!ExplorerSettings.hasSettingsDefined()) {
databasesToRemove.forEach((database) => deleteDatabase(database)); ExplorerSettings.createDefaultSettings();
}
} }
public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> { public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
@ -750,7 +694,9 @@ export default class Explorer {
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`); throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
} }
const notebookTabs = this.tabsManager.getTabs( const notebookTabs = useTabs
.getState()
.getTabs(
ViewModels.CollectionTabKind.NotebookV2, ViewModels.CollectionTabKind.NotebookV2,
(tab) => (tab) =>
(tab as NotebookV2Tab).notebookPath && (tab as NotebookV2Tab).notebookPath &&
@ -759,7 +705,7 @@ export default class Explorer {
let notebookTab = notebookTabs && notebookTabs[0]; let notebookTab = notebookTabs && notebookTabs[0];
if (notebookTab) { if (notebookTab) {
this.tabsManager.activateTab(notebookTab); useTabs.getState().activateTab(notebookTab);
} else { } else {
const options: NotebookTabOptions = { const options: NotebookTabOptions = {
account: userContext.databaseAccount, account: userContext.databaseAccount,
@ -778,7 +724,7 @@ export default class Explorer {
try { try {
const NotebookTabV2 = await import(/* webpackChunkName: "NotebookV2Tab" */ "./Tabs/NotebookV2Tab"); const NotebookTabV2 = await import(/* webpackChunkName: "NotebookV2Tab" */ "./Tabs/NotebookV2Tab");
notebookTab = new NotebookTabV2.default(options); notebookTab = new NotebookTabV2.default(options);
this.tabsManager.activateNewTab(notebookTab); useTabs.getState().activateNewTab(notebookTab);
} catch (reason) { } catch (reason) {
console.error("Import NotebookV2Tab failed!", reason); console.error("Import NotebookV2Tab failed!", reason);
return false; return false;
@ -796,19 +742,17 @@ export default class Explorer {
} }
// Don't delete if tab is open to avoid accidental deletion // Don't delete if tab is open to avoid accidental deletion
const openedNotebookTabs = this.tabsManager.getTabs( const openedNotebookTabs = useTabs
ViewModels.CollectionTabKind.NotebookV2, .getState()
(tab: NotebookV2Tab) => { .getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab: NotebookV2Tab) => {
return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path); return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path);
} });
);
if (openedNotebookTabs.length > 0) { if (openedNotebookTabs.length > 0) {
this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again."); this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again.");
} else { } else {
useSidePanel.getState().openSidePanel( useSidePanel.getState().openSidePanel(
"Rename Notebook", "Rename Notebook",
<StringInputPane <StringInputPane
explorer={this}
closePanel={() => { closePanel={() => {
useSidePanel.getState().closeSidePanel(); useSidePanel.getState().closeSidePanel();
this.resourceTree.triggerRender(); this.resourceTree.triggerRender();
@ -839,7 +783,6 @@ export default class Explorer {
useSidePanel.getState().openSidePanel( useSidePanel.getState().openSidePanel(
"Create new directory", "Create new directory",
<StringInputPane <StringInputPane
explorer={this}
closePanel={() => { closePanel={() => {
useSidePanel.getState().closeSidePanel(); useSidePanel.getState().closeSidePanel();
this.resourceTree.triggerRender(); this.resourceTree.triggerRender();
@ -927,12 +870,11 @@ export default class Explorer {
} }
// Don't delete if tab is open to avoid accidental deletion // Don't delete if tab is open to avoid accidental deletion
const openedNotebookTabs = this.tabsManager.getTabs( const openedNotebookTabs = useTabs
ViewModels.CollectionTabKind.NotebookV2, .getState()
(tab: NotebookV2Tab) => { .getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab: NotebookV2Tab) => {
return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path); return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path);
} });
);
if (openedNotebookTabs.length > 0) { if (openedNotebookTabs.length > 0) {
this.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again."); this.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again.");
return Promise.reject(); return Promise.reject();
@ -1034,10 +976,9 @@ export default class Explorer {
throw new Error("Terminal kind: ${kind} not supported"); throw new Error("Terminal kind: ${kind} not supported");
} }
const terminalTabs: TerminalTab[] = this.tabsManager.getTabs( const terminalTabs: TerminalTab[] = useTabs
ViewModels.CollectionTabKind.Terminal, .getState()
(tab) => tab.tabTitle() === title .getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => tab.tabTitle() === title) as TerminalTab[];
) as TerminalTab[];
let index = 1; let index = 1;
if (terminalTabs.length > 0) { if (terminalTabs.length > 0) {
@ -1058,7 +999,7 @@ export default class Explorer {
index: index, index: index,
}); });
this.tabsManager.activateNewTab(newTab); useTabs.getState().activateNewTab(newTab);
} }
public async openGallery( public async openGallery(
@ -1069,14 +1010,15 @@ export default class Explorer {
) { ) {
const title = "Gallery"; const title = "Gallery";
const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default; const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default;
const galleryTab = this.tabsManager const galleryTab = useTabs
.getState()
.getTabs(ViewModels.CollectionTabKind.Gallery) .getTabs(ViewModels.CollectionTabKind.Gallery)
.find((tab) => tab.tabTitle() == title); .find((tab) => tab.tabTitle() == title);
if (galleryTab instanceof GalleryTab) { if (galleryTab instanceof GalleryTab) {
this.tabsManager.activateTab(galleryTab); useTabs.getState().activateTab(galleryTab);
} else { } else {
this.tabsManager.activateNewTab( useTabs.getState().activateNewTab(
new GalleryTab( new GalleryTab(
{ {
tabKind: ViewModels.CollectionTabKind.Gallery, tabKind: ViewModels.CollectionTabKind.Gallery,
@ -1116,7 +1058,7 @@ export default class Explorer {
} }
private refreshCommandBarButtons(): void { private refreshCommandBarButtons(): void {
const activeTab = this.tabsManager.activeTab(); const activeTab = useTabs.getState().activeTab;
if (activeTab) { if (activeTab) {
activeTab.onActivate(); // TODO only update tabs buttons? activeTab.onActivate(); // TODO only update tabs buttons?
} else { } else {
@ -1208,7 +1150,7 @@ export default class Explorer {
public async refreshExplorer(): Promise<void> { public async refreshExplorer(): Promise<void> {
userContext.authType === AuthType.ResourceToken userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken() ? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases(true); : this.refreshAllDatabases();
await useNotebook.getState().refreshNotebooksEnabledStateForAccount(); await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
const isNotebookEnabled: boolean = const isNotebookEnabled: boolean =
userContext.authType !== AuthType.ResourceToken && userContext.authType !== AuthType.ResourceToken &&

View File

@ -8,6 +8,7 @@ import * as React from "react";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import { StyleConstants } from "../../../Common/Constants"; import { StyleConstants } from "../../../Common/Constants";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
@ -53,7 +54,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor); const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true)); 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")); uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker"));
} }

View File

@ -34,6 +34,7 @@ import {
import { webSocket } from "rxjs/webSocket"; import { webSocket } from "rxjs/webSocket";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import { Areas } from "../../../Common/Constants"; import { Areas } from "../../../Common/Constants";
import { useTabs } from "../../../hooks/useTabs";
import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
@ -776,7 +777,9 @@ const closeUnsupportedMimetypesEpic = (
if (explorer && !TextFile.handles(mimetype)) { if (explorer && !TextFile.handles(mimetype)) {
const filepath = action.payload.filepath; const filepath = action.payload.filepath;
// Close tab and show error message // Close tab and show error message
explorer.tabsManager.closeTabsByComparator( useTabs
.getState()
.closeTabsByComparator(
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath) (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.`; const msg = `${filepath} cannot be rendered. Please download the file, in order to view it outside of Data Explorer.`;
@ -804,7 +807,9 @@ const closeContentFailedToFetchEpic = (
if (explorer) { if (explorer) {
const filepath = action.payload.filepath; const filepath = action.payload.filepath;
// Close tab and show error message // Close tab and show error message
explorer.tabsManager.closeTabsByComparator( useTabs
.getState()
.closeTabsByComparator(
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath) (tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
); );
const msg = `Failed to load file: ${filepath}.`; const msg = `Failed to load file: ${filepath}.`;

View File

@ -4,6 +4,7 @@ import { logError } from "../../../Common/Logger";
import { Query } from "../../../Contracts/DataModels"; import { Query } from "../../../Contracts/DataModels";
import { Collection } from "../../../Contracts/ViewModels"; import { Collection } from "../../../Contracts/ViewModels";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { useSidePanel } from "../../../hooks/useSidePanel";
import { useTabs } from "../../../hooks/useTabs";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { trace } from "../../../Shared/Telemetry/TelemetryProcessor"; import { trace } from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
@ -36,7 +37,7 @@ export const BrowseQueriesPane: FunctionComponent<BrowseQueriesPaneProps> = ({
selectedCollection.onNewQueryClick(selectedCollection, undefined, savedQuery.query); 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.tabTitle(savedQuery.queryName);
queryTab.tabPath(`${selectedCollection.databaseId}>${selectedCollection.id()}>${savedQuery.queryName}`); queryTab.tabPath(`${selectedCollection.databaseId}>${selectedCollection.id()}>${savedQuery.queryName}`);

View File

@ -10,7 +10,6 @@ import { Collection, Database } from "../../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { updateUserContext } from "../../../UserContext"; import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer";
import { useDatabases } from "../../useDatabases"; import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
import { DeleteCollectionConfirmationPane } from "./DeleteCollectionConfirmationPane"; import { DeleteCollectionConfirmationPane } from "./DeleteCollectionConfirmationPane";
@ -53,10 +52,7 @@ describe("Delete Collection Confirmation Pane", () => {
describe("shouldRecordFeedback()", () => { describe("shouldRecordFeedback()", () => {
it("should return true if last collection and database does not have shared throughput else false", () => { it("should return true if last collection and database does not have shared throughput else false", () => {
const fakeExplorer = new Explorer(); const wrapper = shallow(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />);
fakeExplorer.refreshAllDatabases = () => undefined;
const wrapper = shallow(<DeleteCollectionConfirmationPane explorer={fakeExplorer} />);
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false); expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
const database = { id: ko.observable("testDB") } as Database; const database = { id: ko.observable("testDB") } as Database;
@ -65,11 +61,11 @@ describe("Delete Collection Confirmation Pane", () => {
database.isDatabaseShared = ko.computed(() => false); database.isDatabaseShared = ko.computed(() => false);
useDatabases.getState().addDatabases([database]); useDatabases.getState().addDatabases([database]);
useSelectedNode.getState().setSelectedNode(database); useSelectedNode.getState().setSelectedNode(database);
wrapper.setProps({ explorer: fakeExplorer }); wrapper.setProps({});
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true); expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true);
database.isDatabaseShared = ko.computed(() => true); database.isDatabaseShared = ko.computed(() => true);
wrapper.setProps({ explorer: fakeExplorer }); wrapper.setProps({});
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false); expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
}); });
}); });
@ -77,8 +73,6 @@ describe("Delete Collection Confirmation Pane", () => {
describe("submit()", () => { describe("submit()", () => {
const selectedCollectionId = "testCol"; const selectedCollectionId = "testCol";
const databaseId = "testDatabase"; const databaseId = "testDatabase";
const fakeExplorer = {} as Explorer;
fakeExplorer.refreshAllDatabases = () => undefined;
const database = { id: ko.observable(databaseId) } as Database; const database = { id: ko.observable(databaseId) } as Database;
const collection = { const collection = {
id: ko.observable(selectedCollectionId), id: ko.observable(selectedCollectionId),
@ -115,7 +109,7 @@ describe("Delete Collection Confirmation Pane", () => {
}); });
it("should call delete collection", () => { it("should call delete collection", () => {
const wrapper = mount(<DeleteCollectionConfirmationPane explorer={fakeExplorer} />); const wrapper = mount(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#confirmCollectionId")).toBe(true); expect(wrapper.exists("#confirmCollectionId")).toBe(true);
@ -132,7 +126,7 @@ describe("Delete Collection Confirmation Pane", () => {
}); });
it("should record feedback", async () => { it("should record feedback", async () => {
const wrapper = mount(<DeleteCollectionConfirmationPane explorer={fakeExplorer} />); const wrapper = mount(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />);
expect(wrapper.exists("#confirmCollectionId")).toBe(true); expect(wrapper.exists("#confirmCollectionId")).toBe(true);
wrapper wrapper
.find("#confirmCollectionId") .find("#confirmCollectionId")

View File

@ -6,23 +6,23 @@ import DeleteFeedback from "../../../Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { Collection } from "../../../Contracts/ViewModels"; import { Collection } from "../../../Contracts/ViewModels";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { useSidePanel } from "../../../hooks/useSidePanel";
import { useTabs } from "../../../hooks/useTabs";
import { DefaultExperienceUtility } from "../../../Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "../../../Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import { getCollectionName } from "../../../Utils/APITypeUtils"; import { getCollectionName } from "../../../Utils/APITypeUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import { useDatabases } from "../../useDatabases"; import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export interface DeleteCollectionConfirmationPaneProps { export interface DeleteCollectionConfirmationPaneProps {
explorer: Explorer; refreshDatabases: () => Promise<void>;
} }
export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectionConfirmationPaneProps> = ({ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectionConfirmationPaneProps> = ({
explorer, refreshDatabases,
}: DeleteCollectionConfirmationPaneProps) => { }: DeleteCollectionConfirmationPaneProps) => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel); const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>(""); const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>("");
@ -31,8 +31,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
const [isExecuting, setIsExecuting] = useState(false); const [isExecuting, setIsExecuting] = useState(false);
const shouldRecordFeedback = (): boolean => const shouldRecordFeedback = (): boolean =>
useDatabases.getState().isLastCollection() && useDatabases.getState().isLastCollection() && !useDatabases.getState().findSelectedDatabase()?.isDatabaseShared();
!useSelectedNode.getState().findSelectedDatabase()?.isDatabaseShared();
const collectionName = getCollectionName().toLocaleLowerCase(); const collectionName = getCollectionName().toLocaleLowerCase();
const paneTitle = "Delete " + collectionName; const paneTitle = "Delete " + collectionName;
@ -63,10 +62,12 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
setIsExecuting(false); setIsExecuting(false);
useSelectedNode.getState().setSelectedNode(collection.database); useSelectedNode.getState().setSelectedNode(collection.database);
explorer.tabsManager?.closeTabsByComparator( useTabs
.getState()
.closeTabsByComparator(
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId (tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
); );
explorer.refreshAllDatabases(); refreshDatabases();
TelemetryProcessor.traceSuccess(Action.DeleteCollection, paneInfo, startKey); TelemetryProcessor.traceSuccess(Action.DeleteCollection, paneInfo, startKey);

View File

@ -2,11 +2,7 @@
exports[`Delete Collection Confirmation Pane submit() should call delete collection 1`] = ` exports[`Delete Collection Confirmation Pane submit() should call delete collection 1`] = `
<DeleteCollectionConfirmationPane <DeleteCollectionConfirmationPane
explorer={ refreshDatabases={[Function]}
Object {
"refreshAllDatabases": [Function],
}
}
> >
<RightPaneForm <RightPaneForm
formError="" formError=""

View File

@ -10,15 +10,12 @@ import { Collection, Database } from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { updateUserContext } from "../../UserContext"; import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer";
import { TabsManager } from "../Tabs/TabsManager";
import { useDatabases } from "../useDatabases"; import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
import { DeleteDatabaseConfirmationPanel } from "./DeleteDatabaseConfirmationPanel"; import { DeleteDatabaseConfirmationPanel } from "./DeleteDatabaseConfirmationPanel";
describe("Delete Database Confirmation Pane", () => { describe("Delete Database Confirmation Pane", () => {
const selectedDatabaseId = "testDatabase"; const selectedDatabaseId = "testDatabase";
let fakeExplorer: Explorer;
let database: Database; let database: Database;
beforeAll(() => { beforeAll(() => {
@ -37,10 +34,6 @@ describe("Delete Database Confirmation Pane", () => {
}); });
beforeEach(() => { beforeEach(() => {
fakeExplorer = {} as Explorer;
fakeExplorer.refreshAllDatabases = () => undefined;
fakeExplorer.tabsManager = new TabsManager();
database = {} as Database; database = {} as Database;
database.collections = ko.observableArray<Collection>([{ id: ko.observable("testCollection") } as Collection]); database.collections = ko.observableArray<Collection>([{ id: ko.observable("testCollection") } as Collection]);
database.id = ko.observable<string>(selectedDatabaseId); database.id = ko.observable<string>(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", () => { it("shouldRecordFeedback() should return true if last non empty database or is last database that has shared throughput", () => {
const wrapper = shallow(<DeleteDatabaseConfirmationPanel explorer={fakeExplorer} />); const wrapper = shallow(<DeleteDatabaseConfirmationPanel refreshDatabases={() => undefined} />);
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(true); expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(true);
useDatabases.getState().addDatabases([database]); useDatabases.getState().addDatabases([database]);
wrapper.setProps({ explorer: fakeExplorer }); wrapper.setProps({});
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false); expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false);
useDatabases.getState().clearDatabases(); useDatabases.getState().clearDatabases();
}); });
it("Should call delete database", () => { it("Should call delete database", () => {
const wrapper = mount(<DeleteDatabaseConfirmationPanel explorer={fakeExplorer} />); const wrapper = mount(<DeleteDatabaseConfirmationPanel refreshDatabases={() => undefined} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#confirmDatabaseId")).toBe(true); expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
@ -81,7 +74,7 @@ describe("Delete Database Confirmation Pane", () => {
}); });
it("should record feedback", async () => { it("should record feedback", async () => {
const wrapper = mount(<DeleteDatabaseConfirmationPanel explorer={fakeExplorer} />); const wrapper = mount(<DeleteDatabaseConfirmationPanel refreshDatabases={() => undefined} />);
expect(wrapper.exists("#confirmDatabaseId")).toBe(true); expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
wrapper wrapper
.find("#confirmDatabaseId") .find("#confirmDatabaseId")

View File

@ -7,23 +7,23 @@ import DeleteFeedback from "../../Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { Collection, Database } from "../../Contracts/ViewModels"; import { Collection, Database } from "../../Contracts/ViewModels";
import { useSidePanel } from "../../hooks/useSidePanel"; import { useSidePanel } from "../../hooks/useSidePanel";
import { useTabs } from "../../hooks/useTabs";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { logConsoleError } from "../../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases"; import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent"; import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent";
import { RightPaneForm, RightPaneFormProps } from "./RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "./RightPaneForm/RightPaneForm";
interface DeleteDatabaseConfirmationPanelProps { interface DeleteDatabaseConfirmationPanelProps {
explorer: Explorer; refreshDatabases: () => Promise<void>;
} }
export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseConfirmationPanelProps> = ({ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseConfirmationPanelProps> = ({
explorer, refreshDatabases,
}: DeleteDatabaseConfirmationPanelProps): JSX.Element => { }: DeleteDatabaseConfirmationPanelProps): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel); const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const isLastNonEmptyDatabase = useDatabases((state) => state.isLastNonEmptyDatabase); const isLastNonEmptyDatabase = useDatabases((state) => state.isLastNonEmptyDatabase);
@ -32,7 +32,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
const [formError, setFormError] = useState<string>(""); const [formError, setFormError] = useState<string>("");
const [databaseInput, setDatabaseInput] = useState<string>(""); const [databaseInput, setDatabaseInput] = useState<string>("");
const [databaseFeedbackInput, setDatabaseFeedbackInput] = useState<string>(""); const [databaseFeedbackInput, setDatabaseFeedbackInput] = useState<string>("");
const selectedDatabase: Database = useSelectedNode.getState().findSelectedDatabase(); const selectedDatabase: Database = useDatabases.getState().findSelectedDatabase();
const submit = async (): Promise<void> => { const submit = async (): Promise<void> => {
if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) { if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) {
@ -52,14 +52,17 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
try { try {
await deleteDatabase(selectedDatabase.id()); await deleteDatabase(selectedDatabase.id());
closeSidePanel(); closeSidePanel();
explorer.refreshAllDatabases(); refreshDatabases();
explorer.tabsManager.closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id()); useTabs.getState().closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id());
useSelectedNode.getState().setSelectedNode(undefined); useSelectedNode.getState().setSelectedNode(undefined);
selectedDatabase selectedDatabase
.collections() .collections()
.forEach((collection: Collection) => .forEach((collection: Collection) =>
explorer.tabsManager.closeTabsByComparator( useTabs
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId .getState()
.closeTabsByComparator(
(tab) =>
tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
) )
); );
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(

View File

@ -20,7 +20,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isSchemaEnabled": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
@ -38,10 +37,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
}, },
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
},
}, },
"getRepo": [Function], "getRepo": [Function],
"pinRepo": [Function], "pinRepo": [Function],

View File

@ -5,6 +5,7 @@ import { Areas, SavedQueries } from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { Query } from "../../../Contracts/DataModels"; import { Query } from "../../../Contracts/DataModels";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { useSidePanel } from "../../../hooks/useSidePanel";
import { useTabs } from "../../../hooks/useTabs";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor"; import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
@ -34,7 +35,7 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({ explorer
logConsoleError("Failed to save query: account not setup to save queries"); 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(); const query: string = queryTab && queryTab.iTabAccessor.onSaveClickEvent();
if (!queryName || queryName.length === 0) { if (!queryName || queryName.length === 0) {

View File

@ -1,15 +1,14 @@
import { TextField } from "@fluentui/react"; import { TextField } from "@fluentui/react";
import React, { FormEvent, FunctionComponent, useState } from "react"; import React, { FormEvent, FunctionComponent, useState } from "react";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import * as FileSystemUtil from "../../Notebook/FileSystemUtil"; import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
import { NotebookContentItem } from "../../Notebook/NotebookContentItem"; import { NotebookContentItem } from "../../Notebook/NotebookContentItem";
import NotebookV2Tab from "../../Tabs/NotebookV2Tab"; import NotebookV2Tab from "../../Tabs/NotebookV2Tab";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export interface StringInputPanelProps { export interface StringInputPanelProps {
explorer: Explorer;
closePanel: () => void; closePanel: () => void;
errorMessage: string; errorMessage: string;
inProgressMessage: string; inProgressMessage: string;
@ -23,7 +22,6 @@ export interface StringInputPanelProps {
} }
export const StringInputPane: FunctionComponent<StringInputPanelProps> = ({ export const StringInputPane: FunctionComponent<StringInputPanelProps> = ({
explorer: container,
closePanel, closePanel,
errorMessage, errorMessage,
inProgressMessage, inProgressMessage,
@ -55,7 +53,9 @@ export const StringInputPane: FunctionComponent<StringInputPanelProps> = ({
logConsoleInfo(`${successMessage}: ${stringInput}`); logConsoleInfo(`${successMessage}: ${stringInput}`);
const originalPath = notebookFile.path; const originalPath = notebookFile.path;
const notebookTabs = container.tabsManager.getTabs( const notebookTabs = useTabs
.getState()
.getTabs(
ViewModels.CollectionTabKind.NotebookV2, ViewModels.CollectionTabKind.NotebookV2,
(tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath) (tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath)
); );

View File

@ -10,7 +10,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isSchemaEnabled": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
@ -28,10 +27,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
}, },
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
},
} }
} }
inProgressMessage="Creating directory " inProgressMessage="Creating directory "

View File

@ -2,15 +2,7 @@
exports[`Delete Database Confirmation Pane Should call delete database 1`] = ` exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
<DeleteDatabaseConfirmationPanel <DeleteDatabaseConfirmationPanel
explorer={ refreshDatabases={[Function]}
Object {
"refreshAllDatabases": [Function],
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
},
}
}
> >
<RightPaneForm <RightPaneForm
formError="" formError=""

View File

@ -1,12 +1,10 @@
import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil"; import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { TabsManager } from "../Tabs/TabsManager";
import { SplashScreen } from "./SplashScreen"; import { SplashScreen } from "./SplashScreen";
jest.mock("../Explorer"); jest.mock("../Explorer");
const createExplorer = () => { const createExplorer = () => {
const mock = new Explorer(); const mock = new Explorer();
mock.tabsManager = new TabsManager();
return mock as jest.Mocked<Explorer>; return mock as jest.Mocked<Explorer>;
}; };

View File

@ -280,7 +280,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
} }
/* Scale & Settings */ /* Scale & Settings */
const isShared = useSelectedNode.getState().findSelectedDatabase()?.isDatabaseShared(); const isShared = useDatabases.getState().findSelectedDatabase()?.isDatabaseShared();
const label = isShared ? "Settings" : "Scale & Settings"; const label = isShared ? "Settings" : "Scale & Settings";
items.push({ items.push({

View File

@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import type { TabOptions } from "../../../Contracts/ViewModels"; import type { TabOptions } from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import TabsBase from "../TabsBase"; import TabsBase from "../TabsBase";
import MongoShellTabComponent, { IMongoShellTabAccessor, IMongoShellTabComponentProps } from "./MongoShellTabComponent"; import MongoShellTabComponent, { IMongoShellTabAccessor, IMongoShellTabComponentProps } from "./MongoShellTabComponent";
@ -33,7 +34,7 @@ export class NewMongoShellTab extends TabsBase {
} }
public onTabClick(): void { public onTabClick(): void {
this.manager?.activateTab(this); useTabs.getState().activateTab(this);
this.iMongoShellTabAccessor.onTabClickEvent(); this.iMongoShellTabAccessor.onTabClickEvent();
} }
} }

View File

@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import type { QueryTabOptions } from "../../../Contracts/ViewModels"; import type { QueryTabOptions } from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { IQueryTabComponentProps, ITabAccessor } from "../../Tabs/QueryTab/QueryTabComponent"; import { IQueryTabComponentProps, ITabAccessor } from "../../Tabs/QueryTab/QueryTabComponent";
import TabsBase from "../TabsBase"; import TabsBase from "../TabsBase";
@ -40,12 +41,12 @@ export class NewQueryTab extends TabsBase {
} }
public onTabClick(): void { public onTabClick(): void {
this.manager?.activateTab(this); useTabs.getState().activateTab(this);
this.iTabAccessor.onTabClickEvent(); this.iTabAccessor.onTabClickEvent();
} }
public onCloseTabButtonClick(): void { public onCloseTabButtonClick(): void {
this.manager?.closeTab(this); useTabs.getState().closeTab(this);
if (this.iTabAccessor) { if (this.iTabAccessor) {
this.iTabAccessor.onCloseClickEvent(true); this.iTabAccessor.onCloseClickEvent(true);
} }

View File

@ -29,7 +29,6 @@ import { EditorReact } from "../../Controls/Editor/EditorReact";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
import TabsBase from "../TabsBase"; import TabsBase from "../TabsBase";
import { TabsManager } from "../TabsManager";
import "./QueryTabComponent.less"; import "./QueryTabComponent.less";
enum ToggleState { enum ToggleState {
@ -65,7 +64,6 @@ export interface IQueryTabComponentProps {
partitionKey: DataModels.PartitionKey; partitionKey: DataModels.PartitionKey;
container: Explorer; container: Explorer;
activeTab?: TabsBase; activeTab?: TabsBase;
tabManager?: TabsManager;
onTabAccessor: (instance: ITabAccessor) => void; onTabAccessor: (instance: ITabAccessor) => void;
isPreferredApiMongoDB?: boolean; isPreferredApiMongoDB?: boolean;
monacoEditorSetting?: string; monacoEditorSetting?: string;

View File

@ -2,6 +2,7 @@ import React from "react";
import { ExecuteSprocResult } from "../../../Common/dataAccess/executeStoredProcedure"; import { ExecuteSprocResult } from "../../../Common/dataAccess/executeStoredProcedure";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import StoredProcedure from "../../Tree/StoredProcedure"; import StoredProcedure from "../../Tree/StoredProcedure";
import ScriptTabBase from "../ScriptTabBase"; import ScriptTabBase from "../ScriptTabBase";
@ -51,12 +52,12 @@ export class NewStoredProcedureTab extends ScriptTabBase {
} }
public onTabClick(): void { public onTabClick(): void {
this.manager?.activateTab(this); useTabs.getState().activateTab(this);
this.iStoreProcAccessor.onTabClickEvent(); this.iStoreProcAccessor.onTabClickEvent();
} }
public onCloseTabButtonClick(): void { public onCloseTabButtonClick(): void {
this.manager?.closeTab(this); useTabs.getState().closeTab(this);
} }
public onExecuteSprocsResult(result: ExecuteSprocResult): void { public onExecuteSprocsResult(result: ExecuteSprocResult): void {

View File

@ -10,6 +10,7 @@ import { ExecuteSprocResult } from "../../../Common/dataAccess/executeStoredProc
import { updateStoredProcedure } from "../../../Common/dataAccess/updateStoredProcedure"; import { updateStoredProcedure } from "../../../Common/dataAccess/updateStoredProcedure";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { useNotificationConsole } from "../../../hooks/useNotificationConsole"; import { useNotificationConsole } from "../../../hooks/useNotificationConsole";
import { useTabs } from "../../../hooks/useTabs";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { EditorReact } from "../../Controls/Editor/EditorReact"; import { EditorReact } from "../../Controls/Editor/EditorReact";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
@ -144,7 +145,7 @@ export default class StoredProcedureTabComponent extends React.Component<
} }
public onTabClick(): void { public onTabClick(): void {
if (this.props.container.tabsManager.openedTabs().length > 0) { if (useTabs.getState().openedTabs.length > 0) {
useCommandBar.getState().setContextButtons(this.getTabsButtons()); useCommandBar.getState().setContextButtons(this.getTabsButtons());
} }
} }
@ -396,10 +397,8 @@ export default class StoredProcedureTabComponent extends React.Component<
editorModel && editorModel.setValue(createdResource.body as string); editorModel && editorModel.setValue(createdResource.body as string);
this.props.scriptTabBaseInstance.editorContent.setBaseline(createdResource.body as string); this.props.scriptTabBaseInstance.editorContent.setBaseline(createdResource.body as string);
this.node = this.collection.createStoredProcedureNode(createdResource); this.node = this.collection.createStoredProcedureNode(createdResource);
this.props.container.tabsManager.openedTabs()[ this.props.scriptTabBaseInstance.node = this.node;
this.props.container.tabsManager.openedTabs().length - 1 useTabs.getState().updateTab(this.props.scriptTabBaseInstance);
].node = this.node;
this.props.scriptTabBaseInstance.editorState(ViewModels.ScriptEditorState.exisitingNoEdits); this.props.scriptTabBaseInstance.editorState(ViewModels.ScriptEditorState.exisitingNoEdits);
this.setState({ this.setState({

View File

@ -3,28 +3,32 @@ import React, { useEffect, useRef, useState } from "react";
import loadingIcon from "../../../images/circular_loader_black_16x16.gif"; import loadingIcon from "../../../images/circular_loader_black_16x16.gif";
import errorIcon from "../../../images/close-black.svg"; import errorIcon from "../../../images/close-black.svg";
import { useObservable } from "../../hooks/useObservable"; import { useObservable } from "../../hooks/useObservable";
import { useTabs } from "../../hooks/useTabs";
import TabsBase from "./TabsBase"; import TabsBase from "./TabsBase";
type Tab = TabsBase | (TabsBase & { render: () => JSX.Element }); type Tab = TabsBase | (TabsBase & { render: () => JSX.Element });
export const Tabs = ({ tabs, activeTab }: { tabs: readonly Tab[]; activeTab: Tab }): JSX.Element => ( export const Tabs = (): JSX.Element => {
const { openedTabs, activeTab } = useTabs();
return (
<div className="tabsManagerContainer"> <div className="tabsManagerContainer">
<div id="content" className="flexContainer hideOverflows"> <div id="content" className="flexContainer hideOverflows">
<div className="nav-tabs-margin"> <div className="nav-tabs-margin">
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist"> <ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
{tabs.map((tab) => ( {openedTabs.map((tab) => (
<TabNav key={tab.tabId} tab={tab} active={activeTab === tab} /> <TabNav key={tab.tabId} tab={tab} active={activeTab === tab} />
))} ))}
</ul> </ul>
</div> </div>
<div className="tabPanesContainer"> <div className="tabPanesContainer">
{tabs.map((tab) => ( {openedTabs.map((tab) => (
<TabPane key={tab.tabId} tab={tab} active={activeTab === tab} /> <TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />
))} ))}
</div> </div>
</div> </div>
</div> </div>
); );
};
function TabNav({ tab, active }: { tab: Tab; active: boolean }) { function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
const [hovering, setHovering] = useState(false); const [hovering, setHovering] = useState(false);

View File

@ -4,6 +4,7 @@ import * as ThemeUtility from "../../Common/ThemeUtility";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { useNotificationConsole } from "../../hooks/useNotificationConsole"; import { useNotificationConsole } from "../../hooks/useNotificationConsole";
import { useTabs } from "../../hooks/useTabs";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
@ -11,7 +12,6 @@ import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel"; import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
import { TabsManager } from "./TabsManager";
// TODO: Use specific actions for logging telemetry data // TODO: Use specific actions for logging telemetry data
export default class TabsBase extends WaitsForTemplateViewModel { export default class TabsBase extends WaitsForTemplateViewModel {
private static id = 0; private static id = 0;
@ -28,7 +28,6 @@ export default class TabsBase extends WaitsForTemplateViewModel {
public isExecutionError = ko.observable(false); public isExecutionError = ko.observable(false);
public isExecuting = ko.observable(false); public isExecuting = ko.observable(false);
public pendingNotification?: ko.Observable<DataModels.Notification>; public pendingNotification?: ko.Observable<DataModels.Notification>;
public manager?: TabsManager;
protected _theme: string; protected _theme: string;
public onLoadStartKey: number; public onLoadStartKey: number;
@ -60,7 +59,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
} }
public onCloseTabButtonClick(): void { public onCloseTabButtonClick(): void {
this.manager?.closeTab(this); useTabs.getState().closeTab(this);
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, { TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {
tabName: this.constructor.name, tabName: this.constructor.name,
dataExplorerArea: Constants.Areas.Tab, dataExplorerArea: Constants.Areas.Tab,
@ -70,7 +69,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
} }
public onTabClick(): void { public onTabClick(): void {
this.manager?.activateTab(this); useTabs.getState().activateTab(this);
} }
protected updateSelectedNode(): void { 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 */ /** @deprecated this is no longer observable, bind to comparisons with manager.activeTab() instead */
public isActive() { public isActive() {
return this === this.manager?.activeTab(); return this === useTabs.getState().activeTab;
} }
public onActivate(): void { public onActivate(): void {

View File

@ -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<TabsBase>([]);
public activeTab = ko.observable<TabsBase>();
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);
}
}
}
}

View File

@ -1,23 +1,19 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs";
import { updateUserContext } from "../../UserContext"; import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer"; import { container } from "../Controls/Settings/TestUtils";
import DocumentId from "../Tree/DocumentId"; import DocumentId from "../Tree/DocumentId";
import { container } from "./../Controls/Settings/TestUtils";
import DocumentsTab from "./DocumentsTab"; import DocumentsTab from "./DocumentsTab";
import { NewQueryTab } from "./QueryTab/QueryTab"; import { NewQueryTab } from "./QueryTab/QueryTab";
import { TabsManager } from "./TabsManager";
describe("Tabs manager tests", () => { describe("useTabs tests", () => {
let tabsManager: TabsManager;
let explorer: Explorer;
let database: ViewModels.Database; let database: ViewModels.Database;
let collection: ViewModels.Collection; let collection: ViewModels.Collection;
let queryTab: NewQueryTab; let queryTab: NewQueryTab;
let documentsTab: DocumentsTab; let documentsTab: DocumentsTab;
beforeEach(() => { beforeEach(() => {
explorer = new Explorer();
updateUserContext({ updateUserContext({
databaseAccount: { databaseAccount: {
id: "test", id: "test",
@ -30,7 +26,6 @@ describe("Tabs manager tests", () => {
}); });
database = { database = {
container: explorer,
id: ko.observable<string>("test"), id: ko.observable<string>("test"),
isDatabaseShared: () => false, isDatabaseShared: () => false,
} as ViewModels.Database; } as ViewModels.Database;
@ -38,7 +33,6 @@ describe("Tabs manager tests", () => {
database.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>(); database.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
collection = { collection = {
container: explorer,
databaseId: "test", databaseId: "test",
id: ko.observable<string>("test"), id: ko.observable<string>("test"),
} as ViewModels.Collection; } as ViewModels.Collection;
@ -76,63 +70,70 @@ describe("Tabs manager tests", () => {
documentsTab.tabId = "2"; documentsTab.tabId = "2";
}); });
beforeEach(() => (tabsManager = new TabsManager())); beforeEach(() => useTabs.setState({ openedTabs: [], activeTab: undefined }));
it("open new tabs", () => { it("open new tabs", () => {
tabsManager.activateNewTab(queryTab); const { activateNewTab } = useTabs.getState();
expect(tabsManager.openedTabs().length).toBe(1); activateNewTab(queryTab);
expect(tabsManager.openedTabs()[0]).toEqual(queryTab); let tabsState = useTabs.getState();
expect(tabsManager.activeTab()).toEqual(queryTab); expect(tabsState.openedTabs.length).toBe(1);
expect(tabsState.openedTabs[0]).toEqual(queryTab);
expect(tabsState.activeTab).toEqual(queryTab);
expect(queryTab.isActive()).toBe(true); expect(queryTab.isActive()).toBe(true);
tabsManager.activateNewTab(documentsTab); activateNewTab(documentsTab);
expect(tabsManager.openedTabs().length).toBe(2); tabsState = useTabs.getState();
expect(tabsManager.openedTabs()[1]).toEqual(documentsTab); expect(tabsState.openedTabs.length).toBe(2);
expect(tabsManager.activeTab()).toEqual(documentsTab); expect(tabsState.openedTabs[1]).toEqual(documentsTab);
expect(tabsState.activeTab).toEqual(documentsTab);
expect(queryTab.isActive()).toBe(false); expect(queryTab.isActive()).toBe(false);
expect(documentsTab.isActive()).toBe(true); expect(documentsTab.isActive()).toBe(true);
}); });
it("open existing tabs", () => { it("open existing tabs", () => {
tabsManager.activateNewTab(queryTab); const { activateNewTab, activateTab } = useTabs.getState();
tabsManager.activateNewTab(documentsTab); activateNewTab(queryTab);
tabsManager.activateTab(queryTab); activateNewTab(documentsTab);
expect(tabsManager.openedTabs().length).toBe(2); activateTab(queryTab);
expect(tabsManager.activeTab()).toEqual(queryTab);
const { openedTabs, activeTab } = useTabs.getState();
expect(openedTabs.length).toBe(2);
expect(activeTab).toEqual(queryTab);
expect(queryTab.isActive()).toBe(true); expect(queryTab.isActive()).toBe(true);
expect(documentsTab.isActive()).toBe(false); expect(documentsTab.isActive()).toBe(false);
}); });
it("get tabs", () => { it("get tabs", () => {
tabsManager.activateNewTab(queryTab); const { activateNewTab, getTabs } = useTabs.getState();
tabsManager.activateNewTab(documentsTab); activateNewTab(queryTab);
activateNewTab(documentsTab);
const queryTabs = tabsManager.getTabs(ViewModels.CollectionTabKind.Query); const queryTabs = getTabs(ViewModels.CollectionTabKind.Query);
expect(queryTabs.length).toBe(1); expect(queryTabs.length).toBe(1);
expect(queryTabs[0]).toEqual(queryTab); expect(queryTabs[0]).toEqual(queryTab);
const documentsTabs = tabsManager.getTabs( const documentsTabs = getTabs(ViewModels.CollectionTabKind.Documents, (tab) => tab.tabId === documentsTab.tabId);
ViewModels.CollectionTabKind.Documents,
(tab) => tab.tabId === documentsTab.tabId
);
expect(documentsTabs.length).toBe(1); expect(documentsTabs.length).toBe(1);
expect(documentsTabs[0]).toEqual(documentsTab); expect(documentsTabs[0]).toEqual(documentsTab);
}); });
it("close tabs", () => { it("close tabs", () => {
tabsManager.activateNewTab(queryTab); const { activateNewTab, closeTab, closeTabsByComparator } = useTabs.getState();
tabsManager.activateNewTab(documentsTab); activateNewTab(queryTab);
activateNewTab(documentsTab);
closeTab(documentsTab);
tabsManager.closeTab(documentsTab); let tabsState = useTabs.getState();
expect(tabsManager.openedTabs().length).toBe(1); expect(tabsState.openedTabs.length).toBe(1);
expect(tabsManager.openedTabs()[0]).toEqual(queryTab); expect(tabsState.openedTabs[0]).toEqual(queryTab);
expect(tabsManager.activeTab()).toEqual(queryTab); expect(tabsState.activeTab).toEqual(queryTab);
expect(queryTab.isActive()).toBe(true); expect(queryTab.isActive()).toBe(true);
expect(documentsTab.isActive()).toBe(false); expect(documentsTab.isActive()).toBe(false);
tabsManager.closeTabsByComparator((tab) => tab.tabId === queryTab.tabId); closeTabsByComparator((tab) => tab.tabId === queryTab.tabId);
expect(tabsManager.openedTabs().length).toBe(0); tabsState = useTabs.getState();
expect(tabsManager.activeTab()).toEqual(undefined); expect(tabsState.openedTabs.length).toBe(0);
expect(tabsState.activeTab).toEqual(undefined);
expect(queryTab.isActive()).toBe(false); expect(queryTab.isActive()).toBe(false);
}); });
}); });

View File

@ -15,6 +15,7 @@ import { fetchPortalNotifications } from "../../Common/PortalNotifications";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { UploadDetailsRecord } from "../../Contracts/ViewModels"; import { UploadDetailsRecord } from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
@ -239,7 +240,9 @@ export default class Collection implements ViewModels.Collection {
this.expandCollection(); this.expandCollection();
} }
useCommandBar.getState().setContextButtons([]); useCommandBar.getState().setContextButtons([]);
this.container.tabsManager.refreshActiveTab( useTabs
.getState()
.refreshActiveTab(
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
); );
} }
@ -288,14 +291,16 @@ export default class Collection implements ViewModels.Collection {
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
const documentsTabs: DocumentsTab[] = this.container.tabsManager.getTabs( const documentsTabs: DocumentsTab[] = useTabs
.getState()
.getTabs(
ViewModels.CollectionTabKind.Documents, ViewModels.CollectionTabKind.Documents,
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
) as DocumentsTab[]; ) as DocumentsTab[];
let documentsTab: DocumentsTab = documentsTabs && documentsTabs[0]; let documentsTab: DocumentsTab = documentsTabs && documentsTabs[0];
if (documentsTab) { if (documentsTab) {
this.container.tabsManager.activateTab(documentsTab); useTabs.getState().activateTab(documentsTab);
} else { } else {
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseName: this.databaseId, databaseName: this.databaseId,
@ -317,7 +322,7 @@ export default class Collection implements ViewModels.Collection {
onLoadStartKey: startKey, 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, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
const conflictsTabs: ConflictsTab[] = this.container.tabsManager.getTabs( const conflictsTabs: ConflictsTab[] = useTabs
.getState()
.getTabs(
ViewModels.CollectionTabKind.Conflicts, ViewModels.CollectionTabKind.Conflicts,
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
) as ConflictsTab[]; ) as ConflictsTab[];
let conflictsTab: ConflictsTab = conflictsTabs && conflictsTabs[0]; let conflictsTab: ConflictsTab = conflictsTabs && conflictsTabs[0];
if (conflictsTab) { if (conflictsTab) {
this.container.tabsManager.activateTab(conflictsTab); useTabs.getState().activateTab(conflictsTab);
} else { } else {
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseName: this.databaseId, databaseName: this.databaseId,
@ -362,7 +369,7 @@ export default class Collection implements ViewModels.Collection {
onLoadStartKey: startKey, 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( const queryTablesTabs: QueryTablesTab[] = useTabs
.getState()
.getTabs(
ViewModels.CollectionTabKind.QueryTables, ViewModels.CollectionTabKind.QueryTables,
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
) as QueryTablesTab[]; ) as QueryTablesTab[];
let queryTablesTab: QueryTablesTab = queryTablesTabs && queryTablesTabs[0]; let queryTablesTab: QueryTablesTab = queryTablesTabs && queryTablesTabs[0];
if (queryTablesTab) { if (queryTablesTab) {
this.container.tabsManager.activateTab(queryTablesTab); useTabs.getState().activateTab(queryTablesTab);
} else { } else {
this.documentIds([]); this.documentIds([]);
let title = `Entities`; let title = `Entities`;
@ -415,7 +424,7 @@ export default class Collection implements ViewModels.Collection {
onLoadStartKey: startKey, 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, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
const graphTabs: GraphTab[] = this.container.tabsManager.getTabs( const graphTabs: GraphTab[] = useTabs
.getState()
.getTabs(
ViewModels.CollectionTabKind.Graph, ViewModels.CollectionTabKind.Graph,
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
) as GraphTab[]; ) as GraphTab[];
let graphTab: GraphTab = graphTabs && graphTabs[0]; let graphTab: GraphTab = graphTabs && graphTabs[0];
if (graphTab) { if (graphTab) {
this.container.tabsManager.activateTab(graphTab); useTabs.getState().activateTab(graphTab);
} else { } else {
this.documentIds([]); this.documentIds([]);
const title = "Graph"; const title = "Graph";
@ -466,7 +477,7 @@ export default class Collection implements ViewModels.Collection {
onLoadStartKey: startKey, 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, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
const mongoDocumentsTabs: MongoDocumentsTab[] = this.container.tabsManager.getTabs( const mongoDocumentsTabs: MongoDocumentsTab[] = useTabs
.getState()
.getTabs(
ViewModels.CollectionTabKind.Documents, ViewModels.CollectionTabKind.Documents,
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
) as MongoDocumentsTab[]; ) as MongoDocumentsTab[];
let mongoDocumentsTab: MongoDocumentsTab = mongoDocumentsTabs && mongoDocumentsTabs[0]; let mongoDocumentsTab: MongoDocumentsTab = mongoDocumentsTabs && mongoDocumentsTabs[0];
if (mongoDocumentsTab) { if (mongoDocumentsTab) {
this.container.tabsManager.activateTab(mongoDocumentsTab); useTabs.getState().activateTab(mongoDocumentsTab);
} else { } else {
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseName: this.databaseId, databaseName: this.databaseId,
@ -510,7 +523,7 @@ export default class Collection implements ViewModels.Collection {
node: this, node: this,
onLoadStartKey: startKey, 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, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
for (const tab of this.container.tabsManager.openedTabs()) { for (const tab of useTabs.getState().openedTabs) {
if ( if (
tab instanceof SchemaAnalyzerTab && tab instanceof SchemaAnalyzerTab &&
tab.collection?.databaseId === this.databaseId && tab.collection?.databaseId === this.databaseId &&
tab.collection?.id() === this.id() 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", tabTitle: "Schema",
}); });
this.documentIds([]); this.documentIds([]);
this.container.tabsManager.activateNewTab( useTabs.getState().activateNewTab(
new SchemaAnalyzerTab({ new SchemaAnalyzerTab({
account: userContext.databaseAccount, account: userContext.databaseAccount,
masterKey: userContext.masterKey || "", masterKey: userContext.masterKey || "",
@ -571,12 +584,9 @@ export default class Collection implements ViewModels.Collection {
}); });
const tabTitle = !this.offer() ? "Settings" : "Scale & Settings"; const tabTitle = !this.offer() ? "Settings" : "Scale & Settings";
const matchingTabs = this.container.tabsManager.getTabs( const matchingTabs = useTabs.getState().getTabs(ViewModels.CollectionTabKind.CollectionSettingsV2, (tab) => {
ViewModels.CollectionTabKind.CollectionSettingsV2,
(tab) => {
return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id(); return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id();
} });
);
const traceStartData = { const traceStartData = {
databaseName: this.databaseId, databaseName: this.databaseId,
@ -608,15 +618,15 @@ export default class Collection implements ViewModels.Collection {
settingsTabOptions.onLoadStartKey = startKey; settingsTabOptions.onLoadStartKey = startKey;
settingsTabOptions.tabKind = ViewModels.CollectionTabKind.CollectionSettingsV2; settingsTabOptions.tabKind = ViewModels.CollectionTabKind.CollectionSettingsV2;
settingsTabV2 = new CollectionSettingsTabV2(settingsTabOptions); settingsTabV2 = new CollectionSettingsTabV2(settingsTabOptions);
this.container.tabsManager.activateNewTab(settingsTabV2); useTabs.getState().activateNewTab(settingsTabV2);
} else { } else {
this.container.tabsManager.activateTab(settingsTabV2); useTabs.getState().activateTab(settingsTabV2);
} }
}; };
public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) { public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) {
const collection: ViewModels.Collection = source.collection || source; 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 title = "Query " + id;
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseName: this.databaseId, databaseName: this.databaseId,
@ -626,7 +636,7 @@ export default class Collection implements ViewModels.Collection {
tabTitle: title, tabTitle: title,
}); });
this.container.tabsManager.activateNewTab( useTabs.getState().activateNewTab(
new NewQueryTab( new NewQueryTab(
{ {
tabKind: ViewModels.CollectionTabKind.Query, tabKind: ViewModels.CollectionTabKind.Query,
@ -645,7 +655,7 @@ export default class Collection implements ViewModels.Collection {
public onNewMongoQueryClick(source: any, event: MouseEvent, queryText?: string) { public onNewMongoQueryClick(source: any, event: MouseEvent, queryText?: string) {
const collection: ViewModels.Collection = source.collection || source; 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 title = "Query " + id;
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { 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() { 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 title: string = "Graph Query " + id;
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
@ -702,13 +712,11 @@ export default class Collection implements ViewModels.Collection {
onLoadStartKey: startKey, onLoadStartKey: startKey,
}); });
this.container.tabsManager.activateNewTab(graphTab); useTabs.getState().activateNewTab(graphTab);
} }
public onNewMongoShellClick() { public onNewMongoShellClick() {
const mongoShellTabs = this.container.tabsManager.getTabs( const mongoShellTabs = useTabs.getState().getTabs(ViewModels.CollectionTabKind.MongoShell) as NewMongoShellTab[];
ViewModels.CollectionTabKind.MongoShell
) as NewMongoShellTab[];
let index = 1; let index = 1;
if (mongoShellTabs.length > 0) { 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) { public onNewStoredProcedureClick(source: ViewModels.Collection, event: MouseEvent) {
@ -787,7 +795,9 @@ export default class Collection implements ViewModels.Collection {
} else { } else {
this.expandStoredProcedures(); this.expandStoredProcedures();
} }
this.container.tabsManager.refreshActiveTab( useTabs
.getState()
.refreshActiveTab(
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
); );
} }
@ -846,7 +856,9 @@ export default class Collection implements ViewModels.Collection {
} else { } else {
this.expandUserDefinedFunctions(); this.expandUserDefinedFunctions();
} }
this.container.tabsManager.refreshActiveTab( useTabs
.getState()
.refreshActiveTab(
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
); );
} }
@ -905,7 +917,9 @@ export default class Collection implements ViewModels.Collection {
} else { } else {
this.expandTriggers(); this.expandTriggers();
} }
this.container.tabsManager.refreshActiveTab( useTabs
.getState()
.refreshActiveTab(
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
); );
} }

View File

@ -1,7 +1,7 @@
import * as ko from "knockout";
import { HttpStatusCodes } from "../../Common/Constants"; import { HttpStatusCodes } from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { JunoClient } from "../../Juno/JunoClient"; import { JunoClient } from "../../Juno/JunoClient";
import { Features } from "../../Platform/Hosted/extractFeatures";
import { updateUserContext, userContext } from "../../UserContext"; import { updateUserContext, userContext } from "../../UserContext";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import Database from "./Database"; import Database from "./Database";
@ -35,7 +35,6 @@ describe("Add Schema", () => {
collection.analyticalStorageTtl = undefined; collection.analyticalStorageTtl = undefined;
const database = new Database(createMockContainer(), collection); const database = new Database(createMockContainer(), collection);
database.container = createMockContainer(); database.container = createMockContainer();
database.container.isSchemaEnabled = ko.computed<boolean>(() => false);
database.junoClient = new JunoClient(); database.junoClient = new JunoClient();
database.junoClient.requestSchema = jest.fn(); database.junoClient.requestSchema = jest.fn();
@ -52,7 +51,11 @@ describe("Add Schema", () => {
const database = new Database(createMockContainer(), collection); const database = new Database(createMockContainer(), collection);
database.container = createMockContainer(); database.container = createMockContainer();
database.container.isSchemaEnabled = ko.computed<boolean>(() => true); updateUserContext({
features: {
enableSchema: true,
} as Features,
});
database.junoClient = new JunoClient(); database.junoClient = new JunoClient();
database.junoClient.requestSchema = jest.fn(); database.junoClient.requestSchema = jest.fn();

View File

@ -11,6 +11,7 @@ import { fetchPortalNotifications } from "../../Common/PortalNotifications";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { useSidePanel } from "../../hooks/useSidePanel"; import { useSidePanel } from "../../hooks/useSidePanel";
import { useTabs } from "../../hooks/useTabs";
import { IJunoResponse, JunoClient } from "../../Juno/JunoClient"; import { IJunoResponse, JunoClient } from "../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
@ -67,7 +68,7 @@ export default class Database implements ViewModels.Database {
const pendingNotificationsPromise: Promise<DataModels.Notification> = this.getPendingThroughputSplitNotification(); const pendingNotificationsPromise: Promise<DataModels.Notification> = this.getPendingThroughputSplitNotification();
const tabKind = ViewModels.CollectionTabKind.DatabaseSettingsV2; 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; let settingsTab = matchingTabs?.[0] as DatabaseSettingsTabV2;
if (!settingsTab) { if (!settingsTab) {
@ -91,7 +92,7 @@ export default class Database implements ViewModels.Database {
}; };
settingsTab = new DatabaseSettingsTabV2(tabOptions); settingsTab = new DatabaseSettingsTabV2(tabOptions);
settingsTab.pendingNotification(pendingNotification); settingsTab.pendingNotification(pendingNotification);
this.container.tabsManager.activateNewTab(settingsTab); useTabs.getState().activateNewTab(settingsTab);
}, },
(error) => { (error) => {
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
@ -116,11 +117,11 @@ export default class Database implements ViewModels.Database {
pendingNotificationsPromise.then( pendingNotificationsPromise.then(
(pendingNotification: DataModels.Notification) => { (pendingNotification: DataModels.Notification) => {
settingsTab.pendingNotification(pendingNotification); settingsTab.pendingNotification(pendingNotification);
this.container.tabsManager.activateTab(settingsTab); useTabs.getState().activateTab(settingsTab);
}, },
() => { () => {
settingsTab.pendingNotification(undefined); 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; let checkForSchema: NodeJS.Timeout;
interval = interval || 5000; interval = interval || 5000;
if (collection.analyticalStorageTtl !== undefined && this.container.isSchemaEnabled()) { if (collection.analyticalStorageTtl !== undefined && userContext.features.enableSchema) {
collection.requestSchema = () => { collection.requestSchema = () => {
this.junoClient.requestSchema({ this.junoClient.requestSchema({
id: undefined, id: undefined,

View File

@ -2,6 +2,7 @@ import * as ko from "knockout";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
@ -77,7 +78,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) { public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) {
const collection: ViewModels.Collection = source.collection || source; 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 title = "Query " + id;
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseName: this.databaseId, databaseName: this.databaseId,
@ -87,7 +88,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
tabTitle: title, tabTitle: title,
}); });
this.container.tabsManager.activateNewTab( useTabs.getState().activateNewTab(
new NewQueryTab( new NewQueryTab(
{ {
tabKind: ViewModels.CollectionTabKind.Query, tabKind: ViewModels.CollectionTabKind.Query,
@ -115,7 +116,9 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
const documentsTabs: DocumentsTab[] = this.container.tabsManager.getTabs( const documentsTabs: DocumentsTab[] = useTabs
.getState()
.getTabs(
ViewModels.CollectionTabKind.Documents, ViewModels.CollectionTabKind.Documents,
(tab: TabsBase) => (tab: TabsBase) =>
tab.collection?.id() === this.id() && tab.collection?.id() === this.id() &&
@ -124,7 +127,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
let documentsTab: DocumentsTab = documentsTabs && documentsTabs[0]; let documentsTab: DocumentsTab = documentsTabs && documentsTabs[0];
if (documentsTab) { if (documentsTab) {
this.container.tabsManager.activateTab(documentsTab); useTabs.getState().activateTab(documentsTab);
} else { } else {
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseName: this.databaseId, databaseName: this.databaseId,
@ -146,7 +149,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
onLoadStartKey: startKey, onLoadStartKey: startKey,
}); });
this.container.tabsManager.activateNewTab(documentsTab); useTabs.getState().activateNewTab(documentsTab);
} }
} }

View File

@ -1,28 +1,34 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs";
import TabsBase from "../Tabs/TabsBase"; import TabsBase from "../Tabs/TabsBase";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
describe("useSelectedNode.getState()", () => { describe("useSelectedNode", () => {
const mockTab = { const mockTab = {
tabKind: ViewModels.CollectionTabKind.Documents, tabKind: ViewModels.CollectionTabKind.Documents,
} as TabsBase; } as TabsBase;
// TODO isDataNodeSelected needs a better design and refactor, but for now, we protect some of the code paths // TODO isDataNodeSelected needs a better design and refactor, but for now, we protect some of the code paths
describe("isDataNodeSelected", () => { 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", () => { 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(); expect(isDataNodeSelected).toBeFalsy();
}); });
it("it should not select incorrect subnodekinds", () => { it("it should not select incorrect subnodekinds", () => {
useTabs.setState({ activeTab: mockTab });
useSelectedNode.getState().setSelectedNode({ useSelectedNode.getState().setSelectedNode({
nodeKind: "nodeKind", nodeKind: "nodeKind",
rid: "rid", rid: "rid",
id: ko.observable<string>("id"), id: ko.observable<string>("id"),
}); });
const isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected(mockTab, "foo", "bar", undefined); const isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected("foo", "bar", undefined);
expect(isDataNodeSelected).toBeFalsy(); expect(isDataNodeSelected).toBeFalsy();
}); });
@ -32,11 +38,12 @@ describe("useSelectedNode.getState()", () => {
rid: "rid", rid: "rid",
id: ko.observable<string>("id"), id: ko.observable<string>("id"),
}); });
const isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected(undefined, "foo", "bar", undefined); const isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected("foo", "bar", undefined);
expect(isDataNodeSelected).toBeFalsy(); expect(isDataNodeSelected).toBeFalsy();
}); });
it("should select if correct database node regardless of subnodekinds", () => { it("should select if correct database node regardless of subnodekinds", () => {
useTabs.setState({ activeTab: mockTab });
const subNodeKind = ViewModels.CollectionTabKind.Documents; const subNodeKind = ViewModels.CollectionTabKind.Documents;
useSelectedNode.getState().setSelectedNode({ useSelectedNode.getState().setSelectedNode({
nodeKind: "Database", nodeKind: "Database",
@ -46,7 +53,7 @@ describe("useSelectedNode.getState()", () => {
} as ViewModels.TreeNode); } as ViewModels.TreeNode);
const isDataNodeSelected = useSelectedNode const isDataNodeSelected = useSelectedNode
.getState() .getState()
.isDataNodeSelected(mockTab, "dbid", undefined, [ViewModels.CollectionTabKind.Documents]); .isDataNodeSelected("dbid", undefined, [ViewModels.CollectionTabKind.Documents]);
expect(isDataNodeSelected).toBeTruthy(); expect(isDataNodeSelected).toBeTruthy();
}); });
@ -55,6 +62,7 @@ describe("useSelectedNode.getState()", () => {
let activeTab = { let activeTab = {
tabKind: subNodeKind, tabKind: subNodeKind,
} as TabsBase; } as TabsBase;
useTabs.setState({ activeTab });
useSelectedNode.getState().setSelectedNode({ useSelectedNode.getState().setSelectedNode({
nodeKind: "Collection", nodeKind: "Collection",
rid: "collrid", rid: "collrid",
@ -62,15 +70,14 @@ describe("useSelectedNode.getState()", () => {
id: ko.observable<string>("collid"), id: ko.observable<string>("collid"),
selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(subNodeKind), selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(subNodeKind),
} as ViewModels.TreeNode); } as ViewModels.TreeNode);
let isDataNodeSelected = useSelectedNode let isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected("dbid", "collid", [subNodeKind]);
.getState()
.isDataNodeSelected(activeTab, "dbid", "collid", [subNodeKind]);
expect(isDataNodeSelected).toBeTruthy(); expect(isDataNodeSelected).toBeTruthy();
subNodeKind = ViewModels.CollectionTabKind.Graph; subNodeKind = ViewModels.CollectionTabKind.Graph;
activeTab = { activeTab = {
tabKind: subNodeKind, tabKind: subNodeKind,
} as TabsBase; } as TabsBase;
useTabs.setState({ activeTab });
useSelectedNode.getState().setSelectedNode({ useSelectedNode.getState().setSelectedNode({
nodeKind: "Collection", nodeKind: "Collection",
rid: "collrid", rid: "collrid",
@ -78,7 +85,7 @@ describe("useSelectedNode.getState()", () => {
id: ko.observable<string>("collid"), id: ko.observable<string>("collid"),
selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(subNodeKind), selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(subNodeKind),
} as ViewModels.TreeNode); } as ViewModels.TreeNode);
isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected(activeTab, "dbid", "collid", [subNodeKind]); isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected("dbid", "collid", [subNodeKind]);
expect(isDataNodeSelected).toBeTruthy(); expect(isDataNodeSelected).toBeTruthy();
}); });
@ -93,9 +100,10 @@ describe("useSelectedNode.getState()", () => {
const activeTab = { const activeTab = {
tabKind: ViewModels.CollectionTabKind.Documents, tabKind: ViewModels.CollectionTabKind.Documents,
} as TabsBase; } as TabsBase;
useTabs.setState({ activeTab });
const isDataNodeSelected = useSelectedNode const isDataNodeSelected = useSelectedNode
.getState() .getState()
.isDataNodeSelected(activeTab, "dbid", "collid", [ViewModels.CollectionTabKind.Settings]); .isDataNodeSelected("dbid", "collid", [ViewModels.CollectionTabKind.Settings]);
expect(isDataNodeSelected).toBeFalsy(); expect(isDataNodeSelected).toBeFalsy();
}); });
}); });

View File

@ -16,6 +16,7 @@ import { Areas } from "../../Common/Constants";
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs";
import { IPinnedRepo } from "../../Juno/JunoClient"; import { IPinnedRepo } from "../../Juno/JunoClient";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
@ -57,7 +58,10 @@ export class ResourceTreeAdapter implements ReactAdapter {
this.parameters = ko.observable(Date.now()); this.parameters = ko.observable(Date.now());
useSelectedNode.subscribe(() => this.triggerRender()); useSelectedNode.subscribe(() => this.triggerRender());
this.container.tabsManager.activeTab.subscribe((newValue: TabsBase) => this.triggerRender()); useTabs.subscribe(
() => this.triggerRender(),
(state) => state.activeTab
);
useNotebook.subscribe( useNotebook.subscribe(
() => this.triggerRender(), () => this.triggerRender(),
(state) => state.isNotebookEnabled (state) => state.isNotebookEnabled
@ -188,8 +192,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
isExpanded: false, isExpanded: false,
className: "databaseHeader", className: "databaseHeader",
children: [], children: [],
isSelected: () => isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()),
useSelectedNode.getState().isDataNodeSelected(this.container.tabsManager.activeTab(), database.id()),
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(this.container, database.id()), contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(this.container, database.id()),
onClick: async (isExpanded) => { onClick: async (isExpanded) => {
// Rewritten version of expandCollapseDatabase(): // Rewritten version of expandCollapseDatabase():
@ -204,7 +207,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
databaseNode.isLoading = false; databaseNode.isLoading = false;
useSelectedNode.getState().setSelectedNode(database); useSelectedNode.getState().setSelectedNode(database);
useCommandBar.getState().setContextButtons([]); 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), onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(database),
}; };
@ -215,9 +218,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
isSelected: () => isSelected: () =>
useSelectedNode useSelectedNode
.getState() .getState()
.isDataNodeSelected(this.container.tabsManager.activeTab(), database.id(), undefined, [ .isDataNodeSelected(database.id(), undefined, [ViewModels.CollectionTabKind.DatabaseSettings]),
ViewModels.CollectionTabKind.DatabaseSettings,
]),
onClick: database.onSettingsClick.bind(database), onClick: database.onSettingsClick.bind(database),
}); });
} }
@ -265,7 +266,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
isSelected: () => isSelected: () =>
useSelectedNode useSelectedNode
.getState() .getState()
.isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id(), [ .isDataNodeSelected(collection.databaseId, collection.id(), [
ViewModels.CollectionTabKind.Documents, ViewModels.CollectionTabKind.Documents,
ViewModels.CollectionTabKind.Graph, ViewModels.CollectionTabKind.Graph,
]), ]),
@ -283,9 +284,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
isSelected: () => isSelected: () =>
useSelectedNode useSelectedNode
.getState() .getState()
.isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id(), [ .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.SchemaAnalyzer]),
ViewModels.CollectionTabKind.SchemaAnalyzer,
]),
}); });
} }
@ -296,9 +295,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
isSelected: () => isSelected: () =>
useSelectedNode useSelectedNode
.getState() .getState()
.isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id(), [ .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Settings]),
ViewModels.CollectionTabKind.Settings,
]),
}); });
} }
@ -326,9 +323,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
isSelected: () => isSelected: () =>
useSelectedNode useSelectedNode
.getState() .getState()
.isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id(), [ .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Conflicts]),
ViewModels.CollectionTabKind.Conflicts,
]),
}); });
} }
@ -343,7 +338,9 @@ export class ResourceTreeAdapter implements ReactAdapter {
// Rewritten version of expandCollapseCollection // Rewritten version of expandCollapseCollection
useSelectedNode.getState().setSelectedNode(collection); useSelectedNode.getState().setSelectedNode(collection);
useCommandBar.getState().setContextButtons([]); useCommandBar.getState().setContextButtons([]);
this.container.tabsManager.refreshActiveTab( useTabs
.getState()
.refreshActiveTab(
(tab: TabsBase) => (tab: TabsBase) =>
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
); );
@ -355,10 +352,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
collection.loadTriggers(); collection.loadTriggers();
} }
}, },
isSelected: () => isSelected: () => useSelectedNode.getState().isDataNodeSelected(collection.databaseId, collection.id()),
useSelectedNode
.getState()
.isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id()),
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(collection), onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(collection),
}; };
} }
@ -372,14 +366,16 @@ export class ResourceTreeAdapter implements ReactAdapter {
isSelected: () => isSelected: () =>
useSelectedNode useSelectedNode
.getState() .getState()
.isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id(), [ .isDataNodeSelected(collection.databaseId, collection.id(), [
ViewModels.CollectionTabKind.StoredProcedures, ViewModels.CollectionTabKind.StoredProcedures,
]), ]),
contextMenu: ResourceTreeContextMenuButtonFactory.createStoreProcedureContextMenuItems(this.container, sp), contextMenu: ResourceTreeContextMenuButtonFactory.createStoreProcedureContextMenuItems(this.container, sp),
})), })),
onClick: () => { onClick: () => {
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.StoredProcedures); collection.selectedSubnodeKind(ViewModels.CollectionTabKind.StoredProcedures);
this.container.tabsManager.refreshActiveTab( useTabs
.getState()
.refreshActiveTab(
(tab: TabsBase) => (tab: TabsBase) =>
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
); );
@ -396,7 +392,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
isSelected: () => isSelected: () =>
useSelectedNode useSelectedNode
.getState() .getState()
.isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id(), [ .isDataNodeSelected(collection.databaseId, collection.id(), [
ViewModels.CollectionTabKind.UserDefinedFunctions, ViewModels.CollectionTabKind.UserDefinedFunctions,
]), ]),
contextMenu: ResourceTreeContextMenuButtonFactory.createUserDefinedFunctionContextMenuItems( contextMenu: ResourceTreeContextMenuButtonFactory.createUserDefinedFunctionContextMenuItems(
@ -406,7 +402,9 @@ export class ResourceTreeAdapter implements ReactAdapter {
})), })),
onClick: () => { onClick: () => {
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.UserDefinedFunctions); collection.selectedSubnodeKind(ViewModels.CollectionTabKind.UserDefinedFunctions);
this.container.tabsManager.refreshActiveTab( useTabs
.getState()
.refreshActiveTab(
(tab: TabsBase) => (tab: TabsBase) =>
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
); );
@ -423,14 +421,14 @@ export class ResourceTreeAdapter implements ReactAdapter {
isSelected: () => isSelected: () =>
useSelectedNode useSelectedNode
.getState() .getState()
.isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id(), [ .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Triggers]),
ViewModels.CollectionTabKind.Triggers,
]),
contextMenu: ResourceTreeContextMenuButtonFactory.createTriggerContextMenuItems(this.container, trigger), contextMenu: ResourceTreeContextMenuButtonFactory.createTriggerContextMenuItems(this.container, trigger),
})), })),
onClick: () => { onClick: () => {
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Triggers); collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Triggers);
this.container.tabsManager.refreshActiveTab( useTabs
.getState()
.refreshActiveTab(
(tab: TabsBase) => (tab: TabsBase) =>
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId 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), children: this.getSchemaNodes(collection.schema.fields),
onClick: () => { onClick: () => {
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Schema); collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Schema);
this.container.tabsManager.refreshActiveTab( useTabs.getState().refreshActiveTab((tab: TabsBase) => tab.collection && tab.collection.rid === collection.rid);
(tab: TabsBase) => tab.collection && tab.collection.rid === collection.rid
);
}, },
}; };
} }
@ -584,7 +580,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
className: "notebookHeader galleryHeader", className: "notebookHeader galleryHeader",
onClick: () => this.container.openGallery(), onClick: () => this.container.openGallery(),
isSelected: () => { isSelected: () => {
const activeTab = this.container.tabsManager.activeTab(); const activeTab = useTabs.getState().activeTab;
return activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.Gallery; return activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.Gallery;
}, },
}; };
@ -678,7 +674,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
className: "notebookHeader", className: "notebookHeader",
onClick: () => onFileClick(item), onClick: () => onFileClick(item),
isSelected: () => { isSelected: () => {
const activeTab = this.container.tabsManager.activeTab(); const activeTab = useTabs.getState().activeTab;
return ( return (
activeTab && activeTab &&
activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
@ -833,7 +829,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
} }
}, },
isSelected: () => { isSelected: () => {
const activeTab = this.container.tabsManager.activeTab(); const activeTab = useTabs.getState().activeTab;
return ( return (
activeTab && activeTab &&
activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&

View File

@ -3,6 +3,7 @@ import * as React from "react";
import CollectionIcon from "../../../images/tree-collection.svg"; import CollectionIcon from "../../../images/tree-collection.svg";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent"; import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
@ -24,7 +25,10 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
(state) => state.resourceTokenCollection (state) => state.resourceTokenCollection
); );
useSelectedNode.subscribe(() => this.triggerRender()); useSelectedNode.subscribe(() => this.triggerRender());
this.container.tabsManager && this.container.tabsManager.activeTab.subscribe(() => this.triggerRender()); useTabs.subscribe(
() => this.triggerRender(),
(state) => state.activeTab
);
this.triggerRender(); this.triggerRender();
} }
@ -55,9 +59,7 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
isSelected: () => isSelected: () =>
useSelectedNode useSelectedNode
.getState() .getState()
.isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id(), [ .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Documents]),
ViewModels.CollectionTabKind.Documents,
]),
}); });
const collectionNode: TreeNode = { const collectionNode: TreeNode = {
@ -70,14 +72,13 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
// Rewritten version of expandCollapseCollection // Rewritten version of expandCollapseCollection
useSelectedNode.getState().setSelectedNode(collection); useSelectedNode.getState().setSelectedNode(collection);
useCommandBar.getState().setContextButtons([]); useCommandBar.getState().setContextButtons([]);
this.container.tabsManager.refreshActiveTab( useTabs
.getState()
.refreshActiveTab(
(tab) => tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId (tab) => tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
); );
}, },
isSelected: () => isSelected: () => useSelectedNode.getState().isDataNodeSelected(collection.databaseId, collection.id()),
useSelectedNode
.getState()
.isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id()),
}; };
return { return {

View File

@ -4,6 +4,7 @@ import * as Constants from "../../Common/Constants";
import { deleteStoredProcedure } from "../../Common/dataAccess/deleteStoredProcedure"; import { deleteStoredProcedure } from "../../Common/dataAccess/deleteStoredProcedure";
import { executeStoredProcedure } from "../../Common/dataAccess/executeStoredProcedure"; import { executeStoredProcedure } from "../../Common/dataAccess/executeStoredProcedure";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
@ -62,7 +63,7 @@ export default class StoredProcedure {
} }
public static create(source: ViewModels.Collection, event: MouseEvent) { 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 = <StoredProcedureDefinition>{ const storedProcedure = <StoredProcedureDefinition>{
id: "", id: "",
body: sampleStoredProcedureBody, body: sampleStoredProcedureBody,
@ -84,7 +85,7 @@ export default class StoredProcedure {
} }
); );
source.container.tabsManager.activateNewTab(storedProcedureTab); useTabs.getState().activateNewTab(storedProcedureTab);
} }
public select() { public select() {
@ -99,14 +100,16 @@ export default class StoredProcedure {
public open = () => { public open = () => {
this.select(); this.select();
const storedProcedureTabs: NewStoredProcedureTab[] = this.container.tabsManager.getTabs( const storedProcedureTabs: NewStoredProcedureTab[] = useTabs
.getState()
.getTabs(
ViewModels.CollectionTabKind.StoredProcedures, ViewModels.CollectionTabKind.StoredProcedures,
(tab: TabsBase) => tab.node && tab.node.rid === this.rid (tab: TabsBase) => tab.node && tab.node.rid === this.rid
) as NewStoredProcedureTab[]; ) as NewStoredProcedureTab[];
let storedProcedureTab: NewStoredProcedureTab = storedProcedureTabs && storedProcedureTabs[0]; let storedProcedureTab: NewStoredProcedureTab = storedProcedureTabs && storedProcedureTabs[0];
if (storedProcedureTab) { if (storedProcedureTab) {
this.container.tabsManager.activateTab(storedProcedureTab); useTabs.getState().activateTab(storedProcedureTab);
} else { } else {
const storedProcedureData = <StoredProcedureDefinition>{ const storedProcedureData = <StoredProcedureDefinition>{
_rid: this.rid, _rid: this.rid,
@ -131,7 +134,7 @@ export default class StoredProcedure {
} }
); );
this.container.tabsManager.activateNewTab(storedProcedureTab); useTabs.getState().activateNewTab(storedProcedureTab);
} }
}; };
public delete() { public delete() {
@ -141,7 +144,7 @@ export default class StoredProcedure {
deleteStoredProcedure(this.collection.databaseId, this.collection.id(), this.id()).then( 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); this.collection.children.remove(this);
}, },
(reason) => {} (reason) => {}
@ -149,7 +152,9 @@ export default class StoredProcedure {
} }
public execute(params: string[], partitionKeyValue?: string): void { public execute(params: string[], partitionKeyValue?: string): void {
const sprocTabs: NewStoredProcedureTab[] = this.container.tabsManager.getTabs( const sprocTabs: NewStoredProcedureTab[] = useTabs
.getState()
.getTabs(
ViewModels.CollectionTabKind.StoredProcedures, ViewModels.CollectionTabKind.StoredProcedures,
(tab: TabsBase) => tab.node && tab.node.rid === this.rid (tab: TabsBase) => tab.node && tab.node.rid === this.rid
) as NewStoredProcedureTab[]; ) as NewStoredProcedureTab[];

View File

@ -3,6 +3,7 @@ import * as ko from "knockout";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { deleteTrigger } from "../../Common/dataAccess/deleteTrigger"; import { deleteTrigger } from "../../Common/dataAccess/deleteTrigger";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
@ -42,7 +43,7 @@ export default class Trigger {
} }
public static create(source: ViewModels.Collection, event: MouseEvent) { 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 = <StoredProcedureDefinition>{ const trigger = <StoredProcedureDefinition>{
id: "", id: "",
body: "function trigger(){}", body: "function trigger(){}",
@ -60,20 +61,19 @@ export default class Trigger {
node: source, node: source,
}); });
source.container.tabsManager.activateNewTab(triggerTab); useTabs.getState().activateNewTab(triggerTab);
} }
public open = () => { public open = () => {
this.select(); this.select();
const triggerTabs: TriggerTab[] = this.container.tabsManager.getTabs( const triggerTabs: TriggerTab[] = useTabs
ViewModels.CollectionTabKind.Triggers, .getState()
(tab) => tab.node && tab.node.rid === this.rid .getTabs(ViewModels.CollectionTabKind.Triggers, (tab) => tab.node && tab.node.rid === this.rid) as TriggerTab[];
) as TriggerTab[];
let triggerTab: TriggerTab = triggerTabs && triggerTabs[0]; let triggerTab: TriggerTab = triggerTabs && triggerTabs[0];
if (triggerTab) { if (triggerTab) {
this.container.tabsManager.activateTab(triggerTab); useTabs.getState().activateTab(triggerTab);
} else { } else {
const triggerData = <StoredProcedureDefinition>{ const triggerData = <StoredProcedureDefinition>{
_rid: this.rid, _rid: this.rid,
@ -94,7 +94,7 @@ export default class Trigger {
node: this, 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( 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); this.collection.children.remove(this);
}, },
(reason) => {} (reason) => {}

View File

@ -3,6 +3,7 @@ import * as ko from "knockout";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { deleteUserDefinedFunction } from "../../Common/dataAccess/deleteUserDefinedFunction"; import { deleteUserDefinedFunction } from "../../Common/dataAccess/deleteUserDefinedFunction";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
@ -30,7 +31,7 @@ export default class UserDefinedFunction {
} }
public static create(source: ViewModels.Collection, event: MouseEvent) { 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 = { const userDefinedFunction = {
id: "", id: "",
body: "function userDefinedFunction(){}", body: "function userDefinedFunction(){}",
@ -46,20 +47,22 @@ export default class UserDefinedFunction {
node: source, node: source,
}); });
source.container.tabsManager.activateNewTab(userDefinedFunctionTab); useTabs.getState().activateNewTab(userDefinedFunctionTab);
} }
public open = () => { public open = () => {
this.select(); this.select();
const userDefinedFunctionTabs: UserDefinedFunctionTab[] = this.container.tabsManager.getTabs( const userDefinedFunctionTabs: UserDefinedFunctionTab[] = useTabs
.getState()
.getTabs(
ViewModels.CollectionTabKind.UserDefinedFunctions, ViewModels.CollectionTabKind.UserDefinedFunctions,
(tab) => tab.node?.rid === this.rid (tab) => tab.node?.rid === this.rid
) as UserDefinedFunctionTab[]; ) as UserDefinedFunctionTab[];
let userDefinedFunctionTab: UserDefinedFunctionTab = userDefinedFunctionTabs && userDefinedFunctionTabs[0]; let userDefinedFunctionTab: UserDefinedFunctionTab = userDefinedFunctionTabs && userDefinedFunctionTabs[0];
if (userDefinedFunctionTab) { if (userDefinedFunctionTab) {
this.container.tabsManager.activateTab(userDefinedFunctionTab); useTabs.getState().activateTab(userDefinedFunctionTab);
} else { } else {
const userDefinedFunctionData = { const userDefinedFunctionData = {
_rid: this.rid, _rid: this.rid,
@ -78,7 +81,7 @@ export default class UserDefinedFunction {
node: this, 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( 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); this.collection.children.remove(this);
}, },
(reason) => {} (reason) => {}

View File

@ -2,6 +2,7 @@ import _ from "underscore";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import * as Constants from "../Common/Constants"; import * as Constants from "../Common/Constants";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { useSelectedNode } from "./useSelectedNode";
interface DatabasesState { interface DatabasesState {
databases: ViewModels.Database[]; databases: ViewModels.Database[];
@ -17,6 +18,7 @@ interface DatabasesState {
isLastCollection: () => boolean; isLastCollection: () => boolean;
loadDatabaseOffers: () => Promise<void>; loadDatabaseOffers: () => Promise<void>;
isFirstResourceCreated: () => boolean; isFirstResourceCreated: () => boolean;
findSelectedDatabase: () => ViewModels.Database;
} }
export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
@ -112,4 +114,19 @@ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
return false; 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;
},
})); }));

View File

@ -1,17 +1,13 @@
import _ from "underscore";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import TabsBase from "./Tabs/TabsBase"; import { useTabs } from "../hooks/useTabs";
import { useDatabases } from "./useDatabases";
export interface SelectedNodeState { export interface SelectedNodeState {
selectedNode: ViewModels.TreeNode; selectedNode: ViewModels.TreeNode;
setSelectedNode: (node: ViewModels.TreeNode) => void; setSelectedNode: (node: ViewModels.TreeNode) => void;
isDatabaseNodeOrNoneSelected: () => boolean; isDatabaseNodeOrNoneSelected: () => boolean;
findSelectedDatabase: () => ViewModels.Database;
findSelectedCollection: () => ViewModels.Collection; findSelectedCollection: () => ViewModels.Collection;
isDataNodeSelected: ( isDataNodeSelected: (
activeTab: TabsBase,
databaseId: string, databaseId: string,
collectionId?: string, collectionId?: string,
subnodeKinds?: ViewModels.CollectionTabKind[] subnodeKinds?: ViewModels.CollectionTabKind[]
@ -25,30 +21,11 @@ export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) =>
const selectedNode = get().selectedNode; const selectedNode = get().selectedNode;
return !selectedNode || selectedNode.nodeKind === "Database"; 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 => { findSelectedCollection: (): ViewModels.Collection => {
const selectedNode = get().selectedNode; const selectedNode = get().selectedNode;
return (selectedNode.nodeKind === "Collection" ? selectedNode : selectedNode.collection) as ViewModels.Collection; return (selectedNode.nodeKind === "Collection" ? selectedNode : selectedNode.collection) as ViewModels.Collection;
}, },
isDataNodeSelected: ( isDataNodeSelected: (
activeTab: TabsBase,
databaseId: string, databaseId: string,
collectionId?: string, collectionId?: string,
subnodeKinds?: ViewModels.CollectionTabKind[] subnodeKinds?: ViewModels.CollectionTabKind[]
@ -70,6 +47,7 @@ export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) =>
return true; return true;
} }
const activeTab = useTabs.getState().activeTab;
const selectedSubnodeKind = collectionId const selectedSubnodeKind = collectionId
? (selectedNode as ViewModels.Collection).selectedSubnodeKind() ? (selectedNode as ViewModels.Collection).selectedSubnodeKind()
: (selectedNode as ViewModels.Database).selectedSubnodeKind(); : (selectedNode as ViewModels.Database).selectedSubnodeKind();

View File

@ -34,7 +34,6 @@ import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less";
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less"; import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less"; import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
import "./Explorer/Controls/TreeComponent/treeComponent.less"; import "./Explorer/Controls/TreeComponent/treeComponent.less";
import { ExplorerParams } from "./Explorer/Explorer";
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less"; import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
import "./Explorer/Menus/CommandBar/CommandBarComponent.less"; import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter";
@ -56,14 +55,10 @@ initializeIcons();
const App: React.FunctionComponent = () => { const App: React.FunctionComponent = () => {
const [isLeftPaneExpanded, setIsLeftPaneExpanded] = useState<boolean>(true); const [isLeftPaneExpanded, setIsLeftPaneExpanded] = useState<boolean>(true);
const { tabs, activeTab, tabsManager } = useTabs(); const openedTabs = useTabs((state) => state.openedTabs);
const explorerParams: ExplorerParams = {
tabsManager,
};
const config = useConfig(); const config = useConfig();
const explorer = useKnockoutExplorer(config?.platform, explorerParams); const explorer = useKnockoutExplorer(config?.platform);
const toggleLeftPaneExpanded = () => { const toggleLeftPaneExpanded = () => {
setIsLeftPaneExpanded(!isLeftPaneExpanded); setIsLeftPaneExpanded(!isLeftPaneExpanded);
@ -100,8 +95,8 @@ const App: React.FunctionComponent = () => {
</div> </div>
</div> </div>
{/* Collections Tree - End */} {/* Collections Tree - End */}
{tabs.length === 0 && <SplashScreen explorer={explorer} />} {openedTabs.length === 0 && <SplashScreen explorer={explorer} />}
<Tabs tabs={tabs} activeTab={activeTab} /> <Tabs />
</div> </div>
{/* Collections Tree and Tabs - End */} {/* Collections Tree and Tabs - End */}
<div <div

View File

@ -8,7 +8,7 @@ import { configContext, Platform, updateConfigContext } from "../ConfigContext";
import { ActionType, DataExplorerAction } from "../Contracts/ActionContracts"; import { ActionType, DataExplorerAction } from "../Contracts/ActionContracts";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { DataExplorerInputsFrame } from "../Contracts/ViewModels"; import { DataExplorerInputsFrame } from "../Contracts/ViewModels";
import Explorer, { ExplorerParams } from "../Explorer/Explorer"; import Explorer from "../Explorer/Explorer";
import { handleOpenAction } from "../Explorer/OpenActions/OpenActions"; import { handleOpenAction } from "../Explorer/OpenActions/OpenActions";
import { useDatabases } from "../Explorer/useDatabases"; import { useDatabases } from "../Explorer/useDatabases";
import { import {
@ -37,20 +37,20 @@ import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
// This hook has a LOT of magic, but ideally we can delete it once we have removed KO and switched entirely to React // This hook has a LOT of magic, but ideally we can delete it once we have removed KO and switched entirely to React
// Please tread carefully :) // Please tread carefully :)
export function useKnockoutExplorer(platform: Platform, explorerParams: ExplorerParams): Explorer { export function useKnockoutExplorer(platform: Platform): Explorer {
const [explorer, setExplorer] = useState<Explorer>(); const [explorer, setExplorer] = useState<Explorer>();
useEffect(() => { useEffect(() => {
const effect = async () => { const effect = async () => {
if (platform) { if (platform) {
if (platform === Platform.Hosted) { if (platform === Platform.Hosted) {
const explorer = await configureHosted(explorerParams); const explorer = await configureHosted();
setExplorer(explorer); setExplorer(explorer);
} else if (platform === Platform.Emulator) { } else if (platform === Platform.Emulator) {
const explorer = configureEmulator(explorerParams); const explorer = configureEmulator();
setExplorer(explorer); setExplorer(explorer);
} else if (platform === Platform.Portal) { } else if (platform === Platform.Portal) {
const explorer = await configurePortal(explorerParams); const explorer = await configurePortal();
setExplorer(explorer); setExplorer(explorer);
} }
} }
@ -67,21 +67,21 @@ export function useKnockoutExplorer(platform: Platform, explorerParams: Explorer
return explorer; return explorer;
} }
async function configureHosted(explorerParams: ExplorerParams): Promise<Explorer> { async function configureHosted(): Promise<Explorer> {
const win = (window as unknown) as HostedExplorerChildFrame; const win = (window as unknown) as HostedExplorerChildFrame;
if (win.hostedConfig.authType === AuthType.EncryptedToken) { if (win.hostedConfig.authType === AuthType.EncryptedToken) {
return configureHostedWithEncryptedToken(win.hostedConfig, explorerParams); return configureHostedWithEncryptedToken(win.hostedConfig);
} else if (win.hostedConfig.authType === AuthType.ResourceToken) { } else if (win.hostedConfig.authType === AuthType.ResourceToken) {
return configureHostedWithResourceToken(win.hostedConfig, explorerParams); return configureHostedWithResourceToken(win.hostedConfig);
} else if (win.hostedConfig.authType === AuthType.ConnectionString) { } else if (win.hostedConfig.authType === AuthType.ConnectionString) {
return configureHostedWithConnectionString(win.hostedConfig, explorerParams); return configureHostedWithConnectionString(win.hostedConfig);
} else if (win.hostedConfig.authType === AuthType.AAD) { } else if (win.hostedConfig.authType === AuthType.AAD) {
return configureHostedWithAAD(win.hostedConfig, explorerParams); return configureHostedWithAAD(win.hostedConfig);
} }
throw new Error(`Unknown hosted config: ${win.hostedConfig}`); throw new Error(`Unknown hosted config: ${win.hostedConfig}`);
} }
async function configureHostedWithAAD(config: AAD, explorerParams: ExplorerParams): Promise<Explorer> { async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
// TODO: Refactor. updateUserContext needs to be called twice because listKeys below depends on userContext.authorizationToken // TODO: Refactor. updateUserContext needs to be called twice because listKeys below depends on userContext.authorizationToken
updateUserContext({ updateUserContext({
authType: AuthType.AAD, authType: AuthType.AAD,
@ -120,11 +120,11 @@ async function configureHostedWithAAD(config: AAD, explorerParams: ExplorerParam
databaseAccount: config.databaseAccount, databaseAccount: config.databaseAccount,
masterKey: keys.primaryMasterKey, masterKey: keys.primaryMasterKey,
}); });
const explorer = new Explorer(explorerParams); const explorer = new Explorer();
return explorer; return explorer;
} }
function configureHostedWithConnectionString(config: ConnectionString, explorerParams: ExplorerParams): Explorer { function configureHostedWithConnectionString(config: ConnectionString): Explorer {
const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind); const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind);
const databaseAccount = { const databaseAccount = {
id: "", id: "",
@ -142,11 +142,11 @@ function configureHostedWithConnectionString(config: ConnectionString, explorerP
databaseAccount, databaseAccount,
masterKey: config.masterKey, masterKey: config.masterKey,
}); });
const explorer = new Explorer(explorerParams); const explorer = new Explorer();
return explorer; return explorer;
} }
function configureHostedWithResourceToken(config: ResourceToken, explorerParams: ExplorerParams): Explorer { function configureHostedWithResourceToken(config: ResourceToken): Explorer {
const parsedResourceToken = parseResourceTokenConnectionString(config.resourceToken); const parsedResourceToken = parseResourceTokenConnectionString(config.resourceToken);
const databaseAccount = { const databaseAccount = {
id: "", id: "",
@ -167,11 +167,11 @@ function configureHostedWithResourceToken(config: ResourceToken, explorerParams:
partitionKey: parsedResourceToken.partitionKey, partitionKey: parsedResourceToken.partitionKey,
}, },
}); });
const explorer = new Explorer(explorerParams); const explorer = new Explorer();
return explorer; return explorer;
} }
function configureHostedWithEncryptedToken(config: EncryptedToken, explorerParams: ExplorerParams): Explorer { function configureHostedWithEncryptedToken(config: EncryptedToken): Explorer {
const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind); const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind);
updateUserContext({ updateUserContext({
authType: AuthType.EncryptedToken, authType: AuthType.EncryptedToken,
@ -185,20 +185,20 @@ function configureHostedWithEncryptedToken(config: EncryptedToken, explorerParam
properties: getDatabaseAccountPropertiesFromMetadata(config.encryptedTokenMetadata), properties: getDatabaseAccountPropertiesFromMetadata(config.encryptedTokenMetadata),
}, },
}); });
const explorer = new Explorer(explorerParams); const explorer = new Explorer();
return explorer; return explorer;
} }
function configureEmulator(explorerParams: ExplorerParams): Explorer { function configureEmulator(): Explorer {
updateUserContext({ updateUserContext({
databaseAccount: emulatorAccount, databaseAccount: emulatorAccount,
authType: AuthType.MasterKey, authType: AuthType.MasterKey,
}); });
const explorer = new Explorer(explorerParams); const explorer = new Explorer();
return explorer; return explorer;
} }
async function configurePortal(explorerParams: ExplorerParams): Promise<Explorer> { async function configurePortal(): Promise<Explorer> {
updateUserContext({ updateUserContext({
authType: AuthType.AAD, authType: AuthType.AAD,
}); });
@ -214,7 +214,7 @@ async function configurePortal(explorerParams: ExplorerParams): Promise<Explorer
); );
console.dir(message); console.dir(message);
updateContextsFromPortalMessage(message); updateContextsFromPortalMessage(message);
const explorer = new Explorer(explorerParams); const explorer = new Explorer();
// In development mode, save the iframe message from the portal in session storage. // In development mode, save the iframe message from the portal in session storage.
// This allows webpack hot reload to funciton properly // This allows webpack hot reload to funciton properly
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
@ -250,7 +250,7 @@ async function configurePortal(explorerParams: ExplorerParams): Promise<Explorer
} }
updateContextsFromPortalMessage(inputs); updateContextsFromPortalMessage(inputs);
const explorer = new Explorer(explorerParams); const explorer = new Explorer();
resolve(explorer); resolve(explorer);
if (openAction) { if (openAction) {
handleOpenAction(openAction, useDatabases.getState().databases, explorer); handleOpenAction(openAction, useDatabases.getState().databases, explorer);

View File

@ -1,18 +1,77 @@
import { useState } from "react"; import create, { UseStore } from "zustand";
import * as ViewModels from "../Contracts/ViewModels";
import TabsBase from "../Explorer/Tabs/TabsBase"; import TabsBase from "../Explorer/Tabs/TabsBase";
import { TabsManager } from "../Explorer/Tabs/TabsManager";
import { useObservable } from "./useObservable";
export type UseTabs = { interface TabsState {
tabs: readonly TabsBase[]; openedTabs: TabsBase[];
activeTab: TabsBase; activeTab: TabsBase;
tabsManager: TabsManager; activateTab: (tab: TabsBase) => void;
}; activateNewTab: (tab: TabsBase) => void;
updateTab: (tab: TabsBase) => void;
export function useTabs(): UseTabs { getTabs: (tabKind: ViewModels.CollectionTabKind, comparator?: (tab: TabsBase) => boolean) => TabsBase[];
const [tabsManager] = useState(() => new TabsManager()); refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void;
const tabs = useObservable(tabsManager.openedTabs); closeTabsByComparator: (comparator: (tab: TabsBase) => boolean) => void;
const activeTab = useObservable(tabsManager.activeTab); closeTab: (tab: TabsBase) => void;
return { tabs, activeTab, tabsManager };
} }
export const useTabs: UseStore<TabsState> = 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 });
},
}));