@meshmakers/octo-meshboard 3.5.500 → 3.5.520

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -142,6 +142,80 @@ With `plain` the host is responsible for the gutter: a route that loads the view
142
142
  an unpadded outlet should wrap it in a padded page instead. Exported helpers: `MeshBoardChrome`,
143
143
  `MESHBOARD_CHROME_ROUTE_DATA`, `resolveMeshBoardChrome`.
144
144
 
145
+ ### Period set by the host (`timeRange` / `timeFilterControl`, AB#7582)
146
+
147
+ A host page that has its own period control (one "from / to" for the whole page) passes the period
148
+ to the embedded board as **two exact instants** and switches the board's own time-range picker off,
149
+ so page and dashboard always show the same days, whatever time zone the browser is in.
150
+
151
+ | Input | Type | Effect |
152
+ |-------|------|--------|
153
+ | `timeRange` | `{ from: Date \| string; to: Date \| string } \| undefined` | Half-open period `[from, to)`. `Date` objects are taken as they are. Strings must be **RFC 3339 with an offset**: `YYYY-MM-DDTHH:MM:SS`, optional `.s`–`.sss`, then `Z` or `±HH:MM` (e.g. `2026-03-28T05:00:00Z`, `2026-03-28T06:00:00+01:00`). Rejected as invalid: no offset (the browser would read it in its own zone), no seconds, compact offsets (`+0100`), lowercase `t`/`z`, a space instead of `T`, more than 3 fraction digits (these forms parse differently across browsers, e.g. Safari), and dates or times that do not exist (`2026-02-30`, `24:00`, `23:60`, leap second `:60`). The instant is computed from the fields, not by `Date.parse`. |
154
+ | `timeFilterControl` | `'board' \| 'host'` (default `'board'`) | `'host'` hides the board's time-range picker and its reset button. Entity selectors and the toolbar actions stay, in `headerMode` `full` and `compact`. |
155
+
156
+ Rules while `timeRange` is set (and the board's time filter is enabled):
157
+
158
+ - `$timeRangeFrom` / `$timeRangeTo` and the stream-data window of every stream-data widget are
159
+ exactly the two instants — no snapping to 00:00, independent of `pickerConfig.showTime` and of
160
+ the board `timeZoneMode`.
161
+ - **Day buckets and labels still follow the board's `timeZoneMode`:** the date labels of the
162
+ widgets and the calendar rollup a line chart picks (`resolveStreamDataTimeZone()`) use the board's
163
+ zone, not the host's. A board in `'local'` mode opened in a New York browser cuts daily buckets
164
+ at New York midnight even though the window is exactly the host's instants. An embedded board
165
+ should therefore set `timeZoneMode` to the host's business zone (e.g. `Europe/Vienna`). Calendar
166
+ rollups are cut at 00:00 of that zone; they cannot represent a day that starts at 06:00 (gas
167
+ day) — a widget that must show gas days needs a query that does not use a daily calendar rollup.
168
+ - **The host wins:** `tf_*` query parameters and the stored/default selection are ignored, the view
169
+ writes no `tf_*` parameters, the stored selection is not touched and the board shows no unsaved
170
+ changes. The picker is hidden and its events are ignored, whatever the `timeFilterControl`
171
+ value. `tf_*` parameters that were in the URL when the view opened stay in the URL but are never
172
+ applied again once the host has owned the period (also not after `timeRange` goes back to
173
+ `undefined`).
174
+ - A new period reloads the widgets once (an equal period — same instants — reloads nothing); the
175
+ board definition is not reloaded. Setting `timeRange` back to `undefined` returns the board to
176
+ its own selection (and, with `'board'`, shows the picker again).
177
+ - An invalid period (unparseable value, or `from >= to`) sends **no stream-data query that is
178
+ bound to the time filter**: the widgets show their normal empty state (`resolveStreamDataScope`
179
+ returns `{ kind: 'empty', reason: 'invalidTimeRange' }`; the process widget's element bindings
180
+ get no value, a widget group over a stream-data query shows no items), `$timeRangeFrom` /
181
+ `$timeRangeTo` are removed, and the view logs one console error naming the input. A widget with
182
+ **`ignoreTimeFilter: true`** does not depend on the period and keeps querying (with its selector
183
+ scope). Custom widgets pass their `ignoreTimeFilter` as the third argument of
184
+ `resolveStreamDataScope`, or ask `isStreamDataBlockedByHostTimeRange(ignoreTimeFilter)`.
185
+ **Limitation:** widgets backed by a *runtime* query or aggregation whose filter uses
186
+ `$timeRangeFrom` / `$timeRangeTo` are not gated; they run with the placeholder unresolved. An
187
+ invalid range is a host bug — fix it there.
188
+ - A board whose time filter is disabled ignores `timeRange` and logs nothing.
189
+ - With `timeRange` undefined and `timeFilterControl='board'` the view behaves exactly as before.
190
+ - With `timeFilterControl='host'` and `timeRange` undefined (e.g. while the host page is still
191
+ computing its period) the board keeps working on its own selection, with the picker hidden; it
192
+ writes no `tf_*` parameters, because the URL belongs to the host.
193
+
194
+ Day rules belong to the host: the library has no "day start" offset. A host whose day starts at
195
+ 06:00 (e.g. a gas day) computes the instants itself, in its business time zone:
196
+
197
+ ```typescript
198
+ import { MeshBoardHostTimeRange } from '@meshmakers/octo-meshboard';
199
+
200
+ /** Gas day `day` (yyyy-mm-dd) = 06:00 Vienna to 06:00 Vienna the next day (23/24/25 h on DST days). */
201
+ function gasDay(day: string): MeshBoardHostTimeRange {
202
+ return { from: viennaInstant(day, 6), to: viennaInstant(addDays(day, 1), 6) };
203
+ }
204
+ // viennaInstant(day, hour) returns the UTC instant of that Vienna wall-clock time, e.g.
205
+ // viennaInstant('2026-03-28', 6) = '2026-03-28T05:00:00Z', viennaInstant('2026-03-29', 6) = '2026-03-29T04:00:00Z'.
206
+ ```
207
+
208
+ ```html
209
+ <mm-meshboard-view headerMode="compact" timeFilterControl="host" [timeRange]="period()"></mm-meshboard-view>
210
+ ```
211
+
212
+ The period is held transiently by `MeshBoardStateService` (`hostTimeRange`, `setHostTimeRange(state,
213
+ owner)`, `clearHostTimeRange(owner)`) and never persisted. The service is root-provided, so the view
214
+ registers itself as the owner: it clears the period when it is destroyed or drops its `timeRange`,
215
+ but never a period another view set in the meantime, and a view destroyed while its board is still
216
+ loading publishes nothing. Exported helpers: `MeshBoardHostTimeRange`, `MeshBoardTimeFilterControl`,
217
+ `parseHostTimeRange`, `HostTimeRangeState`.
218
+
145
219
  ### URL Sync (`meshBoardSyncUrl`)
146
220
 
147
221
  After the initial load and after every post-init board switch (e.g. via the
@@ -316,6 +390,95 @@ is logged once and ignored. Tables offer string cells of text columns (persisten
316
390
  only when their value type is `ENUM`); pie / donut charts offer every category — the resolved text
317
391
  is the slice name in data labels, legend and tooltips, the status colour still follows the raw
318
392
  value. A resolver that reads a signal re-labels the charts when it changes.
393
+ **Numeric enum cells (AB#7655):** a cell that holds a number in a persistent-query column whose
394
+ value type is an enum (`valueType` matches `ENUM`) is offered too, with `value` = the number as
395
+ string (`2` → `'2'`) and `valueType`; numbers of other columns are not. A column's `valueLabels`
396
+ (below) wins over the resolver — such a column never calls it.
397
+
398
+ **Column titles per language and value labels (AB#7655).** Board authors configure both in the
399
+ board JSON/YAML (the table config dialog does not edit them):
400
+
401
+ ```typescript
402
+ interface TableColumn {
403
+ field: string;
404
+ title: string; // fallback header
405
+ titles?: Record<string, string>; // header per culture code
406
+ valueLabels?: Record<string, TableColumnValueLabel>; // raw value (as string) -> label
407
+ width?: number; dataType?: string; format?: string; statusMapping?: ...;
408
+ }
409
+ interface TableColumnValueLabel {
410
+ label: string | Record<string, string>; // one text, or a text per culture code
411
+ color?: string; // optional CSS text colour
412
+ }
413
+ ```
414
+
415
+ - **Host language.** The library does not know the host's UI language and never derives it from
416
+ `LOCALE_ID` (static in many apps). A host that wants per-language texts hands it over once — a
417
+ culture code or a `Signal<string | null>`; the widgets follow a signal without reloading the board
418
+ or refetching data:
419
+
420
+ ```typescript
421
+ providers: [
422
+ provideMeshBoard({ language: languageService.currentCulture }), // Signal<string>, e.g. 'de-AT'
423
+ // or, when the widgets are registered lazily (no provideMeshBoard()):
424
+ provideMeshBoardLanguage(languageService.currentCulture),
425
+ // or directly: { provide: MESHBOARD_LANGUAGE, useValue: 'en-GB' }
426
+ ]
427
+ ```
428
+
429
+ No language = every column shows `title` and per-language labels show the raw value, exactly as
430
+ before. `pickLocalizedText(texts, language)` is exported for hosts that need the same lookup.
431
+ - **`titles`** — lookup order: the exact culture or language (`de-AT`, `de`; case-insensitive);
432
+ for a culture (`de-AT`) without an exact key, a key equal to its language part (`de`); for a bare
433
+ language (`de`, e.g. from a simple language switcher) without an exact key, the **first authored
434
+ key of that language** (`de-AT` in `{ "en-GB": …, "de-AT": …, "de-DE": … }`, in the order the keys
435
+ are written); then `title`. A culture never picks a sibling culture (`de-DE` does not take `de-AT`).
436
+ Columns derived from a persistent query (no explicit `columns`) keep their generated titles.
437
+ - **`valueLabels`** — keys are the raw value as string; a number `2` and a string `"2"` both match
438
+ `"2"`. The cell shows the label as **plain text** like every other cell (no badge); `color`, when
439
+ set, is the text colour — use a theme token such as `var(--kendo-color-error)`; without it the
440
+ theme text colour applies. A per-language `label` follows the host language like `titles`. A value
441
+ without a label (unmapped, e.g. `4`, or a per-language label without a match) keeps the column's
442
+ rendering: `numeric` with `format` like the `number` pipe, `iso8601` with `format` like the `date`
443
+ pipe, plain text through the board's date-time formatter; `null` / missing is an empty cell. When
444
+ no label applies for the current language at all (e.g. only `de-AT` labels and the host language is
445
+ `en-GB`), the column renders exactly as without `valueLabels`, including `MESHBOARD_LABEL_RESOLVER`.
446
+ Otherwise `valueLabels` win over `MESHBOARD_LABEL_RESOLVER`. **`valueLabels` together with
447
+ `statusMapping` (status icons) is not supported:** the labels are shown, the status icons are not,
448
+ and the widget logs a console warning.
449
+ - **Configuration errors** — a culture key that is not a culture code (`german`, `de_AT`), a
450
+ malformed `titles` entry (no object, non-string or empty text) or a malformed `valueLabels` entry
451
+ (no object, no usable `label`, non-string `color`) is ignored; the widget logs **one** console
452
+ warning per widget, column and set of problems naming them (a different problem after a config
453
+ edit is reported again), and the table still renders.
454
+ - **Refinery Studio** — the table config dialog keeps hand-authored columns: saving a
455
+ persistent-query table without changing the query keeps its explicit `columns` that the query still
456
+ returns (unchanged, in their order), drops those whose field the query no longer returns and adds the
457
+ query's other columns with a generated title, like derived columns (when the query's columns cannot
458
+ be loaded, the explicit columns stay unchanged; a new query starts without explicit columns); for
459
+ runtime-entity tables, columns that stay selected on the same CK type keep all their properties
460
+ (`titles`, `valueLabels`, `width`, `format`, `statusMapping`, ...), newly selected ones get a
461
+ generated title. A new `config` on the same widget instance re-renders headers and labels.
462
+
463
+ Example — the daily quality 1/2/3 shown as L1/L2/L3 with German and English headers:
464
+
465
+ ```json
466
+ {
467
+ "columns": [
468
+ { "field": "day", "title": "Day", "titles": { "de-AT": "Tag", "en-GB": "Day" } },
469
+ {
470
+ "field": "dataquality_max",
471
+ "title": "Dataquality",
472
+ "titles": { "de-AT": "Datenqualität", "en-GB": "Data quality" },
473
+ "valueLabels": {
474
+ "1": { "label": "L1", "color": "var(--kendo-color-success)" },
475
+ "2": { "label": "L2", "color": "var(--kendo-color-warning)" },
476
+ "3": { "label": "L3", "color": "var(--kendo-color-error)" }
477
+ }
478
+ }
479
+ ]
480
+ }
481
+ ```
319
482
 
320
483
  ### Pie Chart Widget
321
484
  Displays data as pie or donut chart.