Widget panel: settings for the bar, icon, forecast and chart axis title

The four widget types added this round had renderers but no way to configure
them. The panel now offers a bar's nested reading, unit and range (no step —
that is a slider's), the forecast's item count on the agenda's field, a chart's
y axis title beside its y range, and the icon's mapping editor: a value, a
glyph and a colour per row, first match wins, with a fallback glyph below.

Adds tests/widgets.spec.ts, which asserts each of them on /view: the nested bar
inside its outer fill, the glyph following the message, five forecast columns
fading outwards, a clock that reads the wall without being flagged unbound,
a segmented control and a latching button reading back what they published,
and an unbound tile that says so instead of taking the page down. The axis
title is drawn into uPlot's canvas, so it is checked by panel round-trip.
mobile.spec.ts grows a bar and a forecast so the width check covers them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HTsT1isxUjw5gtkJk8WhuA
This commit is contained in:
2026-08-20 09:48:42 +02:00
co-authored by Claude Opus 5
parent 2aa911c07a
commit 207b4b6ce3
4 changed files with 549 additions and 25 deletions
+75
View File
@@ -12,6 +12,9 @@ import { api, apiPage, deleteAll } from "./utils/api"
const flowName = `test_mobile_${Date.now().toString(36)}`
const dashboardName = `${flowName}_panel`
/** What the bar and the forecast read. A flow of its own, so the graph the
* editor lays out above stays the shape those assertions were written for. */
const feedName = `${flowName}_feed`
test.describe.configure({ mode: "serial" })
@@ -74,6 +77,54 @@ test.beforeAll(async ({ browser }) => {
data: { version: saved.definition.version },
})
await api(page, `/flows/${feedName}`, {
method: "PUT",
data: {
name: feedName,
title: "Feed",
version: 1,
nodes: [
{
id: "emit",
type: "python",
provides: [
{ name: "level", dtype: "float" },
{ name: "pv", dtype: "float" },
{ name: "days", dtype: "list", item: "record" },
],
},
],
},
})
const feed = await (await api(page, `/flows/${feedName}?draft=true`)).json()
await api(page, `/flows/${feedName}/publish`, {
method: "POST",
data: { version: feed.definition.version },
})
// A bar needs a reading to draw and a forecast needs its days, or neither
// widget is on the page the width check is run against.
await api(page, `/messages/${feedName}.level`, {
method: "POST",
data: { value: 62 },
})
await api(page, `/messages/${feedName}.pv`, {
method: "POST",
data: { value: 24 },
})
await api(page, `/messages/${feedName}.days`, {
method: "POST",
data: {
value: [
{ label: "Mon", icon: "sun", value: "21°" },
{ label: "Tue", icon: "cloudy", value: "18°" },
{ label: "Wed", icon: "cloud-rain", value: "15°" },
{ label: "Thu", icon: "wind", value: "16°" },
{ label: "Fri", icon: "snowflake", value: "2°" },
],
},
})
await api(page, `/dashboards/${dashboardName}`, { method: "POST" })
const dashboard = await (
await api(page, `/dashboards/${dashboardName}`)
@@ -93,6 +144,29 @@ test.beforeAll(async ({ browser }) => {
layout: { lg: { x: 3, y: 0, w: 3, h: 2 } },
config: { message: `${flowName}.scaled` },
},
{
id: "level",
type: "bar",
title: "Level",
layout: { lg: { x: 0, y: 2, w: 4, h: 2 } },
config: {
message: `${feedName}.level`,
dtype: "float",
inner: `${feedName}.pv`,
inner_dtype: "float",
min: 0,
max: 100,
unit: " kW",
},
},
{
// A strip of columns is the widget most likely to widen the page.
id: "week",
type: "forecast",
title: "Week",
layout: { lg: { x: 0, y: 4, w: 6, h: 2 } },
config: { message: `${feedName}.days`, dtype: "list", count: 5 },
},
]
const draft = await (
await api(page, `/dashboards/${dashboardName}`, {
@@ -111,6 +185,7 @@ test.afterAll(async ({ browser }) => {
await deleteAll(browser, [
`/dashboards/${dashboardName}`,
`/flows/${flowName}`,
`/flows/${feedName}`,
])
})