From 32dde3c57765593889216cd3e27d1740ff357af1 Mon Sep 17 00:00:00 2001 From: Matthew Runyon Date: Fri, 26 Jan 2024 15:25:28 -0600 Subject: [PATCH] feat: Multiple dashboards (#1714) Fixes #1683 Test with the dh.ui plugin from https://github.com/deephaven/deephaven-plugins/pull/176 Example dashboard code ``` from deephaven import ui, time_table from deephaven.ui import use_memo, use_state from deephaven.plot.figure import Figure def use_wave_input(): """ Demonstrating a custom hook. Creates an input panel that controls the amplitude, frequency, and phase for a wave """ amplitude, set_amplitude = use_state(1.0) frequency, set_frequency = use_state(1.0) phase, set_phase = use_state(1.0) input_panel = ui.flex( ui.slider( label="Amplitude", default_value=amplitude, min_value=-100.0, max_value=100.0, on_change=set_amplitude, step=0.1, ), ui.slider( label="Frequency", default_value=frequency, min_value=-100.0, max_value=100.0, on_change=set_frequency, step=0.1, ), ui.slider( label="Phase", default_value=phase, min_value=-100.0, max_value=100.0, on_change=set_phase, step=0.1, ), direction="column", ) return amplitude, frequency, phase, input_panel @ui.component def multiwave(): amplitude, frequency, phase, wave_input = use_wave_input() tt = use_memo(lambda: time_table("PT1s").update("x=i"), []) t = use_memo( lambda: tt.update( [ f"y_sin={amplitude}*Math.sin({frequency}*x+{phase})", f"y_cos={amplitude}*Math.cos({frequency}*x+{phase})", f"y_tan={amplitude}*Math.tan({frequency}*x+{phase})", ] ), [amplitude, frequency, phase], ) p_sin = use_memo( lambda: Figure().plot_xy(series_name="Sine", t=t, x="x", y="y_sin").show(), [t] ) p_cos = use_memo( lambda: Figure().plot_xy(series_name="Cosine", t=t, x="x", y="y_cos").show(), [t], ) p_tan = use_memo( lambda: Figure().plot_xy(series_name="Tangent", t=t, x="x", y="y_tan").show(), [t], ) return [ ui.column( ui.row( ui.stack( ui.panel(wave_input, title="Wave Input"), ui.panel(t, title="Wave Table"), activeItemIndex=0 ), height=25 ), ui.row( ui.stack(ui.panel(p_sin, title="Sine"), width=50), ui.stack(ui.panel(p_cos, title="Cosine"), width=30), ui.stack(ui.panel(p_tan, title="Tangent")) ) ) ] mw = ui.dashboard(multiwave()) ``` --- .../code-studio/src/main/AppDashboards.tsx | 95 ++++ .../src/main/AppMainContainer.scss | 4 + .../src/main/AppMainContainer.test.tsx | 62 +-- .../code-studio/src/main/AppMainContainer.tsx | 405 +++++++++++------- .../code-studio/src/main/EmptyDashboard.tsx | 4 +- packages/console/src/ConsoleInput.tsx | 13 +- packages/dashboard/src/Dashboard.tsx | 11 +- packages/dashboard/src/DashboardEvents.ts | 23 + packages/dashboard/src/LazyDashboard.tsx | 60 +++ packages/dashboard/src/index.ts | 2 + packages/dashboard/src/redux/actions.ts | 12 +- packages/dashboard/src/redux/selectors.ts | 4 +- packages/redux/src/store.ts | 7 +- 13 files changed, 490 insertions(+), 212 deletions(-) create mode 100644 packages/code-studio/src/main/AppDashboards.tsx create mode 100644 packages/dashboard/src/DashboardEvents.ts 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..66e14f5671 --- /dev/null +++ b/packages/code-studio/src/main/AppDashboards.tsx @@ -0,0 +1,95 @@ +import React, { useCallback } from 'react'; +import classNames from 'classnames'; +import { + DashboardUtils, + DEFAULT_DASHBOARD_ID, + DehydratedDashboardPanelProps, + LazyDashboard, +} from '@deephaven/dashboard'; +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: { + id: string; + layoutConfig: ItemConfigType[]; + }[]; + activeDashboard: string; + onGoldenLayoutChange: (goldenLayout: LayoutManager) => void; + plugins: JSX.Element[]; + onAutoFillClick: (event: React.MouseEvent) => void; +} + +export function AppDashboards({ + dashboards, + activeDashboard, + onGoldenLayoutChange, + plugins, + onAutoFillClick, +}: AppDashboardsProps): JSX.Element { + const connection = useConnection(); + + 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: async () => connection?.getObject(widget), + ...hydrateProps, + localDashboardId: id, + }; + } + return DashboardUtils.hydrate(hydrateProps, id); + }, + [connection] + ); + + return ( +
+ {dashboards.map(d => ( +
+ + ) : ( + + ) + } + layoutConfig={d.layoutConfig} + onGoldenLayoutChange={onGoldenLayoutChange} + hydrate={hydratePanel} + plugins={plugins} + /> +
+ ))} +
+ ); +} + +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.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(); diff --git a/packages/code-studio/src/main/AppMainContainer.tsx b/packages/code-studio/src/main/AppMainContainer.tsx index 5482a1de96..72c571c8ce 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, @@ -23,17 +24,21 @@ import { Logo, BasicModal, DebouncedModal, + NavTabList, + type NavTabItem, } from '@deephaven/components'; import { SHORTCUTS as IRIS_GRID_SHORTCUTS } from '@deephaven/iris-grid'; import { - ClosedPanels, - Dashboard, - DashboardLayoutConfig, + CreateDashboardPayload, DashboardUtils, DEFAULT_DASHBOARD_ID, DehydratedDashboardPanelProps, + getAllDashboardsData, getDashboardData, + listenForCreateDashboard, PanelEvent, + setDashboardData as setDashboardDataAction, + setDashboardPluginData as setDashboardPluginDataAction, updateDashboardData as updateDashboardDataAction, } from '@deephaven/dashboard'; import { @@ -46,7 +51,6 @@ import { ToolType, FilterSet, Link, - ColumnSelectionValidator, getDashboardConnection, NotebookPanel, } from '@deephaven/dashboard-core-plugins'; @@ -56,6 +60,7 @@ import { dhPanels, vsDebugDisconnect, dhSquareFilled, + vsHome, } from '@deephaven/icons'; import dh from '@deephaven/jsapi-shim'; import type { @@ -77,11 +82,13 @@ import { User, ServerConfigValues, CustomizableWorkspace, + DashboardData, } from '@deephaven/redux'; import { bindAllMethods, copyToClipboard, PromiseUtils, + EMPTY_ARRAY, } from '@deephaven/utils'; import GoldenLayout from '@deephaven/golden-layout'; import type { ItemConfigType } from '@deephaven/golden-layout'; @@ -98,9 +105,9 @@ 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'; import { getFormattedVersionInfo } from '../settings/SettingsUtils'; const log = Log.module('AppMainContainer'); @@ -113,17 +120,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 +132,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: unknown + ) => void; + updateDashboardData: (id: string, data: Partial) => void; updateWorkspaceData: (workspaceData: Partial) => void; user: User; workspace: CustomizableWorkspace; @@ -149,6 +155,8 @@ interface AppMainContainerState { isSettingsMenuShown: boolean; unsavedNotebookCount: number; widgets: VariableDefinition[]; + tabs: NavTabItem[]; + activeTabKey: string; } export class AppMainContainer extends Component< @@ -189,6 +197,8 @@ export class AppMainContainer extends Component< this.importElement = React.createRef(); + const { allDashboardData } = this.props; + this.state = { contextActions: [ { @@ -235,6 +245,13 @@ export class AppMainContainer extends Component< isSettingsMenuShown: false, unsavedNotebookCount: 0, widgets: [], + tabs: Object.entries(allDashboardData) + .filter(([key]) => key !== DEFAULT_DASHBOARD_ID) + .map(([key, value]) => ({ + key, + title: value.title ?? 'Untitled', + })), + activeTabKey: DEFAULT_DASHBOARD_ID, }; } @@ -248,7 +265,10 @@ 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); @@ -437,7 +457,7 @@ 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 @@ -447,11 +467,30 @@ export class AppMainContainer extends Component< handleGoldenLayoutChange(goldenLayout: GoldenLayout): void { this.goldenLayout = goldenLayout; + listenForCreateDashboard( + this.goldenLayout.eventHub, + this.handleCreateDashboard + ); } - handleLayoutConfigChange(layoutConfig?: DashboardLayoutConfig): void { - const { updateWorkspaceData } = this.props; - updateWorkspaceData({ layoutConfig }); + 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 { @@ -674,40 +713,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: async () => 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 @@ -739,11 +744,70 @@ export class AppMainContainer extends Component< }); }); - render(): ReactElement { - const { activeTool, plugins, user, workspace, serverConfigValues } = - this.props; + handleHomeClick(): void { + this.handleTabSelect(DEFAULT_DASHBOARD_ID); + } + + handleTabSelect(tabId: string): void { + this.setState({ activeTabKey: tabId }); + } + + 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 { + // 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; + 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 }; + }); + } + + getDashboards(): { + id: string; + layoutConfig: ItemConfigType[]; + }[] { + const { tabs } = this.state; + const { allDashboardData, workspace } = this.props; const { data: workspaceData } = workspace; const { layoutConfig } = workspaceData; + // TODO: #1746 Read the default dashboard layout from dashboardData instead of workspaceData + return [ + { + id: DEFAULT_DASHBOARD_ID, + layoutConfig: layoutConfig as ItemConfigType[], + }, + ...tabs.map(tab => ({ + id: tab.key, + layoutConfig: (allDashboardData[tab.key]?.layoutConfig ?? + EMPTY_ARRAY) as ItemConfigType[], + })), + ]; + } + + render(): ReactElement { + const { activeTool, plugins, user, serverConfigValues } = this.props; const { permissions } = user; const { canUsePanels } = permissions; const { @@ -755,6 +819,8 @@ export class AppMainContainer extends Component< isSettingsMenuShown, unsavedNotebookCount, widgets, + tabs, + activeTabKey, } = this.state; const dashboardPlugins = this.getDashboardPlugins(plugins); @@ -771,111 +837,123 @@ export class AppMainContainer extends Component< onPaste={this.handlePaste} tabIndex={-1} > - - - } - id={DEFAULT_DASHBOARD_ID} - layoutConfig={layoutConfig as ItemConfigType[]} + + - - {dashboardPlugins} - + onAutoFillClick={this.handleAutoFillClick} + plugins={[ + , + ...dashboardPlugins, + ]} + /> => ({ 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), @@ -964,6 +1045,8 @@ const mapStateToProps = ( const ConnectedAppMainContainer = connect(mapStateToProps, { setActiveTool: setActiveToolAction, + setDashboardData: setDashboardDataAction, + setDashboardPluginData: setDashboardPluginDataAction, updateDashboardData: updateDashboardDataAction, updateWorkspaceData: updateWorkspaceDataAction, })(withRouter(AppMainContainer)); diff --git a/packages/code-studio/src/main/EmptyDashboard.tsx b/packages/code-studio/src/main/EmptyDashboard.tsx index e46d1e3b22..73b7f76eb8 100644 --- a/packages/code-studio/src/main/EmptyDashboard.tsx +++ b/packages/code-studio/src/main/EmptyDashboard.tsx @@ -1,11 +1,11 @@ -import React, { MouseEvent } from 'react'; +import React from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button } from '@deephaven/components'; import { vsArrowUp } from '@deephaven/icons'; import './EmptyDashboard.scss'; export interface EmptyDashboardProps { - onAutoFillClick?: (event: MouseEvent) => void; + onAutoFillClick?: (event: React.MouseEvent) => void; } export function EmptyDashboard({ 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 (
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/LazyDashboard.tsx b/packages/dashboard/src/LazyDashboard.tsx new file mode 100644 index 0000000000..219731d9d2 --- /dev/null +++ b/packages/dashboard/src/LazyDashboard.tsx @@ -0,0 +1,60 @@ +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; + isActive: boolean; + plugins: JSX.Element[]; +} + +export function LazyDashboard({ + id, + isActive, + plugins, + ...rest +}: LazyDashboardProps): JSX.Element { + const [isLoaded, setIsLoaded] = useState(isActive); + const dispatch = useDispatch(); + + const handleLayoutConfigChange = useCallback( + (config?: DashboardLayoutConfig) => { + // 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] + ); + + if (!isLoaded && isActive) { + setIsLoaded(true); + } + + if (!isLoaded) { + return ; + } + + return ( + + {plugins} + + ); +} + +export default LazyDashboard; diff --git a/packages/dashboard/src/index.ts b/packages/dashboard/src/index.ts index 37006159a9..026255f5fc 100644 --- a/packages/dashboard/src/index.ts +++ b/packages/dashboard/src/index.ts @@ -4,10 +4,12 @@ export default Dashboard; export * from './Dashboard'; export * from './DashboardConstants'; +export * from './DashboardEvents'; 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/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 e8e88d3624..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; }; @@ -105,7 +108,7 @@ export type RootState = { user: User; workspace: CustomizableWorkspace; defaultWorkspaceSettings: WorkspaceSettings; - dashboardData: Record; + dashboardData: { [id: string]: DashboardData }; layoutStorage: unknown; serverConfigValues: ServerConfigValues; };