Skip to content

Commit 804258f

Browse files
committed
feat: DH-21757: WidgetPlugin support in deephaven.ui
- Needs deephaven/web-client-ui#2648 - WidgetPlugin just registers directly - Allows for nested ui.resolve deephaven.ui components - Still maintain DashboardPlugin to handle legacy behaviour - Works with the ui_home_screen, nested dashboards, etc - Styling a little bit different (ui.panel is automatically wrapped in a nested dashboard rather than opening up panels in the existing dashboard like before) - Dashboard now has a `show_headers` option that can be set to False to not show headers on a dashboard. Useful if you're doing a homescreen type dashboard
1 parent cc79762 commit 804258f

21 files changed

+533
-145
lines changed

plugins/ui/src/deephaven/ui/components/dashboard.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,20 @@
44
from ..elements import DashboardElement, FunctionElement
55

66

7-
def dashboard(element: FunctionElement) -> DashboardElement:
7+
def dashboard(
8+
element: FunctionElement,
9+
*,
10+
show_headers: bool = True,
11+
) -> DashboardElement:
812
"""
913
A dashboard is the container for an entire layout.
1014
1115
Args:
1216
element: Element to render as the dashboard.
1317
The element should render a layout that contains 1 root column or row.
18+
show_headers: Whether to show headers on the dashboard panels. Defaults to True.
1419
1520
Returns:
1621
The rendered dashboard.
1722
"""
18-
return DashboardElement(element)
23+
return DashboardElement(element, show_headers=show_headers)

plugins/ui/src/deephaven/ui/elements/DashboardElement.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,14 @@
88

99

1010
class DashboardElement(BaseElement):
11-
def __init__(self, element: FunctionElement):
12-
super().__init__("deephaven.ui.components.Dashboard", element)
11+
def __init__(
12+
self,
13+
element: FunctionElement,
14+
*,
15+
show_headers: bool = True,
16+
):
17+
super().__init__(
18+
"deephaven.ui.components.Dashboard",
19+
element,
20+
show_headers=show_headers,
21+
)

plugins/ui/src/js/src/DashboardPlugin.tsx

Lines changed: 14 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,19 @@
11
import React, { useCallback, useEffect, useMemo, useState } from 'react';
2-
import { nanoid } from 'nanoid';
32
import {
43
DashboardPluginComponentProps,
54
LayoutManagerContext,
65
LayoutUtils,
76
PanelEvent,
87
useListener,
98
useDashboardPluginData,
10-
emitCreateDashboard,
119
WidgetDescriptor,
12-
PanelOpenEventDetail,
1310
DEFAULT_DASHBOARD_ID,
1411
useDashboardPanel,
1512
} from '@deephaven/dashboard';
1613
import Log from '@deephaven/log';
1714
import { DeferredApiBootstrap } from '@deephaven/jsapi-bootstrap';
18-
import { dh } from '@deephaven/jsapi-types';
1915
import { ErrorBoundary } from '@deephaven/components';
2016
import { useDebouncedCallback } from '@deephaven/react-hooks';
21-
import styles from './styles.scss?inline';
2217
import {
2318
ReadonlyWidgetData,
2419
WidgetDataUpdate,
@@ -27,11 +22,6 @@ import {
2722
import PortalPanel from './layout/PortalPanel';
2823
import PortalPanelManager from './layout/PortalPanelManager';
2924
import DashboardWidgetHandler from './widget/DashboardWidgetHandler';
30-
import {
31-
getPreservedData,
32-
DASHBOARD_ELEMENT,
33-
WIDGET_ELEMENT,
34-
} from './widget/WidgetUtils';
3525
import { usePanelId } from './layout/ReactPanelContext';
3626

3727
const PLUGIN_NAME = '@deephaven/js-plugin-ui.DashboardPlugin';
@@ -63,6 +53,13 @@ interface WidgetWrapper {
6353
data?: ReadonlyWidgetData;
6454
}
6555

56+
/**
57+
* Handle legacy behaviour of an open widget being saved with the dashboard.
58+
*
59+
* Now UIWidgetPlugin is responsible for opening widgets in the dashboard.
60+
* @param props Dashboard plugin props
61+
* @returns Dashboard plugin content, which is responsible for handling legacy behaviour of an open widget being saved with the dashboard
62+
*/
6663
function InnerDashboardPlugin(
6764
props: DashboardPluginComponentProps
6865
): JSX.Element | null {
@@ -78,66 +75,6 @@ function InnerDashboardPlugin(
7875
ReadonlyMap<WidgetId, WidgetWrapper>
7976
>(new Map());
8077

81-
const handleWidgetOpen = useCallback(
82-
({ widgetId, widget }: { widgetId: string; widget: WidgetDescriptor }) => {
83-
log.debug('Opening widget with ID', widgetId, widget);
84-
setWidgetMap(prevWidgetMap => {
85-
const newWidgetMap = new Map(prevWidgetMap);
86-
const oldWidget = newWidgetMap.get(widgetId);
87-
newWidgetMap.set(widgetId, {
88-
id: widgetId,
89-
widget,
90-
data: getPreservedData(oldWidget?.data),
91-
});
92-
return newWidgetMap;
93-
});
94-
},
95-
[]
96-
);
97-
98-
const handleDashboardOpen = useCallback(
99-
({
100-
widget,
101-
dashboardId,
102-
}: {
103-
widget: WidgetDescriptor;
104-
dashboardId: string;
105-
}) => {
106-
const { name: title } = widget;
107-
log.debug('Emitting create dashboard event for', dashboardId, widget);
108-
emitCreateDashboard(layout.eventHub, {
109-
pluginId: PLUGIN_NAME,
110-
title: title ?? 'Untitled',
111-
data: { openWidgets: { [dashboardId]: { descriptor: widget } } },
112-
});
113-
},
114-
[layout.eventHub]
115-
);
116-
117-
const handlePanelOpen = useCallback(
118-
({
119-
panelId: widgetId = nanoid(),
120-
widget,
121-
}: PanelOpenEventDetail<dh.Widget>) => {
122-
const { type } = widget;
123-
124-
switch (type) {
125-
case WIDGET_ELEMENT: {
126-
handleWidgetOpen({ widgetId, widget });
127-
break;
128-
}
129-
case DASHBOARD_ELEMENT: {
130-
handleDashboardOpen({ widget, dashboardId: widgetId });
131-
break;
132-
}
133-
default: {
134-
break;
135-
}
136-
}
137-
},
138-
[handleDashboardOpen, handleWidgetOpen]
139-
);
140-
14178
useEffect(
14279
function loadInitialPluginData() {
14380
if (initialPluginData == null) {
@@ -203,8 +140,6 @@ function InnerDashboardPlugin(
203140
});
204141
}, []);
205142

206-
// TODO: We need to change up the event system for how objects are opened, since in this case it could be opening multiple panels
207-
useListener(layout.eventHub, PanelEvent.OPEN, handlePanelOpen);
208143
useListener(layout.eventHub, PanelEvent.CLOSE, handlePanelClose);
209144

210145
const sendPluginDataUpdate = useCallback(
@@ -282,12 +217,18 @@ function InnerDashboardPlugin(
282217

283218
return (
284219
<LayoutManagerContext.Provider value={layout}>
285-
<style>{styles}</style>
286220
<PortalPanelManager>{widgetHandlers}</PortalPanelManager>
287221
</LayoutManagerContext.Provider>
288222
);
289223
}
290224

225+
/**
226+
* Dashboard plugin that registers the PortalPanel type for deephaven.ui
227+
*
228+
* It's also responsible for handling legacy behaviour, for old dashboards that may have opened a deephaven.ui widget previously.
229+
* @param props Dashboard plugin props
230+
* @returns Dashboard plugin
231+
*/
291232
export function DashboardPlugin(
292233
props: DashboardPluginComponentProps
293234
): JSX.Element | null {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React, { useCallback, useMemo } from 'react';
2+
import { UriVariableDescriptor } from '@deephaven/jsapi-bootstrap';
3+
import type { dh } from '@deephaven/jsapi-types';
4+
import {
5+
usePersistentState,
6+
type WidgetComponentProps,
7+
} from '@deephaven/plugin';
8+
import { WidgetDescriptor } from '@deephaven/dashboard';
9+
import { nanoid } from 'nanoid';
10+
import { WidgetData, WidgetDataUpdate } from './widget/WidgetTypes';
11+
import WidgetHandler from './widget/WidgetHandler';
12+
13+
type UIComponentProps = WidgetComponentProps<dh.Widget> & {
14+
// TODO: We shouldn't need this, should be added to the WidgetComponentProps type
15+
metadata?: WidgetDescriptor;
16+
17+
// Might be loading a URI resolved widget...
18+
uri?: UriVariableDescriptor;
19+
};
20+
21+
export function UIComponent(props: UIComponentProps): JSX.Element | null {
22+
const { metadata: widgetDescriptor, uri, __dhId } = props;
23+
24+
const [widgetData, setWidgetData] = usePersistentState<
25+
WidgetData | undefined
26+
>(undefined, { type: 'UIComponentWidgetData', version: 1 });
27+
28+
const id = useMemo(
29+
() => __dhId ?? widgetDescriptor?.id ?? nanoid(),
30+
[__dhId, widgetDescriptor]
31+
);
32+
33+
const handleDataChange = useCallback(
34+
(data: WidgetDataUpdate) => {
35+
setWidgetData(oldData => ({ ...oldData, ...data }));
36+
},
37+
[setWidgetData]
38+
);
39+
40+
const descriptor = uri ?? widgetDescriptor;
41+
if (descriptor == null) {
42+
throw new Error('No widget descriptor');
43+
}
44+
return (
45+
<WidgetHandler
46+
widgetDescriptor={descriptor}
47+
initialData={widgetData}
48+
onDataChange={handleDataChange}
49+
id={id}
50+
/>
51+
);
52+
}
53+
54+
export default UIComponent;

plugins/ui/src/js/src/UIWidget.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { dh } from '@deephaven/jsapi-types';
2+
import { type WidgetComponentProps } from '@deephaven/plugin';
3+
import { WidgetDescriptor } from '@deephaven/dashboard';
4+
import UIComponent from './UIComponent';
5+
import PortalPanel from './layout/PortalPanel';
6+
7+
type UIWidgetProps = WidgetComponentProps<dh.Widget> & {
8+
// TODO: We shouldn't need this, should be added to the WidgetComponentProps type
9+
metadata?: WidgetDescriptor;
10+
};
11+
12+
export function UIWidget(props: UIWidgetProps): JSX.Element | null {
13+
const { metadata: widgetDescriptor } = props;
14+
if (widgetDescriptor?.type === PortalPanel.displayName) {
15+
return null;
16+
}
17+
18+
// eslint-disable-next-line react/jsx-props-no-spreading
19+
return <UIComponent {...props} />;
20+
}
21+
22+
export default UIWidget;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { type WidgetPlugin, PluginType } from '@deephaven/plugin';
2+
import { vsGraph } from '@deephaven/icons';
3+
import type { dh } from '@deephaven/jsapi-types';
4+
import { DASHBOARD_ELEMENT, WIDGET_ELEMENT } from './widget/WidgetUtils';
5+
import PortalPanel from './layout/PortalPanel';
6+
import UIWidget from './UIWidget';
7+
import styles from './styles.scss?inline';
8+
9+
// We need to inject the styles into the document when we're loaded... we only want to do this once.
10+
const styleElement = document.createElement('style');
11+
styleElement.textContent = styles;
12+
document.head.appendChild(styleElement);
13+
14+
export const UIWidgetPlugin: WidgetPlugin<dh.Widget> = {
15+
name: '@deephaven/js-plugin-ui',
16+
type: PluginType.WIDGET_PLUGIN,
17+
supportedTypes: [WIDGET_ELEMENT, DASHBOARD_ELEMENT, PortalPanel.displayName],
18+
component: UIWidget,
19+
icon: vsGraph,
20+
};
21+
22+
export default UIWidgetPlugin;

plugins/ui/src/js/src/index.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,23 @@
1-
export * from './DashboardPlugin';
1+
import { PluginType } from '@deephaven/plugin';
2+
import { UIWidgetPlugin } from './UIWidgetPlugin';
3+
import { DashboardPlugin } from './DashboardPlugin';
4+
5+
// TODO: Remove local MultiPlugin type once @deephaven/plugin is updated
6+
// to include it (see deephaven/web-client-ui composite-plugins branch)
7+
const MULTI_PLUGIN = 'MultiPlugin' as const;
8+
9+
const UIDashboardPlugin = {
10+
name: '@deephaven/js-plugin-ui.DashboardPlugin',
11+
type: PluginType.DASHBOARD_PLUGIN as string,
12+
component: DashboardPlugin,
13+
};
14+
15+
const UIMultiPlugin = {
16+
name: '@deephaven/js-plugin-ui',
17+
type: MULTI_PLUGIN as string,
18+
plugins: [UIWidgetPlugin, UIDashboardPlugin],
19+
};
20+
21+
export { DashboardPlugin };
22+
23+
export default UIMultiPlugin;

plugins/ui/src/js/src/layout/Dashboard.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
2+
import { usePanelId as useLayoutPanelId } from '@deephaven/dashboard';
23
import { ElementIdProps, type DashboardElementProps } from './LayoutUtils';
3-
import { usePanelId } from './ReactPanelContext';
4+
import { usePanelId as useReactPanelId } from './ReactPanelContext';
45
import NestedDashboard from './NestedDashboard';
56
import DashboardContent from './DashboardContent';
67

@@ -12,12 +13,14 @@ import DashboardContent from './DashboardContent';
1213
*/
1314
function Dashboard({
1415
children,
15-
__dhId,
16+
...otherProps
1617
}: DashboardElementProps & ElementIdProps): JSX.Element | null {
17-
const contextPanelId = usePanelId();
18-
const isNested = contextPanelId != null;
18+
const contextPanelId = useLayoutPanelId();
19+
const reactPanelId = useReactPanelId();
20+
const isNested = contextPanelId != null || reactPanelId != null;
1921
if (isNested) {
20-
return <NestedDashboard __dhId={__dhId}>{children}</NestedDashboard>;
22+
// eslint-disable-next-line react/jsx-props-no-spreading
23+
return <NestedDashboard {...otherProps}>{children}</NestedDashboard>;
2124
}
2225

2326
return <DashboardContent>{children}</DashboardContent>;

0 commit comments

Comments
 (0)