diff --git a/plugins/ui/src/deephaven/ui/components/dashboard.py b/plugins/ui/src/deephaven/ui/components/dashboard.py index 44ae34491..15e28749f 100644 --- a/plugins/ui/src/deephaven/ui/components/dashboard.py +++ b/plugins/ui/src/deephaven/ui/components/dashboard.py @@ -1,10 +1,14 @@ from __future__ import annotations -from typing import Any -from ..elements import DashboardElement, FunctionElement +from typing import Any, Union +from ..elements import BaseElement, FunctionElement -def dashboard(element: FunctionElement) -> DashboardElement: +def dashboard( + element: FunctionElement, + show_close_icon: Union[bool, None] = None, + show_headers: Union[bool, None] = None, +) -> BaseElement: """ A dashboard is the container for an entire layout. @@ -12,7 +16,11 @@ def dashboard(element: FunctionElement) -> DashboardElement: element: Element to render as the dashboard. The element should render a layout that contains 1 root column or row. + show_close_icon: Whether to show the close icon in the top right corner of the dashboard. Defaults to False. + show_headers: Whether to show headers for the dashboard. Defaults to True. + Returns: The rendered dashboard. """ - return DashboardElement(element) + # return DashboardElement(element) + return BaseElement("deephaven.ui.components.Dashboard", element, show_close_icon=show_close_icon, show_headers=show_headers) # type: ignore[return-value] diff --git a/plugins/ui/src/js/src/DashboardPlugin.tsx b/plugins/ui/src/js/src/DashboardPlugin.tsx index 9f27f04cf..7ecc7e3cf 100644 --- a/plugins/ui/src/js/src/DashboardPlugin.tsx +++ b/plugins/ui/src/js/src/DashboardPlugin.tsx @@ -70,7 +70,9 @@ export function DashboardPlugin( id, PLUGIN_NAME ) as unknown as [DashboardPluginData, (data: DashboardPluginData) => void]; - const [initialPluginData] = useState(pluginData); + const [initialPluginData] = useState({ + openWidgets: {}, + } as DashboardPluginData); // Keep track of the widgets we've got opened. const [widgetMap, setWidgetMap] = useState< @@ -147,20 +149,21 @@ export function DashboardPlugin( log.debug('loadInitialPluginData', initialPluginData); setWidgetMap(prevWidgetMap => { - const newWidgetMap = new Map(prevWidgetMap); - const { openWidgets } = initialPluginData; - if (openWidgets != null) { - Object.entries(openWidgets).forEach( - ([widgetId, { descriptor, data }]) => { - newWidgetMap.set(widgetId, { - id: widgetId, - widget: descriptor, - data, - }); - } - ); - } - return newWidgetMap; + return new Map(); + // const newWidgetMap = new Map(prevWidgetMap); + // const { openWidgets } = initialPluginData; + // if (openWidgets != null) { + // Object.entries(openWidgets).forEach( + // ([widgetId, { descriptor, data }]) => { + // newWidgetMap.set(widgetId, { + // id: widgetId, + // widget: descriptor, + // data, + // }); + // } + // ); + // } + // return newWidgetMap; }); }, [initialPluginData, id] @@ -278,7 +281,7 @@ export function DashboardPlugin( diff --git a/plugins/ui/src/js/src/layout/Column.tsx b/plugins/ui/src/js/src/layout/Column.tsx index c60cbae36..62e24881e 100644 --- a/plugins/ui/src/js/src/layout/Column.tsx +++ b/plugins/ui/src/js/src/layout/Column.tsx @@ -13,6 +13,7 @@ function LayoutColumn({ children, width, }: ColumnElementProps): JSX.Element | null { + console.log('xxx doing a LayoutColumn'); const layoutManager = useLayoutManager(); const parent = useParentItem(); diff --git a/plugins/ui/src/js/src/layout/Dashboard.tsx b/plugins/ui/src/js/src/layout/Dashboard.tsx index 9ce1fe0c1..c9280fb8e 100644 --- a/plugins/ui/src/js/src/layout/Dashboard.tsx +++ b/plugins/ui/src/js/src/layout/Dashboard.tsx @@ -1,20 +1,193 @@ -import React from 'react'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { nanoid } from 'nanoid'; +import { + Dashboard as DHCDashboard, + LayoutManagerContext, +} from '@deephaven/dashboard'; +import GoldenLayout, { + Settings as LayoutSettings, +} from '@deephaven/golden-layout'; +import { useDashboardPlugins } from '@deephaven/plugin'; +import Log from '@deephaven/log'; import { normalizeDashboardChildren, type DashboardElementProps, } from './LayoutUtils'; import { ParentItemContext, useParentItem } from './ParentItemContext'; +import ReactPanel from './ReactPanel'; +import { ReactPanelContext, usePanelId } from './ReactPanelContext'; +import useWidgetStatus from './useWidgetStatus'; +import { ReactPanelManagerContext } from './ReactPanelManager'; +import PortalPanelManager from './PortalPanelManager'; + +const log = Log.module('@deephaven/js-plugin-ui/DocumentHandler'); + +const DEFAULT_SETTINGS: Partial = Object.freeze({ + showCloseIcon: false, + constrainDragToContainer: true, +}); -function Dashboard({ children }: DashboardElementProps): JSX.Element | null { - const parent = useParentItem(); +function Dashboard({ + children, + showCloseIcon = false, + showHeaders = true, +}: DashboardElementProps): JSX.Element | null { + const [childLayout, setChildLayout] = useState(); + // const parent = useParentItem(); + const plugins = useDashboardPlugins(); const normalizedChildren = normalizeDashboardChildren(children); + console.log('xxx doing a dashboard with layout', childLayout, showCloseIcon); + + const panelIdIndex = useRef(0); + // panelIds that are currently opened within this document. This list is tracked by the `onOpen`/`onClose` call on the `ReactPanelManager` from a child component. + // Note that the initial widget data provided will be the `panelIds` for this document to use; this array is what is actually opened currently. + const panelIds = useRef([]); + + // Flag to signal the panel counts have changed in the last render + // We may need to check if we need to close this widget if all panels are closed + const [isPanelsDirty, setPanelsDirty] = useState(false); + + const handleOpen = useCallback( + (panelId: string) => { + if (panelIds.current.includes(panelId)) { + throw new Error('Duplicate panel opens received'); + } + + panelIds.current.push(panelId); + log.debug('Panel opened, open count', panelIds.current.length); + + setPanelsDirty(true); + }, + [panelIds] + ); + + const handleClose = useCallback( + (panelId: string) => { + const panelIndex = panelIds.current.indexOf(panelId); + if (panelIndex === -1) { + throw new Error('Panel close received for unknown panel'); + } + + panelIds.current.splice(panelIndex, 1); + log.debug('Panel closed, open count', panelIds.current.length); + + setPanelsDirty(true); + }, + [panelIds] + ); + + /** + * When there are changes made to panels in a render cycle, check if they've all been closed and fire an `onClose` event if they are. + * Otherwise, fire an `onDataChange` event with the updated panelIds that are open. + */ + // useEffect( + // function syncOpenPanels() { + // if (!isPanelsDirty) { + // return; + // } + + // setPanelsDirty(false); + + // // Check if all the panels in this widget are closed + // // We do it outside of the `handleClose` function in case a new panel opens up in the same render cycle + // log.debug2('dashboard', 'open panel count', panelIds.current.length); + // if (panelIds.current.length === 0) { + // // Let's just ignore this for now ... + // log.debug('Dashboard', 'closed all panels, triggering onClose'); + // // onClose?.(); + // } else { + // // onDataChange({ ...widgetData, panelIds: panelIds.current }); + // } + // }, + // [isPanelsDirty] + // ); + + const getPanelId = useCallback(() => { + // On rehydration, yield known IDs first + // If there are no more known IDs, generate a new one. + // This can happen if the document hasn't been opened before, or if it's rehydrated and a new panel is added. + // Note that if the order of panels changes, the worst case scenario is that panels appear in the wrong location in the layout. + const panelId = nanoid(); + panelIdIndex.current += 1; + return panelId; + }, []); + + const widgetStatus = useWidgetStatus(); + const panelManager = useMemo( + () => ({ + metadata: widgetStatus.descriptor, + onOpen: handleOpen, + onClose: handleClose, + onDataChange: () => log.debug('xxx Panel data changed'), + getPanelId, + getInitialData: () => [], + }), + [ + widgetStatus, + getPanelId, + handleClose, + handleOpen, + // handleDataChange, + // getInitialData, + ] + ); + const [isLayoutInitialized, setLayoutInitialized] = useState(false); + const layoutSettings: Partial = useMemo( + () => ({ + ...DEFAULT_SETTINGS, + showCloseIcon, + hasHeaders: showHeaders, + }), + [showCloseIcon, showHeaders] + ); return ( - - {normalizedChildren} - + // <> + <> + {/* */} + setLayoutInitialized(true)} + layoutSettings={layoutSettings} + > + {plugins} + + + + + {isLayoutInitialized && normalizedChildren} + + + + + + {/* */} + {/* Resetting the panel ID so the children don't get confused */} + {/* + + {childLayout != null && ( + + + {normalizedChildren} + + + )} + + */} + + // ); + // + // {normalizedChildren} + // + // } export default Dashboard; diff --git a/plugins/ui/src/js/src/layout/LayoutUtils.tsx b/plugins/ui/src/js/src/layout/LayoutUtils.tsx index b2d78ec49..ff7934df3 100644 --- a/plugins/ui/src/js/src/layout/LayoutUtils.tsx +++ b/plugins/ui/src/js/src/layout/LayoutUtils.tsx @@ -113,9 +113,12 @@ export function isStackElementNode(obj: unknown): obj is StackElementNode { ); } -export type DashboardElementProps = React.PropsWithChildren< - Record ->; +export type DashboardElementProps = React.PropsWithChildren<{ + /** Whether to show the close icon in the top right corner of the dashboard */ + showCloseIcon?: boolean; + /** Whether to show headers for the dashboard */ + showHeaders?: boolean; +}>; /** * Describes a dashboard element that can be rendered in the UI. @@ -153,6 +156,7 @@ export function isDashboardElementNode( export function normalizeDashboardChildren( children: React.ReactNode ): React.ReactNode { + console.log('xxx normalizeDashboardChildren', children); const needsWrapper = Children.count(children) > 1; const hasRows = Children.toArray(children).some( child => isValidElement(child) && child.type === Row diff --git a/plugins/ui/src/js/src/layout/Row.tsx b/plugins/ui/src/js/src/layout/Row.tsx index cad61b97e..7c7c1dcb2 100644 --- a/plugins/ui/src/js/src/layout/Row.tsx +++ b/plugins/ui/src/js/src/layout/Row.tsx @@ -7,6 +7,7 @@ import { ParentItemContext, useParentItem } from './ParentItemContext'; import { usePanelId } from './ReactPanelContext'; function LayoutRow({ children, height }: RowElementProps): JSX.Element | null { + console.log('xxx doing a LayoutRow'); const layoutManager = useLayoutManager(); const parent = useParentItem(); const row = useMemo(() => { @@ -45,6 +46,7 @@ function Row({ children, height }: RowElementProps): JSX.Element { return {children}; } + console.log('xxx doing a flexRow with panelId', panelId); return ( {children} diff --git a/plugins/ui/src/js/src/styles.scss b/plugins/ui/src/js/src/styles.scss index 223537d1c..9bf789692 100644 --- a/plugins/ui/src/js/src/styles.scss +++ b/plugins/ui/src/js/src/styles.scss @@ -59,15 +59,19 @@ position: absolute; } } + + .dashboard-container { + border: 1px solid var(--dh-color-bg); + } } - &:has(.dh-inner-react-panel > .iris-grid:only-child), + &:has(> .dh-inner-react-panel > .iris-grid:only-child), &:has( - .dh-inner-react-panel + > .dh-inner-react-panel > .ui-table-container:only-child > .iris-grid:only-child ), - &:has(.dh-inner-react-panel > .chart-wrapper:only-child) { + &:has(> .dh-inner-react-panel > .chart-wrapper:only-child) { // remove the default panel padding when grid or chart is the only child padding: 0 !important; // important required to override inline spectrum style .iris-grid { @@ -75,6 +79,15 @@ border-radius: 0; } } + + // remove padding and border around single child dashboards in react panels + &:has(> .dh-inner-react-panel > .dashboard-container:only-child) { + padding: 0 !important; // important required to override inline spectrum style + + > .dh-inner-react-panel > .dashboard-container { + border: none; + } + } } .ui-text-wrap-balance { diff --git a/plugins/ui/src/js/src/widget/DocumentHandler.tsx b/plugins/ui/src/js/src/widget/DocumentHandler.tsx index 8791a86b3..f381d36ea 100644 --- a/plugins/ui/src/js/src/widget/DocumentHandler.tsx +++ b/plugins/ui/src/js/src/widget/DocumentHandler.tsx @@ -130,6 +130,7 @@ function DocumentHandler({ panelIds.current.length ); if (panelIds.current.length === 0) { + // Let's just ignore this for now ... log.debug('Widget', widget.id, 'closed all panels, triggering onClose'); onClose?.(); } else { diff --git a/plugins/ui/src/js/src/widget/DocumentUtils.tsx b/plugins/ui/src/js/src/widget/DocumentUtils.tsx index c145b5874..0274ea4b8 100644 --- a/plugins/ui/src/js/src/widget/DocumentUtils.tsx +++ b/plugins/ui/src/js/src/widget/DocumentUtils.tsx @@ -45,7 +45,7 @@ export function getRootChildren( throw new MixedPanelsError('Cannot mix Panel and Dashboard elements'); } - if (nonLayoutCount === childrenArray.length) { + if (nonLayoutCount === childrenArray.length || dashboardCount > 0) { // Just wrap it in a panel return ( diff --git a/plugins/ui/src/js/src/widget/WidgetHandler.tsx b/plugins/ui/src/js/src/widget/WidgetHandler.tsx index 91646d7db..57dd97185 100644 --- a/plugins/ui/src/js/src/widget/WidgetHandler.tsx +++ b/plugins/ui/src/js/src/widget/WidgetHandler.tsx @@ -98,12 +98,12 @@ function WidgetHandler({ if (widget !== prevWidget) { setPrevWidget(widget); - if (widget != null && widget.type === DASHBOARD_ELEMENT) { - log.info( - 'Dashboard widget has changed, removing previous elements from layout' - ); - layoutManager.root.contentItems.forEach(item => item.remove()); - } + // if (widget != null && widget.type === DASHBOARD_ELEMENT) { + // log.info( + // 'Dashboard widget has changed, removing previous elements from layout' + // ); + // layoutManager.root.contentItems.forEach(item => item.remove()); + // } } if (widgetError != null && isLoading) {