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) {