@devicechain/widgets 0.22.0 → 0.23.0
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 +15 -0
- package/dist/access-strings.d.ts +6 -0
- package/dist/chunks/{aggregate-feed-6ZYKOQOB.js → aggregate-feed-BD3CHNFU.js} +5 -4
- package/dist/chunks/aggregate-feed-BD3CHNFU.js.map +7 -0
- package/dist/chunks/{aggregate-widgets-WVXAW3UL.js → aggregate-widgets-HZQI5HTX.js} +3 -2
- package/dist/chunks/{aggregate-widgets-WVXAW3UL.js.map → aggregate-widgets-HZQI5HTX.js.map} +1 -1
- package/dist/chunks/alarm-widgets-7UY6MIYZ.js +9 -0
- package/dist/chunks/alarm-widgets-7UY6MIYZ.js.map +7 -0
- package/dist/chunks/chunk-6OGI4UFJ.js +367 -0
- package/dist/chunks/chunk-6OGI4UFJ.js.map +7 -0
- package/dist/chunks/chunk-BRH66T5F.js +15 -0
- package/dist/chunks/chunk-BRH66T5F.js.map +7 -0
- package/dist/chunks/chunk-FM7OI5TQ.js +87 -0
- package/dist/chunks/chunk-FM7OI5TQ.js.map +7 -0
- package/dist/chunks/chunk-USRYKW2J.js +55 -0
- package/dist/chunks/chunk-USRYKW2J.js.map +7 -0
- package/dist/chunks/{chunk-S2OMBJLI.js → chunk-WBKHYZU2.js} +7 -3
- package/dist/chunks/{chunk-S2OMBJLI.js.map → chunk-WBKHYZU2.js.map} +2 -2
- package/dist/chunks/chunk-XSLFHCNI.js +419 -0
- package/dist/chunks/chunk-XSLFHCNI.js.map +7 -0
- package/dist/chunks/{control-widgets-UDS4ET3T.js → control-widgets-3V2LSSRF.js} +4 -2
- package/dist/chunks/{control-widgets-UDS4ET3T.js.map → control-widgets-3V2LSSRF.js.map} +1 -1
- package/dist/chunks/entity-selector-3JJFI4DT.js +7 -0
- package/dist/chunks/entity-selector-3JJFI4DT.js.map +7 -0
- package/dist/chunks/{fleet-feed-FK4ZMKIK.js → fleet-feed-5QGDLOV5.js} +27 -7
- package/dist/chunks/fleet-feed-5QGDLOV5.js.map +7 -0
- package/dist/chunks/table-6EVV7QZH.js +7 -0
- package/dist/chunks/table-6EVV7QZH.js.map +7 -0
- package/dist/connected-widget.d.ts +2 -0
- package/dist/dashboard-renderer.d.ts +1 -0
- package/dist/fleet-feed.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +478 -1142
- package/dist/index.js.map +4 -4
- package/dist/preload.d.ts +2 -0
- package/dist/widgets/alarm-count.d.ts +0 -46
- package/dist/widgets/alarm-lazy.d.ts +123 -0
- package/dist/widgets/alarm-table.d.ts +0 -76
- package/dist/widgets/alarm-widgets.d.ts +2 -0
- package/dist/widgets/command-button.d.ts +0 -38
- package/dist/widgets/control-lazy.d.ts +38 -0
- package/dist/widgets/control-widgets.d.ts +1 -0
- package/dist/widgets/entity-selector.d.ts +0 -15
- package/dist/widgets/gauge.d.ts +4 -0
- package/dist/widgets/lazy-chunk.d.ts +2 -1
- package/dist/widgets/selection-lazy.d.ts +16 -0
- package/dist/widgets/table-lazy.d.ts +39 -0
- package/dist/widgets/table.d.ts +0 -38
- package/dist/widgets/time-series-chart.d.ts +4 -0
- package/package.json +10 -7
- package/dist/chunks/aggregate-feed-6ZYKOQOB.js.map +0 -7
- package/dist/chunks/chunk-YPFB6DAL.js +0 -157
- package/dist/chunks/chunk-YPFB6DAL.js.map +0 -7
- package/dist/chunks/fleet-feed-FK4ZMKIK.js.map +0 -7
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
// src/widgets/command-status.ts
|
|
2
|
-
import { isTerminalCommandStatus } from "@devicechain/dashboards";
|
|
3
|
-
var STATUS_COLORS = {
|
|
4
|
-
SUCCESSFUL: "#16a34a",
|
|
5
|
-
// green-600 — acknowledged by the device
|
|
6
|
-
FAILED: "#dc2626",
|
|
7
|
-
// red-600
|
|
8
|
-
TIMEOUT: "#dc2626",
|
|
9
|
-
// red-600 — never acknowledged in time
|
|
10
|
-
EXPIRED: "#64748b",
|
|
11
|
-
// slate-500 — lapsed before it was ever sent
|
|
12
|
-
CANCELLED: "#7c3aed",
|
|
13
|
-
// violet-600 — deliberately called off; a decision, not a failure
|
|
14
|
-
QUEUED: "#0284c7",
|
|
15
|
-
// sky-600 — in flight
|
|
16
|
-
SENT: "#0284c7",
|
|
17
|
-
HELD: "#d97706",
|
|
18
|
-
// amber-600 — withheld: the device is absent, the command still stands
|
|
19
|
-
PARKED: "#f59e0b"
|
|
20
|
-
// amber-500 — published, nobody home; held for the device's return
|
|
21
|
-
};
|
|
22
|
-
var UNKNOWN_COLOR = "#64748b";
|
|
23
|
-
function commandStatusColor(status) {
|
|
24
|
-
return STATUS_COLORS[status] ?? UNKNOWN_COLOR;
|
|
25
|
-
}
|
|
26
|
-
function isTerminalStatus(status) {
|
|
27
|
-
return isTerminalCommandStatus(status);
|
|
28
|
-
}
|
|
29
|
-
function commandStatusLabel(status) {
|
|
30
|
-
if (!status) return "";
|
|
31
|
-
return status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// src/widgets/command-params.ts
|
|
35
|
-
var DATA_TYPES = /* @__PURE__ */ new Set(["DOUBLE", "INT", "BOOLEAN", "STRING"]);
|
|
36
|
-
function parseParameterSchema(schema) {
|
|
37
|
-
if (!schema) return [];
|
|
38
|
-
let parsed;
|
|
39
|
-
try {
|
|
40
|
-
parsed = JSON.parse(schema);
|
|
41
|
-
} catch {
|
|
42
|
-
return [];
|
|
43
|
-
}
|
|
44
|
-
if (!Array.isArray(parsed)) return [];
|
|
45
|
-
const out = [];
|
|
46
|
-
for (const raw of parsed) {
|
|
47
|
-
const norm = normalizeParam(raw);
|
|
48
|
-
if (norm) out.push(norm);
|
|
49
|
-
}
|
|
50
|
-
return out;
|
|
51
|
-
}
|
|
52
|
-
function normalizeParam(raw) {
|
|
53
|
-
if (typeof raw !== "object" || raw === null) return null;
|
|
54
|
-
const r = raw;
|
|
55
|
-
if (typeof r.name !== "string") return null;
|
|
56
|
-
const param = { name: r.name };
|
|
57
|
-
if (typeof r.description === "string") param.description = r.description;
|
|
58
|
-
if (r.kind === "OBJECT") param.kind = "OBJECT";
|
|
59
|
-
else if (r.kind === "SCALAR") param.kind = "SCALAR";
|
|
60
|
-
if (typeof r.dataType === "string" && DATA_TYPES.has(r.dataType)) {
|
|
61
|
-
param.dataType = r.dataType;
|
|
62
|
-
}
|
|
63
|
-
if (typeof r.unit === "string") param.unit = r.unit;
|
|
64
|
-
if (r.required === true) param.required = true;
|
|
65
|
-
if (typeof r.default === "string") param.default = r.default;
|
|
66
|
-
if (typeof r.minValue === "number" && Number.isFinite(r.minValue)) param.minValue = r.minValue;
|
|
67
|
-
if (typeof r.maxValue === "number" && Number.isFinite(r.maxValue)) param.maxValue = r.maxValue;
|
|
68
|
-
if (Array.isArray(r.enum)) {
|
|
69
|
-
const values = r.enum.filter((v) => typeof v === "string");
|
|
70
|
-
if (values.length > 0) param.enum = values;
|
|
71
|
-
}
|
|
72
|
-
return param;
|
|
73
|
-
}
|
|
74
|
-
function isScalar(param) {
|
|
75
|
-
return param.kind !== "OBJECT";
|
|
76
|
-
}
|
|
77
|
-
function parseBool(raw) {
|
|
78
|
-
const s = raw.trim().toLowerCase();
|
|
79
|
-
return s === "true" || s === "1" || s === "t" || s === "yes" || s === "on";
|
|
80
|
-
}
|
|
81
|
-
function defaultValues(params) {
|
|
82
|
-
const values = {};
|
|
83
|
-
for (const p of params) {
|
|
84
|
-
if (!isScalar(p)) continue;
|
|
85
|
-
if (p.dataType === "BOOLEAN") values[p.name] = parseBool(p.default ?? "") ? "true" : "false";
|
|
86
|
-
else if (p.default != null) values[p.name] = p.default;
|
|
87
|
-
}
|
|
88
|
-
return values;
|
|
89
|
-
}
|
|
90
|
-
function validateParams(params, values) {
|
|
91
|
-
const errors = {};
|
|
92
|
-
for (const p of params) {
|
|
93
|
-
if (!isScalar(p)) {
|
|
94
|
-
if (p.required) errors[p.name] = "Structured parameter \u2014 not supported here";
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
if (p.dataType === "BOOLEAN") continue;
|
|
98
|
-
const raw = (values[p.name] ?? "").trim();
|
|
99
|
-
if (raw === "") {
|
|
100
|
-
if (p.required) errors[p.name] = "Required";
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
if (p.dataType === "INT" || p.dataType === "DOUBLE") {
|
|
104
|
-
const n = Number(raw);
|
|
105
|
-
if (!Number.isFinite(n)) {
|
|
106
|
-
errors[p.name] = "Must be a number";
|
|
107
|
-
} else if (p.dataType === "INT" && !Number.isInteger(n)) {
|
|
108
|
-
errors[p.name] = "Must be a whole number";
|
|
109
|
-
} else if (p.minValue != null && n < p.minValue) {
|
|
110
|
-
errors[p.name] = `Must be \u2265 ${p.minValue}`;
|
|
111
|
-
} else if (p.maxValue != null && n > p.maxValue) {
|
|
112
|
-
errors[p.name] = `Must be \u2264 ${p.maxValue}`;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
return errors;
|
|
117
|
-
}
|
|
118
|
-
function buildPayload(params, values) {
|
|
119
|
-
const payload = {};
|
|
120
|
-
for (const p of params) {
|
|
121
|
-
if (!isScalar(p)) continue;
|
|
122
|
-
const raw = (values[p.name] ?? "").trim();
|
|
123
|
-
if (p.dataType === "BOOLEAN") {
|
|
124
|
-
payload[p.name] = parseBool(raw);
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
if (raw === "") continue;
|
|
128
|
-
payload[p.name] = coerce(p, raw);
|
|
129
|
-
}
|
|
130
|
-
return Object.keys(payload).length === 0 ? void 0 : JSON.stringify(payload);
|
|
131
|
-
}
|
|
132
|
-
function coerce(param, raw) {
|
|
133
|
-
switch (param.dataType) {
|
|
134
|
-
case "INT":
|
|
135
|
-
case "DOUBLE": {
|
|
136
|
-
const n = Number(raw);
|
|
137
|
-
return Number.isFinite(n) ? n : raw;
|
|
138
|
-
}
|
|
139
|
-
case "BOOLEAN":
|
|
140
|
-
return parseBool(raw);
|
|
141
|
-
default:
|
|
142
|
-
return raw;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export {
|
|
147
|
-
commandStatusColor,
|
|
148
|
-
isTerminalStatus,
|
|
149
|
-
commandStatusLabel,
|
|
150
|
-
parseParameterSchema,
|
|
151
|
-
isScalar,
|
|
152
|
-
parseBool,
|
|
153
|
-
defaultValues,
|
|
154
|
-
validateParams,
|
|
155
|
-
buildPayload
|
|
156
|
-
};
|
|
157
|
-
//# sourceMappingURL=chunk-YPFB6DAL.js.map
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../src/widgets/command-status.ts", "../../src/widgets/command-params.ts"],
|
|
4
|
-
"sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Command delivery-status presentation for the command-button widget (command-delivery\n// lifecycle: QUEUED \u2192 SENT \u2192 SUCCESSFUL | FAILED, with HELD for a dispatch withheld from\n// a known-absent device, PARKED for one published to a device that turned out to be\n// unreachable, and TIMEOUT / EXPIRED / CANCELLED as the other terminals).\n// Status color is SEMANTIC (good/working/waiting/bad), deliberately fixed rather than\n// derived from the dashboard accent, so the same status reads the same on every theme \u2014\n// and it mirrors the console's DeviceCommandsPanel status variants (success /\n// destructive / outline / in-flight) so an operator reads the same signal on both\n// surfaces.\n\nimport { isTerminalCommandStatus } from '@devicechain/dashboards';\n\n// \uD83D\uDD34 EVERY STATUS NEEDS AN ENTRY, INCLUDING THE ONES THAT LOOK NEUTRAL. UNKNOWN_COLOR\n// below is the same slate as EXPIRED, so a status with no entry does not merely fall\n// back \u2014 it becomes INDISTINGUISHABLE from \"lapsed before it was sent\". HELD and PARKED\n// (the command is still coming) rendered in EXPIRED's slate tell an operator the opposite\n// of the truth, which is why they, and CANCELLED, are spelled out rather than left to the\n// fallback.\n//\n// The scheme is by MEANING, not by lifecycle position: sky = moving under its own steam,\n// amber = waiting on a device that isn't there. HELD and PARKED share the amber family\n// because they are the same answer to \"why hasn't it arrived?\" \u2014 the device is away \u2014 and\n// take different shades because they are different answers to \"what did the platform\n// already try?\": HELD never left the platform, PARKED was published and found nobody home.\nconst STATUS_COLORS: Record<string, string> = {\n SUCCESSFUL: '#16a34a', // green-600 \u2014 acknowledged by the device\n FAILED: '#dc2626', // red-600\n TIMEOUT: '#dc2626', // red-600 \u2014 never acknowledged in time\n EXPIRED: '#64748b', // slate-500 \u2014 lapsed before it was ever sent\n CANCELLED: '#7c3aed', // violet-600 \u2014 deliberately called off; a decision, not a failure\n QUEUED: '#0284c7', // sky-600 \u2014 in flight\n SENT: '#0284c7',\n HELD: '#d97706', // amber-600 \u2014 withheld: the device is absent, the command still stands\n PARKED: '#f59e0b', // amber-500 \u2014 published, nobody home; held for the device's return\n};\n\nconst UNKNOWN_COLOR = '#64748b';\n\n// commandStatusColor maps a status to its badge color, falling back to muted slate for\n// an unrecognized value (a future/hand-edited status).\nexport function commandStatusColor(status: string): string {\n return STATUS_COLORS[status] ?? UNKNOWN_COLOR;\n}\n\n// isTerminalStatus reports whether a command has reached a state it can't leave \u2014 the\n// widget stops treating it as in-flight.\n//\n// It is NOT the question \"can this still be cancelled?\": SENT is non-terminal and is not\n// cancellable, so a host gating a cancel control must ask @devicechain/dashboards'\n// isCancellableCommandStatus instead of negating this.\n//\n// It is an alias, not a second definition: the terminal set lives in @devicechain/dashboards\n// so the console, this package and the preview cannot disagree about it. The name is kept\n// because it is part of this package's published surface.\nexport function isTerminalStatus(status: string): boolean {\n return isTerminalCommandStatus(status);\n}\n\n// commandStatusLabel renders a status in Title Case ('SUCCESSFUL' \u2192 'Successful').\nexport function commandStatusLabel(status: string): string {\n if (!status) return '';\n return status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Pure form logic for the command-button widget's typed parameter form (ADR-043). The\n// console bakes a command definition's `parameterSchema` (a JSON string) into the\n// widget's options; these helpers parse it, seed defaults, validate operator input, and\n// serialize the typed values back to the JSON payload command-delivery sends the device.\n// Kept separate from the widget so the coercion/validation is unit-tested in isolation.\n//\n// SCOPE: the form types top-level SCALAR parameters (DOUBLE / INT / BOOLEAN / STRING,\n// with enum / bounds / required / default). OBJECT (nested) parameters are not\n// form-editable \u2014 the form blocks sending when one is REQUIRED (it can't be satisfied\n// here) and otherwise omits it. The payload is a flat JSON object keyed by parameter name.\n\nimport type { CommandParameter } from '@devicechain/dashboards';\n\nconst DATA_TYPES = new Set(['DOUBLE', 'INT', 'BOOLEAN', 'STRING']);\n\n// parseParameterSchema turns the baked JSON string into descriptors, defensively:\n// a null/empty/malformed schema (or a non-array root) yields an empty list so a command\n// with no arguments (or a hand-broken schema) renders as a bare Send button, never throws.\n// Each descriptor is NORMALIZED (enum\u2192string[], default\u2192string, bounds\u2192finite number,\n// kind/dataType\u2192known value) so a hand-edited dashboard definition with wrong-typed fields\n// (e.g. `enum: \"on\"` or `default: 5`) can't crash the form at render/send time.\nexport function parseParameterSchema(schema: string | null | undefined): CommandParameter[] {\n if (!schema) return [];\n let parsed: unknown;\n try {\n parsed = JSON.parse(schema);\n } catch {\n return [];\n }\n if (!Array.isArray(parsed)) return [];\n const out: CommandParameter[] = [];\n for (const raw of parsed) {\n const norm = normalizeParam(raw);\n if (norm) out.push(norm);\n }\n return out;\n}\n\n// normalizeParam validates one descriptor and coerces every field to its expected type,\n// dropping the descriptor entirely if it has no string `name`. Unknown/wrong-typed fields\n// are discarded rather than trusted, so downstream code never sees a non-array enum or a\n// numeric default.\nfunction normalizeParam(raw: unknown): CommandParameter | null {\n if (typeof raw !== 'object' || raw === null) return null;\n const r = raw as Record<string, unknown>;\n if (typeof r.name !== 'string') return null;\n\n const param: CommandParameter = { name: r.name };\n if (typeof r.description === 'string') param.description = r.description;\n if (r.kind === 'OBJECT') param.kind = 'OBJECT';\n else if (r.kind === 'SCALAR') param.kind = 'SCALAR';\n if (typeof r.dataType === 'string' && DATA_TYPES.has(r.dataType)) {\n param.dataType = r.dataType as CommandParameter['dataType'];\n }\n if (typeof r.unit === 'string') param.unit = r.unit;\n if (r.required === true) param.required = true;\n if (typeof r.default === 'string') param.default = r.default;\n if (typeof r.minValue === 'number' && Number.isFinite(r.minValue)) param.minValue = r.minValue;\n if (typeof r.maxValue === 'number' && Number.isFinite(r.maxValue)) param.maxValue = r.maxValue;\n if (Array.isArray(r.enum)) {\n const values = r.enum.filter((v): v is string => typeof v === 'string');\n if (values.length > 0) param.enum = values;\n }\n return param;\n}\n\n// isScalar reports whether a parameter is a typed single value the form renders an input\n// for. Kind absent means SCALAR (the Go default); only an explicit OBJECT is nested.\nexport function isScalar(param: CommandParameter): boolean {\n return param.kind !== 'OBJECT';\n}\n\n// parseBool interprets a schema/string boolean the way the server's strconv.ParseBool\n// does (accepting '1'/'t'/'true'/'yes'/'on', case-insensitively) so a definition default\n// of \"True\" doesn't silently render \u2014 and send \u2014 as false.\nexport function parseBool(raw: string): boolean {\n const s = raw.trim().toLowerCase();\n return s === 'true' || s === '1' || s === 't' || s === 'yes' || s === 'on';\n}\n\n// defaultValues seeds the form's value map from each scalar parameter. A BOOLEAN always\n// gets a concrete 'true'/'false' (default false) \u2014 a checkbox is never \"empty\", so the\n// value must reflect the box's state exactly. Other scalars seed from their declared\n// default (as a string \u2014 inputs are string-backed), else start empty.\nexport function defaultValues(params: CommandParameter[]): Record<string, string> {\n const values: Record<string, string> = {};\n for (const p of params) {\n if (!isScalar(p)) continue;\n if (p.dataType === 'BOOLEAN') values[p.name] = parseBool(p.default ?? '') ? 'true' : 'false';\n else if (p.default != null) values[p.name] = p.default;\n }\n return values;\n}\n\n// validateParams returns a per-parameter error map (empty = valid). It enforces required\n// (a missing value on a required scalar; a BOOLEAN always has a value so is never\n// \"required-missing\"), numeric validity/bounds for INT/DOUBLE, and blocks a REQUIRED\n// OBJECT parameter (it can't be supplied through the typed form).\nexport function validateParams(\n params: CommandParameter[],\n values: Record<string, string>,\n): Record<string, string> {\n const errors: Record<string, string> = {};\n for (const p of params) {\n if (!isScalar(p)) {\n // A required structured parameter can't be satisfied here \u2014 refuse to send an\n // incomplete command rather than silently omitting it.\n if (p.required) errors[p.name] = 'Structured parameter \u2014 not supported here';\n continue;\n }\n if (p.dataType === 'BOOLEAN') continue; // a checkbox always carries a valid value\n const raw = (values[p.name] ?? '').trim();\n if (raw === '') {\n if (p.required) errors[p.name] = 'Required';\n continue; // an empty optional value is fine \u2014 it's just omitted from the payload\n }\n if (p.dataType === 'INT' || p.dataType === 'DOUBLE') {\n const n = Number(raw);\n if (!Number.isFinite(n)) {\n errors[p.name] = 'Must be a number';\n } else if (p.dataType === 'INT' && !Number.isInteger(n)) {\n errors[p.name] = 'Must be a whole number';\n } else if (p.minValue != null && n < p.minValue) {\n errors[p.name] = `Must be \u2265 ${p.minValue}`;\n } else if (p.maxValue != null && n > p.maxValue) {\n errors[p.name] = `Must be \u2264 ${p.maxValue}`;\n }\n }\n }\n return errors;\n}\n\n// buildPayload serializes the typed values to the JSON payload string command-delivery\n// sends the device \u2014 coercing each scalar to its declared type (INT/DOUBLE \u2192 number,\n// BOOLEAN \u2192 boolean, STRING \u2192 string). A BOOLEAN is always included (its checkbox state\n// is meaningful); other empty optional values are omitted. OBJECT parameters are omitted\n// (validateParams blocks a required one first). Returns undefined when no parameter\n// contributes a value (a parameterless command sends no payload).\nexport function buildPayload(\n params: CommandParameter[],\n values: Record<string, string>,\n): string | undefined {\n const payload: Record<string, unknown> = {};\n for (const p of params) {\n if (!isScalar(p)) continue;\n const raw = (values[p.name] ?? '').trim();\n if (p.dataType === 'BOOLEAN') {\n payload[p.name] = parseBool(raw);\n continue;\n }\n if (raw === '') continue;\n payload[p.name] = coerce(p, raw);\n }\n return Object.keys(payload).length === 0 ? undefined : JSON.stringify(payload);\n}\n\nfunction coerce(param: CommandParameter, raw: string): unknown {\n switch (param.dataType) {\n case 'INT':\n case 'DOUBLE': {\n const n = Number(raw);\n return Number.isFinite(n) ? n : raw; // validation gates this; fall back to raw defensively\n }\n case 'BOOLEAN':\n return parseBool(raw);\n default:\n return raw; // STRING (and untyped)\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../src/fleet-feed.tsx", "../../src/fleet-seed.ts"],
|
|
4
|
-
"sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The data feed of a fleet widget (one row or one bar per member of an anchor): the roster, each\n// member's own live readings, and one seed read per member for what it last reported before the\n// board was opened. Loaded on first use (connected-widget.tsx imports it dynamically), so a board\n// with no fleet widget never downloads it.\n//\n// - ONE subscription per widget (hub.subscribeMembers). The samples it delivers carry their\n// device, and everything here is keyed by device and measurement together -- the measurement\n// channel's name-only `latest` is exactly what this exists to get away from;\n// - the seed is read per member, bounded hub-wide and shared between widgets that ask the same\n// question; what is read is fleet-seed.ts's plan (the last few minutes first, the span only for\n// what that left empty). The window is the widget's pinned span, else the board's, ending at the\n// freeze instant while paused;\n// - paused, live readings queue (newest per device and measurement) and merge on resume, so the\n// table on screen does not move under a \"Paused\" badge;\n// - a change of span re-reads the seeds; a reading older than the span is no longer shown.\n//\n// Rebuilding the whole state per sample would re-render a 500-row table hundreds of times a\n// second, so changes are folded into a store and flushed at most once per FLUSH_MS.\n\nimport {\n DEFAULT_TIME_RANGE_SECONDS,\n type FleetState,\n type MeasurementSample,\n type MeasurementStreamState,\n type MemberRoster,\n type MemberSeed,\n type WidgetActions,\n type WidgetDataSource,\n type WidgetDescriptor,\n type WidgetInstance,\n} from '@devicechain/dashboards';\nimport { optString, resolveStaleAfterSeconds, useWidgetTimeRange, WidgetFrame, WidgetFreshnessProvider } from '@devicechain/widget-sdk';\nimport { useEffect, useMemo, useReducer, useRef, useState, type ComponentType } from 'react';\n\nimport { measurementSpan } from './binding-policy';\nimport { buildFleet, newestMemberTime, seedWindow } from './fleet-model';\nimport { memberSeedPlan } from './fleet-seed';\nimport { fleetStrings } from './fleet-strings';\nimport { LevelEmpty } from './level-readout';\n\n// The most often the state is rebuilt from live samples.\nexport const FLUSH_MS = 250;\n// How often a reading that has aged out of the span is dropped when nothing else changes.\nconst AGE_OUT_MS = 30_000;\n\ninterface Store {\n // The selector this store was filled for: a render that carries a new selector must not show the\n // previous fleet's rows under it while the effect that resets the store has yet to run.\n key: string | null;\n roster: MemberRoster | null;\n live: Map<string, Map<string, MeasurementSample>>;\n seeds: Map<string, MemberSeed | 'failed'>;\n forbidden: boolean;\n error: unknown | null;\n}\n\nconst emptyStore = (key: string | null): Store => ({ key, roster: null, live: new Map(), seeds: new Map(), forbidden: false, error: null });\n\nfunction record(into: Map<string, Map<string, MeasurementSample>>, sample: MeasurementSample): void {\n let byName = into.get(sample.deviceToken);\n if (!byName) into.set(sample.deviceToken, (byName = new Map()));\n // Newest wins; an out-of-order older sample must not roll a cell back.\n const held = byName.get(sample.name);\n if (held?.occurredTime && sample.occurredTime && Date.parse(sample.occurredTime) < Date.parse(held.occurredTime)) return;\n byName.set(sample.name, sample);\n}\n\nexport interface FleetFeedProps {\n hub: WidgetDataSource;\n widget: WidgetInstance;\n descriptor: WidgetDescriptor;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n Component: ComponentType<any>;\n actions: WidgetActions | undefined;\n ErrorFrame: ComponentType<{ widget: WidgetInstance; error: unknown }>;\n // Not a fleet widget, but bound to a group: say so instead (GroupRefusal).\n refuse?: boolean;\n}\n\n// What a widget that shows ONE device's readings keyed by measurement name renders when it is bound\n// to a group: every device reporting \"temperature\" would overwrite the others in it, so it shows a\n// sentence instead of whichever device spoke last. A widget with a per-device mode (the bar chart's\n// one bar per device) is pointed at that mode; any other at the fleet table.\nexport function GroupRefusal({ widget, descriptor }: { widget: WidgetInstance; descriptor: WidgetDescriptor }) {\n const strings = fleetStrings();\n const perDevice = (descriptor.binding as { members?: unknown }).members !== undefined;\n return (\n <WidgetFrame title={optString(widget.options, 'title')}>\n <div data-group-refused=\"\" style={{ display: 'contents' }}>\n <LevelEmpty message={`${strings.oneDevice} ${perDevice ? strings.usePerDevice : strings.useFleetTable}`} />\n </div>\n </WidgetFrame>\n );\n}\n\n// The measurements a fleet widget shows, and so the ones its seed must find AND its live feed must\n// carry: the datasource's, and the `measurement` option a per-device bar chart compares. Empty: every\n// measurement reported. One list for both, because the hub's live feed delivers only the names its\n// selector lists: a bar chart comparing a measurement its datasource does not name would otherwise\n// be seeded with it and then never hear of it again.\nexport function shownMeasurements(widget: WidgetInstance): string[] {\n const one = widget.options?.measurement;\n const named = [...(widget.datasource?.measurements ?? []), ...(typeof one === 'string' && one !== '' ? [one] : [])];\n return [...new Set(named)];\n}\n\nexport default function FleetEntry(props: FleetFeedProps) {\n return props.refuse ? <GroupRefusal widget={props.widget} descriptor={props.descriptor} /> : <FleetFeed {...props} />;\n}\n\nfunction FleetFeed({ hub, widget, descriptor, Component, actions, ErrorFrame }: FleetFeedProps) {\n const range = useWidgetTimeRange();\n const now = useMemo(() => (hub.now ? () => hub.now!() : Date.now), [hub]);\n const spanSeconds = measurementSpan(descriptor, widget, range?.spanSeconds) ?? range?.spanSeconds ?? DEFAULT_TIME_RANGE_SECONDS;\n const pausedAt = range?.paused ? range.pausedAt : undefined;\n const names = shownMeasurements(widget);\n const ds = widget.datasource && { ...widget.datasource, measurements: names };\n const dsKey = JSON.stringify(ds ?? null);\n const namesKey = JSON.stringify(names);\n const supported = hub.subscribeMembers !== undefined && hub.readMemberSeeds !== undefined;\n\n const store = useRef<Store>(emptyStore(dsKey));\n // Live samples that arrive while paused, newest per device and measurement.\n const queued = useRef<Map<string, Map<string, MeasurementSample>>>(new Map());\n const pausedRef = useRef(pausedAt !== undefined);\n const [version, bump] = useReducer((n: number) => n + 1, 0);\n const [rosterKey, setRosterKey] = useState<string | null>(null);\n const flushTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n const schedule = (): void => {\n if (flushTimer.current !== undefined) return;\n flushTimer.current = setTimeout(() => {\n flushTimer.current = undefined;\n bump();\n }, FLUSH_MS);\n };\n\n // The subscription: the roster, then the live samples. A new selector starts from nothing.\n useEffect(() => {\n const s = emptyStore(dsKey);\n store.current = s;\n queued.current = new Map();\n setRosterKey(null);\n bump();\n if (!ds || !supported) return;\n const dispose = hub.subscribeMembers!(ds, {\n roster: (roster) => {\n s.roster = roster;\n setRosterKey(roster.members.map((m) => m.token).join('\\n'));\n schedule();\n },\n next: (sample) => {\n record(pausedRef.current ? queued.current : s.live, sample);\n if (!pausedRef.current) schedule();\n },\n forbidden: () => {\n s.forbidden = true;\n schedule();\n },\n error: (err) => {\n s.error = err;\n schedule();\n },\n });\n return () => {\n dispose();\n if (flushTimer.current !== undefined) clearTimeout(flushTimer.current);\n flushTimer.current = undefined;\n };\n // `ds` is read through `dsKey` (value identity), not by reference.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hub, dsKey, supported]);\n\n // The pause gate: resuming merges what queued while frozen.\n useEffect(() => {\n const was = pausedRef.current;\n pausedRef.current = pausedAt !== undefined;\n if (was && pausedAt === undefined) {\n for (const byName of queued.current.values()) for (const sample of byName.values()) record(store.current.live, sample);\n queued.current = new Map();\n schedule();\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [pausedAt]);\n\n // The seeds: one read per member for the window, again when the span or the freeze instant changes.\n useEffect(() => {\n const s = store.current;\n if (!supported || !s.roster || rosterKey === null) return;\n const tokens = s.roster.members.map((m) => m.token);\n if (tokens.length === 0) return;\n let cancelled = false;\n void hub.readMemberSeeds!(\n tokens,\n memberSeedPlan(seedWindow(spanSeconds, pausedAt ?? now()), names),\n (token, seed, failure) => {\n if (failure === 'forbidden') s.forbidden = true;\n else s.seeds.set(token, seed ?? 'failed');\n schedule();\n },\n () => cancelled,\n );\n return () => {\n cancelled = true;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hub, supported, dsKey, rosterKey, spanSeconds, pausedAt, namesKey]);\n\n // A reading ages out of the span with no event to say so.\n useEffect(() => {\n if (pausedAt !== undefined) return;\n const id = setInterval(bump, AGE_OUT_MS);\n return () => clearInterval(id);\n }, [pausedAt]);\n\n const members = useMemo<FleetState>(() => {\n const s = store.current.key === dsKey ? store.current : emptyStore(dsKey);\n if (!supported) return { status: 'unsupported', members: [], total: 0, truncated: false };\n return buildFleet({\n roster: s.roster,\n seeds: s.seeds,\n live: s.live,\n since: (pausedAt ?? now()) - spanSeconds * 1000,\n forbidden: s.forbidden,\n });\n // `version` is the store's change counter; the store itself is mutable.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [version, supported, dsKey, spanSeconds, pausedAt, now]);\n\n const error = store.current.key === dsKey ? store.current.error : null;\n const data: MeasurementStreamState = { latest: {}, samples: [], error, members };\n\n // Opt-in through the descriptor, as for any measurement widget. The frame's age is the newest\n // reading anywhere in the fleet; a widget that shows ages per row takes them from the members.\n const freshness = useMemo(\n () =>\n descriptor.freshness === true\n ? { at: newestMemberTime(members), staleAfterSeconds: resolveStaleAfterSeconds(widget.options) }\n : undefined,\n [descriptor, widget.options, members],\n );\n if (error) return <ErrorFrame widget={widget} error={error} />;\n return (\n <WidgetFreshnessProvider freshness={freshness}>\n <Component widget={widget} data={data} actions={actions} />\n </WidgetFreshnessProvider>\n );\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// What a fleet widget reads to fill its cells before the live feed has, and how it makes a seed of\n// the answer. Loaded with the fleet feed (a lazy chunk), never by a board without a fleet widget.\n//\n// A fleet of 500 is 500 devices to ask, so the cost per device is what matters. The obvious read --\n// every measurement of the device over the whole span in the span's buckets -- returns up to 240\n// buckets per measurement per device (a megabyte or more per device on a long span, tens of\n// megabytes per viewer) to find ONE number per cell. The plan instead asks the question the cell\n// asks, cheapest first:\n//\n// 1. THE LAST FEW MINUTES (TAIL_SECONDS, in one-minute buckets): at most five rows per measurement,\n// read from the store's one-minute summaries. A device that is reporting -- the ordinary case,\n// and the expensive one, because a busy device is the one with the most history -- is done here,\n// with its newest minute.\n// 2. Only for what that left empty, the whole span. One read PER missing measurement, narrowed to\n// that name (the store aggregates one name or all), when the device did report in the tail --\n// which is also what tells \"does not report this\" from \"sent nothing\", so the narrow read loses\n// nothing. Otherwise (a quiet device, or more missing names than are worth a read each) one read\n// of every measurement over the span, as before.\n//\n// A table that names no columns gets the span read alone: its columns ARE the names its devices\n// reported in the span, which only a read of the whole span can list.\n//\n// 3. A value a span read found is the AVERAGE of a span bucket (42 minutes wide on a week), not the\n// device's latest reading: a machine that read 14, was refuelled to 40 and then parked would\n// show 27. So the newest span bucket is read again in one-minute buckets -- at most a bucket's\n// worth of rows -- and its newest minute is what the cell shows, as for a device seen in the\n// tail. Names whose newest buckets coincide share one such read, and a device gets at most\n// MAX_NARROW_READS of them, newest bucket first. A name left past that bound is not shown\n// rather than shown as an average (an average is never the latest reading), and is marked\n// `unread`: the device has data for it, so its cell says \"not loaded\", never \"no data\".\n//\n// Ages. The store returns a bucket's start, not the time of its newest reading, so a seeded reading is\n// stamped at the start of its bucket (or of the span, when the bucket began before it). The reading\n// happened at or after that instant, so its age is OVERSTATED, never understated: by less than a\n// minute for a device seen in the tail, by up to one span bucket (bucketSecondsFor) otherwise. The\n// first live reading replaces the stamp with the real time.\n\nimport type {\n HistoryWindow,\n MeasurementBucket,\n MeasurementSample,\n MemberBucketCriteria,\n MemberSeed,\n MemberSeedPlan,\n} from '@devicechain/dashboards';\n\nconst MINUTE_MS = 60_000;\n\n// The tail read first: long enough to catch a device that reports every minute or two, short enough\n// that it is a handful of rows per measurement.\nexport const TAIL_SECONDS = 300;\n\n// The most narrowed (one-name) span reads one device gets before a single read of every name is the\n// cheaper question.\nexport const MAX_NARROW_READS = 2;\n\nconst has = (o: object, key: string): boolean => Object.prototype.hasOwnProperty.call(o, key);\n\n// foldMemberSeed turns one device's buckets into its seed: the newest bucket with a mean wins per\n// name, stamped at max(bucket start, `notBefore`) (see \"Ages\" above). A name whose buckets hold\n// readings is observed even when none has a mean.\nexport function foldMemberSeed(deviceToken: string, buckets: readonly MeasurementBucket[], notBefore?: string): MemberSeed {\n const floor = notBefore === undefined ? Number.NaN : Date.parse(notBefore);\n const latest: Record<string, MeasurementSample> = {};\n const at: Record<string, number> = {};\n const observed = new Set<string>();\n for (const b of buckets) {\n if (b.count === undefined ? b.avg != null : b.count > 0) observed.add(b.name);\n if (b.avg == null) continue;\n const start = Date.parse(b.bucketStart);\n if (has(at, b.name) && at[b.name] >= start) continue;\n at[b.name] = start;\n latest[b.name] = {\n id: `seed-${deviceToken}-${b.name}-${b.bucketStart}`,\n deviceToken,\n eventType: 0,\n occurredTime: start < floor ? new Date(floor).toISOString() : b.bucketStart,\n name: b.name,\n value: b.avg,\n classifier: null,\n };\n }\n return { latest, observed: [...observed] };\n}\n\n// Two seeds of one device: `newer` (read over a later stretch) wins per name; observed is the union.\nexport function mergeSeeds(newer: MemberSeed, older: MemberSeed): MemberSeed {\n return {\n latest: { ...older.latest, ...newer.latest },\n observed: [...new Set([...newer.observed, ...older.observed])],\n };\n}\n\n// The tail of a window: its last TAIL_SECONDS, on whole minutes, in one-minute buckets. Undefined\n// when the window is no longer than that (the window itself is then the cheap read).\nexport function tailWindow(window: HistoryWindow): HistoryWindow | undefined {\n const start = Math.floor((Date.parse(window.endTime) + 1) / MINUTE_MS) * MINUTE_MS - TAIL_SECONDS * 1000;\n if (!(start > Date.parse(window.startTime))) return undefined;\n return { startTime: new Date(start).toISOString(), endTime: window.endTime, intervalSeconds: 60 };\n}\n\n// The plan a fleet widget hands the hub: the window is the widget's span, `names` the measurements\n// it shows (empty: every measurement its devices report).\nexport function memberSeedPlan(window: HistoryWindow, names: readonly string[]): MemberSeedPlan {\n const wanted = [...new Set(names)];\n return {\n async seed(deviceToken, read) {\n const fold = (c: MemberBucketCriteria) => read(c).then((b) => foldMemberSeed(deviceToken, b, c.startTime));\n const tail = tailWindow(window);\n let seed: MemberSeed;\n let recent: MemberSeed | undefined;\n if (wanted.length === 0 || !tail) seed = await fold(window);\n else {\n recent = await fold(tail);\n const missing = wanted.filter((name) => !has(recent!.latest, name));\n // Nothing missing falls through the loop below with no read: the tail was the whole answer.\n if (recent.observed.length === 0 || missing.length > MAX_NARROW_READS) seed = mergeSeeds(recent, await fold(window));\n else {\n seed = recent;\n for (const name of missing) seed = mergeSeeds(seed, await fold({ ...window, name }));\n }\n }\n return window.intervalSeconds > 60 ? sharpen(seed, recent, window, fold) : seed;\n },\n };\n}\n\n// Step 3 above: replaces each value a span read found (any not from the tail's minutes) with the\n// newest minute of its span bucket. A span value is stamped at its bucket's start (or the window's),\n// so the bucket's readings lie between that stamp and one bucket width after it.\nasync function sharpen(\n seed: MemberSeed,\n recent: MemberSeed | undefined,\n window: HistoryWindow,\n fold: (c: MemberBucketCriteria) => Promise<MemberSeed>,\n): Promise<MemberSeed> {\n const byStamp = new Map<number, string[]>();\n for (const [name, sample] of Object.entries(seed.latest)) {\n if (recent && has(recent.latest, name)) continue;\n const at = Date.parse(sample.occurredTime ?? '');\n byStamp.set(at, [...(byStamp.get(at) ?? []), name]);\n }\n const latest = { ...seed.latest };\n const unread: string[] = [];\n const end = Date.parse(window.endTime) + 1;\n const stamps = [...byStamp.keys()].sort((a, b) => b - a);\n for (const [i, at] of stamps.entries()) {\n const names = byStamp.get(at)!;\n const minutes =\n i < MAX_NARROW_READS\n ? (\n await fold({\n startTime: new Date(at).toISOString(),\n endTime: new Date(Math.min(at + window.intervalSeconds * 1000, end) - 1).toISOString(),\n intervalSeconds: 60,\n ...(names.length === 1 ? { name: names[0] } : {}),\n })\n ).latest\n : {};\n for (const name of names) {\n if (has(minutes, name)) latest[name] = minutes[name];\n else {\n delete latest[name];\n unread.push(name);\n }\n }\n }\n return { latest, observed: seed.observed, unread };\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|