Bump dashboards: bar, icon, forecast and clock widget types
Plumbing only: the widget-type literal and its dtype table on both sides, the regenerated client, a curated lucide map and four stubs the renderers are wired to. Also a latching switch and a segmented dropdown, both a `style` on the control that already publishes and reads back, plus the option editor a dropdown never had. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HTsT1isxUjw5gtkJk8WhuA
This commit is contained in:
@@ -42,6 +42,10 @@ WidgetType = Literal[
|
||||
"markdown",
|
||||
"agenda",
|
||||
"notification",
|
||||
"bar",
|
||||
"icon",
|
||||
"forecast",
|
||||
"clock",
|
||||
# Input
|
||||
"button",
|
||||
"switch",
|
||||
@@ -66,6 +70,10 @@ WIDGET_DTYPES: dict[str, set[str]] = {
|
||||
"switch": {"bool"},
|
||||
"agenda": {"list"},
|
||||
"notification": {"record"},
|
||||
"bar": {"float", "int"},
|
||||
"forecast": {"list"},
|
||||
# An icon maps weather strings, bool hints and numbers alike, and a clock
|
||||
# binds nothing at all, so neither has a row to be held to.
|
||||
}
|
||||
|
||||
|
||||
@@ -124,7 +132,8 @@ class WidgetDef(BaseModel):
|
||||
if series.get("message")
|
||||
]
|
||||
name = self.config.get("message")
|
||||
return [str(name)] if name else []
|
||||
inner = self.config.get("inner") # only a bar nests a second reading
|
||||
return [str(value) for value in (name, inner) if value]
|
||||
|
||||
@property
|
||||
def target(self) -> str:
|
||||
@@ -161,7 +170,7 @@ class WidgetDef(BaseModel):
|
||||
str(series.get("dtype") or "")
|
||||
for series in self.config.get("series") or []
|
||||
]
|
||||
return [str(self.config.get("dtype") or "")]
|
||||
return [str(self.config.get(key) or "") for key in ("dtype", "inner_dtype")]
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_binding(self) -> WidgetDef:
|
||||
|
||||
@@ -213,6 +213,7 @@ def test_a_widget_refuses_a_dtype_it_cannot_carry():
|
||||
def test_the_structured_widgets_bind_their_shapes():
|
||||
WidgetDef(id="a", type="agenda", config={"message": "a.b", "dtype": "list"})
|
||||
WidgetDef(id="n", type="notification", config={"message": "a.b", "dtype": "record"})
|
||||
WidgetDef(id="f", type="forecast", config={"message": "a.b", "dtype": "list"})
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
WidgetDef(id="a", type="agenda", config={"message": "a.b", "dtype": "json"})
|
||||
@@ -220,6 +221,41 @@ def test_the_structured_widgets_bind_their_shapes():
|
||||
WidgetDef(
|
||||
id="n", type="notification", config={"message": "a.b", "dtype": "list"}
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
WidgetDef(id="f", type="forecast", config={"message": "a.b", "dtype": "json"})
|
||||
|
||||
|
||||
def test_a_bar_nests_a_second_number():
|
||||
WidgetDef(
|
||||
id="b",
|
||||
type="bar",
|
||||
config={
|
||||
"message": "a.in",
|
||||
"dtype": "float",
|
||||
"inner": "a.pv",
|
||||
"inner_dtype": "int",
|
||||
},
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
WidgetDef(
|
||||
id="b",
|
||||
type="bar",
|
||||
config={"message": "a.in", "dtype": "float", "inner_dtype": "bool"},
|
||||
)
|
||||
|
||||
|
||||
def test_a_bar_is_drawn_on_both_readings_it_nests():
|
||||
widget = WidgetDef(id="b", type="bar", config={"message": "a.in", "inner": "a.pv"})
|
||||
|
||||
assert widget.messages == ["a.in", "a.pv"]
|
||||
|
||||
|
||||
def test_a_clock_reads_nothing_and_publishes_nothing():
|
||||
widget = WidgetDef(id="c", type="clock", config={"format": "24h"})
|
||||
|
||||
assert widget.messages == []
|
||||
assert widget.target == ""
|
||||
|
||||
|
||||
def test_a_querying_chart_asks_with_a_record_and_draws_a_series():
|
||||
|
||||
Reference in New Issue
Block a user