mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-20 09:20:16 +00:00
Move tabs manager to zustand (#915)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import type { TabOptions } from "../../../Contracts/ViewModels";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import Explorer from "../../Explorer";
|
||||
import TabsBase from "../TabsBase";
|
||||
import MongoShellTabComponent, { IMongoShellTabAccessor, IMongoShellTabComponentProps } from "./MongoShellTabComponent";
|
||||
@@ -33,7 +34,7 @@ export class NewMongoShellTab extends TabsBase {
|
||||
}
|
||||
|
||||
public onTabClick(): void {
|
||||
this.manager?.activateTab(this);
|
||||
useTabs.getState().activateTab(this);
|
||||
this.iMongoShellTabAccessor.onTabClickEvent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import type { QueryTabOptions } from "../../../Contracts/ViewModels";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import Explorer from "../../Explorer";
|
||||
import { IQueryTabComponentProps, ITabAccessor } from "../../Tabs/QueryTab/QueryTabComponent";
|
||||
import TabsBase from "../TabsBase";
|
||||
@@ -40,12 +41,12 @@ export class NewQueryTab extends TabsBase {
|
||||
}
|
||||
|
||||
public onTabClick(): void {
|
||||
this.manager?.activateTab(this);
|
||||
useTabs.getState().activateTab(this);
|
||||
this.iTabAccessor.onTabClickEvent();
|
||||
}
|
||||
|
||||
public onCloseTabButtonClick(): void {
|
||||
this.manager?.closeTab(this);
|
||||
useTabs.getState().closeTab(this);
|
||||
if (this.iTabAccessor) {
|
||||
this.iTabAccessor.onCloseClickEvent(true);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ import { EditorReact } from "../../Controls/Editor/EditorReact";
|
||||
import Explorer from "../../Explorer";
|
||||
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import TabsBase from "../TabsBase";
|
||||
import { TabsManager } from "../TabsManager";
|
||||
import "./QueryTabComponent.less";
|
||||
|
||||
enum ToggleState {
|
||||
@@ -65,7 +64,6 @@ export interface IQueryTabComponentProps {
|
||||
partitionKey: DataModels.PartitionKey;
|
||||
container: Explorer;
|
||||
activeTab?: TabsBase;
|
||||
tabManager?: TabsManager;
|
||||
onTabAccessor: (instance: ITabAccessor) => void;
|
||||
isPreferredApiMongoDB?: boolean;
|
||||
monacoEditorSetting?: string;
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from "react";
|
||||
import { ExecuteSprocResult } from "../../../Common/dataAccess/executeStoredProcedure";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import Explorer from "../../Explorer";
|
||||
import StoredProcedure from "../../Tree/StoredProcedure";
|
||||
import ScriptTabBase from "../ScriptTabBase";
|
||||
@@ -51,12 +52,12 @@ export class NewStoredProcedureTab extends ScriptTabBase {
|
||||
}
|
||||
|
||||
public onTabClick(): void {
|
||||
this.manager?.activateTab(this);
|
||||
useTabs.getState().activateTab(this);
|
||||
this.iStoreProcAccessor.onTabClickEvent();
|
||||
}
|
||||
|
||||
public onCloseTabButtonClick(): void {
|
||||
this.manager?.closeTab(this);
|
||||
useTabs.getState().closeTab(this);
|
||||
}
|
||||
|
||||
public onExecuteSprocsResult(result: ExecuteSprocResult): void {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ExecuteSprocResult } from "../../../Common/dataAccess/executeStoredProc
|
||||
import { updateStoredProcedure } from "../../../Common/dataAccess/updateStoredProcedure";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { useNotificationConsole } from "../../../hooks/useNotificationConsole";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
||||
import Explorer from "../../Explorer";
|
||||
@@ -144,7 +145,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
}
|
||||
|
||||
public onTabClick(): void {
|
||||
if (this.props.container.tabsManager.openedTabs().length > 0) {
|
||||
if (useTabs.getState().openedTabs.length > 0) {
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
}
|
||||
}
|
||||
@@ -396,10 +397,8 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
editorModel && editorModel.setValue(createdResource.body as string);
|
||||
this.props.scriptTabBaseInstance.editorContent.setBaseline(createdResource.body as string);
|
||||
this.node = this.collection.createStoredProcedureNode(createdResource);
|
||||
this.props.container.tabsManager.openedTabs()[
|
||||
this.props.container.tabsManager.openedTabs().length - 1
|
||||
].node = this.node;
|
||||
|
||||
this.props.scriptTabBaseInstance.node = this.node;
|
||||
useTabs.getState().updateTab(this.props.scriptTabBaseInstance);
|
||||
this.props.scriptTabBaseInstance.editorState(ViewModels.ScriptEditorState.exisitingNoEdits);
|
||||
|
||||
this.setState({
|
||||
|
||||
@@ -3,28 +3,32 @@ import React, { useEffect, useRef, useState } from "react";
|
||||
import loadingIcon from "../../../images/circular_loader_black_16x16.gif";
|
||||
import errorIcon from "../../../images/close-black.svg";
|
||||
import { useObservable } from "../../hooks/useObservable";
|
||||
import { useTabs } from "../../hooks/useTabs";
|
||||
import TabsBase from "./TabsBase";
|
||||
|
||||
type Tab = TabsBase | (TabsBase & { render: () => JSX.Element });
|
||||
|
||||
export const Tabs = ({ tabs, activeTab }: { tabs: readonly Tab[]; activeTab: Tab }): JSX.Element => (
|
||||
<div className="tabsManagerContainer">
|
||||
<div id="content" className="flexContainer hideOverflows">
|
||||
<div className="nav-tabs-margin">
|
||||
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
|
||||
{tabs.map((tab) => (
|
||||
<TabNav key={tab.tabId} tab={tab} active={activeTab === tab} />
|
||||
export const Tabs = (): JSX.Element => {
|
||||
const { openedTabs, activeTab } = useTabs();
|
||||
return (
|
||||
<div className="tabsManagerContainer">
|
||||
<div id="content" className="flexContainer hideOverflows">
|
||||
<div className="nav-tabs-margin">
|
||||
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
|
||||
{openedTabs.map((tab) => (
|
||||
<TabNav key={tab.tabId} tab={tab} active={activeTab === tab} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="tabPanesContainer">
|
||||
{openedTabs.map((tab) => (
|
||||
<TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="tabPanesContainer">
|
||||
{tabs.map((tab) => (
|
||||
<TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
|
||||
const [hovering, setHovering] = useState(false);
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as ThemeUtility from "../../Common/ThemeUtility";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { useNotificationConsole } from "../../hooks/useNotificationConsole";
|
||||
import { useTabs } from "../../hooks/useTabs";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
@@ -11,7 +12,6 @@ import Explorer from "../Explorer";
|
||||
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { useSelectedNode } from "../useSelectedNode";
|
||||
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
|
||||
import { TabsManager } from "./TabsManager";
|
||||
// TODO: Use specific actions for logging telemetry data
|
||||
export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
private static id = 0;
|
||||
@@ -28,7 +28,6 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
public isExecutionError = ko.observable(false);
|
||||
public isExecuting = ko.observable(false);
|
||||
public pendingNotification?: ko.Observable<DataModels.Notification>;
|
||||
public manager?: TabsManager;
|
||||
protected _theme: string;
|
||||
public onLoadStartKey: number;
|
||||
|
||||
@@ -60,7 +59,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
}
|
||||
|
||||
public onCloseTabButtonClick(): void {
|
||||
this.manager?.closeTab(this);
|
||||
useTabs.getState().closeTab(this);
|
||||
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {
|
||||
tabName: this.constructor.name,
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
@@ -70,7 +69,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
}
|
||||
|
||||
public onTabClick(): void {
|
||||
this.manager?.activateTab(this);
|
||||
useTabs.getState().activateTab(this);
|
||||
}
|
||||
|
||||
protected updateSelectedNode(): void {
|
||||
@@ -105,7 +104,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
|
||||
/** @deprecated this is no longer observable, bind to comparisons with manager.activeTab() instead */
|
||||
public isActive() {
|
||||
return this === this.manager?.activeTab();
|
||||
return this === useTabs.getState().activeTab;
|
||||
}
|
||||
|
||||
public onActivate(): void {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,19 @@
|
||||
import * as ko from "knockout";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { useTabs } from "../../hooks/useTabs";
|
||||
import { updateUserContext } from "../../UserContext";
|
||||
import Explorer from "../Explorer";
|
||||
import { container } from "../Controls/Settings/TestUtils";
|
||||
import DocumentId from "../Tree/DocumentId";
|
||||
import { container } from "./../Controls/Settings/TestUtils";
|
||||
import DocumentsTab from "./DocumentsTab";
|
||||
import { NewQueryTab } from "./QueryTab/QueryTab";
|
||||
import { TabsManager } from "./TabsManager";
|
||||
|
||||
describe("Tabs manager tests", () => {
|
||||
let tabsManager: TabsManager;
|
||||
let explorer: Explorer;
|
||||
describe("useTabs tests", () => {
|
||||
let database: ViewModels.Database;
|
||||
let collection: ViewModels.Collection;
|
||||
let queryTab: NewQueryTab;
|
||||
let documentsTab: DocumentsTab;
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer();
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
id: "test",
|
||||
@@ -30,7 +26,6 @@ describe("Tabs manager tests", () => {
|
||||
});
|
||||
|
||||
database = {
|
||||
container: explorer,
|
||||
id: ko.observable<string>("test"),
|
||||
isDatabaseShared: () => false,
|
||||
} as ViewModels.Database;
|
||||
@@ -38,7 +33,6 @@ describe("Tabs manager tests", () => {
|
||||
database.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
|
||||
|
||||
collection = {
|
||||
container: explorer,
|
||||
databaseId: "test",
|
||||
id: ko.observable<string>("test"),
|
||||
} as ViewModels.Collection;
|
||||
@@ -76,63 +70,70 @@ describe("Tabs manager tests", () => {
|
||||
documentsTab.tabId = "2";
|
||||
});
|
||||
|
||||
beforeEach(() => (tabsManager = new TabsManager()));
|
||||
beforeEach(() => useTabs.setState({ openedTabs: [], activeTab: undefined }));
|
||||
|
||||
it("open new tabs", () => {
|
||||
tabsManager.activateNewTab(queryTab);
|
||||
expect(tabsManager.openedTabs().length).toBe(1);
|
||||
expect(tabsManager.openedTabs()[0]).toEqual(queryTab);
|
||||
expect(tabsManager.activeTab()).toEqual(queryTab);
|
||||
const { activateNewTab } = useTabs.getState();
|
||||
activateNewTab(queryTab);
|
||||
let tabsState = useTabs.getState();
|
||||
expect(tabsState.openedTabs.length).toBe(1);
|
||||
expect(tabsState.openedTabs[0]).toEqual(queryTab);
|
||||
expect(tabsState.activeTab).toEqual(queryTab);
|
||||
expect(queryTab.isActive()).toBe(true);
|
||||
|
||||
tabsManager.activateNewTab(documentsTab);
|
||||
expect(tabsManager.openedTabs().length).toBe(2);
|
||||
expect(tabsManager.openedTabs()[1]).toEqual(documentsTab);
|
||||
expect(tabsManager.activeTab()).toEqual(documentsTab);
|
||||
activateNewTab(documentsTab);
|
||||
tabsState = useTabs.getState();
|
||||
expect(tabsState.openedTabs.length).toBe(2);
|
||||
expect(tabsState.openedTabs[1]).toEqual(documentsTab);
|
||||
expect(tabsState.activeTab).toEqual(documentsTab);
|
||||
expect(queryTab.isActive()).toBe(false);
|
||||
expect(documentsTab.isActive()).toBe(true);
|
||||
});
|
||||
|
||||
it("open existing tabs", () => {
|
||||
tabsManager.activateNewTab(queryTab);
|
||||
tabsManager.activateNewTab(documentsTab);
|
||||
tabsManager.activateTab(queryTab);
|
||||
expect(tabsManager.openedTabs().length).toBe(2);
|
||||
expect(tabsManager.activeTab()).toEqual(queryTab);
|
||||
const { activateNewTab, activateTab } = useTabs.getState();
|
||||
activateNewTab(queryTab);
|
||||
activateNewTab(documentsTab);
|
||||
activateTab(queryTab);
|
||||
|
||||
const { openedTabs, activeTab } = useTabs.getState();
|
||||
expect(openedTabs.length).toBe(2);
|
||||
expect(activeTab).toEqual(queryTab);
|
||||
expect(queryTab.isActive()).toBe(true);
|
||||
expect(documentsTab.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it("get tabs", () => {
|
||||
tabsManager.activateNewTab(queryTab);
|
||||
tabsManager.activateNewTab(documentsTab);
|
||||
const { activateNewTab, getTabs } = useTabs.getState();
|
||||
activateNewTab(queryTab);
|
||||
activateNewTab(documentsTab);
|
||||
|
||||
const queryTabs = tabsManager.getTabs(ViewModels.CollectionTabKind.Query);
|
||||
const queryTabs = getTabs(ViewModels.CollectionTabKind.Query);
|
||||
expect(queryTabs.length).toBe(1);
|
||||
expect(queryTabs[0]).toEqual(queryTab);
|
||||
|
||||
const documentsTabs = tabsManager.getTabs(
|
||||
ViewModels.CollectionTabKind.Documents,
|
||||
(tab) => tab.tabId === documentsTab.tabId
|
||||
);
|
||||
const documentsTabs = getTabs(ViewModels.CollectionTabKind.Documents, (tab) => tab.tabId === documentsTab.tabId);
|
||||
expect(documentsTabs.length).toBe(1);
|
||||
expect(documentsTabs[0]).toEqual(documentsTab);
|
||||
});
|
||||
|
||||
it("close tabs", () => {
|
||||
tabsManager.activateNewTab(queryTab);
|
||||
tabsManager.activateNewTab(documentsTab);
|
||||
const { activateNewTab, closeTab, closeTabsByComparator } = useTabs.getState();
|
||||
activateNewTab(queryTab);
|
||||
activateNewTab(documentsTab);
|
||||
closeTab(documentsTab);
|
||||
|
||||
tabsManager.closeTab(documentsTab);
|
||||
expect(tabsManager.openedTabs().length).toBe(1);
|
||||
expect(tabsManager.openedTabs()[0]).toEqual(queryTab);
|
||||
expect(tabsManager.activeTab()).toEqual(queryTab);
|
||||
let tabsState = useTabs.getState();
|
||||
expect(tabsState.openedTabs.length).toBe(1);
|
||||
expect(tabsState.openedTabs[0]).toEqual(queryTab);
|
||||
expect(tabsState.activeTab).toEqual(queryTab);
|
||||
expect(queryTab.isActive()).toBe(true);
|
||||
expect(documentsTab.isActive()).toBe(false);
|
||||
|
||||
tabsManager.closeTabsByComparator((tab) => tab.tabId === queryTab.tabId);
|
||||
expect(tabsManager.openedTabs().length).toBe(0);
|
||||
expect(tabsManager.activeTab()).toEqual(undefined);
|
||||
closeTabsByComparator((tab) => tab.tabId === queryTab.tabId);
|
||||
tabsState = useTabs.getState();
|
||||
expect(tabsState.openedTabs.length).toBe(0);
|
||||
expect(tabsState.activeTab).toEqual(undefined);
|
||||
expect(queryTab.isActive()).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user