mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2024-11-25 15:06:55 +00:00
TabsManager in react (#500)
This commit is contained in:
parent
19cf203606
commit
f2585bba14
@ -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
119
src/Explorer/Tabs/Tabs.tsx
Normal 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" />;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
@ -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
|
||||
|
13
src/hooks/useObservable.ts
Normal file
13
src/hooks/useObservable.ts
Normal 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();
|
||||
}
|
@ -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];
|
||||
}
|
@ -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 };
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user