Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions plugins/ui/src/deephaven/ui/components/dashboard.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
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.

Args:
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]
35 changes: 19 additions & 16 deletions plugins/ui/src/js/src/DashboardPlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -278,7 +281,7 @@ export function DashboardPlugin(
<DashboardWidgetHandler
widgetDescriptor={wrapper.widget}
id={wrapper.id}
initialData={wrapper.data}
// initialData={wrapper.data}
onDataChange={handleWidgetDataChange}
onClose={handleWidgetClose}
/>
Expand Down
1 change: 1 addition & 0 deletions plugins/ui/src/js/src/layout/Column.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ function LayoutColumn({
children,
width,
}: ColumnElementProps): JSX.Element | null {
console.log('xxx doing a LayoutColumn');
const layoutManager = useLayoutManager();
const parent = useParentItem();

Expand Down
185 changes: 179 additions & 6 deletions plugins/ui/src/js/src/layout/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -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<LayoutSettings> = 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<GoldenLayout>();
// 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<string[]>([]);

// 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<LayoutSettings> = useMemo(
() => ({
...DEFAULT_SETTINGS,
showCloseIcon,
hasHeaders: showHeaders,
}),
[showCloseIcon, showHeaders]
);

return (
<ParentItemContext.Provider value={parent}>
{normalizedChildren}
</ParentItemContext.Provider>
// <>
<>
{/* <ReactPanelManagerContext.Provider value={panelManager}> */}
<DHCDashboard
onGoldenLayoutChange={setChildLayout}
onLayoutInitialized={() => setLayoutInitialized(true)}
layoutSettings={layoutSettings}
>
{plugins}
<PortalPanelManager>
<ReactPanelManagerContext.Provider value={panelManager}>
<ParentItemContext.Provider value={null}>
<ReactPanelContext.Provider value={null}>
{isLayoutInitialized && normalizedChildren}
</ReactPanelContext.Provider>
</ParentItemContext.Provider>
</ReactPanelManagerContext.Provider>
</PortalPanelManager>
</DHCDashboard>
{/* </ReactPanelManagerContext.Provider> */}
{/* Resetting the panel ID so the children don't get confused */}
{/* <ReactPanelContext.Provider value={null}>
<ReactPanelManagerContext.Provider value={panelManager}>
{childLayout != null && (
<LayoutManagerContext.Provider value={childLayout}>
<ParentItemContext.Provider value={childLayout.root}>
{normalizedChildren}
</ParentItemContext.Provider>
</LayoutManagerContext.Provider>
)}
</ReactPanelManagerContext.Provider>
</ReactPanelContext.Provider> */}
</>
// </>
);
// <ParentItemContext.Provider value={parent}>
// {normalizedChildren}
// </ParentItemContext.Provider>
// </DashboardLayout>
}

export default Dashboard;
10 changes: 7 additions & 3 deletions plugins/ui/src/js/src/layout/LayoutUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,12 @@ export function isStackElementNode(obj: unknown): obj is StackElementNode {
);
}

export type DashboardElementProps = React.PropsWithChildren<
Record<string, unknown>
>;
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.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions plugins/ui/src/js/src/layout/Row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -45,6 +46,7 @@ function Row({ children, height }: RowElementProps): JSX.Element {
return <LayoutRow height={height}>{children}</LayoutRow>;
}

console.log('xxx doing a flexRow with panelId', panelId);
return (
<Flex height={`${height}%`} direction="row">
{children}
Expand Down
19 changes: 16 additions & 3 deletions plugins/ui/src/js/src/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -59,22 +59,35 @@
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 {
border: none;
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 {
Expand Down
1 change: 1 addition & 0 deletions plugins/ui/src/js/src/widget/DocumentHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion plugins/ui/src/js/src/widget/DocumentUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<ReactPanel title={widget.name ?? widget.id ?? widget.type}>
Expand Down
Loading
Loading