Skip to content
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

feat: Implement python item_table_source #415

Merged
merged 13 commits into from
May 10, 2024
306 changes: 160 additions & 146 deletions plugins/ui/DESIGN.md

Large diffs are not rendered by default.

28 changes: 27 additions & 1 deletion plugins/ui/src/deephaven/ui/_internal/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ def wrap_callable(func: Callable) -> Callable:
return func


def create_props(args: dict[str, Any]) -> tuple[tuple[Any], dict[str, Any]]:
def create_props(args: dict[str, Any]) -> tuple[tuple[Any, ...], dict[str, Any]]:
"""
Create props from the args. Combines the named props with the kwargs props.

Expand Down Expand Up @@ -390,3 +390,29 @@ def convert_date_props(
if not callable(props[key]):
raise TypeError(f"{key} must be a callable")
props[key] = _wrap_date_callable(props[key], converter)


def unpack_item_table_source(
children: tuple[Any, ...],
props: dict[str, Any],
supported_args: set[str],
) -> tuple[tuple[Any, ...], dict[str, Any]]:
"""
Unpack children and props if the children are of type dict
and merge the supported arguments into the props.

Args:
children: The children to possibly unpack.
props: The props to unpack.
supported_args: The supported arguments for the ItemTableSource.

Returns:
The unpacked children and props.
"""
if len(children) == 1 and isinstance(children[0], dict):
item_table_source = children[0]
children = (item_table_source.pop("table"),)
for key in supported_args:
if key in item_table_source:
props[key] = item_table_source.pop(key)
return children, props
2 changes: 2 additions & 0 deletions plugins/ui/src/deephaven/ui/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .list_view import list_view
from .list_action_group import list_action_group
from .list_action_menu import list_action_menu
from .item_table_source import item_table_source
from .date_picker import date_picker

from . import html
Expand All @@ -38,6 +39,7 @@
"icon",
"icon_wrapper",
"item",
"item_table_source",
"illustrated_message",
"list_view",
"list_action_group",
Expand Down
66 changes: 66 additions & 0 deletions plugins/ui/src/deephaven/ui/components/item_table_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from __future__ import annotations

from typing import Union, TypedDict, cast

from deephaven.table import Table, PartitionedTable

from .item import ItemElement
from .list_action_group import ListActionGroupElement
from .list_action_menu import ListActionMenuElement
from ..elements import Element
from ..types import ColumnName, Stringable

ListViewItem = Union[Stringable, ItemElement]
ListViewElement = Element


class ItemTableSource(TypedDict):
table: Table | PartitionedTable
key_column: ColumnName | None
label_column: ColumnName | None
description_column: ColumnName | None
icon_column: ColumnName | None
title_column: ColumnName | None
actions: ListActionGroupElement | ListActionMenuElement | None


def item_table_source(
table: Table | PartitionedTable,
key_column: ColumnName | None = None,
label_column: ColumnName | None = None,
description_column: ColumnName | None = None,
icon_column: ColumnName | None = None,
title_column: ColumnName | None = None,
actions: ListActionGroupElement | ListActionMenuElement | None = None,
) -> ItemTableSource:
"""
An item table source wraps a Table or PartitionedTable to provide additional information for
creating complex items from a table.
A PartitionedTable is only supported if the component itself supports a PartitionedTable as a child.
A PartitionedTable passed here will lead to the same behavior as passing
the PartitionedTable directly to a component, such as creating sections from the partitions in the case of a Picker.

Args:
table:
The table to use as the source of items.
key_column:
The column of values to use as item keys. Defaults to the first column.
label_column:
The column of values to display as primary text. Defaults to the key_column value.
description_column:
The column of values to display as descriptions.
icon_column:
The column of values to map to icons.
title_column:
Only valid if table is of type PartitionedTable.
The column of values to display as section names.
Should be the same for all values in the constituent Table.
If not specified, the section titles will be created from the key_columns of the PartitionedTable.
actions:
The action group or menus to render for all elements within the component, if supported.

Returns:
The item table source to pass as a child to a component that supports it.
"""

return cast(ItemTableSource, locals())
4 changes: 4 additions & 0 deletions plugins/ui/src/deephaven/ui/components/list_action_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def list_action_group(
*children: ActionGroupItem,
on_action: Callable[[ActionKey, Key], None] | None = None,
on_selection_change: Callable[[Selection, Key], None] | None = None,
on_change: Callable[[Selection, Key], None] | None = None,
**props: Any,
) -> ListActionGroupElement:
"""
Expand All @@ -27,6 +28,9 @@ def list_action_group(
The first argument is the key of the action, the second argument is the key of the list_view item.
on_selection_change: Handler that is called when the selection changes.
The first argument is the selection, the second argument is the key of the list_view item.
on_change: Alias of on_selection_change.
Handler that is called when the selection changes.
The first argument is the selection, the second argument is the key of the list_view item.
**props: Any other ActionGroup prop.

Returns:
Expand Down
50 changes: 24 additions & 26 deletions plugins/ui/src/deephaven/ui/components/list_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,57 +5,53 @@
from deephaven.table import Table

from .item import ItemElement
from .list_action_group import ListActionGroupElement
from .list_action_menu import ListActionMenuElement
from .item_table_source import ItemTableSource
from ..elements import BaseElement, Element
from .._internal.utils import create_props
from ..types import ColumnName, Stringable, Selection
from .._internal.utils import create_props, unpack_item_table_source
from ..types import Stringable, Selection, SelectionMode

ListViewItem = Union[Stringable, ItemElement]
ListViewElement = Element

SUPPORTED_SOURCE_ARGS = {
"key_column",
"label_column",
"description_column",
"icon_column",
"actions",
}


def list_view(
*children: ListViewItem | Table,
key_column: ColumnName | None = None,
label_column: ColumnName | None = None,
description_column: ColumnName | None = None,
icon_column: ColumnName | None = None,
actions: ListActionGroupElement | ListActionMenuElement | None = None,
*children: ListViewItem | Table | ItemTableSource,
default_selected_keys: Selection | None = None,
selected_keys: Selection | None = None,
selection_mode: SelectionMode | None = "MULTIPLE",
render_empty_state: Element | None = None,
on_selection_change: Callable[[Selection], None] | None = None,
on_change: Callable[[Selection], None] | None = None,
**props: Any,
) -> ListViewElement:
"""
A list view that can be used to create a list of items. Children should be one of two types:
A list view that can be used to create a list of items. Children should be one of three types:
1. If children are of type `ListViewItem`, they are the list items.
2. If children are of type `Table`, the values in the table are the list items.
There can only be one child, the `Table`.

The first column is used as the key and label by default.
3. If children are of type ItemTableSource, complex items are created from the source.
There can only be one child, the `ItemTableSource`.
Supported `ItemTableSource` arguments are `key_column`, `label_column`, `description_column`,
`icon_column`, and `actions`.

Args:
*children: The options to render within the list_view.
key_column:
Only valid if children are of type Table.
The column of values to use as item keys. Defaults to the first column.
label_column:
Only valid if children are of type Table.
The column of values to display as primary text. Defaults to the key_column value.
description_column:
Only valid if children are of type Table.
The column of values to display as descriptions.
icon_column: Only valid if children are of type Table.
The column of values to map to icons.
actions:
Only valid if children are of type Table.
The action group or menus to render for all elements within the list view.
default_selected_keys:
The initial selected keys in the collection (uncontrolled).
selected_keys:
The currently selected keys in the collection (controlled).
selection_mode:
jnumainville marked this conversation as resolved.
Show resolved Hide resolved
By default `"MULTIPLE"`, which allows multiple selection.
May also be `"SINGLE"` to allow only single selection, or `"None"`/`None` to allow no selection.
render_empty_state:
Sets what the `list_view` should render when there is no content to display.
on_selection_change:
Expand All @@ -70,4 +66,6 @@ def list_view(
"""
children, props = create_props(locals())

children, props = unpack_item_table_source(children, props, SUPPORTED_SOURCE_ARGS)

return BaseElement("deephaven.ui.components.ListView", *children, **props)
54 changes: 25 additions & 29 deletions plugins/ui/src/deephaven/ui/components/picker.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,53 +4,47 @@

from deephaven.table import Table, PartitionedTable
from .section import SectionElement, PickerItem
from .item_table_source import ItemTableSource
from ..elements import BaseElement
from .._internal.utils import create_props
from ..types import ColumnName, Key
from .._internal.utils import create_props, unpack_item_table_source
from ..types import Key

PickerElement = BaseElement

SUPPORTED_SOURCE_ARGS = {
"key_column",
"label_column",
"description_column",
"icon_column",
"title_column",
}


def picker(
*children: PickerItem | SectionElement | Table | PartitionedTable,
key_column: ColumnName | None = None,
label_column: ColumnName | None = None,
description_column: ColumnName | None = None,
icon_column: ColumnName | None = None,
title_column: ColumnName | None = None,
*children: PickerItem | SectionElement | Table | PartitionedTable | ItemTableSource,
default_selected_key: Key | None = None,
selected_key: Key | None = None,
on_selection_change: Callable[[Key], None] | None = None,
on_change: Callable[[Key], None] | None = None,
**props: Any,
) -> PickerElement:
"""
A picker that can be used to select from a list. Children should be one of four types:
If children are of type PickerItem, they are the dropdown options.
If children are of type SectionElement, they are the dropdown sections.
If children are of type Table, the values in the table are the dropdown options.
A picker that can be used to select from a list. Children should be one of five types:
1. If children are of type PickerItem, they are the dropdown options.
2. If children are of type SectionElement, they are the dropdown sections.
3. If children are of type Table, the values in the table are the dropdown options.
There can only be one child, the Table.
If children are of type PartitionedTable, the values in the table are the dropdown options
The first column is used as the key and label by default.
4. If children are of type PartitionedTable, the values in the table are the dropdown options
and the partitions create multiple sections. There can only be one child, the PartitionedTable.
The first column is used as the key and label by default.
5. If children are of type ItemTableSource, complex items are created from the source.
There can only be one child, the ItemTableSource.
Supported ItemTableSource arguments are `key_column`, `label_column`, `description_column`,
`icon_column`, and `title_column`.

Args:
*children: The options to render within the picker.
key_column:
Only valid if children are of type Table or PartitionedTable.
The column of values to use as item keys. Defaults to the first column.
label_column:
Only valid if children are of type Table or PartitionedTable.
The column of values to display as primary text. Defaults to the key_column value.
description_column:
Only valid if children are of type Table or PartitionedTable.
The column of values to display as descriptions.
icon_column: Only valid if children are of type Table or PartitionedTable.
The column of values to map to icons.
title_column:
Only valid if children is of type PartitionedTable.
The column of values to display as section names.
Should be the same for all values in the constituent Table.
If not specified, the section titles will be created from the key_columns of the PartitionedTable.
default_selected_key:
The initial selected key in the collection (uncontrolled).
selected_key:
Expand All @@ -67,4 +61,6 @@ def picker(
"""
children, props = create_props(locals())

children, props = unpack_item_table_source(children, props, SUPPORTED_SOURCE_ARGS)

return BaseElement("deephaven.ui.components.Picker", *children, **props)
3 changes: 2 additions & 1 deletion plugins/ui/src/deephaven/ui/types/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ class RowDataValue(CellData):
ColumnData = List[Any]
TableData = Dict[ColumnName, ColumnData]
SearchMode = Literal["SHOW", "HIDE", "DEFAULT"]
SelectionMode = Literal["CELL", "ROW", "COLUMN"]
SelectionArea = Literal["CELL", "ROW", "COLUMN"]
SelectionMode = Literal["SINGLE", "MULTIPLE", "NONE"]
Sentinel = Any
TransformedData = Any
StringSortDirection = Literal["ASC", "DESC"]
Expand Down
41 changes: 41 additions & 0 deletions plugins/ui/test/deephaven/ui/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import unittest

from .BaseTest import BaseTestCase


Expand Down Expand Up @@ -261,6 +262,46 @@ def test_create_props(self):
},
)

def test_unpack_item_table_source(self):
from deephaven.ui._internal.utils import unpack_item_table_source
from deephaven.ui import item_table_source

children = ("table",)
props = {
"test": "foo",
}

expected_children = ("table",)
expected_props = {
"test": "foo",
}

self.assertTupleEqual(
unpack_item_table_source(children, props, {}),
(expected_children, expected_props),
)

item_data_source = item_table_source(
table="table", key_column="key", actions="actions"
)

children = (item_data_source,)
props = {
"test": "foo",
}

expected_children = ("table",)

expected_props = {
"test": "foo",
"key_column": "key",
}

self.assertTupleEqual(
unpack_item_table_source(children, props, {"table", "key_column"}),
(expected_children, expected_props),
)


if __name__ == "__main__":
unittest.main()
Loading