From 900a604d8990ef8f479ce27afd1ad27905b05af1 Mon Sep 17 00:00:00 2001 From: Matthew Runyon Date: Fri, 5 Jan 2024 16:29:00 -0600 Subject: [PATCH 01/12] WIP kludge --- .../code-studio/src/main/AppDashboards.tsx | 142 +++++++ .../src/main/AppMainContainer.scss | 4 + .../code-studio/src/main/AppMainContainer.tsx | 386 +++++++++++------- .../code-studio/src/main/EmptyDashboard.tsx | 14 +- .../components/src/navigation/NavTabList.tsx | 2 +- packages/dashboard/src/LazyDashboard.tsx | 66 +++ packages/dashboard/src/index.ts | 1 + packages/dashboard/src/redux/selectors.ts | 11 + packages/redux/src/selectors.ts | 7 + 9 files changed, 471 insertions(+), 162 deletions(-) create mode 100644 packages/code-studio/src/main/AppDashboards.tsx create mode 100644 packages/dashboard/src/LazyDashboard.tsx diff --git a/packages/code-studio/src/main/AppDashboards.tsx b/packages/code-studio/src/main/AppDashboards.tsx new file mode 100644 index 0000000000..e76f6a0cfe --- /dev/null +++ b/packages/code-studio/src/main/AppDashboards.tsx @@ -0,0 +1,142 @@ +import React, { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import classNames from 'classnames'; +import JSZip from 'jszip'; +import Dashboard, { + DashboardLayoutConfig, + DashboardUtils, + DEFAULT_DASHBOARD_ID, + DehydratedDashboardPanelProps, + getAllDashboardsData, + LazyDashboard, + useAllDashboardsData, +} from '@deephaven/dashboard'; +import { ConsolePlugin } from '@deephaven/dashboard-core-plugins'; +import { + getWorkspace, + RootState, + updateWorkspaceData, + useWorkspace, +} from '@deephaven/redux'; +import { useConnection } from '@deephaven/jsapi-components'; +import { VariableDefinition } from '@deephaven/jsapi-types'; +import LayoutManager, { ItemConfigType } from '@deephaven/golden-layout'; +import EmptyDashboard from './EmptyDashboard'; + +function hydrateConsole( + props: DehydratedDashboardPanelProps, + id: string +): DehydratedDashboardPanelProps { + return DashboardUtils.hydrate( + { + ...props, + unzip: (zipFile: Blob) => + JSZip.loadAsync(zipFile).then(zip => Object.values(zip.files)), + }, + id + ); +} + +interface AppDashboardsProps { + dashboards: { + id: string; + getLayoutConfig: () => Promise; + }[]; + activeDashboard: string; + onGoldenLayoutChange: (goldenLayout: LayoutManager) => void; + plugins: JSX.Element[]; +} + +export function AppDashboards({ + dashboards, + activeDashboard, + onGoldenLayoutChange, + plugins, +}: AppDashboardsProps): JSX.Element { + const workspace = useWorkspace(); + const { data: workspaceData } = workspace; + const dashboardsData = useAllDashboardsData(); + const connection = useConnection(); + const dispatch = useDispatch(); + + const hydratePanel = useCallback( + (hydrateProps: DehydratedDashboardPanelProps, id: string) => { + const { metadata } = hydrateProps; + if ( + metadata?.type != null && + (metadata?.id != null || metadata?.name != null) + ) { + // Looks like a widget, hydrate it as such + const widget: VariableDefinition = + metadata.id != null + ? { + type: metadata.type, + id: metadata.id, + } + : { + type: metadata.type, + name: metadata.name, + title: metadata.name, + }; + return { + fetch: () => connection.getObject(widget), + ...hydrateProps, + localDashboardId: id, + }; + } + return DashboardUtils.hydrate(hydrateProps, id); + }, + [connection] + ); + + const handleLayoutConfigChange = useCallback( + (layoutConfig?: DashboardLayoutConfig) => { + dispatch(updateWorkspaceData({ layoutConfig })); + }, + [dispatch] + ); + + return ( +
+ {dashboards.map(d => ( +
+ } + getLayoutConfig={d.getLayoutConfig} + onGoldenLayoutChange={onGoldenLayoutChange} + onLayoutConfigChange={handleLayoutConfigChange} + hydrate={hydratePanel} + plugins={plugins} + /> +
+ ))} +
+ ); + + return ( + } + id={DEFAULT_DASHBOARD_ID} + layoutConfig={[]} + onGoldenLayoutChange={onGoldenLayoutChange} + onLayoutConfigChange={handleLayoutConfigChange} + hydrate={hydratePanel} + > + + + ); +} + +export default AppDashboards; diff --git a/packages/code-studio/src/main/AppMainContainer.scss b/packages/code-studio/src/main/AppMainContainer.scss index 450484b2a2..de2ea3b468 100644 --- a/packages/code-studio/src/main/AppMainContainer.scss +++ b/packages/code-studio/src/main/AppMainContainer.scss @@ -42,7 +42,9 @@ $nav-space: 4px; // give a gap around some buttons for focus area that are in na width: 100%; justify-content: space-between; align-items: center; +} +.app-main-right-menu-buttons { .btn-link { font-size: $tab-font-size; text-decoration: none; @@ -67,7 +69,9 @@ $nav-space: 4px; // give a gap around some buttons for focus area that are in na } .tab-pane { + height: 100%; width: 100%; + flex-grow: 1; } .app-main-tabs { diff --git a/packages/code-studio/src/main/AppMainContainer.tsx b/packages/code-studio/src/main/AppMainContainer.tsx index f08e334c9b..0ffd329713 100644 --- a/packages/code-studio/src/main/AppMainContainer.tsx +++ b/packages/code-studio/src/main/AppMainContainer.tsx @@ -23,6 +23,8 @@ import { Logo, BasicModal, DebouncedModal, + NavTabList, + type NavTabItem, } from '@deephaven/components'; import { SHORTCUTS as IRIS_GRID_SHORTCUTS } from '@deephaven/iris-grid'; import { @@ -56,12 +58,14 @@ import { dhPanels, vsDebugDisconnect, dhSquareFilled, + vsHome, } from '@deephaven/icons'; import dh from '@deephaven/jsapi-shim'; import type { IdeConnection, IdeSession, VariableDefinition, + Widget, } from '@deephaven/jsapi-types'; import { SessionConfig } from '@deephaven/jsapi-utils'; import Log from '@deephaven/log'; @@ -97,6 +101,7 @@ import WidgetList, { WindowMouseEvent } from './WidgetList'; import EmptyDashboard from './EmptyDashboard'; import UserLayoutUtils from './UserLayoutUtils'; import LayoutStorage from '../storage/LayoutStorage'; +import AppDashboards from './AppDashboards'; const log = Log.module('AppMainContainer'); @@ -144,6 +149,9 @@ interface AppMainContainerState { isSettingsMenuShown: boolean; unsavedNotebookCount: number; widgets: VariableDefinition[]; + tabs: (NavTabItem & { definition?: VariableDefinition })[]; + activeTabKey: string; + pendingTabChange: boolean; } export class AppMainContainer extends Component< @@ -217,6 +225,9 @@ export class AppMainContainer extends Component< isSettingsMenuShown: false, unsavedNotebookCount: 0, widgets: [], + tabs: [], + pendingTabChange: true, + activeTabKey: DEFAULT_DASHBOARD_ID, }; } @@ -230,11 +241,25 @@ export class AppMainContainer extends Component< ); } - componentDidUpdate(prevProps: AppMainContainerProps): void { + componentDidUpdate( + prevProps: AppMainContainerProps, + prevState: AppMainContainerState + ): void { const { dashboardData } = this.props; if (prevProps.dashboardData !== dashboardData) { this.handleDataChange(dashboardData); } + + if (prevState.pendingTabChange && !this.state.pendingTabChange) { + console.log(this.pendingEvents); + setTimeout(() => { + this.pendingEvents.forEach(([e, args]) => { + console.log('emit pending event'); + this.emitLayoutEvent(e, ...args); + }); + this.pendingEvents = []; + }, 1000); + } } componentWillUnmount(): void { @@ -249,6 +274,8 @@ export class AppMainContainer extends Component< goldenLayout?: GoldenLayout; + pendingEvents: [string, unknown[]][] = []; + importElement: RefObject; widgetListenerRemover?: () => void; @@ -328,6 +355,11 @@ export class AppMainContainer extends Component< } emitLayoutEvent(event: string, ...args: unknown[]): void { + if (this.state.pendingTabChange) { + console.log('add pending event'); + this.pendingEvents.push([event, args]); + return; + } this.goldenLayout?.eventHub.emit(event, ...args); } @@ -424,12 +456,25 @@ export class AppMainContainer extends Component< } handleGoldenLayoutChange(goldenLayout: GoldenLayout): void { + console.log('golden layout change'); this.goldenLayout = goldenLayout; - } - - handleLayoutConfigChange(layoutConfig?: DashboardLayoutConfig): void { - const { updateWorkspaceData } = this.props; - updateWorkspaceData({ layoutConfig }); + this.setState({ pendingTabChange: false }); + this.goldenLayout.eventHub.on('ui.dashboard', (def: VariableDefinition) => { + this.setState(({ tabs }) => { + if ( + def.id == null || + def.title == null || + tabs.some(t => t.key === def.id) + ) { + return { tabs }; + } + return { + tabs: [...tabs, { key: def.id, title: def.title, definition: def }], + activeTabKey: def.id, + pendingTabChange: true, + }; + }); + }); } handleWidgetMenuClick(): void { @@ -453,19 +498,6 @@ export class AppMainContainer extends Component< this.setState({ isPanelsMenuShown: false }); } - handleAutoFillClick(): void { - const { widgets } = this.state; - - log.debug('handleAutoFillClick', widgets); - - const sortedWidgets = widgets.sort((a, b) => - a.title != null && b.title != null ? a.title.localeCompare(b.title) : 0 - ); - for (let i = 0; i < sortedWidgets.length; i += 1) { - this.openWidget(sortedWidgets[i]); - } - } - handleExportLayoutClick(): void { log.info('handleExportLayoutClick'); @@ -644,37 +676,6 @@ export class AppMainContainer extends Component< ); } - hydrateDefault( - props: DehydratedDashboardPanelProps, - id: string - ): DehydratedDashboardPanelProps & { fetch?: () => Promise } { - const { connection } = this.props; - const { metadata } = props; - if ( - metadata?.type != null && - (metadata?.id != null || metadata?.name != null) - ) { - // Looks like a widget, hydrate it as such - const widget: VariableDefinition = - metadata.id != null - ? { - type: metadata.type, - id: metadata.id, - } - : { - type: metadata.type, - name: metadata.name, - title: metadata.name, - }; - return { - fetch: () => connection.getObject(widget), - ...props, - localDashboardId: id, - }; - } - return DashboardUtils.hydrate(props, id); - } - /** * Open a widget up, using a drag event if specified. * @param widget The widget to open @@ -689,6 +690,15 @@ export class AppMainContainer extends Component< }); } + openDashboard(widget: VariableDefinition): void { + const { connection } = this.props; + this.emitLayoutEvent('dashboardOpen', { + undefined, + fetch: () => connection.getObject(widget), + widget, + }); + } + getDashboardPlugins = memoize((plugins: PluginModuleMap) => { const dashboardPlugins = [...plugins.entries()].filter( ([, plugin]) => @@ -706,6 +716,72 @@ export class AppMainContainer extends Component< }); }); + handleHomeClick(): void { + this.handleTabSelect(DEFAULT_DASHBOARD_ID); + } + + handleTabSelect(tabId: string): void { + console.log('tab select'); + this.setState({ activeTabKey: tabId, pendingTabChange: true }); + } + + handleTabReorder(from: number, to: number): void { + this.setState(({ tabs: oldTabs }) => { + const newTabs = [...oldTabs]; + const [t] = newTabs.splice(from, 1); + newTabs.splice(to, 0, t); + return { tabs: newTabs }; + }); + } + + handleTabClose(tabId: string): void { + this.setState(({ tabs: oldTabs, activeTabKey }) => { + const newTabs = oldTabs.filter(tab => tab.key !== tabId); + let newActiveTabKey = activeTabKey; + if (activeTabKey === tabId && newTabs.length > 0) { + const oldActiveTabIndex = oldTabs.findIndex(tab => tab.key === tabId); + newActiveTabKey = + oldActiveTabIndex < oldTabs.length - 1 + ? oldTabs[oldActiveTabIndex + 1].key + : oldTabs[oldActiveTabIndex - 1].key; + } + + if (newTabs.length === 0) { + newActiveTabKey = DEFAULT_DASHBOARD_ID; + } + + return { tabs: newTabs, activeTabKey: newActiveTabKey }; + }); + } + + getCodeStudioLayoutConfig(): ItemConfigType[] { + const { workspace } = this.props; + const { data: workspaceData } = workspace; + const { layoutConfig } = workspaceData; + return layoutConfig as ItemConfigType[]; + } + + getDashboards(): { + id: string; + getLayoutConfig: () => Promise; + }[] { + return [ + { + id: DEFAULT_DASHBOARD_ID, + getLayoutConfig: () => + Promise.resolve(this.getCodeStudioLayoutConfig()), + }, + ...this.state.tabs.map(tab => ({ + id: tab.key, + getLayoutConfig: () => { + this.openDashboard(tab.definition); + + return Promise.resolve([]); + }, + })), + ]; + } + render(): ReactElement { const { activeTool, plugins, user, workspace, serverConfigValues } = this.props; @@ -722,6 +798,8 @@ export class AppMainContainer extends Component< isSettingsMenuShown, unsavedNotebookCount, widgets, + tabs, + activeTabKey, } = this.state; const dashboardPlugins = this.getDashboardPlugins(plugins); @@ -738,111 +816,121 @@ export class AppMainContainer extends Component< onPaste={this.handlePaste} tabIndex={-1} > - - - } - id={DEFAULT_DASHBOARD_ID} - layoutConfig={layoutConfig as ItemConfigType[]} + + - - {dashboardPlugins} - + plugins={[ + , + ...dashboardPlugins, + ]} + /> void; -} - -export function EmptyDashboard({ - onAutoFillClick = () => undefined, -}: EmptyDashboardProps): JSX.Element { +export function EmptyDashboard(): JSX.Element { return (
@@ -19,9 +12,6 @@ export function EmptyDashboard({
Drag Panels Here
-
); diff --git a/packages/components/src/navigation/NavTabList.tsx b/packages/components/src/navigation/NavTabList.tsx index 1f56a86f37..b4379b4fa7 100644 --- a/packages/components/src/navigation/NavTabList.tsx +++ b/packages/components/src/navigation/NavTabList.tsx @@ -169,7 +169,7 @@ function NavTabList({ makeContextActions, }: NavTabListProps): React.ReactElement { const containerRef = useRef(); - const [isOverflowing, setIsOverflowing] = useState(true); + const [isOverflowing, setIsOverflowing] = useState(false); const [disableScrollLeft, setDisableScrollLeft] = useState(true); const [disableScrollRight, setDisableScrollRight] = useState(true); const handleResize = useCallback(() => { diff --git a/packages/dashboard/src/LazyDashboard.tsx b/packages/dashboard/src/LazyDashboard.tsx new file mode 100644 index 0000000000..f493e41af4 --- /dev/null +++ b/packages/dashboard/src/LazyDashboard.tsx @@ -0,0 +1,66 @@ +import React, { useEffect, useState } from 'react'; +import { LoadingOverlay } from '@deephaven/components'; +import type { ItemConfigType } from '@deephaven/golden-layout'; +import { Dashboard, DashboardProps } from './Dashboard'; + +export interface LazyDashboardProps + extends Omit { + id: string; + getLayoutConfig: () => Promise; + plugins: JSX.Element[]; +} + +export function LazyDashboard({ + getLayoutConfig, + plugins, + ...rest +}: LazyDashboardProps): JSX.Element { + const [isLoaded, setIsLoaded] = useState(false); + const [error, setError] = useState(); + const isLoading = !isLoaded && error == null; + const [layoutConfig, setLayoutConfig] = useState([]); + + useEffect(() => { + let isCanceled = false; + if (isLoaded) { + return; + } + getLayoutConfig() + .then(config => { + if (isCanceled) { + return; + } + setLayoutConfig(config); + setIsLoaded(true); + }) + .catch(e => { + if (isCanceled) { + return; + } + setError(`Error loading dashboard: ${e}`); + }); + + return () => { + isCanceled = true; + }; + }, [getLayoutConfig, isLoaded]); + + if (!isLoaded || error != null) { + return ( + + ); + } + + return ( + // eslint-disable-next-line react/jsx-props-no-spreading + + {plugins} + + ); +} + +export default LazyDashboard; diff --git a/packages/dashboard/src/index.ts b/packages/dashboard/src/index.ts index 37006159a9..ed4118fdba 100644 --- a/packages/dashboard/src/index.ts +++ b/packages/dashboard/src/index.ts @@ -8,6 +8,7 @@ export * from './DashboardPlugin'; export * from './DashboardLayout'; export * from './DashboardUtils'; export { default as DashboardUtils } from './DashboardUtils'; +export * from './LazyDashboard'; export * from './layout'; export * from './redux'; export * from './PanelManager'; diff --git a/packages/dashboard/src/redux/selectors.ts b/packages/dashboard/src/redux/selectors.ts index 67dde9cf77..a8eaa5eee6 100644 --- a/packages/dashboard/src/redux/selectors.ts +++ b/packages/dashboard/src/redux/selectors.ts @@ -1,4 +1,5 @@ import { DashboardData, RootState } from '@deephaven/redux'; +import { useSelector } from 'react-redux'; import { ClosedPanels, OpenedPanelMap } from '../PanelManager'; const EMPTY_MAP = new Map(); @@ -18,6 +19,10 @@ export const getAllDashboardsData: Selector< Record > = store => store.dashboardData; +export function useAllDashboardsData(): Record { + return useSelector(getAllDashboardsData); +} + /** * @param store The redux store * @param dashboardId The dashboard ID to get data for @@ -28,6 +33,12 @@ export const getDashboardData = ( dashboardId: string ): DashboardData => getAllDashboardsData(store)[dashboardId] ?? EMPTY_OBJECT; +export function useDashboardData(dashboardId: string): DashboardData { + return useSelector((store: RootState) => + getDashboardData(store, dashboardId) + ); +} + /** * @param store The redux store * @param dashboardId The dashboard ID to get data for diff --git a/packages/redux/src/selectors.ts b/packages/redux/src/selectors.ts index df7670d9c0..8f4401701b 100644 --- a/packages/redux/src/selectors.ts +++ b/packages/redux/src/selectors.ts @@ -1,4 +1,5 @@ import type { UndoPartial } from '@deephaven/utils'; +import { useSelector } from 'react-redux'; import type { RootState, WorkspaceSettings } from './store'; const EMPTY_OBJECT = Object.freeze({}); @@ -54,6 +55,12 @@ export const getWorkspace = ( return workspace; }; +export function useWorkspace< + State extends RootState = RootState, +>(): State['workspace'] { + return useSelector(getWorkspace); +} + // Settings export const getSettings = ( store: State From 36257f9505e686d83e5a2aef5d3500aafe865f2f Mon Sep 17 00:00:00 2001 From: Matthew Runyon Date: Wed, 10 Jan 2024 15:54:39 -0600 Subject: [PATCH 02/12] WIP: Redux mostly working --- .../code-studio/src/main/AppDashboards.tsx | 22 --- .../code-studio/src/main/AppMainContainer.tsx | 149 +++++++++--------- packages/dashboard/src/redux/actions.ts | 26 +++ packages/redux/src/store.ts | 27 +++- 4 files changed, 129 insertions(+), 95 deletions(-) diff --git a/packages/code-studio/src/main/AppDashboards.tsx b/packages/code-studio/src/main/AppDashboards.tsx index e76f6a0cfe..99d7deabd5 100644 --- a/packages/code-studio/src/main/AppDashboards.tsx +++ b/packages/code-studio/src/main/AppDashboards.tsx @@ -53,9 +53,6 @@ export function AppDashboards({ onGoldenLayoutChange, plugins, }: AppDashboardsProps): JSX.Element { - const workspace = useWorkspace(); - const { data: workspaceData } = workspace; - const dashboardsData = useAllDashboardsData(); const connection = useConnection(); const dispatch = useDispatch(); @@ -118,25 +115,6 @@ export function AppDashboards({ ))} ); - - return ( - } - id={DEFAULT_DASHBOARD_ID} - layoutConfig={[]} - onGoldenLayoutChange={onGoldenLayoutChange} - onLayoutConfigChange={handleLayoutConfigChange} - hydrate={hydratePanel} - > - - - ); } export default AppDashboards; diff --git a/packages/code-studio/src/main/AppMainContainer.tsx b/packages/code-studio/src/main/AppMainContainer.tsx index 0ffd329713..4a8e7302ca 100644 --- a/packages/code-studio/src/main/AppMainContainer.tsx +++ b/packages/code-studio/src/main/AppMainContainer.tsx @@ -10,6 +10,7 @@ import memoize from 'memoize-one'; import { CSSTransition } from 'react-transition-group'; import { connect } from 'react-redux'; import { RouteComponentProps, withRouter } from 'react-router-dom'; +import shortid from 'shortid'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { ContextActions, @@ -28,14 +29,14 @@ import { } from '@deephaven/components'; import { SHORTCUTS as IRIS_GRID_SHORTCUTS } from '@deephaven/iris-grid'; import { - ClosedPanels, - Dashboard, - DashboardLayoutConfig, DashboardUtils, DEFAULT_DASHBOARD_ID, DehydratedDashboardPanelProps, + getAllDashboardsData, getDashboardData, PanelEvent, + setDashboardData as setDashboardDataAction, + setDashboardPluginData as setDashboardPluginDataAction, updateDashboardData as updateDashboardDataAction, } from '@deephaven/dashboard'; import { @@ -65,7 +66,6 @@ import type { IdeConnection, IdeSession, VariableDefinition, - Widget, } from '@deephaven/jsapi-types'; import { SessionConfig } from '@deephaven/jsapi-utils'; import Log from '@deephaven/log'; @@ -81,6 +81,7 @@ import { User, ServerConfigValues, CustomizableWorkspace, + DashboardData, } from '@deephaven/redux'; import { bindAllMethods, PromiseUtils } from '@deephaven/utils'; import GoldenLayout from '@deephaven/golden-layout'; @@ -98,7 +99,6 @@ import AppControlsMenu from './AppControlsMenu'; import { getLayoutStorage, getServerConfigValues } from '../redux'; import './AppMainContainer.scss'; import WidgetList, { WindowMouseEvent } from './WidgetList'; -import EmptyDashboard from './EmptyDashboard'; import UserLayoutUtils from './UserLayoutUtils'; import LayoutStorage from '../storage/LayoutStorage'; import AppDashboards from './AppDashboards'; @@ -113,17 +113,10 @@ type InputFileFormat = | Blob | NodeJS.ReadableStream; -export type AppDashboardData = { - closed: ClosedPanels; - columnSelectionValidator?: ColumnSelectionValidator; - filterSets: FilterSet[]; - links: Link[]; - openedMap: Map; -}; - interface AppMainContainerProps { activeTool: string; - dashboardData: AppDashboardData; + allDashboardData: Record; + dashboardData: DashboardData; layoutStorage: LayoutStorage; match: { params: { notebookPath: string }; @@ -132,7 +125,13 @@ interface AppMainContainerProps { session?: IdeSession; sessionConfig?: SessionConfig; setActiveTool: (tool: string) => void; - updateDashboardData: (id: string, data: Partial) => void; + setDashboardData: (id: string, data: DashboardData) => void; + setDashboardPluginData: ( + dashboardId: string, + pluginId: string, + data: any + ) => void; + updateDashboardData: (id: string, data: Partial) => void; updateWorkspaceData: (workspaceData: Partial) => void; user: User; workspace: CustomizableWorkspace; @@ -149,9 +148,8 @@ interface AppMainContainerState { isSettingsMenuShown: boolean; unsavedNotebookCount: number; widgets: VariableDefinition[]; - tabs: (NavTabItem & { definition?: VariableDefinition })[]; + tabs: NavTabItem[]; activeTabKey: string; - pendingTabChange: boolean; } export class AppMainContainer extends Component< @@ -192,6 +190,8 @@ export class AppMainContainer extends Component< this.importElement = React.createRef(); + const { allDashboardData } = this.props; + this.state = { contextActions: [ { @@ -225,8 +225,12 @@ export class AppMainContainer extends Component< isSettingsMenuShown: false, unsavedNotebookCount: 0, widgets: [], - tabs: [], - pendingTabChange: true, + tabs: Object.entries(allDashboardData) + .filter(([key]) => key !== DEFAULT_DASHBOARD_ID) + .map(([key, value]) => ({ + key, + title: value.title ?? 'Untitled', + })), activeTabKey: DEFAULT_DASHBOARD_ID, }; } @@ -249,17 +253,6 @@ export class AppMainContainer extends Component< if (prevProps.dashboardData !== dashboardData) { this.handleDataChange(dashboardData); } - - if (prevState.pendingTabChange && !this.state.pendingTabChange) { - console.log(this.pendingEvents); - setTimeout(() => { - this.pendingEvents.forEach(([e, args]) => { - console.log('emit pending event'); - this.emitLayoutEvent(e, ...args); - }); - this.pendingEvents = []; - }, 1000); - } } componentWillUnmount(): void { @@ -274,8 +267,6 @@ export class AppMainContainer extends Component< goldenLayout?: GoldenLayout; - pendingEvents: [string, unknown[]][] = []; - importElement: RefObject; widgetListenerRemover?: () => void; @@ -355,11 +346,6 @@ export class AppMainContainer extends Component< } emitLayoutEvent(event: string, ...args: unknown[]): void { - if (this.state.pendingTabChange) { - console.log('add pending event'); - this.pendingEvents.push([event, args]); - return; - } this.goldenLayout?.eventHub.emit(event, ...args); } @@ -447,34 +433,42 @@ export class AppMainContainer extends Component< this.sendClearFilter(); } - handleDataChange(data: AppDashboardData): void { + handleDataChange(data: DashboardData): void { const { updateWorkspaceData } = this.props; // Only save the data that is serializable/we want to persist to the workspace - const { closed, filterSets, links } = data; - updateWorkspaceData({ closed, filterSets, links }); + const { closed } = data; + updateWorkspaceData({ closed }); } handleGoldenLayoutChange(goldenLayout: GoldenLayout): void { - console.log('golden layout change'); this.goldenLayout = goldenLayout; - this.setState({ pendingTabChange: false }); - this.goldenLayout.eventHub.on('ui.dashboard', (def: VariableDefinition) => { - this.setState(({ tabs }) => { - if ( - def.id == null || - def.title == null || - tabs.some(t => t.key === def.id) - ) { - return { tabs }; - } - return { - tabs: [...tabs, { key: def.id, title: def.title, definition: def }], - activeTabKey: def.id, - pendingTabChange: true, - }; - }); - }); + this.goldenLayout.eventHub.on( + 'ui.dashboard', + ({ + pluginId, + title, + data, + }: { + pluginId: string; + title: string; + data: unknown; + }) => { + const newId = shortid(); + const { setDashboardPluginData } = this.props; + setDashboardPluginData(newId, pluginId, data); + this.setState(({ tabs }) => ({ + tabs: [ + ...tabs, + { + key: newId, + title, + }, + ], + activeTabKey: newId, + })); + } + ); } handleWidgetMenuClick(): void { @@ -721,8 +715,7 @@ export class AppMainContainer extends Component< } handleTabSelect(tabId: string): void { - console.log('tab select'); - this.setState({ activeTabKey: tabId, pendingTabChange: true }); + this.setState({ activeTabKey: tabId }); } handleTabReorder(from: number, to: number): void { @@ -735,6 +728,10 @@ export class AppMainContainer extends Component< } handleTabClose(tabId: string): void { + const { setDashboardData } = this.props; + // TODO: Figure out how to remove the dashboard data + // without updates after this recreating some dashboard data + setDashboardData(tabId, undefined); this.setState(({ tabs: oldTabs, activeTabKey }) => { const newTabs = oldTabs.filter(tab => tab.key !== tabId); let newActiveTabKey = activeTabKey; @@ -759,25 +756,29 @@ export class AppMainContainer extends Component< const { data: workspaceData } = workspace; const { layoutConfig } = workspaceData; return layoutConfig as ItemConfigType[]; + // TODO: Move this to read dashboard data + // const { dashboardData } = this.props; + // return (dashboardData.layoutConfig ?? []) as ItemConfigType[]; } getDashboards(): { id: string; getLayoutConfig: () => Promise; }[] { + const { tabs } = this.state; + const { allDashboardData } = this.props; return [ { id: DEFAULT_DASHBOARD_ID, getLayoutConfig: () => Promise.resolve(this.getCodeStudioLayoutConfig()), }, - ...this.state.tabs.map(tab => ({ + ...tabs.map(tab => ({ id: tab.key, - getLayoutConfig: () => { - this.openDashboard(tab.definition); - - return Promise.resolve([]); - }, + getLayoutConfig: () => + Promise.resolve( + (allDashboardData[tab.key]?.layoutConfig ?? []) as ItemConfigType[] + ), })), ]; } @@ -920,6 +921,7 @@ export class AppMainContainer extends Component< onGoldenLayoutChange={this.handleGoldenLayoutChange} plugins={[ => ({ activeTool: getActiveTool(state), - dashboardData: getDashboardData( - state, - DEFAULT_DASHBOARD_ID - ) as AppDashboardData, + allDashboardData: getAllDashboardsData(state), + dashboardData: getDashboardData(state, DEFAULT_DASHBOARD_ID), layoutStorage: getLayoutStorage(state), plugins: getPlugins(state), connection: getDashboardConnection(state, DEFAULT_DASHBOARD_ID), @@ -1019,6 +1024,8 @@ const mapStateToProps = ( const ConnectedAppMainContainer = connect(mapStateToProps, { setActiveTool: setActiveToolAction, + setDashboardData: setDashboardDataAction, + setDashboardPluginData: setDashboardPluginDataAction, updateDashboardData: updateDashboardDataAction, updateWorkspaceData: updateWorkspaceDataAction, })(withRouter(AppMainContainer)); diff --git a/packages/dashboard/src/redux/actions.ts b/packages/dashboard/src/redux/actions.ts index 1fd9b14225..76396d5306 100644 --- a/packages/dashboard/src/redux/actions.ts +++ b/packages/dashboard/src/redux/actions.ts @@ -41,3 +41,29 @@ export const updateDashboardData = ...data, }) ); + +/** + * Action to set the plugin data for a dashboard + * @param dashboardId The ID of the dashboard to set the data on + * @param pluginId The ID of the plugin to set the data on + * @param data Data for the dashboard + * @returns The thunk to get dispatched + */ +export const setDashboardPluginData = + ( + dashboardId: string, + pluginId: string, + data: DashboardData + ): ThunkAction> => + (dispatch, getState) => { + const dashboardData = getDashboardData(getState(), dashboardId); + dispatch( + setDashboardData(dashboardId, { + ...dashboardData, + pluginData: { + ...dashboardData.pluginData, + [pluginId]: data, + }, + }) + ); + }; diff --git a/packages/redux/src/store.ts b/packages/redux/src/store.ts index 6462d81162..7621843209 100644 --- a/packages/redux/src/store.ts +++ b/packages/redux/src/store.ts @@ -77,7 +77,30 @@ export interface CustomizableWorkspace { export interface Workspace { data: WorkspaceData; } -export type DashboardData = Record; + +/** + * Most of these aren't actually unknown, but their types are in dashboard or dashboard-core-plugins + * Eventually we can start typing these by changing them to never and using the errors to find the types + */ +export type DashboardData = { + title?: string; + links?: unknown[]; + filterSets?: unknown[]; + consoleSettings?: unknown; + columnSelectionValidator?: unknown; + isolatedLinkerPanelId?: string | string[]; + columns?: unknown[]; + filters?: unknown[]; + tableMap?: unknown; + sessionWrapper?: unknown; + connection?: unknown; + closed?: unknown[]; + openedMap?: Map; + pluginData?: { + [id: string]: unknown; + }; + layoutConfig?: unknown[]; +}; export type WorkspaceStorageLoadOptions = { isConsoleAvailable: boolean; @@ -98,7 +121,7 @@ export type RootState = { user: User; workspace: CustomizableWorkspace; defaultWorkspaceSettings: WorkspaceSettings; - dashboardData: Record; + dashboardData: { [id: string]: DashboardData }; layoutStorage: unknown; serverConfigValues: ServerConfigValues; }; From ace73bb3f951fc98995da4db45c33aefa6c8e1c3 Mon Sep 17 00:00:00 2001 From: Matthew Runyon Date: Thu, 11 Jan 2024 15:11:50 -0600 Subject: [PATCH 03/12] Cleanup --- .../code-studio/src/main/AppDashboards.tsx | 42 ++----------------- .../code-studio/src/main/AppMainContainer.tsx | 10 ++--- packages/dashboard/src/LazyDashboard.tsx | 23 ++++++++-- packages/dashboard/src/redux/actions.ts | 4 +- 4 files changed, 28 insertions(+), 51 deletions(-) diff --git a/packages/code-studio/src/main/AppDashboards.tsx b/packages/code-studio/src/main/AppDashboards.tsx index 99d7deabd5..924e3175f2 100644 --- a/packages/code-studio/src/main/AppDashboards.tsx +++ b/packages/code-studio/src/main/AppDashboards.tsx @@ -1,41 +1,14 @@ import React, { useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; import classNames from 'classnames'; -import JSZip from 'jszip'; -import Dashboard, { - DashboardLayoutConfig, +import { DashboardUtils, - DEFAULT_DASHBOARD_ID, DehydratedDashboardPanelProps, - getAllDashboardsData, LazyDashboard, - useAllDashboardsData, } from '@deephaven/dashboard'; -import { ConsolePlugin } from '@deephaven/dashboard-core-plugins'; -import { - getWorkspace, - RootState, - updateWorkspaceData, - useWorkspace, -} from '@deephaven/redux'; import { useConnection } from '@deephaven/jsapi-components'; import { VariableDefinition } from '@deephaven/jsapi-types'; import LayoutManager, { ItemConfigType } from '@deephaven/golden-layout'; -import EmptyDashboard from './EmptyDashboard'; - -function hydrateConsole( - props: DehydratedDashboardPanelProps, - id: string -): DehydratedDashboardPanelProps { - return DashboardUtils.hydrate( - { - ...props, - unzip: (zipFile: Blob) => - JSZip.loadAsync(zipFile).then(zip => Object.values(zip.files)), - }, - id - ); -} +import { LoadingOverlay } from '@deephaven/components'; interface AppDashboardsProps { dashboards: { @@ -54,7 +27,6 @@ export function AppDashboards({ plugins, }: AppDashboardsProps): JSX.Element { const connection = useConnection(); - const dispatch = useDispatch(); const hydratePanel = useCallback( (hydrateProps: DehydratedDashboardPanelProps, id: string) => { @@ -86,13 +58,6 @@ export function AppDashboards({ [connection] ); - const handleLayoutConfigChange = useCallback( - (layoutConfig?: DashboardLayoutConfig) => { - dispatch(updateWorkspaceData({ layoutConfig })); - }, - [dispatch] - ); - return (
{dashboards.map(d => ( @@ -104,10 +69,9 @@ export function AppDashboards({ > } + emptyDashboard={} getLayoutConfig={d.getLayoutConfig} onGoldenLayoutChange={onGoldenLayoutChange} - onLayoutConfigChange={handleLayoutConfigChange} hydrate={hydratePanel} plugins={plugins} /> diff --git a/packages/code-studio/src/main/AppMainContainer.tsx b/packages/code-studio/src/main/AppMainContainer.tsx index 4a8e7302ca..accf724722 100644 --- a/packages/code-studio/src/main/AppMainContainer.tsx +++ b/packages/code-studio/src/main/AppMainContainer.tsx @@ -49,7 +49,6 @@ import { ToolType, FilterSet, Link, - ColumnSelectionValidator, getDashboardConnection, NotebookPanel, } from '@deephaven/dashboard-core-plugins'; @@ -129,7 +128,7 @@ interface AppMainContainerProps { setDashboardPluginData: ( dashboardId: string, pluginId: string, - data: any + data: unknown ) => void; updateDashboardData: (id: string, data: Partial) => void; updateWorkspaceData: (workspaceData: Partial) => void; @@ -731,7 +730,7 @@ export class AppMainContainer extends Component< const { setDashboardData } = this.props; // TODO: Figure out how to remove the dashboard data // without updates after this recreating some dashboard data - setDashboardData(tabId, undefined); + setDashboardData(tabId, undefined as unknown as DashboardData); this.setState(({ tabs: oldTabs, activeTabKey }) => { const newTabs = oldTabs.filter(tab => tab.key !== tabId); let newActiveTabKey = activeTabKey; @@ -784,10 +783,7 @@ export class AppMainContainer extends Component< } render(): ReactElement { - const { activeTool, plugins, user, workspace, serverConfigValues } = - this.props; - const { data: workspaceData } = workspace; - const { layoutConfig } = workspaceData; + const { activeTool, plugins, user, serverConfigValues } = this.props; const { permissions } = user; const { canUsePanels } = permissions; const { diff --git a/packages/dashboard/src/LazyDashboard.tsx b/packages/dashboard/src/LazyDashboard.tsx index f493e41af4..68322030a7 100644 --- a/packages/dashboard/src/LazyDashboard.tsx +++ b/packages/dashboard/src/LazyDashboard.tsx @@ -1,7 +1,10 @@ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; import { LoadingOverlay } from '@deephaven/components'; import type { ItemConfigType } from '@deephaven/golden-layout'; import { Dashboard, DashboardProps } from './Dashboard'; +import { updateDashboardData } from './redux'; +import { DashboardLayoutConfig } from './DashboardLayout'; export interface LazyDashboardProps extends Omit { @@ -13,12 +16,21 @@ export interface LazyDashboardProps export function LazyDashboard({ getLayoutConfig, plugins, + id, ...rest }: LazyDashboardProps): JSX.Element { const [isLoaded, setIsLoaded] = useState(false); const [error, setError] = useState(); const isLoading = !isLoaded && error == null; const [layoutConfig, setLayoutConfig] = useState([]); + const dispatch = useDispatch(); + + const handleLayoutConfigChange = useCallback( + (config?: DashboardLayoutConfig) => { + dispatch(updateDashboardData(id, { layoutConfig: config })); + }, + [id, dispatch] + ); useEffect(() => { let isCanceled = false; @@ -56,8 +68,13 @@ export function LazyDashboard({ } return ( - // eslint-disable-next-line react/jsx-props-no-spreading - + {plugins} ); diff --git a/packages/dashboard/src/redux/actions.ts b/packages/dashboard/src/redux/actions.ts index 76396d5306..5b31d65e4c 100644 --- a/packages/dashboard/src/redux/actions.ts +++ b/packages/dashboard/src/redux/actions.ts @@ -46,14 +46,14 @@ export const updateDashboardData = * Action to set the plugin data for a dashboard * @param dashboardId The ID of the dashboard to set the data on * @param pluginId The ID of the plugin to set the data on - * @param data Data for the dashboard + * @param data Data for the plugin on the dashboard * @returns The thunk to get dispatched */ export const setDashboardPluginData = ( dashboardId: string, pluginId: string, - data: DashboardData + data: unknown ): ThunkAction> => (dispatch, getState) => { const dashboardData = getDashboardData(getState(), dashboardId); From cb2b3216450411cac1a9d2fc1edcdbe9455e2353 Mon Sep 17 00:00:00 2001 From: Matthew Runyon Date: Tue, 16 Jan 2024 08:56:12 -0600 Subject: [PATCH 04/12] More cleanup --- .../code-studio/src/main/AppDashboards.tsx | 5 +- .../code-studio/src/main/AppMainContainer.tsx | 28 ++++------ packages/dashboard/src/LazyDashboard.tsx | 53 ++++--------------- 3 files changed, 24 insertions(+), 62 deletions(-) diff --git a/packages/code-studio/src/main/AppDashboards.tsx b/packages/code-studio/src/main/AppDashboards.tsx index 924e3175f2..67a85d1f69 100644 --- a/packages/code-studio/src/main/AppDashboards.tsx +++ b/packages/code-studio/src/main/AppDashboards.tsx @@ -13,7 +13,7 @@ import { LoadingOverlay } from '@deephaven/components'; interface AppDashboardsProps { dashboards: { id: string; - getLayoutConfig: () => Promise; + layoutConfig: ItemConfigType[]; }[]; activeDashboard: string; onGoldenLayoutChange: (goldenLayout: LayoutManager) => void; @@ -69,8 +69,9 @@ export function AppDashboards({ > } - getLayoutConfig={d.getLayoutConfig} + layoutConfig={d.layoutConfig} onGoldenLayoutChange={onGoldenLayoutChange} hydrate={hydratePanel} plugins={plugins} diff --git a/packages/code-studio/src/main/AppMainContainer.tsx b/packages/code-studio/src/main/AppMainContainer.tsx index accf724722..4a359cf794 100644 --- a/packages/code-studio/src/main/AppMainContainer.tsx +++ b/packages/code-studio/src/main/AppMainContainer.tsx @@ -82,7 +82,7 @@ import { CustomizableWorkspace, DashboardData, } from '@deephaven/redux'; -import { bindAllMethods, PromiseUtils } from '@deephaven/utils'; +import { bindAllMethods, EMPTY_ARRAY, PromiseUtils } from '@deephaven/utils'; import GoldenLayout from '@deephaven/golden-layout'; import type { ItemConfigType } from '@deephaven/golden-layout'; import { @@ -750,34 +750,26 @@ export class AppMainContainer extends Component< }); } - getCodeStudioLayoutConfig(): ItemConfigType[] { - const { workspace } = this.props; + getDashboards(): { + id: string; + layoutConfig: ItemConfigType[]; + }[] { + const { tabs } = this.state; + const { allDashboardData, workspace } = this.props; const { data: workspaceData } = workspace; const { layoutConfig } = workspaceData; - return layoutConfig as ItemConfigType[]; // TODO: Move this to read dashboard data // const { dashboardData } = this.props; // return (dashboardData.layoutConfig ?? []) as ItemConfigType[]; - } - - getDashboards(): { - id: string; - getLayoutConfig: () => Promise; - }[] { - const { tabs } = this.state; - const { allDashboardData } = this.props; return [ { id: DEFAULT_DASHBOARD_ID, - getLayoutConfig: () => - Promise.resolve(this.getCodeStudioLayoutConfig()), + layoutConfig: layoutConfig as ItemConfigType[], }, ...tabs.map(tab => ({ id: tab.key, - getLayoutConfig: () => - Promise.resolve( - (allDashboardData[tab.key]?.layoutConfig ?? []) as ItemConfigType[] - ), + layoutConfig: (allDashboardData[tab.key]?.layoutConfig ?? + EMPTY_ARRAY) as ItemConfigType[], })), ]; } diff --git a/packages/dashboard/src/LazyDashboard.tsx b/packages/dashboard/src/LazyDashboard.tsx index 68322030a7..ffe6cea4d9 100644 --- a/packages/dashboard/src/LazyDashboard.tsx +++ b/packages/dashboard/src/LazyDashboard.tsx @@ -1,28 +1,24 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { useDispatch } from 'react-redux'; import { LoadingOverlay } from '@deephaven/components'; -import type { ItemConfigType } from '@deephaven/golden-layout'; import { Dashboard, DashboardProps } from './Dashboard'; import { updateDashboardData } from './redux'; import { DashboardLayoutConfig } from './DashboardLayout'; -export interface LazyDashboardProps - extends Omit { +export interface LazyDashboardProps extends DashboardProps { id: string; - getLayoutConfig: () => Promise; + isActive: boolean; plugins: JSX.Element[]; } export function LazyDashboard({ - getLayoutConfig, - plugins, id, + isActive, + plugins, + layoutConfig, ...rest }: LazyDashboardProps): JSX.Element { const [isLoaded, setIsLoaded] = useState(false); - const [error, setError] = useState(); - const isLoading = !isLoaded && error == null; - const [layoutConfig, setLayoutConfig] = useState([]); const dispatch = useDispatch(); const handleLayoutConfigChange = useCallback( @@ -32,39 +28,12 @@ export function LazyDashboard({ [id, dispatch] ); - useEffect(() => { - let isCanceled = false; - if (isLoaded) { - return; - } - getLayoutConfig() - .then(config => { - if (isCanceled) { - return; - } - setLayoutConfig(config); - setIsLoaded(true); - }) - .catch(e => { - if (isCanceled) { - return; - } - setError(`Error loading dashboard: ${e}`); - }); - - return () => { - isCanceled = true; - }; - }, [getLayoutConfig, isLoaded]); + if (!isLoaded && isActive) { + setIsLoaded(true); + } - if (!isLoaded || error != null) { - return ( - - ); + if (!isLoaded) { + return ; } return ( From 559df83a01818a41b492c7ae209cb5a05cfb217d Mon Sep 17 00:00:00 2001 From: Matthew Runyon Date: Wed, 17 Jan 2024 13:52:43 -0600 Subject: [PATCH 05/12] Some fixes --- .../code-studio/src/main/AppDashboards.tsx | 12 +++++++++- .../code-studio/src/main/AppMainContainer.tsx | 23 +++++++++++-------- .../code-studio/src/main/EmptyDashboard.tsx | 12 +++++++++- packages/dashboard/src/LazyDashboard.tsx | 3 +-- 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/packages/code-studio/src/main/AppDashboards.tsx b/packages/code-studio/src/main/AppDashboards.tsx index 67a85d1f69..072678a1d5 100644 --- a/packages/code-studio/src/main/AppDashboards.tsx +++ b/packages/code-studio/src/main/AppDashboards.tsx @@ -2,6 +2,7 @@ import React, { useCallback } from 'react'; import classNames from 'classnames'; import { DashboardUtils, + DEFAULT_DASHBOARD_ID, DehydratedDashboardPanelProps, LazyDashboard, } from '@deephaven/dashboard'; @@ -9,6 +10,7 @@ import { useConnection } from '@deephaven/jsapi-components'; import { VariableDefinition } from '@deephaven/jsapi-types'; import LayoutManager, { ItemConfigType } from '@deephaven/golden-layout'; import { LoadingOverlay } from '@deephaven/components'; +import EmptyDashboard from './EmptyDashboard'; interface AppDashboardsProps { dashboards: { @@ -18,6 +20,7 @@ interface AppDashboardsProps { activeDashboard: string; onGoldenLayoutChange: (goldenLayout: LayoutManager) => void; plugins: JSX.Element[]; + onAutoFillClick: (event: React.MouseEvent) => void; } export function AppDashboards({ @@ -25,6 +28,7 @@ export function AppDashboards({ activeDashboard, onGoldenLayoutChange, plugins, + onAutoFillClick, }: AppDashboardsProps): JSX.Element { const connection = useConnection(); @@ -70,7 +74,13 @@ export function AppDashboards({ } + emptyDashboard={ + d.id === DEFAULT_DASHBOARD_ID ? ( + + ) : ( + + ) + } layoutConfig={d.layoutConfig} onGoldenLayoutChange={onGoldenLayoutChange} hydrate={hydratePanel} diff --git a/packages/code-studio/src/main/AppMainContainer.tsx b/packages/code-studio/src/main/AppMainContainer.tsx index 4a359cf794..d87b6d8863 100644 --- a/packages/code-studio/src/main/AppMainContainer.tsx +++ b/packages/code-studio/src/main/AppMainContainer.tsx @@ -491,6 +491,19 @@ export class AppMainContainer extends Component< this.setState({ isPanelsMenuShown: false }); } + handleAutoFillClick(): void { + const { widgets } = this.state; + + log.debug('handleAutoFillClick', widgets); + + const sortedWidgets = widgets.sort((a, b) => + a.title != null && b.title != null ? a.title.localeCompare(b.title) : 0 + ); + for (let i = 0; i < sortedWidgets.length; i += 1) { + this.openWidget(sortedWidgets[i]); + } + } + handleExportLayoutClick(): void { log.info('handleExportLayoutClick'); @@ -683,15 +696,6 @@ export class AppMainContainer extends Component< }); } - openDashboard(widget: VariableDefinition): void { - const { connection } = this.props; - this.emitLayoutEvent('dashboardOpen', { - undefined, - fetch: () => connection.getObject(widget), - widget, - }); - } - getDashboardPlugins = memoize((plugins: PluginModuleMap) => { const dashboardPlugins = [...plugins.entries()].filter( ([, plugin]) => @@ -907,6 +911,7 @@ export class AppMainContainer extends Component< dashboards={this.getDashboards()} activeDashboard={activeTabKey} onGoldenLayoutChange={this.handleGoldenLayoutChange} + onAutoFillClick={this.handleAutoFillClick} plugins={[ void; +} + +export function EmptyDashboard({ + onAutoFillClick = () => undefined, +}: EmptyDashboardProps): JSX.Element { return (
@@ -12,6 +19,9 @@ export function EmptyDashboard(): JSX.Element {
Drag Panels Here
+
); diff --git a/packages/dashboard/src/LazyDashboard.tsx b/packages/dashboard/src/LazyDashboard.tsx index ffe6cea4d9..a15d79a4cf 100644 --- a/packages/dashboard/src/LazyDashboard.tsx +++ b/packages/dashboard/src/LazyDashboard.tsx @@ -18,7 +18,7 @@ export function LazyDashboard({ layoutConfig, ...rest }: LazyDashboardProps): JSX.Element { - const [isLoaded, setIsLoaded] = useState(false); + const [isLoaded, setIsLoaded] = useState(isActive); const dispatch = useDispatch(); const handleLayoutConfigChange = useCallback( @@ -39,7 +39,6 @@ export function LazyDashboard({ return ( Date: Wed, 17 Jan 2024 15:58:24 -0600 Subject: [PATCH 06/12] Address review comments --- .../code-studio/src/main/AppMainContainer.tsx | 50 +++++++++---------- packages/dashboard/src/DashboardEvents.ts | 23 +++++++++ packages/dashboard/src/index.ts | 1 + 3 files changed, 49 insertions(+), 25 deletions(-) create mode 100644 packages/dashboard/src/DashboardEvents.ts diff --git a/packages/code-studio/src/main/AppMainContainer.tsx b/packages/code-studio/src/main/AppMainContainer.tsx index d87b6d8863..4b719fb3d1 100644 --- a/packages/code-studio/src/main/AppMainContainer.tsx +++ b/packages/code-studio/src/main/AppMainContainer.tsx @@ -29,11 +29,13 @@ import { } from '@deephaven/components'; import { SHORTCUTS as IRIS_GRID_SHORTCUTS } from '@deephaven/iris-grid'; import { + CreateDashboardPayload, DashboardUtils, DEFAULT_DASHBOARD_ID, DehydratedDashboardPanelProps, getAllDashboardsData, getDashboardData, + listenForCreateDashboard, PanelEvent, setDashboardData as setDashboardDataAction, setDashboardPluginData as setDashboardPluginDataAction, @@ -442,34 +444,32 @@ export class AppMainContainer extends Component< handleGoldenLayoutChange(goldenLayout: GoldenLayout): void { this.goldenLayout = goldenLayout; - this.goldenLayout.eventHub.on( - 'ui.dashboard', - ({ - pluginId, - title, - data, - }: { - pluginId: string; - title: string; - data: unknown; - }) => { - const newId = shortid(); - const { setDashboardPluginData } = this.props; - setDashboardPluginData(newId, pluginId, data); - this.setState(({ tabs }) => ({ - tabs: [ - ...tabs, - { - key: newId, - title, - }, - ], - activeTabKey: newId, - })); - } + listenForCreateDashboard( + this.goldenLayout.eventHub, + this.handleCreateDashboard ); } + handleCreateDashboard({ + pluginId, + title, + data, + }: CreateDashboardPayload): void { + const newId = shortid(); + const { setDashboardPluginData } = this.props; + setDashboardPluginData(newId, pluginId, data); + this.setState(({ tabs }) => ({ + tabs: [ + ...tabs, + { + key: newId, + title, + }, + ], + activeTabKey: newId, + })); + } + handleWidgetMenuClick(): void { this.setState(({ isPanelsMenuShown }) => ({ isPanelsMenuShown: !isPanelsMenuShown, diff --git a/packages/dashboard/src/DashboardEvents.ts b/packages/dashboard/src/DashboardEvents.ts new file mode 100644 index 0000000000..22564e867c --- /dev/null +++ b/packages/dashboard/src/DashboardEvents.ts @@ -0,0 +1,23 @@ +import type { EventHub } from '@deephaven/golden-layout'; + +export const CREATE_DASHBOARD = 'CREATE_DASHBOARD'; + +export interface CreateDashboardPayload { + pluginId: string; + title: string; + data: unknown; +} + +export function listenForCreateDashboard( + eventHub: EventHub, + handler: (p: CreateDashboardPayload) => void +): void { + eventHub.on(CREATE_DASHBOARD, handler); +} + +export function emitCreateDashboard( + eventHub: EventHub, + payload: CreateDashboardPayload +): void { + eventHub.emit(CREATE_DASHBOARD, payload); +} diff --git a/packages/dashboard/src/index.ts b/packages/dashboard/src/index.ts index ed4118fdba..026255f5fc 100644 --- a/packages/dashboard/src/index.ts +++ b/packages/dashboard/src/index.ts @@ -4,6 +4,7 @@ export default Dashboard; export * from './Dashboard'; export * from './DashboardConstants'; +export * from './DashboardEvents'; export * from './DashboardPlugin'; export * from './DashboardLayout'; export * from './DashboardUtils'; From 14aa6f1b219443c8246c1f01a78fbcbaea7bfc3c Mon Sep 17 00:00:00 2001 From: Matthew Runyon Date: Wed, 17 Jan 2024 16:05:49 -0600 Subject: [PATCH 07/12] Fix spread props --- packages/dashboard/src/LazyDashboard.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/dashboard/src/LazyDashboard.tsx b/packages/dashboard/src/LazyDashboard.tsx index a15d79a4cf..28d3a15317 100644 --- a/packages/dashboard/src/LazyDashboard.tsx +++ b/packages/dashboard/src/LazyDashboard.tsx @@ -15,7 +15,6 @@ export function LazyDashboard({ id, isActive, plugins, - layoutConfig, ...rest }: LazyDashboardProps): JSX.Element { const [isLoaded, setIsLoaded] = useState(isActive); From 8f8a0b1bc0a5562e6cd7601a2584a65e8bab5cb8 Mon Sep 17 00:00:00 2001 From: Matthew Runyon Date: Tue, 23 Jan 2024 13:01:42 -0600 Subject: [PATCH 08/12] Fix console focus issue after opening dashboard --- packages/console/src/ConsoleInput.tsx | 13 ++++++++----- packages/dashboard/src/Dashboard.tsx | 11 ++--------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/console/src/ConsoleInput.tsx b/packages/console/src/ConsoleInput.tsx index a6066f5f5f..bcd6fcb030 100644 --- a/packages/console/src/ConsoleInput.tsx +++ b/packages/console/src/ConsoleInput.tsx @@ -59,7 +59,7 @@ export class ConsoleInput extends PureComponent< constructor(props: ConsoleInputProps) { super(props); - this.handleWindowResize = this.handleWindowResize.bind(this); + this.handleResize = this.handleResize.bind(this); this.commandContainer = React.createRef(); this.commandHistoryIndex = null; @@ -68,6 +68,7 @@ export class ConsoleInput extends PureComponent< this.history = []; // Tracks every command that has been modified by its commandHistoryIndex. Cleared on any command being executed this.modifiedCommands = new Map(); + this.resizeObserver = new window.ResizeObserver(this.handleResize); this.state = { commandEditorHeight: LINE_HEIGHT, @@ -79,8 +80,6 @@ export class ConsoleInput extends PureComponent< componentDidMount(): void { this.initCommandEditor(); - window.addEventListener('resize', this.handleWindowResize); - this.loadMoreHistory(); } @@ -89,7 +88,7 @@ export class ConsoleInput extends PureComponent< } componentWillUnmount(): void { - window.removeEventListener('resize', this.handleWindowResize); + this.resizeObserver.disconnect(); if (this.loadingPromise != null) { this.loadingPromise.cancel(); @@ -100,6 +99,8 @@ export class ConsoleInput extends PureComponent< cancelListener?: () => void; + resizeObserver: ResizeObserver; + commandContainer: RefObject; commandEditor?: monaco.editor.IStandaloneCodeEditor; @@ -279,6 +280,8 @@ export class ConsoleInput extends PureComponent< this.commandEditor.focus(); + this.resizeObserver.observe(element); + this.updateDimensions(); this.setState({ model: this.commandEditor.getModel() }); @@ -293,7 +296,7 @@ export class ConsoleInput extends PureComponent< } } - handleWindowResize(): void { + handleResize(): void { this.updateDimensions(); } diff --git a/packages/dashboard/src/Dashboard.tsx b/packages/dashboard/src/Dashboard.tsx index 2daea39be2..fd3897d014 100644 --- a/packages/dashboard/src/Dashboard.tsx +++ b/packages/dashboard/src/Dashboard.tsx @@ -10,6 +10,7 @@ import React, { import throttle from 'lodash.throttle'; import GoldenLayout from '@deephaven/golden-layout'; import type { ItemConfigType } from '@deephaven/golden-layout'; +import { useResizeObserver } from '@deephaven/react-hooks'; import './layout/GoldenLayout.scss'; import LayoutUtils from './layout/LayoutUtils'; import PanelPlaceholder from './PanelPlaceholder'; @@ -123,15 +124,7 @@ export function Dashboard({ [layout] ); - useEffect( - function initResizeEventListner() { - window.addEventListener('resize', handleResize); - return () => { - window.removeEventListener('resize', handleResize); - }; - }, - [handleResize] - ); + useResizeObserver(layoutElement.current, handleResize); return (
From 06272d6953c64b5c3aef377ff1258f368a47a175 Mon Sep 17 00:00:00 2001 From: Matthew Runyon Date: Tue, 23 Jan 2024 15:25:33 -0600 Subject: [PATCH 09/12] Address review comments. Add todos ticket numbers --- .../code-studio/src/main/AppMainContainer.tsx | 15 ++++++--------- packages/dashboard/src/LazyDashboard.tsx | 12 +++++++++++- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/code-studio/src/main/AppMainContainer.tsx b/packages/code-studio/src/main/AppMainContainer.tsx index 4b719fb3d1..35e6c241c2 100644 --- a/packages/code-studio/src/main/AppMainContainer.tsx +++ b/packages/code-studio/src/main/AppMainContainer.tsx @@ -438,8 +438,8 @@ export class AppMainContainer extends Component< const { updateWorkspaceData } = this.props; // Only save the data that is serializable/we want to persist to the workspace - const { closed } = data; - updateWorkspaceData({ closed }); + const { closed, filterSets, links } = data; + updateWorkspaceData({ closed, filterSets, links }); } handleGoldenLayoutChange(goldenLayout: GoldenLayout): void { @@ -731,10 +731,9 @@ export class AppMainContainer extends Component< } handleTabClose(tabId: string): void { - const { setDashboardData } = this.props; - // TODO: Figure out how to remove the dashboard data - // without updates after this recreating some dashboard data - setDashboardData(tabId, undefined as unknown as DashboardData); + // TODO: #1746 Do something to mark the dashboard as closed + // Remove any dashboard data we no longer need to keep so + // the dashboard data store doesn't grow unbounded this.setState(({ tabs: oldTabs, activeTabKey }) => { const newTabs = oldTabs.filter(tab => tab.key !== tabId); let newActiveTabKey = activeTabKey; @@ -762,9 +761,7 @@ export class AppMainContainer extends Component< const { allDashboardData, workspace } = this.props; const { data: workspaceData } = workspace; const { layoutConfig } = workspaceData; - // TODO: Move this to read dashboard data - // const { dashboardData } = this.props; - // return (dashboardData.layoutConfig ?? []) as ItemConfigType[]; + // TODO: #1746 Read the default dashboard layout from dashboardData instead of workspaceData return [ { id: DEFAULT_DASHBOARD_ID, diff --git a/packages/dashboard/src/LazyDashboard.tsx b/packages/dashboard/src/LazyDashboard.tsx index 28d3a15317..219731d9d2 100644 --- a/packages/dashboard/src/LazyDashboard.tsx +++ b/packages/dashboard/src/LazyDashboard.tsx @@ -1,9 +1,11 @@ import React, { useCallback, useState } from 'react'; import { useDispatch } from 'react-redux'; import { LoadingOverlay } from '@deephaven/components'; +import { updateWorkspaceData } from '@deephaven/redux'; import { Dashboard, DashboardProps } from './Dashboard'; import { updateDashboardData } from './redux'; import { DashboardLayoutConfig } from './DashboardLayout'; +import { DEFAULT_DASHBOARD_ID } from './DashboardConstants'; export interface LazyDashboardProps extends DashboardProps { id: string; @@ -22,7 +24,15 @@ export function LazyDashboard({ const handleLayoutConfigChange = useCallback( (config?: DashboardLayoutConfig) => { - dispatch(updateDashboardData(id, { layoutConfig: config })); + // TODO: #1746 Call updateDashboardData for every dashboard + // This currently allows the default dashboard to keep its layout since + // other dashboards are not persistent yet and we read workspaceData + // for the default dashboard layout + if (id === DEFAULT_DASHBOARD_ID) { + dispatch(updateWorkspaceData({ layoutConfig: config })); + } else { + dispatch(updateDashboardData(id, { layoutConfig: config })); + } }, [id, dispatch] ); From 6d32ba852c6cf2ea589fa50a72a22fdebc8d3be1 Mon Sep 17 00:00:00 2001 From: Matthew Runyon Date: Tue, 23 Jan 2024 16:31:00 -0600 Subject: [PATCH 10/12] Fix unit tests --- .../src/main/AppMainContainer.test.tsx | 62 +++++++++++-------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/packages/code-studio/src/main/AppMainContainer.test.tsx b/packages/code-studio/src/main/AppMainContainer.test.tsx index a0ca3b9d5e..021cf2d4b7 100644 --- a/packages/code-studio/src/main/AppMainContainer.test.tsx +++ b/packages/code-studio/src/main/AppMainContainer.test.tsx @@ -1,8 +1,10 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import React from 'react'; +import { Provider } from 'react-redux'; import { render, screen } from '@testing-library/react'; import { ToolType } from '@deephaven/dashboard-core-plugins'; import { ApiContext } from '@deephaven/jsapi-bootstrap'; +import { ConnectionContext } from '@deephaven/jsapi-components'; import dh from '@deephaven/jsapi-shim'; import type { IdeConnection, @@ -10,10 +12,10 @@ import type { VariableChanges, } from '@deephaven/jsapi-types'; import { TestUtils } from '@deephaven/utils'; -import { Workspace } from '@deephaven/redux'; +import { Workspace, createMockStore } from '@deephaven/redux'; import userEvent from '@testing-library/user-event'; import { DEFAULT_DASHBOARD_ID } from '@deephaven/dashboard'; -import { AppMainContainer, AppDashboardData } from './AppMainContainer'; +import { AppMainContainer } from './AppMainContainer'; import LocalWorkspaceStorage from '../storage/LocalWorkspaceStorage'; import LayoutStorage from '../storage/LayoutStorage'; @@ -69,30 +71,38 @@ function renderAppMainContainer({ match = makeMatch(), plugins = new Map(), } = {}) { + const store = createMockStore(); return render( - - - + + + + + + + ); } let mockProp = {}; @@ -100,7 +110,7 @@ let mockId = DEFAULT_DASHBOARD_ID; jest.mock('@deephaven/dashboard', () => ({ ...jest.requireActual('@deephaven/dashboard'), __esModule: true, - Dashboard: jest.fn(({ hydrate }) => { + LazyDashboard: jest.fn(({ hydrate }) => { const result = hydrate(mockProp, mockId); if (result.fetch != null) { result.fetch(); From baad0c8df180dcead9d812aa3b5175b3f053b565 Mon Sep 17 00:00:00 2001 From: Matthew Runyon Date: Tue, 23 Jan 2024 16:42:21 -0600 Subject: [PATCH 11/12] Fix more test failures --- packages/components/src/navigation/NavTabList.tsx | 2 +- packages/redux/src/selectors.ts | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/components/src/navigation/NavTabList.tsx b/packages/components/src/navigation/NavTabList.tsx index b4379b4fa7..1f56a86f37 100644 --- a/packages/components/src/navigation/NavTabList.tsx +++ b/packages/components/src/navigation/NavTabList.tsx @@ -169,7 +169,7 @@ function NavTabList({ makeContextActions, }: NavTabListProps): React.ReactElement { const containerRef = useRef(); - const [isOverflowing, setIsOverflowing] = useState(false); + const [isOverflowing, setIsOverflowing] = useState(true); const [disableScrollLeft, setDisableScrollLeft] = useState(true); const [disableScrollRight, setDisableScrollRight] = useState(true); const handleResize = useCallback(() => { diff --git a/packages/redux/src/selectors.ts b/packages/redux/src/selectors.ts index 8f4401701b..df7670d9c0 100644 --- a/packages/redux/src/selectors.ts +++ b/packages/redux/src/selectors.ts @@ -1,5 +1,4 @@ import type { UndoPartial } from '@deephaven/utils'; -import { useSelector } from 'react-redux'; import type { RootState, WorkspaceSettings } from './store'; const EMPTY_OBJECT = Object.freeze({}); @@ -55,12 +54,6 @@ export const getWorkspace = ( return workspace; }; -export function useWorkspace< - State extends RootState = RootState, ->(): State['workspace'] { - return useSelector(getWorkspace); -} - // Settings export const getSettings = ( store: State From 7c4cfb43fc357b1b4b920e8db9aef46a49f9b71e Mon Sep 17 00:00:00 2001 From: Matthew Runyon Date: Fri, 26 Jan 2024 14:57:34 -0600 Subject: [PATCH 12/12] Change plugin data map to object --- packages/dashboard/src/redux/actions.ts | 12 +++++++----- packages/dashboard/src/redux/selectors.ts | 4 ++-- packages/redux/src/store.ts | 5 ++++- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/dashboard/src/redux/actions.ts b/packages/dashboard/src/redux/actions.ts index 7347efebd3..af297b474d 100644 --- a/packages/dashboard/src/redux/actions.ts +++ b/packages/dashboard/src/redux/actions.ts @@ -43,11 +43,12 @@ export const updateDashboardData = ); /** - * Action to update the dashboard data. Will combine the update with any existing dashboard data. + * Action to set the dashboard plugin data. + * Will replace any existing plugin data for the plugin in the dashboard with the data provided. * @param id The id of the dashboard to set the data on * @param pluginId The id of the plugin to set the data on * @param data The data to replace the existing plugin data with - * @returns + * @returns Thunk action to dispatch */ export const setDashboardPluginData = ( @@ -59,8 +60,9 @@ export const setDashboardPluginData = dispatch( setDashboardData(id, { ...getDashboardData(getState(), id), - pluginDataMap: new Map( - getPluginDataMapForDashboard(getState(), id) - ).set(pluginId, data), + pluginDataMap: { + ...getPluginDataMapForDashboard(getState(), id), + [pluginId]: data, + }, }) ); diff --git a/packages/dashboard/src/redux/selectors.ts b/packages/dashboard/src/redux/selectors.ts index 506013d37d..460c45615f 100644 --- a/packages/dashboard/src/redux/selectors.ts +++ b/packages/dashboard/src/redux/selectors.ts @@ -65,7 +65,7 @@ export const getPluginDataMapForDashboard = ( store: RootState, dashboardId: string ): PluginDataMap => - getDashboardData(store, dashboardId).pluginDataMap ?? EMPTY_MAP; + getDashboardData(store, dashboardId).pluginDataMap ?? EMPTY_OBJECT; /** * @param store The redux store @@ -77,4 +77,4 @@ export const getPluginDataForDashboard = ( store: RootState, dashboardId: string, pluginId: string -): PluginData => getPluginDataMapForDashboard(store, dashboardId).get(pluginId); +): PluginData => getPluginDataMapForDashboard(store, dashboardId)[pluginId]; diff --git a/packages/redux/src/store.ts b/packages/redux/src/store.ts index c8f536d7f6..f171163d5f 100644 --- a/packages/redux/src/store.ts +++ b/packages/redux/src/store.ts @@ -80,9 +80,12 @@ export interface Workspace { export type PluginData = unknown; -export type PluginDataMap = Map; +export type PluginDataMap = Record; export type DashboardData = Record & { + title?: string; + closed?: unknown[]; + filterSets?: unknown[]; pluginDataMap?: PluginDataMap; };