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: Add ui.table press event listener support #346

Merged
merged 7 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
824 changes: 467 additions & 357 deletions package-lock.json

Large diffs are not rendered by default.

148 changes: 80 additions & 68 deletions plugins/ui/DESIGN.md

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions plugins/ui/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,29 @@ def stock_table_input(source, default_sym="", default_exchange=""):
sti = stock_table_input(stocks, "CAT", "TPET")
```

### ui.table Events

The `ui.table` component has a few events that you can listen to. You can listen to different kinds of press events that include the data about the region pressed.

```py
import deephaven.ui as ui
import deephaven.plot.express as dx

te = ui.table(
dx.data.stocks(),
on_row_press=lambda row, data: print(f"Row Press: {row}, {data}"),
on_row_double_press=lambda row, data: print(f"Row Double Press: {row}, {data}"),
on_cell_press=lambda cell_index, data: print(f"Cell Press: {cell_index}, {data}"),
on_cell_double_press=lambda cell_index, data: print(
f"Cell Double Press: {cell_index}, {data}"
),
on_column_press=lambda column: print(f"Column Press: {column}"),
on_column_double_press=lambda column: print(f"Column Double Press: {column}"),
)
```

![Table events](table_events.png)

## Re-using components

In a previous example, we created a text_filter_table component. We can re-use that component, and display two tables with an input filter side-by-side:
Expand Down
Binary file added plugins/ui/examples/assets/table_events.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions plugins/ui/setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ install_requires =
deephaven-core>=0.31.0
deephaven-plugin>=0.6.0
json-rpc
typing_extensions
include_package_data = True

[options.packages.find]
Expand Down
43 changes: 40 additions & 3 deletions plugins/ui/src/deephaven/ui/components/table.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,46 @@
from __future__ import annotations

from deephaven.table import Table
from ..elements import UITable
from ..types import (
CellPressCallback,
ColumnPressCallback,
RowPressCallback,
)


def table(table: Table) -> UITable:
def table(
table: Table,
*,
on_row_press: RowPressCallback | None = None,
on_row_double_press: RowPressCallback | None = None,
on_cell_press: CellPressCallback | None = None,
on_cell_double_press: CellPressCallback | None = None,
on_column_press: ColumnPressCallback | None = None,
on_column_double_press: ColumnPressCallback | None = None,
) -> UITable:
"""
Add some extra methods to the Table class for giving hints to displaying a table
Customization to how a table is displayed, how it behaves, and listen to UI events.

Args:
table: The table to wrap
on_row_press: The callback function to run when a row is clicked.
The first parameter is the row index, and the second is the row data provided in a dictionary where the
column names are the keys.
on_row_double_press: The callback function to run when a row is double clicked.
The first parameter is the row index, and the second is the row data provided in a dictionary where the
column names are the keys.
on_cell_press: The callback function to run when a cell is clicked.
The first parameter is the cell index, and the second is the row data provided in a dictionary where the
column names are the keys.
on_cell_double_press: The callback function to run when a cell is double clicked.
The first parameter is the cell index, and the second is the row data provided in a dictionary where the
column names are the keys.
on_column_press: The callback function to run when a column is clicked.
The first parameter is the column name.
on_column_double_press: The callback function to run when a column is double clicked.
The first parameter is the column name.
"""
return UITable(table)
props = locals()
del props["table"]
return UITable(table, **props)
130 changes: 92 additions & 38 deletions plugins/ui/src/deephaven/ui/elements/UITable.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
from __future__ import annotations

import collections
import logging
import sys
from typing import Callable, Literal, Sequence, Any, cast
from warnings import warn

if sys.version_info < (3, 11):
from typing_extensions import TypedDict, NotRequired
else:
from typing import TypedDict, NotRequired

from deephaven.table import Table
from deephaven import SortDirection
from .Element import Element
Expand All @@ -14,6 +21,8 @@
Color,
ContextMenuAction,
CellIndex,
CellPressCallback,
ColumnPressCallback,
RowData,
ContextMenuMode,
DataBarAxis,
Expand Down Expand Up @@ -48,33 +57,83 @@ def remap_sort_direction(direction: TableSortDirection | str) -> Literal["ASC",
raise ValueError(f"Invalid table sort direction: {direction}")


class UITable(Element):
class UITableProps(TypedDict):
can_search: NotRequired[bool]
"""
Wrap a Table with some extra props for giving hints to displaying a table
Whether the search bar is accessible or not. Use the system default if no value set.
"""

on_row_press: NotRequired[RowPressCallback]
"""
Callback function to run when a row is clicked.
The first parameter is the row index, and the second is the row data provided in a dictionary where the
column names are the keys.
"""

on_row_double_press: NotRequired[RowPressCallback]
"""
The callback function to run when a row is double clicked.
The first parameter is the row index, and the second is the row data provided in a dictionary where the
column names are the keys.
"""

_table: Table
on_cell_press: NotRequired[CellPressCallback]
"""
The table that is wrapped with some extra props
The callback function to run when a cell is clicked.
The first parameter is the cell index, and the second is the row data provided in a dictionary where the
column names are the keys.
"""

_props: dict[str, Any]
on_cell_double_press: NotRequired[CellPressCallback]
"""
The extra props that are added by each method
The callback function to run when a cell is double clicked.
The first parameter is the cell index, and the second is the row data provided in a dictionary where the
column names are the keys.
"""

def __init__(self, table: Table, props: dict[str, Any] = {}):
on_column_press: NotRequired[ColumnPressCallback]
"""
The callback function to run when a column is clicked.
The first parameter is the column name.
"""

on_column_double_press: NotRequired[ColumnPressCallback]
"""
The callback function to run when a column is double clicked.
The first parameter is the column name.
"""

table: Table
"""
The table to wrap
"""


class UITable(Element):
"""
Wrap a Table with some extra props for giving hints to displaying a table
"""

_props: UITableProps
"""
The props that are passed to the frontend
"""

def __init__(
self,
table: Table,
**props: Any,
):
"""
Create a UITable from the passed in table. UITable provides an [immutable fluent interface](https://en.wikipedia.org/wiki/Fluent_interface#Immutability) for adding UI hints to a table.

Args:
table: The table to wrap
props: UITableProps props to pass to the frontend.
"""
self._table = table

# Store the extra props that are added by each method
# This is a shallow copy of the props so that we don't mutate the passed in props dict
self._props = {**props}
# Store all the props that were passed in
self._props = UITableProps(**props, table=table)

@property
def name(self):
Expand All @@ -92,7 +151,7 @@ def _with_prop(self, key: str, value: Any) -> "UITable":
A new UITable with the passed in prop added to the existing props
"""
logger.debug("_with_prop(%s, %s)", key, value)
return UITable(self._table, {**self._props, key: value})
return UITable(**{**self._props, key: value})

def _with_appendable_prop(self, key: str, value: Any) -> "UITable":
"""
Expand All @@ -114,9 +173,9 @@ def _with_appendable_prop(self, key: str, value: Any) -> "UITable":

value = value if isinstance(value, list) else [value]

return UITable(self._table, {**self._props, key: existing + value})
return UITable(**{**self._props, key: existing + value})

def _with_dict_prop(self, prop_name: str, value: dict) -> "UITable":
def _with_dict_prop(self, key: str, value: dict[str, Any]) -> "UITable":
"""
Create a new UITable with the passed in prop in a dictionary.
This will override any existing prop with the same key within
Expand All @@ -130,14 +189,13 @@ def _with_dict_prop(self, prop_name: str, value: dict) -> "UITable":
Returns:
A new UITable with the passed in prop added to the existing props
"""
logger.debug("_with_dict_prop(%s, %s)", prop_name, value)
existing = self._props.get(prop_name, {})
new = {**existing, **value}
return UITable(self._table, {**self._props, prop_name: new})
logger.debug("_with_dict_prop(%s, %s)", key, value)
existing = self._props.get(key, {})
return UITable(**{**self._props, key: {**existing, **value}}) # type: ignore

def render(self, context: RenderContext) -> dict[str, Any]:
logger.debug("Returning props %s", self._props)
return dict_to_camel_case({**self._props, "table": self._table})
return dict_to_camel_case({**self._props})

def aggregations(
self,
Expand Down Expand Up @@ -278,9 +336,13 @@ def color_row(

def context_menu(
self,
items: ContextMenuAction
| list[ContextMenuAction]
| Callable[[CellIndex, RowData], ContextMenuAction | list[ContextMenuAction]],
items: (
ContextMenuAction
| list[ContextMenuAction]
| Callable[
[CellIndex, RowData], ContextMenuAction | list[ContextMenuAction]
]
),
mode: ContextMenuMode = "CELL",
) -> "UITable":
"""
Expand Down Expand Up @@ -395,23 +457,10 @@ def hide_columns(self, columns: str | list[str]) -> "UITable":
"""
raise NotImplementedError()

def on_row_press(self, callback: RowPressCallback) -> "UITable":
"""
Add a callback for when a press on a row is released (e.g. a row is clicked).

Args:
callback: The callback function to run when a row is clicked.
The first parameter is the row index, and the second is the row data provided in a dictionary where the
column names are the keys.

Returns:
A new UITable
"""
raise NotImplementedError()

def on_row_double_press(self, callback: RowPressCallback) -> "UITable":
"""
Add a callback for when a row is double clicked.
*Deprecated: Use the on_row_double_press keyword arg instead.

Args:
callback: The callback function to run when a row is double clicked.
Expand All @@ -421,6 +470,11 @@ def on_row_double_press(self, callback: RowPressCallback) -> "UITable":
Returns:
A new UITable
"""
warn(
"on_row_double_press function is deprecated. Use the on_row_double_press keyword arg instead.",
DeprecationWarning,
stacklevel=2,
)
return self._with_prop("on_row_double_press", callback)

def quick_filter(
Expand Down Expand Up @@ -481,7 +535,7 @@ def sort(
remap_sort_direction(direction) for direction in direction_list_unmapped
]

by_list = by if isinstance(by, Sequence) else [by]
by_list = [by] if isinstance(by, str) else by

if direction and len(direction_list) != len(by_list):
raise ValueError("by and direction must be the same length")
Expand Down
Loading
Loading