-
Notifications
You must be signed in to change notification settings - Fork 17
feat: DH-21757: WidgetPlugin support in deephaven.ui #1341
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 33 commits
c68e7ad
16c6325
fd4c6f9
d2b4b1c
7258fab
022311d
1a587ce
2ed7530
5e71959
7303d1b
d4f2271
def07ab
5206dd3
3c12046
8985543
1cd9196
5dc2ae0
0cf90fb
eca77e8
676c9aa
1fe9308
2c00340
563f9d9
3219d0a
c721b22
e901a09
b6ac649
2364957
f0dbf6f
2a26402
b5987f8
94fb997
c6a28c2
f2baf18
4217358
209e903
771f24c
d917157
3fe4566
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {"file":"components/dashboard.md","objects":{"dash_no_headers":{"type":"deephaven.ui.Dashboard","data":{"document":{"props":{"showHeaders":false,"children":{"__dhElemName":"deephaven.ui.components.Row","props":{"children":[{"__dhElemName":"deephaven.ui.components.Panel","props":{"title":"A","direction":"column","alignItems":"start","gap":"size-100","overflow":"auto","padding":"size-100","children":"A"}},{"__dhElemName":"deephaven.ui.components.Panel","props":{"title":"B","direction":"column","alignItems":"start","gap":"size-100","overflow":"auto","padding":"size-100","children":"B"}}]}}},"__dhElemName":"deephaven.ui.components.Dashboard"},"state":"{}"}}}} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {"file":"components/uri.md","objects":{"remote_widget":{"type":"deephaven.ui.Element","data":{"document":{"props":{"uri":"pq://DataServiceQuery/scope/my_widget"},"__dhElemName":"deephaven.ui.elements.UriElement"},"state":"{}"}},"my_dashboard":{"type":"deephaven.ui.Dashboard","data":{"document":{"props":{"showHeaders":true,"children":{"__dhElemName":"__main__.cross_query_dashboard","props":{"children":{"__dhElemName":"deephaven.ui.components.Row","props":{"children":[{"__dhElemName":"deephaven.ui.components.Panel","props":{"title":"Remote Widget","direction":"column","alignItems":"start","gap":"size-100","overflow":"auto","padding":"size-100","children":{"__dhElemName":"deephaven.ui.elements.UriElement","props":{"uri":"pq://DataServiceQuery/scope/my_widget"}}}},{"__dhElemName":"deephaven.ui.components.Panel","props":{"title":"Local","direction":"column","alignItems":"start","gap":"size-100","overflow":"auto","padding":"size-100","children":"Local Content"}}]}}}}},"__dhElemName":"deephaven.ui.components.Dashboard"},"state":"{}"}}}} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,12 @@ | ||
| // Mock @deephaven/plugin package | ||
| const React = require('react'); | ||
| const PluginActual = jest.requireActual('@deephaven/plugin'); | ||
|
|
||
| module.exports = { | ||
| ...PluginActual, | ||
| useDashboardPlugins: jest.fn(() => []), | ||
| // Mock usePersistentState to behave like useState. | ||
| // The real implementation requires FiberProvider which is internal to Dashboard. | ||
| usePersistentState: (initialState, _config) => React.useState(initialState), | ||
| __esModule: true, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import React, { useCallback, useMemo } from 'react'; | ||
| import { type UriVariableDescriptor } from '@deephaven/jsapi-bootstrap'; | ||
| import type { dh } from '@deephaven/jsapi-types'; | ||
| import { | ||
| usePersistentState, | ||
| type WidgetComponentProps, | ||
| } from '@deephaven/plugin'; | ||
| import { nanoid } from 'nanoid'; | ||
| import { type WidgetData, type WidgetDataUpdate } from './widget/WidgetTypes'; | ||
| import WidgetHandler from './widget/WidgetHandler'; | ||
|
|
||
| type UIComponentProps = WidgetComponentProps<dh.Widget> & { | ||
| // Might be loading a URI resolved widget... | ||
| uri?: UriVariableDescriptor; | ||
| }; | ||
|
|
||
| export function UIComponent(props: UIComponentProps): JSX.Element | null { | ||
| const { metadata: widgetDescriptor, uri, __dhId } = props; | ||
|
|
||
| const [widgetData, setWidgetData] = usePersistentState< | ||
| WidgetData | undefined | ||
| >(undefined, { type: 'UIComponentWidgetData', version: 1 }); | ||
|
|
||
| const id = useMemo( | ||
| () => __dhId ?? widgetDescriptor?.id ?? nanoid(), | ||
| [__dhId, widgetDescriptor] | ||
| ); | ||
|
|
||
| const handleDataChange = useCallback( | ||
| (data: WidgetDataUpdate) => { | ||
| setWidgetData(oldData => ({ ...oldData, ...data })); | ||
| }, | ||
| [setWidgetData] | ||
| ); | ||
|
|
||
| const descriptor = uri ?? widgetDescriptor; | ||
| if (descriptor == null) { | ||
| throw new Error('No widget descriptor'); | ||
| } | ||
|
|
||
| const renderEmptyDocument = useCallback( | ||
| () => ( | ||
| // Single-panel or first-time-load case. Returning a fragment causes | ||
| // `getRootChildren` to wrap it in a `DefaultPanelContent`, which renders | ||
| // a `LoadingOverlay` while the widget status is `loading`. | ||
| // eslint-disable-next-line react/jsx-no-useless-fragment | ||
| <></> | ||
| ), | ||
| // We only want to update this callback when the descriptor changes, not | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we update this callback as the return value never changes? I can see that we do it intentionally. Is it to trigger an update in WidgetHandler when the descriptor changes?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, I re-wired this and it shouldn't need to change on the descriptor anymore. |
||
| // every time the widgetData (panelIds) changes. | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| [descriptor] | ||
| ); | ||
|
|
||
| return ( | ||
| <WidgetHandler | ||
| widgetDescriptor={descriptor} | ||
| initialData={widgetData} | ||
| onDataChange={handleDataChange} | ||
| renderEmptyDocument={renderEmptyDocument} | ||
| id={id} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| export default UIComponent; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import type { dh } from '@deephaven/jsapi-types'; | ||
| import { type WidgetComponentProps } from '@deephaven/plugin'; | ||
| import UIComponent from './UIComponent'; | ||
| import PortalPanel from './layout/PortalPanel'; | ||
|
|
||
| type UIWidgetProps = WidgetComponentProps<dh.Widget>; | ||
|
|
||
| export function UIWidget(props: UIWidgetProps): JSX.Element | null { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should also have a comment explaining UIWidget. |
||
| const { metadata: widgetDescriptor } = props; | ||
| if (widgetDescriptor?.type === PortalPanel.displayName) { | ||
| // PortalPanel was used by the legacy DashboardPlugin to render elements. We just ignore them here. | ||
| return null; | ||
| } | ||
|
|
||
| // eslint-disable-next-line react/jsx-props-no-spreading | ||
| return <UIComponent {...props} />; | ||
| } | ||
|
|
||
| export default UIWidget; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { type WidgetPlugin, PluginType } from '@deephaven/plugin'; | ||
| import { vsGraph } from '@deephaven/icons'; | ||
| import type { dh } from '@deephaven/jsapi-types'; | ||
| import { DASHBOARD_ELEMENT, WIDGET_ELEMENT } from './widget/WidgetUtils'; | ||
| import PortalPanel from './layout/PortalPanel'; | ||
| import UIWidget from './UIWidget'; | ||
|
|
||
| export const UIWidgetPlugin: WidgetPlugin<dh.Widget> = { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should also have a comment explaining UIWidgetPlugin. |
||
| name: '@deephaven/js-plugin-ui', | ||
| type: PluginType.WIDGET_PLUGIN, | ||
| supportedTypes: [WIDGET_ELEMENT, DASHBOARD_ELEMENT, PortalPanel.displayName], | ||
| component: UIWidget, | ||
| icon: vsGraph, | ||
| }; | ||
|
|
||
| export default UIWidgetPlugin; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add a comment explaining UIComponent.