Skip to content

Commit

Permalink
feat: UI table layout hints (#587)
Browse files Browse the repository at this point in the history
Fixes #442

The layout hints will only be used as the initial state. Not sure how
we'd want to try to combine a change in `ui.table` from the server w/
other changes the user may have made already. At least initially I think
this is fine to give the same functionality as current
`table.layout_hints`

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

_stocks = dx.data.stocks()

stocks_with_hints = ui.table(
    _stocks,
    front_columns=["exchange"],
    frozen_columns=["sym"],
    back_columns=['side'],
    hidden_columns=['dollars', 'SPet500'],
    column_groups=[{"name": "test_group", "children": ["size", "random"], "color": "lemonchiffon"}]
)
```
  • Loading branch information
mattrunyon authored Jul 9, 2024
1 parent 0564299 commit 5e3c5e2
Show file tree
Hide file tree
Showing 11 changed files with 302 additions and 58 deletions.
86 changes: 49 additions & 37 deletions plugins/ui/DESIGN.md

Large diffs are not rendered by default.

23 changes: 19 additions & 4 deletions plugins/ui/src/deephaven/ui/components/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from ..elements import UITable
from ..types import (
CellPressCallback,
ColumnGroup,
ColumnName,
ColumnPressCallback,
QuickFilterExpression,
Expand All @@ -24,6 +25,11 @@ def table(
quick_filters: dict[ColumnName, QuickFilterExpression] | None = None,
show_quick_filters: bool = False,
show_search: bool = False,
front_columns: list[ColumnName] | None = None,
back_columns: list[ColumnName] | None = None,
frozen_columns: list[ColumnName] | None = None,
hidden_columns: list[ColumnName] | None = None,
column_groups: list[ColumnGroup] | None = None,
context_menu: (
ResolvableContextMenuItem | list[ResolvableContextMenuItem] | None
) = None,
Expand Down Expand Up @@ -53,12 +59,21 @@ def table(
quick_filters: The quick filters to apply to the table. Dictionary of column name to filter value.
show_quick_filters: Whether to show the quick filter bar by default.
show_search: Whether to show the search bar by default.
front_columns: The columns to pin to the front of the table. These will not be movable by the user.
back_columns: The columns to pin to the back of the table. These will not be movable by the user.
frozen_columns: The columns to freeze by default at the front of the table.
These will always be visible regardless of horizontal scrolling.
The user may unfreeze columns or freeze additional columns.
hidden_columns: The columns to hide by default. Users may show the columns by expanding them.
column_groups: Columns to group together by default. The groups will be shown in the table header.
Group names must be unique within the column and group names.
Groups may be nested by providing the group name as a child of another group.
context_menu: The context menu items to show when a cell is right clicked.
May contain action items or submenu items.
May also be a function that receives the cell data and returns the context menu items or None.
May contain action items or submenu items.
May also be a function that receives the cell data and returns the context menu items or None.
context_header_menu: The context menu items to show when a column header is right clicked.
May contain action items or submenu items.
May also be a function that receives the column header data and returns the context menu items or None.
May contain action items or submenu items.
May also be a function that receives the column header data and returns the context menu items or None.
"""
props = locals()
del props["table"]
Expand Down
32 changes: 29 additions & 3 deletions plugins/ui/src/deephaven/ui/types/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
from deephaven import SortDirection
from deephaven.dtypes import DType

DeephavenColor = Literal["salmon", "lemonchiffon"]
HexColor = str
Color = Union[DeephavenColor, HexColor]


class CellData(TypedDict):
"""
Expand Down Expand Up @@ -61,6 +65,31 @@ class RowDataValue(CellData):
"""


class ColumnGroup(TypedDict):
"""
Group of columns in a table.
Groups are displayed in the table header.
Groups may be nested.
"""

name: str
"""
Name of the column group.
Must follow column naming rules and be unique within the column and group names.
"""

children: List[str]
"""
List of child columns or groups in the group.
Names are other columns or groups.
"""

color: Color
"""
Color for the group header.
"""


class ContextMenuActionParams(TypedDict):
"""
Parameters given to a context menu action
Expand Down Expand Up @@ -199,9 +228,6 @@ class SliderChange(TypedDict):
"UNIQUE",
"SKIP",
]
DeephavenColor = Literal["salmon", "lemonchiffon"]
HexColor = str
Color = Union[DeephavenColor, HexColor]
ContextMenuModeOption = Literal["CELL", "ROW_HEADER", "COLUMN_HEADER"]
ContextMenuMode = Union[ContextMenuModeOption, List[ContextMenuModeOption], None]
DataBarAxis = Literal["PROPORTIONAL", "MIDDLE", "DIRECTIONAL"]
Expand Down
100 changes: 100 additions & 0 deletions plugins/ui/src/js/src/elements/UITable/JsTableProxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { dh } from '@deephaven/jsapi-types';

export interface UITableLayoutHints {
frontColumns?: string[];
frozenColumns?: string[];
backColumns?: string[];
hiddenColumns?: string[];
columnGroups?: dh.ColumnGroup[];
}

// This tricks TS into believing the class extends dh.Table
// Even though it is through a proxy
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface JsTableProxy extends dh.Table {}

/**
* Class to proxy JsTable.
* Any methods implemented in this class will be utilized over the underlying JsTable methods.
* Any methods not implemented in this class will be proxied to the table.
*/
class JsTableProxy {
private table: dh.Table;

layoutHints: dh.LayoutHints | null = null;

constructor({
table,
layoutHints,
}: {
table: dh.Table;
layoutHints: UITableLayoutHints;
}) {
this.table = table;

const {
frontColumns = null,
frozenColumns = null,
backColumns = null,
hiddenColumns = null,
columnGroups = null,
} = layoutHints;

this.layoutHints = {
frontColumns,
frozenColumns,
backColumns,
hiddenColumns,
columnGroups,
areSavedLayoutsAllowed: false,
};

// eslint-disable-next-line no-constructor-return
return new Proxy(this, {
// We want to use any properties on the proxy model if defined
// If not, then proxy to the underlying model
get(target, prop, receiver) {
// Does this class have a getter for the prop
// Getter functions are on the prototype
const proxyHasGetter =
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(target), prop)
?.get != null;

if (proxyHasGetter) {
return Reflect.get(target, prop, receiver);
}

// Does this class implement the property
const proxyHasProp = Object.prototype.hasOwnProperty.call(target, prop);

// Does the class implement a function for the property
const proxyHasFn = Object.prototype.hasOwnProperty.call(
Object.getPrototypeOf(target),
prop
);

const trueTarget = proxyHasProp || proxyHasFn ? target : target.table;
const value = Reflect.get(trueTarget, prop, receiver);

if (typeof value === 'function') {
return value.bind(trueTarget);
}

return value;
},
set(target, prop, value) {
const proxyHasSetter =
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(target), prop)
?.set != null;

if (proxyHasSetter) {
return Reflect.set(target, prop, value, target);
}

return Reflect.set(target.table, prop, value, target.table);
},
});
}
}

export default JsTableProxy;
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import type { dh } from '@deephaven/jsapi-types';
import Log from '@deephaven/log';
import { getSettings, RootState } from '@deephaven/redux';
import { GridMouseHandler } from '@deephaven/grid';
import { UITableProps, wrapContextActions } from './utils/UITableUtils';
import UITableMouseHandler from './utils/UITableMouseHandler';
import UITableContextMenuHandler from './utils/UITableContextMenuHandler';
import { UITableProps, wrapContextActions } from './UITableUtils';
import UITableMouseHandler from './UITableMouseHandler';
import JsTableProxy from './JsTableProxy';
import UITableContextMenuHandler from './UITableContextMenuHandler';

const log = Log.module('@deephaven/js-plugin-ui/UITable');

Expand All @@ -34,6 +35,11 @@ export function UITable({
table: exportedTable,
showSearch: showSearchBar,
showQuickFilters,
frontColumns,
backColumns,
frozenColumns,
hiddenColumns,
columnGroups,
contextMenu,
contextHeaderMenu,
}: UITableProps): JSX.Element | null {
Expand All @@ -43,6 +49,13 @@ export function UITable({
const [columns, setColumns] = useState<dh.Table['columns']>();
const utils = useMemo(() => new IrisGridUtils(dh), [dh]);
const settings = useSelector(getSettings<RootState>);
const [layoutHints] = useState({
frontColumns,
backColumns,
frozenColumns,
hiddenColumns,
columnGroups,
});

const hydratedSorts = useMemo(() => {
if (sorts !== undefined && columns !== undefined) {
Expand Down Expand Up @@ -80,7 +93,11 @@ export function UITable({
let isCancelled = false;
async function loadModel() {
const reexportedTable = await exportedTable.reexport();
const newTable = (await reexportedTable.fetch()) as dh.Table;
const table = await reexportedTable.fetch();
const newTable = new JsTableProxy({
table: table as dh.Table,
layoutHints,
});
const newModel = await IrisGridModelFactory.makeModel(dh, newTable);
if (!isCancelled) {
setColumns(newTable.columns);
Expand All @@ -93,7 +110,7 @@ export function UITable({
return () => {
isCancelled = true;
};
}, [dh, exportedTable]);
}, [dh, exportedTable, layoutHints]);

const mouseHandlers = useMemo(
() =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { dh } from '@deephaven/jsapi-types';
import type {
import {
ColumnName,
DehydratedSort,
IrisGridContextMenuData,
Expand All @@ -9,8 +9,9 @@ import type {
ResolvableContextAction,
} from '@deephaven/components';
import { ensureArray } from '@deephaven/utils';
import { ELEMENT_KEY, ElementNode, isElementNode } from './ElementUtils';
import { getIcon } from './IconElementUtils';
import { ELEMENT_KEY, ElementNode, isElementNode } from '../utils/ElementUtils';

import { getIcon } from '../utils/IconElementUtils';
import {
ELEMENT_NAME,
ELEMENT_PREFIX,
Expand Down Expand Up @@ -53,7 +54,7 @@ type ResolvableUIContextItem =
params: UIContextItemParams
) => Promise<UIContextItem | UIContextItem[] | null>);

export interface UITableProps {
export type UITableProps = {
table: dh.WidgetExportedObject;
onCellPress?: (data: CellData) => void;
onCellDoublePress?: (data: CellData) => void;
Expand All @@ -66,10 +67,14 @@ export interface UITableProps {
sorts?: DehydratedSort[];
showSearch: boolean;
showQuickFilters: boolean;
frontColumns?: string[];
backColumns?: string[];
frozenColumns?: string[];
hiddenColumns?: string[];
columnGroups?: dh.ColumnGroup[];
contextMenu?: ResolvableUIContextItem | ResolvableUIContextItem[];
contextHeaderMenu?: ResolvableUIContextItem | ResolvableUIContextItem[];
[key: string]: unknown;
}
};

export type UITableNode = Required<
ElementNode<ElementName['uiTable'], UITableProps>
Expand Down
2 changes: 1 addition & 1 deletion plugins/ui/src/js/src/elements/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ export * from './Slider';
export * from './Tabs';
export * from './TabPanels';
export * from './TextField';
export * from './UITable';
export * from './UITable/UITable';
export * from './utils';
4 changes: 2 additions & 2 deletions plugins/ui/src/js/src/elements/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ export * from './ElementUtils';
export * from './EventUtils';
export * from './HTMLElementUtils';
export * from './IconElementUtils';
export * from './UITableMouseHandler';
export * from './UITableUtils';
export * from '../UITable/UITableMouseHandler';
export * from '../UITable/UITableUtils';
69 changes: 69 additions & 0 deletions plugins/ui/test/deephaven/ui/test_ui_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,72 @@ def test_sort(self):
)

self.assertRaises(ValueError, ui_table.sort, ["X", "Y"], ["INVALID"])

def test_front_columns(self):
import deephaven.ui as ui

t = ui.table(self.source, front_columns=["X"])

self.expect_render(
t,
{
"frontColumns": ["X"],
},
)

def test_back_columns(self):
import deephaven.ui as ui

t = ui.table(self.source, back_columns=["X"])

self.expect_render(
t,
{
"backColumns": ["X"],
},
)

def test_frozen_columns(self):
import deephaven.ui as ui

t = ui.table(self.source, frozen_columns=["X"])

self.expect_render(
t,
{
"frozenColumns": ["X"],
},
)

def test_hidden_columns(self):
import deephaven.ui as ui

t = ui.table(self.source, hidden_columns=["X"])

self.expect_render(
t,
{
"hiddenColumns": ["X"],
},
)

def test_column_groups(self):
import deephaven.ui as ui

t = ui.table(
self.source,
column_groups=[{"name": "Group", "children": ["X"], "color": "red"}],
)

self.expect_render(
t,
{
"columnGroups": [
{
"name": "Group",
"children": ["X"],
"color": "red",
}
],
},
)

0 comments on commit 5e3c5e2

Please sign in to comment.