Skip to content

Commit

Permalink
feat: ui.table front, back, frozen, hidden columns and column groups
Browse files Browse the repository at this point in the history
  • Loading branch information
mattrunyon committed Jun 27, 2024
1 parent 4a25715 commit 88a92ad
Show file tree
Hide file tree
Showing 9 changed files with 251 additions and 15 deletions.
14 changes: 14 additions & 0 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 @@ -23,6 +24,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,
) -> UITable:
"""
Customization to how a table is displayed, how it behaves, and listen to UI events.
Expand All @@ -48,6 +54,14 @@ 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 at the front of the table.
These will always be visible regardless of horizontal scrolling.
hidden_columns: The columns to hide by default. Users may show the columns by expanding them.
column_groups: Columns to group together. 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.
"""
props = locals()
del props["table"]
Expand Down
40 changes: 36 additions & 4 deletions plugins/ui/src/deephaven/ui/types/types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import datetime
import pandas
import numpy
import sys
from typing import (
Any,
Dict,
Expand All @@ -10,9 +8,18 @@
List,
Tuple,
Callable,
TypedDict,
Sequence,
)

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

import datetime
import pandas
import numpy

from deephaven import SortDirection
from deephaven.dtypes import DType

Expand Down Expand Up @@ -54,6 +61,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: NotRequired[str]
"""
Color for the group header.
"""


class SliderChange(TypedDict):
"""
Data for a range slider change event.
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 @@ -13,8 +13,9 @@ import type { dh } from '@deephaven/jsapi-types';
import Log from '@deephaven/log';
import { getSettings, RootState } from '@deephaven/redux';
import { EMPTY_ARRAY } from '@deephaven/utils';
import { UITableProps } from './utils/UITableUtils';
import UITableMouseHandler from './utils/UITableMouseHandler';
import { UITableProps } from './UITableUtils';
import UITableMouseHandler from './UITableMouseHandler';
import JsTableProxy from './JsTableProxy';

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

Expand All @@ -31,12 +32,24 @@ export function UITable({
table: exportedTable,
showSearch: showSearchBar,
showQuickFilters,
frontColumns,
backColumns,
frozenColumns,
hiddenColumns,
columnGroups,
}: UITableProps): JSX.Element | null {
const dh = useApi();
const [model, setModel] = useState<IrisGridModel>();
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 @@ -74,7 +87,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 @@ -87,7 +104,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,6 +1,6 @@
import type { dh } from '@deephaven/jsapi-types';
import { ColumnName, DehydratedSort, RowIndex } from '@deephaven/iris-grid';
import { ELEMENT_KEY, ElementNode, isElementNode } from './ElementUtils';
import { ELEMENT_KEY, ElementNode, isElementNode } from '../utils/ElementUtils';
import { ELEMENT_NAME, ElementName } from '../model/ElementConstants';

export type CellData = {
Expand All @@ -18,7 +18,7 @@ export type ColumnIndex = number;

export type RowDataMap = Record<ColumnName, RowDataValue>;

export interface UITableProps {
export type UITableProps = {
table: dh.WidgetExportedObject;
onCellPress?: (cellIndex: [ColumnIndex, RowIndex], data: CellData) => void;
onCellDoublePress?: (
Expand All @@ -34,8 +34,12 @@ export interface UITableProps {
sorts?: DehydratedSort[];
showSearch: boolean;
showQuickFilters: boolean;
[key: string]: unknown;
}
frontColumns?: string[];
backColumns?: string[];
frozenColumns?: string[];
hiddenColumns?: string[];
columnGroups?: dh.ColumnGroup[];
};

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 @@ -15,5 +15,5 @@ export * from './RangeSlider';
export * from './Slider';
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 88a92ad

Please sign in to comment.