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: ListView - ui plugins #408

Merged
merged 16 commits into from
May 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
1,112 changes: 581 additions & 531 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"test:ci:lint": "jest --config jest.config.lint.cjs --ci --cacheDirectory $PWD/.jest-cache",
"e2e:docker": "./tools/run_docker.sh e2e-tests",
"e2e:update-snapshots": "./tools/run_docker.sh update-snapshots",
"update-dh-packages": "lerna run --concurrency 1 update-dh-packages"
"update-dh-packages": "lerna run --concurrency 1 update-dh-packages",
"update-dh-packages:ui": "npm run update-dh-packages -- --scope=@deephaven/js-plugin-ui --"
},
"devDependencies": {
"@deephaven/babel-preset": "^0.72.0",
Expand Down
2 changes: 2 additions & 0 deletions plugins/ui/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -1247,11 +1247,13 @@ ui.list_view(
*children: Item | Table,
key_column: ColumnName | None = None,
label_column: ColumnName | None = None,
density: Density | None = "COMPACT",
description_column: ColumnName | None = None,
icon_column: ColumnName | None = None,
actions: ListActionGroupElement | ListActionMenuElement | None = None,
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,
Expand Down
86 changes: 86 additions & 0 deletions plugins/ui/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,92 @@ my_picker = ui_picker()

![Use a picker to select from a list of items](_assets/picker.png)

## ListView (string values)
A list view that can be used to create a list of selectable items. Here's a basic example for selecting from a list of string values and displaying the selected key in a text field.

```python
from deephaven import ui


@ui.component
def ui_list_view():
value, set_value = ui.use_state(["Text 2"])

# list_view with text children
lv = ui.list_view(
"Text 1",
"Text 2",
"Text 3",
aria_label="List View - Basic",
on_change=set_value,
selected_keys=value,
Comment on lines +260 to +261
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Selection isn't working for me at all right now. The second item should be selected initially, and it should update when things are checked off.

Looks like the props aren't actually passed through in @deephaven/components/ListView: https://github.com/deephaven/web-client-ui/blob/a283e13fafe1ecb156985fab00ba15344f180ff4/packages/components/src/spectrum/listView/ListView.tsx#L55

Unsure why that didn't get flagged as an unused variable ...

Also getting a ton of warnings in the logs whenever I change selection, we should squash these warnings or provide default values so Spectrum doesn't complain:

An aria-label or aria-labelledby prop is required for accessibility. 
    at $f85fb77f9d4cbc6c$var$ListView (

Copy link
Contributor Author

@bmingles bmingles May 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Selection should be fixed by deephaven/web-client-ui#1986 .

I added the aria-label props to examples. I guess we could set a default of "List View" or something like that. @dsmmcken any opinions on this one?

)

# list_view with item children
lv2 = ui.list_view(
ui.item("Item 1", key="Text 1"),
ui.item("Item 2", key="Text 2"),
ui.item("Item 3", key="Text 3"),
aria_label="List View - Basic",
on_change=set_value,
selected_keys=value,
)

text = ui.text("Selection: " + ", ".join(map(str, value)), grid_column="span 2")

return text, lv, lv2


lv = ui_list_view()
```

## ListView (table)
```python
from deephaven import time_table, ui
import datetime

# Ticking table with initial row count of 200 that adds a row every second
initial_row_count = 200
column_types = time_table(
"PT1S",
start_time=datetime.datetime.now() - datetime.timedelta(seconds=initial_row_count),
).update(
[
"Id=new Integer(i)",
"Display=new String(`Display `+i)",
]
)


@ui.component
def ui_list_view_table():
value, set_value = ui.use_state([2, 4, 5])

lv = ui.list_view(
column_types,
key_column="Id",
label_column="Display",
aria_label="List View",
on_change=set_value,
selected_keys=value,
)

text = ui.text("Selection: " + ", ".join(map(str, value)))

return ui.flex(
lv,
text,
direction="column",
margin=10,
gap=10,
# necessary to avoid overflowing container height
min_height=0,
)


lv_table = ui_list_view_table()
bmingles marked this conversation as resolved.
Show resolved Hide resolved
```

## Form (two variables)

You can have state with multiple different variables in one component. This example creates a [text field](https://react-spectrum.adobe.com/react-spectrum/TextField.html) and a [slider](https://react-spectrum.adobe.com/react-spectrum/Slider.html), and we display the values of both of them.
Expand Down
6 changes: 5 additions & 1 deletion plugins/ui/src/deephaven/ui/components/list_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from .list_action_menu import ListActionMenuElement
from ..elements import BaseElement, Element
from .._internal.utils import create_props
from ..types import ColumnName, Stringable, Selection
from ..types import ColumnName, Density, Stringable, Selection, SelectionMode

ListViewItem = Union[Stringable, ItemElement]
ListViewElement = Element
Expand All @@ -19,11 +19,13 @@ def list_view(
*children: ListViewItem | Table,
key_column: ColumnName | None = None,
label_column: ColumnName | None = None,
density: Density | None = "COMPACT",
description_column: ColumnName | None = None,
icon_column: ColumnName | None = None,
actions: ListActionGroupElement | ListActionMenuElement | None = None,
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,
Expand All @@ -44,6 +46,8 @@ def list_view(
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.
density:
Sets the amount of vertical padding within each cell.
description_column:
Only valid if children are of type Table.
The column of values to display as descriptions.
Expand Down
5 changes: 3 additions & 2 deletions 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"]
Sentinel = Any
TransformedData = Any
StringSortDirection = Literal["ASC", "DESC"]
Expand Down Expand Up @@ -138,6 +139,6 @@ class RowDataValue(CellData):
ZonedDateTimeConvertible,
]
Granularity = Literal["DAY", "HOUR", "MINUTE", "SECOND"]

Density = Literal["COMPACT", "NORMAL", "SPACIOUS"]
Dependencies = Union[Tuple[Any], List[Any]]
Selection = Sequence[Key]
32 changes: 16 additions & 16 deletions plugins/ui/src/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,22 +41,22 @@
"react-dom": "^17.0.2"
},
"dependencies": {
"@adobe/react-spectrum": "^3.34.1",
"@deephaven/chart": "^0.73.0",
"@deephaven/components": "^0.73.0",
"@deephaven/dashboard": "^0.73.0",
"@deephaven/dashboard-core-plugins": "^0.73.0",
"@deephaven/grid": "^0.73.0",
"@deephaven/icons": "^0.73.0",
"@deephaven/iris-grid": "^0.73.0",
"@deephaven/jsapi-bootstrap": "^0.73.0",
"@deephaven/jsapi-components": "^0.73.0",
"@deephaven/jsapi-types": "^1.0.0-dev0.33.3",
"@deephaven/log": "^0.73.0",
"@deephaven/plugin": "^0.73.0",
"@deephaven/react-hooks": "^0.73.0",
"@deephaven/redux": "^0.73.0",
"@deephaven/utils": "^0.73.0",
"@adobe/react-spectrum": "3.33.1",
"@deephaven/chart": "^0.76.0",
"@deephaven/components": "^0.76.0",
"@deephaven/dashboard": "^0.76.0",
"@deephaven/dashboard-core-plugins": "^0.76.0",
"@deephaven/grid": "^0.76.0",
"@deephaven/icons": "^0.76.0",
"@deephaven/iris-grid": "^0.76.0",
"@deephaven/jsapi-bootstrap": "^0.76.0",
"@deephaven/jsapi-components": "^0.76.0",
"@deephaven/jsapi-types": "^1.0.0-dev0.34.0",
"@deephaven/log": "^0.76.0",
"@deephaven/plugin": "^0.76.0",
"@deephaven/react-hooks": "^0.76.0",
"@deephaven/redux": "^0.76.0",
"@deephaven/utils": "^0.76.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"@react-types/shared": "^3.22.0",
"json-rpc-2.0": "^1.6.0",
Expand Down
1 change: 1 addition & 0 deletions plugins/ui/src/js/src/elements/ElementConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export type HTMLElementType =
export const FRAGMENT_ELEMENT_NAME =
`${UI_COMPONENTS_NAMESPACE}.Fragment` as const;
export const ITEM_ELEMENT_NAME = `${UI_COMPONENTS_NAMESPACE}.Item` as const;
export const LIST_VIEW_NAME = `${UI_COMPONENTS_NAMESPACE}.ListView` as const;
export const PICKER_ELEMENT_NAME = `${UI_COMPONENTS_NAMESPACE}.Picker` as const;
export const SECTION_ELEMENT_NAME =
`${UI_COMPONENTS_NAMESPACE}.Section` as const;
40 changes: 40 additions & 0 deletions plugins/ui/src/js/src/elements/ListView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useSelector } from 'react-redux';
import { isElementOfType } from '@deephaven/react-hooks';
import { getSettings, RootState } from '@deephaven/redux';
import { ListView as DHListView } from '@deephaven/components';
import { ListView as DHListViewJSApi } from '@deephaven/jsapi-components';
import { SerializedListViewProps, useListViewProps } from './useListViewProps';
import ObjectView from './ObjectView';
import useReExportedTable from './useReExportedTable';

function ListView(props: SerializedListViewProps): JSX.Element | null {
const settings = useSelector(getSettings<RootState>);
const { children, ...listViewProps } = useListViewProps(props);

const isObjectView = isElementOfType(children, ObjectView);
const table = useReExportedTable(children);

if (isObjectView) {
return (
table && (
<DHListViewJSApi
// eslint-disable-next-line react/jsx-props-no-spreading
{...listViewProps}
table={table}
settings={settings}
/>
)
);
}

return (
<DHListView
// eslint-disable-next-line react/jsx-props-no-spreading
{...listViewProps}
>
{children}
</DHListView>
);
}

export default ListView;
43 changes: 9 additions & 34 deletions plugins/ui/src/js/src/elements/Picker.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,18 @@
import React, { ReactElement } from 'react';
import { useSelector } from 'react-redux';
import {
Picker as DHPicker,
PickerProps as DHPickerProps,
} from '@deephaven/components';
import {
Picker as DHPickerJSApi,
PickerProps as DHPickerJSApiProps,
useTableClose,
} from '@deephaven/jsapi-components';
import { isElementOfType, usePromiseFactory } from '@deephaven/react-hooks';
import { Picker as DHPicker } from '@deephaven/components';
import { Picker as DHPickerJSApi } from '@deephaven/jsapi-components';
import { isElementOfType } from '@deephaven/react-hooks';
import { getSettings, RootState } from '@deephaven/redux';
import { SerializedPickerEventProps, usePickerProps } from './usePickerProps';
import ObjectView, { ObjectViewProps } from './ObjectView';
import { fetchReexportedTable } from './ElementUtils';
import { SerializedPickerProps, usePickerProps } from './usePickerProps';
import ObjectView from './ObjectView';
import useReExportedTable from './useReExportedTable';

type WrappedDHPickerJSApiProps = Omit<DHPickerJSApiProps, 'table'> & {
children: ReactElement<ObjectViewProps>;
};

export type PickerProps = (DHPickerProps | WrappedDHPickerJSApiProps) &
SerializedPickerEventProps;

function Picker({ children, ...props }: PickerProps): JSX.Element {
function Picker(props: SerializedPickerProps): JSX.Element | null {
const settings = useSelector(getSettings<RootState>);
const pickerProps = usePickerProps(props);
const { children, ...pickerProps } = usePickerProps(props);

const isObjectView = isElementOfType(children, ObjectView);

const maybeExportedTable =
isObjectView && children.props.object.type === 'Table'
? children.props.object
: null;

const { data: table } = usePromiseFactory(fetchReexportedTable, [
maybeExportedTable,
]);

useTableClose(table);
const table = useReExportedTable(children);

if (isObjectView) {
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { ItemKey, ItemSelection } from '@deephaven/components';
import { useCallback } from 'react';

export type SerializedSelection = 'all' | ItemKey[];

export type SerializedSelectionEventCallback = (
event: SerializedSelection
) => void;

/**
* Selection can be 'all' or a Set of keys. If it is a Set, serialize it as an
* array.
* @param selection Selection to serialize
* @returns Serialized selection
*/
export function serializeSelectionEvent(
selection: ItemSelection
): SerializedSelection {
if (selection instanceof Set) {
return [...selection];
}

return selection;
}

/**
* Get a callback function that can be passed to selection change event handler
* props of Spectrum components.
* @param callback Callback to be called with the serialized selection
* @returns A callback to be passed into the Spectrum component that transforms
* the selection and calls the provided callback
*/
export function useSelectionEventCallback(
callback?: SerializedSelectionEventCallback
): (selection: ItemSelection) => void {
return useCallback(
(selection: ItemSelection) => {
callback?.(serializeSelectionEvent(selection));
},
[callback]
);
}
Loading
Loading