TabsManager in react (#500)

This commit is contained in:
Jordi Bunster 2021-04-19 13:11:48 -07:00 committed by GitHub
parent 19cf203606
commit f2585bba14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 147 additions and 152 deletions

View File

@ -20,7 +20,6 @@ import QueryTab from "./Tabs/QueryTab";
import QueryTablesTab from "./Tabs/QueryTablesTab";
import { DatabaseSettingsTabV2, SettingsTabV2 } from "./Tabs/SettingsTabV2";
import StoredProcedureTab from "./Tabs/StoredProcedureTab";
import TabsManagerTemplate from "./Tabs/TabsManager.html";
import TerminalTab from "./Tabs/TerminalTab";
import TriggerTab from "./Tabs/TriggerTab";
import UserDefinedFunctionTab from "./Tabs/UserDefinedFunctionTab";
@ -34,7 +33,6 @@ ko.components.register("json-editor", new JsonEditorComponent());
ko.components.register("diff-editor", new DiffEditorComponent());
ko.components.register("dynamic-list", DynamicListComponent);
ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3);
ko.components.register("tabs-manager", { template: TabsManagerTemplate });
// Collection Tabs
[
@ -58,7 +56,6 @@ ko.components.register("tabs-manager", { template: TabsManagerTemplate });
// Panes
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
ko.components.register("add-collection-pane", new PaneComponents.AddCollectionPaneComponent());
ko.components.register("graph-new-vertex-pane", new PaneComponents.GraphNewVertexPaneComponent());
ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent());
ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent());

119
src/Explorer/Tabs/Tabs.tsx Normal file
View File

@ -0,0 +1,119 @@
import ko from "knockout";
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 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} />)}
</ul>
</div>
<div className="tabPanesContainer">
{...tabs.map((tab) => <TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />)}
</div>
</div>
</div>
);
function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
const [hovering, setHovering] = useState(false);
return (
<li
onMouseOver={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
onClick={() => tab.onTabClick()}
onKeyPress={({ nativeEvent: e }) => tab.onKeyPressActivate(undefined, e)}
className={active ? "active tabList" : "tabList"}
title={useObservable(tab.tabPath)}
aria-selected={active}
aria-expanded={active}
aria-controls={tab.tabId}
tabIndex={0}
role="tab"
>
<span className="tabNavContentContainer">
<a data-toggle="tab" href={"#" + tab.tabId} tabIndex={-1}>
<div className="tab_Content">
<span className="statusIconContainer">
{useObservable(tab.isExecutionError) && <ErrorIcon tab={tab} active={active} />}
{useObservable(tab.isExecuting) && (
<img className="loadingIcon" title="Loading" src={loadingIcon} alt="Loading" />
)}
</span>
<span className="tabNavText">{useObservable(tab.tabTitle)}</span>
<span className="tabIconSection">
<CloseButton tab={tab} active={active} hovering={hovering} />
</span>
</div>
</a>
</span>
</li>
);
}
const CloseButton = ({ tab, active, hovering }: { tab: Tab; active: boolean; hovering: boolean }) => (
<span
style={{ display: hovering || active ? undefined : "none" }}
title="Close"
role="button"
aria-label="Close Tab"
className="cancelButton"
onClick={() => tab.onCloseTabButtonClick()}
tabIndex={active ? 0 : undefined}
onKeyPress={({ nativeEvent: e }) => tab.onKeyPressClose(undefined, e)}
>
<span className="tabIcon close-Icon">
<img src={errorIcon} title="Close" alt="Close" />
</span>
</span>
);
const ErrorIcon = ({ tab, active }: { tab: Tab; active: boolean }) => (
<div
id="errorStatusIcon"
role="button"
title="Click to view more details"
tabIndex={active ? 0 : undefined}
className={active ? "actionsEnabled errorIconContainer" : "errorIconContainer"}
onClick={({ nativeEvent: e }) => tab.onErrorDetailsClick(undefined, e)}
onKeyPress={({ nativeEvent: e }) => tab.onErrorDetailsKeyPress(undefined, e)}
>
<span className="errorIcon" />
</div>
);
function TabPane({ tab, active }: { tab: Tab; active: boolean }) {
const ref = useRef<HTMLDivElement>();
const attrs = {
style: { display: active ? undefined : "none" },
className: "tabs-container",
};
useEffect((): (() => void) | void => {
const { current: element } = ref;
if (element) {
ko.applyBindings(tab, element);
const ctx = ko.contextFor(element).createChildContext(tab);
ko.applyBindingsToDescendants(ctx, element);
return () => ko.cleanNode(element);
}
if ("render" in tab) {
tab.isTemplateReady(true);
}
}, [ref, tab]);
if ("render" in tab) {
return <div {...attrs}>{tab.render()}</div>;
}
return <div {...attrs} ref={ref} data-bind="html: constructor.component.template" />;
}

View File

@ -1,5 +1,4 @@
import * as ko from "knockout";
import Q from "q";
import * as Constants from "../../Common/Constants";
import * as ThemeUtility from "../../Common/ThemeUtility";
import * as DataModels from "../../Contracts/DataModels";
@ -21,15 +20,13 @@ export default class TabsBase extends WaitsForTemplateViewModel {
public collection: ViewModels.CollectionBase;
public database: ViewModels.Database;
public rid: string;
public hasFocus: ko.Observable<boolean>;
public isMouseOver: ko.Observable<boolean>;
public tabId = `tab${TabsBase.id++}`;
public tabKind: ViewModels.CollectionTabKind;
public tabTitle: ko.Observable<string>;
public tabPath: ko.Observable<string>;
public hashLocation: ko.Observable<string>;
public isExecutionError: ko.Observable<boolean>;
public isExecuting: ko.Observable<boolean>;
public isExecutionError = ko.observable(false);
public isExecuting = ko.observable(false);
public pendingNotification?: ko.Observable<DataModels.Notification>;
public manager?: TabsManager;
protected _theme: string;
@ -42,16 +39,12 @@ export default class TabsBase extends WaitsForTemplateViewModel {
this.collection = options.collection;
this.database = options.database;
this.rid = options.rid || (this.collection && this.collection.rid) || "";
this.hasFocus = ko.observable<boolean>(false);
this.isMouseOver = ko.observable<boolean>(false);
this.tabKind = options.tabKind;
this.tabTitle = ko.observable<string>(options.title);
this.tabPath =
ko.observable(options.tabPath ?? "") ||
(this.collection &&
ko.observable<string>(`${this.collection.databaseId}>${this.collection.id()}>${this.tabTitle()}`));
this.isExecutionError = ko.observable<boolean>(false);
this.isExecuting = ko.observable<boolean>(false);
this.pendingNotification = ko.observable<DataModels.Notification>(undefined);
this.onLoadStartKey = options.onLoadStartKey;
this.hashLocation = ko.observable<string>(options.hashLocation || "");
@ -117,22 +110,12 @@ export default class TabsBase extends WaitsForTemplateViewModel {
public onActivate(): void {
this.updateSelectedNode();
if (!!this.collection) {
this.collection.selectedSubnodeKind(this.tabKind);
}
if (!!this.database) {
this.database.selectedSubnodeKind(this.tabKind);
}
this.hasFocus(true);
this.collection?.selectedSubnodeKind(this.tabKind);
this.database?.selectedSubnodeKind(this.tabKind);
this.updateGlobalHash(this.hashLocation());
this.updateNavbarWithTabsButtons();
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Open, {
tabName: this.constructor.name,
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
tabId: this.tabId,
@ -140,13 +123,8 @@ export default class TabsBase extends WaitsForTemplateViewModel {
}
public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => {
if (this.collection && this.collection.container) {
this.collection.container.expandConsole();
}
if (this.database && this.database.container) {
this.database.container.expandConsole();
}
this.collection?.container?.expandConsole();
this.database?.container?.expandConsole();
return false;
};
@ -159,9 +137,8 @@ export default class TabsBase extends WaitsForTemplateViewModel {
return true;
};
public refresh(): Q.Promise<any> {
public refresh() {
location.reload();
return Q();
}
public getContainer(): Explorer {
@ -190,8 +167,3 @@ export default class TabsBase extends WaitsForTemplateViewModel {
}
};
}
interface EditorPosition {
line: number;
column: number;
}

View File

@ -1,93 +0,0 @@
<div
id="content"
class="flexContainer hideOverflows"
data-bind="visible: activeTab() && openedTabs && openedTabs().length > 0"
>
<!-- Tabs - Start -->
<div class="nav-tabs-margin">
<ul class="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
<!-- ko foreach: openedTabs -->
<li
class="tabList"
data-bind="
attr: {
title: $data.tabPath,
'aria-selected': $parent.activeTab() === $data,
'aria-expanded': $parent.activeTab() === $data,
'aria-controls': $data.tabId
},
css:{
active: $parent.activeTab() === $data
},
hasFocus: $data.hasFocus,
event: { keypress: onKeyPressActivate },
click: $data.onTabClick,"
tabindex="0"
role="tab"
>
<span class="tabNavContentContainer">
<a data-toggle="tab" data-bind="attr: { href: '#' + $data.tabId }" tabindex="-1">
<div class="tab_Content">
<span class="statusIconContainer">
<div
class="errorIconContainer"
id="errorStatusIcon"
title="Click to view more details"
role="button"
data-bind="
click: onErrorDetailsClick,
event: { keypress: onErrorDetailsKeyPress },
attr: { tabindex: $parent.activeTab() === $data ? 0 : null },
css: { actionsEnabled: $parent.activeTab() === $data },
visible: isExecutionError"
>
<span class="errorIcon"></span>
</div>
<img
class="loadingIcon"
title="Loading"
src="/circular_loader_black_16x16.gif"
data-bind="visible: $data.isExecuting"
alt="Loading"
/>
</span>
<span class="tabNavText" data-bind="text: $data.tabTitle"></span>
<span class="tabIconSection">
<span
aria-label="Close Tab"
role="button"
class="cancelButton"
data-bind="
click: $data.onCloseTabButtonClick,
event: { keypress: onKeyPressClose },
attr: { tabindex: $parent.activeTab() === $data ? 0 : null },
visible: $parent.activeTab() === $data || $data.isMouseOver()"
title="Close"
>
<span
class="tabIcon close-Icon"
data-bind="visible: $parent.activeTab() === $data || $data.isMouseOver()"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</span>
</span>
</span>
</div>
</a>
</span>
</li>
<!-- /ko -->
</ul>
</div>
<!-- Tabs -- End -->
<!-- Tabs Panes -- Start -->
<div class="tabPanesContainer">
<!-- ko foreach: openedTabs -->
<div class="tabs-container" data-bind="visible: $parent.activeTab() === $data">
<span data-bind="component: { name: $data.constructor.component.name, params: $data }"></span>
</div>
<!-- /ko -->
</div>
<!-- Tabs Panes - End -->
</div>

View File

@ -50,6 +50,7 @@ import { PanelContainerComponent } from "./Explorer/Panes/PanelContainerComponen
import { SplashScreen } from "./Explorer/SplashScreen/SplashScreen";
import "./Explorer/SplashScreen/SplashScreen.less";
import "./Explorer/Tabs/QueryTab.less";
import { Tabs } from "./Explorer/Tabs/Tabs";
import { useConfig } from "./hooks/useConfig";
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
import { useSidePanel } from "./hooks/useSidePanel";
@ -79,7 +80,7 @@ const App: React.FunctionComponent = () => {
};
const { isPanelOpen, panelContent, headerText, openSidePanel, closeSidePanel } = useSidePanel();
const { tabs, tabsManager } = useTabs();
const { tabs, activeTab, tabsManager } = useTabs();
const explorerParams: ExplorerParams = {
setIsNotificationConsoleExpanded,
@ -205,7 +206,7 @@ const App: React.FunctionComponent = () => {
</div>
{/* Collections Tree - End */}
{tabs.length === 0 && <SplashScreen explorer={explorer} />}
<div className="tabsManagerContainer" data-bind='component: { name: "tabs-manager", params: tabsManager }' />
<Tabs tabs={tabs} activeTab={activeTab} />
</div>
{/* Collections Tree and Tabs - End */}
<div

View File

@ -0,0 +1,13 @@
import { Observable } from "knockout";
import { useEffect, useState } from "react";
export function useObservable<T>(observable: Pick<Observable<T>, "subscribe" | "peek">): T {
const [, setValue] = useState(0);
useEffect(() => {
const subscription = observable.subscribe(() => setValue((n) => 1 + n), undefined, "change");
return () => subscription.dispose();
}, [observable]);
return observable.peek();
}

View File

@ -1,16 +0,0 @@
import { isObservableArray, Observable, ObservableArray } from "knockout";
import { useEffect, useState } from "react";
export function useObservableState<T>(observable: Observable<T>): [T, (s: T) => void];
export function useObservableState<T>(observable: ObservableArray<T>): [T[], (s: T[]) => void];
export function useObservableState<T>(observable: ObservableArray<T> | Observable<T>): [T | T[], (s: T | T[]) => void] {
const [value, setValue] = useState(observable());
useEffect(() => {
isObservableArray(observable)
? observable.subscribe((values) => setValue([...values]))
: observable.subscribe(setValue);
}, [observable]);
return [value, observable];
}

View File

@ -1,16 +1,18 @@
import { useState } from "react";
import TabsBase from "../Explorer/Tabs/TabsBase";
import { TabsManager } from "../Explorer/Tabs/TabsManager";
import { useObservableState } from "./useObservableState";
import { useObservable } from "./useObservable";
export type UseTabs = {
tabs: readonly TabsBase[];
activeTab: TabsBase;
tabsManager: TabsManager;
};
export function useTabs(): UseTabs {
const [tabsManager] = useState(() => new TabsManager());
const [tabs] = useObservableState(tabsManager.openedTabs);
const tabs = useObservable(tabsManager.openedTabs);
const activeTab = useObservable(tabsManager.activeTab);
return { tabs, tabsManager };
return { tabs, activeTab, tabsManager };
}