From a1769b23646130ab631c76ba5fade4eaf084a5af Mon Sep 17 00:00:00 2001 From: Mike Bender Date: Thu, 23 Nov 2023 16:04:35 -0500 Subject: [PATCH 1/5] WIP render comopnents when decoding - This way components are rendered directly - Still need to automatically wrap tables, figures, and top level to a panel to keep that behaviour Clean up a bit - it's working for a simple counter example - Still need to wrap in a panel automatically, wrap in UITable/UIFigure automatically, etc Add tabs - They work in the most basic example, but they ain't working with tables: ``` from deephaven import ui @ui.component def tab_test(): return ui.panel( ui.tabs( ui.tab_list( ui.item('Hello'), ui.item('World') ), ui.tab_panels( ui.item('Foo'), ui.item('Bar') ) ) ) tt = tab_test() ``` Clean up how objects are implicitly rendered - Add a ui.object_view to explicitly wrap an exported object in an ObjectView - ExportedObjects that are passed as children of an element are implicitly converted to an ObjectView when decoding the JSON in the WidgetHandler - Enforce no mixed panel/non-panels for the document root, as well as implicitly wrapping the root array in a panel if necessary Clean up all tests affected - They all pass again Clean up tables a bit - They now appear correctly in the Tabs component Fix issue where opening a widget twice wouldn't re-open it correctly - Wasn't memoizing the onOpen/onClose callbacks correctly in DocumentHandler Add a spec for ui.fragment and implementation - Also provided an example for it Clean up example a bit Some fixes after Don reviewing - Automatically map "string" that is set as a children in a spectrum element to a `Text` element. Makes padding more correct for buttons/tabs and things - Provide an example for Tabs - Handle object lifecycle in WidgetHandler - This will conflict with my other PR, and the two need to merge - Also need to handle how widget plugins handle the exported object. Not all objects can be copied (only Table can be), and we may also need to fetch an object twice if it's re-used. --- plugins/ui/DESIGN.md | 67 +++++++------- plugins/ui/examples/README.md | 77 ++++++++++++---- .../src/deephaven/ui/components/__init__.py | 8 ++ .../src/deephaven/ui/components/fragment.py | 15 +++ .../deephaven/ui/components/object_view.py | 15 +++ .../deephaven/ui/components/spectrum/basic.py | 32 +++++++ .../ui/src/js/src/DocumentHandler.test.tsx | 42 +++++---- plugins/ui/src/js/src/DocumentHandler.tsx | 91 +++++-------------- plugins/ui/src/js/src/DocumentUtils.tsx | 50 ++++++++++ plugins/ui/src/js/src/ElementUtils.ts | 43 ++++++++- plugins/ui/src/js/src/ElementView.tsx | 68 -------------- plugins/ui/src/js/src/HTMLElementUtils.ts | 4 +- plugins/ui/src/js/src/HTMLElementView.tsx | 7 +- plugins/ui/src/js/src/IconElementUtils.ts | 4 +- plugins/ui/src/js/src/IconElementView.tsx | 7 +- plugins/ui/src/js/src/ObjectUtils.ts | 25 +++++ plugins/ui/src/js/src/ObjectView.tsx | 7 +- plugins/ui/src/js/src/PanelUtils.ts | 13 +-- plugins/ui/src/js/src/ReactPanel.test.tsx | 30 +++--- plugins/ui/src/js/src/ReactPanel.tsx | 40 +++----- plugins/ui/src/js/src/ReactPanelManager.ts | 26 ++++++ plugins/ui/src/js/src/SpectrumElementUtils.ts | 12 ++- plugins/ui/src/js/src/SpectrumElementView.tsx | 8 +- plugins/ui/src/js/src/TableObject.tsx | 47 ---------- plugins/ui/src/js/src/UITable.tsx | 51 ++++++----- plugins/ui/src/js/src/UITableUtils.tsx | 23 ++--- plugins/ui/src/js/src/WidgetHandler.test.tsx | 4 +- plugins/ui/src/js/src/WidgetHandler.tsx | 31 +++++-- plugins/ui/src/js/src/WidgetUtils.tsx | 63 +++++++++++++ .../ui/src/js/src/spectrum/ActionButton.tsx | 5 +- .../js/src/spectrum/mapSpectrumChildren.tsx | 20 ++++ .../src/js/src/spectrum/mapSpectrumProps.ts | 18 ++++ plugins/ui/src/js/src/styles.scss | 8 ++ 33 files changed, 583 insertions(+), 378 deletions(-) create mode 100644 plugins/ui/src/deephaven/ui/components/fragment.py create mode 100644 plugins/ui/src/deephaven/ui/components/object_view.py create mode 100644 plugins/ui/src/js/src/DocumentUtils.tsx delete mode 100644 plugins/ui/src/js/src/ElementView.tsx create mode 100644 plugins/ui/src/js/src/ObjectUtils.ts create mode 100644 plugins/ui/src/js/src/ReactPanelManager.ts delete mode 100644 plugins/ui/src/js/src/TableObject.tsx create mode 100644 plugins/ui/src/js/src/WidgetUtils.tsx create mode 100644 plugins/ui/src/js/src/spectrum/mapSpectrumChildren.tsx create mode 100644 plugins/ui/src/js/src/spectrum/mapSpectrumProps.ts diff --git a/plugins/ui/DESIGN.md b/plugins/ui/DESIGN.md index 477718abe..6b1020cd6 100644 --- a/plugins/ui/DESIGN.md +++ b/plugins/ui/DESIGN.md @@ -1405,6 +1405,15 @@ ui_table.sort( | `by` | `str \| Sequence[str]` | The column(s) to sort by. May be a single column name, or a list of column names. | | `direction` | `SortDirection \| Sequence[SortDirection] \| None` | The sort direction(s) to use. If provided, that must match up with the columns provided. Defaults to "ASC". | +#### ui.fragment + +A fragment maps to a [React.Fragment](https://react.dev/reference/react/Fragment). This lets you group elements without using a wrapper node. It only takes children, and does not take any additional props. + +```py +import deephaven.ui as ui +ui_fragment = ui.fragment(*children: Element) -> Element +``` + #### Deprecations The functionality provided my `ui.table` replaces some of the existing functions on `Table`. Below are the functions that are planned for deprecation/deletion of the `Table` interface, and their replacements with the new `ui.table` interface. @@ -1450,7 +1459,7 @@ use_table_listener( Capture the data in a table. If the table is still loading, a sentinel value will be returned. Data should already be filtered to the desired rows and columns before passing to this hook as it is best to filter before data is retrieved. -Use functions such as [head](https://deephaven.io/core/docs/reference/table-operations/filter/head/) or [slice](https://deephaven.io/core/docs/reference/table-operations/filter/slice/) to retrieve specific rows and functions such +Use functions such as [head](https://deephaven.io/core/docs/reference/table-operations/filter/head/) or [slice](https://deephaven.io/core/docs/reference/table-operations/filter/slice/) to retrieve specific rows and functions such as [select or view](https://deephaven.io/core/docs/how-to-guides/use-select-view-update/) to retrieve specific columns. ###### Syntax @@ -1464,17 +1473,16 @@ use_table_data( ###### Parameters -| Parameter | Type | Description | -|--------------------|--------------------------------------|------------------------------------------------------------------------------| -| `table` | `Table` | The table to retrieve data from. | -| `sentinel` | `Sentinel` | A sentinel value to return if the viewport is still loading. Default `None`. | - +| Parameter | Type | Description | +| ---------- | ---------- | ---------------------------------------------------------------------------- | +| `table` | `Table` | The table to retrieve data from. | +| `sentinel` | `Sentinel` | A sentinel value to return if the viewport is still loading. Default `None`. | ##### use_column_data Capture the data in a column. If the table is still loading, a sentinel value will be returned. Data should already be filtered to desired rows and a specific column before passing to this hook as it is best to filter before data is retrieved and this hook will only return data for the first column. -Use functions such as [head](https://deephaven.io/core/docs/reference/table-operations/filter/head/) or [slice](https://deephaven.io/core/docs/reference/table-operations/filter/slice/) to retrieve specific rows and functions such +Use functions such as [head](https://deephaven.io/core/docs/reference/table-operations/filter/head/) or [slice](https://deephaven.io/core/docs/reference/table-operations/filter/slice/) to retrieve specific rows and functions such as [select or view](https://deephaven.io/core/docs/how-to-guides/use-select-view-update/) to retrieve a specific column. ###### Syntax @@ -1488,17 +1496,16 @@ use_column_data( ###### Parameters -| Parameter | Type | Description | -|-------------------|-----------------|----------------------------------------------------------------------------| -| `table` | `Table` | The table to create a viewport on. | -| `sentinel` | `Sentinel` | A sentinel value to return if the column is still loading. Default `None`. | - +| Parameter | Type | Description | +| ---------- | ---------- | -------------------------------------------------------------------------- | +| `table` | `Table` | The table to create a viewport on. | +| `sentinel` | `Sentinel` | A sentinel value to return if the column is still loading. Default `None`. | ##### use_row_data Capture the data in a row. If the table is still loading, a sentinel value will be returned. Data should already be filtered to a single row and desired columns before passing to this hook as it is best to filter before data is retrieved and this hook will only return data for the first row. -Use functions such as [head](https://deephaven.io/core/docs/reference/table-operations/filter/head/) or [slice](https://deephaven.io/core/docs/reference/table-operations/filter/slice/) to retrieve a specific row and functions such +Use functions such as [head](https://deephaven.io/core/docs/reference/table-operations/filter/head/) or [slice](https://deephaven.io/core/docs/reference/table-operations/filter/slice/) to retrieve a specific row and functions such as [select or view](https://deephaven.io/core/docs/how-to-guides/use-select-view-update/) to retrieve specific columns. ###### Syntax @@ -1512,17 +1519,16 @@ use_row_data( ###### Parameters -| Parameter | Type | Description | -|------------|--------------------------------------|----------------------------------------------------------------------------------| -| `table` | `Table` | The table to create a viewport on. | -| `sentinel` | `Sentinel` | A sentinel value to return if the row is still loading. Default `None`. | - +| Parameter | Type | Description | +| ---------- | ---------- | ----------------------------------------------------------------------- | +| `table` | `Table` | The table to create a viewport on. | +| `sentinel` | `Sentinel` | A sentinel value to return if the row is still loading. Default `None`. | ##### use_row_list Capture the data in a row. If the table is still loading, a sentinel value will be returned. This function is identical to `use_row_data` except that it always returns a list of data instead of a `RowData` object for convenience. Data should already be filtered to a single row and desired columns before passing to this hook as it is best to filter before data is retrieved and this hook will only return data for the first row. -Use functions such as [head](https://deephaven.io/core/docs/reference/table-operations/filter/head/) or [slice](https://deephaven.io/core/docs/reference/table-operations/filter/slice/) to retrieve a specific row and functions such +Use functions such as [head](https://deephaven.io/core/docs/reference/table-operations/filter/head/) or [slice](https://deephaven.io/core/docs/reference/table-operations/filter/slice/) to retrieve a specific row and functions such as [select or view](https://deephaven.io/core/docs/how-to-guides/use-select-view-update/) to retrieve specific columns. ###### Syntax @@ -1536,18 +1542,18 @@ use_row_list( ###### Parameters -| Parameter | Type | Description | -|------------|--------------------------------------|----------------------------------------------------------------------------------| -| `table` | `Table` | The table to create a viewport on. | -| `sentinel` | `Sentinel` | A sentinel value to return if the row is still loading. Default `None`. | - +| Parameter | Type | Description | +| ---------- | ---------- | ----------------------------------------------------------------------- | +| `table` | `Table` | The table to create a viewport on. | +| `sentinel` | `Sentinel` | A sentinel value to return if the row is still loading. Default `None`. | ##### use_cell_data Capture the data in a cell. If the table is still loading, a sentinel value will be returned. Data should already be filtered to a single row and column before passing to this hook as it is best to filter before data is retrieved and this hook will only return data for the first cell. -Use functions such as [head](https://deephaven.io/core/docs/reference/table-operations/filter/head/) or [slice](https://deephaven.io/core/docs/reference/table-operations/filter/slice/) to retrieve a specific row and functions such +Use functions such as [head](https://deephaven.io/core/docs/reference/table-operations/filter/head/) or [slice](https://deephaven.io/core/docs/reference/table-operations/filter/slice/) to retrieve a specific row and functions such as [select or view](#https://deephaven.io/core/docs/how-to-guides/use-select-view-update/) to retrieve a specific column. + ```py use_cell_data( table: Table, @@ -1557,11 +1563,10 @@ use_cell_data( ###### Parameters -| Parameter | Type | Description | -|-------------|--------------------------------------|--------------------------------------------------------------------------| -| `table` | `Table` | The table to create a viewport on. | -| `sentinel` | `Sentinel` | A sentinel value to return if the cell is still loading. Default `None`. | - +| Parameter | Type | Description | +| ---------- | ---------- | ------------------------------------------------------------------------ | +| `table` | `Table` | The table to create a viewport on. | +| `sentinel` | `Sentinel` | A sentinel value to return if the cell is still loading. Default `None`. | #### Custom Types @@ -1635,8 +1640,6 @@ class LinkPoint(TypedDict): ``` - - #### Context By default, the context of a `@ui.component` will be created per client session (same as [Parameterized Query's "parallel universe" today](https://github.com/deephaven-ent/iris/blob/868b868fc9e180ee948137b10b6addbac043605e/ParameterizedQuery/src/main/java/io/deephaven/query/parameterized/impl/ParameterizedQueryServerImpl.java#L140)). However, it would be interesting if it were possible to share a context among all sessions for the current user, and/or share a context with other users even; e.g. if one user selects and applies a filter, it updates immediately for all other users with that dashboard open. So three cases: diff --git a/plugins/ui/examples/README.md b/plugins/ui/examples/README.md index 57245b63e..d9663ee04 100644 --- a/plugins/ui/examples/README.md +++ b/plugins/ui/examples/README.md @@ -570,7 +570,7 @@ dt = double_table(stocks) ## Stock rollup -You can use the `rollup` method to create a rollup table. In this example, we create a rollup table that shows the average price of each stock and/or exchange. You can toggle the rollup by clicking on the [ToggleButton](https://react-spectrum.adobe.com/react-spectrum/ToggleButton.html). You can also highlight a specific stock by entering the symbol in the text field. +You can use the `rollup` method to create a rollup table. In this example, we create a rollup table that shows the average price of each stock and/or exchange. You can toggle the rollup by clicking on the [ToggleButton](https://react-spectrum.adobe.com/react-spectrum/ToggleButton.html). You can also highlight a specific stock by entering the symbol in the text field, but only when a rollup option isn't selected. We wrap the highlight input field with a `ui.fragment` that is conditionally used so that it doesn't appear when the rollup is selected. We also use the `ui.contextual_help` component to display a help message when you hover over the help icon. ```python import deephaven.ui as ui @@ -584,11 +584,10 @@ stocks = dx.data.stocks() def get_by_filter(**byargs): """ Gets a by filter where the arguments are all args passed in where the value is true. - - Examples: - get_by_filter(sym=True, exchange=False) == ["sym"] - get_by_filter(exchange=False) == [] - get_by_filter(sym=True, exchange=True) == ["sym", "exchange"] + e.g. + get_by_filter(sym=True, exchange=False) == ["sym"] + get_by_filter(exchange=False) == [] + get_by_filter(sym=True, exchange=True) == ["sym", "exchange"] """ return [k for k in byargs if byargs[k]] @@ -608,10 +607,7 @@ def stock_table(source): [source, highlight], ) rolled_table = use_memo( - lambda: formatted_table - if len(by) == 0 - else formatted_table.rollup(aggs=aggs, by=by), - [formatted_table, aggs, by], + lambda: t if len(by) == 0 else t.rollup(aggs=aggs, by=by), [t, aggs, by] ) return ui.flex( @@ -620,16 +616,20 @@ def stock_table(source): ui.toggle_button( ui.icon("vsBell"), "By Exchange", on_change=set_is_exchange ), - ui.text_field( - label="Highlight Sym", - label_position="side", - value=highlight, - on_change=set_highlight, - ), - ui.contextual_help( - ui.heading("Highlight Sym"), - ui.content("Enter a sym you would like highlighted."), - ), + ui.fragment( + ui.text_field( + label="Highlight Sym", + label_position="side", + value=highlight, + on_change=set_highlight, + ), + ui.contextual_help( + ui.heading("Highlight Sym"), + ui.content("Enter a sym you would like highlighted."), + ), + ) + if not is_sym and not is_exchange + else None, align_items="center", gap="size-100", margin="size-100", @@ -711,3 +711,40 @@ monitor = monitor_changed_data(t) ``` ![Stock Rollup](assets/change_monitor.png) + +## Tabs + +You can add [Tabs](https://react-spectrum.adobe.com/react-spectrum/Tabs.html) within a panel by using the `ui.tabs` method. In this example, we create a tabbed panel with multiple tabs: + +- Unfiltered table +- Table filtered on sym `CAT`. We also include an icon in the tab header. +- Table filtered on sym `DOG` + +```python +from deephaven import ui +from deephaven.plot import express as dx + +stocks = dx.data.stocks() + + +@ui.component +def table_tabs(source): + return ui.panel( + ui.tabs( + ui.tab_list( + ui.item("Unfiltered", key="Unfiltered"), + ui.item(ui.icon("vsGithubAlt"), "CAT", key="CAT"), + ui.item("DOG", key="DOG"), + ), + ui.tab_panels( + ui.item(source, key="Unfiltered"), + ui.item(source.where("sym=`CAT`"), key="CAT"), + ui.item(source.where("sym=`DOG`"), key="DOG"), + ), + flex_grow=1, + ) + ) + + +tt = table_tabs(stocks) +``` diff --git a/plugins/ui/src/deephaven/ui/components/__init__.py b/plugins/ui/src/deephaven/ui/components/__init__.py index 9a4298174..49763bee0 100644 --- a/plugins/ui/src/deephaven/ui/components/__init__.py +++ b/plugins/ui/src/deephaven/ui/components/__init__.py @@ -1,5 +1,7 @@ from .icon import icon from .make_component import make_component as component +from .fragment import fragment +from .object_view import object_view from .panel import panel from .spectrum import * from .table import table @@ -16,6 +18,7 @@ "contextual_help", "flex", "form", + "fragment", "grid", "heading", "icon", @@ -23,12 +26,17 @@ "illustrated_message", "html", "number_field", + "item", + "object_view", "panel", "range_slider", "slider", "spectrum_element", "switch", "table", + "tab_list", + "tab_panels", + "tabs", "text", "text_field", "toggle_button", diff --git a/plugins/ui/src/deephaven/ui/components/fragment.py b/plugins/ui/src/deephaven/ui/components/fragment.py new file mode 100644 index 000000000..0f5247ddc --- /dev/null +++ b/plugins/ui/src/deephaven/ui/components/fragment.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from typing import Any +from ..elements import BaseElement + + +def fragment(*children: Any): + """ + A React.Fragment: https://react.dev/reference/react/Fragment. + Used to group elements together without a wrapper node. + + Args: + children: The children in the fragment. + """ + return BaseElement("deephaven.ui.components.Fragment", children=children) diff --git a/plugins/ui/src/deephaven/ui/components/object_view.py b/plugins/ui/src/deephaven/ui/components/object_view.py new file mode 100644 index 000000000..ad9e6cb9f --- /dev/null +++ b/plugins/ui/src/deephaven/ui/components/object_view.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from typing import Any +from ..elements import BaseElement + + +def object_view(obj: Any): + """ + A wrapper for an exported object that can be rendered as a view. + E.g. A Table will be rendered as a Grid view. + + Args: + obj: The object to display + """ + return BaseElement("deephaven.ui.components.Object", object=obj) diff --git a/plugins/ui/src/deephaven/ui/components/spectrum/basic.py b/plugins/ui/src/deephaven/ui/components/spectrum/basic.py index 39a3aaf48..2a7d4a20d 100644 --- a/plugins/ui/src/deephaven/ui/components/spectrum/basic.py +++ b/plugins/ui/src/deephaven/ui/components/spectrum/basic.py @@ -83,6 +83,14 @@ def icon_wrapper(*children, **props): return spectrum_element("Icon", *children, **props) +def item(*children, **props): + """ + Python implementation for the Adobe React Spectrum Item component. + Used with Tabs: https://react-spectrum.adobe.com/react-spectrum/Tabs.html + """ + return spectrum_element("Item", *children, **props) + + def illustrated_message(*children, **props): """ Python implementation for the Adobe React Spectrum IllustratedMessage component. @@ -131,6 +139,30 @@ def switch(*children, **props): return spectrum_element("Switch", *children, **props) +def tabs(*children, **props): + """ + Python implementation for the Adobe React Spectrum Tabs component. + https://react-spectrum.adobe.com/react-spectrum/Tabs.html + """ + return spectrum_element("Tabs", *children, **props) + + +def tab_list(*children, **props): + """ + Python implementation for the Adobe React Spectrum TabList component. + https://react-spectrum.adobe.com/react-spectrum/Tabs.html + """ + return spectrum_element("TabList", *children, **props) + + +def tab_panels(*children, **props): + """ + Python implementation for the Adobe React Spectrum TabPanels component. + https://react-spectrum.adobe.com/react-spectrum/Tabs.html + """ + return spectrum_element("TabPanels", *children, **props) + + def text(*children, **props): """ Python implementation for the Adobe React Spectrum Text component. diff --git a/plugins/ui/src/js/src/DocumentHandler.test.tsx b/plugins/ui/src/js/src/DocumentHandler.test.tsx index 6c8712055..758536030 100644 --- a/plugins/ui/src/js/src/DocumentHandler.test.tsx +++ b/plugins/ui/src/js/src/DocumentHandler.test.tsx @@ -3,10 +3,9 @@ import { WidgetDefinition } from '@deephaven/dashboard'; import { TestUtils } from '@deephaven/utils'; import { render } from '@testing-library/react'; import DocumentHandler, { DocumentHandlerProps } from './DocumentHandler'; -import { ElementNode } from './ElementUtils'; -import { PANEL_ELEMENT_NAME } from './PanelUtils'; -import { ReactPanelProps } from './ReactPanel'; +import { PANEL_ELEMENT_NAME, ReactPanelProps } from './PanelUtils'; import { MixedPanelsError, NoChildrenError } from './errors'; +import { getComponentForElement } from './WidgetUtils'; const mockReactPanel = jest.fn((props: ReactPanelProps) => (
ReactPanel
@@ -19,27 +18,32 @@ jest.mock( function makeElement( type = 'test-element', props: Record = {} -): ElementNode { - return { +): React.ReactNode { + return getComponentForElement({ __dhElemName: type, props, - }; + }); } -function makeDocument(children: ElementNode[] = []): ElementNode { - return { +function makeDocument(children: React.ReactNode = []): React.ReactNode { + return getComponentForElement({ __dhElemName: 'test-element', props: { children, }, - }; + }); } function makeDocumentHandler({ - element = makeDocument(), + children = makeDocument(), definition = TestUtils.createMockProxy({}), + onClose = jest.fn(), }: Partial = {}) { - return ; + return ( + + {children} + + ); } beforeEach(() => { @@ -47,33 +51,33 @@ beforeEach(() => { }); it('should throw an error if no children to render', () => { - const element = makeDocument(); - expect(() => render(makeDocumentHandler({ element }))).toThrow( + const children = makeDocument(); + expect(() => render(makeDocumentHandler({ children }))).toThrow( NoChildrenError ); }); it('should throw an error if the document mixes panel and non-panel elements', () => { - const element = makeDocument([ + const children = makeDocument([ makeElement(PANEL_ELEMENT_NAME), makeElement('not panel element'), ]); - expect(() => render(makeDocumentHandler({ element }))).toThrow( + expect(() => render(makeDocumentHandler({ children }))).toThrow( MixedPanelsError ); }); it('should combine multiple single elements into one panel', () => { - const element = makeDocument([makeElement('foo'), makeElement('bar')]); - render(makeDocumentHandler({ element })); + const children = makeDocument([makeElement('foo'), makeElement('bar')]); + render(makeDocumentHandler({ children })); expect(mockReactPanel).toHaveBeenCalledTimes(1); }); it('should render multiple panels', () => { - const element = makeDocument([ + const children = makeDocument([ makeElement(PANEL_ELEMENT_NAME), makeElement(PANEL_ELEMENT_NAME), ]); - render(makeDocumentHandler({ element })); + render(makeDocumentHandler({ children })); expect(mockReactPanel).toHaveBeenCalledTimes(2); }); diff --git a/plugins/ui/src/js/src/DocumentHandler.tsx b/plugins/ui/src/js/src/DocumentHandler.tsx index ec466a8fb..654cbd39f 100644 --- a/plugins/ui/src/js/src/DocumentHandler.tsx +++ b/plugins/ui/src/js/src/DocumentHandler.tsx @@ -1,25 +1,18 @@ import React, { useCallback, useMemo, useRef } from 'react'; import { WidgetDefinition } from '@deephaven/dashboard'; import Log from '@deephaven/log'; -import { ElementNode, getElementKey } from './ElementUtils'; - -import ReactPanel from './ReactPanel'; -import ElementView from './ElementView'; -import { isPanelElementNode } from './PanelUtils'; -import { MixedPanelsError, NoChildrenError } from './errors'; +import { ReactPanelManagerContext } from './ReactPanelManager'; +import { getRootChildren } from './DocumentUtils'; const log = Log.module('@deephaven/js-plugin-ui/DocumentHandler'); -export interface DocumentHandlerProps { +export type DocumentHandlerProps = React.PropsWithChildren<{ /** Definition of the widget used to create this document. Used for titling panels if necessary. */ definition: WidgetDefinition; - /** The root element to render */ - element: ElementNode; - /** Triggered when all panels opened from this document have closed */ onClose?: () => void; -} +}>; /** * Handles rendering a document for one widget. @@ -28,19 +21,27 @@ export interface DocumentHandlerProps { * Responsible for opening any panels or dashboards specified in the document. */ function DocumentHandler({ + children, definition, - element, onClose, }: DocumentHandlerProps) { - log.debug('Rendering document', element); + log.debug('Rendering document', definition); const panelOpenCountRef = useRef(0); - const handlePanelOpen = useCallback(() => { + const metadata = useMemo( + () => ({ + name: definition.title ?? definition.name ?? 'Unknown', + type: definition.type, + }), + [definition] + ); + + const handleOpen = useCallback(() => { panelOpenCountRef.current += 1; log.debug('Panel opened, open count', panelOpenCountRef.current); }, []); - const handlePanelClose = useCallback(() => { + const handleClose = useCallback(() => { panelOpenCountRef.current -= 1; if (panelOpenCountRef.current < 0) { throw new Error('Panel open count is negative'); @@ -51,63 +52,19 @@ function DocumentHandler({ } }, [onClose]); - const metadata = useMemo( + const panelManager = useMemo( () => ({ - name: definition.title ?? definition.name, - type: definition.type, + metadata, + onOpen: handleOpen, + onClose: handleClose, }), - [definition] - ); - const { children } = element.props ?? {}; - const childrenArray = Array.isArray(children) ? children : [children]; - const childPanelCount = childrenArray.reduce( - (count, child) => count + (isPanelElementNode(child) ? 1 : 0), - 0 + [metadata, handleClose, handleOpen] ); - if (childrenArray.length === 0) { - throw new NoChildrenError('No children to render'); - } - if (childPanelCount !== 0 && childPanelCount !== childrenArray.length) { - throw new MixedPanelsError('Cannot mix panel and non-panel elements'); - } - if (childPanelCount === 0) { - // No panels, just add the root element to one panel are render it - return ( - - - - ); - } return ( - <> - {childrenArray.map((child, i) => { - const key = getElementKey(child, `${i}`); - let title = `${definition.title ?? definition.id ?? definition.type}`; - if (childrenArray.length > 1) { - title = `${title} ${i + 1}`; - } - if (isPanelElementNode(child)) { - title = child.props.title ?? title; - } - return ( - - - - ); - })} - + + {getRootChildren(children, definition)} + ); } diff --git a/plugins/ui/src/js/src/DocumentUtils.tsx b/plugins/ui/src/js/src/DocumentUtils.tsx new file mode 100644 index 000000000..2cb43e22e --- /dev/null +++ b/plugins/ui/src/js/src/DocumentUtils.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { WidgetDefinition } from '@deephaven/dashboard'; +import ReactPanel from './ReactPanel'; +import { MixedPanelsError, NoChildrenError } from './errors'; + +/** + * Convert the children passed as the Document root to a valid root node, or throw if it's an invalid root configuration. + * For example, you cannot mix a Panel with another type of element. In that case, it will throw. + * If the root does not have a Panel or Dashboard layout specified, it will automatically wrap the children in a panel. + * + * + * @param children Root children of the document. + */ +export function getRootChildren( + children: React.ReactNode, + definition: WidgetDefinition +): React.ReactNode { + if (children == null) { + return null; + } + + const childrenArray = Array.isArray(children) ? children : [children]; + const childPanelCount = childrenArray.filter( + child => child?.type === ReactPanel + ).length; + + if (childrenArray.length === 0) { + throw new NoChildrenError('No children to render'); + } + if (childPanelCount !== 0 && childPanelCount !== childrenArray.length) { + throw new MixedPanelsError('Cannot mix panel and non-panel elements'); + } + + if (childPanelCount === 0) { + // Just wrap it in a panel + return ( + + {children} + + ); + } + + // It's already got panels defined, just return it + return children; +} + +export default { getRootChildren }; diff --git a/plugins/ui/src/js/src/ElementUtils.ts b/plugins/ui/src/js/src/ElementUtils.ts index 01d69bfff..7b7322a80 100644 --- a/plugins/ui/src/js/src/ElementUtils.ts +++ b/plugins/ui/src/js/src/ElementUtils.ts @@ -1,4 +1,5 @@ import type { WidgetExportedObject } from '@deephaven/jsapi-types'; +import { ReactNode } from 'react'; export const CALLABLE_KEY = '__dhCbid'; export const OBJECT_KEY = '__dhObid'; @@ -19,13 +20,25 @@ export type ObjectNode = { * Extend this type with stricter rules on the element key type to provide types. * See `SpectrumElementNode` for an example. */ -export type ElementNode = { +export type ElementNode< + K extends string = string, + P extends Record = Record +> = { /** * The type of this element. Can be something like `deephaven.ui.components.Panel`, or * a custom component type defined by the user in their plugin. */ - [ELEMENT_KEY]: string; - props?: Record; + [ELEMENT_KEY]: K; + props?: P; +}; + +export type ElementNodeWithChildren< + K extends string = string, + P extends Record = Record +> = ElementNode & { + props: P & { + children: ReactNode; + }; }; export function isObjectNode(obj: unknown): obj is ObjectNode { @@ -64,3 +77,27 @@ export function getElementKey(node: unknown, defaultKey: string): string { } return `${node.props?.key}`; } + +export const FRAGMENT_ELEMENT_NAME = 'deephaven.ui.components.Fragment'; + +export type FragmentElementType = typeof FRAGMENT_ELEMENT_NAME; + +/** + * Describes a fragment element that can be rendered in the UI. + * Will be placed in the current dashboard, or within a user created dashboard if specified. + */ +export type FragmentElementNode = ElementNode; + +/** + * Check if an object is a FragmentElementNode + * @param obj Object to check + * @returns True if the object is a FragmentElementNode + */ +export function isFragmentElementNode( + obj: unknown +): obj is FragmentElementNode { + return ( + isElementNode(obj) && + (obj as ElementNode)[ELEMENT_KEY] === FRAGMENT_ELEMENT_NAME + ); +} diff --git a/plugins/ui/src/js/src/ElementView.tsx b/plugins/ui/src/js/src/ElementView.tsx deleted file mode 100644 index 3552b50d4..000000000 --- a/plugins/ui/src/js/src/ElementView.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; -import { getElementKey, isElementNode, isExportedObject } from './ElementUtils'; -import { isHTMLElementNode } from './HTMLElementUtils'; -import HTMLElementView from './HTMLElementView'; -import { isSpectrumElementNode } from './SpectrumElementUtils'; -import SpectrumElementView from './SpectrumElementView'; -import { isIconElementNode } from './IconElementUtils'; -import IconElementView from './IconElementView'; -import ObjectView from './ObjectView'; -import { isUITable } from './UITableUtils'; -import UITable from './UITable'; - -export type ElementViewProps = { - /** The element to render. */ - element: unknown; -}; - -/** - * Take an object from within a document and attempt to render it. - * If it's an `ElementNode`, then render it as an element and any special handling that may require. - * If it's an `ExportedObject`, then render it as an object, and/or let a plugin handle it. - */ -export function ElementView({ element }: ElementViewProps): JSX.Element | null { - if (element == null) { - return null; - } - if (Array.isArray(element)) { - return ( - <> - {element.map((child, i) => ( - - ))} - - ); - } - if (isElementNode(element)) { - if (isHTMLElementNode(element)) { - return ; - } - if (isSpectrumElementNode(element)) { - return ; - } - if (isIconElementNode(element)) { - return ; - } - if (isUITable(element)) { - return ; - } - - // No special rendering for this node, just render the children - const { props } = element; - if (props == null) { - return null; - } - // eslint-disable-next-line react/prop-types - const { children } = props; - return ; - } - - if (isExportedObject(element)) { - return ; - } - - // Just try and return the element, assume it is renderable. If not, this will throw. - return element as JSX.Element; -} - -export default ElementView; diff --git a/plugins/ui/src/js/src/HTMLElementUtils.ts b/plugins/ui/src/js/src/HTMLElementUtils.ts index 17983ee99..e01124feb 100644 --- a/plugins/ui/src/js/src/HTMLElementUtils.ts +++ b/plugins/ui/src/js/src/HTMLElementUtils.ts @@ -12,9 +12,7 @@ export type HTMLElementType = * For example, `deephaven.ui.html.div` would be rendered as `
`. * The props are passed directly to the HTML element as attributes. */ -export type HTMLElementNode = ElementNode & { - [ELEMENT_KEY]: HTMLElementType; -}; +export type HTMLElementNode = ElementNode; export function isHTMLElementNode(obj: unknown): obj is HTMLElementNode { return ( diff --git a/plugins/ui/src/js/src/HTMLElementView.tsx b/plugins/ui/src/js/src/HTMLElementView.tsx index 31cdd1c0f..903e7ddd0 100644 --- a/plugins/ui/src/js/src/HTMLElementView.tsx +++ b/plugins/ui/src/js/src/HTMLElementView.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { HTMLElementNode, getHTMLTag } from './HTMLElementUtils'; import { ELEMENT_KEY } from './ElementUtils'; -import ElementView from './ElementView'; export type HTMLElementViewProps = { element: HTMLElementNode; @@ -17,11 +16,7 @@ export function HTMLElementView({ } // eslint-disable-next-line react/prop-types const { children, ...otherProps } = props; - return React.createElement( - tag, - otherProps, - - ); + return React.createElement(tag, otherProps, children); } export default HTMLElementView; diff --git a/plugins/ui/src/js/src/IconElementUtils.ts b/plugins/ui/src/js/src/IconElementUtils.ts index 557f0b9e2..9a6fc2980 100644 --- a/plugins/ui/src/js/src/IconElementUtils.ts +++ b/plugins/ui/src/js/src/IconElementUtils.ts @@ -12,9 +12,7 @@ export type IconElementName = * For example, `deephaven.ui.icons.vsBell` will render the icon named `vsBell`. * The props are passed directly to the icon component. */ -export type IconElementNode = ElementNode & { - [ELEMENT_KEY]: IconElementName; -}; +export type IconElementNode = ElementNode; export function isIconElementNode(obj: unknown): obj is IconElementNode { return ( diff --git a/plugins/ui/src/js/src/IconElementView.tsx b/plugins/ui/src/js/src/IconElementView.tsx index 10ff657eb..64edc1ce0 100644 --- a/plugins/ui/src/js/src/IconElementView.tsx +++ b/plugins/ui/src/js/src/IconElementView.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { Icon } from '@adobe/react-spectrum'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { getIcon, IconElementNode } from './IconElementUtils'; import { ELEMENT_KEY } from './ElementUtils'; @@ -17,8 +18,10 @@ export function IconElementView({ } return ( - // eslint-disable-next-line react/jsx-props-no-spreading - + + {/* eslint-disable-next-line react/jsx-props-no-spreading */} + + ); } diff --git a/plugins/ui/src/js/src/ObjectUtils.ts b/plugins/ui/src/js/src/ObjectUtils.ts new file mode 100644 index 000000000..de21386af --- /dev/null +++ b/plugins/ui/src/js/src/ObjectUtils.ts @@ -0,0 +1,25 @@ +import type { WidgetExportedObject } from '@deephaven/jsapi-types'; +import { ELEMENT_KEY, ElementNode, isElementNode } from './ElementUtils'; + +export const OBJECT_ELEMENT_NAME = 'deephaven.ui.components.Object'; + +export type ObjectElementType = typeof OBJECT_ELEMENT_NAME; + +export type ObjectViewProps = { object: WidgetExportedObject }; + +/** Describes an object that can be rendered in the UI. */ +export type ObjectElementNode = ElementNode & { + props: ObjectViewProps; +}; + +/** + * Check if an object is a ObjectElementNode + * @param obj Object to check + * @returns True if the object is a ObjectElementNode + */ +export function isObjectElementNode(obj: unknown): obj is ObjectElementNode { + return ( + isElementNode(obj) && + (obj as ElementNode)[ELEMENT_KEY] === OBJECT_ELEMENT_NAME + ); +} diff --git a/plugins/ui/src/js/src/ObjectView.tsx b/plugins/ui/src/js/src/ObjectView.tsx index 108d42d68..2e84fa650 100644 --- a/plugins/ui/src/js/src/ObjectView.tsx +++ b/plugins/ui/src/js/src/ObjectView.tsx @@ -1,14 +1,11 @@ import React, { useCallback, useMemo } from 'react'; import Log from '@deephaven/log'; import { isWidgetPlugin, usePlugins } from '@deephaven/plugin'; -import type { Widget, WidgetExportedObject } from '@deephaven/jsapi-types'; +import type { Widget } from '@deephaven/jsapi-types'; +import { ObjectViewProps } from './ObjectUtils'; const log = Log.module('@deephaven/js-plugin-ui/ObjectView'); -export interface ObjectViewProps { - object: WidgetExportedObject; -} - function ObjectView(props: ObjectViewProps) { const { object } = props; log.info('Object is', object); diff --git a/plugins/ui/src/js/src/PanelUtils.ts b/plugins/ui/src/js/src/PanelUtils.ts index 62cc7661e..3cfbf17b9 100644 --- a/plugins/ui/src/js/src/PanelUtils.ts +++ b/plugins/ui/src/js/src/PanelUtils.ts @@ -1,19 +1,20 @@ +import React from 'react'; import { ELEMENT_KEY, ElementNode, isElementNode } from './ElementUtils'; export const PANEL_ELEMENT_NAME = 'deephaven.ui.components.Panel'; export type PanelElementType = typeof PANEL_ELEMENT_NAME; +export type ReactPanelProps = React.PropsWithChildren<{ + /** Title of the panel */ + title?: string; +}>; + /** * Describes a panel element that can be rendered in the UI. * Will be placed in the current dashboard, or within a user created dashboard if specified. */ -export type PanelElementNode = ElementNode & { - [ELEMENT_KEY]: PanelElementType; - props: { - title?: string; - }; -}; +export type PanelElementNode = ElementNode; /** * Check if an object is a PanelElementNode diff --git a/plugins/ui/src/js/src/ReactPanel.test.tsx b/plugins/ui/src/js/src/ReactPanel.test.tsx index a65b0bfaf..0ec339558 100644 --- a/plugins/ui/src/js/src/ReactPanel.test.tsx +++ b/plugins/ui/src/js/src/ReactPanel.test.tsx @@ -1,7 +1,12 @@ import React from 'react'; import { render } from '@testing-library/react'; import { LayoutUtils, useListener } from '@deephaven/dashboard'; -import ReactPanel, { ReactPanelProps } from './ReactPanel'; +import ReactPanel from './ReactPanel'; +import { + ReactPanelManager, + ReactPanelManagerContext, +} from './ReactPanelManager'; +import { ReactPanelProps } from './PanelUtils'; // Mock LayoutUtils, useListener, and PanelEvent from @deephaven/dashboard package const mockLayout = { root: {}, eventHub: {} }; @@ -30,20 +35,21 @@ beforeEach(() => { function makeReactPanel({ children, - metadata, - onClose, - onOpen, + metadata = { name: 'test-name', type: 'test-type' }, + onClose = jest.fn(), + onOpen = jest.fn(), title = 'test title', -}: Partial = {}) { +}: Partial & Partial = {}) { return ( - - {children} - + {children} + ); } diff --git a/plugins/ui/src/js/src/ReactPanel.tsx b/plugins/ui/src/js/src/ReactPanel.tsx index 2ae2be53a..25a35d4a6 100644 --- a/plugins/ui/src/js/src/ReactPanel.tsx +++ b/plugins/ui/src/js/src/ReactPanel.tsx @@ -9,38 +9,18 @@ import { } from '@deephaven/dashboard'; import Log from '@deephaven/log'; import PortalPanel from './PortalPanel'; +import { useReactPanelManager } from './ReactPanelManager'; +import { ReactPanelProps } from './PanelUtils'; const log = Log.module('@deephaven/js-plugin-ui/ReactPanel'); -export type ReactPanelProps = React.PropsWithChildren<{ - /** Title of the panel */ - title: string; - - /** - * Metadata to pass to the panel. - * Updating the metadata will cause the panel to be re-opened, or replaced if it is closed. - * Can also be used for rehydration. - */ - metadata?: Record; - - /** Triggered when this panel is opened */ - onOpen?: () => void; - - /** Triggered when this panel is closed */ - onClose?: () => void; -}>; - /** * Adds and tracks a panel to the GoldenLayout. When the child element is updated, the contents of the panel will also be updated. When unmounted, the panel will be removed. */ -function ReactPanel({ - children, - metadata, - onClose, - onOpen, - title, -}: ReactPanelProps) { +function ReactPanel({ children, title }: ReactPanelProps) { const layoutManager = useLayoutManager(); + const panelManager = useReactPanelManager(); + const { metadata, onClose, onOpen } = panelManager; const panelId = useMemo(() => shortid(), []); const [element, setElement] = useState(); const isPanelOpenRef = useRef(false); @@ -54,7 +34,7 @@ function ReactPanel({ log.debug('Closing panel', panelId); LayoutUtils.closeComponent(layoutManager.root, { id: panelId }); isPanelOpenRef.current = false; - onClose?.(); + onClose(panelId); } }, [layoutManager, onClose, panelId] @@ -65,7 +45,7 @@ function ReactPanel({ if (closedPanelId === panelId) { log.debug('Panel closed', panelId); isPanelOpenRef.current = false; - onClose?.(); + onClose(panelId); } }, [onClose, panelId] @@ -78,6 +58,8 @@ function ReactPanel({ isPanelOpenRef.current === false || openedMetadataRef.current !== metadata ) { + const panelTitle = + title ?? (typeof metadata?.name === 'string' ? metadata.name : ''); const config = { type: 'react-component' as const, component: PortalPanel.displayName, @@ -90,7 +72,7 @@ function ReactPanel({ onOpen: setElement, metadata, }, - title, + title: panelTitle, id: panelId, }; @@ -100,7 +82,7 @@ function ReactPanel({ isPanelOpenRef.current = true; openedMetadataRef.current = metadata; - onOpen?.(); + onOpen(panelId); } }, [layoutManager, metadata, onOpen, panelId, title]); diff --git a/plugins/ui/src/js/src/ReactPanelManager.ts b/plugins/ui/src/js/src/ReactPanelManager.ts new file mode 100644 index 000000000..53db36f60 --- /dev/null +++ b/plugins/ui/src/js/src/ReactPanelManager.ts @@ -0,0 +1,26 @@ +import { createContext, useContext } from 'react'; + +export type ReactPanelManager = { + /** + * Metadata to pass to all the panels. + * Updating the metadata will cause the panel to be re-opened, or replaced if it is closed. + * Can also be used for rehydration. + */ + metadata: Record; + + /** Triggered when a panel is opened */ + onOpen: (panelId: string) => void; + + /** Triggered when a panel is closed */ + onClose: (panelId: string) => void; +}; + +export const ReactPanelManagerContext = createContext({ + metadata: { name: '', type: '' }, + onOpen: () => undefined, + onClose: () => undefined, +}); + +export function useReactPanelManager(): ReactPanelManager { + return useContext(ReactPanelManagerContext); +} diff --git a/plugins/ui/src/js/src/SpectrumElementUtils.ts b/plugins/ui/src/js/src/SpectrumElementUtils.ts index 7903e010a..a75078a38 100644 --- a/plugins/ui/src/js/src/SpectrumElementUtils.ts +++ b/plugins/ui/src/js/src/SpectrumElementUtils.ts @@ -7,9 +7,13 @@ import { Grid, Heading, Icon, + Item, IllustratedMessage, NumberField, Switch, + Tabs, + TabList, + TabPanels, Text, ToggleButton, View, @@ -41,9 +45,13 @@ export const SpectrumSupportedTypes = { Icon, IllustratedMessage, NumberField, + Item, RangeSlider, Slider, Switch, + Tabs, + TabList, + TabPanels, Text, TextField, ToggleButton, @@ -60,9 +68,7 @@ export type SpectrumElementName = * The props are passed directly to the Spectrum component. * @see SpectrumSupportedTypes */ -export type SpectrumElementNode = ElementNode & { - [ELEMENT_KEY]: SpectrumElementName; -}; +export type SpectrumElementNode = ElementNode; export function isSpectrumElementNode( obj: unknown diff --git a/plugins/ui/src/js/src/SpectrumElementView.tsx b/plugins/ui/src/js/src/SpectrumElementView.tsx index d8c97bc19..a83ef2daf 100644 --- a/plugins/ui/src/js/src/SpectrumElementView.tsx +++ b/plugins/ui/src/js/src/SpectrumElementView.tsx @@ -4,7 +4,7 @@ import { SpectrumElementNode, } from './SpectrumElementUtils'; import { ELEMENT_KEY } from './ElementUtils'; -import ElementView from './ElementView'; +import { mapSpectrumProps } from './spectrum/mapSpectrumProps'; export type SpectrumElementViewProps = { element: SpectrumElementNode; @@ -18,13 +18,9 @@ export function SpectrumElementView({ if (Component == null) { throw new Error(`Unknown Spectrum component ${name}`); } - // eslint-disable-next-line react/prop-types - const { children, ...otherProps } = props; return ( // eslint-disable-next-line react/jsx-props-no-spreading, @typescript-eslint/no-explicit-any - - - + ); } diff --git a/plugins/ui/src/js/src/TableObject.tsx b/plugins/ui/src/js/src/TableObject.tsx deleted file mode 100644 index 853a3b699..000000000 --- a/plugins/ui/src/js/src/TableObject.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { - IrisGrid, - IrisGridModel, - IrisGridModelFactory, - IrisGridProps, -} from '@deephaven/iris-grid'; -import { useApi } from '@deephaven/jsapi-bootstrap'; -import type { Table } from '@deephaven/jsapi-types'; -import { View } from '@adobe/react-spectrum'; - -export interface TableObjectProps { - /** Table object to render */ - object: Table; - - /** Props to add to the IrisGrid instance */ - irisGridProps?: Partial; -} - -/** - * Displays an IrisGrid for a Deephaven Table object. - */ -export function TableObject({ irisGridProps, object }: TableObjectProps) { - const dh = useApi(); - const [model, setModel] = useState(); - - useEffect(() => { - async function loadModel() { - const newModel = await IrisGridModelFactory.makeModel(dh, object); - setModel(newModel); - } - loadModel(); - }, [dh, object]); - - return ( - - {model && ( - // eslint-disable-next-line react/jsx-props-no-spreading - - )} - - ); -} - -TableObject.displayName = 'TableObject'; - -export default TableObject; diff --git a/plugins/ui/src/js/src/UITable.tsx b/plugins/ui/src/js/src/UITable.tsx index d1f971df5..fdbd852e7 100644 --- a/plugins/ui/src/js/src/UITable.tsx +++ b/plugins/ui/src/js/src/UITable.tsx @@ -1,47 +1,54 @@ import React, { useEffect, useMemo, useState } from 'react'; -import { IrisGridProps } from '@deephaven/iris-grid'; +import { + IrisGrid, + IrisGridModel, + IrisGridModelFactory, + IrisGridProps, +} from '@deephaven/iris-grid'; import { useApi } from '@deephaven/jsapi-bootstrap'; import type { Table } from '@deephaven/jsapi-types'; import Log from '@deephaven/log'; -import { UITableNode } from './UITableUtils'; -import TableObject from './TableObject'; +import { UITableProps } from './UITableUtils'; const log = Log.module('@deephaven/js-plugin-ui/UITable'); -export interface UITableProps { - element: UITableNode; -} - -function UITable({ element }: UITableProps) { +function UITable({ onRowDoublePress, table: exportedTable }: UITableProps) { const dh = useApi(); - const [table, setTable] = useState(); - const { props: elementProps } = element; + const [model, setModel] = useState(); // Just load the object on mount useEffect(() => { let isCancelled = false; async function loadModel() { - log.debug('Loading table from props', element.props); - const reexportedTable = await element.props.table.reexport(); + log.debug('Loading table from props', exportedTable); + const reexportedTable = await exportedTable.reexport(); const newTable = (await reexportedTable.fetch()) as Table; - if (isCancelled) { - newTable.close(); + const newModel = await IrisGridModelFactory.makeModel(dh, newTable); + if (!isCancelled) { + setModel(newModel); + } else { + newModel.close(); } - setTable(newTable); } loadModel(); return () => { isCancelled = true; }; - }, [dh, element]); + }, [dh, exportedTable]); + + const irisGridProps: Partial = useMemo( + () => ({ onDataSelected: onRowDoublePress }), + [onRowDoublePress] + ); - const irisGridProps: Partial = useMemo(() => { - const { onRowDoublePress } = elementProps; - return { onDataSelected: onRowDoublePress }; - }, [elementProps]); + // We want to clean up the model when we unmount or get a new model + useEffect(() => () => model?.close(), [model]); - return table ? ( - + return model ? ( +
+ {/* eslint-disable-next-line react/jsx-props-no-spreading */} + +
) : null; } diff --git a/plugins/ui/src/js/src/UITableUtils.tsx b/plugins/ui/src/js/src/UITableUtils.tsx index a7992b45a..9a10a1280 100644 --- a/plugins/ui/src/js/src/UITableUtils.tsx +++ b/plugins/ui/src/js/src/UITableUtils.tsx @@ -3,18 +3,19 @@ import { ELEMENT_KEY, ElementNode, isElementNode } from './ElementUtils'; export const UITABLE_ELEMENT_TYPE = 'deephaven.ui.elements.UITable'; -export type UITableElementName = `${typeof UITABLE_ELEMENT_TYPE}`; +export type UITableElementName = typeof UITABLE_ELEMENT_TYPE; -export type UITableNode = ElementNode & { - [ELEMENT_KEY]: UITableElementName; - props: { - table: WidgetExportedObject; - onRowDoublePress?: ( - rowIndex: number, - rowData: Record - ) => void; - [key: string]: unknown; - }; +export interface UITableProps { + table: WidgetExportedObject; + onRowDoublePress?: ( + rowIndex: number, + rowData: Record + ) => void; + [key: string]: unknown; +} + +export type UITableNode = ElementNode & { + props: UITableProps; }; export function isUITable(obj: unknown): obj is UITableNode { diff --git a/plugins/ui/src/js/src/WidgetHandler.test.tsx b/plugins/ui/src/js/src/WidgetHandler.test.tsx index 910e09287..81a52d48e 100644 --- a/plugins/ui/src/js/src/WidgetHandler.test.tsx +++ b/plugins/ui/src/js/src/WidgetHandler.test.tsx @@ -69,7 +69,7 @@ it('updates the document when event is received', async () => { expect(mockDocumentHandler).toHaveBeenCalledWith( expect.objectContaining({ definition, - element: initialDocument, + children: initialDocument, }) ); @@ -91,7 +91,7 @@ it('updates the document when event is received', async () => { expect(mockDocumentHandler).toHaveBeenCalledWith( expect.objectContaining({ definition, - element: updatedDocument, + children: updatedDocument, }) ); diff --git a/plugins/ui/src/js/src/WidgetHandler.tsx b/plugins/ui/src/js/src/WidgetHandler.tsx index 9f2df8dfb..72c42a0ec 100644 --- a/plugins/ui/src/js/src/WidgetHandler.tsx +++ b/plugins/ui/src/js/src/WidgetHandler.tsx @@ -2,6 +2,7 @@ * Handles document events for one widget. */ import React, { + ReactNode, useCallback, useEffect, useMemo, @@ -18,13 +19,14 @@ import type { Widget, WidgetExportedObject } from '@deephaven/jsapi-types'; import Log from '@deephaven/log'; import { CALLABLE_KEY, - ElementNode, OBJECT_KEY, isCallableNode, + isElementNode, isObjectNode, } from './ElementUtils'; import { WidgetMessageEvent, WidgetWrapper } from './WidgetTypes'; import DocumentHandler from './DocumentHandler'; +import { getComponentForElement } from './WidgetUtils'; const log = Log.module('@deephaven/js-plugin-ui/WidgetHandler'); @@ -40,7 +42,7 @@ function WidgetHandler({ onClose, widget: wrapper }: WidgetHandlerProps) { const dh = useApi(); const [widget, setWidget] = useState(); - const [element, setElement] = useState(); + const [document, setDocument] = useState(); // When we fetch a widget, the client is then responsible for the exported objects. // These objects could stay alive even after the widget is closed if we wanted to, @@ -66,10 +68,10 @@ function WidgetHandler({ onClose, widget: wrapper }: WidgetHandlerProps) { ); const parseDocument = useCallback( /** - * Parse the data from the server, replacing any callable nodes with functions that call the server. + * Parse the data from the server, replacing some of the nodes on the way. * Replaces all Callables with an async callback that will automatically call the server use JSON-RPC. * Replaces all Objects with the exported object from the server. - * Element nodes are not replaced. Those are handled in `DocumentHandler`. + * Replaces all Element nodes with the ReactNode derived from that Element. * * @param data The data to parse * @returns The parsed data @@ -101,6 +103,16 @@ function WidgetHandler({ onClose, widget: wrapper }: WidgetHandlerProps) { return exportedObject; } + if (isElementNode(value)) { + // Replace the elements node with the Component it maps to + try { + return getComponentForElement(value); + } catch (e) { + log.warn('Error getting component for element', e); + return value; + } + } + return value; }); @@ -146,7 +158,7 @@ function WidgetHandler({ onClose, widget: wrapper }: WidgetHandlerProps) { jsonClient.addMethod('documentUpdated', async (params: [string]) => { log.debug2('documentUpdated', params[0]); const newDocument = parseDocument(params[0]); - setElement(newDocument); + setDocument(newDocument); }); return () => { @@ -230,14 +242,15 @@ function WidgetHandler({ onClose, widget: wrapper }: WidgetHandlerProps) { return useMemo( () => - element ? ( + document != null ? ( + > + {document} + ) : null, - [element, handleDocumentClose, wrapper] + [document, handleDocumentClose, wrapper] ); } diff --git a/plugins/ui/src/js/src/WidgetUtils.tsx b/plugins/ui/src/js/src/WidgetUtils.tsx new file mode 100644 index 000000000..c0daba315 --- /dev/null +++ b/plugins/ui/src/js/src/WidgetUtils.tsx @@ -0,0 +1,63 @@ +/* eslint-disable react/jsx-props-no-spreading */ +/* eslint-disable import/prefer-default-export */ +import React from 'react'; +import { + ElementNode, + isExportedObject, + isFragmentElementNode, +} from './ElementUtils'; +import HTMLElementView from './HTMLElementView'; +import { isHTMLElementNode } from './HTMLElementUtils'; +import { isSpectrumElementNode } from './SpectrumElementUtils'; +import SpectrumElementView from './SpectrumElementView'; +import { isIconElementNode } from './IconElementUtils'; +import IconElementView from './IconElementView'; +import { isUITable } from './UITableUtils'; +import UITable from './UITable'; +import { isPanelElementNode } from './PanelUtils'; +import ReactPanel from './ReactPanel'; +import ObjectView from './ObjectView'; +import { isObjectElementNode } from './ObjectUtils'; + +export function getComponentForElement(element: ElementNode): React.ReactNode { + // Need to convert the children of the element if they are exported objects to an ObjectView + // Else React won't be able to render them + const newElement = { ...element }; + if (newElement.props?.children != null) { + const { children } = newElement.props; + if (Array.isArray(children)) { + newElement.props.children = children.map((child, i) => { + if (isExportedObject(child)) { + return ; + } + return child; + }); + } else if (isExportedObject(children)) { + newElement.props.children = ; + } + } + if (isHTMLElementNode(newElement)) { + return HTMLElementView({ element: newElement }); + } + if (isSpectrumElementNode(newElement)) { + return SpectrumElementView({ element: newElement }); + } + if (isIconElementNode(newElement)) { + return IconElementView({ element: newElement }); + } + if (isUITable(newElement)) { + return ; + } + if (isPanelElementNode(newElement)) { + return ; + } + if (isObjectElementNode(newElement)) { + return ; + } + if (isFragmentElementNode(newElement)) { + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{newElement.props?.children}; + } + + return newElement.props?.children; +} diff --git a/plugins/ui/src/js/src/spectrum/ActionButton.tsx b/plugins/ui/src/js/src/spectrum/ActionButton.tsx index a4343526a..702a721f9 100644 --- a/plugins/ui/src/js/src/spectrum/ActionButton.tsx +++ b/plugins/ui/src/js/src/spectrum/ActionButton.tsx @@ -3,6 +3,7 @@ import { ActionButton as SpectrumActionButton, SpectrumActionButtonProps, } from '@adobe/react-spectrum'; +import { mapSpectrumProps } from './mapSpectrumProps'; function ActionButton( props: SpectrumActionButtonProps & { onPress?: () => void } @@ -20,10 +21,8 @@ function ActionButton( return ( // eslint-disable-next-line react/jsx-props-no-spreading - + ); } -ActionButton.displayName = 'ActionButton'; - export default ActionButton; diff --git a/plugins/ui/src/js/src/spectrum/mapSpectrumChildren.tsx b/plugins/ui/src/js/src/spectrum/mapSpectrumChildren.tsx new file mode 100644 index 000000000..87540ee48 --- /dev/null +++ b/plugins/ui/src/js/src/spectrum/mapSpectrumChildren.tsx @@ -0,0 +1,20 @@ +import { Text } from '@adobe/react-spectrum'; +import React from 'react'; + +/** + * Map the children of an element to Spectrum children, automatically wrapping strings and numbers in `Text` elements. + * @param children Children to map as spectrum children + */ +export function mapSpectrumChildren( + children: React.ReactNode +): React.ReactNode { + const childrenArray = Array.isArray(children) ? children : [children]; + return childrenArray.map(child => { + if (typeof child === 'string') { + return {child}; + } + return child; + }); +} + +export default mapSpectrumChildren; diff --git a/plugins/ui/src/js/src/spectrum/mapSpectrumProps.ts b/plugins/ui/src/js/src/spectrum/mapSpectrumProps.ts new file mode 100644 index 000000000..729d02d0a --- /dev/null +++ b/plugins/ui/src/js/src/spectrum/mapSpectrumProps.ts @@ -0,0 +1,18 @@ +import { PropsWithChildren } from 'react'; +import mapSpectrumChildren from './mapSpectrumChildren'; + +/** + * Map the children of an element to Spectrum children, automatically wrapping strings and numbers in `Text` elements. + * @param children Children to map as spectrum children + */ +export function mapSpectrumProps< + T extends PropsWithChildren> +>(props: T): T { + return { + ...props, + children: + props?.children != null ? mapSpectrumChildren(props.children) : undefined, + }; +} + +export default mapSpectrumChildren; diff --git a/plugins/ui/src/js/src/styles.scss b/plugins/ui/src/js/src/styles.scss index c2d5f1a88..b8640ba07 100644 --- a/plugins/ui/src/js/src/styles.scss +++ b/plugins/ui/src/js/src/styles.scss @@ -6,3 +6,11 @@ overflow: hidden; position: relative; } + +.ui-object-container { + display: contents; + flex-grow: 1; + flex-shrink: 1; + overflow: hidden; + position: relative; +} From fc84ae71c054c9abba555a08e9eeb2dc25ef21b1 Mon Sep 17 00:00:00 2001 From: mikebender Date: Wed, 13 Dec 2023 16:39:25 -0500 Subject: [PATCH 2/5] Update DEMO.md to include tabs example --- docker/data/storage/notebooks/DEMO.md | 29 +++++++++++++++++++++++++++ plugins/ui/examples/README.md | 26 +++++++++++------------- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/docker/data/storage/notebooks/DEMO.md b/docker/data/storage/notebooks/DEMO.md index f4dcad1ad..21e758f4b 100644 --- a/docker/data/storage/notebooks/DEMO.md +++ b/docker/data/storage/notebooks/DEMO.md @@ -268,3 +268,32 @@ def order_table(): result = order_table() ``` + +## Using Tabs + +You can add [Tabs](https://react-spectrum.adobe.com/react-spectrum/Tabs.html) within a panel by using the `ui.tabs` method. In this example, we create a tabbed panel with multiple tabs: + +- Unfiltered table +- Table filtered on sym `CAT`. We also include an icon in the tab header. +- Table filtered on sym `DOG` + +```python +@ui.component +def table_tabs(source): + return ui.tabs( + ui.tab_list( + ui.item("Unfiltered", key="Unfiltered"), + ui.item(ui.icon("vsGithubAlt"), "CAT", key="CAT"), + ui.item("DOG", key="DOG"), + ), + ui.tab_panels( + ui.item(source, key="Unfiltered"), + ui.item(source.where("sym=`CAT`"), key="CAT"), + ui.item(source.where("sym=`DOG`"), key="DOG"), + ), + flex_grow=1, + ) + + +result = table_tabs(stocks) +``` diff --git a/plugins/ui/examples/README.md b/plugins/ui/examples/README.md index d9663ee04..325dcfc7a 100644 --- a/plugins/ui/examples/README.md +++ b/plugins/ui/examples/README.md @@ -729,20 +729,18 @@ stocks = dx.data.stocks() @ui.component def table_tabs(source): - return ui.panel( - ui.tabs( - ui.tab_list( - ui.item("Unfiltered", key="Unfiltered"), - ui.item(ui.icon("vsGithubAlt"), "CAT", key="CAT"), - ui.item("DOG", key="DOG"), - ), - ui.tab_panels( - ui.item(source, key="Unfiltered"), - ui.item(source.where("sym=`CAT`"), key="CAT"), - ui.item(source.where("sym=`DOG`"), key="DOG"), - ), - flex_grow=1, - ) + return ui.tabs( + ui.tab_list( + ui.item("Unfiltered", key="Unfiltered"), + ui.item(ui.icon("vsGithubAlt"), "CAT", key="CAT"), + ui.item("DOG", key="DOG"), + ), + ui.tab_panels( + ui.item(source, key="Unfiltered"), + ui.item(source.where("sym=`CAT`"), key="CAT"), + ui.item(source.where("sym=`DOG`"), key="DOG"), + ), + flex_grow=1, ) From 622af799bbfc9f717fe717ccb72f40f6919fcafc Mon Sep 17 00:00:00 2001 From: mikebender Date: Mon, 18 Dec 2023 14:57:17 -0500 Subject: [PATCH 3/5] Clean up from review - Clean up some comments/params --- plugins/ui/src/deephaven/ui/components/object_view.py | 2 +- .../ui/src/deephaven/ui/components/spectrum/basic.py | 2 +- plugins/ui/src/js/src/DocumentUtils.tsx | 10 +++++----- plugins/ui/src/js/src/ElementUtils.ts | 5 +---- plugins/ui/src/js/src/ObjectUtils.ts | 4 +--- plugins/ui/src/js/src/UITableUtils.tsx | 4 +--- plugins/ui/src/js/src/WidgetUtils.tsx | 7 ++++++- plugins/ui/src/js/src/spectrum/mapSpectrumProps.ts | 4 ++-- 8 files changed, 18 insertions(+), 20 deletions(-) diff --git a/plugins/ui/src/deephaven/ui/components/object_view.py b/plugins/ui/src/deephaven/ui/components/object_view.py index ad9e6cb9f..71a5422ff 100644 --- a/plugins/ui/src/deephaven/ui/components/object_view.py +++ b/plugins/ui/src/deephaven/ui/components/object_view.py @@ -6,7 +6,7 @@ def object_view(obj: Any): """ - A wrapper for an exported object that can be rendered as a view. + A wrapper for an exported object that should be rendered as a view. E.g. A Table will be rendered as a Grid view. Args: diff --git a/plugins/ui/src/deephaven/ui/components/spectrum/basic.py b/plugins/ui/src/deephaven/ui/components/spectrum/basic.py index 2a7d4a20d..7c19e3630 100644 --- a/plugins/ui/src/deephaven/ui/components/spectrum/basic.py +++ b/plugins/ui/src/deephaven/ui/components/spectrum/basic.py @@ -17,7 +17,7 @@ def action_button(*children, **props): return spectrum_element("ActionButton", *children, **props) -def button(*children, **props): +def button(*children, **props: int): """ Python implementation for the Adobe React Spectrum Button component. https://react-spectrum.adobe.com/react-spectrum/Button.html diff --git a/plugins/ui/src/js/src/DocumentUtils.tsx b/plugins/ui/src/js/src/DocumentUtils.tsx index 2cb43e22e..8cf40726e 100644 --- a/plugins/ui/src/js/src/DocumentUtils.tsx +++ b/plugins/ui/src/js/src/DocumentUtils.tsx @@ -10,6 +10,7 @@ import { MixedPanelsError, NoChildrenError } from './errors'; * * * @param children Root children of the document. + * @param definition Definition of the widget used to create this document. Used for titling panels if necessary. */ export function getRootChildren( children: React.ReactNode, @@ -20,14 +21,13 @@ export function getRootChildren( } const childrenArray = Array.isArray(children) ? children : [children]; - const childPanelCount = childrenArray.filter( - child => child?.type === ReactPanel - ).length; - if (childrenArray.length === 0) { throw new NoChildrenError('No children to render'); } - if (childPanelCount !== 0 && childPanelCount !== childrenArray.length) { + const childPanelCount = childrenArray.filter( + child => child?.type === ReactPanel + ).length; + if (childPanelCount > 0 && childPanelCount !== childrenArray.length) { throw new MixedPanelsError('Cannot mix panel and non-panel elements'); } diff --git a/plugins/ui/src/js/src/ElementUtils.ts b/plugins/ui/src/js/src/ElementUtils.ts index 7b7322a80..9fd1357a6 100644 --- a/plugins/ui/src/js/src/ElementUtils.ts +++ b/plugins/ui/src/js/src/ElementUtils.ts @@ -1,5 +1,4 @@ import type { WidgetExportedObject } from '@deephaven/jsapi-types'; -import { ReactNode } from 'react'; export const CALLABLE_KEY = '__dhCbid'; export const OBJECT_KEY = '__dhObid'; @@ -36,9 +35,7 @@ export type ElementNodeWithChildren< K extends string = string, P extends Record = Record > = ElementNode & { - props: P & { - children: ReactNode; - }; + props: React.PropsWithChildren

; }; export function isObjectNode(obj: unknown): obj is ObjectNode { diff --git a/plugins/ui/src/js/src/ObjectUtils.ts b/plugins/ui/src/js/src/ObjectUtils.ts index de21386af..cc4daac60 100644 --- a/plugins/ui/src/js/src/ObjectUtils.ts +++ b/plugins/ui/src/js/src/ObjectUtils.ts @@ -8,9 +8,7 @@ export type ObjectElementType = typeof OBJECT_ELEMENT_NAME; export type ObjectViewProps = { object: WidgetExportedObject }; /** Describes an object that can be rendered in the UI. */ -export type ObjectElementNode = ElementNode & { - props: ObjectViewProps; -}; +export type ObjectElementNode = ElementNode; /** * Check if an object is a ObjectElementNode diff --git a/plugins/ui/src/js/src/UITableUtils.tsx b/plugins/ui/src/js/src/UITableUtils.tsx index 9a10a1280..285843e5d 100644 --- a/plugins/ui/src/js/src/UITableUtils.tsx +++ b/plugins/ui/src/js/src/UITableUtils.tsx @@ -14,9 +14,7 @@ export interface UITableProps { [key: string]: unknown; } -export type UITableNode = ElementNode & { - props: UITableProps; -}; +export type UITableNode = ElementNode; export function isUITable(obj: unknown): obj is UITableNode { return ( diff --git a/plugins/ui/src/js/src/WidgetUtils.tsx b/plugins/ui/src/js/src/WidgetUtils.tsx index c0daba315..934dd6431 100644 --- a/plugins/ui/src/js/src/WidgetUtils.tsx +++ b/plugins/ui/src/js/src/WidgetUtils.tsx @@ -26,9 +26,14 @@ export function getComponentForElement(element: ElementNode): React.ReactNode { if (newElement.props?.children != null) { const { children } = newElement.props; if (Array.isArray(children)) { + const typeMap = new Map(); newElement.props.children = children.map((child, i) => { if (isExportedObject(child)) { - return ; + const { type } = child; + const typeCount = typeMap.get(type) ?? 0; + typeMap.set(type, typeCount + 1); + const key = `${type}-${typeCount}`; + return ; } return child; }); diff --git a/plugins/ui/src/js/src/spectrum/mapSpectrumProps.ts b/plugins/ui/src/js/src/spectrum/mapSpectrumProps.ts index 729d02d0a..daba81688 100644 --- a/plugins/ui/src/js/src/spectrum/mapSpectrumProps.ts +++ b/plugins/ui/src/js/src/spectrum/mapSpectrumProps.ts @@ -2,8 +2,8 @@ import { PropsWithChildren } from 'react'; import mapSpectrumChildren from './mapSpectrumChildren'; /** - * Map the children of an element to Spectrum children, automatically wrapping strings and numbers in `Text` elements. - * @param children Children to map as spectrum children + * Map the props of an element to Spectrum props, automatically wrapping children strings and numbers in `Text` elements. + * @param props Props to map as spectrum props */ export function mapSpectrumProps< T extends PropsWithChildren> From 0e9b590c3be116a3863b7c2c0436fc5f36c8b6eb Mon Sep 17 00:00:00 2001 From: mikebender Date: Tue, 19 Dec 2023 09:25:09 -0500 Subject: [PATCH 4/5] Clean up from review - Delete object_view (wasn't being used) - Remove overflow: hidden - Other small changes --- .../src/deephaven/ui/components/__init__.py | 2 -- .../deephaven/ui/components/object_view.py | 15 ------------ plugins/ui/src/js/src/ObjectUtils.ts | 23 ------------------- plugins/ui/src/js/src/ObjectView.tsx | 4 ++-- plugins/ui/src/js/src/UITableUtils.tsx | 4 +++- plugins/ui/src/js/src/WidgetUtils.tsx | 4 ---- plugins/ui/src/js/src/styles.scss | 1 - 7 files changed, 5 insertions(+), 48 deletions(-) delete mode 100644 plugins/ui/src/deephaven/ui/components/object_view.py delete mode 100644 plugins/ui/src/js/src/ObjectUtils.ts diff --git a/plugins/ui/src/deephaven/ui/components/__init__.py b/plugins/ui/src/deephaven/ui/components/__init__.py index 49763bee0..e5db0b60a 100644 --- a/plugins/ui/src/deephaven/ui/components/__init__.py +++ b/plugins/ui/src/deephaven/ui/components/__init__.py @@ -1,7 +1,6 @@ from .icon import icon from .make_component import make_component as component from .fragment import fragment -from .object_view import object_view from .panel import panel from .spectrum import * from .table import table @@ -27,7 +26,6 @@ "html", "number_field", "item", - "object_view", "panel", "range_slider", "slider", diff --git a/plugins/ui/src/deephaven/ui/components/object_view.py b/plugins/ui/src/deephaven/ui/components/object_view.py deleted file mode 100644 index 71a5422ff..000000000 --- a/plugins/ui/src/deephaven/ui/components/object_view.py +++ /dev/null @@ -1,15 +0,0 @@ -from __future__ import annotations - -from typing import Any -from ..elements import BaseElement - - -def object_view(obj: Any): - """ - A wrapper for an exported object that should be rendered as a view. - E.g. A Table will be rendered as a Grid view. - - Args: - obj: The object to display - """ - return BaseElement("deephaven.ui.components.Object", object=obj) diff --git a/plugins/ui/src/js/src/ObjectUtils.ts b/plugins/ui/src/js/src/ObjectUtils.ts deleted file mode 100644 index cc4daac60..000000000 --- a/plugins/ui/src/js/src/ObjectUtils.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { WidgetExportedObject } from '@deephaven/jsapi-types'; -import { ELEMENT_KEY, ElementNode, isElementNode } from './ElementUtils'; - -export const OBJECT_ELEMENT_NAME = 'deephaven.ui.components.Object'; - -export type ObjectElementType = typeof OBJECT_ELEMENT_NAME; - -export type ObjectViewProps = { object: WidgetExportedObject }; - -/** Describes an object that can be rendered in the UI. */ -export type ObjectElementNode = ElementNode; - -/** - * Check if an object is a ObjectElementNode - * @param obj Object to check - * @returns True if the object is a ObjectElementNode - */ -export function isObjectElementNode(obj: unknown): obj is ObjectElementNode { - return ( - isElementNode(obj) && - (obj as ElementNode)[ELEMENT_KEY] === OBJECT_ELEMENT_NAME - ); -} diff --git a/plugins/ui/src/js/src/ObjectView.tsx b/plugins/ui/src/js/src/ObjectView.tsx index 2e84fa650..630110c89 100644 --- a/plugins/ui/src/js/src/ObjectView.tsx +++ b/plugins/ui/src/js/src/ObjectView.tsx @@ -1,11 +1,11 @@ import React, { useCallback, useMemo } from 'react'; import Log from '@deephaven/log'; import { isWidgetPlugin, usePlugins } from '@deephaven/plugin'; -import type { Widget } from '@deephaven/jsapi-types'; -import { ObjectViewProps } from './ObjectUtils'; +import type { Widget, WidgetExportedObject } from '@deephaven/jsapi-types'; const log = Log.module('@deephaven/js-plugin-ui/ObjectView'); +export type ObjectViewProps = { object: WidgetExportedObject }; function ObjectView(props: ObjectViewProps) { const { object } = props; log.info('Object is', object); diff --git a/plugins/ui/src/js/src/UITableUtils.tsx b/plugins/ui/src/js/src/UITableUtils.tsx index 285843e5d..4cdb272ba 100644 --- a/plugins/ui/src/js/src/UITableUtils.tsx +++ b/plugins/ui/src/js/src/UITableUtils.tsx @@ -14,7 +14,9 @@ export interface UITableProps { [key: string]: unknown; } -export type UITableNode = ElementNode; +export type UITableNode = Required< + ElementNode +>; export function isUITable(obj: unknown): obj is UITableNode { return ( diff --git a/plugins/ui/src/js/src/WidgetUtils.tsx b/plugins/ui/src/js/src/WidgetUtils.tsx index 934dd6431..17fde478a 100644 --- a/plugins/ui/src/js/src/WidgetUtils.tsx +++ b/plugins/ui/src/js/src/WidgetUtils.tsx @@ -17,7 +17,6 @@ import UITable from './UITable'; import { isPanelElementNode } from './PanelUtils'; import ReactPanel from './ReactPanel'; import ObjectView from './ObjectView'; -import { isObjectElementNode } from './ObjectUtils'; export function getComponentForElement(element: ElementNode): React.ReactNode { // Need to convert the children of the element if they are exported objects to an ObjectView @@ -56,9 +55,6 @@ export function getComponentForElement(element: ElementNode): React.ReactNode { if (isPanelElementNode(newElement)) { return ; } - if (isObjectElementNode(newElement)) { - return ; - } if (isFragmentElementNode(newElement)) { // eslint-disable-next-line react/jsx-no-useless-fragment return <>{newElement.props?.children}; diff --git a/plugins/ui/src/js/src/styles.scss b/plugins/ui/src/js/src/styles.scss index b8640ba07..fde7155b8 100644 --- a/plugins/ui/src/js/src/styles.scss +++ b/plugins/ui/src/js/src/styles.scss @@ -11,6 +11,5 @@ display: contents; flex-grow: 1; flex-shrink: 1; - overflow: hidden; position: relative; } From e54149592ef96416157daac03448ba519212e4b1 Mon Sep 17 00:00:00 2001 From: mikebender Date: Wed, 20 Dec 2023 16:36:40 -0500 Subject: [PATCH 5/5] Fix review comments --- plugins/ui/src/deephaven/ui/components/spectrum/basic.py | 2 +- plugins/ui/src/js/src/DocumentUtils.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/ui/src/deephaven/ui/components/spectrum/basic.py b/plugins/ui/src/deephaven/ui/components/spectrum/basic.py index 7c19e3630..2a7d4a20d 100644 --- a/plugins/ui/src/deephaven/ui/components/spectrum/basic.py +++ b/plugins/ui/src/deephaven/ui/components/spectrum/basic.py @@ -17,7 +17,7 @@ def action_button(*children, **props): return spectrum_element("ActionButton", *children, **props) -def button(*children, **props: int): +def button(*children, **props): """ Python implementation for the Adobe React Spectrum Button component. https://react-spectrum.adobe.com/react-spectrum/Button.html diff --git a/plugins/ui/src/js/src/DocumentUtils.tsx b/plugins/ui/src/js/src/DocumentUtils.tsx index 8cf40726e..ab8c9c404 100644 --- a/plugins/ui/src/js/src/DocumentUtils.tsx +++ b/plugins/ui/src/js/src/DocumentUtils.tsx @@ -11,6 +11,7 @@ import { MixedPanelsError, NoChildrenError } from './errors'; * * @param children Root children of the document. * @param definition Definition of the widget used to create this document. Used for titling panels if necessary. + * @returns The children, wrapped in a panel if necessary. */ export function getRootChildren( children: React.ReactNode,