From eb80fa83f24eb0569fdb8f1f8e1a5784c8b30941 Mon Sep 17 00:00:00 2001 From: Steven Wu Date: Tue, 17 Sep 2024 11:41:05 -0400 Subject: [PATCH 01/18] add progress bar and circle --- .../src/deephaven/ui/components/__init__.py | 4 + .../deephaven/ui/components/progress_bar.py | 211 ++++++++++++++++++ .../ui/components/progress_circle.py | 200 +++++++++++++++++ .../deephaven/ui/components/types/__init__.py | 1 + .../deephaven/ui/components/types/progress.py | 4 + .../ui/src/js/src/elements/ProgressBar.tsx | 13 ++ .../ui/src/js/src/elements/ProgressCircle.tsx | 15 ++ plugins/ui/src/js/src/elements/index.ts | 2 + .../js/src/elements/model/ElementConstants.ts | 2 + plugins/ui/src/js/src/widget/WidgetUtils.tsx | 4 + 10 files changed, 456 insertions(+) create mode 100644 plugins/ui/src/deephaven/ui/components/progress_bar.py create mode 100644 plugins/ui/src/deephaven/ui/components/progress_circle.py create mode 100644 plugins/ui/src/deephaven/ui/components/types/progress.py create mode 100644 plugins/ui/src/js/src/elements/ProgressBar.tsx create mode 100644 plugins/ui/src/js/src/elements/ProgressCircle.tsx diff --git a/plugins/ui/src/deephaven/ui/components/__init__.py b/plugins/ui/src/deephaven/ui/components/__init__.py index 04c4f84e5..70d61632c 100644 --- a/plugins/ui/src/deephaven/ui/components/__init__.py +++ b/plugins/ui/src/deephaven/ui/components/__init__.py @@ -32,6 +32,8 @@ from .number_field import number_field from .panel import panel from .picker import picker +from .progress_bar import progress_bar +from .progress_circle import progress_circle from .radio import radio from .radio_group import radio_group from .range_slider import range_slider @@ -88,6 +90,8 @@ "number_field", "panel", "picker", + "progress_bar", + "progress_circle", "radio", "radio_group", "range_slider", diff --git a/plugins/ui/src/deephaven/ui/components/progress_bar.py b/plugins/ui/src/deephaven/ui/components/progress_bar.py new file mode 100644 index 000000000..a717fb6db --- /dev/null +++ b/plugins/ui/src/deephaven/ui/components/progress_bar.py @@ -0,0 +1,211 @@ +from __future__ import annotations +from typing import Any, Callable +from .types import ( + AriaExpanded, + AriaHasPopup, + AriaPressed, + StaticColor, + LabelPosition, + AlignSelf, + CSSProperties, + DimensionValue, + JustifySelf, + LayoutFlex, + Position, + ProgressBarSize, +) + +from .basic import component_element +from ..elements import Element + +ProgressBarButton = Element + + +def progress_bar( + size: ProgressBarSize = "L", + static_color: StaticColor | None = None, + label_position: LabelPosition = "top", + show_value_label: bool | None = None, + label: Element | None = None, + # format_options, # omitted because need to connect it to Deephaven formatting options as well + value_label: Element | None = None, + value: float = 0, + min_value: float = 0, + max_value: float = 100, + is_indeterminate: bool | None = None, + flex: LayoutFlex | None = None, + flex_grow: float | None = None, + flex_shrink: float | None = None, + flex_basis: DimensionValue | None = None, + align_self: AlignSelf | None = None, + justify_self: JustifySelf | None = None, + order: int | None = None, + grid_area: str | None = None, + grid_row: str | None = None, + grid_row_start: str | None = None, + grid_row_end: str | None = None, + grid_column: str | None = None, + grid_column_start: str | None = None, + grid_column_end: str | None = None, + margin: DimensionValue | None = None, + margin_top: DimensionValue | None = None, + margin_bottom: DimensionValue | None = None, + margin_start: DimensionValue | None = None, + margin_end: DimensionValue | None = None, + margin_x: DimensionValue | None = None, + margin_y: DimensionValue | None = None, + width: DimensionValue | None = None, + height: DimensionValue | None = None, + min_width: DimensionValue | None = None, + min_height: DimensionValue | None = None, + max_width: DimensionValue | None = None, + max_height: DimensionValue | None = None, + position: Position | None = None, + top: DimensionValue | None = None, + bottom: DimensionValue | None = None, + start: DimensionValue | None = None, + end: DimensionValue | None = None, + left: DimensionValue | None = None, + right: DimensionValue | None = None, + z_index: int | None = None, + is_hidden: bool | None = None, + id: str | None = None, + exclude_from_tab_order: bool | None = None, + aria_expanded: AriaExpanded | None = None, + aria_haspopup: AriaHasPopup | None = None, + aria_controls: str | None = None, + aria_label: str | None = None, + aria_labelledby: str | None = None, + aria_describedby: str | None = None, + aria_pressed: AriaPressed | None = None, + aria_details: str | None = None, + UNSAFE_class_name: str | None = None, + UNSAFE_style: CSSProperties | None = None, +): + """ + ProgressBars show the progression of a system operation: downloading, uploading, processing, etc., in a visual way. They can represent either determinate or indeterminate progress. + + Args: + size: How thick the bar should be. + static_color: The static color style to apply. Useful when the button appears over a color background. + label_position: The label's overall position relative to the element it is labeling. + show_value_label: Whether the value label should be displayed. True by default if the label is provided. + label: The content to display as the label. + value_label: The content to display as the value's label. + value: The current value (controlled). + min_value: The smallest value allowed for the input. + max_value: The largest value allowed for the input. + is_indeterminate: Indeterminism is presentational only. The indeterminate visual representation remains regardless of user interaction. + flex: When used in a flex layout, specifies how the element will grow or shrink to fit the space available. + flex_grow: When used in a flex layout, specifies how much the element will grow to fit the space available. + flex_shrink: When used in a flex layout, specifies how much the element will shrink to fit the space available. + flex_basis: When used in a flex layout, specifies the initial size of the element. + align_self: Overrides the align_items property of a flex or grid container. + justify_self: Specifies how the element is justified inside a flex or grid container. + order: The layout order for the element within a flex or grid container. + grid_area: The name of the grid area to place the element in. + grid_row: The name of the grid row to place the element in. + grid_row_start: The name of the grid row to start the element in. + grid_row_end: The name of the grid row to end the element in. + grid_column: The name of the grid column to place the element in. + grid_column_start: The name of the grid column to start the element in. + grid_column_end: The name of the grid column to end the element in. + margin: The margin to apply around the element. + margin_top: The margin to apply above the element. + margin_bottom: The margin to apply below the element. + margin_start: The margin to apply before the element. + margin_end: The margin to apply after the element. + margin_x: The margin to apply to the left and right of the element. + margin_y: The margin to apply to the top and bottom of the element. + width: The width of the element. + height: The height of the element. + min_width: The minimum width of the element. + min_height: The minimum height of the element. + max_width: The maximum width of the element. + max_height: The maximum height of the element. + position: Specifies how the element is positioned. + top: The distance from the top of the containing element. + bottom: The distance from the bottom of the containing element. + start: The distance from the start of the containing element. + end: The distance from the end of the containing element. + left: The distance from the left of the containing element. + right: The distance from the right of the containing element. + z_index: The stack order of the element. + is_hidden: Whether the element is hidden. + id: A unique identifier for the element. + exclude_from_tab_order: Whether the element should be excluded from the tab order. + aria_expanded: Whether the element is expanded. + aria_haspopup: Whether the element has a popup. + aria_controls: The id of the element that the element controls. + aria_label: The label for the element. + aria_labelledby: The id of the element that labels the element. + aria_describedby: The id of the element that describes the element. + aria_pressed: Whether the element is pressed. + aria_details: The details for the element. + UNSAFE_class_name: A CSS class to apply to the element. + UNSAFE_style: A CSS style to apply to the element. + + Returns: + The rendered ProgressBar element. + """ + return component_element( + "ProgressBar", + size=size, + static_color=static_color, + label_position=label_position, + show_value_label=show_value_label, + label=label, + value_label=value_label, + value=value, + min_value=min_value, + max_value=max_value, + is_indeterminate=is_indeterminate, + flex=flex, + flex_grow=flex_grow, + flex_shrink=flex_shrink, + flex_basis=flex_basis, + align_self=align_self, + justify_self=justify_self, + order=order, + grid_area=grid_area, + grid_row=grid_row, + grid_row_start=grid_row_start, + grid_row_end=grid_row_end, + grid_column=grid_column, + grid_column_start=grid_column_start, + grid_column_end=grid_column_end, + margin=margin, + margin_top=margin_top, + margin_bottom=margin_bottom, + margin_start=margin_start, + margin_end=margin_end, + margin_x=margin_x, + margin_y=margin_y, + width=width, + height=height, + min_width=min_width, + min_height=min_height, + max_width=max_width, + max_height=max_height, + position=position, + top=top, + bottom=bottom, + start=start, + end=end, + left=left, + right=right, + z_index=z_index, + is_hidden=is_hidden, + id=id, + exclude_from_tab_order=exclude_from_tab_order, + aria_expanded=aria_expanded, + aria_haspopup=aria_haspopup, + aria_controls=aria_controls, + aria_label=aria_label, + aria_labelledby=aria_labelledby, + aria_describedby=aria_describedby, + aria_pressed=aria_pressed, + aria_details=aria_details, + UNSAFE_class_name=UNSAFE_class_name, + UNSAFE_style=UNSAFE_style, + ) diff --git a/plugins/ui/src/deephaven/ui/components/progress_circle.py b/plugins/ui/src/deephaven/ui/components/progress_circle.py new file mode 100644 index 000000000..5a0642d23 --- /dev/null +++ b/plugins/ui/src/deephaven/ui/components/progress_circle.py @@ -0,0 +1,200 @@ +from __future__ import annotations +from typing import Any, Callable +from .types import ( + # Accessibility + AriaExpanded, + AriaHasPopup, + AriaPressed, + # Events + StaticColor, + # Layout + AlignSelf, + CSSProperties, + DimensionValue, + JustifySelf, + LayoutFlex, + Position, + ProgressCircleSize, +) + +from .basic import component_element +from ..elements import Element + +ProgressCircleButton = Element + + +def progress_circle( + size: ProgressCircleSize = "M", + static_color: StaticColor | None = None, + is_indeterminate: bool | None = None, + value: float = 0, + min_value: float = 0, + max_value: float = 100, + flex: LayoutFlex | None = None, + flex_grow: float | None = None, + flex_shrink: float | None = None, + flex_basis: DimensionValue | None = None, + align_self: AlignSelf | None = None, + justify_self: JustifySelf | None = None, + order: int | None = None, + grid_area: str | None = None, + grid_row: str | None = None, + grid_row_start: str | None = None, + grid_row_end: str | None = None, + grid_column: str | None = None, + grid_column_start: str | None = None, + grid_column_end: str | None = None, + margin: DimensionValue | None = None, + margin_top: DimensionValue | None = None, + margin_bottom: DimensionValue | None = None, + margin_start: DimensionValue | None = None, + margin_end: DimensionValue | None = None, + margin_x: DimensionValue | None = None, + margin_y: DimensionValue | None = None, + width: DimensionValue | None = None, + height: DimensionValue | None = None, + min_width: DimensionValue | None = None, + min_height: DimensionValue | None = None, + max_width: DimensionValue | None = None, + max_height: DimensionValue | None = None, + position: Position | None = None, + top: DimensionValue | None = None, + bottom: DimensionValue | None = None, + start: DimensionValue | None = None, + end: DimensionValue | None = None, + left: DimensionValue | None = None, + right: DimensionValue | None = None, + z_index: int | None = None, + is_hidden: bool | None = None, + id: str | None = None, + exclude_from_tab_order: bool | None = None, + aria_expanded: AriaExpanded | None = None, + aria_haspopup: AriaHasPopup | None = None, + aria_controls: str | None = None, + aria_label: str | None = None, + aria_labelledby: str | None = None, + aria_describedby: str | None = None, + aria_pressed: AriaPressed | None = None, + aria_details: str | None = None, + UNSAFE_class_name: str | None = None, + UNSAFE_style: CSSProperties | None = None, +): + """ + ProgressCircles show the progression of a system operation such as downloading, uploading, or processing, in a visual way. They can represent determinate or indeterminate progress. + + Args: + size: What the progress_circle's diameter should be. + static_color: The static color style to apply. Useful when the button appears over a color background. + is_indeterminate: Indeterminism is presentational only. The indeterminate visual representation remains regardless of user interaction. + value: The current value (controlled). + min_value: The smallest value allowed for the input. + max_value: The largest value allowed for the input. + flex: When used in a flex layout, specifies how the element will grow or shrink to fit the space available. + flex_grow: When used in a flex layout, specifies how much the element will grow to fit the space available. + flex_shrink: When used in a flex layout, specifies how much the element will shrink to fit the space available. + flex_basis: When used in a flex layout, specifies the initial size of the element. + align_self: Overrides the align_items property of a flex or grid container. + justify_self: Specifies how the element is justified inside a flex or grid container. + order: The layout order for the element within a flex or grid container. + grid_area: The name of the grid area to place the element in. + grid_row: The name of the grid row to place the element in. + grid_row_start: The name of the grid row to start the element in. + grid_row_end: The name of the grid row to end the element in. + grid_column: The name of the grid column to place the element in. + grid_column_start: The name of the grid column to start the element in. + grid_column_end: The name of the grid column to end the element in. + margin: The margin to apply around the element. + margin_top: The margin to apply above the element. + margin_bottom: The margin to apply below the element. + margin_start: The margin to apply before the element. + margin_end: The margin to apply after the element. + margin_x: The margin to apply to the left and right of the element. + margin_y: The margin to apply to the top and bottom of the element. + width: The width of the element. + height: The height of the element. + min_width: The minimum width of the element. + min_height: The minimum height of the element. + max_width: The maximum width of the element. + max_height: The maximum height of the element. + position: Specifies how the element is positioned. + top: The distance from the top of the containing element. + bottom: The distance from the bottom of the containing element. + start: The distance from the start of the containing element. + end: The distance from the end of the containing element. + left: The distance from the left of the containing element. + right: The distance from the right of the containing element. + z_index: The stack order of the element. + is_hidden: Whether the element is hidden. + id: A unique identifier for the element. + exclude_from_tab_order: Whether the element should be excluded from the tab order. + aria_expanded: Whether the element is expanded. + aria_haspopup: Whether the element has a popup. + aria_controls: The id of the element that the element controls. + aria_label: The label for the element. + aria_labelledby: The id of the element that labels the element. + aria_describedby: The id of the element that describes the element. + aria_pressed: Whether the element is pressed. + aria_details: The details for the element. + UNSAFE_class_name: A CSS class to apply to the element. + UNSAFE_style: A CSS style to apply to the element. + + Returns: + The rendered ProgressCircle element. + """ + return component_element( + "ProgressCircle", + size=size, + static_color=static_color, + is_indeterminate=is_indeterminate, + value=value, + min_value=min_value, + max_value=max_value, + flex=flex, + flex_grow=flex_grow, + flex_shrink=flex_shrink, + flex_basis=flex_basis, + align_self=align_self, + justify_self=justify_self, + order=order, + grid_area=grid_area, + grid_row=grid_row, + grid_row_start=grid_row_start, + grid_row_end=grid_row_end, + grid_column=grid_column, + grid_column_start=grid_column_start, + grid_column_end=grid_column_end, + margin=margin, + margin_top=margin_top, + margin_bottom=margin_bottom, + margin_start=margin_start, + margin_end=margin_end, + margin_x=margin_x, + margin_y=margin_y, + width=width, + height=height, + min_width=min_width, + min_height=min_height, + max_width=max_width, + max_height=max_height, + position=position, + top=top, + bottom=bottom, + start=start, + end=end, + left=left, + right=right, + z_index=z_index, + is_hidden=is_hidden, + id=id, + exclude_from_tab_order=exclude_from_tab_order, + aria_expanded=aria_expanded, + aria_haspopup=aria_haspopup, + aria_controls=aria_controls, + aria_label=aria_label, + aria_labelledby=aria_labelledby, + aria_describedby=aria_describedby, + aria_pressed=aria_pressed, + aria_details=aria_details, + UNSAFE_class_name=UNSAFE_class_name, + UNSAFE_style=UNSAFE_style, + ) diff --git a/plugins/ui/src/deephaven/ui/components/types/__init__.py b/plugins/ui/src/deephaven/ui/components/types/__init__.py index b55b4aec9..4208d7bf6 100644 --- a/plugins/ui/src/deephaven/ui/components/types/__init__.py +++ b/plugins/ui/src/deephaven/ui/components/types/__init__.py @@ -3,5 +3,6 @@ from .date_picker import * from .events import * from .layout import * +from .progress import * from .validate import * from .icon_types import * diff --git a/plugins/ui/src/deephaven/ui/components/types/progress.py b/plugins/ui/src/deephaven/ui/components/types/progress.py new file mode 100644 index 000000000..00ea22fe6 --- /dev/null +++ b/plugins/ui/src/deephaven/ui/components/types/progress.py @@ -0,0 +1,4 @@ +from typing import Literal + +ProgressBarSize = Literal["S", "L"] +ProgressCircleSize = Literal["S", "M", "L"] diff --git a/plugins/ui/src/js/src/elements/ProgressBar.tsx b/plugins/ui/src/js/src/elements/ProgressBar.tsx new file mode 100644 index 000000000..5071e6949 --- /dev/null +++ b/plugins/ui/src/js/src/elements/ProgressBar.tsx @@ -0,0 +1,13 @@ +import { + ProgressBar as DHCProgressBar, + ProgressBarProps as DHCProgressBarProps, +} from '@deephaven/components'; + +export function ProgressBar(props: DHCProgressBarProps): JSX.Element | null { + // eslint-disable-next-line react/jsx-props-no-spreading + return ; +} + +ProgressBar.displayName = 'ProgressBar'; + +export default ProgressBar; diff --git a/plugins/ui/src/js/src/elements/ProgressCircle.tsx b/plugins/ui/src/js/src/elements/ProgressCircle.tsx new file mode 100644 index 000000000..74f2a536c --- /dev/null +++ b/plugins/ui/src/js/src/elements/ProgressCircle.tsx @@ -0,0 +1,15 @@ +import { + ProgressCircle as DHCProgressCircle, + ProgressCircleProps as DHCProgressCircleProps, +} from '@deephaven/components'; + +export function ProgressCircle( + props: DHCProgressCircleProps +): JSX.Element | null { + // eslint-disable-next-line react/jsx-props-no-spreading + return ; +} + +ProgressCircle.displayName = 'ProgressCircle'; + +export default ProgressCircle; diff --git a/plugins/ui/src/js/src/elements/index.ts b/plugins/ui/src/js/src/elements/index.ts index 8decb03b7..6e4ec7db4 100644 --- a/plugins/ui/src/js/src/elements/index.ts +++ b/plugins/ui/src/js/src/elements/index.ts @@ -15,6 +15,8 @@ export * from './ListView'; export * from './model'; export * from './ObjectView'; export * from './Picker'; +export * from './ProgressBar'; +export * from './ProgressCircle'; export * from './Radio'; export * from './RadioGroup'; export * from './RangeSlider'; diff --git a/plugins/ui/src/js/src/elements/model/ElementConstants.ts b/plugins/ui/src/js/src/elements/model/ElementConstants.ts index c1b8cd3d6..3bee12e86 100644 --- a/plugins/ui/src/js/src/elements/model/ElementConstants.ts +++ b/plugins/ui/src/js/src/elements/model/ElementConstants.ts @@ -48,6 +48,8 @@ export const ELEMENT_NAME = { listView: uiComponentName('ListView'), numberField: uiComponentName('NumberField'), picker: uiComponentName('Picker'), + progressBar: uiComponentName('ProgressBar'), + progressCircle: uiComponentName('ProgressCircle'), radio: uiComponentName('Radio'), radioGroup: uiComponentName('RadioGroup'), rangeSlider: uiComponentName('RangeSlider'), diff --git a/plugins/ui/src/js/src/widget/WidgetUtils.tsx b/plugins/ui/src/js/src/widget/WidgetUtils.tsx index 85d031dd3..c6a75260a 100644 --- a/plugins/ui/src/js/src/widget/WidgetUtils.tsx +++ b/plugins/ui/src/js/src/widget/WidgetUtils.tsx @@ -59,6 +59,7 @@ import { Image, ListView, Picker, + ProgressCircle, Radio, RadioGroup, RangeSlider, @@ -69,6 +70,7 @@ import { ToggleButton, UITable, Tabs, + ProgressBar, } from '../elements'; /** @@ -123,6 +125,8 @@ export const elementComponentMap = { [ELEMENT_NAME.listView]: ListView, [ELEMENT_NAME.numberField]: NumberField, [ELEMENT_NAME.picker]: Picker, + [ELEMENT_NAME.progressBar]: ProgressBar, + [ELEMENT_NAME.progressCircle]: ProgressCircle, [ELEMENT_NAME.radio]: Radio, [ELEMENT_NAME.radioGroup]: RadioGroup, [ELEMENT_NAME.rangeSlider]: RangeSlider, From af92dd3634731e3b3572fa554c9b179d4e5daa0d Mon Sep 17 00:00:00 2001 From: Steven Wu Date: Wed, 18 Sep 2024 10:13:41 -0400 Subject: [PATCH 02/18] reorder import --- plugins/ui/src/js/src/widget/WidgetUtils.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ui/src/js/src/widget/WidgetUtils.tsx b/plugins/ui/src/js/src/widget/WidgetUtils.tsx index c6a75260a..9b8bae277 100644 --- a/plugins/ui/src/js/src/widget/WidgetUtils.tsx +++ b/plugins/ui/src/js/src/widget/WidgetUtils.tsx @@ -59,6 +59,7 @@ import { Image, ListView, Picker, + ProgressBar, ProgressCircle, Radio, RadioGroup, @@ -70,7 +71,6 @@ import { ToggleButton, UITable, Tabs, - ProgressBar, } from '../elements'; /** From bad422715d606fd561e152c347063aa5904b755d Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 17 Sep 2024 14:59:26 -0500 Subject: [PATCH 03/18] build: Post version update for utilities (#886) [0.0.2](https://pypi.org/project/deephaven-plugin-utilities/0.0.2/) is released --- plugins/utilities/setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/utilities/setup.cfg b/plugins/utilities/setup.cfg index 18cbf5ade..308cd2e58 100644 --- a/plugins/utilities/setup.cfg +++ b/plugins/utilities/setup.cfg @@ -3,7 +3,7 @@ name = deephaven-plugin-utilities description = Deephaven Plugin Utilities long_description = file: README.md long_description_content_type = text/markdown -version = 0.0.2 +version = 0.0.2.dev0 url = https://github.com/deephaven/deephaven-plugins project_urls = Source Code = https://github.com/deephaven/deephaven-plugins From c38d9415e7b60b452177bb48ec70cfe8fa913a68 Mon Sep 17 00:00:00 2001 From: Steven Wu Date: Thu, 19 Sep 2024 14:58:11 -0400 Subject: [PATCH 04/18] add docs --- plugins/ui/docs/components/progress_bar.md | 71 +++++++++++++++++++ plugins/ui/docs/components/progress_circle.md | 62 ++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 plugins/ui/docs/components/progress_bar.md create mode 100644 plugins/ui/docs/components/progress_circle.md diff --git a/plugins/ui/docs/components/progress_bar.md b/plugins/ui/docs/components/progress_bar.md new file mode 100644 index 000000000..13386dcd5 --- /dev/null +++ b/plugins/ui/docs/components/progress_bar.md @@ -0,0 +1,71 @@ +# Progress Bar + +Progress Bars show the progression of a system operation: downloading, uploading, processing, etc., in a visual way. They can represent either determinate or indeterminate progress. + +## Example + +```python +from deephaven import ui + + +@ui.component +def ui_progress_bar(): + return ui.progress_bar(size="L", is_indeterminate=True) + + +progress_bar = ui_progress_bar() +``` + +## UI Recommendations + +1. Use the appropriate size based on parent's size +2. Use `static_color="white"` or `static_color="white"` if necessary to make sure the progress circle has enough contrast with the background +3. If the value of the progress is unknown, use `is_indeterminate=True` + +## Visual Options + +Progress Bar comes in 2 different sizes by the `size` prop: `"S"` and `"L"`. Furthermore, the `static_color` prop can be used to control the color of the progress circle. + +```python +def progress_bar_variants(): + return ui.view( + ui.flex( + ui.progress_bar(size="S", value=30, margin="10px"), + ui.progress_bar(size="L", value=60, margin="10px"), + ui.progress_bar(size="L", is_indeterminate=True, margin="10px"), + direction="column", + ), + ui.flex( + ui.progress_bar(size="S", value=30, margin="10px", static_color="white"), + ui.progress_bar(size="L", value=60, margin="10px", static_color="white"), + ui.progress_bar( + size="L", is_indeterminate=True, margin="10px", static_color="white" + ), + direction="column", + ), + ui.view( + ui.flex( + ui.progress_bar( + size="S", value=30, margin="10px", static_color="black" + ), + ui.progress_bar( + size="L", value=60, margin="10px", static_color="black" + ), + ui.progress_bar( + size="L", is_indeterminate=True, margin="10px", static_color="black" + ), + direction="column", + ), + background_color="white", + ), + ) + + +progress_bar_variants_example = progress_bar_variants() +``` + +## API Reference + +```{eval-rst} +.. dhautofunction:: deephaven.ui.progress_bar +``` diff --git a/plugins/ui/docs/components/progress_circle.md b/plugins/ui/docs/components/progress_circle.md new file mode 100644 index 000000000..a7bbd19d2 --- /dev/null +++ b/plugins/ui/docs/components/progress_circle.md @@ -0,0 +1,62 @@ +# Progress Circle + +Progress circles show the progression of a system operation such as downloading, uploading, or processing, in a visual way. They can represent determinate or indeterminate progress. + +## Example + +```python +from deephaven import ui + + +@ui.component +def ui_progress_circle(): + return ui.progress_circle(size="L", is_indeterminate=True) + + +progress_circle = ui_progress_circle() +``` + +## UI Recommendations + +1. Use the appropriate size based on parent's size +2. Use `static_color="white"` or `static_color="white"` if necessary to make sure the progress circle has enough contrast with the background +3. If the value of the progress is unknown, use `is_indeterminate=True` + +## Visual Options + +Progress Circle comes in 3 different sizes by the `size` prop: `"S"`, `"M"`, and `"L"`. Furthermore, the `static_color` prop can be used to control the color of the progress circle. + +```python +def progress_circle_variants(): + return ui.view( + ui.view( + ui.progress_circle(size="S", value=30, margin="5px"), + ui.progress_circle(size="M", value=60, margin="5px"), + ui.progress_circle(size="L", is_indeterminate=True, margin="5px"), + ), + ui.view( + ui.progress_circle(size="S", value=30, margin="5px", static_color="white"), + ui.progress_circle(size="M", value=60, margin="5px", static_color="white"), + ui.progress_circle( + size="L", is_indeterminate=True, margin="5px", static_color="white" + ), + ), + ui.view( + ui.progress_circle(size="S", value=30, margin="5px", static_color="black"), + ui.progress_circle(size="M", value=60, margin="5px", static_color="black"), + ui.progress_circle( + size="L", is_indeterminate=True, margin="5px", static_color="black" + ), + background_color="white", + ), + ) + + +progress_circle_variants_example = progress_circle_variants() +``` + +## API Reference + +```{eval-rst} +.. dhautofunction:: deephaven.ui.progress_circle +``` From dcadc95831d186b9cf2fad7cf572a89a82a079ad Mon Sep 17 00:00:00 2001 From: Joe Date: Thu, 19 Sep 2024 16:47:54 -0500 Subject: [PATCH 05/18] fix: Correct type for generated JsPlugin (#741) Followup for #740 Modifies our plugins to use `CommonJsPlugin` by dropping the arg (since it's made optional in #740) and using the default. --------- Co-authored-by: mikebender --- plugins/matplotlib/setup.cfg | 2 +- .../deephaven/plugin/matplotlib/_js_plugin.py | 32 ----------------- .../deephaven/plugin/matplotlib/_register.py | 8 +---- plugins/plotly-express/setup.cfg | 2 +- .../src/deephaven/plot/express/_js_plugin.py | 32 ----------------- .../src/deephaven/plot/express/_register.py | 8 +---- plugins/ui/setup.cfg | 2 +- plugins/ui/src/deephaven/ui/_js_plugin.py | 32 ----------------- plugins/ui/src/deephaven/ui/_register.py | 9 +---- plugins/utilities/setup.cfg | 2 +- .../README.md | 3 +- .../js_plugin.py | 34 ------------------- .../register.py | 6 +--- 13 files changed, 9 insertions(+), 163 deletions(-) delete mode 100644 plugins/matplotlib/src/deephaven/plugin/matplotlib/_js_plugin.py delete mode 100644 plugins/plotly-express/src/deephaven/plot/express/_js_plugin.py delete mode 100644 plugins/ui/src/deephaven/ui/_js_plugin.py delete mode 100644 templates/widget/{{ cookiecutter.python_project_name }}/src/{{ cookiecutter.__src_folder_name }}/js_plugin.py diff --git a/plugins/matplotlib/setup.cfg b/plugins/matplotlib/setup.cfg index 09af60996..4fb380b96 100644 --- a/plugins/matplotlib/setup.cfg +++ b/plugins/matplotlib/setup.cfg @@ -29,7 +29,7 @@ install_requires = jpy>=0.14.0 deephaven-plugin>=0.5.0 matplotlib - deephaven-plugin-utilities + deephaven-plugin-utilities>=0.0.2 include_package_data = True [options.extras_require] diff --git a/plugins/matplotlib/src/deephaven/plugin/matplotlib/_js_plugin.py b/plugins/matplotlib/src/deephaven/plugin/matplotlib/_js_plugin.py deleted file mode 100644 index 7c2e9bf65..000000000 --- a/plugins/matplotlib/src/deephaven/plugin/matplotlib/_js_plugin.py +++ /dev/null @@ -1,32 +0,0 @@ -import pathlib - -from deephaven.plugin.js import JsPlugin - - -class MatplotlibJsPlugin(JsPlugin): - def __init__( - self, - name: str, - version: str, - main: str, - path: pathlib.Path, - ) -> None: - self._name = name - self._version = version - self._main = main - self._path = path - - @property - def name(self) -> str: - return self._name - - @property - def version(self) -> str: - return self._version - - @property - def main(self) -> str: - return self._main - - def path(self) -> pathlib.Path: - return self._path diff --git a/plugins/matplotlib/src/deephaven/plugin/matplotlib/_register.py b/plugins/matplotlib/src/deephaven/plugin/matplotlib/_register.py index 33d18567a..23b18c32f 100644 --- a/plugins/matplotlib/src/deephaven/plugin/matplotlib/_register.py +++ b/plugins/matplotlib/src/deephaven/plugin/matplotlib/_register.py @@ -2,11 +2,9 @@ import matplotlib.pyplot as plt from deephaven.plugin import Registration, Callback from deephaven.plugin.utilities import create_js_plugin, DheSafeCallbackWrapper -from ._js_plugin import MatplotlibJsPlugin PACKAGE_NAMESPACE = "deephaven.plugin.matplotlib" JS_NAME = "_js" -PLUGIN_CLASS = MatplotlibJsPlugin def _init_theme(): @@ -30,10 +28,6 @@ def register_into(cls, callback: Callback) -> None: callback.register(figure_type.FigureType) - js_plugin = create_js_plugin( - PACKAGE_NAMESPACE, - JS_NAME, - PLUGIN_CLASS, - ) + js_plugin = create_js_plugin(PACKAGE_NAMESPACE, JS_NAME) callback.register(js_plugin) diff --git a/plugins/plotly-express/setup.cfg b/plugins/plotly-express/setup.cfg index 94db3d940..6a6334208 100644 --- a/plugins/plotly-express/setup.cfg +++ b/plugins/plotly-express/setup.cfg @@ -28,7 +28,7 @@ install_requires = deephaven-core>=0.36.0 deephaven-plugin>=0.6.0 plotly - deephaven-plugin-utilities + deephaven-plugin-utilities>=0.0.2 include_package_data = True [options.packages.find] diff --git a/plugins/plotly-express/src/deephaven/plot/express/_js_plugin.py b/plugins/plotly-express/src/deephaven/plot/express/_js_plugin.py deleted file mode 100644 index f1df67cba..000000000 --- a/plugins/plotly-express/src/deephaven/plot/express/_js_plugin.py +++ /dev/null @@ -1,32 +0,0 @@ -import pathlib - -from deephaven.plugin.js import JsPlugin - - -class ExpressJsPlugin(JsPlugin): - def __init__( - self, - name: str, - version: str, - main: str, - path: pathlib.Path, - ) -> None: - self._name = name - self._version = version - self._main = main - self._path = path - - @property - def name(self) -> str: - return self._name - - @property - def version(self) -> str: - return self._version - - @property - def main(self) -> str: - return self._main - - def path(self) -> pathlib.Path: - return self._path diff --git a/plugins/plotly-express/src/deephaven/plot/express/_register.py b/plugins/plotly-express/src/deephaven/plot/express/_register.py index 4a2f27bb7..2cc711117 100644 --- a/plugins/plotly-express/src/deephaven/plot/express/_register.py +++ b/plugins/plotly-express/src/deephaven/plot/express/_register.py @@ -2,11 +2,9 @@ from deephaven.plugin import Registration, Callback from deephaven.plugin.utilities import create_js_plugin, DheSafeCallbackWrapper from . import DeephavenFigureType -from ._js_plugin import ExpressJsPlugin PACKAGE_NAMESPACE = "deephaven.plot.express" JS_NAME = "_js" -PLUGIN_CLASS = ExpressJsPlugin class ExpressRegistration(Registration): @@ -31,10 +29,6 @@ def register_into(cls, callback: Callback) -> None: callback.register(DeephavenFigureType) - js_plugin = create_js_plugin( - PACKAGE_NAMESPACE, - JS_NAME, - PLUGIN_CLASS, - ) + js_plugin = create_js_plugin(PACKAGE_NAMESPACE, JS_NAME) callback.register(js_plugin) diff --git a/plugins/ui/setup.cfg b/plugins/ui/setup.cfg index ac4e8e7f4..5f66c9051 100644 --- a/plugins/ui/setup.cfg +++ b/plugins/ui/setup.cfg @@ -28,7 +28,7 @@ install_requires = deephaven-core>=0.34.1 deephaven-plugin>=0.6.0 json-rpc - deephaven-plugin-utilities + deephaven-plugin-utilities>=0.0.2 typing_extensions;python_version<'3.11' include_package_data = True diff --git a/plugins/ui/src/deephaven/ui/_js_plugin.py b/plugins/ui/src/deephaven/ui/_js_plugin.py deleted file mode 100644 index fa3b9a05e..000000000 --- a/plugins/ui/src/deephaven/ui/_js_plugin.py +++ /dev/null @@ -1,32 +0,0 @@ -import pathlib - -from deephaven.plugin.js import JsPlugin - - -class UiJsPlugin(JsPlugin): - def __init__( - self, - name: str, - version: str, - main: str, - path: pathlib.Path, - ) -> None: - self._name = name - self._version = version - self._main = main - self._path = path - - @property - def name(self) -> str: - return self._name - - @property - def version(self) -> str: - return self._version - - @property - def main(self) -> str: - return self._main - - def path(self) -> pathlib.Path: - return self._path diff --git a/plugins/ui/src/deephaven/ui/_register.py b/plugins/ui/src/deephaven/ui/_register.py index 3dfe4502b..b79755a64 100644 --- a/plugins/ui/src/deephaven/ui/_register.py +++ b/plugins/ui/src/deephaven/ui/_register.py @@ -2,11 +2,8 @@ from deephaven.plugin import Registration, Callback from deephaven.plugin.utilities import create_js_plugin, DheSafeCallbackWrapper -from ._js_plugin import UiJsPlugin - PACKAGE_NAMESPACE = "deephaven.ui" JS_NAME = "_js" -PLUGIN_CLASS = UiJsPlugin class UIRegistration(Registration): @@ -18,10 +15,6 @@ def register_into(cls, callback: Callback) -> None: callback.register(DashboardType) callback.register(ElementType) - js_plugin = create_js_plugin( - PACKAGE_NAMESPACE, - JS_NAME, - PLUGIN_CLASS, - ) + js_plugin = create_js_plugin(PACKAGE_NAMESPACE, JS_NAME) callback.register(js_plugin) diff --git a/plugins/utilities/setup.cfg b/plugins/utilities/setup.cfg index 308cd2e58..4ee51e716 100644 --- a/plugins/utilities/setup.cfg +++ b/plugins/utilities/setup.cfg @@ -3,7 +3,7 @@ name = deephaven-plugin-utilities description = Deephaven Plugin Utilities long_description = file: README.md long_description_content_type = text/markdown -version = 0.0.2.dev0 +version = 0.0.3.dev0 url = https://github.com/deephaven/deephaven-plugins project_urls = Source Code = https://github.com/deephaven/deephaven-plugins diff --git a/templates/widget/{{ cookiecutter.python_project_name }}/README.md b/templates/widget/{{ cookiecutter.python_project_name }}/README.md index 458631eb7..ae50f2465 100644 --- a/templates/widget/{{ cookiecutter.python_project_name }}/README.md +++ b/templates/widget/{{ cookiecutter.python_project_name }}/README.md @@ -13,8 +13,7 @@ Within the `src` directory, the {{ cookiecutter.python_project_name }} directory The Python files have the following structure: `{{ cookiecutter.__object_file_name }}.py` defines a simple Python class that can send messages to the client. This object can be modified to have other plugin functionality or replaced with a different object entirely, depending on the plugin's needs. `{{ cookiecutter.__type_file_name }}.py` defines the Python type for the plugin (which is used for registration) and a simple message stream. These can be modified to handle different objects or messages. An initial message is sent from the Python side to the client, then additional messages can be sent back and forth. -`js_plugin.py` defines the Python class that will be used to setup the JavaScript side of the plugin. -`register.py` registers the plugin with Deephaven. This file will not need to be modified for most plugins at the initial stages, but will need to be if the package is renamed or JavaScript files are moved. +`register.py` registers the plugin with Deephaven. This file will not need to be modified for most plugins at the initial stages, but will need to be if the package is renamed or JavaScript files are moved. The JavaScript files have the following structure: `{{ cookiecutter.__js_plugin_obj }}.ts` registers the plugin with Deephaven. This contains the client equivalent of the type in `{{ cookiecutter.__type_file_name }}.py` and these should be kept in sync. diff --git a/templates/widget/{{ cookiecutter.python_project_name }}/src/{{ cookiecutter.__src_folder_name }}/js_plugin.py b/templates/widget/{{ cookiecutter.python_project_name }}/src/{{ cookiecutter.__src_folder_name }}/js_plugin.py deleted file mode 100644 index 1cc424257..000000000 --- a/templates/widget/{{ cookiecutter.python_project_name }}/src/{{ cookiecutter.__src_folder_name }}/js_plugin.py +++ /dev/null @@ -1,34 +0,0 @@ -import pathlib - -from deephaven.plugin.js import JsPlugin - - -# This plugin allows the Python plugin to register a JavaScript plugin. -# The properties will be filled in during the registration process. -class {{ cookiecutter.__py_js_plugin_obj_name }}(JsPlugin): - def __init__( - self, - name: str, - version: str, - main: str, - path: pathlib.Path, - ) -> None: - self._name = name - self._version = version - self._main = main - self._path = path - - @property - def name(self) -> str: - return self._name - - @property - def version(self) -> str: - return self._version - - @property - def main(self) -> str: - return self._main - - def path(self) -> pathlib.Path: - return self._path diff --git a/templates/widget/{{ cookiecutter.python_project_name }}/src/{{ cookiecutter.__src_folder_name }}/register.py b/templates/widget/{{ cookiecutter.python_project_name }}/src/{{ cookiecutter.__src_folder_name }}/register.py index b30fce84a..fd55450d8 100644 --- a/templates/widget/{{ cookiecutter.python_project_name }}/src/{{ cookiecutter.__src_folder_name }}/register.py +++ b/templates/widget/{{ cookiecutter.python_project_name }}/src/{{ cookiecutter.__src_folder_name }}/register.py @@ -1,15 +1,12 @@ from deephaven.plugin import Registration, Callback from deephaven.plugin.utilities import create_js_plugin, DheSafeCallbackWrapper -from .js_plugin import {{ cookiecutter.__py_js_plugin_obj_name }} from .{{ cookiecutter.__type_file_name }} import {{ cookiecutter.__type_name }} # The namespace that the Python plugin will be registered under. PACKAGE_NAMESPACE = "{{cookiecutter.__py_namespace}}" # Where the Javascript plugin is. This is set in setup.py. JS_NAME = "_js" -# The JsPlugin class that will be created and registered. -PLUGIN_CLASS = {{ cookiecutter.__py_js_plugin_obj_name }} class {{ cookiecutter.__registration_name }}(Registration): @@ -23,8 +20,7 @@ def register_into(cls, callback: Callback) -> None: # The JavaScript plugin requires a special registration process, which is handled here js_plugin = create_js_plugin( PACKAGE_NAMESPACE, - JS_NAME, - PLUGIN_CLASS, + JS_NAME ) callback.register(js_plugin) From c9bd5c15109d27c54e0eaadd361a5e591d7180a9 Mon Sep 17 00:00:00 2001 From: Matthew Runyon Date: Thu, 19 Sep 2024 17:55:16 -0500 Subject: [PATCH 06/18] ci: Specify deephaven-plugin-utilities version for pre-commit (#898) Looks like pre-commit uses its own venv if you specify dependencies, so this version needs to be explicit. Otherwise our local pre-commit hooks will have old dependencies and fail locally --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 005a5854a..9a8f4b48f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: plotly, json-rpc, matplotlib, - deephaven-plugin-utilities, + deephaven-plugin-utilities>=0.0.2, sphinx, click, watchdog, @@ -31,4 +31,4 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.2.2 hooks: - - id: ruff + - id: ruff From e34ad1433c7a6a6d683b4f82e6a014d73df94ed3 Mon Sep 17 00:00:00 2001 From: Steven Wu Date: Fri, 20 Sep 2024 10:31:36 -0400 Subject: [PATCH 07/18] add return type --- plugins/ui/src/deephaven/ui/components/progress_bar.py | 4 ++-- plugins/ui/src/deephaven/ui/components/progress_circle.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/ui/src/deephaven/ui/components/progress_bar.py b/plugins/ui/src/deephaven/ui/components/progress_bar.py index a717fb6db..0b22f32db 100644 --- a/plugins/ui/src/deephaven/ui/components/progress_bar.py +++ b/plugins/ui/src/deephaven/ui/components/progress_bar.py @@ -18,7 +18,7 @@ from .basic import component_element from ..elements import Element -ProgressBarButton = Element +ProgressBarElement = Element def progress_bar( @@ -81,7 +81,7 @@ def progress_bar( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, -): +) -> ProgressBarElement: """ ProgressBars show the progression of a system operation: downloading, uploading, processing, etc., in a visual way. They can represent either determinate or indeterminate progress. diff --git a/plugins/ui/src/deephaven/ui/components/progress_circle.py b/plugins/ui/src/deephaven/ui/components/progress_circle.py index 5a0642d23..4c7ad256c 100644 --- a/plugins/ui/src/deephaven/ui/components/progress_circle.py +++ b/plugins/ui/src/deephaven/ui/components/progress_circle.py @@ -20,7 +20,7 @@ from .basic import component_element from ..elements import Element -ProgressCircleButton = Element +ProgressCircleElement = Element def progress_circle( @@ -78,7 +78,7 @@ def progress_circle( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, -): +) -> ProgressCircleElement: """ ProgressCircles show the progression of a system operation such as downloading, uploading, or processing, in a visual way. They can represent determinate or indeterminate progress. From 785efb0ccf1787538ea1bea996ba42c061b7e43a Mon Sep 17 00:00:00 2001 From: Steven Wu Date: Fri, 20 Sep 2024 12:22:07 -0400 Subject: [PATCH 08/18] update docs from review Co-authored-by: margaretkennedy <82049573+margaretkennedy@users.noreply.github.com> --- plugins/ui/docs/components/progress_bar.md | 8 ++++---- plugins/ui/docs/components/progress_circle.md | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/ui/docs/components/progress_bar.md b/plugins/ui/docs/components/progress_bar.md index 13386dcd5..93b7690f9 100644 --- a/plugins/ui/docs/components/progress_bar.md +++ b/plugins/ui/docs/components/progress_bar.md @@ -18,13 +18,13 @@ progress_bar = ui_progress_bar() ## UI Recommendations -1. Use the appropriate size based on parent's size -2. Use `static_color="white"` or `static_color="white"` if necessary to make sure the progress circle has enough contrast with the background -3. If the value of the progress is unknown, use `is_indeterminate=True` +1. Use the appropriate size based on the parent's size. +2. Use `static_color="white"` or `static_color="black"` if necessary to ensure the progress circle has enough contrast with the background. +3. If the value of the progress is unknown, use `is_indeterminate=True`. ## Visual Options -Progress Bar comes in 2 different sizes by the `size` prop: `"S"` and `"L"`. Furthermore, the `static_color` prop can be used to control the color of the progress circle. +Progress Bar comes in two different sizes determined by the `size` prop: `"S"` and `"L"`. Furthermore, the `static_color` prop can be used to control the color of the progress circle. ```python def progress_bar_variants(): diff --git a/plugins/ui/docs/components/progress_circle.md b/plugins/ui/docs/components/progress_circle.md index a7bbd19d2..6dd12b23a 100644 --- a/plugins/ui/docs/components/progress_circle.md +++ b/plugins/ui/docs/components/progress_circle.md @@ -18,13 +18,13 @@ progress_circle = ui_progress_circle() ## UI Recommendations -1. Use the appropriate size based on parent's size -2. Use `static_color="white"` or `static_color="white"` if necessary to make sure the progress circle has enough contrast with the background -3. If the value of the progress is unknown, use `is_indeterminate=True` +1. Use the appropriate size based on the parent's size. +2. Use `static_color="white"` or `static_color="black"` if necessary to make sure the progress circle has enough contrast with the background. +3. If the value of the progress is unknown, use `is_indeterminate=True`. ## Visual Options -Progress Circle comes in 3 different sizes by the `size` prop: `"S"`, `"M"`, and `"L"`. Furthermore, the `static_color` prop can be used to control the color of the progress circle. +Progress Circle comes in three different sizes determined by the `size` prop: `"S"`, `"M"`, and `"L"`. Furthermore, the `static_color` prop can be used to control the color of the progress circle. ```python def progress_circle_variants(): From 15262dd1938e4a6ce42cf7cc1ff2bd641b50ca28 Mon Sep 17 00:00:00 2001 From: Steven Wu Date: Fri, 20 Sep 2024 10:29:27 -0400 Subject: [PATCH 09/18] fix: allows keys to be set in props (#810) - Fixes #731 - Extract and save `key` prop - Modify key generation to use the `key` prop if it exists - Allow `map` to be a valid component - Converts directly to a list while checking for unique props --- .../deephaven/ui/components/action_button.py | 3 ++ .../deephaven/ui/components/action_group.py | 3 ++ .../deephaven/ui/components/action_menu.py | 3 ++ .../ui/src/deephaven/ui/components/button.py | 3 ++ .../deephaven/ui/components/button_group.py | 3 ++ .../src/deephaven/ui/components/checkbox.py | 3 ++ .../ui/src/deephaven/ui/components/column.py | 5 ++- .../src/deephaven/ui/components/combo_box.py | 2 + .../ui/src/deephaven/ui/components/content.py | 3 ++ .../ui/components/contextual_help.py | 3 ++ .../src/deephaven/ui/components/date_field.py | 2 + .../deephaven/ui/components/date_picker.py | 2 + .../ui/components/date_range_picker.py | 2 + .../ui/src/deephaven/ui/components/flex.py | 5 ++- .../ui/src/deephaven/ui/components/form.py | 3 ++ .../src/deephaven/ui/components/fragment.py | 5 ++- .../ui/src/deephaven/ui/components/grid.py | 3 ++ .../ui/src/deephaven/ui/components/heading.py | 3 ++ .../ui/src/deephaven/ui/components/icon.py | 2 + .../ui/components/illustrated_message.py | 3 ++ .../ui/src/deephaven/ui/components/image.py | 3 ++ .../ui/src/deephaven/ui/components/item.py | 2 + .../ui/components/item_table_source.py | 3 ++ .../ui/components/list_action_group.py | 3 ++ .../ui/components/list_action_menu.py | 3 ++ .../src/deephaven/ui/components/list_view.py | 3 ++ .../deephaven/ui/components/make_component.py | 6 +-- .../deephaven/ui/components/number_field.py | 3 ++ .../ui/src/deephaven/ui/components/panel.py | 2 + .../ui/src/deephaven/ui/components/picker.py | 2 + .../ui/src/deephaven/ui/components/radio.py | 2 + .../deephaven/ui/components/radio_group.py | 2 + .../deephaven/ui/components/range_slider.py | 3 ++ plugins/ui/src/deephaven/ui/components/row.py | 5 ++- .../ui/src/deephaven/ui/components/section.py | 8 ++-- .../ui/src/deephaven/ui/components/slider.py | 3 ++ .../ui/src/deephaven/ui/components/stack.py | 3 ++ .../ui/src/deephaven/ui/components/switch.py | 3 ++ .../src/deephaven/ui/components/tab_list.py | 3 ++ .../src/deephaven/ui/components/tab_panels.py | 3 ++ .../ui/src/deephaven/ui/components/table.py | 2 + .../ui/src/deephaven/ui/components/tabs.py | 3 ++ .../ui/src/deephaven/ui/components/text.py | 3 ++ .../src/deephaven/ui/components/text_area.py | 3 ++ .../src/deephaven/ui/components/text_field.py | 3 ++ .../deephaven/ui/components/toggle_button.py | 3 ++ .../ui/src/deephaven/ui/components/view.py | 3 ++ .../src/deephaven/ui/elements/BaseElement.py | 11 +++++- .../ui/src/deephaven/ui/elements/Element.py | 10 +++++ .../deephaven/ui/elements/FunctionElement.py | 9 ++++- .../ui/src/deephaven/ui/elements/UITable.py | 5 +++ .../ui/src/deephaven/ui/renderer/Renderer.py | 8 ++-- tests/app.d/ui.py | 39 +++++++++++++++++++ tests/ui.spec.ts | 19 +++++++++ 54 files changed, 227 insertions(+), 20 deletions(-) diff --git a/plugins/ui/src/deephaven/ui/components/action_button.py b/plugins/ui/src/deephaven/ui/components/action_button.py index dd61e5dfc..e87669ebe 100644 --- a/plugins/ui/src/deephaven/ui/components/action_button.py +++ b/plugins/ui/src/deephaven/ui/components/action_button.py @@ -91,6 +91,7 @@ def action_button( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> ActionButtonElement: """ ActionButtons allow users to perform an action. They're used for similar, task-based options within a workflow, and are ideal for interfaces where buttons aren't meant to draw a lot of attention. @@ -161,6 +162,7 @@ def action_button( aria_details: The details for the element. UNSAFE_class_name: A CSS class to apply to the element. UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. Returns: The rendered ActionButton element. @@ -231,4 +233,5 @@ def action_button( aria_details=aria_details, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/action_group.py b/plugins/ui/src/deephaven/ui/components/action_group.py index 6e4bc2024..04c6d01f4 100644 --- a/plugins/ui/src/deephaven/ui/components/action_group.py +++ b/plugins/ui/src/deephaven/ui/components/action_group.py @@ -84,6 +84,7 @@ def action_group( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> Element: """ An action grouping of action items that are related to each other. @@ -152,6 +153,7 @@ def action_group( aria-details: Identifies the element (or elements) that provide a detailed, extended description for the object. UNSAFE_class_name: Set the CSS className for the element. Only use as a last resort. Use style props instead. UNSAFE_style: Set the inline style for the element. Only use as a last resort. Use style props instead. + key: A unique identifier used by React to render elements in a list. """ return component_element( "ActionGroup", @@ -217,4 +219,5 @@ def action_group( aria_details=aria_details, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/action_menu.py b/plugins/ui/src/deephaven/ui/components/action_menu.py index 4529b4e4f..557e1f532 100644 --- a/plugins/ui/src/deephaven/ui/components/action_menu.py +++ b/plugins/ui/src/deephaven/ui/components/action_menu.py @@ -85,6 +85,7 @@ def action_menu( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> Element: """ ActionMenu combines an ActionButton with a Menu for simple "more actions" use cases. @@ -147,6 +148,7 @@ def action_menu( aria-details: Identifies the element (or elements) that provide a detailed, extended description for the object. UNSAFE_class_name: Set the CSS className for the element. Only use as a last resort. Use style props instead. UNSAFE_style: Set the inline style for the element. Only use as a last resort. Use style props instead. + key: A unique identifier used by React to render elements in a list. """ return component_element( f"ActionMenu", @@ -207,4 +209,5 @@ def action_menu( aria_details=aria_details, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/button.py b/plugins/ui/src/deephaven/ui/components/button.py index 7d7ed8f73..003ebb659 100644 --- a/plugins/ui/src/deephaven/ui/components/button.py +++ b/plugins/ui/src/deephaven/ui/components/button.py @@ -95,6 +95,7 @@ def button( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> Element: """ Buttons allow users to perform an action or to navigate to another page. They have multiple styles for various needs, and are ideal for calling attention to where a user needs to do something in order to move forward in a flow. @@ -170,6 +171,7 @@ def button( aria_details: The details of the current element. UNSAFE_class_name: A CSS class to apply to the element. UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. Returns: The rendered button component. @@ -248,4 +250,5 @@ def button( aria_details=aria_details, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/button_group.py b/plugins/ui/src/deephaven/ui/components/button_group.py index d88d1a09a..484a2eeb3 100644 --- a/plugins/ui/src/deephaven/ui/components/button_group.py +++ b/plugins/ui/src/deephaven/ui/components/button_group.py @@ -59,6 +59,7 @@ def button_group( id: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> Element: """ A button group is a grouping of button whose actions are related to each other. @@ -101,6 +102,7 @@ def button_group( id: The unique identifier of the element. UNSAFE_class_name: Set the CSS className for the element. Only use as a last resort. Use style props instead. UNSAFE_style: Set the inline style for the element. Only use as a last resort. Use style props instead. + key: A unique identifier used by React to render elements in a list. """ return component_element( "ButtonGroup", @@ -147,4 +149,5 @@ def button_group( id=id, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/checkbox.py b/plugins/ui/src/deephaven/ui/components/checkbox.py index be74ab9e4..9450ffb87 100644 --- a/plugins/ui/src/deephaven/ui/components/checkbox.py +++ b/plugins/ui/src/deephaven/ui/components/checkbox.py @@ -84,6 +84,7 @@ def checkbox( aria_errormessage: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> Element: """ Checkboxes allow users to select multiple items from a list of individual items, or to mark one individual item as selected. @@ -153,6 +154,7 @@ def checkbox( aria_errormessage: The id of the element that provides error information for the current element. UNSAFE_class_name: A CSS class to apply to the element. UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. Returns: The rendered checkbox. @@ -226,4 +228,5 @@ def checkbox( aria_errormessage=aria_errormessage, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/column.py b/plugins/ui/src/deephaven/ui/components/column.py index a869753ee..ef6868097 100644 --- a/plugins/ui/src/deephaven/ui/components/column.py +++ b/plugins/ui/src/deephaven/ui/components/column.py @@ -4,7 +4,7 @@ from .basic import component_element -def column(*children: Any, width: float | None = None, **kwargs: Any): +def column(*children: Any, width: float | None = None, key: str | None = None): """ A column is a container that can be used to group elements. Each element will be placed below its prior sibling. @@ -12,5 +12,6 @@ def column(*children: Any, width: float | None = None, **kwargs: Any): Args: children: Elements to render in the column. width: The percent width of the column relative to other children of its parent. If not provided, the column will be sized automatically. + key: A unique identifier used by React to render elements in a list. """ - return component_element("Column", *children, width=width, **kwargs) + return component_element("Column", *children, width=width, key=key) diff --git a/plugins/ui/src/deephaven/ui/components/combo_box.py b/plugins/ui/src/deephaven/ui/components/combo_box.py index a5327583b..7fe4ef89e 100644 --- a/plugins/ui/src/deephaven/ui/components/combo_box.py +++ b/plugins/ui/src/deephaven/ui/components/combo_box.py @@ -126,6 +126,7 @@ def combo_box( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> ComboBoxElement: """ A combo box that can be used to search or select from a list. Children should be one of five types: @@ -231,6 +232,7 @@ def combo_box( aria_details: The details for the element. UNSAFE_class_name: A CSS class to apply to the element. UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. Returns: The rendered ComboBox. diff --git a/plugins/ui/src/deephaven/ui/components/content.py b/plugins/ui/src/deephaven/ui/components/content.py index e496b2997..b3d4bc0c5 100644 --- a/plugins/ui/src/deephaven/ui/components/content.py +++ b/plugins/ui/src/deephaven/ui/components/content.py @@ -54,6 +54,7 @@ def content( id: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> Element: """ Content represents the primary content within a Spectrum container. @@ -99,6 +100,7 @@ def content( id: The unique identifier of the element. UNSAFE_class_name: A CSS class to apply to the element. UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. """ return component_element( "Content", @@ -142,4 +144,5 @@ def content( id=id, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/contextual_help.py b/plugins/ui/src/deephaven/ui/components/contextual_help.py index c509b23a8..8de37b75e 100644 --- a/plugins/ui/src/deephaven/ui/components/contextual_help.py +++ b/plugins/ui/src/deephaven/ui/components/contextual_help.py @@ -70,6 +70,7 @@ def contextual_help( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> Element: """ A contextual help is a quiet action button that triggers an informational popover. @@ -127,6 +128,7 @@ def contextual_help( aria-details: Identifies the element (or elements) that provide a detailed, extended description for the object. UNSAFE_class_name: Set the CSS className for the element. Only use as a last resort. Use style props instead. UNSAFE_style: Set the inline style for the element. Only use as a last resort. Use style props instead. + key: A unique identifier used by React to render elements in a list. """ return component_element( "ContextualHelp", @@ -183,4 +185,5 @@ def contextual_help( aria_details=aria_details, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/date_field.py b/plugins/ui/src/deephaven/ui/components/date_field.py index 7426b8f5d..b24451fdb 100644 --- a/plugins/ui/src/deephaven/ui/components/date_field.py +++ b/plugins/ui/src/deephaven/ui/components/date_field.py @@ -155,6 +155,7 @@ def date_field( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> DateFieldElement: """ A date field allows the user to select a date. @@ -251,6 +252,7 @@ def date_field( aria_details: The details for the element. UNSAFE_class_name: A CSS class to apply to the element. UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. Returns: The date field element. diff --git a/plugins/ui/src/deephaven/ui/components/date_picker.py b/plugins/ui/src/deephaven/ui/components/date_picker.py index f2182b9f3..1763f8c8b 100644 --- a/plugins/ui/src/deephaven/ui/components/date_picker.py +++ b/plugins/ui/src/deephaven/ui/components/date_picker.py @@ -162,6 +162,7 @@ def date_picker( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> DatePickerElement: """ A date picker allows the user to select a date. @@ -265,6 +266,7 @@ def date_picker( aria_details: The details for the element. UNSAFE_class_name: A CSS class to apply to the element. UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. Returns: The date picker element. diff --git a/plugins/ui/src/deephaven/ui/components/date_range_picker.py b/plugins/ui/src/deephaven/ui/components/date_range_picker.py index a852bed2d..86768abbc 100644 --- a/plugins/ui/src/deephaven/ui/components/date_range_picker.py +++ b/plugins/ui/src/deephaven/ui/components/date_range_picker.py @@ -162,6 +162,7 @@ def date_range_picker( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> DatePickerElement: """ A date range picker allows the user to select a range of dates. @@ -268,6 +269,7 @@ def date_range_picker( aria_details: The details for the element. UNSAFE_class_name: A CSS class to apply to the element. UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. Returns: The date range picker element. diff --git a/plugins/ui/src/deephaven/ui/components/flex.py b/plugins/ui/src/deephaven/ui/components/flex.py index a015bc51d..91832047e 100644 --- a/plugins/ui/src/deephaven/ui/components/flex.py +++ b/plugins/ui/src/deephaven/ui/components/flex.py @@ -23,7 +23,7 @@ def flex( gap: DimensionValue | None = "size-100", column_gap: DimensionValue | None = None, row_gap: DimensionValue | None = None, - **props: Any, + key: str | None = None, ): """ Base Flex component for laying out children in a flexbox. @@ -39,6 +39,7 @@ def flex( gap: The space to display between both rows and columns of children. column_gap: The space to display between columns of children. row_gap: The space to display between rows of children. + key: A unique identifier used by React to render elements in a list """ return component_element( "Flex", @@ -52,5 +53,5 @@ def flex( gap=gap, column_gap=column_gap, row_gap=row_gap, - **props, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/form.py b/plugins/ui/src/deephaven/ui/components/form.py index 3ea71cabf..ad107ef5e 100644 --- a/plugins/ui/src/deephaven/ui/components/form.py +++ b/plugins/ui/src/deephaven/ui/components/form.py @@ -88,6 +88,7 @@ def form( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> Element: """ Forms allow users to enter data that can be submitted while providing alignment and styling for form fields @@ -158,6 +159,7 @@ def form( aria_details: The details for the element. UNSAFE_class_name: A CSS class to apply to the element. UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. """ return component_element( "Form", @@ -226,4 +228,5 @@ def form( aria_details=aria_details, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/fragment.py b/plugins/ui/src/deephaven/ui/components/fragment.py index dd963d2fc..1086407a0 100644 --- a/plugins/ui/src/deephaven/ui/components/fragment.py +++ b/plugins/ui/src/deephaven/ui/components/fragment.py @@ -4,12 +4,13 @@ from .basic import component_element -def fragment(*children: Any): +def fragment(*children: Any, key: str | None = None): """ A React.Fragment: https://react.dev/reference/react/Fragment. Used to group elements together without a wrapper node. Args: *children: The children in the fragment. + key: A unique identifier used by React to render elements in a list. """ - return component_element("Fragment", children=children) + return component_element("Fragment", children=children, key=key) diff --git a/plugins/ui/src/deephaven/ui/components/grid.py b/plugins/ui/src/deephaven/ui/components/grid.py index 86b4480d1..c82cef999 100644 --- a/plugins/ui/src/deephaven/ui/components/grid.py +++ b/plugins/ui/src/deephaven/ui/components/grid.py @@ -72,6 +72,7 @@ def grid( id: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> Element: """ A layout container using CSS grid. Supports Spectrum dimensions as values to ensure consistent and adaptive sizing and spacing. @@ -130,6 +131,7 @@ def grid( id: The unique identifier of the element. UNSAFE_class_name: A CSS class to apply to the element. UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. """ return component_element( "Grid", @@ -186,4 +188,5 @@ def grid( id=id, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/heading.py b/plugins/ui/src/deephaven/ui/components/heading.py index 1e1e9f3e0..3f770694d 100644 --- a/plugins/ui/src/deephaven/ui/components/heading.py +++ b/plugins/ui/src/deephaven/ui/components/heading.py @@ -58,6 +58,7 @@ def heading( id: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> Element: """ A layout container using CSS grid. Supports Spectrum dimensions as values to ensure consistent and adaptive sizing and spacing. @@ -105,6 +106,7 @@ def heading( id: The unique identifier of the element. UNSAFE_class_name: A CSS class to apply to the element. UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. """ return component_element( "Heading", @@ -150,4 +152,5 @@ def heading( id=id, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/icon.py b/plugins/ui/src/deephaven/ui/components/icon.py index caa469a35..e3b0c4981 100644 --- a/plugins/ui/src/deephaven/ui/components/icon.py +++ b/plugins/ui/src/deephaven/ui/components/icon.py @@ -65,6 +65,7 @@ def icon( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> Element: """ Get a Deephaven icon by name. @@ -113,6 +114,7 @@ def icon( id: The unique identifier of the element. UNSAFE_class_name: Set the CSS className for the element. Only use as a last resort. Use style props instead. UNSAFE_style: Set the inline style for the element. Only use as a last resort. Use style props instead. + key: A unique identifier used by React to render elements in a list. """ children, props = create_props(locals()) diff --git a/plugins/ui/src/deephaven/ui/components/illustrated_message.py b/plugins/ui/src/deephaven/ui/components/illustrated_message.py index 9d0317ff8..4e284c6d7 100644 --- a/plugins/ui/src/deephaven/ui/components/illustrated_message.py +++ b/plugins/ui/src/deephaven/ui/components/illustrated_message.py @@ -53,6 +53,7 @@ def illustrated_message( id: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> Element: """ An IllustratedMessage displays an illustration and a message, usually for an empty state or an error page. @@ -98,6 +99,7 @@ def illustrated_message( id: The unique identifier of the element. UNSAFE_class_name: Set the CSS className for the element. Only use as a last resort. Use style props instead. UNSAFE_style: Set the inline style for the element. Only use as a last resort. Use style props instead. + key: A unique identifier used by React to render elements in a list. Returns: The rendered IllustratedMessage component. @@ -145,4 +147,5 @@ def illustrated_message( id=id, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/image.py b/plugins/ui/src/deephaven/ui/components/image.py index 0e885d0c0..99a75d81e 100644 --- a/plugins/ui/src/deephaven/ui/components/image.py +++ b/plugins/ui/src/deephaven/ui/components/image.py @@ -59,6 +59,7 @@ def image( id: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> Element: """ Image is used to insert and display an image within a component. @@ -108,6 +109,7 @@ def image( id: The unique identifier of the element. UNSAFE_class_name: A CSS class to apply to the element. UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. Returns: The rendered Image element. @@ -160,4 +162,5 @@ def image( id=id, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/item.py b/plugins/ui/src/deephaven/ui/components/item.py index 282f4c18d..b12aa953a 100644 --- a/plugins/ui/src/deephaven/ui/components/item.py +++ b/plugins/ui/src/deephaven/ui/components/item.py @@ -16,6 +16,7 @@ def item( title: str | None = None, text_value: str | None = None, aria_label: str | None = None, + key: str | None = None, **props: Any, ) -> ItemElement: """ @@ -26,6 +27,7 @@ def item( title: Rendered contents of the item if `children` contains child items. text_value: A string representation of the item's contents, used for features like typeahead. aria_label: An accessibility label for this item. + key: A unique identifier used by React to render elements in a list. **props: Any other Item prop. """ children, props = create_props(locals()) diff --git a/plugins/ui/src/deephaven/ui/components/item_table_source.py b/plugins/ui/src/deephaven/ui/components/item_table_source.py index f9d580396..c26947ed6 100644 --- a/plugins/ui/src/deephaven/ui/components/item_table_source.py +++ b/plugins/ui/src/deephaven/ui/components/item_table_source.py @@ -32,6 +32,7 @@ def item_table_source( icon_column: ColumnName | None = None, title_column: ColumnName | None = None, actions: ListActionGroupElement | ListActionMenuElement | None = None, + key: str | None = None, ) -> ItemTableSource: """ An item table source wraps a Table or PartitionedTable to provide additional information for @@ -58,6 +59,8 @@ def item_table_source( If not specified, the section titles will be created from the key_columns of the PartitionedTable. actions: The action group or menus to render for all elements within the component, if supported. + key: + A unique identifier used by React to render elements in a list. Returns: The item table source to pass as a child to a component that supports it. diff --git a/plugins/ui/src/deephaven/ui/components/list_action_group.py b/plugins/ui/src/deephaven/ui/components/list_action_group.py index 45831594c..6293c9a67 100644 --- a/plugins/ui/src/deephaven/ui/components/list_action_group.py +++ b/plugins/ui/src/deephaven/ui/components/list_action_group.py @@ -93,6 +93,7 @@ def list_action_group( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> ListActionGroupElement: """ A group of action buttons that can be used to create a list of actions. @@ -162,6 +163,7 @@ def list_action_group( aria-details: Identifies the element (or elements) that provide a detailed, extended description for the object. UNSAFE_class_name: Set the CSS className for the element. Only use as a last resort. Use style props instead. UNSAFE_style: Set the inline style for the element. Only use as a last resort. Use style props instead. + key: A unique identifier used by React to render elements in a list. Returns: A ListActionGroup that can be used within the actions prop of a `ui.list_view` component. @@ -230,4 +232,5 @@ def list_action_group( aria_details=aria_details, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/list_action_menu.py b/plugins/ui/src/deephaven/ui/components/list_action_menu.py index d61d18bf1..899bedc49 100644 --- a/plugins/ui/src/deephaven/ui/components/list_action_menu.py +++ b/plugins/ui/src/deephaven/ui/components/list_action_menu.py @@ -81,6 +81,7 @@ def list_action_menu( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> ListActionMenuElement: """ A menu of action buttons that can be used to create a list of actions. @@ -146,6 +147,7 @@ def list_action_menu( aria-details: Identifies the element (or elements) that provide a detailed, extended description for the object. UNSAFE_class_name: Set the CSS className for the element. Only use as a last resort. Use style props instead. UNSAFE_style: Set the inline style for the element. Only use as a last resort. Use style props instead. + key: A unique identifier used by React to render elements in a list. Returns: A ListActionMenu that can be used within the actions prop of a `ui.list_view` component. """ @@ -209,4 +211,5 @@ def list_action_menu( aria_details=aria_details, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/list_view.py b/plugins/ui/src/deephaven/ui/components/list_view.py index 173cedb3a..365a40385 100644 --- a/plugins/ui/src/deephaven/ui/components/list_view.py +++ b/plugins/ui/src/deephaven/ui/components/list_view.py @@ -31,6 +31,7 @@ def list_view( render_empty_state: Element | None = None, on_selection_change: Callable[[Selection], None] | None = None, on_change: Callable[[Selection], None] | None = None, + key: str | None = None, **props: Any, ) -> ListViewElement: """ @@ -61,6 +62,8 @@ def list_view( Handler that is called when the selection changes. on_change: Alias of `on_selection_change`. Handler that is called when the selection changes. + key: + A unique identifier used by React to render elements in a list. **props: Any other ListView prop, except items, dragAndDropHooks, and onLoadMore. diff --git a/plugins/ui/src/deephaven/ui/components/make_component.py b/plugins/ui/src/deephaven/ui/components/make_component.py index b7e43c652..4d045b349 100644 --- a/plugins/ui/src/deephaven/ui/components/make_component.py +++ b/plugins/ui/src/deephaven/ui/components/make_component.py @@ -1,3 +1,4 @@ +from __future__ import annotations import functools import logging from typing import Any, Callable @@ -17,9 +18,8 @@ def make_component(func: Callable[..., Any]): """ @functools.wraps(func) - def make_component_node(*args: Any, **kwargs: Any): + def make_component_node(*args: Any, key: str | None = None, **kwargs: Any): component_type = get_component_qualname(func) - - return FunctionElement(component_type, lambda: func(*args, **kwargs)) + return FunctionElement(component_type, lambda: func(*args, **kwargs), key=key) return make_component_node diff --git a/plugins/ui/src/deephaven/ui/components/number_field.py b/plugins/ui/src/deephaven/ui/components/number_field.py index 8fff0e919..b57b29d40 100644 --- a/plugins/ui/src/deephaven/ui/components/number_field.py +++ b/plugins/ui/src/deephaven/ui/components/number_field.py @@ -97,6 +97,7 @@ def number_field( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, # missing properties that are clipboard or composition events ) -> Element: """ @@ -175,6 +176,7 @@ def number_field( aria_details: The id of the element that provides additional information about the current element. UNSAFE_class_name: A CSS class to apply to the element. UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. """ return component_element( @@ -251,4 +253,5 @@ def number_field( aria_details=aria_details, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/panel.py b/plugins/ui/src/deephaven/ui/components/panel.py index db538aa1b..9faead210 100644 --- a/plugins/ui/src/deephaven/ui/components/panel.py +++ b/plugins/ui/src/deephaven/ui/components/panel.py @@ -31,6 +31,7 @@ def panel( padding_end: DimensionValue | None = None, padding_x: DimensionValue | None = None, padding_y: DimensionValue | None = None, + key: str | None = None, **props: Any, ): """ @@ -54,6 +55,7 @@ def panel( padding_end: The padding to apply after the element. padding_x: The padding to apply to the left and right of the element. padding_y: The padding to apply to the top and bottom of the element. + key: A unique identifier used by React to render elements in a list. """ diff --git a/plugins/ui/src/deephaven/ui/components/picker.py b/plugins/ui/src/deephaven/ui/components/picker.py index b752b7e18..214ef5e13 100644 --- a/plugins/ui/src/deephaven/ui/components/picker.py +++ b/plugins/ui/src/deephaven/ui/components/picker.py @@ -115,6 +115,7 @@ def picker( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> PickerElement: """ A picker that can be used to select from a list. Children should be one of five types: @@ -215,6 +216,7 @@ def picker( aria_details: The details for the element. UNSAFE_class_name: A CSS class to apply to the element. UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. Returns: diff --git a/plugins/ui/src/deephaven/ui/components/radio.py b/plugins/ui/src/deephaven/ui/components/radio.py index baf245175..0c8b095f2 100644 --- a/plugins/ui/src/deephaven/ui/components/radio.py +++ b/plugins/ui/src/deephaven/ui/components/radio.py @@ -68,6 +68,7 @@ def radio( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> Element: """ Radio buttons allow users to select a single option from a list of mutually @@ -127,6 +128,7 @@ def radio( aria-details: Identifies the element (or elements) that provide a detailed, extended description for the object. UNSAFE_class_name: Set the CSS className for the element. Only use as a last resort. Use style props instead. UNSAFE_style: Set the inline style for the element. Only use as a last resort. Use style props instead. + key: A unique identifier used by React to render elements in a list. """ diff --git a/plugins/ui/src/deephaven/ui/components/radio_group.py b/plugins/ui/src/deephaven/ui/components/radio_group.py index d699a00f9..1ae0a1849 100644 --- a/plugins/ui/src/deephaven/ui/components/radio_group.py +++ b/plugins/ui/src/deephaven/ui/components/radio_group.py @@ -91,6 +91,7 @@ def radio_group( aria_errormessage: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> Element: """ Radio buttons allow users to select a single option from a list of mutually @@ -165,6 +166,7 @@ def radio_group( aria_errormessage: Identifies the element that provides an error message for the object. UNSAFE_class_name: Set the CSS className for the element. Only use as a last resort. Use style props instead. UNSAFE_style: Set the inline style for the element. Only use as a last resort. Use style props instead. + key: A unique identifier used by React to render elements in a list. Returns: The rendered radio group component. diff --git a/plugins/ui/src/deephaven/ui/components/range_slider.py b/plugins/ui/src/deephaven/ui/components/range_slider.py index b6aab50ae..6e69e071f 100644 --- a/plugins/ui/src/deephaven/ui/components/range_slider.py +++ b/plugins/ui/src/deephaven/ui/components/range_slider.py @@ -80,6 +80,7 @@ def range_slider( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> Element: """ Sliders allow users to quickly select a value within a range. They should be used when the upper and lower bounds to the range are invariable. @@ -144,6 +145,7 @@ def range_slider( aria_details: The id of the element that provides additional information about the current element. UNSAFE_class_name: A CSS class to apply to the element. UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. Returns: The rendered range slider component. @@ -210,4 +212,5 @@ def range_slider( aria_details=aria_details, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/row.py b/plugins/ui/src/deephaven/ui/components/row.py index 33f7bfd1b..801ef063d 100644 --- a/plugins/ui/src/deephaven/ui/components/row.py +++ b/plugins/ui/src/deephaven/ui/components/row.py @@ -4,7 +4,7 @@ from .basic import component_element -def row(*children: Any, height: float | None = None, **kwargs: Any): +def row(*children: Any, height: float | None = None, key: str | None = None): """ A row is a container that can be used to group elements. Each element will be placed to the right of its prior sibling. @@ -12,5 +12,6 @@ def row(*children: Any, height: float | None = None, **kwargs: Any): Args: *children: Elements to render in the row. height: The percent height of the row relative to other children of its parent. If not provided, the row will be sized automatically. + key: A unique identifier used by React to render elements in a list. """ - return component_element("Row", *children, height=height, **kwargs) + return component_element("Row", *children, height=height, key=key) diff --git a/plugins/ui/src/deephaven/ui/components/section.py b/plugins/ui/src/deephaven/ui/components/section.py index 3aa17674a..058ec76a8 100644 --- a/plugins/ui/src/deephaven/ui/components/section.py +++ b/plugins/ui/src/deephaven/ui/components/section.py @@ -10,14 +10,16 @@ SectionElement = Element -def section(*children: Item, title: str | None = None, **props: Any) -> SectionElement: +def section( + *children: Item, title: str | None = None, key: str | None = None +) -> SectionElement: """ A section that can be added to a menu, such as a picker. Children are the dropdown options. Args: *children: The options to render within the section. title: The title of the section. - **props: Any other Section prop. + key: A unique identifier used by React to render elements in a list. Returns: The rendered Section. @@ -25,4 +27,4 @@ def section(*children: Item, title: str | None = None, **props: Any) -> SectionE children, props = create_props(locals()) - return component_element("Section", *children, **props) + return component_element("Section", *children) diff --git a/plugins/ui/src/deephaven/ui/components/slider.py b/plugins/ui/src/deephaven/ui/components/slider.py index 36ea10800..71a679286 100644 --- a/plugins/ui/src/deephaven/ui/components/slider.py +++ b/plugins/ui/src/deephaven/ui/components/slider.py @@ -79,6 +79,7 @@ def slider( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> Element: """ Sliders allow users to quickly select a value within a range. They should be used when the upper and lower bounds to the range are invariable. @@ -144,6 +145,7 @@ def slider( aria_details: The id of the element that provides additional information about the current element. UNSAFE_class_name: A CSS class to apply to the element. UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. Returns: The rendered slider component. @@ -211,4 +213,5 @@ def slider( aria_details=aria_details, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/stack.py b/plugins/ui/src/deephaven/ui/components/stack.py index b3e2f3779..b5cd93e40 100644 --- a/plugins/ui/src/deephaven/ui/components/stack.py +++ b/plugins/ui/src/deephaven/ui/components/stack.py @@ -9,6 +9,7 @@ def stack( height: float | None = None, width: float | None = None, activeItemIndex: int | None = None, + key: str | None = None, **kwargs: Any, ): """ @@ -19,6 +20,7 @@ def stack( *children: Elements to render in the row. height: The percent height of the stack relative to other children of its parent. If not provided, the stack will be sized automatically. width: The percent width of the stack relative to other children of its parent. If not provided, the stack will be sized automatically. + key: A unique identifier used by React to render elements in a list. """ return component_element( "Stack", @@ -26,5 +28,6 @@ def stack( height=height, width=width, activeItemIndex=activeItemIndex, + key=key, **kwargs, ) diff --git a/plugins/ui/src/deephaven/ui/components/switch.py b/plugins/ui/src/deephaven/ui/components/switch.py index 2e8c78f56..ff5642a50 100644 --- a/plugins/ui/src/deephaven/ui/components/switch.py +++ b/plugins/ui/src/deephaven/ui/components/switch.py @@ -76,6 +76,7 @@ def switch( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> Element: """ Switches allow users to turn an individual option on or off. They are usually used to activate or deactivate a specific setting. @@ -142,6 +143,7 @@ def switch( aria_details: The details for the element. UNSAFE_class_name: A CSS class to apply to the element. UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. Returns: The rendered switch element. @@ -210,4 +212,5 @@ def switch( aria_details=aria_details, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/tab_list.py b/plugins/ui/src/deephaven/ui/components/tab_list.py index 15e75c8fb..941611a33 100644 --- a/plugins/ui/src/deephaven/ui/components/tab_list.py +++ b/plugins/ui/src/deephaven/ui/components/tab_list.py @@ -54,6 +54,7 @@ def tab_list( id: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ): """ Python implementation for the Adobe React Spectrum TabList component. @@ -94,6 +95,7 @@ def tab_list( id: The unique identifier of the element. UNSAFE_class_name: Set the CSS className for the element. Only use as a last resort. Use style props instead. UNSAFE_style: Set the inline style for the element. Only use as a last resort. Use style props instead. + key: A unique identifier used by React to render elements in a list. """ if not children: raise ValueError("Tab Lists must have at least one child.") @@ -140,4 +142,5 @@ def tab_list( id=id, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/tab_panels.py b/plugins/ui/src/deephaven/ui/components/tab_panels.py index 604a5117b..e6b80a1e5 100644 --- a/plugins/ui/src/deephaven/ui/components/tab_panels.py +++ b/plugins/ui/src/deephaven/ui/components/tab_panels.py @@ -54,6 +54,7 @@ def tab_panels( id: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ): """ Python implementation for the Adobe React Spectrum TabPanels component. @@ -94,6 +95,7 @@ def tab_panels( id: The unique identifier of the element. UNSAFE_class_name: Set the CSS className for the element. Only use as a last resort. Use style props instead. UNSAFE_style: Set the inline style for the element. Only use as a last resort. Use style props instead. + key: A unique identifier used by React to render elements in a list. """ if not children: raise ValueError("Tab Panels must have at least one child.") @@ -140,4 +142,5 @@ def tab_panels( id=id, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/table.py b/plugins/ui/src/deephaven/ui/components/table.py index 2e8781fe1..96f274528 100644 --- a/plugins/ui/src/deephaven/ui/components/table.py +++ b/plugins/ui/src/deephaven/ui/components/table.py @@ -42,6 +42,7 @@ def table( ResolvableContextMenuItem | list[ResolvableContextMenuItem] | None ) = None, databars: list[DatabarConfig] | None = None, + key: str | None = None, ) -> UITable: """ Customization to how a table is displayed, how it behaves, and listen to UI events. @@ -86,6 +87,7 @@ def table( 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. databars: Databars are experimental and will be moved to column_formatting in the future. + key: A unique identifier used by React to render elements in a list. Returns: The rendered Table. diff --git a/plugins/ui/src/deephaven/ui/components/tabs.py b/plugins/ui/src/deephaven/ui/components/tabs.py index a4350cf02..29832f75e 100644 --- a/plugins/ui/src/deephaven/ui/components/tabs.py +++ b/plugins/ui/src/deephaven/ui/components/tabs.py @@ -74,6 +74,7 @@ def tabs( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ): """ Python implementation for the Adobe React Spectrum Tabs component. @@ -133,6 +134,7 @@ def tabs( aria_details: Identifies the element (or elements) that provide a detailed, extended description for the object. UNSAFE_class_name: Set the CSS className for the element. Only use as a last resort. Use style props instead. UNSAFE_style: Set the inline style for the element. Only use as a last resort. Use style props instead. + key: A unique identifier used by React to render elements in a list. """ if not children: raise ValueError("Tabs must have at least one child.") @@ -215,4 +217,5 @@ def tabs( aria_details=aria_details, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/text.py b/plugins/ui/src/deephaven/ui/components/text.py index 15740a9e6..7b5e7a5af 100644 --- a/plugins/ui/src/deephaven/ui/components/text.py +++ b/plugins/ui/src/deephaven/ui/components/text.py @@ -56,6 +56,7 @@ def text( id: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> Element: """ Text represents text with no specific semantic meaning. @@ -103,6 +104,7 @@ def text( id: The unique identifier of the element. UNSAFE_class_name: A CSS class to apply to the element. UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. """ return component_element( "Text", @@ -148,4 +150,5 @@ def text( id=id, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/text_area.py b/plugins/ui/src/deephaven/ui/components/text_area.py index 727b6b7d9..69f1b204d 100644 --- a/plugins/ui/src/deephaven/ui/components/text_area.py +++ b/plugins/ui/src/deephaven/ui/components/text_area.py @@ -105,6 +105,7 @@ def text_area( aria_errormessage: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, # missing properties that are clipboard or composition events ) -> Element: """ @@ -186,6 +187,7 @@ def text_area( aria_errormessage: The id of the element that provides an error message for the current element. UNSAFE_class_name: A CSS class to apply to the element. UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. Returns: The element representing the text area @@ -268,4 +270,5 @@ def text_area( aria_errormessage=aria_errormessage, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/text_field.py b/plugins/ui/src/deephaven/ui/components/text_field.py index bb4113f59..83f3b1a26 100644 --- a/plugins/ui/src/deephaven/ui/components/text_field.py +++ b/plugins/ui/src/deephaven/ui/components/text_field.py @@ -104,6 +104,7 @@ def text_field( aria_errormessage: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, # missing properties that are clipboard or composition events ) -> Element: """ @@ -187,6 +188,7 @@ def text_field( aria_errormessage: The id of the element that provides an error message for the current element. UNSAFE_class_name: A CSS class to apply to the element. UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. Returns: The rendered text field element. @@ -271,4 +273,5 @@ def text_field( aria_errormessage=aria_errormessage, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/toggle_button.py b/plugins/ui/src/deephaven/ui/components/toggle_button.py index a9a16c018..ebd49984c 100644 --- a/plugins/ui/src/deephaven/ui/components/toggle_button.py +++ b/plugins/ui/src/deephaven/ui/components/toggle_button.py @@ -92,6 +92,7 @@ def toggle_button( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> Element: """ ToggleButtons allow users to toggle a selection on or off, for example switching between two states or modes. @@ -165,6 +166,7 @@ def toggle_button( aria_details: The details for the element. UNSAFE_class_name: A CSS class to apply to the element. UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. """ return component_element( @@ -237,4 +239,5 @@ def toggle_button( aria_details=aria_details, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/view.py b/plugins/ui/src/deephaven/ui/components/view.py index 1331ce1e6..0511048d5 100644 --- a/plugins/ui/src/deephaven/ui/components/view.py +++ b/plugins/ui/src/deephaven/ui/components/view.py @@ -86,6 +86,7 @@ def view( id: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> Element: """ View is a general purpose container with no specific semantics that can be used for custom styling purposes. It supports Spectrum style props to ensure consistency with other Spectrum components. @@ -162,6 +163,7 @@ def view( id: The unique identifier of the element. UNSAFE_class_name: A CSS class to apply to the element. UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. Returns: The rendered view. @@ -238,4 +240,5 @@ def view( id=id, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/elements/BaseElement.py b/plugins/ui/src/deephaven/ui/elements/BaseElement.py index e23cf7d9e..ea6bde2bb 100644 --- a/plugins/ui/src/deephaven/ui/elements/BaseElement.py +++ b/plugins/ui/src/deephaven/ui/elements/BaseElement.py @@ -11,8 +11,13 @@ class BaseElement(Element): Must provide a name for the element. """ - def __init__(self, name: str, /, *children: Any, **props: Any): + def __init__( + self, name: str, /, *children: Any, key: str | None = None, **props: Any + ): self._name = name + self._key = key + props["key"] = key + if len(children) > 0 and props.get("children") is not None: raise ValueError("Cannot provide both children and props.children") @@ -28,5 +33,9 @@ def __init__(self, name: str, /, *children: Any, **props: Any): def name(self) -> str: return self._name + @property + def key(self) -> str | None: + return self._key + def render(self, context: RenderContext) -> dict[str, Any]: return self._props diff --git a/plugins/ui/src/deephaven/ui/elements/Element.py b/plugins/ui/src/deephaven/ui/elements/Element.py index 810f52fea..fe6a168f9 100644 --- a/plugins/ui/src/deephaven/ui/elements/Element.py +++ b/plugins/ui/src/deephaven/ui/elements/Element.py @@ -22,6 +22,16 @@ def name(self) -> str: """ return "deephaven.ui.Element" + @property + def key(self) -> str | None: + """ + Get the key prop of this element. Useful to check if a key prop was provided. + + Returns: + The unique key prop of this element. + """ + return None + @abstractmethod def render(self, context: RenderContext) -> PropsType: """ diff --git a/plugins/ui/src/deephaven/ui/elements/FunctionElement.py b/plugins/ui/src/deephaven/ui/elements/FunctionElement.py index bcf63188c..6b1ea35f9 100644 --- a/plugins/ui/src/deephaven/ui/elements/FunctionElement.py +++ b/plugins/ui/src/deephaven/ui/elements/FunctionElement.py @@ -8,7 +8,9 @@ class FunctionElement(Element): - def __init__(self, name: str, render: Callable[[], list[Element]]): + def __init__( + self, name: str, render: Callable[[], list[Element]], key: str | None = None + ): """ Create an element that takes a function to render. @@ -18,11 +20,16 @@ def __init__(self, name: str, render: Callable[[], list[Element]]): """ self._name = name self._render = render + self._key = key @property def name(self): return self._name + @property + def key(self) -> str | None: + return self._key + def render(self, context: RenderContext) -> PropsType: """ Render the component. Should only be called when actually rendering the component, e.g. exporting it to the client. diff --git a/plugins/ui/src/deephaven/ui/elements/UITable.py b/plugins/ui/src/deephaven/ui/elements/UITable.py index 77680f052..24e909984 100644 --- a/plugins/ui/src/deephaven/ui/elements/UITable.py +++ b/plugins/ui/src/deephaven/ui/elements/UITable.py @@ -124,11 +124,16 @@ def __init__( # Store all the props that were passed in self._props = UITableProps(**props, table=table) + self._key = props.get("key") @property def name(self): return "deephaven.ui.elements.UITable" + @property + def key(self) -> str | None: + return self._key + def _with_prop(self, key: str, value: Any) -> "UITable": """ Create a new UITable with the passed in prop added to the existing props diff --git a/plugins/ui/src/deephaven/ui/renderer/Renderer.py b/plugins/ui/src/deephaven/ui/renderer/Renderer.py index 496f5e7ed..f811fb375 100644 --- a/plugins/ui/src/deephaven/ui/renderer/Renderer.py +++ b/plugins/ui/src/deephaven/ui/renderer/Renderer.py @@ -22,11 +22,11 @@ def _get_context_key(item: Any, index_key: str) -> Union[str, None]: - If `item` is an `Element` generate a key based on the `index_key` and the `name` of the `Element`. - If the item is another iterable, just return the `index_key`. - Otherwise, return `None` as the key. - - TODO #731: use a `key` prop if it exists on the `Element`. """ if isinstance(item, Element): - return f"{index_key}-{item.name}" - if isinstance(item, (Dict, List, Tuple)): + key = item.key + return key if key is not None else f"{index_key}-{item.name}" + if isinstance(item, (Dict, List, Tuple, map)): return index_key return None @@ -61,7 +61,7 @@ def _render_item(item: Any, context: RenderContext) -> Any: The rendered item. """ logger.debug("_render_item context is %s", context) - if isinstance(item, (list, tuple)): + if isinstance(item, (list, map, tuple)): # I couldn't figure out how to map a `list[Unknown]` to a `list[Any]`, or to accept a `list[Unknown]` as a parameter return _render_list(item, context) # type: ignore if isinstance(item, dict): diff --git a/tests/app.d/ui.py b/tests/app.d/ui.py index 290ec12d8..61990a1a5 100644 --- a/tests/app.d/ui.py +++ b/tests/app.d/ui.py @@ -1,4 +1,5 @@ from deephaven import ui +from itertools import count @ui.component @@ -31,6 +32,44 @@ def ui_boom_counter_component(): return ui.button(f"Count is {value}", on_press=lambda _: set_value(value + 1)) +@ui.component +def ui_cell(label: str = "Cell"): + text, set_text = ui.use_state("") + + return ui.text_field(label=label, value=text, on_change=set_text) + + +@ui.component +def ui_cells(): + id_iter, _ = ui.use_state(lambda: count()) + cells, set_cells = ui.use_state(lambda: [next(id_iter)]) + + def add_cell(): + set_cells(lambda old_cells: old_cells + [next(id_iter)]) + + def delete_cell(delete_id: int): + set_cells(lambda old_cells: [c for c in old_cells if c != delete_id]) + + return ui.view( + map( + lambda i: ui.flex( + ui_cell(label=f"Cell {i}"), + ui.action_button( + ui.icon("vsTrash"), + aria_label="Delete cell", + on_press=lambda: delete_cell(i), + ), + align_items="end", + key=str(i), + ), + cells, + ), + ui.action_button(ui.icon("vsAdd"), "Add cell", on_press=add_cell), + overflow="auto", + ) + + ui_component = ui_basic_component() ui_boom = ui_boom_component() ui_boom_counter = ui_boom_counter_component() +ui_cells = ui_cells() diff --git a/tests/ui.spec.ts b/tests/ui.spec.ts index c22138e73..173e97e63 100644 --- a/tests/ui.spec.ts +++ b/tests/ui.spec.ts @@ -48,6 +48,25 @@ test('boom counter component shows error overlay after clicking the button twice await expect(panelLocator.getByText('BOOM! Value too big.')).toBeVisible(); }); +test('Using keys for lists works', async ({ page }) => { + await gotoPage(page, ''); + await openPanel(page, 'ui_cells', selector.REACT_PANEL_VISIBLE); + + // setup cells + await page.getByRole('button', { name: 'Add cell' }).click(); + await page.getByRole('button', { name: 'Add cell' }).click(); + await page.getByRole('textbox', { name: 'Cell 0' }).fill('a'); + await page.getByRole('textbox', { name: 'Cell 1' }).fill('b'); + await page.getByRole('textbox', { name: 'Cell 2' }).fill('c'); + await page.getByRole('button', { name: 'Delete cell' }).nth(1).click(); + + expect(page.getByRole('textbox', { name: 'Cell 0' })).toBeVisible(); + expect(page.getByRole('textbox', { name: 'Cell 0' })).toHaveValue('a'); + expect(page.getByRole('textbox', { name: 'Cell 1' })).not.toBeVisible(); + expect(page.getByRole('textbox', { name: 'Cell 2' })).toBeVisible(); + expect(page.getByRole('textbox', { name: 'Cell 2' })).toHaveValue('c'); +}); + test('UI all components render 1', async ({ page }) => { await gotoPage(page, ''); await openPanel(page, 'ui_render_all1', selector.REACT_PANEL_VISIBLE); From b780b27a946c8ab6683eaec2377e9d5e64c324a8 Mon Sep 17 00:00:00 2001 From: Steven Wu Date: Fri, 20 Sep 2024 14:13:54 -0400 Subject: [PATCH 10/18] fix: empty list view (#828) - Fixes #744 - Update props types to convert `renderEmptyState` from `Element` in Python to `() => Element` in TypeScript - Throw error if `children` is undefined --- plugins/ui/src/js/src/elements/ListView.tsx | 3 +++ .../js/src/elements/hooks/useListViewProps.ts | 18 ++++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/plugins/ui/src/js/src/elements/ListView.tsx b/plugins/ui/src/js/src/elements/ListView.tsx index 21e052f84..a9db083df 100644 --- a/plugins/ui/src/js/src/elements/ListView.tsx +++ b/plugins/ui/src/js/src/elements/ListView.tsx @@ -3,6 +3,7 @@ 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 { assertNotNull } from '@deephaven/utils'; import { SerializedListViewProps, useListViewProps, @@ -17,6 +18,8 @@ export function ListView(props: SerializedListViewProps): JSX.Element | null { const isObjectView = isElementOfType(children, ObjectView); const table = useReExportedTable(children); + assertNotNull(children, 'Children must be defined for list_view.'); + if (isObjectView) { return ( table && ( diff --git a/plugins/ui/src/js/src/elements/hooks/useListViewProps.ts b/plugins/ui/src/js/src/elements/hooks/useListViewProps.ts index b7dcfcfb3..0ce81041b 100644 --- a/plugins/ui/src/js/src/elements/hooks/useListViewProps.ts +++ b/plugins/ui/src/js/src/elements/hooks/useListViewProps.ts @@ -16,20 +16,28 @@ type WrappedDHListViewJSApiProps = Omit & { children: ReactElement; }; -type WrappedDHListViewProps = Omit< +type SerializedDHListViewProps = Omit< DHListViewProps, - 'density' | 'selectionMode' + 'density' | 'renderEmptyState' | 'selectionMode' > & { // The dh UI spec specifies that density and selectionMode should be uppercase, // but the Spectrum props are lowercase. We'll accept either to keep things // more flexible. density?: Density | Uppercase; + renderEmptyState?: JSX.Element; selectionMode?: SelectionMode | Uppercase; }; +type SerializedDHListViewJSApiProps = Omit< + WrappedDHListViewJSApiProps, + 'renderEmptyState' +> & { + renderEmptyState?: JSX.Element; +}; + export type SerializedListViewProps = ( - | WrappedDHListViewProps - | WrappedDHListViewJSApiProps + | SerializedDHListViewProps + | SerializedDHListViewJSApiProps ) & SerializedSelectionProps; @@ -43,6 +51,7 @@ export function useListViewProps({ selectionMode: selectionModeMaybeUppercase, onChange: serializedOnChange, onSelectionChange: serializedOnSelectionChange, + renderEmptyState, ...otherProps }: SerializedListViewProps): DHListViewProps | WrappedDHListViewJSApiProps { const densityLc = densityMaybeUppercase?.toLowerCase() as Density | undefined; @@ -58,6 +67,7 @@ export function useListViewProps({ selectionMode, onChange, onSelectionChange, + renderEmptyState: renderEmptyState && (() => renderEmptyState), // The @deephaven/components `ListView` has its own normalization logic that // handles primitive children types (string, number, boolean). It also // handles nested children inside of `Item` and `Section` components, so From 23bcd526664e1292ef9c7e903234925da605e80b Mon Sep 17 00:00:00 2001 From: Matthew Runyon Date: Fri, 20 Sep 2024 13:43:53 -0500 Subject: [PATCH 11/18] build: Upgrade to Vite 5 (#899) Upgrades and centralizes the Vite version since we want to use the same version across all plugins --- package-lock.json | 2213 ++++++++++------- package.json | 3 +- plugins/auth-keycloak/src/js/package.json | 4 +- .../src/js/package.json | 4 +- plugins/example-theme/src/js/package.json | 3 +- plugins/matplotlib/src/js/package.json | 4 +- plugins/plotly-express/src/js/package.json | 4 +- plugins/table-example/src/js/package.json | 4 +- plugins/ui/src/js/package.json | 4 +- 9 files changed, 1348 insertions(+), 895 deletions(-) diff --git a/package-lock.json b/package-lock.json index 229136caf..0124c9306 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@types/jest": "^29.2.5", "@types/node": "^20.11.17", "@types/prop-types": "^15.7.10", + "@vitejs/plugin-react-swc": "^3.7.0", "eslint": "^8.37.0", "identity-obj-proxy": "^3.0.0", "jest": "^29.6.2", @@ -36,7 +37,7 @@ "npm-run-all": "^4.1.5", "nx": "15.9.2", "prettier": "3.0.0", - "vite": "~4.1.4" + "vite": "^5.4.6" }, "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", @@ -10060,6 +10061,19 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.0.tgz", + "integrity": "sha512-YJ5Ku5BmNJZb58A4qSEo3JlIG4d3G2lWyBi13ABlXzO41SsdnUKi3HQHe83VpwBVG4jHFTW65jOQb8qyoR+qzg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-arm64-gnu": { "version": "4.9.6", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz", @@ -10084,6 +10098,19 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.0.tgz", + "integrity": "sha512-9fx6Zj/7vve/Fp4iexUFRKb5+RjLCff6YTRQl4CoDhdMfDoobWmhAxQWV3NfShMzQk1Q/iCnageFyGfqnsmeqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { "version": "4.9.6", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz", @@ -10096,6 +10123,19 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.0.tgz", + "integrity": "sha512-EHmPnPWvyYqncObwqrosb/CpH3GOjE76vWVs0g4hWsDRUVhg61hBmlVg5TPXqF+g+PvIbqkC7i3h8wbn4Gp2Fg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.9.6", "cpu": [ @@ -10351,13 +10391,14 @@ } }, "node_modules/@swc/core": { - "version": "1.3.99", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.26.tgz", + "integrity": "sha512-f5uYFf+TmMQyYIoxkn/evWhNGuUzC730dFwAKGwBVHHVoPyak1/GvJUm6i1SKl+2Hrj9oN0i3WSoWWZ4pgI8lw==", "dev": true, "hasInstallScript": true, - "license": "Apache-2.0", "dependencies": { - "@swc/counter": "^0.1.1", - "@swc/types": "^0.1.5" + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.12" }, "engines": { "node": ">=10" @@ -10367,18 +10408,19 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.3.99", - "@swc/core-darwin-x64": "1.3.99", - "@swc/core-linux-arm64-gnu": "1.3.99", - "@swc/core-linux-arm64-musl": "1.3.99", - "@swc/core-linux-x64-gnu": "1.3.99", - "@swc/core-linux-x64-musl": "1.3.99", - "@swc/core-win32-arm64-msvc": "1.3.99", - "@swc/core-win32-ia32-msvc": "1.3.99", - "@swc/core-win32-x64-msvc": "1.3.99" + "@swc/core-darwin-arm64": "1.7.26", + "@swc/core-darwin-x64": "1.7.26", + "@swc/core-linux-arm-gnueabihf": "1.7.26", + "@swc/core-linux-arm64-gnu": "1.7.26", + "@swc/core-linux-arm64-musl": "1.7.26", + "@swc/core-linux-x64-gnu": "1.7.26", + "@swc/core-linux-x64-musl": "1.7.26", + "@swc/core-win32-arm64-msvc": "1.7.26", + "@swc/core-win32-ia32-msvc": "1.7.26", + "@swc/core-win32-x64-msvc": "1.7.26" }, "peerDependencies": { - "@swc/helpers": "^0.5.0" + "@swc/helpers": "*" }, "peerDependenciesMeta": { "@swc/helpers": { @@ -10416,6 +10458,22 @@ "node": ">=10" } }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.26.tgz", + "integrity": "sha512-VYPFVJDO5zT5U3RpCdHE5v1gz4mmR8BfHecUZTmD2v1JeFY6fv9KArJUpjrHEEsjK/ucXkQFmJ0jaiWXmpOV9Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, "node_modules/@swc/core-linux-arm64-gnu": { "version": "1.3.99", "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.99.tgz", @@ -10519,10 +10577,155 @@ "node": ">=10" } }, - "node_modules/@swc/counter": { - "version": "0.1.2", + "node_modules/@swc/core/node_modules/@swc/core-darwin-arm64": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.26.tgz", + "integrity": "sha512-FF3CRYTg6a7ZVW4yT9mesxoVVZTrcSWtmZhxKCYJX9brH4CS/7PRPjAKNk6kzWgWuRoglP7hkjQcd6EpMcZEAw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core/node_modules/@swc/core-darwin-x64": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.26.tgz", + "integrity": "sha512-az3cibZdsay2HNKmc4bjf62QVukuiMRh5sfM5kHR/JMTrLyS6vSw7Ihs3UTkZjUxkLTT8ro54LI6sV6sUQUbLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core/node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.26.tgz", + "integrity": "sha512-YKevOV7abpjcAzXrhsl+W48Z9mZvgoVs2eP5nY+uoMAdP2b3GxC0Df1Co0I90o2lkzO4jYBpTMcZlmUXLdXn+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core/node_modules/@swc/core-linux-arm64-musl": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.26.tgz", + "integrity": "sha512-3w8iZICMkQQON0uIcvz7+Q1MPOW6hJ4O5ETjA0LSP/tuKqx30hIniCGOgPDnv3UTMruLUnQbtBwVCZTBKR3Rkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core/node_modules/@swc/core-linux-x64-gnu": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.26.tgz", + "integrity": "sha512-c+pp9Zkk2lqb06bNGkR2Looxrs7FtGDMA4/aHjZcCqATgp348hOKH5WPvNLBl+yPrISuWjbKDVn3NgAvfvpH4w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core/node_modules/@swc/core-linux-x64-musl": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.26.tgz", + "integrity": "sha512-PgtyfHBF6xG87dUSSdTJHwZ3/8vWZfNIXQV2GlwEpslrOkGqy+WaiiyE7Of7z9AvDILfBBBcJvJ/r8u980wAfQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core/node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.26.tgz", + "integrity": "sha512-9TNXPIJqFynlAOrRD6tUQjMq7KApSklK3R/tXgIxc7Qx+lWu8hlDQ/kVPLpU7PWvMMwC/3hKBW+p5f+Tms1hmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core/node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.26.tgz", + "integrity": "sha512-9YngxNcG3177GYdsTum4V98Re+TlCeJEP4kEwEg9EagT5s3YejYdKwVAkAsJszzkXuyRDdnHUpYbTrPG6FiXrQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core/node_modules/@swc/core-win32-x64-msvc": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.26.tgz", + "integrity": "sha512-VR+hzg9XqucgLjXxA13MtV5O3C0bK0ywtLIBw/+a+O+Oc6mxFWHtdUeXDbIi5AiPbn0fjgVJMqYnyjGyyX8u0w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true }, "node_modules/@swc/helpers": { "version": "0.5.3", @@ -10532,9 +10735,13 @@ } }, "node_modules/@swc/types": { - "version": "0.1.5", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.12.tgz", + "integrity": "sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==", "dev": true, - "license": "Apache-2.0" + "dependencies": { + "@swc/counter": "^0.1.3" + } }, "node_modules/@testing-library/dom": { "version": "10.1.0", @@ -11011,8 +11218,9 @@ }, "node_modules/@types/estree": { "version": "1.0.5", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true }, "node_modules/@types/graceful-fs": { "version": "4.1.9", @@ -11558,11 +11766,12 @@ "license": "ISC" }, "node_modules/@vitejs/plugin-react-swc": { - "version": "3.5.0", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.0.tgz", + "integrity": "sha512-yrknSb3Dci6svCd/qhHqhFPDSw0QtjumcqdKMoNNzmOl5lMXTTiqzjWtG4Qask2HdvvzaNgSunbQGet8/GrKdA==", "dev": true, - "license": "MIT", "dependencies": { - "@swc/core": "^1.3.96" + "@swc/core": "^1.5.7" }, "peerDependencies": { "vite": "^4 || ^5" @@ -14597,10 +14806,11 @@ } }, "node_modules/esbuild": { - "version": "0.16.17", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, - "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -14608,28 +14818,397 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.16.17", - "@esbuild/android-arm64": "0.16.17", - "@esbuild/android-x64": "0.16.17", - "@esbuild/darwin-arm64": "0.16.17", - "@esbuild/darwin-x64": "0.16.17", - "@esbuild/freebsd-arm64": "0.16.17", - "@esbuild/freebsd-x64": "0.16.17", - "@esbuild/linux-arm": "0.16.17", - "@esbuild/linux-arm64": "0.16.17", - "@esbuild/linux-ia32": "0.16.17", - "@esbuild/linux-loong64": "0.16.17", - "@esbuild/linux-mips64el": "0.16.17", - "@esbuild/linux-ppc64": "0.16.17", - "@esbuild/linux-riscv64": "0.16.17", - "@esbuild/linux-s390x": "0.16.17", - "@esbuild/linux-x64": "0.16.17", - "@esbuild/netbsd-x64": "0.16.17", - "@esbuild/openbsd-x64": "0.16.17", - "@esbuild/sunos-x64": "0.16.17", - "@esbuild/win32-arm64": "0.16.17", - "@esbuild/win32-ia32": "0.16.17", - "@esbuild/win32-x64": "0.16.17" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/esbuild/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" } }, "node_modules/escalade": { @@ -25360,9 +25939,10 @@ "license": "MIT" }, "node_modules/picocolors": { - "version": "1.0.1", - "dev": true, - "license": "ISC" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true }, "node_modules/picomatch": { "version": "2.3.1", @@ -25571,7 +26151,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, "funding": [ { @@ -25587,11 +26169,10 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -26951,20 +27532,209 @@ } }, "node_modules/rollup": { - "version": "3.29.4", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.0.tgz", + "integrity": "sha512-W21MUIFPZ4+O2Je/EU+GP3iz7PH4pVPUXSbEZdatQnxo29+3rsUjgrJmzuAZU24z7yRAnFN6ukxeAhZh/c7hzg==", "dev": true, - "license": "MIT", + "dependencies": { + "@types/estree": "1.0.5" + }, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=14.18.0", + "node": ">=18.0.0", "npm": ">=8.0.0" }, "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.22.0", + "@rollup/rollup-android-arm64": "4.22.0", + "@rollup/rollup-darwin-arm64": "4.22.0", + "@rollup/rollup-darwin-x64": "4.22.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.0", + "@rollup/rollup-linux-arm-musleabihf": "4.22.0", + "@rollup/rollup-linux-arm64-gnu": "4.22.0", + "@rollup/rollup-linux-arm64-musl": "4.22.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.0", + "@rollup/rollup-linux-riscv64-gnu": "4.22.0", + "@rollup/rollup-linux-s390x-gnu": "4.22.0", + "@rollup/rollup-linux-x64-gnu": "4.22.0", + "@rollup/rollup-linux-x64-musl": "4.22.0", + "@rollup/rollup-win32-arm64-msvc": "4.22.0", + "@rollup/rollup-win32-ia32-msvc": "4.22.0", + "@rollup/rollup-win32-x64-msvc": "4.22.0", "fsevents": "~2.3.2" } }, + "node_modules/rollup/node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.0.tgz", + "integrity": "sha512-/IZQvg6ZR0tAkEi4tdXOraQoWeJy9gbQ/cx4I7k9dJaCk9qrXEcdouxRVz5kZXt5C2bQ9pILoAA+KB4C/d3pfw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-android-arm64": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.0.tgz", + "integrity": "sha512-ETHi4bxrYnvOtXeM7d4V4kZWixib2jddFacJjsOjwbgYSRsyXYtZHC4ht134OsslPIcnkqT+TKV4eU8rNBKyyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.0.tgz", + "integrity": "sha512-ZWgARzhSKE+gVUX7QWaECoRQsPwaD8ZR0Oxb3aUpzdErTvlEadfQpORPXkKSdKbFci9v8MJfkTtoEHnnW9Ulng==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.0.tgz", + "integrity": "sha512-h0ZAtOfHyio8Az6cwIGS+nHUfRMWBDO5jXB8PQCARVF6Na/G6XS2SFxDl8Oem+S5ZsHQgtsI7RT4JQnI1qrlaw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.0.tgz", + "integrity": "sha512-9pxQJSPwFsVi0ttOmqLY4JJ9pg9t1gKhK0JDbV1yUEETSx55fdyCjt39eBQ54OQCzAF0nVGO6LfEH1KnCPvelA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.0.tgz", + "integrity": "sha512-U4G4u7f+QCqHlVg1Nlx+qapZy+QoG+NV6ux+upo/T7arNGwKvKP2kmGM4W5QTbdewWFgudQxi3kDNST9GT1/mg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.0.tgz", + "integrity": "sha512-aQpNlKmx3amwkA3a5J6nlXSahE1ijl0L9KuIjVOUhfOh7uw2S4piR3mtpxpRtbnK809SBtyPsM9q15CPTsY7HQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.0.tgz", + "integrity": "sha512-VWQiCcN7zBgZYLjndIEh5tamtnKg5TGxyZPWcN9zBtXBwfcGSZ5cHSdQZfQH/GB4uRxk0D3VYbOEe/chJhPGLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.0.tgz", + "integrity": "sha512-tsSWy3YQzmpjDKnQ1Vcpy3p9Z+kMFbSIesCdMNgLizDWFhrLZIoN21JSq01g+MZMDFF+Y1+4zxgrlqPjid5ohg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.0.tgz", + "integrity": "sha512-anr1Y11uPOQrpuU8XOikY5lH4Qu94oS6j0xrulHk3NkLDq19MlX8Ng/pVipjxBJ9a2l3+F39REZYyWQFkZ4/fw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.0.tgz", + "integrity": "sha512-7LB+Bh+Ut7cfmO0m244/asvtIGQr5pG5Rvjz/l1Rnz1kDzM02pSX9jPaS0p+90H5I1x4d1FkCew+B7MOnoatNw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.0.tgz", + "integrity": "sha512-+3qZ4rer7t/QsC5JwMpcvCVPRcJt1cJrYS/TMJZzXIJbxWFQEVhrIc26IhB+5Z9fT9umfVc+Es2mOZgl+7jdJQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.0.tgz", + "integrity": "sha512-YdicNOSJONVx/vuPkgPTyRoAPx3GbknBZRCOUkK84FJ/YTfs/F0vl/YsMscrB6Y177d+yDRcj+JWMPMCgshwrA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/run-async": { "version": "2.4.1", "dev": true, @@ -27443,9 +28213,10 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -29300,28 +30071,33 @@ } }, "node_modules/vite": { - "version": "4.1.5", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", + "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", "dev": true, - "license": "MIT", "dependencies": { - "esbuild": "^0.16.14", - "postcss": "^8.4.21", - "resolve": "^1.22.1", - "rollup": "^3.10.0" + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" }, "optionalDependencies": { - "fsevents": "~2.3.2" + "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": ">= 14", + "@types/node": "^18.0.0 || >=20.0.0", "less": "*", + "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -29333,9 +30109,15 @@ "less": { "optional": true }, + "lightningcss": { + "optional": true + }, "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -29347,6 +30129,20 @@ } } }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/vt-pbf": { "version": "3.1.3", "license": "MIT", @@ -29939,10 +30735,8 @@ "devDependencies": { "@types/plotly.js": "^2.12.18", "@types/react": "^17.0.2", - "@vitejs/plugin-react-swc": "^3.0.0", "react": "^17.0.2", - "typescript": "^4.5.4", - "vite": "~4.1.4" + "typescript": "^4.5.4" }, "peerDependencies": { "react": "^17.0.2" @@ -29974,11 +30768,9 @@ }, "devDependencies": { "@types/react": "^17.0.2", - "@vitejs/plugin-react-swc": "^3.0.0", "react": "^17.0.2", "sass": "^1.60.0", - "typescript": "^4.5.4", - "vite": "~4.1.4" + "typescript": "^4.5.4" }, "peerDependencies": { "react": "^17.0.2", @@ -30002,8 +30794,7 @@ "version": "0.1.0", "devDependencies": { "@deephaven/plugin": "^0.58.0", - "typescript": "^5.2.2", - "vite": "^5.0.8" + "typescript": "^5.2.2" } }, "plugins/example-theme/src/js/node_modules/@deephaven/chart": { @@ -30318,357 +31109,6 @@ "node": ">=16" } }, - "plugins/example-theme/src/js/node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "plugins/example-theme/src/js/node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "plugins/example-theme/src/js/node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "plugins/example-theme/src/js/node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "plugins/example-theme/src/js/node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "plugins/example-theme/src/js/node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "plugins/example-theme/src/js/node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "plugins/example-theme/src/js/node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "plugins/example-theme/src/js/node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "plugins/example-theme/src/js/node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "plugins/example-theme/src/js/node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "plugins/example-theme/src/js/node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "plugins/example-theme/src/js/node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "plugins/example-theme/src/js/node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "plugins/example-theme/src/js/node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "plugins/example-theme/src/js/node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "plugins/example-theme/src/js/node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "plugins/example-theme/src/js/node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "plugins/example-theme/src/js/node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "plugins/example-theme/src/js/node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "plugins/example-theme/src/js/node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "plugins/example-theme/src/js/node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "plugins/example-theme/src/js/node_modules/color-convert": { "version": "2.0.1", "dev": true, @@ -30685,43 +31125,6 @@ "dev": true, "license": "MIT" }, - "plugins/example-theme/src/js/node_modules/esbuild": { - "version": "0.19.12", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" - } - }, "plugins/example-theme/src/js/node_modules/event-target-shim": { "version": "6.0.2", "dev": true, @@ -30733,105 +31136,6 @@ "url": "https://github.com/sponsors/mysticatea" } }, - "plugins/example-theme/src/js/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "plugins/example-theme/src/js/node_modules/rollup": { - "version": "4.9.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.5" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.9.6", - "@rollup/rollup-android-arm64": "4.9.6", - "@rollup/rollup-darwin-arm64": "4.9.6", - "@rollup/rollup-darwin-x64": "4.9.6", - "@rollup/rollup-linux-arm-gnueabihf": "4.9.6", - "@rollup/rollup-linux-arm64-gnu": "4.9.6", - "@rollup/rollup-linux-arm64-musl": "4.9.6", - "@rollup/rollup-linux-riscv64-gnu": "4.9.6", - "@rollup/rollup-linux-x64-gnu": "4.9.6", - "@rollup/rollup-linux-x64-musl": "4.9.6", - "@rollup/rollup-win32-arm64-msvc": "4.9.6", - "@rollup/rollup-win32-ia32-msvc": "4.9.6", - "@rollup/rollup-win32-x64-msvc": "4.9.6", - "fsevents": "~2.3.2" - } - }, - "plugins/example-theme/src/js/node_modules/vite": { - "version": "5.0.12", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.19.3", - "postcss": "^8.4.32", - "rollup": "^4.2.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, "plugins/matplotlib/src/js": { "name": "@deephaven/js-plugin-matplotlib", "version": "0.5.0", @@ -30849,11 +31153,9 @@ "devDependencies": { "@types/react": "^17.0.2", "@types/react-dom": "^17.0.2", - "@vitejs/plugin-react-swc": "^3.0.0", "react": "^17.0.2", "react-dom": "^17.0.2", - "typescript": "^4.5.4", - "vite": "~4.1.4" + "typescript": "^4.5.4" }, "peerDependencies": { "react": "^17.0.2", @@ -31621,11 +31923,9 @@ "@types/plotly.js-dist-min": "^2.3.1", "@types/react": "^17.0.2", "@types/react-plotly.js": "^2.6.0", - "@vitejs/plugin-react-swc": "^3.0.0", "react": "^17.0.2", "react-dom": "^17.0.2", - "typescript": "^4.5.4", - "vite": "~4.1.4" + "typescript": "^4.5.4" }, "peerDependencies": { "react": "^17.0.2", @@ -31851,10 +32151,8 @@ }, "devDependencies": { "@types/react": "^17.0.2", - "@vitejs/plugin-react-swc": "^3.0.0", "react": "^17.0.2", - "typescript": "^4.9.0", - "vite": "~4.1.4" + "typescript": "^4.9.0" }, "peerDependencies": { "react": "^17.0.2" @@ -31903,11 +32201,9 @@ }, "devDependencies": { "@types/react": "^17.0.2", - "@vitejs/plugin-react-swc": "^3.0.0", "react": "^17.0.2", "react-dom": "^17.0.2", - "typescript": "^4.5.4", - "vite": "~4.1.4" + "typescript": "^4.5.4" }, "peerDependencies": { "react": "^17.0.2", @@ -34503,11 +34799,9 @@ "@deephaven/utils": "^0.40.0", "@types/plotly.js": "^2.12.18", "@types/react": "^17.0.2", - "@vitejs/plugin-react-swc": "^3.0.0", "keycloak-js": "^21.0.2", "react": "^17.0.2", - "typescript": "^4.5.4", - "vite": "~4.1.4" + "typescript": "^4.5.4" }, "dependencies": { "typescript": { @@ -34524,13 +34818,11 @@ "@deephaven/jsapi-types": "^0.40.0", "@deephaven/log": "^0.40.0", "@types/react": "^17.0.2", - "@vitejs/plugin-react-swc": "^3.0.0", "nanoid": "^5.0.7", "react": "^17.0.2", "react-json-view": "^1.21.3", "sass": "^1.60.0", - "typescript": "^4.5.4", - "vite": "~4.1.4" + "typescript": "^4.5.4" }, "dependencies": { "typescript": { @@ -34543,8 +34835,7 @@ "version": "file:plugins/example-theme/src/js", "requires": { "@deephaven/plugin": "^0.58.0", - "typescript": "^5.2.2", - "vite": "^5.0.8" + "typescript": "^5.2.2" }, "dependencies": { "@deephaven/chart": { @@ -34763,158 +35054,6 @@ "version": "0.58.0", "dev": true }, - "@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", - "dev": true, - "optional": true - }, - "@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-x64": { - "version": "0.19.12", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", - "dev": true, - "optional": true - }, - "@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", - "dev": true, - "optional": true - }, - "@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", - "dev": true, - "optional": true - }, - "@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", - "dev": true, - "optional": true - }, - "@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", - "dev": true, - "optional": true - }, "color-convert": { "version": "2.0.1", "dev": true, @@ -34926,76 +35065,9 @@ "version": "1.1.4", "dev": true }, - "esbuild": { - "version": "0.19.12", - "dev": true, - "requires": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" - } - }, "event-target-shim": { "version": "6.0.2", "dev": true - }, - "fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "optional": true - }, - "rollup": { - "version": "4.9.6", - "dev": true, - "requires": { - "@rollup/rollup-android-arm-eabi": "4.9.6", - "@rollup/rollup-android-arm64": "4.9.6", - "@rollup/rollup-darwin-arm64": "4.9.6", - "@rollup/rollup-darwin-x64": "4.9.6", - "@rollup/rollup-linux-arm-gnueabihf": "4.9.6", - "@rollup/rollup-linux-arm64-gnu": "4.9.6", - "@rollup/rollup-linux-arm64-musl": "4.9.6", - "@rollup/rollup-linux-riscv64-gnu": "4.9.6", - "@rollup/rollup-linux-x64-gnu": "4.9.6", - "@rollup/rollup-linux-x64-musl": "4.9.6", - "@rollup/rollup-win32-arm64-msvc": "4.9.6", - "@rollup/rollup-win32-ia32-msvc": "4.9.6", - "@rollup/rollup-win32-x64-msvc": "4.9.6", - "@types/estree": "1.0.5", - "fsevents": "~2.3.2" - } - }, - "vite": { - "version": "5.0.12", - "dev": true, - "requires": { - "esbuild": "^0.19.3", - "fsevents": "~2.3.3", - "postcss": "^8.4.32", - "rollup": "^4.2.0" - } } } }, @@ -35011,12 +35083,10 @@ "@deephaven/plugin": "^0.86.0", "@types/react": "^17.0.2", "@types/react-dom": "^17.0.2", - "@vitejs/plugin-react-swc": "^3.0.0", "nanoid": "^5.0.7", "react": "^17.0.2", "react-dom": "^17.0.2", - "typescript": "^4.5.4", - "vite": "~4.1.4" + "typescript": "^4.5.4" }, "dependencies": { "@deephaven/components": { @@ -35584,7 +35654,6 @@ "@types/plotly.js-dist-min": "^2.3.1", "@types/react": "^17.0.2", "@types/react-plotly.js": "^2.6.0", - "@vitejs/plugin-react-swc": "^3.0.0", "deep-equal": "^2.2.1", "nanoid": "^5.0.7", "plotly.js": "^2.29.1", @@ -35593,8 +35662,7 @@ "react-dom": "^17.0.2", "react-plotly.js": "^2.4.0", "react-redux": "^7.2.9", - "typescript": "^4.5.4", - "vite": "~4.1.4" + "typescript": "^4.5.4" }, "dependencies": { "@deephaven/components": { @@ -35733,10 +35801,8 @@ "@deephaven/components": "^0.40.0", "@deephaven/jsapi-types": "^0.40.0", "@types/react": "^17.0.2", - "@vitejs/plugin-react-swc": "^3.0.0", "react": "^17.0.2", - "typescript": "^4.9.0", - "vite": "~4.1.4" + "typescript": "^4.9.0" }, "dependencies": { "typescript": { @@ -35768,15 +35834,13 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@internationalized/date": "^3.5.5", "@types/react": "^17.0.2", - "@vitejs/plugin-react-swc": "^3.0.0", "classnames": "^2.5.1", "json-rpc-2.0": "^1.6.0", "nanoid": "^5.0.7", "react": "^17.0.2", "react-dom": "^17.0.2", "react-redux": "^7.x", - "typescript": "^4.5.4", - "vite": "~4.1.4" + "typescript": "^4.5.4" }, "dependencies": { "@deephaven/chart": { @@ -40883,6 +40947,13 @@ "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==", "optional": true }, + "@rollup/rollup-linux-arm-musleabihf": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.0.tgz", + "integrity": "sha512-YJ5Ku5BmNJZb58A4qSEo3JlIG4d3G2lWyBi13ABlXzO41SsdnUKi3HQHe83VpwBVG4jHFTW65jOQb8qyoR+qzg==", + "dev": true, + "optional": true + }, "@rollup/rollup-linux-arm64-gnu": { "version": "4.9.6", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz", @@ -40895,12 +40966,26 @@ "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==", "optional": true }, + "@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.0.tgz", + "integrity": "sha512-9fx6Zj/7vve/Fp4iexUFRKb5+RjLCff6YTRQl4CoDhdMfDoobWmhAxQWV3NfShMzQk1Q/iCnageFyGfqnsmeqQ==", + "dev": true, + "optional": true + }, "@rollup/rollup-linux-riscv64-gnu": { "version": "4.9.6", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz", "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==", "optional": true }, + "@rollup/rollup-linux-s390x-gnu": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.0.tgz", + "integrity": "sha512-EHmPnPWvyYqncObwqrosb/CpH3GOjE76vWVs0g4hWsDRUVhg61hBmlVg5TPXqF+g+PvIbqkC7i3h8wbn4Gp2Fg==", + "dev": true, + "optional": true + }, "@rollup/rollup-linux-x64-gnu": { "version": "4.9.6", "optional": true @@ -41066,20 +41151,88 @@ } }, "@swc/core": { - "version": "1.3.99", - "dev": true, - "requires": { - "@swc/core-darwin-arm64": "1.3.99", - "@swc/core-darwin-x64": "1.3.99", - "@swc/core-linux-arm64-gnu": "1.3.99", - "@swc/core-linux-arm64-musl": "1.3.99", - "@swc/core-linux-x64-gnu": "1.3.99", - "@swc/core-linux-x64-musl": "1.3.99", - "@swc/core-win32-arm64-msvc": "1.3.99", - "@swc/core-win32-ia32-msvc": "1.3.99", - "@swc/core-win32-x64-msvc": "1.3.99", - "@swc/counter": "^0.1.1", - "@swc/types": "^0.1.5" + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.26.tgz", + "integrity": "sha512-f5uYFf+TmMQyYIoxkn/evWhNGuUzC730dFwAKGwBVHHVoPyak1/GvJUm6i1SKl+2Hrj9oN0i3WSoWWZ4pgI8lw==", + "dev": true, + "requires": { + "@swc/core-darwin-arm64": "1.7.26", + "@swc/core-darwin-x64": "1.7.26", + "@swc/core-linux-arm-gnueabihf": "1.7.26", + "@swc/core-linux-arm64-gnu": "1.7.26", + "@swc/core-linux-arm64-musl": "1.7.26", + "@swc/core-linux-x64-gnu": "1.7.26", + "@swc/core-linux-x64-musl": "1.7.26", + "@swc/core-win32-arm64-msvc": "1.7.26", + "@swc/core-win32-ia32-msvc": "1.7.26", + "@swc/core-win32-x64-msvc": "1.7.26", + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.12" + }, + "dependencies": { + "@swc/core-darwin-arm64": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.26.tgz", + "integrity": "sha512-FF3CRYTg6a7ZVW4yT9mesxoVVZTrcSWtmZhxKCYJX9brH4CS/7PRPjAKNk6kzWgWuRoglP7hkjQcd6EpMcZEAw==", + "dev": true, + "optional": true + }, + "@swc/core-darwin-x64": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.26.tgz", + "integrity": "sha512-az3cibZdsay2HNKmc4bjf62QVukuiMRh5sfM5kHR/JMTrLyS6vSw7Ihs3UTkZjUxkLTT8ro54LI6sV6sUQUbLQ==", + "dev": true, + "optional": true + }, + "@swc/core-linux-arm64-gnu": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.26.tgz", + "integrity": "sha512-YKevOV7abpjcAzXrhsl+W48Z9mZvgoVs2eP5nY+uoMAdP2b3GxC0Df1Co0I90o2lkzO4jYBpTMcZlmUXLdXn+Q==", + "dev": true, + "optional": true + }, + "@swc/core-linux-arm64-musl": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.26.tgz", + "integrity": "sha512-3w8iZICMkQQON0uIcvz7+Q1MPOW6hJ4O5ETjA0LSP/tuKqx30hIniCGOgPDnv3UTMruLUnQbtBwVCZTBKR3Rkg==", + "dev": true, + "optional": true + }, + "@swc/core-linux-x64-gnu": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.26.tgz", + "integrity": "sha512-c+pp9Zkk2lqb06bNGkR2Looxrs7FtGDMA4/aHjZcCqATgp348hOKH5WPvNLBl+yPrISuWjbKDVn3NgAvfvpH4w==", + "dev": true, + "optional": true + }, + "@swc/core-linux-x64-musl": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.26.tgz", + "integrity": "sha512-PgtyfHBF6xG87dUSSdTJHwZ3/8vWZfNIXQV2GlwEpslrOkGqy+WaiiyE7Of7z9AvDILfBBBcJvJ/r8u980wAfQ==", + "dev": true, + "optional": true + }, + "@swc/core-win32-arm64-msvc": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.26.tgz", + "integrity": "sha512-9TNXPIJqFynlAOrRD6tUQjMq7KApSklK3R/tXgIxc7Qx+lWu8hlDQ/kVPLpU7PWvMMwC/3hKBW+p5f+Tms1hmA==", + "dev": true, + "optional": true + }, + "@swc/core-win32-ia32-msvc": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.26.tgz", + "integrity": "sha512-9YngxNcG3177GYdsTum4V98Re+TlCeJEP4kEwEg9EagT5s3YejYdKwVAkAsJszzkXuyRDdnHUpYbTrPG6FiXrQ==", + "dev": true, + "optional": true + }, + "@swc/core-win32-x64-msvc": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.26.tgz", + "integrity": "sha512-VR+hzg9XqucgLjXxA13MtV5O3C0bK0ywtLIBw/+a+O+Oc6mxFWHtdUeXDbIi5AiPbn0fjgVJMqYnyjGyyX8u0w==", + "dev": true, + "optional": true + } } }, "@swc/core-darwin-arm64": { @@ -41094,6 +41247,13 @@ "integrity": "sha512-wR7m9QVJjgiBu1PSOHy7s66uJPa45Kf9bZExXUL+JAa9OQxt5y+XVzr+n+F045VXQOwdGWplgPnWjgbUUHEVyw==", "optional": true }, + "@swc/core-linux-arm-gnueabihf": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.26.tgz", + "integrity": "sha512-VYPFVJDO5zT5U3RpCdHE5v1gz4mmR8BfHecUZTmD2v1JeFY6fv9KArJUpjrHEEsjK/ucXkQFmJ0jaiWXmpOV9Q==", + "dev": true, + "optional": true + }, "@swc/core-linux-arm64-gnu": { "version": "1.3.99", "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.99.tgz", @@ -41133,7 +41293,9 @@ "optional": true }, "@swc/counter": { - "version": "0.1.2", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", "dev": true }, "@swc/helpers": { @@ -41143,8 +41305,13 @@ } }, "@swc/types": { - "version": "0.1.5", - "dev": true + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.12.tgz", + "integrity": "sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==", + "dev": true, + "requires": { + "@swc/counter": "^0.1.3" + } }, "@testing-library/dom": { "version": "10.1.0", @@ -41453,6 +41620,8 @@ }, "@types/estree": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, "@types/graceful-fs": { @@ -41815,10 +41984,12 @@ "dev": true }, "@vitejs/plugin-react-swc": { - "version": "3.5.0", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.0.tgz", + "integrity": "sha512-yrknSb3Dci6svCd/qhHqhFPDSw0QtjumcqdKMoNNzmOl5lMXTTiqzjWtG4Qask2HdvvzaNgSunbQGet8/GrKdA==", "dev": true, "requires": { - "@swc/core": "^1.3.96" + "@swc/core": "^1.5.7" } }, "@yarnpkg/lockfile": { @@ -43813,31 +43984,197 @@ } }, "esbuild": { - "version": "0.16.17", - "dev": true, - "requires": { - "@esbuild/android-arm": "0.16.17", - "@esbuild/android-arm64": "0.16.17", - "@esbuild/android-x64": "0.16.17", - "@esbuild/darwin-arm64": "0.16.17", - "@esbuild/darwin-x64": "0.16.17", - "@esbuild/freebsd-arm64": "0.16.17", - "@esbuild/freebsd-x64": "0.16.17", - "@esbuild/linux-arm": "0.16.17", - "@esbuild/linux-arm64": "0.16.17", - "@esbuild/linux-ia32": "0.16.17", - "@esbuild/linux-loong64": "0.16.17", - "@esbuild/linux-mips64el": "0.16.17", - "@esbuild/linux-ppc64": "0.16.17", - "@esbuild/linux-riscv64": "0.16.17", - "@esbuild/linux-s390x": "0.16.17", - "@esbuild/linux-x64": "0.16.17", - "@esbuild/netbsd-x64": "0.16.17", - "@esbuild/openbsd-x64": "0.16.17", - "@esbuild/sunos-x64": "0.16.17", - "@esbuild/win32-arm64": "0.16.17", - "@esbuild/win32-ia32": "0.16.17", - "@esbuild/win32-x64": "0.16.17" + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + }, + "dependencies": { + "@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "dev": true, + "optional": true + } } }, "escalade": { @@ -50605,7 +50942,9 @@ "version": "1.2.0" }, "picocolors": { - "version": "1.0.1", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", "dev": true }, "picomatch": { @@ -50746,12 +51085,14 @@ "version": "1.16.1" }, "postcss": { - "version": "8.4.38", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, "requires": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "dependencies": { "nanoid": { @@ -51633,10 +51974,122 @@ } }, "rollup": { - "version": "3.29.4", - "dev": true, - "requires": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.0.tgz", + "integrity": "sha512-W21MUIFPZ4+O2Je/EU+GP3iz7PH4pVPUXSbEZdatQnxo29+3rsUjgrJmzuAZU24z7yRAnFN6ukxeAhZh/c7hzg==", + "dev": true, + "requires": { + "@rollup/rollup-android-arm-eabi": "4.22.0", + "@rollup/rollup-android-arm64": "4.22.0", + "@rollup/rollup-darwin-arm64": "4.22.0", + "@rollup/rollup-darwin-x64": "4.22.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.0", + "@rollup/rollup-linux-arm-musleabihf": "4.22.0", + "@rollup/rollup-linux-arm64-gnu": "4.22.0", + "@rollup/rollup-linux-arm64-musl": "4.22.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.0", + "@rollup/rollup-linux-riscv64-gnu": "4.22.0", + "@rollup/rollup-linux-s390x-gnu": "4.22.0", + "@rollup/rollup-linux-x64-gnu": "4.22.0", + "@rollup/rollup-linux-x64-musl": "4.22.0", + "@rollup/rollup-win32-arm64-msvc": "4.22.0", + "@rollup/rollup-win32-ia32-msvc": "4.22.0", + "@rollup/rollup-win32-x64-msvc": "4.22.0", + "@types/estree": "1.0.5", "fsevents": "~2.3.2" + }, + "dependencies": { + "@rollup/rollup-android-arm-eabi": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.0.tgz", + "integrity": "sha512-/IZQvg6ZR0tAkEi4tdXOraQoWeJy9gbQ/cx4I7k9dJaCk9qrXEcdouxRVz5kZXt5C2bQ9pILoAA+KB4C/d3pfw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-android-arm64": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.0.tgz", + "integrity": "sha512-ETHi4bxrYnvOtXeM7d4V4kZWixib2jddFacJjsOjwbgYSRsyXYtZHC4ht134OsslPIcnkqT+TKV4eU8rNBKyyQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-arm64": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.0.tgz", + "integrity": "sha512-ZWgARzhSKE+gVUX7QWaECoRQsPwaD8ZR0Oxb3aUpzdErTvlEadfQpORPXkKSdKbFci9v8MJfkTtoEHnnW9Ulng==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-x64": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.0.tgz", + "integrity": "sha512-h0ZAtOfHyio8Az6cwIGS+nHUfRMWBDO5jXB8PQCARVF6Na/G6XS2SFxDl8Oem+S5ZsHQgtsI7RT4JQnI1qrlaw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.0.tgz", + "integrity": "sha512-9pxQJSPwFsVi0ttOmqLY4JJ9pg9t1gKhK0JDbV1yUEETSx55fdyCjt39eBQ54OQCzAF0nVGO6LfEH1KnCPvelA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-gnu": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.0.tgz", + "integrity": "sha512-U4G4u7f+QCqHlVg1Nlx+qapZy+QoG+NV6ux+upo/T7arNGwKvKP2kmGM4W5QTbdewWFgudQxi3kDNST9GT1/mg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-musl": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.0.tgz", + "integrity": "sha512-aQpNlKmx3amwkA3a5J6nlXSahE1ijl0L9KuIjVOUhfOh7uw2S4piR3mtpxpRtbnK809SBtyPsM9q15CPTsY7HQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-riscv64-gnu": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.0.tgz", + "integrity": "sha512-VWQiCcN7zBgZYLjndIEh5tamtnKg5TGxyZPWcN9zBtXBwfcGSZ5cHSdQZfQH/GB4uRxk0D3VYbOEe/chJhPGLQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-gnu": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.0.tgz", + "integrity": "sha512-tsSWy3YQzmpjDKnQ1Vcpy3p9Z+kMFbSIesCdMNgLizDWFhrLZIoN21JSq01g+MZMDFF+Y1+4zxgrlqPjid5ohg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-musl": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.0.tgz", + "integrity": "sha512-anr1Y11uPOQrpuU8XOikY5lH4Qu94oS6j0xrulHk3NkLDq19MlX8Ng/pVipjxBJ9a2l3+F39REZYyWQFkZ4/fw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-arm64-msvc": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.0.tgz", + "integrity": "sha512-7LB+Bh+Ut7cfmO0m244/asvtIGQr5pG5Rvjz/l1Rnz1kDzM02pSX9jPaS0p+90H5I1x4d1FkCew+B7MOnoatNw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-ia32-msvc": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.0.tgz", + "integrity": "sha512-+3qZ4rer7t/QsC5JwMpcvCVPRcJt1cJrYS/TMJZzXIJbxWFQEVhrIc26IhB+5Z9fT9umfVc+Es2mOZgl+7jdJQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-x64-msvc": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.0.tgz", + "integrity": "sha512-YdicNOSJONVx/vuPkgPTyRoAPx3GbknBZRCOUkK84FJ/YTfs/F0vl/YsMscrB6Y177d+yDRcj+JWMPMCgshwrA==", + "dev": true, + "optional": true + } } }, "run-async": { @@ -51940,7 +52393,9 @@ "devOptional": true }, "source-map-js": { - "version": "1.2.0", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true }, "source-map-support": { @@ -53136,14 +53591,24 @@ } }, "vite": { - "version": "4.1.5", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", + "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", "dev": true, "requires": { - "esbuild": "^0.16.14", - "fsevents": "~2.3.2", - "postcss": "^8.4.21", - "resolve": "^1.22.1", - "rollup": "^3.10.0" + "esbuild": "^0.21.3", + "fsevents": "~2.3.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "dependencies": { + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + } } }, "vt-pbf": { diff --git a/package.json b/package.json index e92a3d65a..68cbeb778 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@types/jest": "^29.2.5", "@types/node": "^20.11.17", "@types/prop-types": "^15.7.10", + "@vitejs/plugin-react-swc": "^3.7.0", "eslint": "^8.37.0", "identity-obj-proxy": "^3.0.0", "jest": "^29.6.2", @@ -51,7 +52,7 @@ "npm-run-all": "^4.1.5", "nx": "15.9.2", "prettier": "3.0.0", - "vite": "~4.1.4" + "vite": "^5.4.6" }, "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", diff --git a/plugins/auth-keycloak/src/js/package.json b/plugins/auth-keycloak/src/js/package.json index 3b9a462f5..a3e98a0f3 100644 --- a/plugins/auth-keycloak/src/js/package.json +++ b/plugins/auth-keycloak/src/js/package.json @@ -29,10 +29,8 @@ "devDependencies": { "@types/plotly.js": "^2.12.18", "@types/react": "^17.0.2", - "@vitejs/plugin-react-swc": "^3.0.0", "react": "^17.0.2", - "typescript": "^4.5.4", - "vite": "~4.1.4" + "typescript": "^4.5.4" }, "peerDependencies": { "react": "^17.0.2" diff --git a/plugins/dashboard-object-viewer/src/js/package.json b/plugins/dashboard-object-viewer/src/js/package.json index 8fdd4c665..f13b3b580 100644 --- a/plugins/dashboard-object-viewer/src/js/package.json +++ b/plugins/dashboard-object-viewer/src/js/package.json @@ -28,11 +28,9 @@ }, "devDependencies": { "@types/react": "^17.0.2", - "@vitejs/plugin-react-swc": "^3.0.0", "react": "^17.0.2", "sass": "^1.60.0", - "typescript": "^4.5.4", - "vite": "~4.1.4" + "typescript": "^4.5.4" }, "peerDependencies": { "react": "^17.0.2", diff --git a/plugins/example-theme/src/js/package.json b/plugins/example-theme/src/js/package.json index 1da28d1e5..256ef3385 100644 --- a/plugins/example-theme/src/js/package.json +++ b/plugins/example-theme/src/js/package.json @@ -12,8 +12,7 @@ }, "devDependencies": { "@deephaven/plugin": "^0.58.0", - "typescript": "^5.2.2", - "vite": "^5.0.8" + "typescript": "^5.2.2" }, "private": true } diff --git a/plugins/matplotlib/src/js/package.json b/plugins/matplotlib/src/js/package.json index 33c8fc05d..c630c96ce 100644 --- a/plugins/matplotlib/src/js/package.json +++ b/plugins/matplotlib/src/js/package.json @@ -27,12 +27,10 @@ }, "devDependencies": { "@types/react": "^17.0.2", - "@vitejs/plugin-react-swc": "^3.0.0", "@types/react-dom": "^17.0.2", "react": "^17.0.2", "react-dom": "^17.0.2", - "typescript": "^4.5.4", - "vite": "~4.1.4" + "typescript": "^4.5.4" }, "peerDependencies": { "react": "^17.0.2", diff --git a/plugins/plotly-express/src/js/package.json b/plugins/plotly-express/src/js/package.json index 1decd219f..df2dd9b16 100644 --- a/plugins/plotly-express/src/js/package.json +++ b/plugins/plotly-express/src/js/package.json @@ -42,11 +42,9 @@ "@types/plotly.js-dist-min": "^2.3.1", "@types/react": "^17.0.2", "@types/react-plotly.js": "^2.6.0", - "@vitejs/plugin-react-swc": "^3.0.0", "react": "^17.0.2", "react-dom": "^17.0.2", - "typescript": "^4.5.4", - "vite": "~4.1.4" + "typescript": "^4.5.4" }, "peerDependencies": { "react": "^17.0.2", diff --git a/plugins/table-example/src/js/package.json b/plugins/table-example/src/js/package.json index aeb0abd07..b1c99a10f 100644 --- a/plugins/table-example/src/js/package.json +++ b/plugins/table-example/src/js/package.json @@ -23,10 +23,8 @@ }, "devDependencies": { "@types/react": "^17.0.2", - "@vitejs/plugin-react-swc": "^3.0.0", "react": "^17.0.2", - "typescript": "^4.9.0", - "vite": "~4.1.4" + "typescript": "^4.9.0" }, "peerDependencies": { "react": "^17.0.2" diff --git a/plugins/ui/src/js/package.json b/plugins/ui/src/js/package.json index 21c5af932..f1c84fc46 100644 --- a/plugins/ui/src/js/package.json +++ b/plugins/ui/src/js/package.json @@ -30,11 +30,9 @@ }, "devDependencies": { "@types/react": "^17.0.2", - "@vitejs/plugin-react-swc": "^3.0.0", "react": "^17.0.2", "react-dom": "^17.0.2", - "typescript": "^4.5.4", - "vite": "~4.1.4" + "typescript": "^4.5.4" }, "peerDependencies": { "react": "^17.0.2", From cf3f463934f512cd25655d338748f2b6b0776bf2 Mon Sep 17 00:00:00 2001 From: dgodinez-dh <77981300+dgodinez-dh@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:20:22 -0600 Subject: [PATCH 12/18] feat: Time Field UI Component (#825) Closes https://github.com/deephaven/deephaven-plugins/issues/763 --------- Co-authored-by: margaretkennedy <82049573+margaretkennedy@users.noreply.github.com> --- plugins/ui/DESIGN.md | 135 ++++++ plugins/ui/docs/components/time_field.md | 440 ++++++++++++++++++ .../ui/src/deephaven/ui/_internal/utils.py | 178 ++++++- .../src/deephaven/ui/components/__init__.py | 2 + .../src/deephaven/ui/components/time_field.py | 248 ++++++++++ plugins/ui/src/deephaven/ui/types/types.py | 21 + plugins/ui/src/js/src/elements/DateField.tsx | 2 +- plugins/ui/src/js/src/elements/DatePicker.tsx | 2 +- .../src/js/src/elements/DateRangePicker.tsx | 2 +- plugins/ui/src/js/src/elements/TimeField.tsx | 74 +++ plugins/ui/src/js/src/elements/hooks/index.ts | 2 + .../elements/hooks/useTimeComponentProps.ts | 212 +++++++++ .../js/src/elements/hooks/useTimeValueMemo.ts | 15 + plugins/ui/src/js/src/elements/index.ts | 1 + .../js/src/elements/model/ElementConstants.ts | 1 + .../src/elements/utils/DateTimeUtils.test.ts | 65 +++ .../js/src/elements/utils/DateTimeUtils.ts | 95 +++- plugins/ui/src/js/src/widget/WidgetUtils.tsx | 2 + .../ui/test/deephaven/ui/test_time_field.py | 85 ++++ tests/app.d/ui_render_all.py | 1 + ...l-components-render-2-1-chromium-linux.png | Bin 50738 -> 51645 bytes ...ll-components-render-2-1-firefox-linux.png | Bin 74141 -> 75599 bytes ...all-components-render-2-1-webkit-linux.png | Bin 53103 -> 54052 bytes 23 files changed, 1565 insertions(+), 18 deletions(-) create mode 100644 plugins/ui/docs/components/time_field.md create mode 100644 plugins/ui/src/deephaven/ui/components/time_field.py create mode 100644 plugins/ui/src/js/src/elements/TimeField.tsx create mode 100644 plugins/ui/src/js/src/elements/hooks/useTimeComponentProps.ts create mode 100644 plugins/ui/src/js/src/elements/hooks/useTimeValueMemo.ts create mode 100644 plugins/ui/test/deephaven/ui/test_time_field.py diff --git a/plugins/ui/DESIGN.md b/plugins/ui/DESIGN.md index c8d16decc..af28d9266 100644 --- a/plugins/ui/DESIGN.md +++ b/plugins/ui/DESIGN.md @@ -2415,6 +2415,141 @@ ui_table.sort( | `by` | `str \| Sequence[str]` | The column(s) to sort by. May be a single column name, or a list of column names. | | `direction` | `TableSortDirection \| Sequence[TableSortDirection] \| None` | The sort direction(s) to use. If provided, that must match up with the columns provided. Defaults to "ASC". | +###### ui.time_field + +A time field that can be used to select a time. + +The time field accepts the following time types as inputs: + +- `None` +- `LocaTime` +- `ZonedDateTime` +- `Instant` +- `int` +- `str` +- `datetime.datetime` +- `numpy.datetime64` +- `pandas.Timestamp` + +The input will be converted to one of three Java time types: + +1. `LocalTime`: A LocalTime is a time without a time zone in the ISO-8601 system, such as "10:30:45" or "16:10:00". + This will create a time field with a granularity of seconds. +2. `Instant`: An Instant represents an unambiguous specific point on the timeline, such as 2021-04-12T14:13:07 UTC. + This will create a time field with a granularity of seconds in UTC. The time zone will be rendered as the time zone in user settings. +3. `ZonedDateTime`: A ZonedDateTime represents an unambiguous specific point on the timeline with an associated time zone, such as 2021-04-12T14:13:07 America/New_York. + This will create a time field with a granularity of seconds in the specified time zone. The time zone will be rendered as the specified time zone. + +4. If the input is one of the three Java time types, use that type. +5. A time string such as "10:30:45" will parse to a `LocaTime` +6. A string with a date, time, and timezone such as "2021-04-12T14:13:07 America/New_York" will parse to a `ZonedDateTime` +7. All other types will attempt to convert in this order: `LocaTime`, `Instant`, `ZonedDateTime` + +The format of the time field and the type of the value passed to the `on_change` handler +is determined by the type of the following props in order of precedence: + +1. `value` +2. `default_value` +3. `placeholder_value` + +If none of these are provided, the `on_change` handler passes a range of `LocaTime`. + +```py +import deephaven.ui as ui +ui.time_field( + placeholder_value: Time | None = None, + value: Time | None = None, + default_value: Time | None = None, + min_value: Time | None = None, + max_value: Time | None = None, + granularity: Granularity | None = None, + on_change: Callable[[Time], None] | None = None, + **props: Any +) -> TimeFieldElement +``` + +###### Parameters + +| Parameter | Type | Description | +| ------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `placeholder_value` | `Time \| None` | A placeholder time that influences the format of the placeholder shown when no value is selected. Defaults to 12:00 AM or 00:00 depending on the hour cycle | +| `value` | `Time \| None` | The current value (controlled). | +| `default_value` | `Time \| None` | The default value (uncontrolled). | +| `min_value` | `Time \| None` | The minimum allowed time that a user may select. | +| `max_value` | `Time \| None` | The maximum allowed time that a user may select. | +| `granularity` | `Granularity \| None` | Determines the smallest unit that is displayed in the time field. By default, this is `"SECOND". | +| `on_change` | `Callable[[Time], None] \| None` | Handler that is called when the value changes. The exact `Time` type will be the same as the type passed to `value`, `default_value` or `placeholder_value`, in that order of precedence. | +| `**props` | `Any` | Any other [TimeField](https://react-spectrum.adobe.com/react-spectrum/TimeField.html) prop, with the exception of `validate`, and `errorMessage` (as a callback) | + +```py + +import deephaven.ui as ui +from deephaven.time import to_j_local_time, to_j_instant, to_j_zdt + +zoned_date_time = to_j_zdt("1995-03-22T11:11:11.23142 America/New_York") +instant = to_j_instant("2022-01-01T00:00:00 ET") +local_time = to_j_local_time("12:30:45") + +# simple time field that takes ui.items and is uncontrolled +time_field1 = ui.time_field( + default_value=local_time +) + +# simple time field that takes list view items directly and is controlled +# this creates a time field with a granularity of seconds in UTC +# the on_change handler is passed an instant +time, set_time = ui.use_state(instant) + +time_field2 = ui.time_field( + value=time, + on_change=set_time +) + +# this creates a time field with a granularity of seconds in the specified time zone +# the on_change handler is passed a zoned date time +time, set_time = ui.use_state(None) + +time_field3 = ui.time_field( + placeholder_value=zoned_date_time, + on_change=set_time +) + +# this creates a time field with a granularity of seconds in UTC +# the on_change handler is passed an instant +time, set_time = ui.use_state(None) + +time_field4 = ui.time_field( + placeholder_value=instant, + on_change=set_time +) + +# this creates a time field with a granularity of seconds +# the on_change handler is passed a local time +time, set_time = ui.use_state(None) + +time_field5 = ui.time_field( + placeholder_value=local_time, + on_change=set_time +) + +# this creates a time field with a granularity of hours, but the on_change handler is still passed an instant +time, set_time = ui.use_state(None) + +time_field6 = ui.time_field( + placeholder_value=instant, + granularity="hour", + on_change=set_time +) + +# this creates a time field with a granularity of seconds and the on_change handler is passed an instant +time, set_time = ui.use_state(None) + +time_field7 = ui.time_field( + on_change=set_time +) + +``` + #### ui.fragment A fragment maps to a [React.Fragment](https://react.dev/reference/react/Fragment). This lets you group elements without using a wrapper node. It only takes children, and does not take any additional props. diff --git a/plugins/ui/docs/components/time_field.md b/plugins/ui/docs/components/time_field.md new file mode 100644 index 000000000..53c099b48 --- /dev/null +++ b/plugins/ui/docs/components/time_field.md @@ -0,0 +1,440 @@ +# Time field + +Time fields allow users to input a time using a text field. + +## Example + +```python +from deephaven import ui + +my_time_field_basic = ui.time_field(label="Time field") +``` + +## Time types + +A time field can be used to input a time. + +The time field accepts the following time types as inputs: + +- `None` +- `LocaTime` +- `ZonedDateTime` +- `Instant` +- `int` +- `str` +- `datetime.datetime` +- `numpy.datetime64` +- `pandas.Timestamp` + +The input will be converted to one of three Java time types: + +1. `LocalTime`: A LocalTime is a time without a time zone in the ISO-8601 system, such as "10:30:45" or "16:10:00". + This will create a time field with a granularity of seconds. +2. `Instant`: An Instant represents an unambiguous specific point on the timeline, such as 2021-04-12T14:13:07 UTC. + This will create a time field with a granularity of seconds in UTC. The time zone will be rendered as the time zone in user settings. +3. `ZonedDateTime`: A ZonedDateTime represents an unambiguous specific point on the timeline with an associated time zone, such as 2021-04-12T14:13:07 America/New_York. + This will create a time field with a granularity of seconds in the specified time zone. The time zone will be rendered as the specified time zone. + +4. If the input is one of the three Java time types, use that type. +5. A time string such as "10:30:45" will parse to a `LocaTime`. +6. A string with a date, time, and timezone such as "2021-04-12T14:13:07 America/New_York" will parse to a `ZonedDateTime`. +7. All other types will attempt to convert in this order: `LocaTime`, `Instant`, `ZonedDateTime`. + +The format of the time field and the type of the value passed to the `on_change` handler +is determined by the type of the following props in order of precedence: + +1. `value` +2. `default_value` +3. `placeholder_value` + +If none of these are provided, the `on_change` handler passes a range of `LocalTime`. + +```python +from deephaven import ui +from deephaven.time import to_j_local_time, dh_now, to_j_instant, to_j_zdt + +zoned_date_time = to_j_zdt("1995-03-22T11:11:11.23142 America/New_York") +instant = to_j_instant("2022-01-01T00:00:00 ET") +local_time = to_j_local_time("12:30:45") + + +@ui.component +def time_field_test(value): + time, set_time = ui.use_state(value) + return ui.time_field(on_change=set_time, value=time) + + +zoned_time_field = time_field_test(zoned_date_time) +instant_time_field = time_field_test(instant) +local_time_field = time_field_test(local_time) +``` + +## Value + +A time field displays a `placeholder` by default. An initial, uncontrolled value can be provided to the time field using the `defaultValue` prop. Alternatively, a controlled value can be provided using the `value` prop. + +```python +from deephaven import ui + + +@ui.component +def example(): + value, set_value = ui.use_state("11:45") + return ui.flex( + ui.time_field( + label="Time field (uncontrolled)", + default_value="11:45", + ), + ui.time_field( + label="Time field (controlled)", value=value, on_change=set_value + ), + gap="size-150", + wrap=True, + ) + + +my_example = example() +``` + +## Time zones + +Time field is time zone aware when `ZonedTimeTime` or `Instant` objects are provided as the value. In this case, the time zone abbreviation is displayed, and time zone concerns such as daylight saving time are taken into account when the value is manipulated. + +In most cases, your data will come from and be sent to a server as an `ISO 8601` formatted string. + +- For `ZonedTimeTime` objects, the time field displays the specified time zone. +- For `Instant` objects, the time field displays the time zone from the user settings. + +```python +from deephaven import ui +from deephaven.time import to_j_instant + +my_zoned_time_time = ui.time_field( + label="Time field", + default_value="2022-11-07T00:45 America/Los_Angeles", +) + +my_instant = ui.time_field( + label="Time field", + default_value=to_j_instant("2022-11-07T00:45Z"), +) +``` + +## Granularity + +The `granularity` prop allows you to control the smallest unit displayed by a time field. By default, values are displayed with "SECOND" granularity. + +In addition, when a value with a time is provided but you wish to display only the time, you can set the granularity to "DAY". This has no effect on the actual value (it still has a time component), only on what fields are displayed. In the following example, two time fields are synchronized with the same value but display different granularities. + +```python +from deephaven import ui + + +@ui.component +def granularity_example(): + value, set_value = ui.use_state("2021-04-07T18:45:22 UTC") + return ui.flex( + ui.time_field( + label="Time field and time field", + granularity="SECOND", + value=value, + on_change=set_value, + ), + ui.time_field( + label="Time field", granularity="HOUR", value=value, on_change=set_value + ), + gap="size-150", + wrap=True, + ) + + +my_granularity_example = granularity_example() +``` + +## HTML forms + +The time field supports the `name` prop for integration with HTML forms. The value will be submitted to the server as an `ISO 8601` formatted string, e.g., "08:45:00." + +```python +from deephaven import ui + +my_time_field_forms = ui.form( + ui.time_field(label="Meeting time", name="meetingTime"), + ui.button("Submit", type="submit"), + on_submit=print, +) +``` + +## Labeling + +A visual label should be provided for the time field using the `label` prop. If the time field is required, the `is_required` and `necessity_indicator` props can be used to show a required state. + +```python +from deephaven import ui + +my_time_field_labeling = ui.flex( + ui.time_field(label="Time field"), + ui.time_field(label="Time field", is_required=True, necessity_indicator="icon"), + ui.time_field(label="Time field", is_required=True, necessity_indicator="label"), + ui.time_field(label="Time field", necessity_indicator="label"), +) +``` + +## Events + +Time fields support selection through mouse, keyboard, and touch inputs via the `on_change` prop, which receives the value as an argument. + +```python +from deephaven import ui + + +@ui.component +def event_example(): + value, set_value = ui.use_state("11:45") + return ui.time_field( + label="Time field (controlled)", value=value, on_change=set_value + ) + + +my_event_example = event_example() +``` + +## Validation + +The `is_required` prop ensures that the user selects a time field. The related `validation_behaviour` prop allows the user to specify aria or native verification. + +When the prop is set to "native", the validation errors block form submission and are displayed as help text automatically. + +```python +from deephaven import ui + + +@ui.component +def time_field_validation_behaviour_example(): + return ui.form( + ui.time_field( + validation_behavior="native", + is_required=True, + ) + ) + + +my_time_field_validation_behaviour_example = time_field_validation_behaviour_example() +``` + +## Minimum and maximum values + +The `min_value` and `max_value` props can also be used to ensure the value is within a specific field. Time field also validates that the end time is after the start time. + +```python +from deephaven import ui + +my_time_field_basic = ui.time_field( + label="Time field", + min_value="11:00", + default_value="11:45", +) +``` + +## Label position + +By default, the position of a time field's label is above the time field, but it can be moved to the side using the `label_position` prop. + +```python +from deephaven import ui + + +@ui.component +def time_field_label_position_examples(): + return [ + ui.time_field( + label="Test Label", + ), + ui.time_field( + label="Test Label", + label_position="side", + ), + ] + + +my_time_field_label_position_examples = time_field_label_position_examples() +``` + +## Quiet state + +The `is_quiet` prop makes a time field "quiet". This can be useful when its corresponding styling should not distract users from surrounding content. + +```python +from deephaven import ui + + +my_time_field_is_quiet_example = ui.time_field( + is_quiet=True, +) +``` + +## Disabled state + +The `is_disabled` prop disables the time field to prevent user interaction. This is useful when the time field should be visible but not available for selection. + +```python +from deephaven import ui + + +my_time_field_is_disabled_example = ui.time_field( + is_disabled=True, +) +``` + +## Read only + +The `is_read_only` prop makes the time field's value immutable. The time field remains focusable, unlike when `is_disabled` is used. + +```python +from deephaven import ui + + +my_time_field_is_read_only_example = ui.time_field( + is_read_only=True, +) +``` + +## Help text + +A time field can have both a `description` and an `error_message`. Use the error message to offer specific guidance on how to correct the input. + +The `validation_state` prop can be used to set whether the current time field state is `valid` or `invalid`. + +```python +from deephaven import ui + + +@ui.component +def time_field_help_text_examples(): + return [ + ui.time_field( + label="Sample Label", + description="Enter a time field.", + ), + ui.time_field( + label="Sample Label", + validation_state="valid", + error_message="Sample invalid error message.", + ), + ui.time_field( + label="Sample Label", + validation_state="invalid", + error_message="Sample invalid error message.", + ), + ] + + +my_time_field_help_text_examples = time_field_help_text_examples() +``` + +## Contextual help + +Using the `contextual_help` prop, a `ui.contextual_help` can be placed next to the label to provide additional information about the time field. + +```python +from deephaven import ui + + +time_field_contextual_help_example = ui.time_field( + label="Sample Label", + contextual_help=ui.contextual_help(ui.heading("Content tips")), +) +``` + +## Custom width + +The `width` prop adjusts the width of a time field, and the `max_width` prop enforces a maximum width. + +```python +from deephaven import ui + + +@ui.component +def time_field_width_examples(): + return [ + ui.time_field( + width="size-3600", + ), + ui.time_field( + width="size-3600", + max_width="100%", + ), + ] + + +my_time_field_width_examples = time_field_width_examples() +``` + +## Hide time zone + +The time zone can be hidden using the `hide_time_zone` option. + +```python +from deephaven import ui + +my_hide_time_zone_example = ui.time_field( + label="Time field", + default_value="2022-11-07T00:45 America/Los_Angeles", + hide_time_zone=True, +) +``` + +## Hour cycle + +By default, time field displays times in either a `12` or `24` hour format depending on the user's locale. However, this can be overridden using the `hour_cycle` prop. + +```python +from deephaven import ui + + +time_field_hour_cycle_example = ui.time_field(label="Time field", hour_cycle=24) +``` + +## Time table filtering + +Time fields can be used to filter tables with time columns. + +```python +from deephaven.time import dh_now +from deephaven import time_table, ui + + +@ui.component +def time_table_filter(table, start_time, end_time, time_col="Timestamp"): + after_time, set_after_time = ui.use_state(start_time) + before_time, set_before_time = ui.use_state(end_time) + return [ + ui.time_field( + label="Start Time", + value=after_time, + on_change=set_after_time, + hour_cycle=24, + ), + ui.time_field( + label="End Time", + value=before_time, + on_change=set_before_time, + hour_cycle=24, + ), + table.where(f"{time_col} >= after_time && {time_col} < before_time"), + ] + + +SECONDS_IN_HOUR = 3600 +today = dh_now() +_table = time_table("PT1s").update_view( + ["Timestamp=today.plusSeconds(SECONDS_IN_HOUR*i)", "Row=i"] +) +time_filter = time_table_filter(_table, today, today.plusSeconds(SECONDS_IN_HOUR * 10)) +``` + +## API Reference + +```{eval-rst} +.. dhautofunction:: deephaven.ui.time_field +``` diff --git a/plugins/ui/src/deephaven/ui/_internal/utils.py b/plugins/ui/src/deephaven/ui/_internal/utils.py index 691abe679..59f480dd3 100644 --- a/plugins/ui/src/deephaven/ui/_internal/utils.py +++ b/plugins/ui/src/deephaven/ui/_internal/utils.py @@ -4,22 +4,26 @@ from inspect import signature import sys from functools import partial -from deephaven.time import to_j_instant, to_j_zdt, to_j_local_date +from deephaven.time import to_j_instant, to_j_zdt, to_j_local_date, to_j_local_time from deephaven.dtypes import ZonedDateTime, Instant -from ..types import Date, JavaDate, DateRange +from ..types import Date, JavaDate, DateRange, Time, JavaTime _UNSAFE_PREFIX = "UNSAFE_" _ARIA_PREFIX = "aria_" _ARIA_PREFIX_REPLACEMENT = "aria-" -_CONVERTERS = { +_DATE_CONVERTERS = { "java.time.Instant": to_j_instant, "java.time.ZonedDateTime": to_j_zdt, "java.time.LocalDate": to_j_local_date, } -_LOCAL_DATE = "java.time.LocalDate" +_TIME_CONVERTERS = { + "java.time.ZonedDateTime": to_j_zdt, + "java.time.Instant": to_j_instant, + "java.time.LocalTime": to_j_local_time, +} def get_component_name(component: Any) -> str: @@ -257,6 +261,49 @@ def _convert_to_java_date( ) +def _convert_to_java_time( + time: Time, +) -> JavaTime: + """ + Convert a Time to a Java time type. + In order of preference, tries to convert to LocalTime, Instant, ZonedDateTime. + If none of these work, raises a TypeError. + + Args: + time: The time to convert to a Java time type. + + Returns: + The Java time type. + """ + try: + return to_j_local_time(time) # type: ignore + except Exception: + # ignore, try next + pass + + # For strings, parseInstant and parseZonedDateTime both succeed for the same strings + # Try parsing as a ZonedDateTime first per the documentation + if isinstance(time, str): + try: + return to_j_zdt(time) # type: ignore + except Exception: + # ignore, try next + pass + + try: + return to_j_instant(time) # type: ignore + except Exception: + # ignore, try next + pass + + try: + return to_j_zdt(time) # type: ignore + except Exception: + raise TypeError( + f"Could not convert {time} to one of LocalTime, Instant, or ZonedDateTime." + ) + + def get_jclass_name(value: Any) -> str: """ Get the name of the Java class of the value. @@ -270,7 +317,7 @@ def get_jclass_name(value: Any) -> str: return str(value.jclass)[6:] -def _jclass_converter( +def _jclass_date_converter( value: JavaDate, ) -> Callable[[Date], Any]: """ @@ -282,7 +329,22 @@ def _jclass_converter( Returns: The converter for the Java date type. """ - return _CONVERTERS[get_jclass_name(value)] + return _DATE_CONVERTERS[get_jclass_name(value)] + + +def _jclass_time_converter( + value: JavaTime, +) -> Callable[[Time], Any]: + """ + Get the converter for the Java time type. + + Args: + value: The Java time type to get the converter for. + + Returns: + The converter for the Java time type. + """ + return _TIME_CONVERTERS[get_jclass_name(value)] def _wrap_date_callable( @@ -312,6 +374,33 @@ def no_error_date_callable(date: Date) -> None: return no_error_date_callable +def _wrap_time_callable( + time_callable: Callable[[Time], None], + converter: Callable[[Time], Any], +) -> Callable[[Time], None]: + """ + Wrap a callable to convert the Time argument to a Java time type. + This maintains the original callable signature so that the Time argument can be dropped. + + Args: + time_callable: The callable to wrap. + converter: The time converter to use. + + Returns: + The wrapped callable. + """ + # When the user is typing a time, they may enter a value that does not parse + # This will skip those errors rather than printing them to the screen + def no_error_time_callable(time: Time) -> None: + wrapped_time_callable = wrap_callable(time_callable) + try: + wrapped_time_callable(converter(time)) + except Exception: + pass + + return no_error_time_callable + + def _get_first_set_key(props: dict[str, Any], sequence: Sequence[str]) -> str | None: """ Of the keys in sequence, get the first key that has a non-None value in props. @@ -345,7 +434,7 @@ def _date_or_range(value: JavaDate | DateRange) -> Any: return value -def _prioritized_callable_converter( +def _prioritized_date_callable_converter( props: dict[str, Any], priority: Sequence[str], default_converter: Callable[[Date], Any], @@ -368,7 +457,36 @@ def _prioritized_callable_converter( first_set_key = _get_first_set_key(props, priority) return ( - _jclass_converter(_date_or_range(props[first_set_key])) + _jclass_date_converter(_date_or_range(props[first_set_key])) + if first_set_key is not None + else default_converter + ) + + +def _prioritized_time_callable_converter( + props: dict[str, Any], + priority: Sequence[str], + default_converter: Callable[[Time], Any], +) -> Callable[[Time], Any]: + """ + Get a callable time converter based on the type of the first non-None prop set. + Checks the props in the order provided by the `priority` sequence. + All the props in `priority` should be Java time types already. + We do this so conversion so that the type returned on callbacks matches the type passed in by the user. + If none of the props in `priority` are present, returns the default converter. + + Args: + props: The props passed to the component. + priority: The priority of the props to check. + default_converter: The default converter to use if none of the priority props are present. + + Returns: + The callable time converter. + """ + + first_set_key = _get_first_set_key(props, priority) + return ( + _jclass_time_converter(props[first_set_key]) if first_set_key is not None else default_converter ) @@ -477,7 +595,7 @@ def convert_date_props( props[key] = convert_date_range(props[key], _convert_to_java_date) # the simple props must be converted before this to simplify the callable conversion - converter = _prioritized_callable_converter(props, priority, default_converter) + converter = _prioritized_date_callable_converter(props, priority, default_converter) # based on the convert set the granularity if it is not set # Local Dates will default to DAY but we need to default to SECOND for the other types @@ -506,6 +624,48 @@ def convert_date_props( props[key] = _wrap_date_callable(props[key], converter) +def convert_time_props( + props: dict[str, Any], + simple_time_props: set[str], + callable_time_props: set[str], + priority: Sequence[str], + default_converter: Callable[[Time], Any] = to_j_local_time, +) -> None: + """ + Convert time props to Java time types in place. + + Args: + props: The props passed to the component. + simple_time_props: A set of simple time keys to convert. The prop value should be a single Time. + callable_time_props: A set of callable time keys to convert. + The prop value should be a callable that takes a Time. + priority: The priority of the props to check. + granularity_key: The key for the granularity + default_converter: The default converter to use if none of the priority props are present. + + Returns: + The converted props. + """ + for key in simple_time_props: + if props.get(key) is not None: + props[key] = _convert_to_java_time(props[key]) + + # the simple props must be converted before this to simplify the callable conversion + converter = _prioritized_time_callable_converter(props, priority, default_converter) + + # now that the converter is set, we can convert simple props to strings + for key in simple_time_props: + if props.get(key) is not None: + props[key] = str(props[key]) + + # wrap the date callable with the convert + for key in callable_time_props: + if props.get(key) is not None: + if not callable(props[key]): + raise TypeError(f"{key} must be a callable") + props[key] = _wrap_time_callable(props[key], converter) + + def unpack_item_table_source( children: tuple[Any, ...], props: dict[str, Any], diff --git a/plugins/ui/src/deephaven/ui/components/__init__.py b/plugins/ui/src/deephaven/ui/components/__init__.py index 70d61632c..54926ffc5 100644 --- a/plugins/ui/src/deephaven/ui/components/__init__.py +++ b/plugins/ui/src/deephaven/ui/components/__init__.py @@ -50,6 +50,7 @@ from .text import text from .text_area import text_area from .text_field import text_field +from .time_field import time_field from .toggle_button import toggle_button from .view import view @@ -108,6 +109,7 @@ "text", "text_area", "text_field", + "time_field", "toggle_button", "view", ] diff --git a/plugins/ui/src/deephaven/ui/components/time_field.py b/plugins/ui/src/deephaven/ui/components/time_field.py new file mode 100644 index 000000000..db5d5a98d --- /dev/null +++ b/plugins/ui/src/deephaven/ui/components/time_field.py @@ -0,0 +1,248 @@ +from __future__ import annotations + +from typing import Any, Sequence, Callable + +from .types import ( + FocusEventCallable, + KeyboardEventCallable, + LayoutFlex, + DimensionValue, + AlignSelf, + JustifySelf, + Position, + AriaPressed, + CSSProperties, + LabelPosition, + ValidationBehavior, + NecessityIndicator, + ValidationState, + HourCycle, + Alignment, +) + +from ..elements import Element +from .._internal.utils import ( + create_props, + convert_time_props, +) +from ..types import Time, TimeGranularity +from .basic import component_element +from .make_component import make_component + +TimeFieldElement = Element + +# All the props that can be time types +_SIMPLE_TIME_PROPS = { + "placeholder_value", + "value", + "default_value", + "min_value", + "max_value", +} +_CALLABLE_TIME_PROPS = {"on_change"} + +# The priority of the time props to determine the format of the time passed to the callable time props +_TIME_PROPS_PRIORITY = ["value", "default_value", "placeholder_value"] + + +def _convert_time_field_props( + props: dict[str, Any], +) -> dict[str, Any]: + """ + Convert time field props to Java time types. + + Args: + props: The props passed to the time field. + + Returns: + The converted props. + """ + + convert_time_props( + props, + _SIMPLE_TIME_PROPS, + _CALLABLE_TIME_PROPS, + _TIME_PROPS_PRIORITY, + ) + + return props + + +@make_component +def time_field( + placeholder_value: Time | None = None, + value: Time | None = None, + default_value: Time | None = None, + min_value: Time | None = None, + max_value: Time | None = None, + granularity: TimeGranularity | None = "SECOND", + hour_cycle: HourCycle | None = None, + hide_time_zone: bool = False, + should_force_leading_zeros: bool | None = None, + is_disabled: bool | None = None, + is_read_only: bool | None = None, + is_required: bool | None = None, + validation_behavior: ValidationBehavior | None = None, + auto_focus: bool | None = None, + label: Element | None = None, + description: Element | None = None, + error_message: Element | None = None, + name: str | None = None, + is_quiet: bool | None = None, + label_position: LabelPosition | None = None, + label_align: Alignment | None = None, + necessity_indicator: NecessityIndicator | None = None, + contextual_help: Element | None = None, + validation_state: ValidationState | None = None, + on_focus: FocusEventCallable | None = None, + on_blur: FocusEventCallable | None = None, + on_focus_change: Callable[[bool], None] | None = None, + on_key_down: KeyboardEventCallable | None = None, + on_key_up: KeyboardEventCallable | None = None, + on_open_change: Callable[[bool], None] | None = None, + on_change: Callable[[Time], None] | None = None, + flex: LayoutFlex | None = None, + flex_grow: float | None = None, + flex_shrink: float | None = None, + flex_basis: DimensionValue | None = None, + align_self: AlignSelf | None = None, + justify_self: JustifySelf | None = None, + order: int | None = None, + grid_area: str | None = None, + grid_row: str | None = None, + grid_row_start: str | None = None, + grid_row_end: str | None = None, + grid_column: str | None = None, + grid_column_start: str | None = None, + grid_column_end: str | None = None, + margin: DimensionValue | None = None, + margin_top: DimensionValue | None = None, + margin_bottom: DimensionValue | None = None, + margin_start: DimensionValue | None = None, + margin_end: DimensionValue | None = None, + margin_x: DimensionValue | None = None, + margin_y: DimensionValue | None = None, + width: DimensionValue | None = None, + height: DimensionValue | None = None, + min_width: DimensionValue | None = None, + min_height: DimensionValue | None = None, + max_width: DimensionValue | None = None, + max_height: DimensionValue | None = None, + position: Position | None = None, + top: DimensionValue | None = None, + bottom: DimensionValue | None = None, + start: DimensionValue | None = None, + end: DimensionValue | None = None, + left: DimensionValue | None = None, + right: DimensionValue | None = None, + z_index: int | None = None, + is_hidden: bool | None = None, + id: str | None = None, + aria_label: str | None = None, + aria_labelledby: str | None = None, + aria_describedby: str | None = None, + aria_pressed: AriaPressed | None = None, + aria_details: str | None = None, + UNSAFE_class_name: str | None = None, + UNSAFE_style: CSSProperties | None = None, + key: str | None = None, +) -> TimeFieldElement: + """ + A time field allows the user to select a time. + + + Args: + placeholder_value: A placeholder time that influences the format of the + placeholder shown when no value is selected. Defaults to 12:00 AM or + 00:00 depending on the hour cycle. + value: The current value (controlled). + default_value: The default value (uncontrolled). + min_value: The minimum allowed time that a user may select. + max_value: The maximum allowed time that a user may select. + granularity: Determines the smallest unit that is displayed in the time field. + By default, this is `"SECOND"`. + hour_cycle: Whether to display the time in 12 or 24 hour format. + By default, this is determined by the user's locale. + hide_time_zone: Whether to hide the time zone abbreviation. + should_force_leading_zeros: Whether to force leading zeros in the time field. + is_disabled: Whether the input is disabled. + is_read_only: Whether the input can be selected but not changed by the user. + is_required: Whether user input is required on the input before form submission. + validation_behavior: Whether to use native HTML form validation to prevent form + submission when the value is missing or invalid, + or mark the field as required or invalid via ARIA. + auto_focus: Whether the element should receive focus on render. + label: The content to display as the label. + description: A description for the field. + Provides a hint such as specific requirements for what to choose. + error_message: An error message for the field. + name: The name of the input element, used when submitting an HTML form. + is_quiet: Whether the time field should be displayed with a quiet style. + label_position: The label's overall position relative to the element it is labeling. + label_align: The label's horizontal alignment relative to the element it is labeling. + necessity_indicator: Whether the required state should be shown as an icon or text. + contextual_help: A ContextualHelp element to place next to the label. + validation_state: Whether the input should display its "valid" or "invalid" visual styling. + on_focus: Function called when the button receives focus. + on_blur: Function called when the button loses focus. + on_focus_change: Function called when the focus state changes. + on_key_down: Function called when a key is pressed. + on_key_up: Function called when a key is released. + on_open_change: Handler that is called when the overlay's open state changes. + on_change: Handler that is called when the value changes. + The exact `Time` type will be the same as the type passed to + `value`, `default_value` or `placeholder_value`, in that order of precedence. + flex: When used in a flex layout, specifies how the element will grow or shrink to fit the space available. + flex_grow: When used in a flex layout, specifies how much the element will grow to fit the space available. + flex_shrink: When used in a flex layout, specifies how much the element will shrink to fit the space available. + flex_basis: When used in a flex layout, specifies the initial size of the element. + align_self: Overrides the align_items property of a flex or grid container. + justify_self: Specifies how the element is justified inside a flex or grid container. + order: The layout order for the element within a flex or grid container. + grid_area: The name of the grid area to place the element in. + grid_row: The name of the grid row to place the element in. + grid_row_start: The name of the grid row to start the element in. + grid_row_end: The name of the grid row to end the element in. + grid_column: The name of the grid column to place the element in. + grid_column_start: The name of the grid column to start the element in. + grid_column_end: The name of the grid column to end the element in. + margin: The margin to apply around the element. + margin_top: The margin to apply above the element. + margin_bottom: The margin to apply below the element. + margin_start: The margin to apply before the element. + margin_end: The margin to apply after the element. + margin_x: The margin to apply to the left and right of the element. + margin_y: The margin to apply to the top and bottom of the element. + width: The width of the element. + height: The height of the element. + min_width: The minimum width of the element. + min_height: The minimum height of the element. + max_width: The maximum width of the element. + max_height: The maximum height of the element. + position: Specifies how the element is positioned. + top: The distance from the top of the containing element. + bottom: The distance from the bottom of the containing element. + start: The distance from the start of the containing element. + end: The distance from the end of the containing element. + left: The distance from the left of the containing element. + right: The distance from the right of the containing element. + z_index: The stack order of the element. + is_hidden: Whether the element is hidden. + id: A unique identifier for the element. + aria_label: The label for the element. + aria_labelledby: The id of the element that labels the element. + aria_describedby: The id of the element that describes the element. + aria_pressed: Whether the element is pressed. + aria_details: The details for the element. + UNSAFE_class_name: A CSS class to apply to the element. + UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. + + Returns: + The time field element. + """ + _, props = create_props(locals()) + + _convert_time_field_props(props) + + return component_element("TimeField", **props) diff --git a/plugins/ui/src/deephaven/ui/types/types.py b/plugins/ui/src/deephaven/ui/types/types.py index e4f0086bd..d0fa28513 100644 --- a/plugins/ui/src/deephaven/ui/types/types.py +++ b/plugins/ui/src/deephaven/ui/types/types.py @@ -506,6 +506,27 @@ class SliderChange(TypedDict): TabDensity = Literal["compact", "regular"] Dependencies = Union[Tuple[Any], List[Any]] Selection = Sequence[Key] +LocalTime = DType +JavaTime = Union[LocalTime, Instant, ZonedDateTime] +LocalTimeConvertible = Union[ + None, + LocalTime, + int, + str, + datetime.time, + datetime.datetime, + numpy.datetime64, + pandas.Timestamp, +] +Time = Union[ + LocalTime, + Instant, + ZonedDateTime, + LocalTimeConvertible, + InstantConvertible, + ZonedDateTimeConvertible, +] +TimeGranularity = Literal["HOUR", "MINUTE", "SECOND"] class DateRange(TypedDict): diff --git a/plugins/ui/src/js/src/elements/DateField.tsx b/plugins/ui/src/js/src/elements/DateField.tsx index 1ae0a50cf..60f79b59a 100644 --- a/plugins/ui/src/js/src/elements/DateField.tsx +++ b/plugins/ui/src/js/src/elements/DateField.tsx @@ -59,7 +59,7 @@ export function DateField( value instanceof ZonedDateTime ) { const newValue = toTimeZone(value, timeZone); - onChange(newValue); + onChange?.(newValue); } }, [isDateFieldInstantValue, value, onChange, timeZone, prevTimeZone]); diff --git a/plugins/ui/src/js/src/elements/DatePicker.tsx b/plugins/ui/src/js/src/elements/DatePicker.tsx index 0c3567c59..00005075f 100644 --- a/plugins/ui/src/js/src/elements/DatePicker.tsx +++ b/plugins/ui/src/js/src/elements/DatePicker.tsx @@ -59,7 +59,7 @@ export function DatePicker( value instanceof ZonedDateTime ) { const newValue = toTimeZone(value, timeZone); - onChange(newValue); + onChange?.(newValue); } }, [isDatePickerInstantValue, value, onChange, timeZone, prevTimeZone]); diff --git a/plugins/ui/src/js/src/elements/DateRangePicker.tsx b/plugins/ui/src/js/src/elements/DateRangePicker.tsx index 25724e565..38c492b61 100644 --- a/plugins/ui/src/js/src/elements/DateRangePicker.tsx +++ b/plugins/ui/src/js/src/elements/DateRangePicker.tsx @@ -65,7 +65,7 @@ export function DateRangePicker( const newStart = toTimeZone(value.start, timeZone); const newEnd = toTimeZone(value.end, timeZone); const newValue = { start: newStart, end: newEnd }; - onChange(newValue); + onChange?.(newValue); } }, [isDateRangePickerInstantValue, value, onChange, timeZone, prevTimeZone]); diff --git a/plugins/ui/src/js/src/elements/TimeField.tsx b/plugins/ui/src/js/src/elements/TimeField.tsx new file mode 100644 index 000000000..ce5a9fd80 --- /dev/null +++ b/plugins/ui/src/js/src/elements/TimeField.tsx @@ -0,0 +1,74 @@ +import React, { useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { + TimeField as DHCTimeField, + TimeFieldProps as DHCTimeFieldProps, +} from '@deephaven/components'; +import { usePrevious } from '@deephaven/react-hooks'; +import { getSettings, RootState } from '@deephaven/redux'; +import { toTimeZone, ZonedDateTime } from '@internationalized/date'; +import useDebouncedOnChange from './hooks/useDebouncedOnChange'; +import { + SerializedTimeComponentProps, + useTimeComponentProps, +} from './hooks/useTimeComponentProps'; +import { TimeValue, isStringInstant } from './utils/DateTimeUtils'; + +const EMPTY_FUNCTION = () => undefined; + +function isTimeFieldInstant( + props: SerializedTimeComponentProps> +): boolean { + const { value, defaultValue, placeholderValue } = props; + if (value != null) { + return isStringInstant(value); + } + if (defaultValue != null) { + return isStringInstant(defaultValue); + } + return isStringInstant(placeholderValue); +} + +export function TimeField( + props: SerializedTimeComponentProps> +): JSX.Element { + const isTimeFieldInstantValue = isTimeFieldInstant(props); + const settings = useSelector(getSettings); + const { timeZone } = settings; + + const { + defaultValue = null, + value: propValue, + onChange: propOnChange = EMPTY_FUNCTION, + ...otherProps + } = useTimeComponentProps(props, timeZone); + + const [value, onChange] = useDebouncedOnChange( + propValue ?? defaultValue, + propOnChange + ); + + // When the time zone changes, the serialized prop value will change, so we need to update the value state + const prevTimeZone = usePrevious(timeZone); + useEffect(() => { + // The timezone is intially undefined, so we don't want to trigger a change in that case + if ( + isTimeFieldInstantValue && + prevTimeZone !== undefined && + timeZone !== prevTimeZone && + value instanceof ZonedDateTime + ) { + const newValue = toTimeZone(value, timeZone); + onChange?.(newValue); + } + }, [isTimeFieldInstantValue, value, onChange, timeZone, prevTimeZone]); + + return ( + // eslint-disable-next-line react/jsx-props-no-spreading + + ); +} + +TimeField.displayName = 'TimeField'; + +export default TimeField; diff --git a/plugins/ui/src/js/src/elements/hooks/index.ts b/plugins/ui/src/js/src/elements/hooks/index.ts index 8658d4a98..035e577bc 100644 --- a/plugins/ui/src/js/src/elements/hooks/index.ts +++ b/plugins/ui/src/js/src/elements/hooks/index.ts @@ -9,4 +9,6 @@ export * from './usePickerProps'; export * from './usePressEventCallback'; export * from './useReExportedTable'; export * from './useSelectionProps'; +export * from './useTimeComponentProps'; +export * from './useTimeValueMemo'; export * from './useDebouncedOnChange'; diff --git a/plugins/ui/src/js/src/elements/hooks/useTimeComponentProps.ts b/plugins/ui/src/js/src/elements/hooks/useTimeComponentProps.ts new file mode 100644 index 000000000..45b1e5c08 --- /dev/null +++ b/plugins/ui/src/js/src/elements/hooks/useTimeComponentProps.ts @@ -0,0 +1,212 @@ +import { useCallback, useMemo } from 'react'; +import { + DeserializedFocusEventCallback, + SerializedFocusEventCallback, + useFocusEventCallback, +} from './useFocusEventCallback'; +import { + DeserializedKeyboardEventCallback, + SerializedKeyboardEventCallback, + useKeyboardEventCallback, +} from './useKeyboardEventCallback'; +import useTimeValueMemo from './useTimeValueMemo'; +import { + TimeValue, + TimeGranularity, + MappedTimeValue, + parseNullableTimeValue, +} from '../utils/DateTimeUtils'; + +export type SerializedTimeValue = string | null; + +export type SerializedTimeValueCallback = (value: SerializedTimeValue) => void; + +export type DeserializedTimeValueCallback = + | (() => void) + | ((value: MappedTimeValue | null) => Promise); + +export interface SerializedTimeComponentPropsInterface { + /** Handler that is called when the element receives focus. */ + onFocus?: SerializedFocusEventCallback; + + /** Handler that is called when the element loses focus. */ + onBlur?: SerializedFocusEventCallback; + + /** Handler that is called when a key is pressed */ + onKeyDown?: SerializedKeyboardEventCallback; + + /** Handler that is called when a key is released */ + onKeyUp?: SerializedKeyboardEventCallback; + + /** Handler that is called when the value changes */ + onChange?: SerializedTimeValueCallback; + + /** The current value (controlled) */ + value?: string | null; + + /** The default value (uncontrolled) */ + defaultValue?: string | null; + + /** The minimum allowed time that a user may select */ + minValue?: string; + + /** The maximum allowed time that a user may select */ + maxValue?: string; + + /** A placeholder time that influences the format of the placeholder shown when no value is selected */ + placeholderValue?: string; + + /** Determines the smallest unit that is displayed in the time component. */ + granularity?: TimeGranularity; +} + +export interface DeserializedTimeComponentPropsInterface { + /** Handler that is called when the element receives focus. */ + onFocus?: DeserializedFocusEventCallback; + + /** Handler that is called when the element loses focus. */ + onBlur?: DeserializedFocusEventCallback; + + /** Handler that is called when a key is pressed */ + onKeyDown?: DeserializedKeyboardEventCallback; + + /** Handler that is called when a key is released */ + onKeyUp?: DeserializedKeyboardEventCallback; + + /** Handler that is called when the value changes */ + onChange?: DeserializedTimeValueCallback; + + /** The current value (controlled) */ + value?: TimeValue | null; + + /** The default value (uncontrolled) */ + defaultValue?: TimeValue | null; + + /** The minimum allowed time that a user may select */ + minValue?: TimeValue; + + /** The maximum allowed time that a user may select */ + maxValue?: TimeValue; + + /** A placeholder time that influences the format of the placeholder shown when no value is selected */ + placeholderValue?: TimeValue; + + /** Determines the smallest unit that is displayed in the time component. */ + granularity?: TimeGranularity; +} + +export type SerializedTimeComponentProps = TProps & + SerializedTimeComponentPropsInterface; + +export type DeserializedTimeComponentProps = Omit< + TProps, + keyof SerializedTimeComponentPropsInterface +> & + DeserializedTimeComponentPropsInterface; + +/** + * Uses the toString representation of the TimeValue as the serialized value. + * @param value TimeValue to serialize + * @returns Serialized TimeValue + */ +export function serializeTimeValue( + value?: MappedTimeValue +): SerializedTimeValue { + if (value == null) { + return null; + } + + return value.toString(); +} + +/** + * Get a callback function that can be passed to the onChange event handler + * props of a Spectrum Time component. + * @param callback Callback to be called with the serialized value + * @returns A callback to be passed into the Spectrum component that transforms + * the value and calls the provided callback + */ +export function useOnChangeTimeCallback( + callback?: SerializedTimeValueCallback +): DeserializedTimeValueCallback { + return useCallback( + (value?: MappedTimeValue) => { + if (callback == null) { + return; + } + callback(serializeTimeValue(value)); + }, + [callback] + ); +} + +/** + * Use memo to get a TimeValue from a nullable string. + * + * @param value the string time value + * @returns TimeValue or null + */ +export function useNullableTimeValueMemo( + timeZone: string, + value?: string | null +): TimeValue | null | undefined { + return useMemo( + () => parseNullableTimeValue(timeZone, value), + [timeZone, value] + ); +} + +/** + * Wrap Time component props with the appropriate serialized event callbacks. + * @param props Props to wrap + * @returns Wrapped props + */ +export function useTimeComponentProps( + { + onFocus, + onBlur, + onKeyDown, + onKeyUp, + onChange: serializedOnChange, + value: serializedValue, + defaultValue: serializedDefaultValue, + minValue: serializedMinValue, + maxValue: serializedMaxValue, + placeholderValue: serializedPlaceholderValue, + granularity: upperCaseTimeGranularity, + ...otherProps + }: SerializedTimeComponentProps, + timeZone: string +): DeserializedTimeComponentProps { + const serializedOnFocus = useFocusEventCallback(onFocus); + const serializedOnBlur = useFocusEventCallback(onBlur); + const serializedOnKeyDown = useKeyboardEventCallback(onKeyDown); + const serializedOnKeyUp = useKeyboardEventCallback(onKeyUp); + const onChange = useOnChangeTimeCallback(serializedOnChange); + const deserializedValue = useNullableTimeValueMemo(timeZone, serializedValue); + const deserializedDefaultValue = useNullableTimeValueMemo( + timeZone, + serializedDefaultValue + ); + const deserializedMinValue = useTimeValueMemo(timeZone, serializedMinValue); + const deserializedMaxValue = useTimeValueMemo(timeZone, serializedMaxValue); + const deserializedPlaceholderValue = useTimeValueMemo( + timeZone, + serializedPlaceholderValue + ); + + return { + onFocus: serializedOnFocus, + onBlur: serializedOnBlur, + onKeyDown: serializedOnKeyDown, + onKeyUp: serializedOnKeyUp, + onChange: serializedOnChange == null ? undefined : onChange, + value: deserializedValue, + defaultValue: deserializedDefaultValue, + minValue: deserializedMinValue, + maxValue: deserializedMaxValue, + placeholderValue: deserializedPlaceholderValue, + granularity: upperCaseTimeGranularity?.toLowerCase() as TimeGranularity, + ...otherProps, + }; +} diff --git a/plugins/ui/src/js/src/elements/hooks/useTimeValueMemo.ts b/plugins/ui/src/js/src/elements/hooks/useTimeValueMemo.ts new file mode 100644 index 000000000..6ffb13c3f --- /dev/null +++ b/plugins/ui/src/js/src/elements/hooks/useTimeValueMemo.ts @@ -0,0 +1,15 @@ +import { useMemo } from 'react'; +import { TimeValue, parseTimeValue } from '../utils/DateTimeUtils'; + +/** + * Use memo to get a TimeValue from a string. + * + * @param value the string time value + * @returns TimeValue + */ +export default function useDateTimeMemo( + timeZone: string, + value?: string +): TimeValue | undefined { + return useMemo(() => parseTimeValue(timeZone, value), [timeZone, value]); +} diff --git a/plugins/ui/src/js/src/elements/index.ts b/plugins/ui/src/js/src/elements/index.ts index 6e4ec7db4..afeb7ed56 100644 --- a/plugins/ui/src/js/src/elements/index.ts +++ b/plugins/ui/src/js/src/elements/index.ts @@ -25,6 +25,7 @@ export * from './Tabs'; export * from './TabPanels'; export * from './TextField'; export * from './TextArea'; +export * from './TimeField'; export * from './ToggleButton'; export * from './UITable/UITable'; export * from './utils'; diff --git a/plugins/ui/src/js/src/elements/model/ElementConstants.ts b/plugins/ui/src/js/src/elements/model/ElementConstants.ts index 3bee12e86..ca409d9a5 100644 --- a/plugins/ui/src/js/src/elements/model/ElementConstants.ts +++ b/plugins/ui/src/js/src/elements/model/ElementConstants.ts @@ -63,6 +63,7 @@ export const ELEMENT_NAME = { text: uiComponentName('Text'), textArea: uiComponentName('TextArea'), textField: uiComponentName('TextField'), + timeField: uiComponentName('TimeField'), toggleButton: uiComponentName('ToggleButton'), view: uiComponentName('View'), } as const; diff --git a/plugins/ui/src/js/src/elements/utils/DateTimeUtils.test.ts b/plugins/ui/src/js/src/elements/utils/DateTimeUtils.test.ts index 77cda081d..c61524f1c 100644 --- a/plugins/ui/src/js/src/elements/utils/DateTimeUtils.test.ts +++ b/plugins/ui/src/js/src/elements/utils/DateTimeUtils.test.ts @@ -2,6 +2,8 @@ import { parseDateValue, parseNullableDateValue, isStringInstant, + parseTimeValue, + parseNullableTimeValue, } from './DateTimeUtils'; const DEFAULT_TIME_ZONE = 'UTC'; @@ -81,3 +83,66 @@ describe('parseDateValue', () => { ); }); }); + +describe('parseTimeValue', () => { + const isoTime = '04:05:06'; + const isoDateTime = '2021-03-03T04:05:06'; + const isoZonedDateTime = '2021-04-04T05:06:07-04:00[America/New_York]'; + const nonIsoZonedDateTime = '2021-04-04T05:06:07 America/New_York'; + const instantString = '2021-03-03T04:05:06Z'; + const instantStringUTC = '2021-03-03T04:05:06Z[UTC]'; + const utcOutput = '2021-03-03T04:05:06+00:00[UTC]'; + const nyOutput = '2021-03-02T23:05:06-05:00[America/New_York]'; + const invalidTime = 'invalid-time'; + + it('should return null if the value is null', () => { + expect(parseNullableTimeValue(DEFAULT_TIME_ZONE, null)).toBeNull(); + }); + + it('should return undefined if the value is undefined', () => { + expect(parseTimeValue(DEFAULT_TIME_ZONE, undefined)).toBeUndefined(); + }); + + it('should parse an ISO 8601 time string', () => { + expect(parseTimeValue(DEFAULT_TIME_ZONE, isoTime)?.toString()).toEqual( + isoTime + ); + }); + + it('should parse an ISO 8601 date time string', () => { + expect(parseTimeValue(DEFAULT_TIME_ZONE, isoDateTime)?.toString()).toEqual( + isoDateTime + ); + }); + + it('should parse an ISO 8601 zoned date time string', () => { + expect( + parseTimeValue(DEFAULT_TIME_ZONE, isoZonedDateTime)?.toString() + ).toEqual(isoZonedDateTime); + }); + + it('should parse a non-ISO 8601 zoned date time string', () => { + expect( + parseTimeValue(DEFAULT_TIME_ZONE, nonIsoZonedDateTime)?.toString() + ).toEqual(isoZonedDateTime); + }); + + it('should parse an instant string', () => { + expect( + parseTimeValue(DEFAULT_TIME_ZONE, instantString)?.toString() + ).toEqual(utcOutput); + expect( + parseTimeValue(DEFAULT_TIME_ZONE, instantStringUTC)?.toString() + ).toEqual(utcOutput); + }); + + it('should throw an error if the value is invalid', () => { + expect(() => parseTimeValue(DEFAULT_TIME_ZONE, invalidTime)).toThrow(); + }); + + it('should parse an instant time string with a different time zone', () => { + expect(parseTimeValue(NY_TIME_ZONE, instantString)?.toString()).toEqual( + nyOutput + ); + }); +}); diff --git a/plugins/ui/src/js/src/elements/utils/DateTimeUtils.ts b/plugins/ui/src/js/src/elements/utils/DateTimeUtils.ts index 9c377245f..6123108e6 100644 --- a/plugins/ui/src/js/src/elements/utils/DateTimeUtils.ts +++ b/plugins/ui/src/js/src/elements/utils/DateTimeUtils.ts @@ -3,9 +3,11 @@ import { CalendarDate, CalendarDateTime, ZonedDateTime, + Time, parseDate, parseDateTime, parseZonedDateTime, + parseTime, toTimeZone, } from '@internationalized/date'; @@ -19,6 +21,20 @@ export type MappedDateValue = T extends ZonedDateTime export type Granularity = 'day' | 'hour' | 'minute' | 'second'; +export type TimeValue = Time | CalendarDateTime | ZonedDateTime; + +export type TimeGranularity = 'hour' | 'minute' | 'second'; + +export type MappedTimeValue = T extends ZonedDateTime + ? ZonedDateTime + : T extends CalendarDateTime + ? CalendarDateTime + : T extends Time + ? Time + : never; + +type DateTimeValue = CalendarDateTime | ZonedDateTime; + /** * Checks if a string is an Instant. * @@ -51,6 +67,43 @@ export function parseDateValue( // ignore } + const dateTime = parseDateTimeInternal(timeZone, value); + if (dateTime != null) { + return dateTime; + } + + throw new Error(`Invalid date value string: ${value}`); +} + +/** + * Parses a date value string into a DateValue. Allows null. + * + * @param timeZone the time zone to use + * @param value the string date value + * @returns DateValue or null + */ +export function parseNullableDateValue( + timeZone: string, + value?: string | null +): DateValue | null | undefined { + if (value === null) { + return value; + } + + return parseDateValue(timeZone, value); +} + +/** + * Common parsing used for both DateTimes and Times. + * + * @param timeZone the time zone to use + * @param value the string date value + * @returns a DateTimeValue or null + */ +function parseDateTimeInternal( + timeZone: string, + value: string +): DateTimeValue | null { // Note that the Python API will never send a string like this. This is here for correctness. // Try to parse an ISO 8601 date time string, e.g. "2021-03-03T04:05:06" try { @@ -100,23 +153,53 @@ export function parseDateValue( } } - throw new Error(`Invalid date value string: ${value}`); + return null; } /** - * Parses a date value string into a DateValue. Allows null. + * Parses a time value string into a TimeValue. * * @param timeZone the time zone to use * @param value the string date value - * @returns DateValue or null + * @returns TimeValue */ -export function parseNullableDateValue( +export function parseTimeValue( + timeZone: string, + value?: string +): TimeValue | undefined { + if (value === undefined) { + return value; + } + + // Try to parse and ISO 8601 time string, e.g. "04:05:06" + try { + return parseTime(value); + } catch (ignore) { + // ignore + } + + const dateTime = parseDateTimeInternal(timeZone, value); + if (dateTime != null) { + return dateTime; + } + + throw new Error(`Invalid time value string: ${value}`); +} + +/** + * Parses a time value string into a TimeValue. Allows null. + * + * @param timeZone the time zone to use + * @param value the string time value + * @returns TimeValue or null + */ +export function parseNullableTimeValue( timeZone: string, value?: string | null -): DateValue | null | undefined { +): TimeValue | null | undefined { if (value === null) { return value; } - return parseDateValue(timeZone, value); + return parseTimeValue(timeZone, value); } diff --git a/plugins/ui/src/js/src/widget/WidgetUtils.tsx b/plugins/ui/src/js/src/widget/WidgetUtils.tsx index 9b8bae277..37d29803c 100644 --- a/plugins/ui/src/js/src/widget/WidgetUtils.tsx +++ b/plugins/ui/src/js/src/widget/WidgetUtils.tsx @@ -68,6 +68,7 @@ import { TabPanels, TextField, TextArea, + TimeField, ToggleButton, UITable, Tabs, @@ -140,6 +141,7 @@ export const elementComponentMap = { [ELEMENT_NAME.text]: Text, [ELEMENT_NAME.textArea]: TextArea, [ELEMENT_NAME.textField]: TextField, + [ELEMENT_NAME.timeField]: TimeField, [ELEMENT_NAME.toggleButton]: ToggleButton, [ELEMENT_NAME.view]: View, } as const satisfies Record, unknown>; diff --git a/plugins/ui/test/deephaven/ui/test_time_field.py b/plugins/ui/test/deephaven/ui/test_time_field.py new file mode 100644 index 000000000..60ee64550 --- /dev/null +++ b/plugins/ui/test/deephaven/ui/test_time_field.py @@ -0,0 +1,85 @@ +import unittest + +from .BaseTest import BaseTestCase + + +class TimeFieldTest(BaseTestCase): + def test_convert_time_props(self): + from deephaven.time import to_j_instant, to_j_zdt, to_j_local_time + from deephaven.ui.components.time_field import _convert_time_field_props + from deephaven.ui._internal.utils import ( + get_jclass_name, + _convert_to_java_time, + ) + + def verify_is_local_time(timeStr): + self.assertEqual( + get_jclass_name(_convert_to_java_time(timeStr)), "java.time.LocalTime" + ) + + def verify_is_instant(timeStr): + self.assertEqual( + get_jclass_name(_convert_to_java_time(timeStr)), "java.time.Instant" + ) + + def verify_is_zdt(timeStr): + self.assertEqual( + get_jclass_name(_convert_to_java_time(timeStr)), + "java.time.ZonedDateTime", + ) + + def empty_on_change(): + pass + + props1 = { + "placeholder_value": "10:30:45", + "value": "10:30:45", + "default_value": "10:30:45", + "min_value": to_j_zdt("2021-01-01T10:30:45 ET"), + "max_value": to_j_local_time("10:30:45"), + } + + props2 = { + "value": to_j_local_time("10:30:45"), + "default_value": to_j_zdt("2021-01-01T10:30:45 ET"), + "placeholder_value": to_j_instant("2021-01-01T10:30:45 UTC"), + "on_change": verify_is_local_time, + } + + props3 = { + "default_value": to_j_instant("2021-01-01T10:30:45 UTC"), + "placeholder_value": to_j_zdt("2021-01-01T10:30:45 ET"), + "on_change": verify_is_instant, + } + + props4 = { + "placeholder_value": to_j_zdt("2021-01-01T10:30:45 ET"), + "on_change": verify_is_zdt, + } + + props5 = {"on_change": verify_is_instant} + + props6 = {"on_change": empty_on_change} + + _convert_time_field_props(props3) + _convert_time_field_props(props4) + _convert_time_field_props(props5) + _convert_time_field_props(props6) + + verify_is_local_time(props1["max_value"]) + verify_is_zdt(props1["min_value"]) + verify_is_local_time(props1["value"]) + verify_is_local_time(props1["default_value"]) + verify_is_local_time(props1["placeholder_value"]) + + props2["on_change"]("10:30:45") + props3["on_change"]("2021-01-01T10:30:45 UTC") + props4["on_change"]("2021-01-01T10:30:45 ET") + props5["on_change"]("2021-01-01T10:30:45 UTC") + + # pass an Instant but it should be dropped with no error + props6["on_change"]("2021-01-01T10:30:45 UTC") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/app.d/ui_render_all.py b/tests/app.d/ui_render_all.py index 75e7b7cfe..592bf5835 100644 --- a/tests/app.d/ui_render_all.py +++ b/tests/app.d/ui_render_all.py @@ -122,6 +122,7 @@ def ui_components2(): ui.text_field( ui.icon("vsSymbolMisc"), default_value="Text Field", label="Text Field" ), + ui.time_field(default_value="12:30:00", hour_cycle=24), ui.toggle_button( ui.icon("vsBell"), "By Exchange", diff --git a/tests/ui.spec.ts-snapshots/UI-all-components-render-2-1-chromium-linux.png b/tests/ui.spec.ts-snapshots/UI-all-components-render-2-1-chromium-linux.png index 615285e217eb81acb17e47b9148bd7b750eacb79..b9fc7c5da5923b39670699fd98b0ed9d243577e9 100644 GIT binary patch delta 31124 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%0A^!Ec8`=4@lVL8CSkZ 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 NbzSpXF8aZ<{{kD*HT?hp diff --git a/tests/ui.spec.ts-snapshots/UI-all-components-render-2-1-firefox-linux.png b/tests/ui.spec.ts-snapshots/UI-all-components-render-2-1-firefox-linux.png index 1468d660dda5a67494f882b8be2d058829da4f43..93b19b01559addabb30d0372c06971de6c1f6c99 100644 GIT binary patch delta 25189 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 delta 14716 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 From 9b95ac296936ada5e5e038e6a531f372f4716ab2 Mon Sep 17 00:00:00 2001 From: Steven Wu Date: Fri, 20 Sep 2024 16:00:57 -0400 Subject: [PATCH 13/18] test: fix flaky key tests (#901) - Fixes #900 - Add missing `await`s --- tests/ui.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/ui.spec.ts b/tests/ui.spec.ts index 173e97e63..c107ff9a7 100644 --- a/tests/ui.spec.ts +++ b/tests/ui.spec.ts @@ -60,11 +60,11 @@ test('Using keys for lists works', async ({ page }) => { await page.getByRole('textbox', { name: 'Cell 2' }).fill('c'); await page.getByRole('button', { name: 'Delete cell' }).nth(1).click(); - expect(page.getByRole('textbox', { name: 'Cell 0' })).toBeVisible(); - expect(page.getByRole('textbox', { name: 'Cell 0' })).toHaveValue('a'); - expect(page.getByRole('textbox', { name: 'Cell 1' })).not.toBeVisible(); - expect(page.getByRole('textbox', { name: 'Cell 2' })).toBeVisible(); - expect(page.getByRole('textbox', { name: 'Cell 2' })).toHaveValue('c'); + await expect(page.getByRole('textbox', { name: 'Cell 0' })).toBeVisible(); + await expect(page.getByRole('textbox', { name: 'Cell 0' })).toHaveValue('a'); + await expect(page.getByRole('textbox', { name: 'Cell 1' })).not.toBeVisible(); + await expect(page.getByRole('textbox', { name: 'Cell 2' })).toBeVisible(); + await expect(page.getByRole('textbox', { name: 'Cell 2' })).toHaveValue('c'); }); test('UI all components render 1', async ({ page }) => { From fdf710b2884603b511d5b1aea12af6ad5f4b933a Mon Sep 17 00:00:00 2001 From: Steven Wu Date: Fri, 20 Sep 2024 16:16:44 -0400 Subject: [PATCH 14/18] add key prop --- plugins/ui/src/deephaven/ui/components/progress_bar.py | 3 +++ plugins/ui/src/deephaven/ui/components/progress_circle.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/plugins/ui/src/deephaven/ui/components/progress_bar.py b/plugins/ui/src/deephaven/ui/components/progress_bar.py index 0b22f32db..5d572dbce 100644 --- a/plugins/ui/src/deephaven/ui/components/progress_bar.py +++ b/plugins/ui/src/deephaven/ui/components/progress_bar.py @@ -81,6 +81,7 @@ def progress_bar( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> ProgressBarElement: """ ProgressBars show the progression of a system operation: downloading, uploading, processing, etc., in a visual way. They can represent either determinate or indeterminate progress. @@ -144,6 +145,7 @@ def progress_bar( aria_details: The details for the element. UNSAFE_class_name: A CSS class to apply to the element. UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. Returns: The rendered ProgressBar element. @@ -208,4 +210,5 @@ def progress_bar( aria_details=aria_details, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) diff --git a/plugins/ui/src/deephaven/ui/components/progress_circle.py b/plugins/ui/src/deephaven/ui/components/progress_circle.py index 4c7ad256c..c65fd159e 100644 --- a/plugins/ui/src/deephaven/ui/components/progress_circle.py +++ b/plugins/ui/src/deephaven/ui/components/progress_circle.py @@ -78,6 +78,7 @@ def progress_circle( aria_details: str | None = None, UNSAFE_class_name: str | None = None, UNSAFE_style: CSSProperties | None = None, + key: str | None = None, ) -> ProgressCircleElement: """ ProgressCircles show the progression of a system operation such as downloading, uploading, or processing, in a visual way. They can represent determinate or indeterminate progress. @@ -137,6 +138,7 @@ def progress_circle( aria_details: The details for the element. UNSAFE_class_name: A CSS class to apply to the element. UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. Returns: The rendered ProgressCircle element. @@ -197,4 +199,5 @@ def progress_circle( aria_details=aria_details, UNSAFE_class_name=UNSAFE_class_name, UNSAFE_style=UNSAFE_style, + key=key, ) From a9e1d212d12ae2d069869f9e29e1f917227e2593 Mon Sep 17 00:00:00 2001 From: Steven Wu Date: Mon, 23 Sep 2024 11:41:53 -0400 Subject: [PATCH 15/18] update docs --- plugins/ui/docs/components/progress_bar.md | 94 +++++++++++++------ plugins/ui/docs/components/progress_circle.md | 83 +++++++++++----- plugins/ui/docs/sidebar.json | 8 ++ 3 files changed, 131 insertions(+), 54 deletions(-) diff --git a/plugins/ui/docs/components/progress_bar.md b/plugins/ui/docs/components/progress_bar.md index 93b7690f9..0c6c8fd6f 100644 --- a/plugins/ui/docs/components/progress_bar.md +++ b/plugins/ui/docs/components/progress_bar.md @@ -22,46 +22,78 @@ progress_bar = ui_progress_bar() 2. Use `static_color="white"` or `static_color="black"` if necessary to ensure the progress circle has enough contrast with the background. 3. If the value of the progress is unknown, use `is_indeterminate=True`. -## Visual Options +## Value -Progress Bar comes in two different sizes determined by the `size` prop: `"S"` and `"L"`. Furthermore, the `static_color` prop can be used to control the color of the progress circle. +The progress is controlled by the `value`, `min_value`, and `max_value` props. The default values of `min_value` and `max_value` are `0` and `100`, respectively. ```python -def progress_bar_variants(): - return ui.view( - ui.flex( - ui.progress_bar(size="S", value=30, margin="10px"), - ui.progress_bar(size="L", value=60, margin="10px"), - ui.progress_bar(size="L", is_indeterminate=True, margin="10px"), - direction="column", - ), - ui.flex( - ui.progress_bar(size="S", value=30, margin="10px", static_color="white"), - ui.progress_bar(size="L", value=60, margin="10px", static_color="white"), - ui.progress_bar( - size="L", is_indeterminate=True, margin="10px", static_color="white" - ), - direction="column", - ), +def value_variants(): + return ui.flex( + ui.progress_bar(value=50), + ui.progress_bar(value=50, min_value=25, max_value=125), + align_items="start", + direction="column", + row_gap="20px", + ) + + +progress_bar_value_examples = value_variants() +``` + +## Indeterminate + +Use `is_indeterminate=True` if the progress can not be determined. + +```python +def indeterminate_variants(): + return ui.flex( + ui.progress_bar(value=70), + ui.progress_bar(is_indeterminate=True), + align_items="start", + direction="column", + row_gap="20px", + ) + + +progress_bar_indeterminate_examples = indeterminate_variants() +``` + +## Size + +Progress Bar comes in two different sizes determined by the `size` prop: `"S"` and `"L"`. By default, the size is `"L"`. + +```python +def size_variants(): + return ui.flex( + ui.progress_bar(value=70, size="S"), + ui.progress_bar(value=70), + align_items="start", + direction="column", + row_gap="20px", + ) + + +progress_bar_size_examples = size_variants() +``` + +## Static Color + +The `static_color` prop can be used to control the color of the progress bar between the default color, `"black"`, and `"white"`. + +```python +def color_variants(): + return ui.flex( + ui.view(ui.progress_bar(value=70, margin="10px")), + ui.view(ui.progress_bar(value=70, static_color="white", margin="10px")), ui.view( - ui.flex( - ui.progress_bar( - size="S", value=30, margin="10px", static_color="black" - ), - ui.progress_bar( - size="L", value=60, margin="10px", static_color="black" - ), - ui.progress_bar( - size="L", is_indeterminate=True, margin="10px", static_color="black" - ), - direction="column", - ), + ui.progress_bar(value=70, static_color="black", margin="10px"), background_color="white", ), + direction="column", ) -progress_bar_variants_example = progress_bar_variants() +progress_bar_color_examples = color_variants() ``` ## API Reference diff --git a/plugins/ui/docs/components/progress_circle.md b/plugins/ui/docs/components/progress_circle.md index 6dd12b23a..dceac7785 100644 --- a/plugins/ui/docs/components/progress_circle.md +++ b/plugins/ui/docs/components/progress_circle.md @@ -22,37 +22,74 @@ progress_circle = ui_progress_circle() 2. Use `static_color="white"` or `static_color="black"` if necessary to make sure the progress circle has enough contrast with the background. 3. If the value of the progress is unknown, use `is_indeterminate=True`. -## Visual Options +## Value -Progress Circle comes in three different sizes determined by the `size` prop: `"S"`, `"M"`, and `"L"`. Furthermore, the `static_color` prop can be used to control the color of the progress circle. +The progress is controlled by the `value`, `min_value`, and `max_value` props. The default values of `min_value` and `max_value` are `0` and `100`, respectively. ```python -def progress_circle_variants(): +def value_variants(): + return ui.flex( + ui.progress_circle(value=50), + ui.progress_circle(value=50, min_value=25, max_value=125), + column_gap="20px", + ) + + +progress_circle_value_examples = value_variants() +``` + +## Indeterminate + +Use `is_indeterminate=True` if the progress can not be determined. + +```python +def indeterminate_variants(): + return ui.flex( + ui.progress_circle(value=70), + ui.progress_circle(is_indeterminate=True), + column_gap="20px", + ) + + +progress_circle_indeterminate_examples = indeterminate_variants() +``` + +## Size + +Progress Circle comes in three different sizes determined by the `size` prop: `"S"`, `"M"`, and `"L"`. By default, the size is `"M"`. + +```python +def size_variants(): + return ui.flex( + ui.progress_circle(value=70, size="S"), + ui.progress_circle(value=70), + ui.progress_circle(value=70, size="L"), + column_gap="20px", + ) + + +progress_circle_size_examples = size_variants() +``` + +## Static Color + +The `static_color` prop can be used to control the color of the progress circle between the default color, `"black"`, and `"white"`. + +```python +def color_variants(): return ui.view( - ui.view( - ui.progress_circle(size="S", value=30, margin="5px"), - ui.progress_circle(size="M", value=60, margin="5px"), - ui.progress_circle(size="L", is_indeterminate=True, margin="5px"), - ), - ui.view( - ui.progress_circle(size="S", value=30, margin="5px", static_color="white"), - ui.progress_circle(size="M", value=60, margin="5px", static_color="white"), - ui.progress_circle( - size="L", is_indeterminate=True, margin="5px", static_color="white" - ), - ), - ui.view( - ui.progress_circle(size="S", value=30, margin="5px", static_color="black"), - ui.progress_circle(size="M", value=60, margin="5px", static_color="black"), - ui.progress_circle( - size="L", is_indeterminate=True, margin="5px", static_color="black" + ui.flex( + ui.view(ui.progress_circle(value=70, margin="10px")), + ui.view(ui.progress_circle(value=70, static_color="white", margin="10px")), + ui.view( + ui.progress_circle(value=70, static_color="black", margin="10px"), + background_color="white", ), - background_color="white", - ), + ) ) -progress_circle_variants_example = progress_circle_variants() +progress_circle_color_examples = color_variants() ``` ## API Reference diff --git a/plugins/ui/docs/sidebar.json b/plugins/ui/docs/sidebar.json index 86f9278a2..7feb60b58 100644 --- a/plugins/ui/docs/sidebar.json +++ b/plugins/ui/docs/sidebar.json @@ -57,6 +57,14 @@ "label": "picker", "path": "components/picker.md" }, + { + "label": "progress_bar", + "path": "components/progress_bar.md" + }, + { + "label": "progress_circle", + "path": "components/progress_circle.md" + }, { "label": "radio_group", "path": "components/radio_group.md" From 2d5f692f8cb3a66d4d6301691b55bce2c0cad83d Mon Sep 17 00:00:00 2001 From: Steven Wu Date: Tue, 24 Sep 2024 15:21:47 -0400 Subject: [PATCH 16/18] update docs from review Co-authored-by: Don --- plugins/ui/docs/components/progress_bar.md | 38 ++++++++++++------- plugins/ui/docs/components/progress_circle.md | 31 ++++++++++----- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/plugins/ui/docs/components/progress_bar.md b/plugins/ui/docs/components/progress_bar.md index 0c6c8fd6f..6a2a47be7 100644 --- a/plugins/ui/docs/components/progress_bar.md +++ b/plugins/ui/docs/components/progress_bar.md @@ -10,7 +10,7 @@ from deephaven import ui @ui.component def ui_progress_bar(): - return ui.progress_bar(size="L", is_indeterminate=True) + return ui.progress_bar(is_indeterminate=True) progress_bar = ui_progress_bar() @@ -27,14 +27,15 @@ progress_bar = ui_progress_bar() The progress is controlled by the `value`, `min_value`, and `max_value` props. The default values of `min_value` and `max_value` are `0` and `100`, respectively. ```python +from deephaven import ui + + +@ui.component def value_variants(): - return ui.flex( + return [ ui.progress_bar(value=50), ui.progress_bar(value=50, min_value=25, max_value=125), - align_items="start", - direction="column", - row_gap="20px", - ) + ] progress_bar_value_examples = value_variants() @@ -45,14 +46,12 @@ progress_bar_value_examples = value_variants() Use `is_indeterminate=True` if the progress can not be determined. ```python +from deephaven import ui + + +@ui.component def indeterminate_variants(): - return ui.flex( - ui.progress_bar(value=70), - ui.progress_bar(is_indeterminate=True), - align_items="start", - direction="column", - row_gap="20px", - ) + return ui.progress_bar(is_indeterminate=True) progress_bar_indeterminate_examples = indeterminate_variants() @@ -63,6 +62,10 @@ progress_bar_indeterminate_examples = indeterminate_variants() Progress Bar comes in two different sizes determined by the `size` prop: `"S"` and `"L"`. By default, the size is `"L"`. ```python +from deephaven import ui + + +@ui.component def size_variants(): return ui.flex( ui.progress_bar(value=70, size="S"), @@ -81,10 +84,17 @@ progress_bar_size_examples = size_variants() The `static_color` prop can be used to control the color of the progress bar between the default color, `"black"`, and `"white"`. ```python +from deephaven import ui + + +@ui.component def color_variants(): return ui.flex( ui.view(ui.progress_bar(value=70, margin="10px")), - ui.view(ui.progress_bar(value=70, static_color="white", margin="10px")), + ui.view( + ui.progress_bar(value=70, static_color="white", margin="10px"), + background_color="black", + ), ui.view( ui.progress_bar(value=70, static_color="black", margin="10px"), background_color="white", diff --git a/plugins/ui/docs/components/progress_circle.md b/plugins/ui/docs/components/progress_circle.md index dceac7785..8d228482b 100644 --- a/plugins/ui/docs/components/progress_circle.md +++ b/plugins/ui/docs/components/progress_circle.md @@ -10,7 +10,7 @@ from deephaven import ui @ui.component def ui_progress_circle(): - return ui.progress_circle(size="L", is_indeterminate=True) + return ui.progress_circle(is_indeterminate=True) progress_circle = ui_progress_circle() @@ -27,11 +27,14 @@ progress_circle = ui_progress_circle() The progress is controlled by the `value`, `min_value`, and `max_value` props. The default values of `min_value` and `max_value` are `0` and `100`, respectively. ```python +from deephaven import ui + + +@ui.component def value_variants(): return ui.flex( ui.progress_circle(value=50), ui.progress_circle(value=50, min_value=25, max_value=125), - column_gap="20px", ) @@ -43,12 +46,12 @@ progress_circle_value_examples = value_variants() Use `is_indeterminate=True` if the progress can not be determined. ```python +from deephaven import ui + + +@ui.component def indeterminate_variants(): - return ui.flex( - ui.progress_circle(value=70), - ui.progress_circle(is_indeterminate=True), - column_gap="20px", - ) + return ui.progress_circle(is_indeterminate=True) progress_circle_indeterminate_examples = indeterminate_variants() @@ -59,12 +62,15 @@ progress_circle_indeterminate_examples = indeterminate_variants() Progress Circle comes in three different sizes determined by the `size` prop: `"S"`, `"M"`, and `"L"`. By default, the size is `"M"`. ```python +from deephaven import ui + + +@ui.component def size_variants(): return ui.flex( ui.progress_circle(value=70, size="S"), ui.progress_circle(value=70), ui.progress_circle(value=70, size="L"), - column_gap="20px", ) @@ -76,11 +82,18 @@ progress_circle_size_examples = size_variants() The `static_color` prop can be used to control the color of the progress circle between the default color, `"black"`, and `"white"`. ```python +from deephaven import ui + + +@ui.component def color_variants(): return ui.view( ui.flex( ui.view(ui.progress_circle(value=70, margin="10px")), - ui.view(ui.progress_circle(value=70, static_color="white", margin="10px")), + ui.view( + ui.progress_circle(value=70, static_color="white", margin="10px"), + background_color="black", + ), ui.view( ui.progress_circle(value=70, static_color="black", margin="10px"), background_color="white", From a82c1115d5093b3755d9e25f52c5b62101357bdf Mon Sep 17 00:00:00 2001 From: Steven Wu Date: Tue, 1 Oct 2024 11:46:22 -0400 Subject: [PATCH 17/18] update docs from review Co-authored-by: Don --- plugins/ui/docs/components/progress_bar.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/plugins/ui/docs/components/progress_bar.md b/plugins/ui/docs/components/progress_bar.md index 6a2a47be7..3a5de1298 100644 --- a/plugins/ui/docs/components/progress_bar.md +++ b/plugins/ui/docs/components/progress_bar.md @@ -67,13 +67,10 @@ from deephaven import ui @ui.component def size_variants(): - return ui.flex( + return [ ui.progress_bar(value=70, size="S"), ui.progress_bar(value=70), - align_items="start", - direction="column", - row_gap="20px", - ) + ] progress_bar_size_examples = size_variants() From 22ce47bd0cddc366a2053dcb23b6b5142474acdf Mon Sep 17 00:00:00 2001 From: Steven Wu Date: Fri, 4 Oct 2024 09:57:10 -0400 Subject: [PATCH 18/18] update docs from review --- plugins/ui/docs/components/progress_bar.md | 4 ++-- plugins/ui/docs/components/progress_circle.md | 4 ++-- plugins/ui/src/deephaven/ui/components/progress_bar.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/ui/docs/components/progress_bar.md b/plugins/ui/docs/components/progress_bar.md index 3a5de1298..a0ac2931a 100644 --- a/plugins/ui/docs/components/progress_bar.md +++ b/plugins/ui/docs/components/progress_bar.md @@ -50,11 +50,11 @@ from deephaven import ui @ui.component -def indeterminate_variants(): +def indeterminate_variant(): return ui.progress_bar(is_indeterminate=True) -progress_bar_indeterminate_examples = indeterminate_variants() +progress_bar_indeterminate_example = indeterminate_variant() ``` ## Size diff --git a/plugins/ui/docs/components/progress_circle.md b/plugins/ui/docs/components/progress_circle.md index 8d228482b..f66da715d 100644 --- a/plugins/ui/docs/components/progress_circle.md +++ b/plugins/ui/docs/components/progress_circle.md @@ -50,11 +50,11 @@ from deephaven import ui @ui.component -def indeterminate_variants(): +def indeterminate_variant(): return ui.progress_circle(is_indeterminate=True) -progress_circle_indeterminate_examples = indeterminate_variants() +progress_circle_indeterminate_example = indeterminate_variant() ``` ## Size diff --git a/plugins/ui/src/deephaven/ui/components/progress_bar.py b/plugins/ui/src/deephaven/ui/components/progress_bar.py index 5d572dbce..ee2a7ef28 100644 --- a/plugins/ui/src/deephaven/ui/components/progress_bar.py +++ b/plugins/ui/src/deephaven/ui/components/progress_bar.py @@ -95,7 +95,7 @@ def progress_bar( value_label: The content to display as the value's label. value: The current value (controlled). min_value: The smallest value allowed for the input. - max_value: The largest value allowed for the input. + max_value: The largest value allowed for the input. is_indeterminate: Indeterminism is presentational only. The indeterminate visual representation remains regardless of user interaction. flex: When used in a flex layout, specifies how the element will grow or shrink to fit the space available. flex_grow: When used in a flex layout, specifies how much the element will grow to fit the space available.