diff --git a/.github/workflows/typescript-check.yml b/.github/workflows/typescript-check.yml
new file mode 100644
index 000000000..73042926a
--- /dev/null
+++ b/.github/workflows/typescript-check.yml
@@ -0,0 +1,27 @@
+name: Check TypeScript types
+
+on:
+ push:
+ branches:
+ - main
+ - 'release/**'
+ - 'feature/**'
+ pull_request:
+ branches:
+ - main
+ - 'release/**'
+ - 'feature/**'
+
+jobs:
+ typescript-check:
+ runs-on: ubuntu-latest
+ concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install dependencies
+ run: npm ci
+ - name: Check TypeScript types
+ run: python tools/check_typescript_ci.py
+
diff --git a/package-lock.json b/package-lock.json
index 7d00aff72..05d19d59e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -32033,7 +32033,7 @@
},
"plugins/plotly-express/src/js": {
"name": "@deephaven/js-plugin-plotly-express",
- "version": "0.12.0",
+ "version": "0.12.1",
"license": "Apache-2.0",
"dependencies": {
"@deephaven/chart": "0.97.0",
@@ -32318,7 +32318,7 @@
},
"plugins/ui/src/js": {
"name": "@deephaven/js-plugin-ui",
- "version": "0.23.1",
+ "version": "0.24.0",
"license": "Apache-2.0",
"dependencies": {
"@deephaven/chart": "^0.99.0",
diff --git a/plugins/dashboard-object-viewer/src/js/src/DashboardPlugin/DashboardPlugin.tsx b/plugins/dashboard-object-viewer/src/js/src/DashboardPlugin/DashboardPlugin.tsx
index 75e6c4cad..2475c1fb5 100644
--- a/plugins/dashboard-object-viewer/src/js/src/DashboardPlugin/DashboardPlugin.tsx
+++ b/plugins/dashboard-object-viewer/src/js/src/DashboardPlugin/DashboardPlugin.tsx
@@ -28,7 +28,7 @@ export function DashboardPlugin({
widget: VariableDefinition;
}) => {
const { id: widgetId, name, type } = widget;
- if (type === dh.VariableType.TABLE || type === dh.VariableType.FIGURE) {
+ if (type === 'Table' || type === 'Figure') {
// Just ignore table and figure types - only want interesting other types
return;
}
diff --git a/plugins/plotly-express/CHANGELOG.md b/plugins/plotly-express/CHANGELOG.md
index dace500b4..88b82fa39 100644
--- a/plugins/plotly-express/CHANGELOG.md
+++ b/plugins/plotly-express/CHANGELOG.md
@@ -2,6 +2,12 @@
All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.
- - -
+## plotly-express-v0.12.1 - 2024-12-12
+#### Bug Fixes
+- switch to webgl by default for line plot (#992) - (2c7bc01) - Joe
+
+- - -
+
## plotly-express-v0.12.0 - 2024-11-23
#### Bug Fixes
- `dx` now respects the webgl flag (#934) - (9cdf1ee) - Joe
diff --git a/plugins/plotly-express/docs/indicator.md b/plugins/plotly-express/docs/indicator.md
new file mode 100644
index 000000000..c7e52f4d3
--- /dev/null
+++ b/plugins/plotly-express/docs/indicator.md
@@ -0,0 +1,273 @@
+# Indicator Plot
+
+An indicator plot is a type of plot that highlights a collection of numeric values.
+
+### What are indicator plots useful for?
+
+- **Highlight specific metrics**: Indicator plots are useful when you want to highlight specific numeric metrics in a visually appealing way.
+- **Compare metrics to a reference value**: Indicator plots are useful to compare metrics to a reference value, such as a starting value or a target value.
+- **Compare metrics to each other**: Indicator plots are useful to compare multiple metrics to each other by highlighting where they fall relative to each other.
+
+## Examples
+
+### A basic indicator plot
+
+Visualize a single numeric value by passing the column name to the `value` argument. The table should contain only one row.
+
+```python
+import deephaven.plot.express as dx
+from deephaven import agg as agg
+
+my_table = dx.data.stocks()
+
+# subset data and aggregate for DOG prices
+dog_avg = my_table.where("Sym = `DOG`").agg_by([agg.avg(cols="Price")])
+
+indicator_plot = dx.indicator(dog_avg, value="Price")
+```
+
+### A delta indicator plot
+
+Visualize a single numeric value with a delta to a reference value by passing the reference column name to the `reference` argument.
+
+```python order=indicator_plot,my_table
+import deephaven.plot.express as dx
+from deephaven import agg as agg
+my_table = dx.data.stocks()
+
+# subset data and aggregate for DOG prices
+dog_agg = my_table.where("Sym = `DOG`").agg_by([agg.avg(cols="Price"), agg.first(cols="StartingPrice = Price")])
+
+indicator_plot = dx.indicator(dog_agg, value="Price", reference="StartingPrice")
+```
+
+## Indicator plots from variables
+
+Pass variables into a table to create an indicator plot.
+
+```python order=indicator_plot,my_table
+import deephaven.plot.express as dx
+from deephaven import new_table
+from deephaven.column import int_col
+
+my_value = 10
+my_reference = 5
+
+my_table = new_table([
+ int_col("MyValue", [my_value]),
+ int_col("MyReference", [my_reference])
+])
+
+indicator_plot = dx.indicator(my_table, value="MyValue", reference="MyReference")
+```
+
+# Delta only indicator plot
+
+Visualize only the delta to a reference value by passing `number=False`.
+
+```python order=indicator_plot,my_table
+import deephaven.plot.express as dx
+from deephaven import agg as agg
+my_table = dx.data.stocks()
+
+# subset data and aggregate for DOG prices
+dog_agg = my_table.where("Sym = `DOG`").agg_by([agg.avg(cols="Price"), agg.first(cols="StartingPrice = Price")])
+
+indicator_plot = dx.indicator(dog_agg, value="Price", reference="StartingPrice", number=False)
+```
+
+### An angular indicator plot
+
+Visualize a single numeric value with an angular gauge by passing `gauge="angular"`.
+
+```python order=indicator_plot,my_table
+import deephaven.plot.express as dx
+from deephaven import agg as agg
+my_table = dx.data.stocks()
+
+# subset data and aggregate for DOG prices
+dog_avg = my_table.where("Sym = `DOG`").agg_by([agg.avg(cols="Price")])
+
+indicator_plot = dx.indicator(dog_avg, value="Price", gauge="angular")
+```
+
+### A hidden axis bullet indicator plot
+
+Visualize a single numeric value with a bullet gauge by passing `gauge="bullet"`. Hide the axis by passing `axis=False`.
+
+```python order=indicator_plot,my_table
+import deephaven.plot.express as dx
+from deephaven import agg as agg
+my_table = dx.data.stocks()
+
+# subset data and aggregate for DOG prices
+dog_avg = my_table.where("Sym = `DOG`").agg_by([agg.avg(cols="Price")])
+
+indicator_plot = dx.indicator(dog_avg, value="Price", gauge="bullet", axis=False)
+```
+
+### Prefixes and suffixes
+
+Add a prefix and suffix to the numeric value by passing `prefix` and `suffix`.
+
+```python order=indicator_plot,my_table
+import deephaven.plot.express as dx
+from deephaven import agg as agg
+my_table = dx.data.stocks()
+
+# subset data and aggregate for DOG prices
+dog_avg = my_table.where("Sym = `DOG`").agg_by([agg.avg(cols="Price")])
+
+indicator_plot = dx.indicator(dog_avg, value="Price", prefix="$", suffix="USD")
+```
+
+### Delta Symbols
+
+Modify the symbol before the delta value by passing `increasing_text` and `decreasing_text`.
+
+```python order=indicator_plot,my_table
+import deephaven.plot.express as dx
+from deephaven import agg as agg
+my_table = dx.data.stocks()
+
+# subset data and aggregate for DOG prices
+dog_avg = my_table.where("Sym = `DOG`").agg_by([agg.avg(cols="Price")])
+
+indicator_plot = dx.indicator(dog_avg, value="Price", increasing_text="Up: ", decreasing_text="Down: ")
+```
+
+### Indicator with text
+
+Add text to the indicator by passing the text column name to the `text` argument.
+
+```python order=indicator_plot,my_table
+import deephaven.plot.express as dx
+from deephaven import agg as agg
+my_table = dx.data.stocks()
+
+# subset data and aggregate prices, keeping the Sym
+dog_avg = my_table.where("Sym = `DOG`").agg_by([agg.avg(cols="Price")])
+
+indicator_plot = dx.indicator(dog_avg, value="Price", text="Sym")
+```
+
+### Multiple indicators
+
+Visualize multiple numeric values by passing in a table with multiple rows. By default, a square grid of indicators is created.
+
+```python order=indicator_plot,my_table
+import deephaven.plot.express as dx
+my_table = dx.data.stocks()
+
+# aggregate for average prices by Sym
+sym_avg = my_table.agg_by([agg.avg(cols="Price")], by="Sym")
+
+indicator_plot = dx.indicator(sym_avg, value="Price")
+```
+
+### Multiple rows
+
+By default, a grid of indicators is created. To create a specific amount of rows with a dynamic number of columns, pass the number of rows to the `rows` argument.
+
+```python order=indicator_plot,my_table
+import deephaven.plot.express as dx
+my_table = dx.data.stocks()
+
+# aggregate for average prices by Sym
+sym_avg = my_table.agg_by([agg.avg(cols="Price")], by="Sym")
+
+indicator_plot = dx.indicator(sym_avg, value="Price", rows=2)
+```
+
+### Multiple columns
+
+By default, a grid of indicators is created. To create a specific amount of columns with a dynamic number of rows, pass the number of columns to the `columns` argument.
+
+```python order=indicator_plot,my_table
+import deephaven.plot.express as dx
+my_table = dx.data.stocks()
+
+# aggregate for average prices by Sym
+sym_avg = my_table.agg_by([agg.avg(cols="Price")], by="Sym")
+
+indicator_plot = dx.indicator(sym_avg, value="Price", columns=2)
+```
+
+### Delta colors
+
+Change the color of the delta value based on whether it is increasing or decreasing by passing `increasing_color_sequence` and `decreasing_color_sequence`.
+These colors are applied sequentially to the indicators and looped if there are more indicators than colors.
+
+```python
+import deephaven.plot.express as dx
+from deephaven import agg as agg
+
+my_table = dx.data.stocks()
+
+# subset data and aggregate for DOG prices
+sym_agg = my_table.agg_by(
+ [agg.avg(cols="Price"), agg.first(cols="StartingPrice = Price")]
+)
+
+indicator_plot = dx.indicator(
+ sym_agg,
+ value="Price",
+ reference="Starting Price",
+ increasing_color_sequence=["green", "darkgreen"],
+ decreasing_color_sequence=["red", "darkred"],
+)
+```
+
+### Gauge colors
+
+Change the color of the gauge based on the value by passing `gauge_color_sequence`.
+These colors are applied sequentially to the indicators and looped if there are more indicators than colors.
+
+```python
+import deephaven.plot.express as dx
+from deephaven import agg as agg
+
+my_table = dx.data.stocks()
+
+# subset data and aggregate for DOG prices
+sym_agg = my_table.agg_by([agg.avg(cols="Price")])
+
+indicator_plot = dx.indicator(
+ sym_agg, value="Price", gauge_color_sequence=["green", "darkgreen"]
+)
+```
+
+### Plot by
+
+Create groups of styled indicators by passing the grouping categorical column name to the `by` argument.
+`increasing_color_map` and `decreasing_color_map` can be used to style the indicators based on the group.
+
+```python
+import deephaven.plot.express as dx
+from deephaven import agg as agg
+
+my_table = dx.data.stocks()
+
+# subset data and aggregate prices, keeping the Sym
+sym_agg = my_table.agg_by(
+ [
+ agg.avg(cols="Price"),
+ agg.first(cols="StartingPrice = Price"),
+ agg.last(cols="Sym"),
+ ]
+)
+
+indicator_plot = dx.indicator(
+ sym_agg,
+ value="Price",
+ reference="StartingPrice",
+ by="Sym",
+ increasing_color_map={"DOG": "darkgreen"},
+ decreasing_color_map={"DOG": "darkred"},
+)
+```
+
+## API Reference
+```{eval-rst}
+.. dhautofunction:: deephaven.plot.express.indicator
+```
diff --git a/plugins/plotly-express/setup.cfg b/plugins/plotly-express/setup.cfg
index 7c1405431..501796c6c 100644
--- a/plugins/plotly-express/setup.cfg
+++ b/plugins/plotly-express/setup.cfg
@@ -3,7 +3,7 @@ name = deephaven-plugin-plotly-express
description = Deephaven Chart Plugin
long_description = file: README.md
long_description_content_type = text/markdown
-version = 0.12.0.dev0
+version = 0.12.1.dev0
url = https://github.com/deephaven/deephaven-plugins
project_urls =
Source Code = https://github.com/deephaven/deephaven-plugins
diff --git a/plugins/plotly-express/src/deephaven/plot/express/plots/indicator.py b/plugins/plotly-express/src/deephaven/plot/express/plots/indicator.py
new file mode 100644
index 000000000..16a54117e
--- /dev/null
+++ b/plugins/plotly-express/src/deephaven/plot/express/plots/indicator.py
@@ -0,0 +1,105 @@
+from __future__ import annotations
+
+from typing import Callable
+
+from ..shared import default_callback
+from ..deephaven_figure import DeephavenFigure
+from ..types import PartitionableTableLike, Gauge, StyleDict
+
+
+def indicator(
+ table: PartitionableTableLike,
+ value: str | None,
+ reference: str | None = None,
+ text: str | None = None,
+ by: str | list[str] | None = None,
+ by_vars: str | tuple[str, ...] = "gauge_color",
+ increasing_color: str | list[str] | None = None,
+ decreasing_color: str | list[str] | None = None,
+ gauge_color: str | list[str] | None = None,
+ increasing_color_sequence: list[str] | None = None,
+ increasing_color_map: StyleDict | None = None,
+ decreasing_color_sequence: list[str] | None = None,
+ decreasing_color_map: StyleDict | None = None,
+ gauge_color_sequence: list[str] | None = None,
+ gauge_color_map: StyleDict | None = None,
+ number: bool = True,
+ gauge: Gauge | None = None,
+ axis: bool = True,
+ prefix: str | None = None,
+ suffix: str | None = None,
+ increasing_text: str | None = "▲",
+ decreasing_text: str | None = "▼",
+ rows: int | None = None,
+ columns: int | None = None,
+ unsafe_update_figure: Callable = default_callback,
+) -> DeephavenFigure:
+ """
+ Create an indicator chart.
+
+ Args:
+ table: A table to pull data from.
+ value: The column to use as the value.
+ reference: The column to use as the reference value.
+ by: A column or list of columns that contain values to plot the figure traces by.
+ All values or combination of values map to a unique design. The variable
+ by_vars specifies which design elements are used.
+ This is overriden if any specialized design variables such as increasing_color are specified
+ by_vars: A string or list of string that contain design elements to plot by.
+ Can contain increasing_color and decreasing_color
+ If associated maps or sequences are specified, they are used to map by column values
+ to designs. Otherwise, default values are used.
+ increasing_color: A column or list of columns used for a plot by on delta increasing color.
+ Only valid if reference is not None.
+ See increasing_color_map for additional behaviors.
+ decreasing_color: A column or list of columns used for a plot by on delta increasing color.
+ Only valid if reference is not None.
+ See decreasing_color_map for additional behaviors.
+ gauge_color: A column or list of columns used for a plot by on color.
+ Only valid if gauge is not None.
+ See gauge_color_map for additional behaviors.
+ text: A column that contains text annotations.
+ increasing_color_sequence: A list of colors to sequentially apply to
+ the series. The colors loop, so if there are more series than colors,
+ colors are reused.
+ increasing_color_map: A dict with keys that are strings of the column values (or a tuple
+ of combinations of column values) which map to colors.
+ decreasing_color_sequence: A list of colors to sequentially apply to
+ the series. The colors loop, so if there are more series than colors,
+ colors are reused.
+ decreasing_color_map: A dict with keys that are strings of the column values (or a tuple
+ of combinations of column values) which map to colors.
+ gauge_color_sequence: A list of colors to sequentially apply to
+ the series. The colors loop, so if there are more series than colors,
+ colors are reused.
+ gauge_color_map: A dict with keys that are strings of the column values (or a tuple
+ of combinations of column values) which map to colors.
+ number: True to show the number, False to hide it.
+ gauge: Specifies the type of gauge to use.
+ Set to "angular" for a half-circle gauge and "bullet" for a horizontal gauge.
+ axis: True to show the axis. Only valid if gauge is set.
+ prefix: A string to prepend to the number value.
+ suffix: A string to append to the number value.
+ increasing_text: The text to display before the delta if the number value
+ is greater than the reference value.
+ decreasing_text: The text to display before the delta if the number value
+ is less than the reference value.
+ rows: The number of rows of indicators to create.
+ If None, the number of rows is determined by the number of columns.
+ If both rows and columns are None, a square grid is created.
+ columns: The number of columns of indicators to create.
+ If None, the number of columns is determined by the number of rows.
+ If both rows and columns are None, a square grid is created.
+ unsafe_update_figure: An update function that takes a plotly figure
+ as an argument and optionally returns a plotly figure. If a figure is
+ not returned, the plotly figure passed will be assumed to be the return
+ value. Used to add any custom changes to the underlying plotly figure.
+ Note that the existing data traces should not be removed. This may lead
+ to unexpected behavior if traces are modified in a way that break data
+ mappings.
+
+ Returns:
+ A DeephavenFigure that contains the indicator chart
+
+ """
+ raise NotImplementedError
diff --git a/plugins/plotly-express/src/deephaven/plot/express/plots/line.py b/plugins/plotly-express/src/deephaven/plot/express/plots/line.py
index 3be48a0f0..9c1783058 100644
--- a/plugins/plotly-express/src/deephaven/plot/express/plots/line.py
+++ b/plugins/plotly-express/src/deephaven/plot/express/plots/line.py
@@ -56,7 +56,7 @@ def line(
line_shape: str = "linear",
title: str | None = None,
template: str | None = None,
- render_mode: str = "svg",
+ render_mode: str = "webgl",
unsafe_update_figure: Callable = default_callback,
) -> DeephavenFigure:
"""Returns a line chart
@@ -170,8 +170,9 @@ def line(
'spline', 'vhv', 'hvh', 'vh', 'hv'. Default 'linear'
title: The title of the chart
template: The template for the chart.
- render_mode: Either "svg" or "webgl". Setting to "webgl" will lead to a more
- performant plot but there may be graphical bugs.
+ render_mode: Either "svg" or "webgl". The default is "webgl" as it leads to a more
+ performant plot but there may be graphical bugs, in which case it is
+ recommended to switch to "svg"
unsafe_update_figure: An update function that takes a plotly figure
as an argument and optionally returns a plotly figure. If a figure is
not returned, the plotly figure passed will be assumed to be the return
diff --git a/plugins/plotly-express/src/deephaven/plot/express/types/__init__.py b/plugins/plotly-express/src/deephaven/plot/express/types/__init__.py
index 0417cd135..3f89c08c0 100644
--- a/plugins/plotly-express/src/deephaven/plot/express/types/__init__.py
+++ b/plugins/plotly-express/src/deephaven/plot/express/types/__init__.py
@@ -1 +1 @@
-from .plots import PartitionableTableLike, TableLike
+from .plots import PartitionableTableLike, TableLike, Gauge, StyleDict, StyleMap
diff --git a/plugins/plotly-express/src/deephaven/plot/express/types/plots.py b/plugins/plotly-express/src/deephaven/plot/express/types/plots.py
index 25cc51aa9..172d8d220 100644
--- a/plugins/plotly-express/src/deephaven/plot/express/types/plots.py
+++ b/plugins/plotly-express/src/deephaven/plot/express/types/plots.py
@@ -1,6 +1,22 @@
-from typing import Union
+from __future__ import annotations
+
+from typing import Union, Literal, Tuple, Dict
from pandas import DataFrame
from deephaven.table import Table, PartitionedTable
TableLike = Union[Table, DataFrame]
PartitionableTableLike = Union[PartitionedTable, TableLike]
+Gauge = Literal["shape", "bullet"]
+
+# StyleDict is a dictionary that maps column values to style values.
+StyleDict = Dict[Union[str, Tuple[str]], str]
+
+# In addition to StyleDict, StyleMap can also be a string literal "identity" or "by"
+# that specifies how to map column values to style values.
+# If "identity", the column values are taken as literal style values.
+# If "by", the column values are used to map to style values.
+# "by" is only used to override parameters that default to numeric mapping on a continuous scale, such as scatter color.
+# Providing a tuple of "by" and a StyleDict is equivalent to providing a StyleDict.
+StyleMap = Union[
+ Literal["identity"], Literal["by"], Tuple[Literal["by"], StyleDict], StyleDict
+]
diff --git a/plugins/plotly-express/src/js/package.json b/plugins/plotly-express/src/js/package.json
index 5bf082ab3..61c4739ef 100644
--- a/plugins/plotly-express/src/js/package.json
+++ b/plugins/plotly-express/src/js/package.json
@@ -1,6 +1,6 @@
{
"name": "@deephaven/js-plugin-plotly-express",
- "version": "0.12.0",
+ "version": "0.12.1",
"description": "Deephaven plotly express plugin",
"keywords": [
"Deephaven",
diff --git a/plugins/plotly-express/test/deephaven/plot/express/plots/test_line.py b/plugins/plotly-express/test/deephaven/plot/express/plots/test_line.py
new file mode 100644
index 000000000..bb96322fb
--- /dev/null
+++ b/plugins/plotly-express/test/deephaven/plot/express/plots/test_line.py
@@ -0,0 +1,78 @@
+import unittest
+
+from ..BaseTest import BaseTestCase
+
+
+class LineTestCase(BaseTestCase):
+ def setUp(self) -> None:
+ from deephaven import new_table
+ from deephaven.column import int_col
+
+ self.source = new_table(
+ [
+ int_col("X", [1, 2, 2, 3, 3, 3, 4, 4, 5]),
+ int_col("X2", [1, 2, 2, 3, 3, 3, 4, 4, 5]),
+ int_col("Y", [1, 2, 2, 3, 3, 3, 4, 4, 5]),
+ int_col("Y2", [1, 2, 2, 3, 3, 3, 4, 4, 5]),
+ int_col("size", [1, 2, 2, 3, 3, 3, 4, 4, 5]),
+ int_col("text", [1, 2, 2, 3, 3, 3, 4, 4, 5]),
+ int_col("hover_name", [1, 2, 2, 3, 3, 3, 4, 4, 5]),
+ int_col("category", [1, 2, 1, 2, 1, 2, 1, 2, 1]),
+ ]
+ )
+
+ def test_basic_scatter(self):
+ import src.deephaven.plot.express as dx
+ from deephaven.constants import NULL_INT
+
+ chart = dx.line(self.source, x="X", y="Y").to_dict(self.exporter)
+
+ expected_data = [
+ {
+ "hovertemplate": "X=%{x}
Y=%{y}",
+ "legendgroup": "",
+ "line": {"color": "#636efa", "dash": "solid", "shape": "linear"},
+ "marker": {"symbol": "circle"},
+ "mode": "lines",
+ "name": "",
+ "showlegend": False,
+ "x": [NULL_INT],
+ "xaxis": "x",
+ "y": [NULL_INT],
+ "yaxis": "y",
+ "type": "scattergl",
+ }
+ ]
+
+ expected_layout = {
+ "legend": {"tracegroupgap": 0},
+ "margin": {"t": 60},
+ "xaxis": {
+ "anchor": "y",
+ "domain": [0.0, 1.0],
+ "side": "bottom",
+ "title": {"text": "X"},
+ },
+ "yaxis": {
+ "anchor": "x",
+ "domain": [0.0, 1.0],
+ "side": "left",
+ "title": {"text": "Y"},
+ },
+ }
+
+ expected_mappings = [
+ {
+ "table": 0,
+ "data_columns": {"X": ["/plotly/data/0/x"], "Y": ["/plotly/data/0/y"]},
+ }
+ ]
+
+ self.assert_chart_equals(
+ chart,
+ expected_data=expected_data,
+ expected_layout=expected_layout,
+ expected_mappings=expected_mappings,
+ expected_is_user_set_template=False,
+ expected_is_user_set_color=False,
+ )
diff --git a/plugins/ui/CHANGELOG.md b/plugins/ui/CHANGELOG.md
index 28d768713..557e10ac6 100644
--- a/plugins/ui/CHANGELOG.md
+++ b/plugins/ui/CHANGELOG.md
@@ -2,6 +2,21 @@
All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.
- - -
+## ui-v0.24.0 - 2024-12-12
+#### Bug Fixes
+- UI loading duplicate panels in embed iframe (#1043) - (e1559a4) - Matthew Runyon
+#### Documentation
+- Working with Tables (#1059) - (6e73350) - dgodinez-dh
+- Importing and Exporting Components (#1054) - (21b752c) - dgodinez-dh
+- Your First Component (#1052) - (ce3843a) - dgodinez-dh
+- Add Stack with tabs to dashboard docs (#1048) - (cf0c994) - mofojed
+#### Features
+- ui.meter (#1032) - (6730aa9) - ethanalvizo
+- ui.avatar (#1027) - (2738a1d) - Akshat Jawne
+- Toast Implementation (#1030) - (e53b322) - dgodinez-dh
+
+- - -
+
## ui-v0.23.1 - 2024-11-23
- - -
diff --git a/plugins/ui/docs/_assets/component_rules_1.png b/plugins/ui/docs/_assets/component_rules_1.png
new file mode 100644
index 000000000..9a649aaa0
Binary files /dev/null and b/plugins/ui/docs/_assets/component_rules_1.png differ
diff --git a/plugins/ui/docs/_assets/component_rules_2.png b/plugins/ui/docs/_assets/component_rules_2.png
new file mode 100644
index 000000000..b07fb0e8b
Binary files /dev/null and b/plugins/ui/docs/_assets/component_rules_2.png differ
diff --git a/plugins/ui/docs/_assets/conditional_rendering1.png b/plugins/ui/docs/_assets/conditional_rendering1.png
new file mode 100644
index 000000000..16dbe5eaf
Binary files /dev/null and b/plugins/ui/docs/_assets/conditional_rendering1.png differ
diff --git a/plugins/ui/docs/_assets/conditional_rendering2.png b/plugins/ui/docs/_assets/conditional_rendering2.png
new file mode 100644
index 000000000..98075135d
Binary files /dev/null and b/plugins/ui/docs/_assets/conditional_rendering2.png differ
diff --git a/plugins/ui/docs/_assets/conditional_rendering3.png b/plugins/ui/docs/_assets/conditional_rendering3.png
new file mode 100644
index 000000000..feae519d1
Binary files /dev/null and b/plugins/ui/docs/_assets/conditional_rendering3.png differ
diff --git a/plugins/ui/docs/_assets/pure_components1.png b/plugins/ui/docs/_assets/pure_components1.png
new file mode 100644
index 000000000..d2af21eed
Binary files /dev/null and b/plugins/ui/docs/_assets/pure_components1.png differ
diff --git a/plugins/ui/docs/_assets/pure_components2.png b/plugins/ui/docs/_assets/pure_components2.png
new file mode 100644
index 000000000..480e41fef
Binary files /dev/null and b/plugins/ui/docs/_assets/pure_components2.png differ
diff --git a/plugins/ui/docs/_assets/pure_components3.png b/plugins/ui/docs/_assets/pure_components3.png
new file mode 100644
index 000000000..9d4f35563
Binary files /dev/null and b/plugins/ui/docs/_assets/pure_components3.png differ
diff --git a/plugins/ui/docs/_assets/render_lists1.png b/plugins/ui/docs/_assets/render_lists1.png
new file mode 100644
index 000000000..eca88bb26
Binary files /dev/null and b/plugins/ui/docs/_assets/render_lists1.png differ
diff --git a/plugins/ui/docs/_assets/render_lists2.png b/plugins/ui/docs/_assets/render_lists2.png
new file mode 100644
index 000000000..3b876c3b5
Binary files /dev/null and b/plugins/ui/docs/_assets/render_lists2.png differ
diff --git a/plugins/ui/docs/_assets/work_with_tables1.png b/plugins/ui/docs/_assets/work_with_tables1.png
new file mode 100644
index 000000000..1f438f7ab
Binary files /dev/null and b/plugins/ui/docs/_assets/work_with_tables1.png differ
diff --git a/plugins/ui/docs/_assets/work_with_tables2.png b/plugins/ui/docs/_assets/work_with_tables2.png
new file mode 100644
index 000000000..ba27ec66d
Binary files /dev/null and b/plugins/ui/docs/_assets/work_with_tables2.png differ
diff --git a/plugins/ui/docs/_assets/your_first_component1.png b/plugins/ui/docs/_assets/your_first_component1.png
new file mode 100644
index 000000000..530f5a4b4
Binary files /dev/null and b/plugins/ui/docs/_assets/your_first_component1.png differ
diff --git a/plugins/ui/docs/_assets/your_first_component2.png b/plugins/ui/docs/_assets/your_first_component2.png
new file mode 100644
index 000000000..a779c55dc
Binary files /dev/null and b/plugins/ui/docs/_assets/your_first_component2.png differ
diff --git a/plugins/ui/docs/architecture.md b/plugins/ui/docs/architecture.md
index 1da0c0fb3..b36c3cbc2 100644
--- a/plugins/ui/docs/architecture.md
+++ b/plugins/ui/docs/architecture.md
@@ -31,6 +31,25 @@ def my_app():
app = my_app()
```
+## Props
+
+For almost all components, Python positional arguments are mapped to React children and keyword-only arguments are mapped to React props. Rarely, some arguments are positional and keyword. For example, in `contextual_help`, the footer argument is positional and keyword since it has a default of `None`. It will still be passed as a child.
+
+```python
+from deephaven import ui
+
+
+my_prop_variations = ui.flex("Hello", "World", direction="column")
+footer_as_positional = ui.contextual_help("Heading", "Content", "Footer")
+footer_as_keyword = ui.contextual_help("Heading", "Content", footer="Footer")
+```
+
+The strings `"Hello"` and `"World"` will be passed to flex as a child, while `"column"` is passed as the value to the `direction` prop. `"Footer"` is passed as a child even if it's used in a keyword-manner. For more information, see the [`contextual_help`](./components/contextual_help.md) doc.
+
+### Handling `null` vs `undefined`
+
+Python has one nullish value (`None`) while JavaScript has two (`null` and `undefined`). In most cases, a distinction is not needed and `None` is mapped to `undefined`. However, for some props, such as `picker`'s `selected_value`, we differentiate between `null` and `undefined` with `None` and `ui.types.Undefined`, respectively. A list of props that need the distinction is passed through the `_nullable_props` parameter to `component_element`/`BaseElement`.
+
## Rendering
When you call a function decorated by `@ui.component`, it will return an `Element` object that references the function it is decorated by; that is to say, the function does _not_ run immediately. The function runs when the `Element` is rendered by the client, and the result is sent back to the client. This allows the `@ui.component` decorator to execute the function with the appropriate rendering context. The client must also set the initial state before rendering, allowing the client to persist the state and re-render in the future.
diff --git a/plugins/ui/docs/components/logic_button.md b/plugins/ui/docs/components/logic_button.md
new file mode 100644
index 000000000..7186813ed
--- /dev/null
+++ b/plugins/ui/docs/components/logic_button.md
@@ -0,0 +1,59 @@
+# Logic Button
+
+A Logic Button shows an operator in a boolean logic sequence.
+
+## Example
+
+```python
+from deephaven import ui
+
+my_logic_button_basic = ui.logic_button("Or", variant="or")
+```
+
+## Events
+
+Logic buttons handles user interaction through the `on_press` prop.
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def ui_toggle_logic_button():
+ variant, set_variant = ui.use_state("or")
+
+ return ui.logic_button(
+ variant,
+ variant=variant,
+ on_press=lambda: set_variant("and" if variant == "or" else "or"),
+ )
+
+
+my_toggle_logic_button = ui_toggle_logic_button()
+```
+
+## Variants
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def ui_logic_button_variants():
+
+ return [
+ ui.logic_button("Or", variant="or"),
+ ui.logic_button("And", variant="and"),
+ ]
+
+
+my_logic_button_variants = ui_logic_button_variants()
+```
+
+## Disabled state
+
+```python
+from deephaven import ui
+
+my_logic_button_disabled = ui.logic_button("Or", variant="or", is_disabled=True)
+```
diff --git a/plugins/ui/docs/components/meter.md b/plugins/ui/docs/components/meter.md
new file mode 100644
index 000000000..2dd610cc6
--- /dev/null
+++ b/plugins/ui/docs/components/meter.md
@@ -0,0 +1,158 @@
+# Meter
+
+Meters visually represent a quantity or achievement, displaying progress on a bar with a label.
+
+## Example
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def ui_meter():
+ return ui.meter(label="RAM Usage", value=35)
+
+
+my_meter = ui_meter()
+```
+
+## Value
+
+The `value` prop controls the meter and represents the current percentage of progress. By default, the minimum and maximum values are 0 and 100 but a different scale can be used by setting the `min_value` and `max_value` props.
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def ui_meter_value():
+ return ui.meter(label="Tutorials completed", value=100, min_value=50, max_value=150)
+
+
+my_meter_value = ui_meter_value()
+```
+
+## Formatting
+
+The `format_options` prop dictates how the value is displayed and which characters can be inputted. This parameter supports three styles: Percentage, Currency, and Units.
+
+Note: This prop is compatible with the option parameter of [Intl.NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat).
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def ui_meter_format():
+ return ui.meter(
+ label="Currency",
+ value=75,
+ format_options={"style": "currency", "currency": "USD"},
+ )
+
+
+my_meter_format = ui_meter_format()
+```
+
+## Labeling
+
+When a label is provided, value labels are positioned above the meter by default. The `label_position` prop can change where these labels are placed, while the `show_value_prop` can hide them entirely.
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def ui_meter_label():
+ return [
+ ui.meter(
+ label="Label",
+ value=50,
+ ),
+ ui.meter(
+ label="Label",
+ value=50,
+ label_position="side",
+ ),
+ ui.meter(label="Label", value=50, show_value_label=False),
+ ]
+
+
+my_meter_label = ui_meter_label()
+```
+
+The `value_label` prop can update the value label directly where showing a different scale makes sense.
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def ui_meter_value_label():
+ return ui.meter(label="Currency", value=20, value_label="1 of 5")
+
+
+my_meter_value_label = ui_meter_value_label()
+```
+
+## Size
+
+The `size` prop controls how thick the meter bar is displayed.
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def ui_meter_size():
+ return [
+ ui.meter(label="Progress", value=75, size="S"),
+ ui.meter(label="Progress", value=75, size="L"),
+ ]
+
+
+my_meter_size = ui_meter_size()
+```
+
+## Variants
+
+The `variant` prop changes the meter's visual style. It supports four options: informative, positive, critical, and warning.
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def ui_meter_variant():
+ return [
+ ui.meter(
+ label="Progress",
+ value=75,
+ variant="informative",
+ ),
+ ui.meter(
+ label="Progress",
+ value=75,
+ variant="positive",
+ ),
+ ui.meter(
+ label="Progress",
+ value=75,
+ variant="critical",
+ ),
+ ui.meter(
+ label="Progress",
+ value=75,
+ variant="warning",
+ ),
+ ]
+
+
+my_meter_variant = ui_meter_variant()
+```
+
+## API Reference
+
+```{eval-rst}
+.. dhautofunction:: deephaven.ui.meter
+```
\ No newline at end of file
diff --git a/plugins/ui/docs/components/picker.md b/plugins/ui/docs/components/picker.md
index ad375fdc0..0720dddb5 100644
--- a/plugins/ui/docs/components/picker.md
+++ b/plugins/ui/docs/components/picker.md
@@ -10,7 +10,7 @@ from deephaven import ui
@ui.component
def ui_picker_basic():
- option, set_option = ui.use_state("")
+ option, set_option = ui.use_state(None)
return ui.picker(
"Rarely",
@@ -182,6 +182,36 @@ def ui_picker_selected_key_examples():
my_picker_selected_key_examples = ui_picker_selected_key_examples()
```
+Providing a value to the `selected_key` prop runs the component in "controlled" mode where the selection state is driven from the provided value. A value of `None` can be used to indicate nothing is selected while keeping the component in controlled mode. The default value is `ui.types.Undefined`, which causes the component to run in "uncontrolled" mode.
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def ui_picker_key_variations():
+ controlled_value, set_controlled_value = ui.use_state(None)
+
+ return [
+ ui.picker(
+ "Option 1",
+ "Option 2",
+ selected_key=controlled_value,
+ on_change=set_controlled_value,
+ label="Key: Controlled",
+ ),
+ ui.picker(
+ "Option 1",
+ "Option 2",
+ on_change=lambda x: print(x),
+ label="Key: Undefined",
+ ),
+ ]
+
+
+my_picker_key_variations = ui_picker_key_variations()
+```
+
## HTML Forms
diff --git a/plugins/ui/docs/describing/component_rules.md b/plugins/ui/docs/describing/component_rules.md
new file mode 100644
index 000000000..aabad3d85
--- /dev/null
+++ b/plugins/ui/docs/describing/component_rules.md
@@ -0,0 +1,130 @@
+# Component Rules
+
+This guide presents some important rules to remember for `deephaven.ui` components when writing queries.
+
+## Children and props
+
+Arguments passed to a component may be either `children` or `props`. `Children` are positional arguments passed to a `parent` component. `Props` are keyword arguments that determine the behavior and rendering style of the component. The `child` positional arguments must be passed first in the desired order. The `prop` keyword arguments can then be added in any order. Placing a `prop` before a `child` argument will cause the `child` to be out of order.
+
+```python
+from deephaven import ui
+
+my_flex = ui.flex(
+ ui.heading("Heading"),
+ ui.button("Button"),
+ ui.text("Text"),
+ direction="column",
+ wrap=True,
+ width="200px",
+)
+```
+
+![Children and props](../_assets/component_rules_1.png)
+
+In the above example, the `flex` component is the `parent`. It has three `children`: a `heading`, a `button`, and a `text` component. These `children` will be rendered inside the `flex`. It also has three props: `direction`, `wrap`, and `width`. These three props indicate that the flex should be rendered as a 200 pixel column with wrap enabled.
+
+## Comparison with JSX
+
+For developers familiar with React JSX, this example shows how `prop` and `child` arguments are specified in JSX.
+
+```html
+Hello World
+```
+
+Here is the same component written in `deephaven.ui`.
+
+```python
+my_component("Hello World", prop1="value1")
+```
+
+## Define your own children and props
+
+To define `children` and `props` for a custom component, add them as arguments to the component function. As a convention, you may declare the children using the `*` symbol to take any number of arguments.
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def custom_flex(*children, is_column):
+ return ui.flex(
+ ui.heading("My Component"),
+ children,
+ direction="column" if is_column else "row",
+ )
+
+
+my_custom_flex = custom_flex(ui.text("text"), ui.button("button"), is_column=True)
+```
+
+![Define your own children and props](../_assets/component_rules_2.png)
+
+## Component return values
+
+A `deephaven.ui` component usually returns a component. However, it may also return:
+
+- a list or tuple of components.
+- `None` if it should perform logic but does not need to be rendered.
+- a single value like a `string` or `int`.
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def return_component():
+ return ui.text("component")
+
+
+@ui.component
+def list_of_components():
+ return [ui.text("list"), ui.text("of"), ui.text("components")]
+
+
+@ui.component
+def return_tuple():
+ return (ui.text("a"), ui.text("b"))
+
+
+@ui.component
+def return_none():
+ print("return none")
+ return None
+
+
+@ui.component
+def return_string():
+ return "string"
+
+
+@ui.component
+def return_int():
+ return 1
+
+
+my_return_component = return_component()
+my_list_of_components = list_of_components()
+my_return_tuple = return_tuple()
+my_return_none = return_none()
+my_return_string = return_string()
+my_return_int = return_int()
+```
+
+## Conditional return
+
+Return statements can be conditional in order to render different components based on inputs.
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def return_conditional(is_button):
+ if is_button:
+ return ui.button("button")
+ return ui.text("text")
+
+
+my_button = return_conditional(True)
+my_text = return_conditional(False)
+```
diff --git a/plugins/ui/docs/describing/conditional_rendering.md b/plugins/ui/docs/describing/conditional_rendering.md
new file mode 100644
index 000000000..bd92cec11
--- /dev/null
+++ b/plugins/ui/docs/describing/conditional_rendering.md
@@ -0,0 +1,191 @@
+# Conditional Rendering
+
+Your components will often need to display different things depending on different conditions. In `deephaven.ui`, you can conditionally render components using Python syntax like if statements, the `and` operator, and the ternary operator.
+
+## Conditional returning
+
+Consider a `packing_list` component rendering several `item` components, which can be marked as packed or not:
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def item(name, is_packed):
+ return ui.text("- ", name)
+
+
+@ui.component
+def packing_list():
+ return ui.flex(
+ ui.heading("Packing list"),
+ item("Clothes", is_packed=True),
+ item("Shoes", is_packed=True),
+ item("Wallet", is_packed=False),
+ direction="column",
+ )
+
+
+my_packing_list = packing_list()
+```
+
+![my_packing_list](../_assets/conditional_rendering1.png)
+
+Some of the `item` components have their `is_packed` prop set to `True` instead of `False`.
+
+To add a checkmark (✅) to packed items if `is_packed=True`, you can write an if/else statement like so:
+
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def item(name, is_packed):
+ if is_packed:
+ return ui.text("- ", name + " ✅")
+ return ui.text("- ", name)
+
+
+@ui.component
+def packing_list():
+ return ui.flex(
+ ui.heading("Packing list"),
+ item("Clothes", is_packed=True),
+ item("Shoes", is_packed=True),
+ item("Wallet", is_packed=False),
+ direction="column",
+ )
+
+
+my_packing_list = packing_list()
+```
+
+![my_packing_list2](../_assets/conditional_rendering2.png)
+
+Notice you are creating branching logic with Python's `if` and `return` statements. In `deephaven.ui`, control flow (like conditions) is handled by Python.
+
+### Conditionally return nothing with `None`
+
+In some situations, you do not want to render anything at all. For example, you do not want to show any packed items. A component must return something. In this case, you can return `None`:
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def item(name, is_packed):
+ if is_packed:
+ return None
+ return ui.text("- ", name)
+
+
+@ui.component
+def packing_list():
+ return ui.flex(
+ ui.heading("Packing list"),
+ item("Clothes", is_packed=True),
+ item("Shoes", is_packed=True),
+ item("Wallet", is_packed=False),
+ direction="column",
+ )
+
+
+my_packing_list = packing_list()
+```
+
+![my_packing_list3](../_assets/conditional_rendering3.png)
+
+If `is_packed` is `True`, the component will return nothing. Otherwise, it will return a component to render.
+
+In practice, returning `None` from a component is not common because it might surprise a developer trying to render it. More often, you would conditionally include or exclude the component in the parent component. The next section explains how to do that.
+
+## Conditionally including components
+
+In the previous example, you controlled which component would be returned by using an [`if`/`else` statement](https://docs.python.org/3/tutorial/controlflow.html#if-statements). This led to some code duplication. You can remove this duplication by conditionally including components.
+
+### Conditional ternary
+
+Python has a [ternary conditional](https://docs.python.org/3/reference/expressions.html#conditional-expressions) in the form: `a if condition else b`. This can simplify the `item` component.
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def item(name, is_packed):
+ return ui.text("- ", name + " ✅" if is_packed else name)
+
+
+@ui.component
+def packing_list():
+ return ui.flex(
+ ui.heading("Packing list"),
+ item("Clothes", is_packed=True),
+ item("Shoes", is_packed=True),
+ item("Wallet", is_packed=False),
+ direction="column",
+ )
+
+
+my_packing_list = packing_list()
+```
+
+### Logical `and` operator
+
+Another common shortcut is the Python [logical `and` operator](https://docs.python.org/3/reference/expressions.html#and). Inside `deephaven.ui` components, it often comes up when you want to render a component when the condition is `True`, or render nothing otherwise. With `and`, you could conditionally render the checkmark only if `is_packed` is `True`:
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def item(name, is_packed):
+ return ui.text("- ", name, is_packed and " ✅")
+
+
+@ui.component
+def packing_list():
+ return ui.flex(
+ ui.heading("Packing list"),
+ item("Clothes", is_packed=True),
+ item("Shoes", is_packed=True),
+ item("Wallet", is_packed=False),
+ direction="column",
+ )
+
+
+my_packing_list = packing_list()
+```
+
+A Python `and` expression returns the value of its right side (in our case, the checkmark) if the left side (our condition) is `True`. But if the condition is `False`, the whole expression becomes `False`. `deephaven.ui` considers `False` to be like `None` and does not render anything in its place.
+
+### Conditionally assigning to a variable
+
+When the shortcuts get in the way of writing plain code, try using an `if` statement and a variable. You can reassign variables, so start by providing the default content you want to display. Use an `if` statement to reassign an expression to `item_content` if `is_packed` is `True`.
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def item(name, is_packed):
+ item_content = name
+ if is_packed:
+ item_content = name + " ✅"
+ return ui.text("- ", item_content)
+
+
+@ui.component
+def packing_list():
+ return ui.flex(
+ ui.heading("Packing list"),
+ item("Clothes", is_packed=True),
+ item("Shoes", is_packed=True),
+ item("Wallet", is_packed=False),
+ direction="column",
+ )
+
+
+my_packing_list = packing_list()
+```
diff --git a/plugins/ui/docs/describing/importing_and_exporting_components.md b/plugins/ui/docs/describing/importing_and_exporting_components.md
new file mode 100644
index 000000000..0621e5253
--- /dev/null
+++ b/plugins/ui/docs/describing/importing_and_exporting_components.md
@@ -0,0 +1,101 @@
+# Importing and Exporting Components
+
+The value of `deephaven.ui` components lies in their reusability: you can create components that are composed of other components. But as you nest more and more components, it often makes sense to start splitting them into different files. This lets you keep your files easy to scan and reuse components in more places.
+
+## Exporting and Importing in Deephaven Core
+
+In Deephaven Core, Python scripts cannot import from other Python scripts by default. In order to import from another script, you must place the script in the `data directory` and tell the Python interpreter where the `data directory` is located. For details on how to do this, see [How do I import one Python script into another in the Deephaven IDE?](/core/docs/reference/community-questions/import-python-script)
+
+### Example Export in Deephaven Core
+
+```python
+# file1.py
+from deephaven import ui
+
+
+@ui.component
+def table_of_contents():
+ return ui.flex(
+ ui.heading("My First Component"),
+ ui.text("- Components: UI Building Blocks"),
+ ui.text("- Defining a Component"),
+ ui.text("- Using a Component"),
+ direction="column",
+ )
+```
+
+### Example Import in Deephaven Core
+
+```python
+# file2.py
+# Tell the Python interpreter where the data directory is located
+import sys
+
+sys.path.append("/data/storage/notebooks")
+
+from deephaven import ui
+
+# Import component from file1
+from file1 import table_of_contents
+
+
+@ui.component
+def multiple_contents():
+ return ui.flex(
+ table_of_contents(),
+ table_of_contents(),
+ table_of_contents(),
+ )
+
+
+my_multiple_contents = multiple_contents()
+```
+
+## Exporting and Importing in Deephaven Enterprise
+
+In Deephaven Enterprise, notebook files are stored in a secure file system which prevents importing by default. In order to import from another script, you can use the `deephaven_enterprise.notebook` module to do either an `exec_notebook` or a `meta_import`. For details on how to do this, see [Modularizing Queries](/enterprise/docs/development/modularizing-queries).
+
+### Example Export in Deephaven Enterprise
+
+```python
+# file1.py
+from deephaven import ui
+
+
+@ui.component
+def table_of_contents():
+ return ui.flex(
+ ui.heading("My First Component"),
+ ui.text("- Components: UI Building Blocks"),
+ ui.text("- Defining a Component"),
+ ui.text("- Using a Component"),
+ direction="column",
+ )
+```
+
+### Example Import in Deephaven Enterprise
+
+```python
+# file2.py
+# Use the notebook module to meta_import file1.py
+from deephaven_enterprise.notebook import meta_import
+
+meta_import(db, "nb")
+
+# Import component from file1
+from nb.file1 import table_of_contents
+
+from deephaven import ui
+
+
+@ui.component
+def multiple_contents():
+ return ui.flex(
+ table_of_contents(),
+ table_of_contents(),
+ table_of_contents(),
+ )
+
+
+my_multiple_contents = multiple_contents()
+```
diff --git a/plugins/ui/docs/describing/pure_components.md b/plugins/ui/docs/describing/pure_components.md
new file mode 100644
index 000000000..1e0f95d82
--- /dev/null
+++ b/plugins/ui/docs/describing/pure_components.md
@@ -0,0 +1,116 @@
+# Pure Components
+
+A [pure function](https://en.wikipedia.org/wiki/Pure_function) returns the same value given the same arguments and has no side effects. By writing `deephaven.ui` components as pure functions, you can avoid bugs and unpredictable behavior.
+
+## Unintentional side effects
+
+The rendering process must always be pure. Component functions should always return the same value for the same arguments. They should not _change_ any objects or variables that existed before rendering. That would not be pure.
+
+Here is a component that breaks this rule by reading and writing a `guest` variable declared outside of it:
+
+```python
+from deephaven import ui
+
+guest = [0]
+
+
+@ui.component
+def cup():
+ # changing a preexisting variable
+ guest[0] += 1
+ return ui.text(f"Tea cup for guest {guest[0]}")
+
+
+@ui.component
+def tea_set():
+ return ui.flex(cup(), cup(), cup(), direction="column")
+
+
+my_tea_set1 = tea_set()
+my_tea_set2 = tea_set()
+```
+
+![side effects](../_assets/pure_components1.png)
+
+Calling this component multiple times will produce different results. If other components read `guest`, they will produce different results, too, depending on when they are rendered. That is not predictable.
+
+You can fix this component by passing `guest` as a prop instead:
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def cup(guest):
+ return ui.text(f"Tea cup for guest {guest}")
+
+
+@ui.component
+def tea_set():
+ return ui.flex(cup(guest=1), cup(guest=2), cup(guest=3), direction="column")
+
+
+my_tea_set1 = tea_set()
+my_tea_set2 = tea_set()
+```
+
+![side effects 2](../_assets/pure_components2.png)
+
+Now the component is pure. Its returns only depend on the `guest` prop.
+
+In general, you should not expect components to be rendered in any particular order. Each component should only “think for itself”, and not attempt to coordinate with or depend upon others during rendering.
+
+## Local mutations
+
+Pure functions do not mutate variables outside of the function's scope or objects that were created before the function call. However, it is fine to change variables and objects created inside the function. In this example, the component creates a list and adds a dozen cups to it:
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def cup(guest):
+ return ui.text(f"Tea cup for guest {guest}")
+
+
+@ui.component
+def tea_set():
+ cups = []
+ for i in range(1, 13):
+ cups.append(cup(guest=i))
+ return ui.flex(cups, direction="column")
+
+
+my_tea_set1 = tea_set()
+my_tea_set2 = tea_set()
+```
+
+![local mutations](../_assets/pure_components3.png)
+
+If the `cups` variable was outside the `tea_set` function, this would be a problem. You would be changing a preexisting object by appending items to that list.
+
+However, because you created them during the same render, no code outside of `tea_set` will be impacted by this. This is a local mutation.
+
+## Intentional side effects
+
+While the rendering process must remain pure, at some point, something needs to change. You may need to print a message, update the screen, start an animation, or change data. These changes are called side effects. They must happen on the side rather than during rendering.
+
+In `deephaven.ui`, side effects usually belong in event handlers. Event handlers are functions that run when you perform some action like clicking a button. Even though the event handlers are defined inside your component, they do not run during rendering, so even handlers do not need to be pure.
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def event_handler_example():
+ # An event handler for a button
+ def button_handler():
+ print("button pressed")
+
+ return ui.button("button", on_press=button_handler)
+
+
+my_event_handler_example = event_handler_example()
+```
+
+If an event handler is not the correct place for a certain side effect, you can place it in a [`use_effect`](../hooks/use_effect.md) hook. This tells `deephaven.ui` to execute it later, after rendering, when side effects are allowed.
diff --git a/plugins/ui/docs/describing/render_lists.md b/plugins/ui/docs/describing/render_lists.md
new file mode 100644
index 000000000..61f309c34
--- /dev/null
+++ b/plugins/ui/docs/describing/render_lists.md
@@ -0,0 +1,159 @@
+# Render Lists
+
+You will often want to display multiple similar components from a collection of data. You can use the Python [`filter`](https://docs.python.org/3/library/functions.html#filter) function and [`list comprehensions`](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions) with `deephaven.ui` to filter and transform your list of data into a list of components.
+
+## Render data from lists
+
+Here is an example list of content:
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def content_list():
+ return ui.flex(
+ ui.text("apple: fruit"),
+ ui.text("broccoli: vegetable"),
+ ui.text("banana: fruit"),
+ ui.text("yogurt: dairy"),
+ ui.text("carrot: vegetable"),
+ direction="column",
+ )
+
+
+my_content_list = content_list()
+```
+
+![my_content_list](../_assets/render_lists1.png)
+
+The only difference among those list items is their contents (their data). You will often need to show several instances of the same component using different data when building interfaces. Here is a short example of how to generate a list of items from a list of data:
+
+1. Move the data into a list
+2. Use list comprehension to map the list of data to a list of components
+3. Use the list of components in your component
+
+```python
+from deephaven import ui
+
+food = [
+ "apple: fruit",
+ "broccoli: vegetable",
+ "banana: fruit",
+ "yogurt: dairy",
+ "carrot: vegetable",
+]
+
+
+@ui.component
+def content_list(data):
+ # map the text items to components
+ components = [ui.text(item) for item in data]
+ return ui.flex(components, direction="column")
+
+
+my_content_list = content_list(food)
+```
+
+## Filter lists of items
+
+If you want a way to only show items of type vegetable, you can use the Python `filter` function to return just those items.
+
+```python
+from deephaven import ui
+
+food = [
+ "apple: fruit",
+ "broccoli: vegetable",
+ "banana: fruit",
+ "yogurt: dairy",
+ "carrot: vegetable",
+]
+
+
+@ui.component
+def content_list(data, data_type):
+ # filter for items that end with the desired data type
+ filtered = list(filter(lambda item: item.endswith(data_type), data))
+ # map the text items to components
+ components = [ui.text(item) for item in filtered]
+ return ui.flex(components, direction="column")
+
+
+my_content_list = content_list(food, "vegetable")
+```
+
+![my_content_list2](../_assets/render_lists2.png)
+
+## Keep list items in order with keys
+
+Keys tell `deephaven.ui` which list item each component corresponds to so that it can match them up later. This becomes important if your list items can move (e.g., due to sorting), get inserted, or get deleted. A well-chosen key helps `deephaven.ui` infer exactly what happened and make the correct updates.
+
+Rather than generating keys on the fly, you should include them in your data.
+
+### Where to get your key
+
+Different sources of data provide different sources of keys:
+
+- Data from a database: If your data is coming from a database, you can use the database keys/IDs, which are unique by nature.
+- Locally generated data: If your data is generated and persisted locally, use an incrementing counter or a package like `uuid` when creating items.
+
+### Rules of keys
+
+- Keys must be unique among siblings. However, it is okay to use the same keys for items in different lists.
+- Keys must not change. Do not generate them while rendering.
+
+In this example, the `ui_cells` component can add cell which can be deleted. The line `key=str(i)` is commented out, so the cell components do not have keys. If the user tries to delete a cell in the middle of the component, the last cell will be deleted instead. Comment in the line that sets the key. Now the correct cell will be deleted.
+
+```python
+from deephaven import ui
+import itertools
+
+
+@ui.component
+def ui_cell(label="Cell"):
+ text, set_text = ui.use_state("")
+ return ui.text_field(label=label, value=text, on_change=set_text)
+
+
+@ui.component
+def ui_deletable_cell(i, delete_cell):
+ return 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",
+ )
+
+
+@ui.component
+def ui_cells():
+ id_iter, set_id_iter = ui.use_state(lambda: itertools.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(
+ [
+ ui_deletable_cell(
+ i,
+ delete_cell,
+ # uncomment this line to fix
+ # key=str(i)
+ )
+ for i in cells
+ ],
+ ui.action_button(ui.icon("vsAdd"), "Add cell", on_press=add_cell),
+ overflow="auto",
+ )
+
+
+cells = ui_cells()
+```
diff --git a/plugins/ui/docs/describing/use_hooks.md b/plugins/ui/docs/describing/use_hooks.md
new file mode 100644
index 000000000..15576ece8
--- /dev/null
+++ b/plugins/ui/docs/describing/use_hooks.md
@@ -0,0 +1,186 @@
+# Use Hooks
+
+Hooks are Python functions that isolate reusable parts of a component. Built-in `deephaven.ui` hooks allow you to manage state, cache values, synchronize with external systems, and much more. You can either use the built-in hooks or combine them to build your own.
+
+## Rules for Hooks
+
+Hooks are Python functions, but you need to follow two rules when using them.
+
+1. Only call hooks at the top level.
+
+Don’t call hooks inside loops, conditions, or nested functions. Instead, always use hooks at the top level of your `deephaven.ui` component function, before any early returns. By following this rule, you ensure that hooks are called in the same order each time a component renders.
+
+2. Only call hooks from components and custom hooks
+
+Don’t call hooks from regular Python functions. Instead, you can:
+
+- Call Hooks from `@ui.component` decorated functions.
+- Call hooks from custom hooks.
+
+Following this rule ensures that all stateful logic in a component is clearly visible from its source code.
+
+## Built-in Hooks
+
+`deephaven.ui` has a large number of built-in hooks to help with the development of components. More details can be found in the [`hooks` section](../hooks/overview.md) of the documentation.
+
+### Use State Hook
+
+Call [`use_state`](../hooks/use_state.md) at the top level of your component to declare a state variable.
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def ui_counter():
+ count, set_count = ui.use_state(0)
+ return ui.button(f"Pressed {count} times", on_press=lambda: set_count(count + 1))
+
+
+counter = ui_counter()
+```
+
+The `use_state` hook takes an optional parameter, the initial state. If this is omitted, it initializes to `None`. The hook returns two values: a state variable and a `set` function that lets you update the state and trigger a re-render.
+
+### Use Memo Hook
+
+Call [`use_memo`](../hooks/use_memo.md) to cache the result of a calculation, function, or operation. This is useful when you have a value that is expensive to compute and you want to avoid re-computing it on every render.
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def ui_todo_list(todos: list[str], filter: str):
+ filtered_todos = ui.use_memo(
+ lambda: [todo for todo in todos if filter in todo], [todos, filter]
+ )
+
+ return [
+ ui.text(f"Showing {len(filtered_todos)} of {len(todos)} todos"),
+ *[ui.checkbox(todo) for todo in filtered_todos],
+ ]
+
+
+result = ui_todo_list(["Do grocery shopping", "Walk the dog", "Do laundry"], "Do")
+```
+
+The `use_memo` hook takes two parameters: a `callable` that returns a value and a list of dependencies. When dependencies are changed, the value is computed once and then stored in the memoized value. The memoized value is returned on subsequent renders until the dependencies change. The memoized value is returned on subsequent renders until the dependencies change.
+
+### Use Effect Hook
+
+Call [`use_effect`](../hooks/use_effect.md) to synchronize a component with an external system. An effect runs when it is mounted or a dependency changes. An optional cleanup function runs when dependencies change or the component is unmounted.
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def ui_effect_example():
+ def handle_mount():
+ # effect prints "Mounted" once when component is first rendered
+ print("Mounted")
+ # cleanup function prints "Unmounted" when component is closed
+ return lambda: print("Unmounted")
+
+ # Passing in an empty list for dependencies will run the effect only once when the component is mounted, and cleanup when the component is unmounted
+ ui.use_effect(handle_mount, [])
+
+ return ui.text("Effect Example")
+
+
+effect_example = ui_effect_example()
+```
+
+The `use_effect` hook takes two parameters: a callable and a list of dependencies. The callable may return a function for cleanup.
+
+### Use Callback Hook
+
+Call [`use_callback`](../hooks/use_callback.md) to memoize a callback function. This prevents unnecessary re-renders when the dependencies of the callback have not changed.
+
+```python
+from deephaven import ui
+import time
+
+
+@ui.component
+def ui_server():
+ theme, set_theme = ui.use_state("red")
+
+ create_server = ui.use_callback(lambda: {"host": "localhost"}, [])
+
+ def connect():
+ server = create_server()
+ print(f"Connecting to {server}")
+ time.sleep(0.5)
+
+ ui.use_effect(connect, [create_server])
+
+ return ui.view(
+ ui.picker(
+ "red",
+ "orange",
+ "yellow",
+ label="Theme",
+ selected_key=theme,
+ on_change=set_theme,
+ ),
+ padding="size-100",
+ background_color=theme,
+ )
+
+
+my_server = ui_server()
+```
+
+The `use_callback` hook takes two parameters: a callable and a list of dependencies. It returns a memoized callback. The memoized callback is returned on subsequent renders until the dependencies change.
+
+## Build your own hooks
+
+When you have reusable logic involving one or more hooks, you may want to write a custom hook to encapsulate that logic. A hook is a Python function that follows these guidelines:
+
+- Hooks can call other hooks, but usage of hooks within hooks follows the same rules as using hooks within components.
+- Custom hooks should start with the word `use` to indicate that is a hook and may contain component state and effects.
+
+### Example: Extracting the `use_server` hook
+
+Look back at the code example for the `use_callback` hook. The component uses two hooks to connect to a server. This logic can be extracted into a `use_server` hook to make it reusable by other components.
+
+```python
+from deephaven import ui
+import time
+
+# Custom hook
+def use_server():
+ create_server = ui.use_callback(lambda: {"host": "localhost"}, [])
+
+ def connect():
+ server = create_server()
+ print(f"Connecting to {server}")
+ time.sleep(0.5)
+
+ ui.use_effect(connect, [create_server])
+
+
+@ui.component
+def ui_server():
+ theme, set_theme = ui.use_state("red")
+
+ use_server()
+
+ return ui.view(
+ ui.picker(
+ "red",
+ "orange",
+ "yellow",
+ label="Theme",
+ selected_key=theme,
+ on_change=set_theme,
+ ),
+ padding="size-100",
+ background_color=theme,
+ )
+
+
+my_server = ui_server()
+```
diff --git a/plugins/ui/docs/describing/work_with_tables.md b/plugins/ui/docs/describing/work_with_tables.md
new file mode 100644
index 000000000..e283d2983
--- /dev/null
+++ b/plugins/ui/docs/describing/work_with_tables.md
@@ -0,0 +1,139 @@
+# Work with Tables
+
+The Deephaven table is the key abstraction that unites static and real-time data for a seamless, integrated experience. Combining tables with `deephaven.ui` components allows you to create your own powerful, data centered workflows.
+
+For more information, see [Working with Deephaven Tables](/core/docs/getting-started/quickstart/#4-working-with-deephaven-tables).
+
+## Display a table in a component
+
+You can display a Deephaven table in a component by doing one of the following:
+
+- return a table directly from a component
+- return a table as part of a `list` or `tuple`
+- add a table to a container such as a `flex` or `panel`
+- [use ui.table](#use-ui.table)
+
+```python
+from deephaven import new_table, ui
+from deephaven.column import int_col
+
+# Prepend name with an underscore to avoid displaying the source table
+_source = new_table([int_col("IntegerColumn", [1, 2, 3])])
+
+
+@ui.component
+def single_table(t):
+ ui.use_effect(lambda: print("displaying table"), [])
+ return t
+
+
+@ui.component
+def list_table(t):
+ return [ui.text("list table"), t]
+
+
+@ui.component
+def flex_table(t):
+ return ui.flex(ui.text("flex table"), t)
+
+
+my_single_table = single_table(_source)
+my_list_table = list_table(_source)
+my_flex_table = flex_table(_source)
+```
+
+![Display a table in a component](../_assets/work_with_tables1.png)
+
+## Use `ui.table`
+
+[`ui.table`](../components/table.md) is a wrapper for Deephaven tables that allows you to change how the table is displayed in the UI and how to handle user events. Here is an example of adding custom color formatting.
+
+```py
+from deephaven import ui
+import deephaven.plot.express as dx
+
+_stocks_table = dx.data.stocks()
+
+t = ui.table(
+ _stocks_table,
+ format_=[
+ ui.TableFormat(color="fg"),
+ ui.TableFormat(cols="Sym", color="white"),
+ ],
+)
+```
+
+![Use ui.table](../_assets/work_with_tables2.png)
+
+## Memoize table operations
+
+If you are working with a table, memoize the table operation. This stores the result in a memoized value and prevents the table from being re-computed on every render. This can be done with the [use_memo](../hooks/use_memo.md) hook.
+
+```python
+from deephaven import time_table, ui
+from deephaven.table import Table
+
+
+theme_options = ["accent-200", "red-200", "green-200"]
+
+
+@ui.component
+def ui_memo_table_app():
+ n, set_n = ui.use_state(1)
+ theme, set_theme = ui.use_state(theme_options[0])
+
+ # ✅ Memoize the table operation, only recompute when the dependency `n` changes
+ result_table = ui.use_memo(
+ lambda: time_table("PT1s").update(f"x=i*{n}").reverse(), [n]
+ )
+
+ return ui.view(
+ ui.flex(
+ ui.picker(
+ *theme_options, label="Theme", selected_key=theme, on_change=set_theme
+ ),
+ ui.slider(value=n, min_value=1, max_value=999, on_change=set_n, label="n"),
+ result_table,
+ direction="column",
+ height="100%",
+ ),
+ background_color=theme,
+ align_self="stretch",
+ flex_grow=1,
+ )
+
+
+memo_table_app = ui_memo_table_app()
+```
+
+## Hooks for tables
+
+The [`use_table_data`](../hooks/use_table_data.md) hook lets you use a table's data. This is useful when you want to listen to an updating table and use the data in your component.
+
+```python
+from deephaven import time_table, ui
+
+
+@ui.component
+def ui_table_data(table):
+ table_data = ui.use_table_data(table)
+ return ui.heading(f"The table data is {table_data}")
+
+
+table_data = ui_table_data(time_table("PT1s").update("x=i").tail(5))
+```
+
+The [`use_cell_data`](../hooks/use_cell_data.md) hook lets you use the cell data of the first cell (first row in the first column) in a table. This is useful when you want to listen to an updating table and use the data in your component.
+
+```python
+from deephaven import time_table, ui
+
+
+@ui.component
+def ui_table_first_cell(table):
+ cell_value = ui.use_cell_data(table)
+ return ui.heading(f"The first cell value is {cell_value}")
+
+
+table_first_cell = ui_table_first_cell(time_table("PT1s").tail(1))
+```
diff --git a/plugins/ui/docs/describing/your_first_component.md b/plugins/ui/docs/describing/your_first_component.md
new file mode 100644
index 000000000..0f968ae26
--- /dev/null
+++ b/plugins/ui/docs/describing/your_first_component.md
@@ -0,0 +1,107 @@
+# Your First Component
+
+`Components` are one of the core concepts of `deephaven.ui`. They are the foundation upon which you build user interfaces (UI).
+
+## Components: UI building blocks
+
+On the Web, HTML lets us create rich structured documents with its built-in set of tags like `
` and `
`:
+
+```html
+
+
My First Component
+
+ - Components: UI Building Blocks
+ - Defining a Component
+ - Defining a Component
+
+
+```
+
+This markup represents an article ``, its heading `
`, and an (abbreviated) table of contents as an ordered list ``. Markup like this, combined with CSS for style, and JavaScript for interactivity, lies behind every sidebar, avatar, modal, dropdown—every piece of UI you see on the Web.
+
+`Deephaven.ui` lets you use Python code to write custom "components", reusable UI elements for your app. The table of contents code you saw above could be turned into a `table_of_contents` component you could render in the UI.
+
+As your project grows, you will notice that many of your designs can be composed by reusing components you already wrote, speeding up your development.
+
+## Defining a component
+
+A `deephaven.ui` component is a Python function annotated with `@ui.component`. Here is what it looks like:
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def table_of_contents():
+ return ui.flex(
+ ui.heading("My First Component"),
+ ui.text("- Components: UI Building Blocks"),
+ ui.text("- Defining a Component"),
+ ui.text("- Using a Component"),
+ direction="column",
+ )
+
+
+my_table_of_contents = table_of_contents()
+```
+
+![table_of_contents](../_assets/your_first_component1.png)
+
+And here’s how to build a component:
+
+### Step 1: Import deephaven.ui
+
+Your Python code must include this import:
+
+```python
+from deephaven import ui
+```
+
+This allows you to access the `@ui.component` annotation and all of the `deephaven.ui` components which you will use to build your component.
+
+### Step 2: Define the function
+
+With `def table_of_contents():` you define a Python function with the name `table_of_contents`. It must have the `@ui.component` annotation.
+
+### Step 3: Add deephaven.ui components
+
+The component returns a `ui.flex` component with child components `ui.heading` and `ui.text`.
+
+## Using a component
+
+Now that you’ve defined your `table_of_contents` component, you can nest it inside other components. You can export an `multiple_contents` component that uses multiple `table_of_contents` components:
+
+```python
+from deephaven import ui
+
+
+@ui.component
+def table_of_contents():
+ return ui.flex(
+ ui.heading("My First Component"),
+ ui.text("- Components: UI Building Blocks"),
+ ui.text("- Defining a Component"),
+ ui.text("- Using a Component"),
+ direction="column",
+ )
+
+
+@ui.component
+def multiple_contents():
+ return ui.flex(
+ table_of_contents(),
+ table_of_contents(),
+ table_of_contents(),
+ )
+
+
+my_multiple_contents = multiple_contents()
+```
+
+![multiple_contents](../_assets/your_first_component2.png)
+
+## Nesting and organizing components
+
+Components are regular Python functions, so you can keep multiple components in the same file. This is convenient when components are relatively small or tightly related to each other. If this file gets crowded, you can always move a component to a separate file. See [How do I import one Python script into another in the Deephaven IDE?](/core/docs/reference/community-questions/import-python-script) and [Modularizing Queries](/enterprise/docs/development/modularizing-queries)
+
+Because the `table_of_contents` components are rendered inside `multiple_contents` we can say that `multiple_contents` is a parent component, rendering each `table_of_contents` as a "child". You can define a component once, and then use it in as many places and as many times as you like.
diff --git a/plugins/ui/docs/sidebar.json b/plugins/ui/docs/sidebar.json
index 8d2f995ff..d0f924735 100644
--- a/plugins/ui/docs/sidebar.json
+++ b/plugins/ui/docs/sidebar.json
@@ -22,6 +22,43 @@
"label": "Architecture",
"path": "architecture.md"
},
+ {
+ "label": "Describing the UI",
+ "items": [
+ {
+ "label": "Your First Component",
+ "path": "describing/your_first_component.md"
+ },
+ {
+ "label": "Importing and Exporting Components",
+ "path": "describing/importing_and_exporting_components.md"
+ },
+ {
+ "label": "Component Rules",
+ "path": "describing/component_rules.md"
+ },
+ {
+ "label": "Using Hooks",
+ "path": "describing/use_hooks.md"
+ },
+ {
+ "label": "Working with Tables",
+ "path": "describing/work_with_tables.md"
+ },
+ {
+ "label": "Conditional Rendering",
+ "path": "describing/conditional_rendering.md"
+ },
+ {
+ "label": "Render Lists",
+ "path": "describing/render_lists.md"
+ },
+ {
+ "label": "Pure Components",
+ "path": "describing/pure_components.md"
+ }
+ ]
+ },
{
"label": "Components",
"items": [
@@ -137,10 +174,18 @@
"label": "list_view",
"path": "components/list_view.md"
},
+ {
+ "label": "logic_button",
+ "path": "components/logic_button.md"
+ },
{
"label": "markdown",
"path": "components/markdown.md"
},
+ {
+ "label": "meter",
+ "path": "components/meter.md"
+ },
{
"label": "number_field",
"path": "components/number_field.md"
diff --git a/plugins/ui/setup.cfg b/plugins/ui/setup.cfg
index 4a4948d6c..6af963036 100644
--- a/plugins/ui/setup.cfg
+++ b/plugins/ui/setup.cfg
@@ -3,7 +3,7 @@ name = deephaven-plugin-ui
description = deephaven.ui plugin
long_description = file: README.md
long_description_content_type = text/markdown
-version = 0.23.1.dev0
+version = 0.24.0.dev0
url = https://github.com/deephaven/deephaven-plugins
project_urls =
Source Code = https://github.com/deephaven/deephaven-plugins
diff --git a/plugins/ui/src/deephaven/ui/_internal/utils.py b/plugins/ui/src/deephaven/ui/_internal/utils.py
index 22a3ec1c5..633fbf850 100644
--- a/plugins/ui/src/deephaven/ui/_internal/utils.py
+++ b/plugins/ui/src/deephaven/ui/_internal/utils.py
@@ -5,7 +5,6 @@
import sys
from functools import partial
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,
@@ -15,6 +14,7 @@
JavaTime,
LocalDateConvertible,
LocalDate,
+ Undefined,
)
T = TypeVar("T")
@@ -36,6 +36,19 @@
}
+def is_nullish(value: Any) -> bool:
+ """
+ Check if a value is nullish (`None` or `Undefined`).
+
+ Args:
+ value: The value to check.
+
+ Returns:
+ Checks if the value is nullish.
+ """
+ return value is None or value is Undefined
+
+
def get_component_name(component: Any) -> str:
"""
Get the name of the component
@@ -138,7 +151,9 @@ def dict_to_camel_case(
return convert_dict_keys(dict, to_camel_case)
-def dict_to_react_props(dict: dict[str, Any]) -> dict[str, Any]:
+def dict_to_react_props(
+ dict: dict[str, Any], _nullable_props: list[str] = []
+) -> dict[str, Any]:
"""
Convert a dict to React-style prop names ready for the web.
Converts snake_case to camelCase with the exception of special props like `UNSAFE_` or `aria_` props.
@@ -150,20 +165,36 @@ def dict_to_react_props(dict: dict[str, Any]) -> dict[str, Any]:
Returns:
The React props dict.
"""
- return convert_dict_keys(remove_empty_keys(dict), to_react_prop_case)
+ return convert_dict_keys(
+ remove_empty_keys(dict, _nullable_props), to_react_prop_case
+ )
-def remove_empty_keys(dict: dict[str, Any]) -> dict[str, Any]:
+def remove_empty_keys(
+ dict: dict[str, Any], _nullable_props: list[str] = []
+) -> dict[str, Any]:
"""
- Remove keys from a dict that have a value of None.
+ Remove keys from a dict that have a value of None, or Undefined if in _nullable_props.
Args:
dict: The dict to remove keys from.
+ _nullable_props: A list of props that get removed if they are Undefined (instead of None).
Returns:
The dict with keys removed.
"""
- return {k: v for k, v in dict.items() if v is not None}
+ cleaned = {}
+ for k, v in dict.items():
+ if k in _nullable_props:
+ if v is not Undefined:
+ cleaned[k] = v
+ else:
+ if v is Undefined:
+ raise ValueError("UndefinedType found in a non-nullable prop.")
+ elif v is not None:
+ cleaned[k] = v
+
+ return cleaned
def _wrapped_callable(
@@ -478,10 +509,10 @@ def _get_first_set_key(props: dict[str, Any], sequence: Sequence[str]) -> str |
sequence: The sequence to check.
Returns:
- The first non-None prop, or None if all props are None.
+ The first non-nullish prop, or None if all props are None.
"""
for key in sequence:
- if props.get(key) is not None:
+ if not is_nullish(props.get(key)):
return key
return None
@@ -523,9 +554,14 @@ def _prioritized_date_callable_converter(
"""
first_set_key = _get_first_set_key(props, priority)
+ # type ignore because pyright is not recognizing the nullish check
return (
- _jclass_date_converter(_date_or_range(props[first_set_key]))
- if first_set_key is not None
+ _jclass_date_converter(
+ _date_or_range(
+ props[first_set_key] # pyright: ignore[reportGeneralTypeIssues]
+ )
+ )
+ if not is_nullish(first_set_key)
else default_converter
)
@@ -552,9 +588,12 @@ def _prioritized_time_callable_converter(
"""
first_set_key = _get_first_set_key(props, priority)
+ # type ignore because pyright is not recognizing the nullish check
return (
- _jclass_time_converter(props[first_set_key])
- if first_set_key is not None
+ _jclass_time_converter(
+ props[first_set_key] # pyright: ignore[reportGeneralTypeIssues]
+ )
+ if not is_nullish(first_set_key)
else default_converter
)
@@ -666,11 +705,11 @@ def convert_date_props(
The converted props.
"""
for key in simple_date_props:
- if props.get(key) is not None:
+ if not is_nullish(props.get(key)):
props[key] = _convert_to_java_date(props[key])
for key in date_range_props:
- if props.get(key) is not None:
+ if not is_nullish(props.get(key)):
props[key] = convert_date_range(props[key], _convert_to_java_date)
# the simple props must be converted before this to simplify the callable conversion
@@ -680,25 +719,25 @@ def convert_date_props(
# Local Dates will default to DAY but we need to default to SECOND for the other types
if (
granularity_key is not None
- and props.get(granularity_key) is None
+ and is_nullish(props.get(granularity_key))
and converter != to_j_local_date
):
props[granularity_key] = "SECOND"
# now that the converter is set, we can convert simple props to strings
for key in simple_date_props:
- if props.get(key) is not None:
+ if not is_nullish(props.get(key)):
props[key] = str(props[key])
# and convert the date range props to strings
for key in date_range_props:
- if props.get(key) is not None:
+ if not is_nullish(props.get(key)):
props[key] = convert_date_range(props[key], str)
# wrap the date callable with the convert
# if there are date range props, we need to convert as a date range
for key in callable_date_props:
- if props.get(key) is not None:
+ if not is_nullish(props.get(key)):
if not callable(props[key]):
raise TypeError(f"{key} must be a callable")
if len(date_range_props) > 0:
@@ -730,7 +769,7 @@ def convert_time_props(
The converted props.
"""
for key in simple_time_props:
- if props.get(key) is not None:
+ if not is_nullish(props.get(key)):
props[key] = _convert_to_java_time(props[key])
# the simple props must be converted before this to simplify the callable conversion
@@ -738,12 +777,12 @@ def convert_time_props(
# 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:
+ if not is_nullish(props.get(key)):
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 is_nullish(props.get(key)):
if not callable(props[key]):
raise TypeError(f"{key} must be a callable")
props[key] = _wrap_time_callable(props[key], converter)
diff --git a/plugins/ui/src/deephaven/ui/components/__init__.py b/plugins/ui/src/deephaven/ui/components/__init__.py
index c54e95e4a..2702cd42a 100644
--- a/plugins/ui/src/deephaven/ui/components/__init__.py
+++ b/plugins/ui/src/deephaven/ui/components/__init__.py
@@ -36,8 +36,10 @@
from .list_action_group import list_action_group
from .list_action_menu import list_action_menu
from .list_view import list_view
+from .logic_button import logic_button
from .make_component import make_component as component
from .markdown import markdown
+from .meter import meter
from .number_field import number_field
from .panel import panel
from .picker import picker
@@ -107,8 +109,10 @@
"list_view",
"list_action_group",
"list_action_menu",
+ "logic_button",
"html",
"markdown",
+ "meter",
"number_field",
"panel",
"picker",
diff --git a/plugins/ui/src/deephaven/ui/components/action_menu.py b/plugins/ui/src/deephaven/ui/components/action_menu.py
index 2eca69c3b..10bb485ca 100644
--- a/plugins/ui/src/deephaven/ui/components/action_menu.py
+++ b/plugins/ui/src/deephaven/ui/components/action_menu.py
@@ -91,7 +91,7 @@ def action_menu(
ActionMenu combines an ActionButton with a Menu for simple "more actions" use cases.
Args:
- children: The contents of the collection.
+ *children: The contents of the collection.
is_disabled: Whether the button is disabled.
is_quiet: Whether the button should be displayed with a quiet style.
auto_focus: Whether the element should receive focus on render.
diff --git a/plugins/ui/src/deephaven/ui/components/calendar.py b/plugins/ui/src/deephaven/ui/components/calendar.py
index 4e3e1d76f..400c54494 100644
--- a/plugins/ui/src/deephaven/ui/components/calendar.py
+++ b/plugins/ui/src/deephaven/ui/components/calendar.py
@@ -15,10 +15,9 @@
from ..elements import Element
from .._internal.utils import create_props, convert_date_props, wrap_local_date_callable
-from ..types import Date, LocalDateConvertible
+from ..types import Date, LocalDateConvertible, Undefined, UndefinedType
from .basic import component_element
from .make_component import make_component
-from deephaven.time import dh_now
CalendarElement = Element
@@ -43,6 +42,8 @@
"default_focused_value",
]
+_NULLABLE_PROPS = ["value", "default_value"]
+
def _convert_calendar_props(
props: dict[str, Any],
@@ -75,8 +76,8 @@ def _convert_calendar_props(
@make_component
def calendar(
- value: Date | None = None,
- default_value: Date | None = None,
+ value: Date | None | UndefinedType = Undefined,
+ default_value: Date | None | UndefinedType = Undefined,
focused_value: Date | None = None,
default_focused_value: Date | None = None,
min_value: Date | None = None,
@@ -213,4 +214,4 @@ def calendar(
_convert_calendar_props(props)
- return component_element("Calendar", **props)
+ return component_element("Calendar", _nullable_props=_NULLABLE_PROPS, **props)
diff --git a/plugins/ui/src/deephaven/ui/components/column.py b/plugins/ui/src/deephaven/ui/components/column.py
index 4dbeea67b..85d9a981a 100644
--- a/plugins/ui/src/deephaven/ui/components/column.py
+++ b/plugins/ui/src/deephaven/ui/components/column.py
@@ -13,7 +13,7 @@ def column(
Each element will be placed below its prior sibling.
Args:
- children: Elements to render in the column.
+ *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.
diff --git a/plugins/ui/src/deephaven/ui/components/combo_box.py b/plugins/ui/src/deephaven/ui/components/combo_box.py
index 80defb967..b8d6514cd 100644
--- a/plugins/ui/src/deephaven/ui/components/combo_box.py
+++ b/plugins/ui/src/deephaven/ui/components/combo_box.py
@@ -27,9 +27,9 @@
from .section import SectionElement
from .item import Item
from .item_table_source import ItemTableSource
-from ..elements import BaseElement, Element
+from ..elements import BaseElement, Element, NodeType
from .._internal.utils import create_props, unpack_item_table_source
-from ..types import Key
+from ..types import Key, Undefined, UndefinedType
from .basic import component_element
ComboBoxElement = BaseElement
@@ -42,6 +42,8 @@
"title_column",
}
+_NULLABLE_PROPS = ["selected_key"]
+
def combo_box(
*children: Item | SectionElement | Table | PartitionedTable | ItemTableSource,
@@ -58,14 +60,14 @@ def combo_box(
default_input_value: str | None = None,
allows_custom_value: bool | None = None,
disabled_keys: list[Key] | None = None,
- selected_key: Key | None = None,
+ selected_key: Key | None | UndefinedType = Undefined,
default_selected_key: Key | None = None,
is_disabled: bool | None = None,
is_read_only: bool | None = None,
is_required: bool | None = None,
validation_behavior: ValidationBehavior = "aria",
auto_focus: bool | None = None,
- label: Element | None = None,
+ label: NodeType = None,
description: Element | None = None,
error_message: Element | None = None,
name: str | None = None,
@@ -75,7 +77,7 @@ def combo_box(
necessity_indicator: NecessityIndicator | None = None,
contextual_help: Element | None = None,
on_open_change: Callable[[bool, MenuTriggerAction], None] | None = None,
- on_selection_change: Callable[[Key], None] | None = None,
+ on_selection_change: Callable[[Key | None], None] | None = None,
on_change: Callable[[Key], None] | None = None,
on_input_change: Callable[[str], None] | None = None,
on_focus: Callable[[FocusEventCallable], None] | None = None,
@@ -241,4 +243,6 @@ def combo_box(
children, props = unpack_item_table_source(children, props, SUPPORTED_SOURCE_ARGS)
- return component_element("ComboBox", *children, **props)
+ return component_element(
+ "ComboBox", *children, _nullable_props=_NULLABLE_PROPS, **props
+ )
diff --git a/plugins/ui/src/deephaven/ui/components/date_field.py b/plugins/ui/src/deephaven/ui/components/date_field.py
index b24451fdb..8dafbd244 100644
--- a/plugins/ui/src/deephaven/ui/components/date_field.py
+++ b/plugins/ui/src/deephaven/ui/components/date_field.py
@@ -20,12 +20,12 @@
Alignment,
)
-from ..elements import Element
+from ..elements import Element, NodeType
from .._internal.utils import (
create_props,
convert_date_props,
)
-from ..types import Date, Granularity
+from ..types import Date, Granularity, Undefined, UndefinedType
from .basic import component_element
from .make_component import make_component
from deephaven.time import dh_now
@@ -47,6 +47,8 @@
# The priority of the date props to determine the format of the date passed to the callable date props
_DATE_PROPS_PRIORITY = ["value", "default_value", "placeholder_value"]
+_NULLABLE_PROPS = ["value", "default_value"]
+
def _convert_date_field_props(
props: dict[str, Any],
@@ -76,8 +78,8 @@ def _convert_date_field_props(
@make_component
def date_field(
placeholder_value: Date | None = dh_now(),
- value: Date | None = None,
- default_value: Date | None = None,
+ value: Date | None | UndefinedType = Undefined,
+ default_value: Date | None | UndefinedType = Undefined,
min_value: Date | None = None,
max_value: Date | None = None,
# TODO (issue # 698) we need to implement unavailable_values
@@ -91,7 +93,7 @@ def date_field(
is_required: bool | None = None,
validation_behavior: ValidationBehavior | None = None,
auto_focus: bool | None = None,
- label: Element | None = None,
+ label: NodeType = None,
description: Element | None = None,
error_message: Element | None = None,
is_open: bool | None = None,
@@ -261,4 +263,4 @@ def date_field(
_convert_date_field_props(props)
- return component_element("DateField", **props)
+ return component_element("DateField", _nullable_props=_NULLABLE_PROPS, **props)
diff --git a/plugins/ui/src/deephaven/ui/components/date_picker.py b/plugins/ui/src/deephaven/ui/components/date_picker.py
index 1763f8c8b..ab886b40a 100644
--- a/plugins/ui/src/deephaven/ui/components/date_picker.py
+++ b/plugins/ui/src/deephaven/ui/components/date_picker.py
@@ -22,13 +22,13 @@
)
from ..hooks import use_memo
-from ..elements import Element
+from ..elements import Element, NodeType
from .._internal.utils import (
create_props,
convert_date_props,
convert_list_prop,
)
-from ..types import Date, Granularity
+from ..types import Date, Granularity, Undefined, UndefinedType
from .basic import component_element
from .make_component import make_component
from deephaven.time import dh_now
@@ -51,6 +51,8 @@
# The priority of the date props to determine the format of the date passed to the callable date props
_DATE_PROPS_PRIORITY = ["value", "default_value", "placeholder_value"]
+_NULLABLE_PROPS = ["value", "default_value"]
+
def _convert_date_picker_props(
props: dict[str, Any],
@@ -80,8 +82,8 @@ def _convert_date_picker_props(
@make_component
def date_picker(
placeholder_value: Date | None = dh_now(),
- value: Date | None = None,
- default_value: Date | None = None,
+ value: Date | None | UndefinedType = Undefined,
+ default_value: Date | None | UndefinedType = Undefined,
min_value: Date | None = None,
max_value: Date | None = None,
# TODO (issue # 698) we need to implement unavailable_values
@@ -96,7 +98,7 @@ def date_picker(
is_required: bool | None = None,
validation_behavior: ValidationBehavior | None = None,
auto_focus: bool | None = None,
- label: Element | None = None,
+ label: NodeType = None,
description: Element | None = None,
error_message: Element | None = None,
is_open: bool | None = None,
@@ -280,4 +282,4 @@ def date_picker(
# [unavailable_values],
# )
- return component_element("DatePicker", **props)
+ return component_element("DatePicker", _nullable_props=_NULLABLE_PROPS, **props)
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 86768abbc..45d4fb950 100644
--- a/plugins/ui/src/deephaven/ui/components/date_range_picker.py
+++ b/plugins/ui/src/deephaven/ui/components/date_range_picker.py
@@ -22,13 +22,13 @@
)
from ..hooks import use_memo
-from ..elements import Element
+from ..elements import Element, NodeType
from .._internal.utils import (
create_props,
convert_date_props,
convert_list_prop,
)
-from ..types import Date, Granularity, DateRange
+from ..types import Date, Granularity, DateRange, Undefined, UndefinedType
from .basic import component_element
from .make_component import make_component
from deephaven.time import dh_now
@@ -49,6 +49,8 @@
# The priority of the date props to determine the format of the date passed to the callable date props
_DATE_PROPS_PRIORITY = ["value", "default_value", "placeholder_value"]
+_NULLABLE_PROPS = ["value", "default_value"]
+
def _convert_date_range_picker_props(
props: dict[str, Any],
@@ -78,8 +80,8 @@ def _convert_date_range_picker_props(
@make_component
def date_range_picker(
placeholder_value: Date | None = dh_now(),
- value: DateRange | None = None,
- default_value: DateRange | None = None,
+ value: DateRange | None | UndefinedType = Undefined,
+ default_value: DateRange | None | UndefinedType = Undefined,
min_value: Date | None = None,
max_value: Date | None = None,
# TODO (issue # 698) we need to implement unavailable_values
@@ -94,7 +96,7 @@ def date_range_picker(
is_required: bool | None = None,
validation_behavior: ValidationBehavior | None = None,
auto_focus: bool | None = None,
- label: Element | None = None,
+ label: NodeType = None,
description: Element | None = None,
error_message: Element | None = None,
is_open: bool | None = None,
@@ -278,4 +280,6 @@ def date_range_picker(
_convert_date_range_picker_props(props)
- return component_element("DateRangePicker", **props)
+ return component_element(
+ "DateRangePicker", _nullable_props=_NULLABLE_PROPS, **props
+ )
diff --git a/plugins/ui/src/deephaven/ui/components/logic_button.py b/plugins/ui/src/deephaven/ui/components/logic_button.py
new file mode 100644
index 000000000..d836bdd0e
--- /dev/null
+++ b/plugins/ui/src/deephaven/ui/components/logic_button.py
@@ -0,0 +1,233 @@
+from __future__ import annotations
+from typing import Any, Callable
+from .types import (
+ # Accessibility
+ AriaExpanded,
+ AriaHasPopup,
+ AriaPressed,
+ # Events
+ ButtonType,
+ FocusEventCallable,
+ KeyboardEventCallable,
+ PressEventCallable,
+ StaticColor,
+ # Layout
+ AlignSelf,
+ CSSProperties,
+ DimensionValue,
+ JustifySelf,
+ LayoutFlex,
+ Position,
+)
+from .basic import component_element
+from ..elements import Element
+
+
+def logic_button(
+ *children: Any,
+ variant: str | None = None,
+ is_disabled: bool | None = None,
+ auto_focus: bool | None = None,
+ type: ButtonType = "button",
+ on_press: PressEventCallable | None = None,
+ on_press_start: PressEventCallable | None = None,
+ on_press_end: PressEventCallable | None = None,
+ on_press_change: Callable[[bool], None] | None = None,
+ on_press_up: PressEventCallable | 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,
+ 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_column: str | None = None,
+ grid_row: str | None = None,
+ grid_column_start: str | None = None,
+ grid_column_end: str | None = None,
+ grid_row_start: str | None = None,
+ grid_row_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,
+ left: DimensionValue | None = None,
+ right: DimensionValue | None = None,
+ start: DimensionValue | None = None,
+ end: 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,
+ key: str | None = None,
+) -> Element:
+ """
+
+ A LogicButton shows an operator in a boolean logic sequence.
+
+ Args:
+ *children: The children to render inside the button.
+ variant: The variant of the button. (default: "primary")
+ is_disabled: Whether the button is disabled.
+ auto_focus: Whether the button should automatically get focus when the page loads.
+ type: The type of button to render. (default: "button")
+ on_press: Function called when the button is pressed.
+ on_press_start: Function called when the button is pressed.
+ on_press_end: Function called when a press interaction ends, either over the target or when the pointer leaves the target.
+ on_press_up: Function called when the button is released.
+ on_press_change: Function called when the press state changes.
+ 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.
+ 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.
+ key: A unique identifier used by React to render elements in a list.
+
+ Returns:
+ The rendered toggle button element.
+
+ """
+
+ return component_element(
+ "LogicButton",
+ *children,
+ variant=variant,
+ is_disabled=is_disabled,
+ type=type,
+ auto_focus=auto_focus,
+ on_press=on_press,
+ on_press_start=on_press_start,
+ on_press_end=on_press_end,
+ on_press_change=on_press_change,
+ on_press_up=on_press_up,
+ on_focus=on_focus,
+ on_blur=on_blur,
+ on_focus_change=on_focus_change,
+ on_key_down=on_key_down,
+ on_key_up=on_key_up,
+ 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_column=grid_column,
+ grid_row=grid_row,
+ grid_column_start=grid_column_start,
+ grid_column_end=grid_column_end,
+ grid_row_start=grid_row_start,
+ grid_row_end=grid_row_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,
+ left=left,
+ right=right,
+ start=start,
+ end=end,
+ 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,
+ key=key,
+ )
diff --git a/plugins/ui/src/deephaven/ui/components/meter.py b/plugins/ui/src/deephaven/ui/components/meter.py
new file mode 100644
index 000000000..fb724d73e
--- /dev/null
+++ b/plugins/ui/src/deephaven/ui/components/meter.py
@@ -0,0 +1,194 @@
+from __future__ import annotations
+from typing import Any
+from .types import (
+ AlignSelf,
+ CSSProperties,
+ DimensionValue,
+ JustifySelf,
+ LayoutFlex,
+ Position,
+ LabelPosition,
+ NumberFormatOptions,
+ MeterVariants,
+ MeterSizes,
+)
+from .basic import component_element
+from ..elements import Element
+
+
+def meter(
+ variant: MeterVariants = "informative",
+ size: MeterSizes = "L",
+ label_position: LabelPosition = "top",
+ show_value_label: bool | None = None,
+ label: Any | None = None,
+ format_options: NumberFormatOptions | None = None,
+ value_label: Any | 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,
+ aria_label: str | None = None,
+ aria_labelledby: str | None = None,
+ aria_describedby: str | None = None,
+ aria_details: str | None = None,
+ UNSAFE_class_name: str | None = None,
+ UNSAFE_style: CSSProperties | None = None,
+ key: str | None = None,
+) -> Element:
+ """
+ Meters visually represent a quantity or achievement, displaying progress on a bar with a label.
+
+ Args:
+ variant: The visual style of the meter.
+ size: How thick the bar should be.
+ label_position: The position of the label relative to the input.
+ show_value_label: Whether to show the value label.
+ label: The label for the input.
+ format_options: Options for formatting the displayed value, which also restricts input characters.
+ value_label: The label for the value (e.g. 1 of 4).
+ value: The current value of the input
+ min_value: The minimum value of the input
+ max_value: The maximum value of 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 the element will grow to fit the space available.
+ flex_shrink: When used in a flex layout, specifies how the element will shrink to fit the space available.
+ flex_basis: When used in a flex layout, specifies the initial main size of the element.
+ align_self: Overrides the alignItems property of a flex or grid container.
+ justify_self: Species 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: When used in a grid layout specifies, specifies the named grid area that the element should be placed in within the grid.
+ grid_row: When used in a grid layout, specifies the row the element should be placed in within the grid.
+ grid_column: When used in a grid layout, specifies the column the element should be placed in within the grid.
+ grid_row_start: When used in a grid layout, specifies the starting row to span within the grid.
+ grid_row_end: When used in a grid layout, specifies the ending row to span within the grid.
+ grid_column_start: When used in a grid layout, specifies the starting column to span within the grid.
+ grid_column_end: When used in a grid layout, specifies the ending column to span within the grid.
+ margin: The margin for all four sides of the element.
+ margin_top: The margin for the top side of the element.
+ margin_bottom: The margin for the bottom side of the element.
+ margin_start: The margin for the logical start side of the element, depending on layout direction.
+ margin_end: The margin for the logical end side of the element, depending on layout direction.
+ margin_x: The margin for the left and right sides of the element.
+ margin_y: The margin for the top and bottom sides of the element.
+ width: The width of the element.
+ min_width: The minimum width of the element.
+ max_width: The maximum width of the element.
+ height: The height of the element.
+ min_height: The minimum height of the element.
+ max_height: The maximum height of the element.
+ position: The position of the element.
+ top: The distance from the top of the containing element.
+ bottom: The distance from the bottom of the containing element.
+ left: The distance from the left of the containing element.
+ right: The distance from the right of the containing element.
+ start: The distance from the start of the containing element, depending on layout direction.
+ end: The distance from the end of the containing element, depending on layout direction.
+ z_index: The stack order of the element.
+ is_hidden: Whether the element is hidden.
+ id: The unique identifier of the element.
+ aria_label: The label for the element.
+ aria_labelledby: The id of the element that labels the current element.
+ aria_describedby: The id of the element that describes the current element.
+ 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 meter element.
+ """
+
+ return component_element(
+ "Meter",
+ variant=variant,
+ size=size,
+ label_position=label_position,
+ show_value_label=show_value_label,
+ label=label,
+ format_options=format_options,
+ value_label=value_label,
+ 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,
+ aria_label=aria_label,
+ aria_labelledby=aria_labelledby,
+ aria_describedby=aria_describedby,
+ 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/number_field.py b/plugins/ui/src/deephaven/ui/components/number_field.py
index 92f4adaf1..9b17c8e28 100644
--- a/plugins/ui/src/deephaven/ui/components/number_field.py
+++ b/plugins/ui/src/deephaven/ui/components/number_field.py
@@ -12,7 +12,7 @@
LayoutFlex,
Position,
LabelPosition,
- NumberFieldFormatOptions,
+ NumberFormatOptions,
Alignment,
)
from .basic import component_element
@@ -25,7 +25,7 @@ def number_field(
decrement_aria_label: str | None = None,
increment_aria_label: str | None = None,
is_wheel_disabled: bool | None = None,
- format_options: NumberFieldFormatOptions | None = None,
+ format_options: NumberFormatOptions | None = None,
is_disabled: bool | None = None,
is_read_only: bool | None = None,
is_required: bool | None = None,
diff --git a/plugins/ui/src/deephaven/ui/components/picker.py b/plugins/ui/src/deephaven/ui/components/picker.py
index 1f3fa4471..cc02c583d 100644
--- a/plugins/ui/src/deephaven/ui/components/picker.py
+++ b/plugins/ui/src/deephaven/ui/components/picker.py
@@ -6,9 +6,9 @@
from .basic import component_element
from .section import SectionElement, Item
from .item_table_source import ItemTableSource
-from ..elements import BaseElement, Element
+from ..elements import BaseElement, Element, NodeType
from .._internal.utils import create_props, unpack_item_table_source
-from ..types import Key
+from ..types import Key, Undefined, UndefinedType
from .types import (
AlignSelf,
CSSProperties,
@@ -35,11 +35,13 @@
"title_column",
}
+_NULLABLE_PROPS = ["selected_key"]
+
def picker(
*children: Item | SectionElement | Table | PartitionedTable | ItemTableSource,
default_selected_key: Key | None = None,
- selected_key: Key | None = None,
+ selected_key: Key | None | UndefinedType = Undefined,
on_selection_change: Callable[[Key], None] | None = None,
on_change: Callable[[Key], None] | None = None,
is_quiet: bool | None = None,
@@ -58,7 +60,7 @@ def picker(
validation_behavior: ValidationBehavior | None = None,
description: Element | None = None,
error_message: Element | None = None,
- label: Element | None = None,
+ label: NodeType = None,
placeholder: str | None = None,
is_loading: bool | None = None,
label_position: LabelPosition = "top",
@@ -227,4 +229,6 @@ def picker(
children, props = unpack_item_table_source(children, props, SUPPORTED_SOURCE_ARGS)
- return component_element("Picker", *children, **props)
+ return component_element(
+ "Picker", *children, _nullable_props=_NULLABLE_PROPS, **props
+ )
diff --git a/plugins/ui/src/deephaven/ui/components/progress_bar.py b/plugins/ui/src/deephaven/ui/components/progress_bar.py
index ee2a7ef28..9c2945165 100644
--- a/plugins/ui/src/deephaven/ui/components/progress_bar.py
+++ b/plugins/ui/src/deephaven/ui/components/progress_bar.py
@@ -14,9 +14,8 @@
Position,
ProgressBarSize,
)
-
from .basic import component_element
-from ..elements import Element
+from ..elements import Element, NodeType
ProgressBarElement = Element
@@ -26,9 +25,9 @@ def progress_bar(
static_color: StaticColor | None = None,
label_position: LabelPosition = "top",
show_value_label: bool | None = None,
- label: Element | None = None,
+ label: NodeType = None,
# format_options, # omitted because need to connect it to Deephaven formatting options as well
- value_label: Element | None = None,
+ value_label: NodeType = None,
value: float = 0,
min_value: float = 0,
max_value: float = 100,
diff --git a/plugins/ui/src/deephaven/ui/components/radio_group.py b/plugins/ui/src/deephaven/ui/components/radio_group.py
index 2567fe9ce..4477f6f23 100644
--- a/plugins/ui/src/deephaven/ui/components/radio_group.py
+++ b/plugins/ui/src/deephaven/ui/components/radio_group.py
@@ -19,15 +19,19 @@
)
from .basic import component_element
from ..elements import Element
+from ..types import Undefined, UndefinedType
from .._internal.utils import create_props
+_NULLABLE_PROPS = ["value", "default_value"]
+
+
def radio_group(
*children: Any,
is_emphasized: bool | None = None,
orientation: Orientation = "vertical",
- value: str | None = None,
- default_value: str | None = None,
+ value: str | None | UndefinedType = Undefined,
+ default_value: str | None | UndefinedType = Undefined,
is_disabled: bool | None = None,
is_read_only: bool | None = None,
name: str | None = None,
@@ -174,4 +178,6 @@ def radio_group(
children, props = create_props(locals())
- return component_element(f"RadioGroup", *children, **props)
+ return component_element(
+ f"RadioGroup", *children, _nullable_props=_NULLABLE_PROPS, **props
+ )
diff --git a/plugins/ui/src/deephaven/ui/components/range_calendar.py b/plugins/ui/src/deephaven/ui/components/range_calendar.py
index a3082fa83..f006e3f9e 100644
--- a/plugins/ui/src/deephaven/ui/components/range_calendar.py
+++ b/plugins/ui/src/deephaven/ui/components/range_calendar.py
@@ -15,10 +15,15 @@
from ..elements import Element
from .._internal.utils import create_props, convert_date_props, wrap_local_date_callable
-from ..types import Date, LocalDateConvertible, DateRange
+from ..types import (
+ Date,
+ LocalDateConvertible,
+ DateRange,
+ Undefined,
+ UndefinedType,
+)
from .basic import component_element
from .make_component import make_component
-from deephaven.time import dh_now
RangeCalendarElement = Element
@@ -41,6 +46,8 @@
"default_focused_value",
]
+_NULLABLE_PROPS = ["value", "default_value"]
+
def _convert_range_calendar_props(
props: dict[str, Any],
@@ -73,8 +80,8 @@ def _convert_range_calendar_props(
@make_component
def range_calendar(
- value: DateRange | None = None,
- default_value: DateRange | None = None,
+ value: DateRange | None | UndefinedType = Undefined,
+ default_value: DateRange | None | UndefinedType = Undefined,
focused_value: Date | None = None,
default_focused_value: Date | None = None,
min_value: Date | None = None,
@@ -211,4 +218,4 @@ def range_calendar(
_convert_range_calendar_props(props)
- return component_element("RangeCalendar", **props)
+ return component_element("RangeCalendar", _nullable_props=_NULLABLE_PROPS, **props)
diff --git a/plugins/ui/src/deephaven/ui/components/tabs.py b/plugins/ui/src/deephaven/ui/components/tabs.py
index ebed99672..354db8d48 100644
--- a/plugins/ui/src/deephaven/ui/components/tabs.py
+++ b/plugins/ui/src/deephaven/ui/components/tabs.py
@@ -14,12 +14,15 @@
Position,
)
-from ..types import Key, TabDensity
+from ..types import Key, TabDensity, Undefined, UndefinedType
from ..elements import BaseElement
TabElement = BaseElement
+_NULLABLE_PROPS = ["selected_key"]
+
+
def tabs(
*children: Any,
disabled_keys: Iterable[Key] | None = None,
@@ -30,7 +33,7 @@ def tabs(
keyboard_activation: KeyboardActivationType | None = "automatic",
orientation: Orientation | None = "horizontal",
disallow_empty_selection: bool | None = None,
- selected_key: Key | None = None,
+ selected_key: Key | None | UndefinedType = Undefined,
default_selected_key: Key | None = None,
on_selection_change: Callable[[Key], None] | None = None,
on_change: Callable[[Key], None] | None = None,
@@ -231,4 +234,5 @@ def tabs(
UNSAFE_class_name=UNSAFE_class_name,
UNSAFE_style=UNSAFE_style,
key=key,
+ _nullable_props=_NULLABLE_PROPS,
)
diff --git a/plugins/ui/src/deephaven/ui/components/text_area.py b/plugins/ui/src/deephaven/ui/components/text_area.py
index 67b394947..9df997110 100644
--- a/plugins/ui/src/deephaven/ui/components/text_area.py
+++ b/plugins/ui/src/deephaven/ui/components/text_area.py
@@ -25,12 +25,16 @@
from .types import IconTypes
from .basic import component_element
from ..elements import Element
+from ..types import Undefined, UndefinedType
from .icon import icon as icon_component
+_NULLABLE_PROPS = ["icon"]
+
+
def text_area(
- icon: Element | IconTypes | None = None,
+ icon: Element | IconTypes | None | UndefinedType = Undefined,
is_quiet: bool | None = None,
is_disabled: bool | None = None,
is_read_only: bool | None = None,
@@ -271,4 +275,5 @@ def text_area(
UNSAFE_class_name=UNSAFE_class_name,
UNSAFE_style=UNSAFE_style,
key=key,
+ _nullable_props=_NULLABLE_PROPS,
)
diff --git a/plugins/ui/src/deephaven/ui/components/text_field.py b/plugins/ui/src/deephaven/ui/components/text_field.py
index 52debc960..41985445c 100644
--- a/plugins/ui/src/deephaven/ui/components/text_field.py
+++ b/plugins/ui/src/deephaven/ui/components/text_field.py
@@ -24,10 +24,14 @@
)
from .basic import component_element
from ..elements import Element
+from ..types import Undefined, UndefinedType
+
+
+_NULLABLE_PROPS = ["icon"]
def text_field(
- icon: Element | None = None,
+ icon: Element | None | UndefinedType = Undefined,
is_quiet: bool | None = None,
is_disabled: bool | None = None,
is_read_only: bool | None = None,
@@ -274,4 +278,5 @@ def text_field(
UNSAFE_class_name=UNSAFE_class_name,
UNSAFE_style=UNSAFE_style,
key=key,
+ _nullable_props=_NULLABLE_PROPS,
)
diff --git a/plugins/ui/src/deephaven/ui/components/time_field.py b/plugins/ui/src/deephaven/ui/components/time_field.py
index db5d5a98d..91d1129d5 100644
--- a/plugins/ui/src/deephaven/ui/components/time_field.py
+++ b/plugins/ui/src/deephaven/ui/components/time_field.py
@@ -20,12 +20,12 @@
Alignment,
)
-from ..elements import Element
+from ..elements import Element, NodeType
from .._internal.utils import (
create_props,
convert_time_props,
)
-from ..types import Time, TimeGranularity
+from ..types import Time, TimeGranularity, Undefined, UndefinedType
from .basic import component_element
from .make_component import make_component
@@ -44,6 +44,8 @@
# 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"]
+_NULLABLE_PROPS = ["value", "default_value"]
+
def _convert_time_field_props(
props: dict[str, Any],
@@ -71,8 +73,8 @@ def _convert_time_field_props(
@make_component
def time_field(
placeholder_value: Time | None = None,
- value: Time | None = None,
- default_value: Time | None = None,
+ value: Time | None | UndefinedType = Undefined,
+ default_value: Time | None | UndefinedType = Undefined,
min_value: Time | None = None,
max_value: Time | None = None,
granularity: TimeGranularity | None = "SECOND",
@@ -84,7 +86,7 @@ def time_field(
is_required: bool | None = None,
validation_behavior: ValidationBehavior | None = None,
auto_focus: bool | None = None,
- label: Element | None = None,
+ label: NodeType = None,
description: Element | None = None,
error_message: Element | None = None,
name: str | None = None,
@@ -245,4 +247,4 @@ def time_field(
_convert_time_field_props(props)
- return component_element("TimeField", **props)
+ return component_element("TimeField", _nullable_props=_NULLABLE_PROPS, **props)
diff --git a/plugins/ui/src/deephaven/ui/components/types/Intl/number_field.py b/plugins/ui/src/deephaven/ui/components/types/Intl/number_format.py
similarity index 99%
rename from plugins/ui/src/deephaven/ui/components/types/Intl/number_field.py
rename to plugins/ui/src/deephaven/ui/components/types/Intl/number_format.py
index cb9849cf3..cf79b980c 100644
--- a/plugins/ui/src/deephaven/ui/components/types/Intl/number_field.py
+++ b/plugins/ui/src/deephaven/ui/components/types/Intl/number_format.py
@@ -480,7 +480,7 @@ class Options(TypedDict):
"""
-class NumberFieldFormatOptions(TypedDict):
+class NumberFormatOptions(TypedDict):
"""
Options for formatting the value of a NumberField.
This also affects the characters allowed in the input.
diff --git a/plugins/ui/src/deephaven/ui/components/types/__init__.py b/plugins/ui/src/deephaven/ui/components/types/__init__.py
index 12557f66e..618b8072e 100644
--- a/plugins/ui/src/deephaven/ui/components/types/__init__.py
+++ b/plugins/ui/src/deephaven/ui/components/types/__init__.py
@@ -6,4 +6,4 @@
from .progress import *
from .validate import *
from .icon_types import *
-from .Intl.number_field import *
+from .Intl.number_format import *
diff --git a/plugins/ui/src/deephaven/ui/components/types/progress.py b/plugins/ui/src/deephaven/ui/components/types/progress.py
index 00ea22fe6..62a7cff8d 100644
--- a/plugins/ui/src/deephaven/ui/components/types/progress.py
+++ b/plugins/ui/src/deephaven/ui/components/types/progress.py
@@ -2,3 +2,6 @@
ProgressBarSize = Literal["S", "L"]
ProgressCircleSize = Literal["S", "M", "L"]
+
+MeterVariants = Literal["informative", "positive", "critical", "warning"]
+MeterSizes = Literal["S", "L"]
diff --git a/plugins/ui/src/deephaven/ui/elements/BaseElement.py b/plugins/ui/src/deephaven/ui/elements/BaseElement.py
index c6a425c0f..9cc5a0163 100644
--- a/plugins/ui/src/deephaven/ui/elements/BaseElement.py
+++ b/plugins/ui/src/deephaven/ui/elements/BaseElement.py
@@ -9,10 +9,23 @@ class BaseElement(Element):
"""
Base class for basic UI Elements that don't have any special rendering logic.
Must provide a name for the element.
+
+ Args:
+ name: The name of the element, e.g. "div", "span", "deephaven.ui.button", etc.
+ children: The children
+ key: The key for the element
+ _nullable_props: A list of props that can be nullable
+ props: The props for the element
"""
def __init__(
- self, name: str, /, *children: Any, key: str | None = None, **props: Any
+ self,
+ name: str,
+ /,
+ *children: Any,
+ key: str | None = None,
+ _nullable_props: list[str] = [],
+ **props: Any,
):
self._name = name
self._key = key
@@ -27,7 +40,7 @@ def __init__(
# If there's only one child, we pass it as a single child, not a list
# There are many React elements that expect only a single child, and will fail if they get a list (even if it only has one element)
props["children"] = children[0]
- self._props = dict_to_react_props(props)
+ self._props = dict_to_react_props(props, _nullable_props)
@property
def name(self) -> str:
diff --git a/plugins/ui/src/deephaven/ui/elements/Element.py b/plugins/ui/src/deephaven/ui/elements/Element.py
index fe6a168f9..d094ae7fc 100644
--- a/plugins/ui/src/deephaven/ui/elements/Element.py
+++ b/plugins/ui/src/deephaven/ui/elements/Element.py
@@ -1,7 +1,7 @@
from __future__ import annotations
from abc import ABC, abstractmethod
-from typing import Any, Dict
+from typing import Any, Dict, List, Union
from .._internal import RenderContext
PropsType = Dict[str, Any]
@@ -45,3 +45,7 @@ def render(self, context: RenderContext) -> PropsType:
The props of this element.
"""
pass
+
+
+# Some props don't support Undefined, so they need to add it themselves
+NodeType = Union[None, bool, int, str, Element, List["NodeType"]]
diff --git a/plugins/ui/src/deephaven/ui/elements/__init__.py b/plugins/ui/src/deephaven/ui/elements/__init__.py
index 21143a2f9..8065f588d 100644
--- a/plugins/ui/src/deephaven/ui/elements/__init__.py
+++ b/plugins/ui/src/deephaven/ui/elements/__init__.py
@@ -1,4 +1,4 @@
-from .Element import Element, PropsType
+from .Element import Element, PropsType, NodeType
from .BaseElement import BaseElement
from .DashboardElement import DashboardElement
from .FunctionElement import FunctionElement
diff --git a/plugins/ui/src/deephaven/ui/types/types.py b/plugins/ui/src/deephaven/ui/types/types.py
index 9fb60b648..314dd5527 100644
--- a/plugins/ui/src/deephaven/ui/types/types.py
+++ b/plugins/ui/src/deephaven/ui/types/types.py
@@ -573,3 +573,32 @@ class DateRange(TypedDict):
ToastVariant = Literal["positive", "negative", "neutral", "info"]
+
+
+_DISABLE_NULLISH_CONSTRUCTORS = False
+
+
+class UndefinedType:
+ """
+ Placeholder for undefined values.
+ """
+
+ def __init__(self) -> None:
+ if _DISABLE_NULLISH_CONSTRUCTORS:
+ raise NotImplementedError
+
+ def __bool__(self) -> bool:
+ return False
+
+ def __copy__(self) -> "UndefinedType":
+ return self
+
+ def __deepcopy__(self, _: Any) -> "UndefinedType":
+ return self
+
+ def __eq__(self, other: object) -> bool:
+ return isinstance(other, UndefinedType) or other is None
+
+
+Undefined = UndefinedType()
+_DISABLE_NULLISH_CONSTRUCTORS = True
diff --git a/plugins/ui/src/js/package.json b/plugins/ui/src/js/package.json
index 57c71f8b3..a51cc6cf4 100644
--- a/plugins/ui/src/js/package.json
+++ b/plugins/ui/src/js/package.json
@@ -1,6 +1,6 @@
{
"name": "@deephaven/js-plugin-ui",
- "version": "0.23.1",
+ "version": "0.24.0",
"description": "Deephaven UI plugin",
"keywords": [
"Deephaven",
diff --git a/plugins/ui/src/js/src/elements/LogicButton.tsx b/plugins/ui/src/js/src/elements/LogicButton.tsx
new file mode 100644
index 000000000..2be528af9
--- /dev/null
+++ b/plugins/ui/src/js/src/elements/LogicButton.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import {
+ LogicButton as DHCLogicButton,
+ LogicButtonProps as DHCLogicButtonProps,
+} from '@deephaven/components';
+import { useButtonProps } from './hooks/useButtonProps';
+import { SerializedButtonEventProps } from './model/SerializedPropTypes';
+
+export function LogicButton(
+ props: SerializedButtonEventProps
+): JSX.Element {
+ const buttonProps = useButtonProps(props);
+
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ return ;
+}
+
+export default LogicButton;
diff --git a/plugins/ui/src/js/src/elements/Meter.tsx b/plugins/ui/src/js/src/elements/Meter.tsx
new file mode 100644
index 000000000..c3aadd819
--- /dev/null
+++ b/plugins/ui/src/js/src/elements/Meter.tsx
@@ -0,0 +1,13 @@
+import {
+ Meter as DHCMeter,
+ MeterProps as DHCMeterProps,
+} from '@deephaven/components';
+
+export function Meter(props: DHCMeterProps): JSX.Element | null {
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ return ;
+}
+
+Meter.displayName = 'Meter';
+
+export default Meter;
diff --git a/plugins/ui/src/js/src/elements/SearchField.tsx b/plugins/ui/src/js/src/elements/SearchField.tsx
index 4f58dd796..a58b292ce 100644
--- a/plugins/ui/src/js/src/elements/SearchField.tsx
+++ b/plugins/ui/src/js/src/elements/SearchField.tsx
@@ -8,9 +8,10 @@ import {
useKeyboardEventCallback,
} from './hooks';
import useDebouncedOnChange from './hooks/useDebouncedOnChange';
+import { SerializedTextInputEventProps } from './model';
export function SearchField(
- props: DHCSearchFieldProps & {
+ props: SerializedTextInputEventProps & {
onSubmit?: (value: string) => void;
onClear?: () => void;
}
diff --git a/plugins/ui/src/js/src/elements/UITable/UITable.tsx b/plugins/ui/src/js/src/elements/UITable/UITable.tsx
index f0f29b211..bd3283858 100644
--- a/plugins/ui/src/js/src/elements/UITable/UITable.tsx
+++ b/plugins/ui/src/js/src/elements/UITable/UITable.tsx
@@ -306,7 +306,9 @@ export function UITable({
if (alwaysFetchColumnsArray[0] === false) {
return [];
}
- return alwaysFetchColumnsArray.filter(v => typeof v === 'string');
+ return alwaysFetchColumnsArray.filter(
+ v => typeof v === 'string'
+ ) as string[];
}, [alwaysFetchColumnsArray, modelColumns]);
const mouseHandlers = useMemo(
diff --git a/plugins/ui/src/js/src/elements/UITable/UITableModel.test.ts b/plugins/ui/src/js/src/elements/UITable/UITableModel.test.ts
index 0c7df094e..707086e1b 100644
--- a/plugins/ui/src/js/src/elements/UITable/UITableModel.test.ts
+++ b/plugins/ui/src/js/src/elements/UITable/UITableModel.test.ts
@@ -33,6 +33,7 @@ describe('Formatting', () => {
table: MOCK_TABLE,
databars: [],
format: [{ color: 'red' }, { color: 'blue' }],
+ displayNameMap: {},
});
expect(model.getFormatOptionForCell(0, 0, 'color')).toBe('blue');
expect(model.getFormatOptionForCell(1, 1, 'color')).toBe('blue');
@@ -48,6 +49,7 @@ describe('Formatting', () => {
{ cols: 'column0', color: 'red' },
{ cols: 'column1', color: 'blue' },
],
+ displayNameMap: {},
});
expect(model.getFormatOptionForCell(0, 0, 'color')).toBe('red');
expect(model.getFormatOptionForCell(1, 1, 'color')).toBe('blue');
@@ -71,6 +73,7 @@ describe('Formatting', () => {
{ color: 'red', if_: 'even' },
{ cols: 'column1', color: 'blue', if_: 'even' },
],
+ displayNameMap: {},
});
expect(model.getFormatOptionForCell(0, 0, 'color')).toBe('red');
expect(model.getFormatOptionForCell(0, 1, 'color')).toBeUndefined();
@@ -86,6 +89,7 @@ describe('Formatting', () => {
table: MOCK_TABLE,
databars: [],
format: [{ cols: 'column0', color: 'red' }],
+ displayNameMap: {},
});
expect(model.getFormatOptionForCell(1, 1, 'color')).toBeUndefined();
expect(
@@ -104,6 +108,7 @@ describe('Formatting', () => {
table: MOCK_TABLE,
databars: [],
format: [{ color: 'red', if_: 'even' }],
+ displayNameMap: {},
});
expect(model.getFormatOptionForCell(0, 0, 'color')).toBeUndefined();
expect(model.getFormatOptionForCell(0, 1, 'color')).toBeUndefined();
@@ -119,6 +124,7 @@ describe('Formatting', () => {
table: MOCK_TABLE,
databars: [],
format: [{ color: 'red' }],
+ displayNameMap: {},
});
expect(model.colorForCell(0, 0, {} as IrisGridThemeType)).toBe('red');
});
@@ -130,6 +136,7 @@ describe('Formatting', () => {
table: MOCK_TABLE,
databars: [],
format: [],
+ displayNameMap: {},
});
expect(model.colorForCell(0, 0, {} as IrisGridThemeType)).toBeUndefined();
expect(MOCK_BASE_MODEL.colorForCell).toHaveBeenCalledTimes(1);
@@ -145,6 +152,7 @@ describe('Formatting', () => {
table: MOCK_TABLE,
databars: [],
format: [{ background_color: 'black' }],
+ displayNameMap: {},
});
expect(
model.colorForCell(0, 0, { white: 'white' } as IrisGridThemeType)
@@ -162,6 +170,7 @@ describe('Formatting', () => {
table: MOCK_TABLE,
databars: [],
format: [{ background_color: 'white' }],
+ displayNameMap: {},
});
expect(
model.colorForCell(0, 0, { black: 'black' } as IrisGridThemeType)
@@ -176,6 +185,7 @@ describe('Formatting', () => {
table: MOCK_TABLE,
databars: [],
format: [{ color: 'foo' }],
+ displayNameMap: {},
});
model.setColorMap(new Map([['foo', 'bar']]));
expect(model.colorForCell(0, 0, {} as IrisGridThemeType)).toBe('bar');
@@ -190,6 +200,7 @@ describe('Formatting', () => {
table: MOCK_TABLE,
databars: [],
format: [],
+ displayNameMap: {},
});
expect(
model.backgroundColorForCell(0, 0, {} as IrisGridThemeType)
@@ -204,6 +215,7 @@ describe('Formatting', () => {
table: MOCK_TABLE,
databars: [],
format: [{ background_color: 'foo' }],
+ displayNameMap: {},
});
model.setColorMap(new Map([['foo', 'bar']]));
expect(model.backgroundColorForCell(0, 0, {} as IrisGridThemeType)).toBe(
diff --git a/plugins/ui/src/js/src/elements/index.ts b/plugins/ui/src/js/src/elements/index.ts
index 58c8846f7..8bb2aec39 100644
--- a/plugins/ui/src/js/src/elements/index.ts
+++ b/plugins/ui/src/js/src/elements/index.ts
@@ -19,7 +19,9 @@ export * from './IllustratedMessage';
export * from './Image';
export * from './InlineAlert';
export * from './ListView';
+export * from './LogicButton';
export * from './Markdown';
+export * from './Meter';
export * from './model';
export * from './ObjectView';
export * from './Picker';
diff --git a/plugins/ui/src/js/src/elements/model/ElementConstants.ts b/plugins/ui/src/js/src/elements/model/ElementConstants.ts
index 83d386a79..2b16d4678 100644
--- a/plugins/ui/src/js/src/elements/model/ElementConstants.ts
+++ b/plugins/ui/src/js/src/elements/model/ElementConstants.ts
@@ -54,7 +54,9 @@ export const ELEMENT_NAME = {
listActionMenu: uiComponentName('ListActionMenu'),
link: uiComponentName('Link'),
listView: uiComponentName('ListView'),
+ logicButton: uiComponentName('LogicButton'),
markdown: uiComponentName('Markdown'),
+ meter: uiComponentName('Meter'),
numberField: uiComponentName('NumberField'),
picker: uiComponentName('Picker'),
progressBar: uiComponentName('ProgressBar'),
diff --git a/plugins/ui/src/js/src/widget/WidgetUtils.tsx b/plugins/ui/src/js/src/widget/WidgetUtils.tsx
index a3fc677bf..ed42404b8 100644
--- a/plugins/ui/src/js/src/widget/WidgetUtils.tsx
+++ b/plugins/ui/src/js/src/widget/WidgetUtils.tsx
@@ -66,7 +66,9 @@ import {
Image,
InlineAlert,
ListView,
+ LogicButton,
Markdown,
+ Meter,
Picker,
ProgressBar,
ProgressCircle,
@@ -146,7 +148,9 @@ export const elementComponentMap = {
[ELEMENT_NAME.listActionGroup]: ListActionGroup,
[ELEMENT_NAME.listActionMenu]: ListActionMenu,
[ELEMENT_NAME.listView]: ListView,
+ [ELEMENT_NAME.logicButton]: LogicButton,
[ELEMENT_NAME.markdown]: Markdown,
+ [ELEMENT_NAME.meter]: Meter,
[ELEMENT_NAME.numberField]: NumberField,
[ELEMENT_NAME.picker]: Picker,
[ELEMENT_NAME.progressBar]: ProgressBar,
diff --git a/plugins/ui/test/deephaven/ui/test_types.py b/plugins/ui/test/deephaven/ui/test_types.py
new file mode 100644
index 000000000..eb6086547
--- /dev/null
+++ b/plugins/ui/test/deephaven/ui/test_types.py
@@ -0,0 +1,36 @@
+import unittest
+
+from .BaseTest import BaseTestCase
+
+
+class TypesTest(BaseTestCase):
+ def test_nullish_equivalences(self):
+ from deephaven.ui.types import Undefined
+
+ self.assertEqual(Undefined, None)
+ self.assertEqual(None, Undefined)
+
+ self.assertIsNot(Undefined, None)
+ self.assertIsNot(None, Undefined)
+
+ def test_nullish_bool(self):
+ from deephaven.ui.types import Undefined
+
+ self.assertFalse(Undefined)
+
+ def test_nullish_init(self):
+ from deephaven.ui.types import UndefinedType
+
+ with self.assertRaises(NotImplementedError):
+ UndefinedType()
+
+ def test_copy(self):
+ from copy import copy, deepcopy
+ from deephaven.ui.types import Undefined
+
+ self.assertIs(Undefined, copy(Undefined))
+ self.assertIs(Undefined, deepcopy(Undefined))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/plugins/ui/test/deephaven/ui/test_utils.py b/plugins/ui/test/deephaven/ui/test_utils.py
index 21872969e..51c6412a4 100644
--- a/plugins/ui/test/deephaven/ui/test_utils.py
+++ b/plugins/ui/test/deephaven/ui/test_utils.py
@@ -117,11 +117,50 @@ def test_dict_to_react_props(self):
def test_remove_empty_keys(self):
from deephaven.ui._internal.utils import remove_empty_keys
+ from deephaven.ui.types import Undefined
self.assertDictEqual(
remove_empty_keys({"foo": "bar", "biz": None, "baz": 0}),
{"foo": "bar", "baz": 0},
)
+ self.assertDictEqual(
+ remove_empty_keys(
+ {
+ "foo": "bar",
+ "biz": None,
+ "baz": 0,
+ "is_undefined": Undefined,
+ },
+ _nullable_props={"is_undefined"},
+ ),
+ {"foo": "bar", "baz": 0},
+ )
+ self.assertDictEqual(
+ remove_empty_keys(
+ {
+ "foo": "bar",
+ "biz": None,
+ "baz": 0,
+ "is_undefined": Undefined,
+ },
+ _nullable_props={"biz", "is_undefined"},
+ ),
+ {"foo": "bar", "biz": None, "baz": 0},
+ )
+
+ with self.assertRaises(ValueError) as err:
+ remove_empty_keys(
+ {
+ "foo": "bar",
+ "biz": None,
+ "baz": 0,
+ "is_undefined": Undefined,
+ }
+ )
+ self.assertEqual(
+ str(err.exception),
+ "UndefinedType found in a non-nullable prop.",
+ )
def test_wrap_callable(self):
from deephaven.ui._internal.utils import wrap_callable
diff --git a/sphinx_ext/deephaven_autodoc.py b/sphinx_ext/deephaven_autodoc.py
index 56e6e946b..d987a12c2 100644
--- a/sphinx_ext/deephaven_autodoc.py
+++ b/sphinx_ext/deephaven_autodoc.py
@@ -46,10 +46,13 @@ class SignatureData(TypedDict):
AUTOFUNCTION_COMMENT_PREFIX = "AutofunctionCommentPrefix:"
+# Some parameters don't need to be documented such as function parameter dividers
+UNDOCUMENTED_PARAMS = {"*", "/"}
+
def extract_parameter_defaults(
node: sphinx.addnodes.desc_parameterlist,
-) -> ParamDefaults:
+) -> tuple[ParamDefaults, set[str]]:
"""
Extract the default values for the parameters from the parameter list
@@ -57,20 +60,24 @@ def extract_parameter_defaults(
node: The node to extract the defaults from
Returns:
- The parameter defaults
+ The parameter defaults and the expected parameters for comparison
"""
defaults = {}
+ expected_params = set()
for child in node.children:
params = child.astext().split("=")
if len(params) == 2:
defaults[params[0]] = params[1]
- # otherwise, no default value - do not add it because None is not the same as no default
- return defaults
+ # otherwise, no default value - do not add it to defaults because None is not the same as no default
+ # but add it to expected_params so that we can check that all parameters are documented
+ expected_params.add(params[0])
+
+ return defaults, expected_params
def extract_signature_data(
node: sphinx.addnodes.desc_signature,
-) -> tuple[FunctionMetadata, ParamDefaults]:
+) -> tuple[FunctionMetadata, ParamDefaults, set[str]]:
"""
Extract the signature data from the signature node
The default values for the parameters are extracted from the signature
@@ -79,18 +86,19 @@ def extract_signature_data(
node: The node to extract the signature data from
Returns:
- The function metadata and the parameter defaults
+ The function metadata, the parameter defaults, and the expected parameters
"""
result = {}
param_defaults = {}
+ expected_params = set()
for child in node.children:
if isinstance(child, sphinx.addnodes.desc_addname):
result["module_name"] = child.astext()
elif isinstance(child, sphinx.addnodes.desc_name):
result["name"] = child.astext()
elif isinstance(child, sphinx.addnodes.desc_parameterlist):
- param_defaults = extract_parameter_defaults(child)
- return FunctionMetadata(**result), param_defaults
+ param_defaults, expected_params = extract_parameter_defaults(child)
+ return FunctionMetadata(**result), param_defaults, expected_params
def extract_list_item(node: docutils.nodes.list_item) -> ParamData:
@@ -108,7 +116,7 @@ def extract_list_item(node: docutils.nodes.list_item) -> ParamData:
match = re.match(r"(.+?) \((.*?)\) -- (.+)", field, re.DOTALL)
if match is None:
raise ValueError(
- f"Could not match {field} to extract param data. "
+ f"Could not match '{field}' to extract param data. "
f"Verify this parameter is documented correctly within 'Args:' with type and description."
)
matched = match.groups()
@@ -245,6 +253,18 @@ def attach_parameter_defaults(params: Params, param_defaults: ParamDefaults) ->
param["default"] = param_defaults[name]
+def missing_parameters(params: Params, expected_params: set[str]) -> set[str]:
+ """
+ Get the parameters that are missing from the documentation
+
+ Args:
+ params: The parameters that are documented
+ expected_params: The parameters that are expected
+ """
+ param_names = set(param["name"] for param in params)
+ return expected_params - UNDOCUMENTED_PARAMS - param_names
+
+
def extract_desc_data(node: sphinx.addnodes.desc) -> SignatureData:
"""
Extract the content of the description.
@@ -258,21 +278,27 @@ def extract_desc_data(node: sphinx.addnodes.desc) -> SignatureData:
"""
result = {}
param_defaults = {}
+ params_expected = set()
for child_node in node.children:
if isinstance(child_node, sphinx.addnodes.desc_signature):
- signature_results, param_defaults = extract_signature_data(child_node)
+ signature_results, param_defaults, params_expected = extract_signature_data(
+ child_node
+ )
result.update(signature_results)
elif isinstance(child_node, sphinx.addnodes.desc_content):
result.update(extract_content_data(child_node))
# map all to lowercase for consistency
function = f"{result['module_name']}{result['name']}"
- try:
- result["parameters"] = result.pop("Parameters")
- except KeyError:
+
+ result["parameters"] = result.pop("Parameters") if "Parameters" in result else []
+ missing_params = missing_parameters(result["parameters"], params_expected)
+
+ if missing_params:
raise ValueError(
- "Parameters missing from description. "
- f"Verify the function description for {function} is formatted correctly."
+ f"Missing documentation for {function} parameters {missing_params}. "
+ "Verify that parameter names have leading asterisks in the description."
)
+
try:
result["return_description"] = result.pop("Returns")
except KeyError:
diff --git a/tests/app.d/ui_flex.py b/tests/app.d/ui_flex.py
index 4c0292aff..a2b4f6ef2 100644
--- a/tests/app.d/ui_flex.py
+++ b/tests/app.d/ui_flex.py
@@ -3,7 +3,9 @@
from deephaven import empty_table
_t_flex = empty_table(100).update(["x = i", "y = sin(i)"])
-_p_flex = dx.line(_t_flex, x="x", y="y")
+# By default, dx.line renders with webgl but some tests use the trace class to see if the chart is rendered,
+# which is not there in webgl.
+_p_flex = dx.line(_t_flex, x="x", y="y", render_mode="svg")
@ui.component
diff --git a/tests/ui.spec.ts-snapshots/UI-all-components-render-1-1-chromium-linux.png b/tests/ui.spec.ts-snapshots/UI-all-components-render-1-1-chromium-linux.png
index 01eeab591..7150458c7 100644
Binary files a/tests/ui.spec.ts-snapshots/UI-all-components-render-1-1-chromium-linux.png and b/tests/ui.spec.ts-snapshots/UI-all-components-render-1-1-chromium-linux.png differ
diff --git a/tests/ui.spec.ts-snapshots/UI-all-components-render-1-1-firefox-linux.png b/tests/ui.spec.ts-snapshots/UI-all-components-render-1-1-firefox-linux.png
index 2b2ca3238..ed838b361 100644
Binary files a/tests/ui.spec.ts-snapshots/UI-all-components-render-1-1-firefox-linux.png and b/tests/ui.spec.ts-snapshots/UI-all-components-render-1-1-firefox-linux.png differ
diff --git a/tests/ui.spec.ts-snapshots/UI-all-components-render-1-1-webkit-linux.png b/tests/ui.spec.ts-snapshots/UI-all-components-render-1-1-webkit-linux.png
index 50e60dbb2..ba00b3cd4 100644
Binary files a/tests/ui.spec.ts-snapshots/UI-all-components-render-1-1-webkit-linux.png and b/tests/ui.spec.ts-snapshots/UI-all-components-render-1-1-webkit-linux.png differ
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 92492a7d7..5e821a5bd 100644
Binary files a/tests/ui.spec.ts-snapshots/UI-all-components-render-2-1-chromium-linux.png and b/tests/ui.spec.ts-snapshots/UI-all-components-render-2-1-chromium-linux.png differ
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 55e199088..083699b0a 100644
Binary files a/tests/ui.spec.ts-snapshots/UI-all-components-render-2-1-firefox-linux.png and b/tests/ui.spec.ts-snapshots/UI-all-components-render-2-1-firefox-linux.png differ
diff --git a/tests/ui.spec.ts-snapshots/UI-all-components-render-2-1-webkit-linux.png b/tests/ui.spec.ts-snapshots/UI-all-components-render-2-1-webkit-linux.png
index 14da881b1..a46551b34 100644
Binary files a/tests/ui.spec.ts-snapshots/UI-all-components-render-2-1-webkit-linux.png and b/tests/ui.spec.ts-snapshots/UI-all-components-render-2-1-webkit-linux.png differ
diff --git a/tools/check_typescript_ci.py b/tools/check_typescript_ci.py
new file mode 100644
index 000000000..6f07f38dd
--- /dev/null
+++ b/tools/check_typescript_ci.py
@@ -0,0 +1,50 @@
+from re import fullmatch
+from subprocess import run
+from sys import exit
+
+
+def main():
+ print("Checking TypeScript types...")
+ res = run(
+ ["npx", "tsc", "-p", ".", "--emitDeclarationOnly", "false", "--noEmit"],
+ capture_output=True,
+ )
+
+ if res.returncode == 0:
+ return 0
+
+ messages = []
+ for line in res.stdout.decode("utf-8").splitlines():
+ if len(line) == 0:
+ continue
+ # If there's an indent, that means it's a continuation of the previous line
+ # For example, the error message could be like:
+ # > Argument of type 'FUNCTION_1_TYPE | undefined' is not assignable to parameter of type 'FUNCTION_2_TYPE'.
+ # > Type 'FUNCTION_1_TYPE' is not assignable to type 'FUNCTION_2_TYPE'.
+ # > Types of parameters `PARAM_1` and `PARAM_2` are incompatible.
+ # > Type 'PARAM_1_TYPE' is not assignable to type 'PARAM_2_TYPE'.
+ if line[0] == " " and len(messages) > 0:
+ messages[-1] += "\n" + line
+ else:
+ messages.append(line)
+
+ for message in messages:
+ # Check if the message is actually an error and extract the details
+ # Error message format: file(line,col): error_message
+ match = fullmatch(r"(.+?)\((\d+),(\d+)\): ([\s\S]+)", message)
+ if match is None:
+ continue
+
+ file, line, col, error_message = match.groups()
+ # Newlines in GitHub Actions annotations are escaped as %0A
+ # https://github.com/actions/toolkit/issues/193#issuecomment-605394935
+ error_message = error_message.replace("\n", "%0A")
+ # GitHub Actions annotation format
+ print(f"::error file={file},line={line},col={col}::{error_message}")
+
+ return res.returncode
+
+
+if __name__ == "__main__":
+ # Exit with returncode so GitHub Actions fails properly
+ exit(main())