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: UI table layout hints #587

Merged
merged 6 commits into from
Jul 9, 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
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;
}) {
mofojed marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -17,5 +17,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",
}
],
},
)
Loading