@devicechain/dashboards 0.21.1-rc.1 → 0.22.0-rc.1
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 +88 -7
- package/dist/channel-snapshots.d.ts +43 -0
- package/dist/connection.d.ts +27 -0
- package/dist/definition.d.ts +8 -6
- package/dist/editor-clipboard.d.ts +42 -0
- package/dist/editor-history.d.ts +30 -0
- package/dist/editor-model.d.ts +62 -13
- package/dist/grid.d.ts +53 -0
- package/dist/history.d.ts +1 -0
- package/dist/hub.d.ts +25 -0
- package/dist/index.d.ts +21 -8
- package/dist/index.js +2332 -243
- package/dist/index.js.map +4 -4
- package/dist/is-record.d.ts +1 -0
- package/dist/layout-lint.d.ts +24 -0
- package/dist/legacy-grid.d.ts +4 -0
- package/dist/legacy-window.d.ts +5 -0
- package/dist/members.d.ts +36 -0
- package/dist/option-schema.d.ts +10 -1
- package/dist/queries.d.ts +14 -0
- package/dist/recorded.d.ts +4 -0
- package/dist/reflow.d.ts +26 -0
- package/dist/resolver.d.ts +1 -0
- package/dist/same-label.d.ts +1 -0
- package/dist/slots.d.ts +2 -1
- package/dist/synthetic.d.ts +8 -4
- package/dist/time-range.d.ts +52 -0
- package/dist/types.d.ts +96 -11
- package/dist/widget-descriptor.d.ts +25 -4
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -59,9 +59,545 @@ var WIDGET_TYPES = [
|
|
|
59
59
|
"alarm-count",
|
|
60
60
|
"command-button",
|
|
61
61
|
"entity-selector",
|
|
62
|
-
"map"
|
|
62
|
+
"map",
|
|
63
|
+
"linear-gauge",
|
|
64
|
+
"level-gauge",
|
|
65
|
+
"battery",
|
|
66
|
+
"status-light",
|
|
67
|
+
"signal-bars",
|
|
68
|
+
"compass",
|
|
69
|
+
"progress-ring",
|
|
70
|
+
"command-slider",
|
|
71
|
+
"command-toggle",
|
|
72
|
+
"stat-card",
|
|
73
|
+
"bar-chart",
|
|
74
|
+
"pie-chart",
|
|
75
|
+
"fleet-table"
|
|
63
76
|
];
|
|
77
|
+
var PLACEMENT_ANCHORS = [
|
|
78
|
+
"top-left",
|
|
79
|
+
"top",
|
|
80
|
+
"top-right",
|
|
81
|
+
"left",
|
|
82
|
+
"center",
|
|
83
|
+
"right",
|
|
84
|
+
"bottom-left",
|
|
85
|
+
"bottom",
|
|
86
|
+
"bottom-right"
|
|
87
|
+
];
|
|
88
|
+
var PLACEMENT_MODES = ["grid", "relative"];
|
|
89
|
+
var RESERVED_PLACEMENT_MODES = ["pinned", "sticky"];
|
|
90
|
+
var THEME_COLOR_TOKENS = [
|
|
91
|
+
"background",
|
|
92
|
+
"foreground",
|
|
93
|
+
"card",
|
|
94
|
+
"card-foreground",
|
|
95
|
+
"muted",
|
|
96
|
+
"muted-foreground",
|
|
97
|
+
"primary",
|
|
98
|
+
"primary-foreground",
|
|
99
|
+
"border",
|
|
100
|
+
"destructive",
|
|
101
|
+
"chart-1",
|
|
102
|
+
"chart-2",
|
|
103
|
+
"chart-3",
|
|
104
|
+
"chart-4",
|
|
105
|
+
"chart-5",
|
|
106
|
+
"sev-critical",
|
|
107
|
+
"sev-major",
|
|
108
|
+
"sev-minor",
|
|
109
|
+
"sev-warning"
|
|
110
|
+
];
|
|
111
|
+
var SPACE_TOKEN_PX = {
|
|
112
|
+
"space-0": 0,
|
|
113
|
+
"space-1": 4,
|
|
114
|
+
"space-2": 8,
|
|
115
|
+
"space-3": 12,
|
|
116
|
+
"space-4": 16
|
|
117
|
+
};
|
|
118
|
+
var FRAME_VARIANTS = ["card", "flat", "none"];
|
|
119
|
+
var FRAME_RADII = ["none", "sm", "md", "lg"];
|
|
120
|
+
var FRAME_BORDER_WIDTHS = [0, 1, 2, 3];
|
|
121
|
+
var FRAME_HEADERS = ["auto", "show", "hide"];
|
|
64
122
|
var UNKNOWN_WIDGET_TYPE = "unknown-widget";
|
|
123
|
+
var CANVAS_DENSITIES = ["compact", "comfortable", "wall"];
|
|
124
|
+
var DEFAULT_CANVAS_DENSITY = "comfortable";
|
|
125
|
+
var DENSITY_SCALE = {
|
|
126
|
+
compact: 0.9,
|
|
127
|
+
comfortable: 1,
|
|
128
|
+
wall: 1.5
|
|
129
|
+
};
|
|
130
|
+
function isCanvasDensity(value) {
|
|
131
|
+
return typeof value === "string" && CANVAS_DENSITIES.includes(value);
|
|
132
|
+
}
|
|
133
|
+
var CURRENT_SCHEMA_VERSION = 3;
|
|
134
|
+
|
|
135
|
+
// src/grid.ts
|
|
136
|
+
var BASE_BREAKPOINT = "base";
|
|
137
|
+
var DEFAULT_GRID = Object.freeze({
|
|
138
|
+
columns: 12,
|
|
139
|
+
minCellSize: 56,
|
|
140
|
+
gutter: 8,
|
|
141
|
+
padding: 0
|
|
142
|
+
});
|
|
143
|
+
var PHONE_MIN_COLUMNS = 4;
|
|
144
|
+
function isSpaceToken(v) {
|
|
145
|
+
return typeof v === "string" && Object.prototype.hasOwnProperty.call(SPACE_TOKEN_PX, v);
|
|
146
|
+
}
|
|
147
|
+
function gridLengthPx(len, scale = 1) {
|
|
148
|
+
const base = typeof len === "number" ? len : SPACE_TOKEN_PX[len];
|
|
149
|
+
return base * scale;
|
|
150
|
+
}
|
|
151
|
+
function gutterPx(grid, scale = 1) {
|
|
152
|
+
const g = grid.gutter;
|
|
153
|
+
if (typeof g === "object") return { x: gridLengthPx(g.x, scale), y: gridLengthPx(g.y, scale) };
|
|
154
|
+
const v = gridLengthPx(g, scale);
|
|
155
|
+
return { x: v, y: v };
|
|
156
|
+
}
|
|
157
|
+
function paddingPx(grid, scale = 1) {
|
|
158
|
+
const p = grid.padding;
|
|
159
|
+
if (typeof p === "object") {
|
|
160
|
+
return {
|
|
161
|
+
top: gridLengthPx(p.top, scale),
|
|
162
|
+
right: gridLengthPx(p.right, scale),
|
|
163
|
+
bottom: gridLengthPx(p.bottom, scale),
|
|
164
|
+
left: gridLengthPx(p.left, scale)
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const v = gridLengthPx(p, scale);
|
|
168
|
+
return { top: v, right: v, bottom: v, left: v };
|
|
169
|
+
}
|
|
170
|
+
function cellSizeAt(grid, containerWidth, columns = grid.columns, scale = 1) {
|
|
171
|
+
const pad = paddingPx(grid, scale);
|
|
172
|
+
const g = gutterPx(grid, scale);
|
|
173
|
+
return Math.max(0, (containerWidth - pad.left - pad.right - (columns - 1) * g.x) / columns);
|
|
174
|
+
}
|
|
175
|
+
function containerWidthFor(grid, cell, columns = grid.columns, scale = 1) {
|
|
176
|
+
const pad = paddingPx(grid, scale);
|
|
177
|
+
return columns * cell + (columns - 1) * gutterPx(grid, scale).x + pad.left + pad.right;
|
|
178
|
+
}
|
|
179
|
+
function breakpointMinWidth(spec) {
|
|
180
|
+
return typeof spec === "number" ? spec : spec.minWidth;
|
|
181
|
+
}
|
|
182
|
+
function breakpointColumns(canvas, breakpoint) {
|
|
183
|
+
if (breakpoint === BASE_BREAKPOINT) return canvas.grid.columns;
|
|
184
|
+
const spec = Object.prototype.hasOwnProperty.call(canvas.breakpoints, breakpoint) ? canvas.breakpoints[breakpoint] : void 0;
|
|
185
|
+
return spec !== void 0 && typeof spec === "object" ? spec.columns : canvas.grid.columns;
|
|
186
|
+
}
|
|
187
|
+
function activeBreakpoint(breakpoints, width) {
|
|
188
|
+
let best = BASE_BREAKPOINT;
|
|
189
|
+
let bestWidth = -1;
|
|
190
|
+
for (const [name, spec] of Object.entries(breakpoints)) {
|
|
191
|
+
const minWidth = breakpointMinWidth(spec);
|
|
192
|
+
if (width >= minWidth && minWidth > bestWidth) {
|
|
193
|
+
best = name;
|
|
194
|
+
bestWidth = minWidth;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return best;
|
|
198
|
+
}
|
|
199
|
+
function resolvePlacement(layout, breakpoint) {
|
|
200
|
+
return Object.prototype.hasOwnProperty.call(layout, breakpoint) ? layout[breakpoint] : layout[BASE_BREAKPOINT];
|
|
201
|
+
}
|
|
202
|
+
function layoutBreakpoints(breakpoints, widgets) {
|
|
203
|
+
const names = /* @__PURE__ */ new Set([BASE_BREAKPOINT, ...Object.keys(breakpoints)]);
|
|
204
|
+
for (const w of widgets) for (const bp of Object.keys(w.layout)) names.add(bp);
|
|
205
|
+
return [...names];
|
|
206
|
+
}
|
|
207
|
+
function gridFootprintsOverlap(a, b) {
|
|
208
|
+
return a.col < b.col + b.cols && b.col < a.col + a.cols && a.row < b.row + b.rows && b.row < a.row + a.rows;
|
|
209
|
+
}
|
|
210
|
+
function pushDown(items, first = []) {
|
|
211
|
+
const order = items.map((item, index) => ({ ...item, index }));
|
|
212
|
+
const priority = new Map(first.map((id, i) => [id, i]));
|
|
213
|
+
order.sort((a, b) => {
|
|
214
|
+
const pa = priority.get(a.id);
|
|
215
|
+
const pb = priority.get(b.id);
|
|
216
|
+
if (pa !== void 0 || pb !== void 0) return (pa ?? Infinity) - (pb ?? Infinity);
|
|
217
|
+
return a.placement.row - b.placement.row || a.placement.col - b.placement.col || a.index - b.index;
|
|
218
|
+
});
|
|
219
|
+
const placed = [];
|
|
220
|
+
const out = /* @__PURE__ */ new Map();
|
|
221
|
+
for (const item of order) {
|
|
222
|
+
let p = item.placement;
|
|
223
|
+
for (; ; ) {
|
|
224
|
+
const hits = placed.filter((q) => gridFootprintsOverlap(p, q));
|
|
225
|
+
if (hits.length === 0) break;
|
|
226
|
+
p = { ...p, row: Math.max(...hits.map((q) => q.row + q.rows)) };
|
|
227
|
+
}
|
|
228
|
+
placed.push(p);
|
|
229
|
+
out.set(item.id, p);
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
function pushAside(items, first = []) {
|
|
234
|
+
const order = items.map((item, index) => ({ ...item, index }));
|
|
235
|
+
const priority = new Map(first.map((id, i) => [id, i]));
|
|
236
|
+
order.sort((a, b) => {
|
|
237
|
+
const pa = priority.get(a.id);
|
|
238
|
+
const pb = priority.get(b.id);
|
|
239
|
+
if (pa !== void 0 || pb !== void 0) return (pa ?? Infinity) - (pb ?? Infinity);
|
|
240
|
+
return a.placement.row - b.placement.row || a.placement.col - b.placement.col || a.index - b.index;
|
|
241
|
+
});
|
|
242
|
+
const pending = new Map(order.map((item) => [item.id, item.placement]));
|
|
243
|
+
const placed = [];
|
|
244
|
+
const out = /* @__PURE__ */ new Map();
|
|
245
|
+
const clear = (p, others) => {
|
|
246
|
+
for (const q of others) if (gridFootprintsOverlap(p, q)) return false;
|
|
247
|
+
return true;
|
|
248
|
+
};
|
|
249
|
+
for (const item of order) {
|
|
250
|
+
pending.delete(item.id);
|
|
251
|
+
let p = item.placement;
|
|
252
|
+
if (priority.has(item.id)) {
|
|
253
|
+
for (; ; ) {
|
|
254
|
+
const hits = placed.filter((q) => gridFootprintsOverlap(p, q));
|
|
255
|
+
if (hits.length === 0) break;
|
|
256
|
+
p = { ...p, row: Math.max(...hits.map((q) => q.row + q.rows)) };
|
|
257
|
+
}
|
|
258
|
+
} else if (!clear(p, placed)) {
|
|
259
|
+
const from = p.row;
|
|
260
|
+
for (let k = 1; ; k++) {
|
|
261
|
+
const up = { ...p, row: from - k };
|
|
262
|
+
if (up.row >= 0 && clear(up, placed) && clear(up, pending.values())) {
|
|
263
|
+
p = up;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
const down = { ...p, row: from + k };
|
|
267
|
+
if (clear(down, placed)) {
|
|
268
|
+
p = down;
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
placed.push(p);
|
|
274
|
+
out.set(item.id, p);
|
|
275
|
+
}
|
|
276
|
+
return out;
|
|
277
|
+
}
|
|
278
|
+
function firstFreeSlot(occupied, cols, rows, columns) {
|
|
279
|
+
const lastCol = Math.max(0, columns - cols);
|
|
280
|
+
const bottom = occupied.reduce((m, p) => Math.max(m, p.row + p.rows), 0);
|
|
281
|
+
for (let row = 0; row <= bottom; row++) {
|
|
282
|
+
for (let col = 0; col <= lastCol; col++) {
|
|
283
|
+
const probe = { mode: "grid", col, row, cols, rows };
|
|
284
|
+
if (!occupied.some((q) => gridFootprintsOverlap(probe, q))) return { col, row };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return { col: 0, row: bottom };
|
|
288
|
+
}
|
|
289
|
+
function spanPx(n, cell, gutter) {
|
|
290
|
+
return n * cell + (n - 1) * gutter;
|
|
291
|
+
}
|
|
292
|
+
function gridRect(p, cell, gutter) {
|
|
293
|
+
return {
|
|
294
|
+
x: p.col * (cell + gutter.x),
|
|
295
|
+
y: p.row * (cell + gutter.y),
|
|
296
|
+
w: spanPx(p.cols, cell, gutter.x),
|
|
297
|
+
h: spanPx(p.rows, cell, gutter.y)
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function anchorFraction(anchor) {
|
|
301
|
+
const fx = anchor.endsWith("left") ? 0 : anchor.endsWith("right") ? 1 : 0.5;
|
|
302
|
+
const fy = anchor.startsWith("top") ? 0 : anchor.startsWith("bottom") ? 1 : 0.5;
|
|
303
|
+
return { fx, fy };
|
|
304
|
+
}
|
|
305
|
+
function relativeRect(p, host, cell, gutter) {
|
|
306
|
+
const w = "fill" in p.size ? host.w : spanPx(p.size.cols, cell, gutter.x);
|
|
307
|
+
const h = "fill" in p.size ? host.h : spanPx(p.size.rows, cell, gutter.y);
|
|
308
|
+
const a = anchorFraction(p.anchor);
|
|
309
|
+
const s = anchorFraction(p.self ?? p.anchor);
|
|
310
|
+
return {
|
|
311
|
+
x: host.x + a.fx * host.w - s.fx * w + (p.offset?.x ?? 0) * (cell + gutter.x),
|
|
312
|
+
y: host.y + a.fy * host.h - s.fy * h + (p.offset?.y ?? 0) * (cell + gutter.y),
|
|
313
|
+
w,
|
|
314
|
+
h
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function layoutRects(widgets, breakpoint, cell, gutter) {
|
|
318
|
+
const rects = /* @__PURE__ */ new Map();
|
|
319
|
+
const relatives = [];
|
|
320
|
+
for (const w of widgets) {
|
|
321
|
+
const p = resolvePlacement(w.layout, breakpoint);
|
|
322
|
+
if (p.mode === "grid") rects.set(w.id, gridRect(p, cell, gutter));
|
|
323
|
+
else relatives.push({ id: w.id, p });
|
|
324
|
+
}
|
|
325
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
326
|
+
for (const { id, p } of relatives) {
|
|
327
|
+
const host = rects.get(p.host);
|
|
328
|
+
if (host && !rects.has(id)) rects.set(id, relativeRect(p, host, cell, gutter));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return rects;
|
|
332
|
+
}
|
|
333
|
+
var FRAME_PRESETS = {
|
|
334
|
+
card: {
|
|
335
|
+
variant: "card",
|
|
336
|
+
border: { width: 1, radius: "md", color: "border" },
|
|
337
|
+
padding: "space-3",
|
|
338
|
+
background: "card",
|
|
339
|
+
header: "auto"
|
|
340
|
+
},
|
|
341
|
+
flat: {
|
|
342
|
+
variant: "flat",
|
|
343
|
+
border: { width: 0, radius: "md", color: "border" },
|
|
344
|
+
padding: "space-3",
|
|
345
|
+
background: "card",
|
|
346
|
+
header: "auto"
|
|
347
|
+
},
|
|
348
|
+
none: {
|
|
349
|
+
variant: "none",
|
|
350
|
+
border: { width: 0, radius: "none", color: "border" },
|
|
351
|
+
padding: "space-0",
|
|
352
|
+
background: "transparent",
|
|
353
|
+
header: "hide"
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
function resolveFrame(canvasFrame, widgetFrame) {
|
|
357
|
+
const c = canvasFrame ?? {};
|
|
358
|
+
const w = widgetFrame ?? {};
|
|
359
|
+
const variant = w.variant ?? c.variant ?? "card";
|
|
360
|
+
const preset = FRAME_PRESETS[variant];
|
|
361
|
+
return {
|
|
362
|
+
variant,
|
|
363
|
+
border: { ...preset.border, ...c.border, ...w.border },
|
|
364
|
+
padding: w.padding ?? c.padding ?? preset.padding,
|
|
365
|
+
background: w.background ?? c.background ?? preset.background,
|
|
366
|
+
header: w.header ?? c.header ?? preset.header
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/is-record.ts
|
|
371
|
+
function isRecord(v) {
|
|
372
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/legacy-grid.ts
|
|
376
|
+
var GRID_SCHEMA_VERSION = 2;
|
|
377
|
+
var LEGACY_REFERENCE_WIDTH = 1366;
|
|
378
|
+
var LEGACY_DEFAULTS = { columns: 24, gap: 8, rowHeight: 40 };
|
|
379
|
+
function num(rec, key, fallback) {
|
|
380
|
+
const v = rec[key];
|
|
381
|
+
return typeof v === "number" && Number.isFinite(v) ? v : fallback;
|
|
382
|
+
}
|
|
383
|
+
function storedSchemaVersion(raw) {
|
|
384
|
+
if (typeof raw.schemaVersion === "number" && Number.isFinite(raw.schemaVersion)) return raw.schemaVersion;
|
|
385
|
+
const grid = isRecord(raw.canvas) && isRecord(raw.canvas.grid) ? raw.canvas.grid : {};
|
|
386
|
+
if ("rowHeight" in grid || "gap" in grid) return 1;
|
|
387
|
+
const widgets = Array.isArray(raw.widgets) ? raw.widgets : [];
|
|
388
|
+
for (const w of widgets) {
|
|
389
|
+
if (!isRecord(w) || !isRecord(w.layout)) continue;
|
|
390
|
+
for (const box of Object.values(w.layout)) {
|
|
391
|
+
if (!isRecord(box)) continue;
|
|
392
|
+
if ("colSpan" in box || "rowSpan" in box || "z" in box) return 1;
|
|
393
|
+
if (!("mode" in box) && !("cols" in box)) return 1;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return CURRENT_SCHEMA_VERSION;
|
|
397
|
+
}
|
|
398
|
+
function legacyGrid(raw) {
|
|
399
|
+
const rec = isRecord(raw) ? raw : {};
|
|
400
|
+
const columns = Math.max(1, Math.round(num(rec, "columns", LEGACY_DEFAULTS.columns)));
|
|
401
|
+
const rowHeight = Math.max(1, num(rec, "rowHeight", LEGACY_DEFAULTS.rowHeight));
|
|
402
|
+
let gapRow = LEGACY_DEFAULTS.gap;
|
|
403
|
+
let gapCol = LEGACY_DEFAULTS.gap;
|
|
404
|
+
if (typeof rec.gap === "number" && Number.isFinite(rec.gap)) {
|
|
405
|
+
gapRow = gapCol = Math.max(0, rec.gap);
|
|
406
|
+
} else if (isRecord(rec.gap)) {
|
|
407
|
+
gapRow = Math.max(0, num(rec.gap, "row", LEGACY_DEFAULTS.gap));
|
|
408
|
+
gapCol = Math.max(0, num(rec.gap, "col", LEGACY_DEFAULTS.gap));
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
columns,
|
|
412
|
+
rowPitch: rowHeight + gapRow,
|
|
413
|
+
gutter: gapRow === gapCol ? gapCol : { x: gapCol, y: gapRow }
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function restack(items) {
|
|
417
|
+
const order = [...items].sort((a, b) => a.old.row - b.old.row || a.old.col - b.old.col || a.index - b.index);
|
|
418
|
+
const placed = [];
|
|
419
|
+
const moved = /* @__PURE__ */ new Map();
|
|
420
|
+
for (const item of order) {
|
|
421
|
+
let p = item.placement;
|
|
422
|
+
const sharesColumn = (q) => p.col < q.col + q.cols && q.col < p.col + p.cols;
|
|
423
|
+
const floor = placed.filter((q) => q.old.end <= item.old.row && sharesColumn(q.placement)).reduce((m, q) => Math.max(m, q.placement.row + q.placement.rows), 0);
|
|
424
|
+
if (floor > p.row) p = { ...p, row: floor };
|
|
425
|
+
for (; ; ) {
|
|
426
|
+
const hits = placed.filter((q) => gridFootprintsOverlap(p, q.placement));
|
|
427
|
+
if (hits.length === 0) break;
|
|
428
|
+
p = { ...p, row: Math.max(...hits.map((q) => q.placement.row + q.placement.rows)) };
|
|
429
|
+
}
|
|
430
|
+
placed.push({ ...item, placement: p });
|
|
431
|
+
if (p !== item.placement) moved.set(item.index, p);
|
|
432
|
+
}
|
|
433
|
+
return moved;
|
|
434
|
+
}
|
|
435
|
+
function convertLegacyDefinition(raw) {
|
|
436
|
+
const canvasIn = isRecord(raw.canvas) ? raw.canvas : {};
|
|
437
|
+
const old = legacyGrid(canvasIn.grid);
|
|
438
|
+
const grid = { ...DEFAULT_GRID, gutter: old.gutter };
|
|
439
|
+
const columns = grid.columns;
|
|
440
|
+
const pitch = cellSizeAt(grid, LEGACY_REFERENCE_WIDTH) + gutterPx(grid).y;
|
|
441
|
+
const toCols = (line) => Math.round(line * columns / old.columns);
|
|
442
|
+
const toRows = (line) => Math.round(line * old.rowPitch / pitch);
|
|
443
|
+
const convertBox = (box) => {
|
|
444
|
+
const col = Math.max(0, Math.round(num(box, "col", 0)));
|
|
445
|
+
const colSpan = Math.max(1, Math.round(num(box, "colSpan", 1)));
|
|
446
|
+
const row = Math.max(0, Math.round(num(box, "row", 0)));
|
|
447
|
+
const rowSpan = Math.max(1, Math.round(num(box, "rowSpan", 1)));
|
|
448
|
+
const c0 = Math.min(toCols(col), columns - 1);
|
|
449
|
+
const c1 = Math.min(Math.max(toCols(col + colSpan), c0 + 1), columns);
|
|
450
|
+
const r0 = toRows(row);
|
|
451
|
+
const r1 = Math.max(toRows(row + rowSpan), r0 + 1);
|
|
452
|
+
return { mode: "grid", col: c0, row: r0, cols: c1 - c0, rows: r1 - r0 };
|
|
453
|
+
};
|
|
454
|
+
const oldBoxes = /* @__PURE__ */ new Map();
|
|
455
|
+
const widgetsIn = Array.isArray(raw.widgets) ? raw.widgets : [];
|
|
456
|
+
const widgets = widgetsIn.map((w, i) => {
|
|
457
|
+
if (!isRecord(w) || !isRecord(w.layout)) return w;
|
|
458
|
+
const layout = {};
|
|
459
|
+
const olds = {};
|
|
460
|
+
for (const [bp, box] of Object.entries(w.layout)) {
|
|
461
|
+
if (!isRecord(box) || bp === "__proto__") continue;
|
|
462
|
+
layout[bp] = convertBox(box);
|
|
463
|
+
const row = Math.max(0, Math.round(num(box, "row", 0)));
|
|
464
|
+
olds[bp] = { row, end: row + Math.max(1, Math.round(num(box, "rowSpan", 1))), col: Math.max(0, Math.round(num(box, "col", 0))) };
|
|
465
|
+
}
|
|
466
|
+
oldBoxes.set(i, olds);
|
|
467
|
+
return { ...w, layout };
|
|
468
|
+
});
|
|
469
|
+
const byBreakpoint = /* @__PURE__ */ new Set([BASE_BREAKPOINT]);
|
|
470
|
+
for (const w of widgets) if (isRecord(w) && isRecord(w.layout)) for (const bp of Object.keys(w.layout)) byBreakpoint.add(bp);
|
|
471
|
+
for (const bp of byBreakpoint) {
|
|
472
|
+
const items = [];
|
|
473
|
+
widgets.forEach((w, i) => {
|
|
474
|
+
if (!isRecord(w) || !isRecord(w.layout)) return;
|
|
475
|
+
const layout = w.layout;
|
|
476
|
+
const own2 = Object.prototype.hasOwnProperty.call(layout, bp);
|
|
477
|
+
const p = own2 ? layout[bp] : layout[BASE_BREAKPOINT];
|
|
478
|
+
const old2 = oldBoxes.get(i)?.[own2 ? bp : BASE_BREAKPOINT];
|
|
479
|
+
if (p && isRecord(p) && p.mode === "grid" && old2) items.push({ index: i, placement: p, old: old2 });
|
|
480
|
+
});
|
|
481
|
+
for (const [index, moved] of restack(items)) {
|
|
482
|
+
const w = widgets[index];
|
|
483
|
+
w.layout[bp] = moved;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
const breakpoints = {};
|
|
487
|
+
if (isRecord(canvasIn.breakpoints)) {
|
|
488
|
+
for (const [name, width] of Object.entries(canvasIn.breakpoints)) {
|
|
489
|
+
if (typeof width === "number" && Number.isFinite(width)) breakpoints[name] = width;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
...raw,
|
|
494
|
+
schemaVersion: GRID_SCHEMA_VERSION,
|
|
495
|
+
canvas: { ...canvasIn, grid, breakpoints },
|
|
496
|
+
widgets
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// src/legacy-window.ts
|
|
501
|
+
var TIME_SERIES = "timeseries-chart";
|
|
502
|
+
function convertSampleWindows(raw) {
|
|
503
|
+
const widgetsIn = Array.isArray(raw.widgets) ? raw.widgets : [];
|
|
504
|
+
const flagged = [];
|
|
505
|
+
const widgets = widgetsIn.map((w, i) => {
|
|
506
|
+
if (!isRecord(w) || w.type !== TIME_SERIES) return w;
|
|
507
|
+
let out = w;
|
|
508
|
+
if (isRecord(w.options) && Object.prototype.hasOwnProperty.call(w.options, "window")) {
|
|
509
|
+
const { window: _count, ...options } = w.options;
|
|
510
|
+
out = { ...out, options };
|
|
511
|
+
flagged.push(i);
|
|
512
|
+
}
|
|
513
|
+
if (isRecord(w.widget) && w.widget.optionsVersion === 1) {
|
|
514
|
+
out = { ...out, widget: { ...w.widget, optionsVersion: 2 } };
|
|
515
|
+
}
|
|
516
|
+
return out;
|
|
517
|
+
});
|
|
518
|
+
return { doc: { ...raw, schemaVersion: CURRENT_SCHEMA_VERSION, widgets }, flagged };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/time-range.ts
|
|
522
|
+
var TIME_RANGE_PRESETS = [
|
|
523
|
+
{ id: "15m", seconds: 15 * 60 },
|
|
524
|
+
{ id: "1h", seconds: 60 * 60 },
|
|
525
|
+
{ id: "6h", seconds: 6 * 60 * 60 },
|
|
526
|
+
{ id: "24h", seconds: 24 * 60 * 60 },
|
|
527
|
+
{ id: "7d", seconds: 7 * 24 * 60 * 60 }
|
|
528
|
+
];
|
|
529
|
+
var DEFAULT_TIME_RANGE_SECONDS = 60 * 60;
|
|
530
|
+
var MIN_TIME_RANGE_SECONDS = 60;
|
|
531
|
+
var MAX_TIME_RANGE_SECONDS = 7 * 24 * 60 * 60;
|
|
532
|
+
function isValidSpanSeconds(v) {
|
|
533
|
+
return typeof v === "number" && Number.isInteger(v) && v >= MIN_TIME_RANGE_SECONDS && v <= MAX_TIME_RANGE_SECONDS;
|
|
534
|
+
}
|
|
535
|
+
function initialTimeRange(defaultSeconds, link = {}, now = Date.now()) {
|
|
536
|
+
const spanSeconds = link.spanSeconds ?? (isValidSpanSeconds(defaultSeconds) ? defaultSeconds : DEFAULT_TIME_RANGE_SECONDS);
|
|
537
|
+
return link.paused ? { spanSeconds, paused: true, pausedAt: now } : { spanSeconds, paused: false };
|
|
538
|
+
}
|
|
539
|
+
function pauseRange(state, now) {
|
|
540
|
+
return state.paused ? state : { spanSeconds: state.spanSeconds, paused: true, pausedAt: now };
|
|
541
|
+
}
|
|
542
|
+
function resumeRange(state) {
|
|
543
|
+
return state.paused ? { spanSeconds: state.spanSeconds, paused: false } : state;
|
|
544
|
+
}
|
|
545
|
+
function withSpan(state, spanSeconds) {
|
|
546
|
+
return isValidSpanSeconds(spanSeconds) ? { ...state, spanSeconds } : state;
|
|
547
|
+
}
|
|
548
|
+
function rangeEnd(state, now) {
|
|
549
|
+
return state.paused && state.pausedAt !== void 0 ? state.pausedAt : now;
|
|
550
|
+
}
|
|
551
|
+
function rangeWindow(state, now) {
|
|
552
|
+
const end = rangeEnd(state, now);
|
|
553
|
+
return {
|
|
554
|
+
startTime: new Date(end - state.spanSeconds * 1e3).toISOString(),
|
|
555
|
+
endTime: new Date(end).toISOString()
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
var MAX_SEED_BUCKETS = 240;
|
|
559
|
+
var BASE_BUCKET_SECONDS = 60;
|
|
560
|
+
function bucketSecondsFor(spanSeconds) {
|
|
561
|
+
const wanted = Math.ceil(spanSeconds / MAX_SEED_BUCKETS);
|
|
562
|
+
return Math.max(BASE_BUCKET_SECONDS, Math.ceil(wanted / BASE_BUCKET_SECONDS) * BASE_BUCKET_SECONDS);
|
|
563
|
+
}
|
|
564
|
+
function seedBucketCount(spanSeconds) {
|
|
565
|
+
return Math.ceil(spanSeconds / bucketSecondsFor(spanSeconds));
|
|
566
|
+
}
|
|
567
|
+
function parseRangeParam(value) {
|
|
568
|
+
if (!value) return void 0;
|
|
569
|
+
return TIME_RANGE_PRESETS.find((p) => p.id === value)?.seconds;
|
|
570
|
+
}
|
|
571
|
+
function parsePausedParam(value) {
|
|
572
|
+
return value === "1";
|
|
573
|
+
}
|
|
574
|
+
function timeRangeFromSearch(search) {
|
|
575
|
+
let params;
|
|
576
|
+
try {
|
|
577
|
+
params = new URLSearchParams(search);
|
|
578
|
+
} catch {
|
|
579
|
+
return {};
|
|
580
|
+
}
|
|
581
|
+
const link = {};
|
|
582
|
+
const spanSeconds = parseRangeParam(params.get("range"));
|
|
583
|
+
if (spanSeconds !== void 0) link.spanSeconds = spanSeconds;
|
|
584
|
+
if (parsePausedParam(params.get("paused"))) link.paused = true;
|
|
585
|
+
return link;
|
|
586
|
+
}
|
|
587
|
+
function presetIdOf(spanSeconds) {
|
|
588
|
+
return TIME_RANGE_PRESETS.find((p) => p.seconds === spanSeconds)?.id;
|
|
589
|
+
}
|
|
590
|
+
function spanLabel(spanSeconds) {
|
|
591
|
+
if (spanSeconds % 86400 === 0) return `${spanSeconds / 86400}d`;
|
|
592
|
+
if (spanSeconds % 3600 === 0) return `${spanSeconds / 3600}h`;
|
|
593
|
+
if (spanSeconds % 60 === 0) return `${spanSeconds / 60}m`;
|
|
594
|
+
return `${spanSeconds}s`;
|
|
595
|
+
}
|
|
596
|
+
function setBoardTimeRange(def, defaultSeconds) {
|
|
597
|
+
const { timeRange: _previous, ...rest } = def;
|
|
598
|
+
if (!isValidSpanSeconds(defaultSeconds) || defaultSeconds === DEFAULT_TIME_RANGE_SECONDS) return rest;
|
|
599
|
+
return { ...rest, timeRange: { defaultSeconds } };
|
|
600
|
+
}
|
|
65
601
|
|
|
66
602
|
// src/widget-compat.ts
|
|
67
603
|
var BUILTIN_TYPES = new Set(WIDGET_TYPES);
|
|
@@ -72,9 +608,6 @@ function isResolvedWidget(widget) {
|
|
|
72
608
|
return widget.type !== UNKNOWN_WIDGET_TYPE;
|
|
73
609
|
}
|
|
74
610
|
var SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
75
|
-
function isRecord(v) {
|
|
76
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
77
|
-
}
|
|
78
611
|
function parseWidgetRef(raw) {
|
|
79
612
|
if (raw === void 0) return void 0;
|
|
80
613
|
if (!isRecord(raw)) return null;
|
|
@@ -142,43 +675,95 @@ function unresolvedWidgets(definition) {
|
|
|
142
675
|
}
|
|
143
676
|
|
|
144
677
|
// src/definition.ts
|
|
145
|
-
var BASE_BREAKPOINT = "base";
|
|
146
678
|
var WIDGET_TYPE_SET = new Set(WIDGET_TYPES);
|
|
147
|
-
var DEFAULT_GRID = { columns: 24, gap: 8, rowHeight: 40 };
|
|
148
679
|
var DEFAULT_SIZING = "fill";
|
|
149
680
|
var DashboardDefinitionError = class extends Error {
|
|
150
|
-
constructor(message) {
|
|
681
|
+
constructor(message, code, found) {
|
|
151
682
|
super(`invalid dashboard definition: ${message}`);
|
|
683
|
+
this.code = code;
|
|
684
|
+
this.found = found;
|
|
152
685
|
this.name = "DashboardDefinitionError";
|
|
153
686
|
}
|
|
154
687
|
};
|
|
155
|
-
function isRecord2(v) {
|
|
156
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
157
|
-
}
|
|
158
688
|
function numberAt(rec, key, fallback) {
|
|
159
689
|
const v = rec[key];
|
|
160
690
|
return typeof v === "number" && Number.isFinite(v) ? v : fallback;
|
|
161
691
|
}
|
|
162
|
-
function parseDashboardDefinition(
|
|
163
|
-
if (!
|
|
692
|
+
function parseDashboardDefinition(input, options = {}) {
|
|
693
|
+
if (!isRecord(input)) throw new DashboardDefinitionError("not a JSON object");
|
|
694
|
+
const stored = storedSchemaVersion(input);
|
|
695
|
+
if (stored > CURRENT_SCHEMA_VERSION) {
|
|
696
|
+
throw new DashboardDefinitionError(`schemaVersion ${stored} is too new`, "schemaTooNew", stored);
|
|
697
|
+
}
|
|
698
|
+
if (!Array.isArray(input.widgets)) throw new DashboardDefinitionError("widgets must be an array");
|
|
699
|
+
const legacy = stored < CURRENT_SCHEMA_VERSION;
|
|
700
|
+
const gridConverted = stored < GRID_SCHEMA_VERSION;
|
|
701
|
+
let raw = input;
|
|
702
|
+
if (gridConverted) raw = convertLegacyDefinition(raw);
|
|
703
|
+
let flaggedWindows = [];
|
|
704
|
+
if (legacy) {
|
|
705
|
+
const converted = convertSampleWindows(raw);
|
|
706
|
+
raw = converted.doc;
|
|
707
|
+
flaggedWindows = converted.flagged;
|
|
708
|
+
}
|
|
164
709
|
const widgetsRaw = raw.widgets;
|
|
165
|
-
if (!Array.isArray(widgetsRaw)) throw new DashboardDefinitionError("widgets must be an array");
|
|
166
710
|
const def = {
|
|
167
|
-
|
|
711
|
+
// The format this release writes (a newer one was refused above): a save writes that format.
|
|
712
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
168
713
|
title: typeof raw.title === "string" ? raw.title : "",
|
|
169
714
|
canvas: parseCanvas(raw.canvas),
|
|
170
715
|
widgets: widgetsRaw.map((w, i) => parseWidget(w, i, options.catalog))
|
|
171
716
|
};
|
|
172
717
|
const slots = parseSlots(raw.slots);
|
|
173
718
|
if (slots) def.slots = slots;
|
|
719
|
+
const timeRange = parseBoardTimeRange(raw.timeRange);
|
|
720
|
+
if (timeRange) def.timeRange = timeRange;
|
|
721
|
+
validatePlacements(def);
|
|
722
|
+
const flaggedIds = flaggedWindows.map((i) => def.widgets[i]?.id).filter((id) => id !== void 0);
|
|
723
|
+
if (gridConverted && def.widgets.length > 0 || flaggedIds.length > 0) def.convertedFrom = stored;
|
|
724
|
+
else if (typeof raw.convertedFrom === "number") def.convertedFrom = raw.convertedFrom;
|
|
725
|
+
if (flaggedIds.length > 0) def.convertedWindows = flaggedIds;
|
|
726
|
+
else if (Array.isArray(raw.convertedWindows) && raw.convertedWindows.every((id) => typeof id === "string")) {
|
|
727
|
+
def.convertedWindows = raw.convertedWindows;
|
|
728
|
+
}
|
|
174
729
|
return def;
|
|
175
730
|
}
|
|
731
|
+
function validatePlacements(def) {
|
|
732
|
+
for (const bp of layoutBreakpoints(def.canvas.breakpoints, def.widgets)) {
|
|
733
|
+
const at = bp === BASE_BREAKPOINT ? "" : ` at breakpoint '${bp}'`;
|
|
734
|
+
const placements = /* @__PURE__ */ new Map();
|
|
735
|
+
for (const w of def.widgets) placements.set(w.id, resolvePlacement(w.layout, bp));
|
|
736
|
+
const grid = [...placements].filter((e) => e[1].mode === "grid");
|
|
737
|
+
for (let i = 0; i < grid.length; i++) {
|
|
738
|
+
for (let j = i + 1; j < grid.length; j++) {
|
|
739
|
+
if (gridFootprintsOverlap(grid[i][1], grid[j][1])) {
|
|
740
|
+
throw new DashboardDefinitionError(
|
|
741
|
+
`widgets '${grid[i][0]}' and '${grid[j][0]}' overlap on the grid${at}; a widget that is meant to sit over another is placed relative to it`
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
for (const [id, p] of placements) {
|
|
747
|
+
if (p.mode !== "relative") continue;
|
|
748
|
+
if (p.host === id) throw new DashboardDefinitionError(`widget '${id}' is placed relative to itself${at}`);
|
|
749
|
+
const host = placements.get(p.host);
|
|
750
|
+
if (!host) {
|
|
751
|
+
throw new DashboardDefinitionError(`widget '${id}' is placed relative to '${p.host}', which does not exist${at}`);
|
|
752
|
+
}
|
|
753
|
+
if (host.mode === "relative" && placements.get(host.host)?.mode !== "grid") {
|
|
754
|
+
throw new DashboardDefinitionError(
|
|
755
|
+
`widget '${id}' is placed relative to '${p.host}', which is relative to another relative widget${at}; a relative widget's host may be relative only when that host sits on a grid widget`
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
176
761
|
function parseSlots(raw) {
|
|
177
|
-
if (!
|
|
762
|
+
if (!isRecord(raw)) return void 0;
|
|
178
763
|
const slots = {};
|
|
179
764
|
for (const [name, spec] of Object.entries(raw)) {
|
|
180
765
|
if (name === "__proto__") continue;
|
|
181
|
-
if (!
|
|
766
|
+
if (!isRecord(spec)) continue;
|
|
182
767
|
const type = spec.type === "anchor" ? "anchor" : "device";
|
|
183
768
|
const slot = { type };
|
|
184
769
|
if (typeof spec.label === "string") slot.label = spec.label;
|
|
@@ -193,7 +778,7 @@ function parseSlots(raw) {
|
|
|
193
778
|
return slots;
|
|
194
779
|
}
|
|
195
780
|
function parseScope(raw) {
|
|
196
|
-
if (!
|
|
781
|
+
if (!isRecord(raw)) return void 0;
|
|
197
782
|
const parent = typeof raw.parent === "string" ? raw.parent : "";
|
|
198
783
|
if (!parent) return void 0;
|
|
199
784
|
return { parent, strategy: raw.strategy === "manual" ? "manual" : "first" };
|
|
@@ -237,13 +822,13 @@ function canScopeSlot(slots, child, parentName) {
|
|
|
237
822
|
return true;
|
|
238
823
|
}
|
|
239
824
|
function parseSlotBinding(raw) {
|
|
240
|
-
if (!
|
|
825
|
+
if (!isRecord(raw)) return void 0;
|
|
241
826
|
if (raw.kind === "device") {
|
|
242
827
|
const token = stringAt(raw, "deviceToken");
|
|
243
828
|
return token ? { kind: "device", deviceToken: token } : void 0;
|
|
244
829
|
}
|
|
245
830
|
if (raw.kind === "anchor") {
|
|
246
|
-
const anchorRec =
|
|
831
|
+
const anchorRec = isRecord(raw.anchor) ? raw.anchor : {};
|
|
247
832
|
const targetToken = stringAt(anchorRec, "targetToken");
|
|
248
833
|
if (!targetToken) return void 0;
|
|
249
834
|
return {
|
|
@@ -257,23 +842,86 @@ function parseSlotBinding(raw) {
|
|
|
257
842
|
}
|
|
258
843
|
return void 0;
|
|
259
844
|
}
|
|
845
|
+
function parseGridLength(raw, fallback, where) {
|
|
846
|
+
if (raw === void 0) return fallback;
|
|
847
|
+
if (typeof raw === "number" && Number.isFinite(raw)) return Math.max(0, raw);
|
|
848
|
+
if (isSpaceToken(raw)) return raw;
|
|
849
|
+
throw new DashboardDefinitionError(`${where} is ${JSON.stringify(raw)}; a length is a number of px or a space-0..space-4 token`);
|
|
850
|
+
}
|
|
260
851
|
function parseGrid(raw) {
|
|
261
|
-
const rec =
|
|
852
|
+
const rec = isRecord(raw) ? raw : {};
|
|
262
853
|
const columns = Math.max(1, Math.round(numberAt(rec, "columns", DEFAULT_GRID.columns)));
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
854
|
+
const minCellSize = Math.max(1, numberAt(rec, "minCellSize", DEFAULT_GRID.minCellSize));
|
|
855
|
+
const dg = DEFAULT_GRID.gutter;
|
|
856
|
+
const gutter = isRecord(rec.gutter) ? {
|
|
857
|
+
x: parseGridLength(rec.gutter.x, dg, "canvas.grid.gutter.x"),
|
|
858
|
+
y: parseGridLength(rec.gutter.y, dg, "canvas.grid.gutter.y")
|
|
859
|
+
} : parseGridLength(rec.gutter, dg, "canvas.grid.gutter");
|
|
860
|
+
const dp = DEFAULT_GRID.padding;
|
|
861
|
+
const padding = isRecord(rec.padding) ? {
|
|
862
|
+
top: parseGridLength(rec.padding.top, dp, "canvas.grid.padding.top"),
|
|
863
|
+
right: parseGridLength(rec.padding.right, dp, "canvas.grid.padding.right"),
|
|
864
|
+
bottom: parseGridLength(rec.padding.bottom, dp, "canvas.grid.padding.bottom"),
|
|
865
|
+
left: parseGridLength(rec.padding.left, dp, "canvas.grid.padding.left")
|
|
866
|
+
} : parseGridLength(rec.padding, dp, "canvas.grid.padding");
|
|
867
|
+
return { columns, minCellSize, gutter, padding };
|
|
868
|
+
}
|
|
869
|
+
function parseBreakpoints(raw, grid) {
|
|
870
|
+
const rec = isRecord(raw) ? raw : {};
|
|
871
|
+
const breakpoints = {};
|
|
872
|
+
for (const [name, spec] of Object.entries(rec)) {
|
|
873
|
+
if (name === "__proto__") continue;
|
|
874
|
+
if (typeof spec === "number" && Number.isFinite(spec)) {
|
|
875
|
+
breakpoints[name] = spec;
|
|
876
|
+
} else if (isRecord(spec) && typeof spec.minWidth === "number" && Number.isFinite(spec.minWidth) && typeof spec.columns === "number" && Number.isInteger(spec.columns) && spec.columns >= 1) {
|
|
877
|
+
breakpoints[name] = { minWidth: spec.minWidth, columns: spec.columns };
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
const base = breakpoints[BASE_BREAKPOINT];
|
|
881
|
+
if (base === void 0) breakpoints[BASE_BREAKPOINT] = 0;
|
|
882
|
+
else if (typeof base === "object" && base.columns !== grid.columns) {
|
|
883
|
+
throw new DashboardDefinitionError(
|
|
884
|
+
`breakpoint '${BASE_BREAKPOINT}' declares ${base.columns} columns; the base layout is in canvas.grid.columns (${grid.columns})`
|
|
885
|
+
);
|
|
272
886
|
}
|
|
273
|
-
return
|
|
887
|
+
return breakpoints;
|
|
888
|
+
}
|
|
889
|
+
function oneOf(values, raw, where) {
|
|
890
|
+
if (raw === void 0) return void 0;
|
|
891
|
+
if (values.includes(raw)) return raw;
|
|
892
|
+
throw new DashboardDefinitionError(`${where} is ${JSON.stringify(raw)}; expected one of ${values.map((v) => JSON.stringify(v)).join(", ")}`);
|
|
893
|
+
}
|
|
894
|
+
function parseFrame(raw, where) {
|
|
895
|
+
if (raw === void 0) return void 0;
|
|
896
|
+
if (!isRecord(raw)) throw new DashboardDefinitionError(`${where} is not an object`);
|
|
897
|
+
const frame = {};
|
|
898
|
+
const variant = oneOf(FRAME_VARIANTS, raw.variant, `${where}.variant`);
|
|
899
|
+
if (variant) frame.variant = variant;
|
|
900
|
+
if (raw.border !== void 0) {
|
|
901
|
+
if (!isRecord(raw.border)) throw new DashboardDefinitionError(`${where}.border is not an object`);
|
|
902
|
+
const border = {};
|
|
903
|
+
const width = oneOf(FRAME_BORDER_WIDTHS, raw.border.width, `${where}.border.width`);
|
|
904
|
+
if (width !== void 0) border.width = width;
|
|
905
|
+
const radius = oneOf(FRAME_RADII, raw.border.radius, `${where}.border.radius`);
|
|
906
|
+
if (radius) border.radius = radius;
|
|
907
|
+
const color = oneOf(THEME_COLOR_TOKENS, raw.border.color, `${where}.border.color`);
|
|
908
|
+
if (color) border.color = color;
|
|
909
|
+
frame.border = border;
|
|
910
|
+
}
|
|
911
|
+
if (raw.padding !== void 0) {
|
|
912
|
+
if (!isSpaceToken(raw.padding)) {
|
|
913
|
+
throw new DashboardDefinitionError(`${where}.padding is ${JSON.stringify(raw.padding)}; expected a space-0..space-4 token`);
|
|
914
|
+
}
|
|
915
|
+
frame.padding = raw.padding;
|
|
916
|
+
}
|
|
917
|
+
const background = oneOf([...THEME_COLOR_TOKENS, "transparent"], raw.background, `${where}.background`);
|
|
918
|
+
if (background) frame.background = background;
|
|
919
|
+
const header = oneOf(FRAME_HEADERS, raw.header, `${where}.header`);
|
|
920
|
+
if (header) frame.header = header;
|
|
921
|
+
return frame;
|
|
274
922
|
}
|
|
275
923
|
function parseSizing(raw) {
|
|
276
|
-
if (
|
|
924
|
+
if (isRecord(raw)) {
|
|
277
925
|
if (typeof raw.width === "number" && Number.isFinite(raw.width)) {
|
|
278
926
|
return { width: Math.max(1, raw.width) };
|
|
279
927
|
}
|
|
@@ -283,18 +931,20 @@ function parseSizing(raw) {
|
|
|
283
931
|
}
|
|
284
932
|
return DEFAULT_SIZING;
|
|
285
933
|
}
|
|
934
|
+
function parseBoardTimeRange(raw) {
|
|
935
|
+
if (!isRecord(raw) || !isValidSpanSeconds(raw.defaultSeconds)) return void 0;
|
|
936
|
+
return { defaultSeconds: raw.defaultSeconds };
|
|
937
|
+
}
|
|
286
938
|
function parseCanvas(raw) {
|
|
287
|
-
const rec =
|
|
939
|
+
const rec = isRecord(raw) ? raw : {};
|
|
288
940
|
const grid = parseGrid(rec.grid);
|
|
289
941
|
const sizing = parseSizing(rec.sizing);
|
|
290
|
-
const
|
|
291
|
-
const breakpoints = {};
|
|
292
|
-
for (const [name, width] of Object.entries(bpRec)) {
|
|
293
|
-
if (typeof width === "number" && Number.isFinite(width)) breakpoints[name] = width;
|
|
294
|
-
}
|
|
295
|
-
if (!(BASE_BREAKPOINT in breakpoints)) breakpoints[BASE_BREAKPOINT] = 0;
|
|
942
|
+
const breakpoints = parseBreakpoints(rec.breakpoints, grid);
|
|
296
943
|
const canvas = { grid, sizing, breakpoints };
|
|
297
|
-
|
|
944
|
+
const frame = parseFrame(rec.frame, "canvas.frame");
|
|
945
|
+
if (frame) canvas.frame = frame;
|
|
946
|
+
if (isCanvasDensity(rec.density)) canvas.density = rec.density;
|
|
947
|
+
if (isRecord(rec.background)) {
|
|
298
948
|
const { color, imageUrl } = rec.background;
|
|
299
949
|
canvas.background = {
|
|
300
950
|
color: typeof color === "string" ? color : null,
|
|
@@ -304,12 +954,12 @@ function parseCanvas(raw) {
|
|
|
304
954
|
return canvas;
|
|
305
955
|
}
|
|
306
956
|
function parseWidget(raw, index, catalog) {
|
|
307
|
-
if (!
|
|
957
|
+
if (!isRecord(raw)) throw new DashboardDefinitionError(`widgets[${index}] is not an object`);
|
|
308
958
|
const type = raw.type;
|
|
309
959
|
if (typeof type !== "string" || type.length === 0) {
|
|
310
960
|
throw new DashboardDefinitionError(`widgets[${index}] has unknown type ${JSON.stringify(type)}`);
|
|
311
961
|
}
|
|
312
|
-
if (type === UNKNOWN_WIDGET_TYPE &&
|
|
962
|
+
if (type === UNKNOWN_WIDGET_TYPE && isRecord(raw.raw)) return reparseUnknownWidget(raw, index, catalog);
|
|
313
963
|
const builtin = WIDGET_TYPE_SET.has(type);
|
|
314
964
|
const reason = unresolvedReason(type, raw.widget, catalog, builtin) ?? (builtin ? void 0 : reservedFieldReason(type, raw));
|
|
315
965
|
if (reason) return parseUnknownWidget(raw, index, reason);
|
|
@@ -328,11 +978,13 @@ function parseWidget(raw, index, catalog) {
|
|
|
328
978
|
};
|
|
329
979
|
const ds = parseDatasource(raw.datasource, index);
|
|
330
980
|
if (ds) widget.datasource = ds;
|
|
331
|
-
if (
|
|
981
|
+
if (isRecord(raw.options)) widget.options = raw.options;
|
|
982
|
+
const frame = parseFrame(raw.frame, `widgets[${index}].frame`);
|
|
983
|
+
if (frame) widget.frame = frame;
|
|
332
984
|
if (!builtin) return withStoredFields(raw, widget);
|
|
333
985
|
return widget;
|
|
334
986
|
}
|
|
335
|
-
var OWNED_WIDGET_KEYS = /* @__PURE__ */ new Set(["id", "type", "widget", "layout", "datasource", "options"]);
|
|
987
|
+
var OWNED_WIDGET_KEYS = /* @__PURE__ */ new Set(["id", "type", "widget", "layout", "datasource", "options", "frame"]);
|
|
336
988
|
function withStoredFields(raw, parsed) {
|
|
337
989
|
const owned = parsed;
|
|
338
990
|
const out = {};
|
|
@@ -387,7 +1039,7 @@ function stringArrayAt(rec, key) {
|
|
|
387
1039
|
return Array.isArray(v) ? v.filter((m) => typeof m === "string") : [];
|
|
388
1040
|
}
|
|
389
1041
|
function parseLocationSelection(raw) {
|
|
390
|
-
if (!
|
|
1042
|
+
if (!isRecord(raw)) return void 0;
|
|
391
1043
|
return raw.series === "latest" ? { series: "latest" } : void 0;
|
|
392
1044
|
}
|
|
393
1045
|
function withLocation(selector, raw) {
|
|
@@ -395,7 +1047,7 @@ function withLocation(selector, raw) {
|
|
|
395
1047
|
return location ? { ...selector, location } : selector;
|
|
396
1048
|
}
|
|
397
1049
|
function parseDatasource(raw, index) {
|
|
398
|
-
if (!
|
|
1050
|
+
if (!isRecord(raw)) return void 0;
|
|
399
1051
|
const kind = raw.kind;
|
|
400
1052
|
if (typeof kind !== "string" || kind.length === 0) return void 0;
|
|
401
1053
|
if (kind === "unbound") {
|
|
@@ -410,7 +1062,7 @@ function parseDatasource(raw, index) {
|
|
|
410
1062
|
);
|
|
411
1063
|
}
|
|
412
1064
|
if (kind === "anchor") {
|
|
413
|
-
const anchorRec =
|
|
1065
|
+
const anchorRec = isRecord(raw.anchor) ? raw.anchor : {};
|
|
414
1066
|
const selector = withLocation(
|
|
415
1067
|
{
|
|
416
1068
|
kind: "anchor",
|
|
@@ -425,7 +1077,7 @@ function parseDatasource(raw, index) {
|
|
|
425
1077
|
},
|
|
426
1078
|
raw
|
|
427
1079
|
);
|
|
428
|
-
if (
|
|
1080
|
+
if (isRecord(raw.aggregation)) {
|
|
429
1081
|
selector.aggregation = raw.aggregation;
|
|
430
1082
|
}
|
|
431
1083
|
return selector;
|
|
@@ -439,36 +1091,88 @@ function parseDatasource(raw, index) {
|
|
|
439
1091
|
return raw;
|
|
440
1092
|
}
|
|
441
1093
|
function parseLayout(raw, index) {
|
|
442
|
-
if (!
|
|
1094
|
+
if (!isRecord(raw)) throw new DashboardDefinitionError(`widgets[${index}].layout is missing`);
|
|
443
1095
|
const layout = {};
|
|
444
|
-
for (const [bp,
|
|
445
|
-
if (
|
|
1096
|
+
for (const [bp, placement] of Object.entries(raw)) {
|
|
1097
|
+
if (bp === "__proto__") continue;
|
|
1098
|
+
if (isRecord(placement)) layout[bp] = parsePlacement(placement, `widgets[${index}].layout.${bp}`);
|
|
446
1099
|
}
|
|
447
|
-
if (!(BASE_BREAKPOINT
|
|
448
|
-
throw new DashboardDefinitionError(`widgets[${index}].layout has no '${BASE_BREAKPOINT}'
|
|
1100
|
+
if (!Object.prototype.hasOwnProperty.call(layout, BASE_BREAKPOINT)) {
|
|
1101
|
+
throw new DashboardDefinitionError(`widgets[${index}].layout has no '${BASE_BREAKPOINT}' placement`);
|
|
449
1102
|
}
|
|
450
1103
|
return layout;
|
|
451
1104
|
}
|
|
452
|
-
function
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
1105
|
+
function cellCount(rec, key, min, where) {
|
|
1106
|
+
const v = rec[key];
|
|
1107
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
1108
|
+
throw new DashboardDefinitionError(`${where}.${key} is ${JSON.stringify(v)}; expected a number of cells`);
|
|
1109
|
+
}
|
|
1110
|
+
return Math.max(min, Math.round(v));
|
|
1111
|
+
}
|
|
1112
|
+
function parsePlacement(rec, where) {
|
|
1113
|
+
const mode = rec.mode ?? "grid";
|
|
1114
|
+
if (mode === "grid") {
|
|
1115
|
+
return {
|
|
1116
|
+
mode: "grid",
|
|
1117
|
+
col: cellCount(rec, "col", 0, where),
|
|
1118
|
+
row: cellCount(rec, "row", 0, where),
|
|
1119
|
+
cols: cellCount(rec, "cols", 1, where),
|
|
1120
|
+
rows: cellCount(rec, "rows", 1, where)
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
if (mode === "relative") return parseRelative(rec, where);
|
|
1124
|
+
if (RESERVED_PLACEMENT_MODES.includes(mode)) {
|
|
1125
|
+
throw new DashboardDefinitionError(`${where}.mode '${String(mode)}' is reserved and not supported by this release`);
|
|
1126
|
+
}
|
|
1127
|
+
throw new DashboardDefinitionError(`${where}.mode is ${JSON.stringify(mode)}; expected 'grid' or 'relative'`);
|
|
1128
|
+
}
|
|
1129
|
+
function parseAnchor(raw, where) {
|
|
1130
|
+
if (PLACEMENT_ANCHORS.includes(raw)) return raw;
|
|
1131
|
+
throw new DashboardDefinitionError(`${where} is ${JSON.stringify(raw)}; expected one of ${PLACEMENT_ANCHORS.join(", ")}`);
|
|
1132
|
+
}
|
|
1133
|
+
function parseRelative(rec, where) {
|
|
1134
|
+
if (typeof rec.host !== "string" || rec.host.length === 0) {
|
|
1135
|
+
throw new DashboardDefinitionError(`${where}.host is ${JSON.stringify(rec.host)}; a relative widget names the widget it is placed against`);
|
|
1136
|
+
}
|
|
1137
|
+
const placement = {
|
|
1138
|
+
mode: "relative",
|
|
1139
|
+
host: rec.host,
|
|
1140
|
+
anchor: parseAnchor(rec.anchor, `${where}.anchor`),
|
|
1141
|
+
size: { fill: "host" }
|
|
461
1142
|
};
|
|
462
|
-
if (
|
|
463
|
-
|
|
1143
|
+
if (rec.self !== void 0) placement.self = parseAnchor(rec.self, `${where}.self`);
|
|
1144
|
+
if (rec.offset !== void 0) {
|
|
1145
|
+
const o = rec.offset;
|
|
1146
|
+
if (!isRecord(o) || !isFiniteNumber(o.x) || !isFiniteNumber(o.y)) {
|
|
1147
|
+
throw new DashboardDefinitionError(`${where}.offset is ${JSON.stringify(o)}; expected {x, y} in cells`);
|
|
1148
|
+
}
|
|
1149
|
+
placement.offset = { x: o.x, y: o.y };
|
|
464
1150
|
}
|
|
465
|
-
|
|
1151
|
+
const size = rec.size;
|
|
1152
|
+
if (isRecord(size) && size.fill === "host") {
|
|
1153
|
+
placement.size = { fill: "host" };
|
|
1154
|
+
} else if (isRecord(size)) {
|
|
1155
|
+
placement.size = { cols: cellCount(size, "cols", 1, `${where}.size`), rows: cellCount(size, "rows", 1, `${where}.size`) };
|
|
1156
|
+
} else {
|
|
1157
|
+
throw new DashboardDefinitionError(`${where}.size is ${JSON.stringify(size)}; expected {cols, rows} or {fill: 'host'}`);
|
|
1158
|
+
}
|
|
1159
|
+
if (rec.layer !== void 0) {
|
|
1160
|
+
if (!isFiniteNumber(rec.layer) || !Number.isInteger(rec.layer)) {
|
|
1161
|
+
throw new DashboardDefinitionError(`${where}.layer is ${JSON.stringify(rec.layer)}; expected a whole number`);
|
|
1162
|
+
}
|
|
1163
|
+
placement.layer = rec.layer;
|
|
1164
|
+
}
|
|
1165
|
+
return placement;
|
|
1166
|
+
}
|
|
1167
|
+
function isFiniteNumber(v) {
|
|
1168
|
+
return typeof v === "number" && Number.isFinite(v);
|
|
466
1169
|
}
|
|
467
1170
|
function storedDefinition(def) {
|
|
468
|
-
|
|
1171
|
+
const unmarked = withoutConversionMark(def);
|
|
1172
|
+
if (!unmarked.widgets.some((w) => w.type === UNKNOWN_WIDGET_TYPE)) return unmarked;
|
|
469
1173
|
return {
|
|
470
|
-
...
|
|
471
|
-
widgets:
|
|
1174
|
+
...unmarked,
|
|
1175
|
+
widgets: unmarked.widgets.map((w) => w.type === UNKNOWN_WIDGET_TYPE ? storedUnknownWidget(w) : w)
|
|
472
1176
|
};
|
|
473
1177
|
}
|
|
474
1178
|
function storedUnknownWidget(w) {
|
|
@@ -480,11 +1184,11 @@ function storedUnknownWidget(w) {
|
|
|
480
1184
|
return out;
|
|
481
1185
|
}
|
|
482
1186
|
function storedUnknownLayout(layout, rawLayout) {
|
|
483
|
-
const stored =
|
|
1187
|
+
const stored = isRecord(rawLayout) ? rawLayout : {};
|
|
484
1188
|
const out = {};
|
|
485
|
-
let changed = !
|
|
1189
|
+
let changed = !isRecord(rawLayout);
|
|
486
1190
|
for (const [bp, box] of Object.entries(stored)) {
|
|
487
|
-
if (!
|
|
1191
|
+
if (!isRecord(box)) {
|
|
488
1192
|
out[bp] = box;
|
|
489
1193
|
continue;
|
|
490
1194
|
}
|
|
@@ -493,48 +1197,52 @@ function storedUnknownLayout(layout, rawLayout) {
|
|
|
493
1197
|
changed = true;
|
|
494
1198
|
continue;
|
|
495
1199
|
}
|
|
496
|
-
if (
|
|
1200
|
+
if (samePlacement(parsePlacement(box, bp), current)) {
|
|
497
1201
|
out[bp] = box;
|
|
498
1202
|
} else {
|
|
499
|
-
out[bp] =
|
|
1203
|
+
out[bp] = movedPlacement(box, current);
|
|
500
1204
|
changed = true;
|
|
501
1205
|
}
|
|
502
1206
|
}
|
|
503
1207
|
for (const [bp, box] of Object.entries(layout)) {
|
|
504
|
-
if (Object.prototype.hasOwnProperty.call(stored, bp) &&
|
|
1208
|
+
if (Object.prototype.hasOwnProperty.call(stored, bp) && isRecord(stored[bp])) continue;
|
|
505
1209
|
out[bp] = box;
|
|
506
1210
|
changed = true;
|
|
507
1211
|
}
|
|
508
1212
|
return changed ? out : rawLayout;
|
|
509
1213
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
1214
|
+
var PLACEMENT_KEYS = /* @__PURE__ */ new Set([
|
|
1215
|
+
"mode",
|
|
1216
|
+
"col",
|
|
1217
|
+
"row",
|
|
1218
|
+
"cols",
|
|
1219
|
+
"rows",
|
|
1220
|
+
"host",
|
|
1221
|
+
"anchor",
|
|
1222
|
+
"self",
|
|
1223
|
+
"offset",
|
|
1224
|
+
"size",
|
|
1225
|
+
"layer"
|
|
1226
|
+
]);
|
|
1227
|
+
function movedPlacement(stored, current) {
|
|
1228
|
+
const out = { ...current };
|
|
1229
|
+
for (const [k, v] of Object.entries(stored)) if (!PLACEMENT_KEYS.has(k)) out[k] = v;
|
|
513
1230
|
return out;
|
|
514
1231
|
}
|
|
515
|
-
function
|
|
516
|
-
return
|
|
1232
|
+
function samePlacement(a, b) {
|
|
1233
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
517
1234
|
}
|
|
518
1235
|
function serializeDefinition(def) {
|
|
519
1236
|
return JSON.stringify(storedDefinition(def));
|
|
520
1237
|
}
|
|
1238
|
+
function withoutConversionMark(def) {
|
|
1239
|
+
if (def.convertedFrom === void 0 && def.convertedWindows === void 0) return def;
|
|
1240
|
+
const { convertedFrom: _mark, convertedWindows: _windows, ...rest } = def;
|
|
1241
|
+
return rest;
|
|
1242
|
+
}
|
|
521
1243
|
function isDirty(a, b) {
|
|
522
1244
|
return serializeDefinition(a) !== serializeDefinition(b);
|
|
523
1245
|
}
|
|
524
|
-
function resolveWidgetBox(layout, breakpoint) {
|
|
525
|
-
return layout[breakpoint] ?? layout[BASE_BREAKPOINT];
|
|
526
|
-
}
|
|
527
|
-
function activeBreakpoint(breakpoints, viewportWidth) {
|
|
528
|
-
let best = BASE_BREAKPOINT;
|
|
529
|
-
let bestWidth = -1;
|
|
530
|
-
for (const [name, minWidth] of Object.entries(breakpoints)) {
|
|
531
|
-
if (viewportWidth >= minWidth && minWidth > bestWidth) {
|
|
532
|
-
best = name;
|
|
533
|
-
bestWidth = minWidth;
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
return best;
|
|
537
|
-
}
|
|
538
1246
|
var idCounter = 0;
|
|
539
1247
|
function generateWidgetId() {
|
|
540
1248
|
const c = globalThis.crypto;
|
|
@@ -901,6 +1609,9 @@ var DEVICES_FOR_ANCHOR = `
|
|
|
901
1609
|
token
|
|
902
1610
|
}
|
|
903
1611
|
}
|
|
1612
|
+
pagination {
|
|
1613
|
+
totalRecords
|
|
1614
|
+
}
|
|
904
1615
|
}
|
|
905
1616
|
}
|
|
906
1617
|
`;
|
|
@@ -911,6 +1622,14 @@ var DEVICES_BY_TOKEN = `
|
|
|
911
1622
|
}
|
|
912
1623
|
}
|
|
913
1624
|
`;
|
|
1625
|
+
var DEVICE_NAMES = `
|
|
1626
|
+
query DashboardDeviceNames($tokens: [String!]!) {
|
|
1627
|
+
devicesByToken(tokens: $tokens) {
|
|
1628
|
+
token
|
|
1629
|
+
name
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
`;
|
|
914
1633
|
var LIST_DEVICES = `
|
|
915
1634
|
query DashboardListDevices($criteria: DeviceSearchCriteria!) {
|
|
916
1635
|
devices(criteria: $criteria) {
|
|
@@ -957,6 +1676,10 @@ var BUCKETED_MEASUREMENTS = `
|
|
|
957
1676
|
bucketStart
|
|
958
1677
|
name
|
|
959
1678
|
avg
|
|
1679
|
+
min
|
|
1680
|
+
max
|
|
1681
|
+
sum
|
|
1682
|
+
count
|
|
960
1683
|
}
|
|
961
1684
|
}
|
|
962
1685
|
`;
|
|
@@ -993,6 +1716,104 @@ function fetchKind(kind, criteria) {
|
|
|
993
1716
|
}
|
|
994
1717
|
}
|
|
995
1718
|
|
|
1719
|
+
// src/same-label.ts
|
|
1720
|
+
function sameLabel(a, b) {
|
|
1721
|
+
if (!a || !b) return false;
|
|
1722
|
+
const x = a.trim().toLowerCase();
|
|
1723
|
+
return x !== "" && x === b.trim().toLowerCase();
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// src/connection.ts
|
|
1727
|
+
var OFFLINE_AFTER_MS = 2e4;
|
|
1728
|
+
var STABLE_AFTER_MS = 1e4;
|
|
1729
|
+
var ConnectionTracker = class {
|
|
1730
|
+
constructor(offlineAfterMs = OFFLINE_AFTER_MS, stableAfterMs = STABLE_AFTER_MS) {
|
|
1731
|
+
this.offlineAfterMs = offlineAfterMs;
|
|
1732
|
+
this.stableAfterMs = stableAfterMs;
|
|
1733
|
+
this.down = /* @__PURE__ */ new Set();
|
|
1734
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
1735
|
+
this.escalated = false;
|
|
1736
|
+
this.browserOffline = false;
|
|
1737
|
+
this.current = "live";
|
|
1738
|
+
}
|
|
1739
|
+
get state() {
|
|
1740
|
+
return this.current;
|
|
1741
|
+
}
|
|
1742
|
+
register() {
|
|
1743
|
+
const handle = {
|
|
1744
|
+
connected: () => {
|
|
1745
|
+
this.down.delete(handle);
|
|
1746
|
+
this.settle();
|
|
1747
|
+
},
|
|
1748
|
+
closed: () => {
|
|
1749
|
+
this.down.add(handle);
|
|
1750
|
+
this.settle();
|
|
1751
|
+
},
|
|
1752
|
+
release: () => {
|
|
1753
|
+
this.down.delete(handle);
|
|
1754
|
+
this.settle();
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
return handle;
|
|
1758
|
+
}
|
|
1759
|
+
// Listeners are what keep the browser online/offline hooks attached, so a hub nobody is
|
|
1760
|
+
// watching holds no window listeners. Returns the unsubscribe.
|
|
1761
|
+
subscribe(listener) {
|
|
1762
|
+
this.listeners.add(listener);
|
|
1763
|
+
if (this.listeners.size === 1) this.attach();
|
|
1764
|
+
return () => {
|
|
1765
|
+
this.listeners.delete(listener);
|
|
1766
|
+
if (this.listeners.size === 0) this.release();
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
attach() {
|
|
1770
|
+
if (typeof window === "undefined") return;
|
|
1771
|
+
const sync = () => {
|
|
1772
|
+
this.browserOffline = typeof navigator !== "undefined" && navigator.onLine === false;
|
|
1773
|
+
this.settle();
|
|
1774
|
+
};
|
|
1775
|
+
window.addEventListener("offline", sync);
|
|
1776
|
+
window.addEventListener("online", sync);
|
|
1777
|
+
this.detach = () => {
|
|
1778
|
+
window.removeEventListener("offline", sync);
|
|
1779
|
+
window.removeEventListener("online", sync);
|
|
1780
|
+
};
|
|
1781
|
+
sync();
|
|
1782
|
+
}
|
|
1783
|
+
release() {
|
|
1784
|
+
this.detach?.();
|
|
1785
|
+
this.detach = void 0;
|
|
1786
|
+
this.browserOffline = false;
|
|
1787
|
+
this.settle();
|
|
1788
|
+
}
|
|
1789
|
+
settle() {
|
|
1790
|
+
if (this.down.size === 0) {
|
|
1791
|
+
if ((this.escalate || this.escalated) && !this.recover) {
|
|
1792
|
+
this.recover = setTimeout(() => {
|
|
1793
|
+
this.recover = void 0;
|
|
1794
|
+
if (this.escalate) clearTimeout(this.escalate);
|
|
1795
|
+
this.escalate = void 0;
|
|
1796
|
+
this.escalated = false;
|
|
1797
|
+
}, this.stableAfterMs);
|
|
1798
|
+
}
|
|
1799
|
+
} else {
|
|
1800
|
+
if (this.recover) clearTimeout(this.recover);
|
|
1801
|
+
this.recover = void 0;
|
|
1802
|
+
}
|
|
1803
|
+
if (this.down.size > 0 && !this.escalate && !this.escalated) {
|
|
1804
|
+
this.escalate = setTimeout(() => {
|
|
1805
|
+
this.escalate = void 0;
|
|
1806
|
+
this.escalated = true;
|
|
1807
|
+
this.settle();
|
|
1808
|
+
}, this.offlineAfterMs);
|
|
1809
|
+
}
|
|
1810
|
+
const next = this.browserOffline ? "offline" : this.down.size === 0 ? "live" : this.escalated ? "offline" : "reconnecting";
|
|
1811
|
+
if (next === this.current) return;
|
|
1812
|
+
this.current = next;
|
|
1813
|
+
for (const listener of [...this.listeners]) listener();
|
|
1814
|
+
}
|
|
1815
|
+
};
|
|
1816
|
+
|
|
996
1817
|
// src/hub.ts
|
|
997
1818
|
import { gql as gql2, isForbiddenError, subscribe } from "@devicechain/client";
|
|
998
1819
|
|
|
@@ -1109,6 +1930,34 @@ var LATEST_LOCATIONS_QUERY = `
|
|
|
1109
1930
|
}
|
|
1110
1931
|
`;
|
|
1111
1932
|
|
|
1933
|
+
// src/members.ts
|
|
1934
|
+
var MEMBER_SEED_CONCURRENCY = 6;
|
|
1935
|
+
var SEED_CANCELLED = /* @__PURE__ */ Symbol("member seed cancelled");
|
|
1936
|
+
function createLimiter(max) {
|
|
1937
|
+
let active = 0;
|
|
1938
|
+
const queue = [];
|
|
1939
|
+
const next = () => {
|
|
1940
|
+
while (active < max && queue.length > 0) queue.shift()();
|
|
1941
|
+
};
|
|
1942
|
+
return function run(task, cancelled = () => false) {
|
|
1943
|
+
return new Promise((resolve, reject) => {
|
|
1944
|
+
const start = () => {
|
|
1945
|
+
if (cancelled()) {
|
|
1946
|
+
reject(SEED_CANCELLED);
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
active += 1;
|
|
1950
|
+
task().then(resolve, reject).finally(() => {
|
|
1951
|
+
active -= 1;
|
|
1952
|
+
next();
|
|
1953
|
+
});
|
|
1954
|
+
};
|
|
1955
|
+
queue.push(start);
|
|
1956
|
+
next();
|
|
1957
|
+
});
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1112
1961
|
// src/internal/measurement-doc.ts
|
|
1113
1962
|
var MEASUREMENT_STREAM = `
|
|
1114
1963
|
subscription MeasurementStream($deviceToken: String, $name: String) {
|
|
@@ -1161,9 +2010,32 @@ var DashboardHub = class {
|
|
|
1161
2010
|
// commandDisposers — the location channel holds a poll per subscription, not a shared
|
|
1162
2011
|
// device stream), so disposeAll() can tear down every map widget's poll.
|
|
1163
2012
|
this.locationDisposers = /* @__PURE__ */ new Set();
|
|
2013
|
+
// Which of this hub's sockets are down, folded into one state a header can show.
|
|
2014
|
+
this.connection = new ConnectionTracker();
|
|
2015
|
+
// Streams evicted by an upstream error that still have subscribers: out of `streams`, but
|
|
2016
|
+
// still holding their connection handle (see ensureStream), so disposeAll() can reach them.
|
|
2017
|
+
this.deadStreams = /* @__PURE__ */ new Set();
|
|
2018
|
+
// The hub-wide cap on in-flight member seed reads, and the reads currently in flight (so a second
|
|
2019
|
+
// widget asking the same question of the same device shares one request).
|
|
2020
|
+
this.seedLimiter = createLimiter(MEMBER_SEED_CONCURRENCY);
|
|
2021
|
+
this.seedReads = /* @__PURE__ */ new Map();
|
|
2022
|
+
// bucketMeasurements reads the buckets of every (device, measurement) pair over a window. Widgets
|
|
2023
|
+
// that ask the same question while a read is in flight share it: stat cards on one measurement
|
|
2024
|
+
// and span read on the same minute boundaries, so a board of them costs one read per question.
|
|
2025
|
+
this.bucketReads = /* @__PURE__ */ new Map();
|
|
1164
2026
|
this.resolver = config.resolver;
|
|
1165
2027
|
this.authorities = new Set(config.authorities ?? []);
|
|
1166
2028
|
}
|
|
2029
|
+
// The hub's live-connection state: `live`, `reconnecting` (a socket dropped and is being
|
|
2030
|
+
// retried) or `offline` (down past the point a retry is a blip, or the browser is offline).
|
|
2031
|
+
// A hub with nothing subscribed has nothing to lose and reads `live`. Pair with
|
|
2032
|
+
// onConnectionState (a subscribe/getSnapshot pair, so it drops into useSyncExternalStore).
|
|
2033
|
+
get connectionState() {
|
|
2034
|
+
return this.connection.state;
|
|
2035
|
+
}
|
|
2036
|
+
onConnectionState(listener) {
|
|
2037
|
+
return this.connection.subscribe(listener);
|
|
2038
|
+
}
|
|
1167
2039
|
// subscribeWidget binds a widget's datasource to a sink and returns a disposer.
|
|
1168
2040
|
// Selector resolution is async (anchor→devices); the disposer is returned
|
|
1169
2041
|
// synchronously and cancels a still-pending resolution, so tearing a widget down
|
|
@@ -1185,6 +2057,99 @@ var DashboardHub = class {
|
|
|
1185
2057
|
});
|
|
1186
2058
|
return dispose;
|
|
1187
2059
|
}
|
|
2060
|
+
// subscribeMembers: the roster, then the live samples of every member. Resolution is async, so
|
|
2061
|
+
// the disposer is returned at once and cancels a still-pending resolution (a widget torn down
|
|
2062
|
+
// before its roster arrives never attaches a leaked subscriber). Each member rides the same
|
|
2063
|
+
// ref-counted device stream every other widget on it uses.
|
|
2064
|
+
subscribeMembers(datasource, sink) {
|
|
2065
|
+
let disposed = false;
|
|
2066
|
+
const detachers = [];
|
|
2067
|
+
this.resolveRoster(datasource).then(({ roster, names }) => {
|
|
2068
|
+
if (disposed) return;
|
|
2069
|
+
sink.roster(roster);
|
|
2070
|
+
for (const member of roster.members) {
|
|
2071
|
+
detachers.push(
|
|
2072
|
+
this.attach(member.token, names, { next: (sample) => sink.next(sample), error: (err) => sink.error?.(err) })
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
}).catch((err) => {
|
|
2076
|
+
if (disposed) return;
|
|
2077
|
+
if (isForbiddenError(err)) sink.forbidden?.();
|
|
2078
|
+
else sink.error?.(err);
|
|
2079
|
+
});
|
|
2080
|
+
return () => {
|
|
2081
|
+
disposed = true;
|
|
2082
|
+
for (const detach of detachers.splice(0)) detach();
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
// One resolution of a selector into a roster. An anchor goes through the resolver's member read
|
|
2086
|
+
// when it has one (names and the true size); everything else is the tokens resolveDevices yields.
|
|
2087
|
+
async resolveRoster(datasource) {
|
|
2088
|
+
const names = new Set(datasource.measurements);
|
|
2089
|
+
if (datasource.kind === "anchor" && this.resolver.anchorMembers) {
|
|
2090
|
+
const { members: members2, total } = await this.resolver.anchorMembers(datasource.anchor);
|
|
2091
|
+
return { roster: { members: members2, total, unbound: false }, names };
|
|
2092
|
+
}
|
|
2093
|
+
const groups = await this.resolveDevices(datasource);
|
|
2094
|
+
const members = groups.map((g) => ({ token: g.deviceToken, name: null }));
|
|
2095
|
+
return { roster: { members, total: members.length, unbound: datasource.kind === "unbound" }, names };
|
|
2096
|
+
}
|
|
2097
|
+
async readMemberSeeds(deviceTokens, plan, onSeed, cancelled = () => false) {
|
|
2098
|
+
await Promise.all(
|
|
2099
|
+
deviceTokens.map(async (token) => {
|
|
2100
|
+
let seed;
|
|
2101
|
+
try {
|
|
2102
|
+
seed = await plan.seed(token, (criteria) => this.memberBuckets(token, criteria, cancelled));
|
|
2103
|
+
} catch (err) {
|
|
2104
|
+
if (cancelled() || err === SEED_CANCELLED) return;
|
|
2105
|
+
onSeed(token, null, isForbiddenError(err) ? "forbidden" : "failed");
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
if (!cancelled()) onSeed(token, seed);
|
|
2109
|
+
})
|
|
2110
|
+
);
|
|
2111
|
+
}
|
|
2112
|
+
async memberBuckets(token, criteria, cancelled) {
|
|
2113
|
+
const key = JSON.stringify([token, criteria]);
|
|
2114
|
+
for (; ; ) {
|
|
2115
|
+
let read = this.seedReads.get(key);
|
|
2116
|
+
if (!read) {
|
|
2117
|
+
read = this.seedLimiter(
|
|
2118
|
+
() => gql2(EVENT_AREA, BUCKETED_MEASUREMENTS, { criteria: { deviceToken: token, ...criteria } }).then((r) => r.bucketedMeasurements),
|
|
2119
|
+
cancelled
|
|
2120
|
+
);
|
|
2121
|
+
this.seedReads.set(key, read);
|
|
2122
|
+
const forget = () => this.seedReads.delete(key);
|
|
2123
|
+
read.then(forget, forget);
|
|
2124
|
+
}
|
|
2125
|
+
try {
|
|
2126
|
+
return await read;
|
|
2127
|
+
} catch (err) {
|
|
2128
|
+
if (err === SEED_CANCELLED && !cancelled()) continue;
|
|
2129
|
+
throw err;
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
bucketMeasurements(datasource, window2) {
|
|
2134
|
+
const key = JSON.stringify([datasource, window2]);
|
|
2135
|
+
let read = this.bucketReads.get(key);
|
|
2136
|
+
if (!read) {
|
|
2137
|
+
read = this.readBuckets(datasource, window2);
|
|
2138
|
+
this.bucketReads.set(key, read);
|
|
2139
|
+
const forget = () => this.bucketReads.delete(key);
|
|
2140
|
+
read.then(forget, forget);
|
|
2141
|
+
}
|
|
2142
|
+
return read;
|
|
2143
|
+
}
|
|
2144
|
+
async readBuckets(datasource, window2) {
|
|
2145
|
+
const groups = await this.resolveDevices(datasource);
|
|
2146
|
+
const reads = groups.flatMap(
|
|
2147
|
+
(g) => (g.names.size ? [...g.names] : [void 0]).map(
|
|
2148
|
+
(name) => gql2(EVENT_AREA, BUCKETED_MEASUREMENTS, { criteria: { deviceToken: g.deviceToken, name, ...window2 } })
|
|
2149
|
+
)
|
|
2150
|
+
);
|
|
2151
|
+
return (await Promise.all(reads)).flatMap((r) => r.bucketedMeasurements);
|
|
2152
|
+
}
|
|
1188
2153
|
// subscribeAlarms binds an alarm widget's scope+filters to a sink and returns a
|
|
1189
2154
|
// disposer. Unlike the measurement channel it is NOT multiplexed — alarm widgets are
|
|
1190
2155
|
// few, and each carries its own filter — so every subscription opens its own trigger
|
|
@@ -1227,13 +2192,20 @@ var DashboardHub = class {
|
|
|
1227
2192
|
if (debounce) clearTimeout(debounce);
|
|
1228
2193
|
debounce = setTimeout(() => reconcile(scope.tokens, scope.tenantWide), ALARM_RECONCILE_DEBOUNCE_MS);
|
|
1229
2194
|
};
|
|
2195
|
+
const handle = this.connection.register();
|
|
1230
2196
|
const adapter = {
|
|
1231
2197
|
next: () => trigger(),
|
|
1232
2198
|
connected: (wasRetry) => {
|
|
2199
|
+
handle.connected();
|
|
1233
2200
|
if (wasRetry) reconcile(scope.tokens, scope.tenantWide);
|
|
1234
|
-
}
|
|
2201
|
+
},
|
|
2202
|
+
closed: () => handle.closed()
|
|
2203
|
+
};
|
|
2204
|
+
const closeSocket = subscribe(DEVICE_AREA, ALARM_STREAM, {}, adapter);
|
|
2205
|
+
unsubscribe = () => {
|
|
2206
|
+
handle.release();
|
|
2207
|
+
closeSocket();
|
|
1235
2208
|
};
|
|
1236
|
-
unsubscribe = subscribe(DEVICE_AREA, ALARM_STREAM, {}, adapter);
|
|
1237
2209
|
poll = setInterval(() => reconcile(scope.tokens, scope.tenantWide), ALARM_POLL_MS);
|
|
1238
2210
|
reconciler = () => reconcile(scope.tokens, scope.tenantWide);
|
|
1239
2211
|
this.alarmReconcilers.add(reconciler);
|
|
@@ -1483,6 +2455,8 @@ var DashboardHub = class {
|
|
|
1483
2455
|
disposeAll() {
|
|
1484
2456
|
for (const stream of this.streams.values()) stream.unsubscribe();
|
|
1485
2457
|
this.streams.clear();
|
|
2458
|
+
for (const stream of this.deadStreams) stream.unsubscribe();
|
|
2459
|
+
this.deadStreams.clear();
|
|
1486
2460
|
for (const dispose of [...this.alarmDisposers]) dispose();
|
|
1487
2461
|
this.alarmDisposers.clear();
|
|
1488
2462
|
for (const dispose of [...this.commandDisposers]) dispose();
|
|
@@ -1566,9 +2540,10 @@ var DashboardHub = class {
|
|
|
1566
2540
|
stream.subscribers.add(subscriber);
|
|
1567
2541
|
return () => {
|
|
1568
2542
|
if (!stream.subscribers.delete(subscriber)) return;
|
|
1569
|
-
if (stream.subscribers.size === 0
|
|
2543
|
+
if (stream.subscribers.size === 0) {
|
|
1570
2544
|
stream.unsubscribe();
|
|
1571
|
-
this.
|
|
2545
|
+
this.deadStreams.delete(stream);
|
|
2546
|
+
if (this.streams.get(deviceToken) === stream) this.streams.delete(deviceToken);
|
|
1572
2547
|
}
|
|
1573
2548
|
};
|
|
1574
2549
|
}
|
|
@@ -1578,16 +2553,63 @@ var DashboardHub = class {
|
|
|
1578
2553
|
const stream = { subscribers: /* @__PURE__ */ new Set(), unsubscribe: () => {
|
|
1579
2554
|
} };
|
|
1580
2555
|
this.streams.set(deviceToken, stream);
|
|
2556
|
+
const handle = this.connection.register();
|
|
2557
|
+
let closeSocket;
|
|
2558
|
+
let operationOpen = true;
|
|
2559
|
+
let ended = 0;
|
|
2560
|
+
let up = true;
|
|
2561
|
+
let reopen;
|
|
2562
|
+
const closeOperation = () => {
|
|
2563
|
+
if (!operationOpen) return;
|
|
2564
|
+
operationOpen = false;
|
|
2565
|
+
clearTimeout(reopen);
|
|
2566
|
+
closeSocket?.();
|
|
2567
|
+
};
|
|
2568
|
+
const open = () => {
|
|
2569
|
+
closeSocket = subscribe(EVENT_AREA, MEASUREMENT_STREAM, variables, adapter);
|
|
2570
|
+
};
|
|
1581
2571
|
const adapter = {
|
|
1582
|
-
next: (data) =>
|
|
2572
|
+
next: (data) => {
|
|
2573
|
+
ended = 0;
|
|
2574
|
+
this.fanout(deviceToken, data.measurementStream);
|
|
2575
|
+
},
|
|
2576
|
+
connected: () => {
|
|
2577
|
+
up = true;
|
|
2578
|
+
handle.connected();
|
|
2579
|
+
},
|
|
2580
|
+
closed: () => {
|
|
2581
|
+
up = false;
|
|
2582
|
+
handle.closed();
|
|
2583
|
+
},
|
|
2584
|
+
// 🔴 The server ended this operation on a socket that is still up: its live source
|
|
2585
|
+
// closed, or could not be opened (a resolver error reaches the client as an error-only
|
|
2586
|
+
// `next`, which carries no data, and then this). Left alone, the stream would sit here
|
|
2587
|
+
// receiving nothing while the board read `live` -- a widget frozen until someone reloads,
|
|
2588
|
+
// one stream at a time. Open it again, after a doubling wait capped at 30 s, for as long
|
|
2589
|
+
// as anything shows it. Two other completes are not the server's and are never re-opened:
|
|
2590
|
+
// a widget's own unsubscribe (closeOperation has marked it), and the client being disposed
|
|
2591
|
+
// on sign-out, which closes the socket first (re-opening would bring back the socket the
|
|
2592
|
+
// sign-out exists to drop).
|
|
2593
|
+
complete: () => {
|
|
2594
|
+
if (!operationOpen || !up) return;
|
|
2595
|
+
closeSocket?.();
|
|
2596
|
+
reopen = setTimeout(open, Math.min(1e3 * 2 ** ended++, 3e4));
|
|
2597
|
+
},
|
|
1583
2598
|
error: (err) => {
|
|
1584
2599
|
if (this.streams.get(deviceToken) === stream) this.streams.delete(deviceToken);
|
|
1585
|
-
|
|
2600
|
+
closeOperation();
|
|
2601
|
+
if (stream.subscribers.size > 0) this.deadStreams.add(stream);
|
|
2602
|
+
else handle.release();
|
|
1586
2603
|
for (const subscriber of stream.subscribers) subscriber.sink.error?.(err);
|
|
1587
2604
|
}
|
|
1588
2605
|
};
|
|
1589
2606
|
const variables = { deviceToken, name: null };
|
|
1590
|
-
|
|
2607
|
+
open();
|
|
2608
|
+
if (!operationOpen) closeSocket?.();
|
|
2609
|
+
stream.unsubscribe = () => {
|
|
2610
|
+
handle.release();
|
|
2611
|
+
closeOperation();
|
|
2612
|
+
};
|
|
1591
2613
|
return stream;
|
|
1592
2614
|
}
|
|
1593
2615
|
fanout(deviceToken, sample) {
|
|
@@ -1602,11 +2624,7 @@ var DashboardHub = class {
|
|
|
1602
2624
|
};
|
|
1603
2625
|
|
|
1604
2626
|
// src/synthetic.ts
|
|
1605
|
-
var SYNTHETIC_GENERATORS = [
|
|
1606
|
-
{ value: "sine", label: "Sine wave" },
|
|
1607
|
-
{ value: "ramp", label: "Ramp" },
|
|
1608
|
-
{ value: "random-walk", label: "Random walk" }
|
|
1609
|
-
];
|
|
2627
|
+
var SYNTHETIC_GENERATORS = ["sine", "ramp", "random-walk"];
|
|
1610
2628
|
var DEFAULT_NAME = "value";
|
|
1611
2629
|
var SYNTHETIC_ALARMS = [
|
|
1612
2630
|
{ severity: "CRITICAL", state: "ACTIVE", acknowledged: false, alarmKey: "over-temperature", metricKey: "temperature", lastValue: 87.4, originatorToken: "thermostat-01" },
|
|
@@ -1825,8 +2843,71 @@ var SyntheticDataSource = class {
|
|
|
1825
2843
|
}
|
|
1826
2844
|
async clearAlarm() {
|
|
1827
2845
|
}
|
|
1828
|
-
async sendCommand() {
|
|
1829
|
-
return { status: "sent", token: "syn-dispatch" };
|
|
2846
|
+
async sendCommand() {
|
|
2847
|
+
return { status: "sent", token: "syn-dispatch" };
|
|
2848
|
+
}
|
|
2849
|
+
// bucketMeasurements answers with ONE bucket per measurement spanning the window, folded from a
|
|
2850
|
+
// synthetic reading per interval, so a preview stat reads something the waveform actually did
|
|
2851
|
+
// (a minimum and maximum inside [min, max]).
|
|
2852
|
+
async bucketMeasurements(datasource, window2) {
|
|
2853
|
+
const names = datasource.measurements.length > 0 ? datasource.measurements : [DEFAULT_NAME];
|
|
2854
|
+
const from = Date.parse(window2.startTime);
|
|
2855
|
+
const to = Date.parse(window2.endTime);
|
|
2856
|
+
return names.map((name) => {
|
|
2857
|
+
const walk = /* @__PURE__ */ new Map();
|
|
2858
|
+
const values = Array.from({ length: 120 }, (_, i) => this.valueFor(name, from + (to - from) * i / 119, walk));
|
|
2859
|
+
const sum = values.reduce((a, v) => a + v, 0);
|
|
2860
|
+
return {
|
|
2861
|
+
bucketStart: window2.startTime,
|
|
2862
|
+
name,
|
|
2863
|
+
avg: sum / values.length,
|
|
2864
|
+
min: Math.min(...values),
|
|
2865
|
+
max: Math.max(...values),
|
|
2866
|
+
sum,
|
|
2867
|
+
count: values.length
|
|
2868
|
+
};
|
|
2869
|
+
});
|
|
2870
|
+
}
|
|
2871
|
+
// A preview fleet: a handful of fabricated devices, each out of phase with the others (the
|
|
2872
|
+
// series is keyed by device and name together), and one that never reports its second
|
|
2873
|
+
// measurement, so a table previews its "does not report this" cell. An unbound slot has no
|
|
2874
|
+
// members, as in the live hub.
|
|
2875
|
+
previewMembers(datasource) {
|
|
2876
|
+
if (datasource.kind === "unbound") return [];
|
|
2877
|
+
return Array.from({ length: 5 }, (_, i) => ({ token: `synthetic-${i + 1}`, name: `Unit ${i + 1}` }));
|
|
2878
|
+
}
|
|
2879
|
+
subscribeMembers(datasource, sink) {
|
|
2880
|
+
const members = this.previewMembers(datasource);
|
|
2881
|
+
const names = datasource.measurements.length > 0 ? datasource.measurements : [DEFAULT_NAME];
|
|
2882
|
+
const walk = /* @__PURE__ */ new Map();
|
|
2883
|
+
let seq = 0;
|
|
2884
|
+
const emit = (token, tMs) => {
|
|
2885
|
+
for (const name of token === "synthetic-4" ? names.slice(0, 1) : names) {
|
|
2886
|
+
sink.next({
|
|
2887
|
+
id: `syn-m-${seq++}`,
|
|
2888
|
+
deviceToken: token,
|
|
2889
|
+
eventType: 0,
|
|
2890
|
+
occurredTime: new Date(tMs).toISOString(),
|
|
2891
|
+
name,
|
|
2892
|
+
value: this.valueFor(`${token}/${name}`, tMs, walk),
|
|
2893
|
+
classifier: null
|
|
2894
|
+
});
|
|
2895
|
+
}
|
|
2896
|
+
};
|
|
2897
|
+
sink.roster({ members, total: members.length, unbound: datasource.kind === "unbound" });
|
|
2898
|
+
for (const m of members) emit(m.token, Date.now());
|
|
2899
|
+
const timer = setInterval(() => {
|
|
2900
|
+
for (const m of members) emit(m.token, Date.now());
|
|
2901
|
+
}, this.intervalMs);
|
|
2902
|
+
this.timers.add(timer);
|
|
2903
|
+
return () => {
|
|
2904
|
+
if (this.timers.delete(timer)) clearInterval(timer);
|
|
2905
|
+
};
|
|
2906
|
+
}
|
|
2907
|
+
// A preview has no history to read: its members report the moment they are subscribed, so the
|
|
2908
|
+
// seed settles empty and the live feed alone fills the cells.
|
|
2909
|
+
async readMemberSeeds(deviceTokens, _plan, onSeed) {
|
|
2910
|
+
for (const token of deviceTokens) onSeed(token, { latest: {}, observed: [] });
|
|
1830
2911
|
}
|
|
1831
2912
|
// disposeAll stops every live stream (e.g. when preview is turned off). Individual
|
|
1832
2913
|
// widget disposers already clear their own timer; this is the belt-and-braces
|
|
@@ -1907,7 +2988,7 @@ function bool(v, path) {
|
|
|
1907
2988
|
if (typeof v !== "boolean") throw new RecordingFormatError(path, "expected a boolean");
|
|
1908
2989
|
return v;
|
|
1909
2990
|
}
|
|
1910
|
-
function
|
|
2991
|
+
function num2(v, path) {
|
|
1911
2992
|
if (typeof v !== "number" || !Number.isFinite(v)) throw new RecordingFormatError(path, "expected a finite number");
|
|
1912
2993
|
return v;
|
|
1913
2994
|
}
|
|
@@ -1915,7 +2996,7 @@ function arr(v, path) {
|
|
|
1915
2996
|
if (!Array.isArray(v)) throw new RecordingFormatError(path, "expected an array");
|
|
1916
2997
|
return v;
|
|
1917
2998
|
}
|
|
1918
|
-
function
|
|
2999
|
+
function oneOf2(v, path, allowed) {
|
|
1919
3000
|
if (typeof v !== "string" || !allowed.includes(v)) {
|
|
1920
3001
|
throw new RecordingFormatError(path, `expected one of ${allowed.join(", ")}`);
|
|
1921
3002
|
}
|
|
@@ -1961,7 +3042,7 @@ function column(v, path, item, length) {
|
|
|
1961
3042
|
return a.map((x, i) => item(x, `${path}[${i}]`));
|
|
1962
3043
|
}
|
|
1963
3044
|
function nonNegative(v, path) {
|
|
1964
|
-
const n =
|
|
3045
|
+
const n = num2(v, path);
|
|
1965
3046
|
if (n < 0) throw new RecordingFormatError(path, "must not be negative");
|
|
1966
3047
|
return n;
|
|
1967
3048
|
}
|
|
@@ -2001,7 +3082,7 @@ function parseBoardRecording(input) {
|
|
|
2001
3082
|
`unsupported version ${JSON.stringify(root.formatVersion)}; this reader understands ${RECORDING_FORMAT_VERSION}`
|
|
2002
3083
|
);
|
|
2003
3084
|
}
|
|
2004
|
-
const durationMs =
|
|
3085
|
+
const durationMs = num2(root.durationMs, "$.durationMs");
|
|
2005
3086
|
if (durationMs <= 0) throw new RecordingFormatError("$.durationMs", "must be positive");
|
|
2006
3087
|
const b = strict(root.build, "$.build", ["gitSha", "sdkCommit"]);
|
|
2007
3088
|
const build = {
|
|
@@ -2010,7 +3091,7 @@ function parseBoardRecording(input) {
|
|
|
2010
3091
|
};
|
|
2011
3092
|
const clock = column(root.clock, "$.clock", (x, p) => {
|
|
2012
3093
|
const o = strict(x, p, ["fromMs", "mode", "scale"]);
|
|
2013
|
-
const scale =
|
|
3094
|
+
const scale = num2(o.scale, `${p}.scale`);
|
|
2014
3095
|
if (scale <= 0) throw new RecordingFormatError(`${p}.scale`, "must be positive");
|
|
2015
3096
|
return { fromMs: nonNegative(o.fromMs, `${p}.fromMs`), mode: str(o.mode, `${p}.mode`), scale };
|
|
2016
3097
|
});
|
|
@@ -2055,7 +3136,7 @@ function parseBoardRecording(input) {
|
|
|
2055
3136
|
sourceHashes[key] = matching(sh[key], `$.sourceHashes.${key}`, HEX64, "a lowercase hex SHA-256");
|
|
2056
3137
|
}
|
|
2057
3138
|
const cv = strict(root.converter, "$.converter", ["version"]);
|
|
2058
|
-
const converter = { version:
|
|
3139
|
+
const converter = { version: num2(cv.version, "$.converter.version") };
|
|
2059
3140
|
let excerpt;
|
|
2060
3141
|
if (root.excerpt !== void 0) {
|
|
2061
3142
|
const e = strict(root.excerpt, "$.excerpt", ["fromMs", "toMs"]);
|
|
@@ -2068,7 +3149,7 @@ function parseBoardRecording(input) {
|
|
|
2068
3149
|
const hi = excerpt ? excerpt.toMs : durationMs;
|
|
2069
3150
|
const chapters = column(root.chapters, "$.chapters", (x, p) => {
|
|
2070
3151
|
const o = strict(x, p, ["tMs", "name", "note"]);
|
|
2071
|
-
const tMs =
|
|
3152
|
+
const tMs = num2(o.tMs, `${p}.tMs`);
|
|
2072
3153
|
if (tMs < 0 || tMs > durationMs) throw new RecordingFormatError(`${p}.tMs`, "is outside the run");
|
|
2073
3154
|
if (excerpt && (tMs < lo || tMs > hi)) throw new RecordingFormatError(`${p}.tMs`, "is outside the excerpt window");
|
|
2074
3155
|
return { tMs, name: str(o.name, `${p}.name`), note: str(o.note, `${p}.note`) };
|
|
@@ -2077,7 +3158,7 @@ function parseBoardRecording(input) {
|
|
|
2077
3158
|
if (i > 0 && c.tMs < chapters[i - 1].tMs) throw new RecordingFormatError(`$.chapters[${i}].tMs`, "chapters must not decrease in time");
|
|
2078
3159
|
});
|
|
2079
3160
|
const m = strict(root.measurements, "$.measurements", ["d", "n", "t", "v", "s"]);
|
|
2080
|
-
const t = column(m.t, "$.measurements.t",
|
|
3161
|
+
const t = column(m.t, "$.measurements.t", num2);
|
|
2081
3162
|
let prev = lo;
|
|
2082
3163
|
t.forEach((x, i) => {
|
|
2083
3164
|
if (x < lo || x > hi) throw new RecordingFormatError(`$.measurements.t[${i}]`, `${x} is outside [${lo}, ${hi}]`);
|
|
@@ -2085,7 +3166,7 @@ function parseBoardRecording(input) {
|
|
|
2085
3166
|
prev = x;
|
|
2086
3167
|
});
|
|
2087
3168
|
const index = (name, limit) => (x, p) => {
|
|
2088
|
-
const n =
|
|
3169
|
+
const n = num2(x, p);
|
|
2089
3170
|
if (!Number.isInteger(n) || n < 0 || n >= limit) throw new RecordingFormatError(p, `${name} index out of range`);
|
|
2090
3171
|
return n;
|
|
2091
3172
|
};
|
|
@@ -2093,7 +3174,7 @@ function parseBoardRecording(input) {
|
|
|
2093
3174
|
d: column(m.d, "$.measurements.d", index("device", devices.length), t.length),
|
|
2094
3175
|
n: column(m.n, "$.measurements.n", index("measurement", channels.measurements.length), t.length),
|
|
2095
3176
|
t,
|
|
2096
|
-
v: column(m.v, "$.measurements.v",
|
|
3177
|
+
v: column(m.v, "$.measurements.v", num2, t.length),
|
|
2097
3178
|
s: column(
|
|
2098
3179
|
m.s,
|
|
2099
3180
|
"$.measurements.s",
|
|
@@ -2115,8 +3196,8 @@ function parseBoardRecording(input) {
|
|
|
2115
3196
|
dev,
|
|
2116
3197
|
key: str(o.key, `${p}.key`),
|
|
2117
3198
|
metric: str(o.metric, `${p}.metric`),
|
|
2118
|
-
state:
|
|
2119
|
-
sev:
|
|
3199
|
+
state: oneOf2(o.state, `${p}.state`, ALARM_STATES),
|
|
3200
|
+
sev: oneOf2(o.sev, `${p}.sev`, ALARM_SEVERITIES),
|
|
2120
3201
|
occ: instant(o.occ, `${p}.occ`)
|
|
2121
3202
|
};
|
|
2122
3203
|
if (o.ack === true) out.ack = true;
|
|
@@ -2139,7 +3220,7 @@ function parseBoardRecording(input) {
|
|
|
2139
3220
|
if (total !== alarms.length) {
|
|
2140
3221
|
throw new RecordingFormatError(`${p}.total`, `is ${total} but ${alarms.length} alarms are listed (truncated snapshot)`);
|
|
2141
3222
|
}
|
|
2142
|
-
const tMs = checkTime(
|
|
3223
|
+
const tMs = checkTime(num2(o.tMs, `${p}.tMs`), `${p}.tMs`);
|
|
2143
3224
|
const requestedAtMs = nonNegative(o.requestedAtMs, `${p}.requestedAtMs`);
|
|
2144
3225
|
if (!Number.isInteger(requestedAtMs)) throw new RecordingFormatError(`${p}.requestedAtMs`, "expected an integer");
|
|
2145
3226
|
if (requestedAtMs > tMs) throw new RecordingFormatError(`${p}.requestedAtMs`, "a query cannot be sent after its answer");
|
|
@@ -2147,7 +3228,7 @@ function parseBoardRecording(input) {
|
|
|
2147
3228
|
});
|
|
2148
3229
|
const events = column(al.events, "$.alarms.events", (x, p) => {
|
|
2149
3230
|
const o = strict(x, p, [...ALARM_KEYS, "tMs"], ["ack"]);
|
|
2150
|
-
return { ...alarm(o, p), tMs: checkTime(
|
|
3231
|
+
return { ...alarm(o, p), tMs: checkTime(num2(o.tMs, `${p}.tMs`), `${p}.tMs`) };
|
|
2151
3232
|
});
|
|
2152
3233
|
for (const [name, list] of [["snapshots", snapshots], ["events", events]]) {
|
|
2153
3234
|
list.forEach((e, i) => {
|
|
@@ -2442,6 +3523,11 @@ var RecordedDataSource = class {
|
|
|
2442
3523
|
get isStale() {
|
|
2443
3524
|
return this.stale;
|
|
2444
3525
|
}
|
|
3526
|
+
// The recorded instant the view is at: samples are dated by the run, so a time span is measured
|
|
3527
|
+
// from here, not from the wall clock.
|
|
3528
|
+
now() {
|
|
3529
|
+
return this.startMs + this.cursor;
|
|
3530
|
+
}
|
|
2445
3531
|
onClock(ev) {
|
|
2446
3532
|
if (this.stale) return;
|
|
2447
3533
|
if (ev.reason === "seek") {
|
|
@@ -2571,6 +3657,62 @@ var RecordedDataSource = class {
|
|
|
2571
3657
|
advance: flush
|
|
2572
3658
|
});
|
|
2573
3659
|
}
|
|
3660
|
+
// ── fleets ────────────────────────────────────────────────────────────────
|
|
3661
|
+
// A fleet widget's roster (the run's devices) and then the same back-filled, ticking rows
|
|
3662
|
+
// subscribeWidget delivers, each carrying its device.
|
|
3663
|
+
//
|
|
3664
|
+
// 🔴 A member has NO NAME here, so the widget labels it by its token, exactly as the live board
|
|
3665
|
+
// does. A recorded device's `id` is its external id (the scene's binding key, "SP-HL-0003"),
|
|
3666
|
+
// not a device name: the live roster reads device NAMES, which the sim never sets, and every
|
|
3667
|
+
// other surface of the board (a gauge's subject line, the machine selector, an alarm's
|
|
3668
|
+
// originator) names a device by its token. Labelling the table's rows by external id made one
|
|
3669
|
+
// machine "SP-HL-0003" in the table and "sp-hauler-03" in the alarm beside it.
|
|
3670
|
+
subscribeMembers(datasource, sink) {
|
|
3671
|
+
let members;
|
|
3672
|
+
try {
|
|
3673
|
+
members = this.resolve(datasource).map((g) => ({ token: g.deviceToken, name: null }));
|
|
3674
|
+
} catch (err) {
|
|
3675
|
+
return this.failLater(err, sink);
|
|
3676
|
+
}
|
|
3677
|
+
let stop;
|
|
3678
|
+
const off = this.open({
|
|
3679
|
+
start: () => {
|
|
3680
|
+
sink.roster({ members, total: members.length, unbound: datasource.kind === "unbound" });
|
|
3681
|
+
stop = this.subscribeWidget(datasource, sink);
|
|
3682
|
+
},
|
|
3683
|
+
advance: () => {
|
|
3684
|
+
}
|
|
3685
|
+
});
|
|
3686
|
+
return () => {
|
|
3687
|
+
off();
|
|
3688
|
+
stop?.();
|
|
3689
|
+
};
|
|
3690
|
+
}
|
|
3691
|
+
// Each device's seed comes from the recording itself, with no read to make: its newest reading per
|
|
3692
|
+
// measurement inside the history window, and every measurement it reported in that window, which
|
|
3693
|
+
// is how "does not report this" (a dozer's tyres) is told from "sent nothing".
|
|
3694
|
+
async readMemberSeeds(deviceTokens, _plan, onSeed) {
|
|
3695
|
+
const { t, v } = this.rec.measurements;
|
|
3696
|
+
for (const token of deviceTokens) {
|
|
3697
|
+
const seed = { latest: {}, observed: [] };
|
|
3698
|
+
for (const s of this.index.byDevice.get(token) ?? []) {
|
|
3699
|
+
const at = upperBound(s.times, this.cursor) - 1;
|
|
3700
|
+
if (at < 0 || s.times[at] < this.cursor - RECORDED_HISTORY_WINDOW_MS) continue;
|
|
3701
|
+
const row = s.rows[at];
|
|
3702
|
+
seed.observed.push(s.name);
|
|
3703
|
+
seed.latest[s.name] = {
|
|
3704
|
+
id: `${token}|${s.name}|${row}`,
|
|
3705
|
+
deviceToken: token,
|
|
3706
|
+
eventType: 0,
|
|
3707
|
+
occurredTime: this.iso(t[row]),
|
|
3708
|
+
name: s.name,
|
|
3709
|
+
value: v[row],
|
|
3710
|
+
classifier: null
|
|
3711
|
+
};
|
|
3712
|
+
}
|
|
3713
|
+
onSeed(token, seed);
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
2574
3716
|
// ── alarms ────────────────────────────────────────────────────────────────
|
|
2575
3717
|
subscribeAlarms(subscription, sink) {
|
|
2576
3718
|
let scope = null;
|
|
@@ -2705,28 +3847,269 @@ function toAlarmRow(a) {
|
|
|
2705
3847
|
};
|
|
2706
3848
|
}
|
|
2707
3849
|
|
|
3850
|
+
// src/widget-descriptor.ts
|
|
3851
|
+
var BUILTIN_IDS = new Set(WIDGET_TYPES);
|
|
3852
|
+
function isBuiltinWidgetId(id) {
|
|
3853
|
+
return typeof id === "string" && BUILTIN_IDS.has(id);
|
|
3854
|
+
}
|
|
3855
|
+
var NAMESPACED_ID = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
|
|
3856
|
+
function isNamespacedWidgetId(id) {
|
|
3857
|
+
return typeof id === "string" && NAMESPACED_ID.test(id);
|
|
3858
|
+
}
|
|
3859
|
+
function pinnedAxes(mode) {
|
|
3860
|
+
switch (mode) {
|
|
3861
|
+
case "horizontal":
|
|
3862
|
+
return ["rows"];
|
|
3863
|
+
case "vertical":
|
|
3864
|
+
return ["cols"];
|
|
3865
|
+
case "none":
|
|
3866
|
+
return ["cols", "rows"];
|
|
3867
|
+
default:
|
|
3868
|
+
return [];
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
var FALLBACK_DEFAULT_SPAN = Object.freeze({ cols: 4, rows: 2 });
|
|
3872
|
+
var SDK_API_VERSION = 1;
|
|
3873
|
+
function defineWidgetDescriptor(descriptor) {
|
|
3874
|
+
return descriptor;
|
|
3875
|
+
}
|
|
3876
|
+
|
|
3877
|
+
// src/layout-lint.ts
|
|
3878
|
+
var PUBLISH_BLOCKING_LAYOUT_CODES = [
|
|
3879
|
+
"span-out-of-limits",
|
|
3880
|
+
"overrun-columns",
|
|
3881
|
+
"relative-off-canvas"
|
|
3882
|
+
];
|
|
3883
|
+
function blockingLayoutFindings(findings) {
|
|
3884
|
+
return findings.filter((f) => PUBLISH_BLOCKING_LAYOUT_CODES.includes(f.code));
|
|
3885
|
+
}
|
|
3886
|
+
function spanLimitsOfLayout(layout) {
|
|
3887
|
+
if (!layout) return void 0;
|
|
3888
|
+
const min = { ...layout.minSpan };
|
|
3889
|
+
const max = { ...layout.maxSpan };
|
|
3890
|
+
for (const axis of pinnedAxes(layout.resize)) {
|
|
3891
|
+
min[axis] = layout.defaultSpan[axis];
|
|
3892
|
+
max[axis] = layout.defaultSpan[axis];
|
|
3893
|
+
}
|
|
3894
|
+
const limits = {};
|
|
3895
|
+
if (Object.keys(min).length > 0) limits.minSpan = min;
|
|
3896
|
+
if (Object.keys(max).length > 0) limits.maxSpan = max;
|
|
3897
|
+
return limits;
|
|
3898
|
+
}
|
|
3899
|
+
var RELATIVE_PROBE_CELLS = (minCellSize) => [minCellSize, minCellSize * 16];
|
|
3900
|
+
var EDGE_EPSILON = 0.01;
|
|
3901
|
+
function lintLayout(def, options) {
|
|
3902
|
+
const findings = [];
|
|
3903
|
+
const columnsAt = (bp) => bp === BASE_BREAKPOINT || breakpointColumns(def.canvas, bp) === def.canvas.grid.columns ? options.columns : breakpointColumns(def.canvas, bp);
|
|
3904
|
+
const breakpoints = layoutBreakpoints(def.canvas.breakpoints, def.widgets);
|
|
3905
|
+
for (const widget of def.widgets) {
|
|
3906
|
+
const limits = options.spanLimitsOf(widget);
|
|
3907
|
+
if (!limits) {
|
|
3908
|
+
findings.push({
|
|
3909
|
+
code: "no-layout-declared",
|
|
3910
|
+
severity: "info",
|
|
3911
|
+
widgetId: widget.id,
|
|
3912
|
+
message: `${widget.type} declares no span limits, so its size cannot be checked`
|
|
3913
|
+
});
|
|
3914
|
+
}
|
|
3915
|
+
for (const [bp, placement] of Object.entries(widget.layout)) {
|
|
3916
|
+
const where = bp === BASE_BREAKPOINT ? "" : ` at breakpoint ${bp}`;
|
|
3917
|
+
const at = bp === BASE_BREAKPOINT ? {} : { breakpoint: bp };
|
|
3918
|
+
const columns = columnsAt(bp);
|
|
3919
|
+
if (placement.mode === "grid" && placement.col + placement.cols > columns) {
|
|
3920
|
+
findings.push({
|
|
3921
|
+
code: "overrun-columns",
|
|
3922
|
+
severity: "error",
|
|
3923
|
+
widgetId: widget.id,
|
|
3924
|
+
...at,
|
|
3925
|
+
message: `spans columns ${placement.col}..${placement.col + placement.cols} of ${columns}${where}`
|
|
3926
|
+
});
|
|
3927
|
+
}
|
|
3928
|
+
const span = spanOf(def, widget, bp, placement);
|
|
3929
|
+
if (limits && span) {
|
|
3930
|
+
const problem = spanProblem(span.cols, span.rows, limits);
|
|
3931
|
+
if (problem) {
|
|
3932
|
+
findings.push({
|
|
3933
|
+
code: "span-out-of-limits",
|
|
3934
|
+
severity: "error",
|
|
3935
|
+
widgetId: widget.id,
|
|
3936
|
+
...at,
|
|
3937
|
+
message: `${span.cols}x${span.rows} ${problem}${where}`
|
|
3938
|
+
});
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
const duplicate = titleDuplicatesSubtitle(def, widget);
|
|
3943
|
+
if (duplicate) {
|
|
3944
|
+
findings.push({
|
|
3945
|
+
code: "title-duplicates-subtitle",
|
|
3946
|
+
severity: "warning",
|
|
3947
|
+
widgetId: widget.id,
|
|
3948
|
+
message: duplicate
|
|
3949
|
+
});
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
const byId = new Map(def.widgets.map((w) => [w.id, w]));
|
|
3953
|
+
const differsFromBase = (w, bp) => {
|
|
3954
|
+
if (columnsAt(bp) !== options.columns) return true;
|
|
3955
|
+
for (let cur = w, hops = 0; cur && hops < 3; hops++) {
|
|
3956
|
+
if (Object.prototype.hasOwnProperty.call(cur.layout, bp)) return true;
|
|
3957
|
+
const p = resolvePlacement(cur.layout, bp);
|
|
3958
|
+
cur = p.mode === "relative" ? byId.get(p.host) : void 0;
|
|
3959
|
+
}
|
|
3960
|
+
return false;
|
|
3961
|
+
};
|
|
3962
|
+
for (const bp of breakpoints) {
|
|
3963
|
+
const columns = columnsAt(bp);
|
|
3964
|
+
const gutter = gutterPx(def.canvas.grid);
|
|
3965
|
+
const off = /* @__PURE__ */ new Set();
|
|
3966
|
+
for (const cell of RELATIVE_PROBE_CELLS(def.canvas.grid.minCellSize)) {
|
|
3967
|
+
const width = spanPx(columns, cell, gutter.x);
|
|
3968
|
+
for (const [id, r] of layoutRects(def.widgets, bp, cell, gutter)) {
|
|
3969
|
+
const w = byId.get(id);
|
|
3970
|
+
if (resolvePlacement(w.layout, bp).mode !== "relative" || off.has(id)) continue;
|
|
3971
|
+
if (bp !== BASE_BREAKPOINT && !differsFromBase(w, bp)) continue;
|
|
3972
|
+
if (r.x < -EDGE_EPSILON || r.y < -EDGE_EPSILON || r.x + r.w > width + EDGE_EPSILON) {
|
|
3973
|
+
off.add(id);
|
|
3974
|
+
const where = bp === BASE_BREAKPOINT ? "" : ` at breakpoint ${bp}`;
|
|
3975
|
+
findings.push({
|
|
3976
|
+
code: "relative-off-canvas",
|
|
3977
|
+
severity: "error",
|
|
3978
|
+
widgetId: id,
|
|
3979
|
+
...bp === BASE_BREAKPOINT ? {} : { breakpoint: bp },
|
|
3980
|
+
message: `falls outside the ${columns}-column canvas${where} (left ${round(r.x)}, top ${round(r.y)}, right ${round(r.x + r.w)} of ${round(width)} px at ${round(cell)} px cells)`
|
|
3981
|
+
});
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
return findings;
|
|
3987
|
+
}
|
|
3988
|
+
function round(n) {
|
|
3989
|
+
return Math.round(n * 10) / 10;
|
|
3990
|
+
}
|
|
3991
|
+
function spanOf(def, widget, bp, p) {
|
|
3992
|
+
if (p.mode === "grid") return { cols: p.cols, rows: p.rows };
|
|
3993
|
+
if (!("fill" in p.size)) return p.size;
|
|
3994
|
+
const host = def.widgets.find((w) => w.id === p.host);
|
|
3995
|
+
if (!host || host === widget) return void 0;
|
|
3996
|
+
return spanOf(def, host, bp, resolvePlacement(host.layout, bp));
|
|
3997
|
+
}
|
|
3998
|
+
function spanProblem(cols, rows, limits) {
|
|
3999
|
+
const { minSpan = {}, maxSpan = {} } = limits;
|
|
4000
|
+
const bound = (b) => b.cols === void 0 ? `${b.rows}-row` : b.rows === void 0 ? `${b.cols}-column` : `${b.cols}x${b.rows}`;
|
|
4001
|
+
if (cols < (minSpan.cols ?? 1) || rows < (minSpan.rows ?? 1)) {
|
|
4002
|
+
return `is below the ${bound(minSpan)} minimum`;
|
|
4003
|
+
}
|
|
4004
|
+
if (cols > (maxSpan.cols ?? Infinity) || rows > (maxSpan.rows ?? Infinity)) {
|
|
4005
|
+
return `is above the ${bound(maxSpan)} maximum`;
|
|
4006
|
+
}
|
|
4007
|
+
return void 0;
|
|
4008
|
+
}
|
|
4009
|
+
function titleDuplicatesSubtitle(def, widget) {
|
|
4010
|
+
const title = widget.options?.title;
|
|
4011
|
+
if (typeof title !== "string" || title.trim() === "") return void 0;
|
|
4012
|
+
const binding = widgetBinding(def, widget);
|
|
4013
|
+
const subject = binding?.kind === "device" ? binding.deviceToken : binding?.anchor.targetToken;
|
|
4014
|
+
if (!subject) return void 0;
|
|
4015
|
+
const ds = widget.datasource;
|
|
4016
|
+
const slotLabel = ds?.kind === "slot" ? def.slots?.[ds.slot]?.label : void 0;
|
|
4017
|
+
const same = (a) => sameLabel(a, title);
|
|
4018
|
+
if (same(subject)) return `the title and the subject line both read "${title.trim()}"`;
|
|
4019
|
+
if (same(slotLabel)) {
|
|
4020
|
+
return `the title "${title.trim()}" is the slot's name for "${subject}", which the subject line already shows`;
|
|
4021
|
+
}
|
|
4022
|
+
return void 0;
|
|
4023
|
+
}
|
|
4024
|
+
|
|
2708
4025
|
// src/editor-model.ts
|
|
2709
|
-
function
|
|
4026
|
+
function basePlacement(widget) {
|
|
2710
4027
|
return widget.layout[BASE_BREAKPOINT];
|
|
2711
4028
|
}
|
|
2712
|
-
function
|
|
2713
|
-
|
|
4029
|
+
function resolveCollisions(def, breakpoint, first, aside = false) {
|
|
4030
|
+
const placed = [];
|
|
4031
|
+
for (const w of def.widgets) {
|
|
4032
|
+
const p = resolvePlacement(w.layout, breakpoint);
|
|
4033
|
+
if (p.mode === "grid") placed.push({ id: w.id, placement: p });
|
|
4034
|
+
}
|
|
4035
|
+
const resolved = aside ? pushAside(placed, first) : pushDown(placed, first);
|
|
4036
|
+
let changed = false;
|
|
4037
|
+
const widgets = def.widgets.map((w) => {
|
|
4038
|
+
const before = resolvePlacement(w.layout, breakpoint);
|
|
4039
|
+
const after = resolved.get(w.id);
|
|
4040
|
+
if (!after || after === before) return w;
|
|
4041
|
+
changed = true;
|
|
4042
|
+
return { ...w, layout: { ...w.layout, [breakpoint]: after } };
|
|
4043
|
+
});
|
|
4044
|
+
return changed ? { ...def, widgets } : def;
|
|
4045
|
+
}
|
|
4046
|
+
function resolveEverywhere(def, first, aside = false) {
|
|
4047
|
+
let next = def;
|
|
4048
|
+
for (const bp of layoutBreakpoints(def.canvas.breakpoints, def.widgets))
|
|
4049
|
+
next = resolveCollisions(next, bp, first, aside && bp === BASE_BREAKPOINT);
|
|
4050
|
+
return next;
|
|
4051
|
+
}
|
|
4052
|
+
function setWidgetPlacement(def, id, placement, options = {}) {
|
|
4053
|
+
const next = {
|
|
2714
4054
|
...def,
|
|
2715
|
-
widgets: def.widgets.map(
|
|
2716
|
-
(w) => w.id === id ? { ...w, layout: { ...w.layout, [BASE_BREAKPOINT]: box } } : w
|
|
2717
|
-
)
|
|
4055
|
+
widgets: def.widgets.map((w) => w.id === id ? { ...w, layout: { ...w.layout, [BASE_BREAKPOINT]: placement } } : w)
|
|
2718
4056
|
};
|
|
4057
|
+
return placement.mode === "grid" ? resolveEverywhere(next, [id], options.nearest) : next;
|
|
4058
|
+
}
|
|
4059
|
+
function clampSpan(span, limits, room) {
|
|
4060
|
+
const cols = Math.min(room, Math.max(limits?.minSpan?.cols ?? 1, Math.min(limits?.maxSpan?.cols ?? Infinity, span.cols)));
|
|
4061
|
+
const rows = Math.max(limits?.minSpan?.rows ?? 1, Math.min(limits?.maxSpan?.rows ?? Infinity, span.rows));
|
|
4062
|
+
return { cols: Math.max(1, Math.round(cols)), rows: Math.max(1, Math.round(rows)) };
|
|
4063
|
+
}
|
|
4064
|
+
function moveWidget(def, id, to, columns = def.canvas.grid.columns, options = {}) {
|
|
4065
|
+
const w = def.widgets.find((x) => x.id === id);
|
|
4066
|
+
if (!w) return def;
|
|
4067
|
+
const p = basePlacement(w);
|
|
4068
|
+
if (p.mode !== "grid") return def;
|
|
4069
|
+
const col = Math.min(Math.max(0, Math.round(to.col)), Math.max(0, columns - p.cols));
|
|
4070
|
+
const row = Math.max(0, Math.round(to.row));
|
|
4071
|
+
if (col === p.col && row === p.row) return def;
|
|
4072
|
+
return setWidgetPlacement(def, id, { ...p, col, row }, options);
|
|
4073
|
+
}
|
|
4074
|
+
function resizeWidget(def, id, target, limits, edges = {}, columns = def.canvas.grid.columns, options = {}) {
|
|
4075
|
+
const w = def.widgets.find((x) => x.id === id);
|
|
4076
|
+
if (!w) return def;
|
|
4077
|
+
const p = basePlacement(w);
|
|
4078
|
+
if (p.mode !== "grid") return def;
|
|
4079
|
+
const right = target.col + target.cols;
|
|
4080
|
+
const bottom = target.row + target.rows;
|
|
4081
|
+
const room = edges.fromLeft ? Math.max(1, right) : Math.max(1, columns - Math.max(0, target.col));
|
|
4082
|
+
const { cols, rows } = clampSpan(target, limits, Math.min(columns, room));
|
|
4083
|
+
const col = edges.fromLeft ? Math.max(0, right - cols) : Math.max(0, target.col);
|
|
4084
|
+
const row = edges.fromTop ? Math.max(0, bottom - rows) : Math.max(0, target.row);
|
|
4085
|
+
if (col === p.col && row === p.row && cols === p.cols && rows === p.rows) return def;
|
|
4086
|
+
return setWidgetPlacement(def, id, { mode: "grid", col, row, cols, rows }, options);
|
|
2719
4087
|
}
|
|
2720
4088
|
function deleteWidget(def, id) {
|
|
2721
|
-
|
|
4089
|
+
const gone = def.widgets.find((w) => w.id === id);
|
|
4090
|
+
let next = { ...def, widgets: def.widgets.filter((w) => w.id !== id) };
|
|
4091
|
+
if (!gone) return next;
|
|
4092
|
+
for (const bp of layoutBreakpoints(def.canvas.breakpoints, def.widgets)) {
|
|
4093
|
+
const hostPlacement = resolvePlacement(gone.layout, bp);
|
|
4094
|
+
const columns = breakpointColumns(def.canvas, bp);
|
|
4095
|
+
for (const w of next.widgets) {
|
|
4096
|
+
const p = resolvePlacement(w.layout, bp);
|
|
4097
|
+
if (p.mode !== "relative" || p.host !== id) continue;
|
|
4098
|
+
const size = "fill" in p.size ? hostSpan(hostPlacement, next, bp) : p.size;
|
|
4099
|
+
const occupied = next.widgets.map((o) => resolvePlacement(o.layout, bp)).filter((o) => o.mode === "grid");
|
|
4100
|
+
const cols = Math.min(size.cols, columns);
|
|
4101
|
+
const slot = firstFreeSlot(occupied, cols, size.rows, columns);
|
|
4102
|
+
const grid = { mode: "grid", col: slot.col, row: slot.row, cols, rows: size.rows };
|
|
4103
|
+
next = { ...next, widgets: next.widgets.map((o) => o.id === w.id ? { ...o, layout: { ...o.layout, [bp]: grid } } : o) };
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
4106
|
+
return resolveEverywhere(next, []);
|
|
2722
4107
|
}
|
|
2723
|
-
function
|
|
2724
|
-
|
|
2725
|
-
if (!
|
|
2726
|
-
const
|
|
2727
|
-
|
|
2728
|
-
if (box.z > maxOther) return def;
|
|
2729
|
-
return setWidgetBox(def, id, { ...box, z: maxOther + 1 });
|
|
4108
|
+
function hostSpan(p, def, bp) {
|
|
4109
|
+
if (p.mode === "grid") return { cols: p.cols, rows: p.rows };
|
|
4110
|
+
if (!("fill" in p.size)) return p.size;
|
|
4111
|
+
const host = def.widgets.find((w) => w.id === p.host);
|
|
4112
|
+
return host ? hostSpan(resolvePlacement(host.layout, bp), def, bp) : { cols: 1, rows: 1 };
|
|
2730
4113
|
}
|
|
2731
4114
|
function setTitle(def, title) {
|
|
2732
4115
|
return { ...def, title };
|
|
@@ -2734,28 +4117,44 @@ function setTitle(def, title) {
|
|
|
2734
4117
|
function setCanvasGrid(def, patch) {
|
|
2735
4118
|
const grid = { ...def.canvas.grid, ...patch };
|
|
2736
4119
|
if (patch.columns !== void 0) grid.columns = Math.max(1, Math.round(patch.columns));
|
|
2737
|
-
if (patch.
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
for (const
|
|
2744
|
-
|
|
2745
|
-
|
|
4120
|
+
if (patch.minCellSize !== void 0) grid.minCellSize = Math.max(1, patch.minCellSize);
|
|
4121
|
+
let next = { ...def, canvas: { ...def.canvas, grid } };
|
|
4122
|
+
if (grid.columns >= def.canvas.grid.columns) return next;
|
|
4123
|
+
const affected = layoutBreakpoints(def.canvas.breakpoints, def.widgets).filter(
|
|
4124
|
+
(bp) => breakpointColumns(next.canvas, bp) === grid.columns
|
|
4125
|
+
);
|
|
4126
|
+
for (const bp of affected) {
|
|
4127
|
+
next = {
|
|
4128
|
+
...next,
|
|
4129
|
+
widgets: next.widgets.map((w) => {
|
|
4130
|
+
const p = resolvePlacement(w.layout, bp);
|
|
4131
|
+
if (p.mode !== "grid" || p.col + p.cols <= grid.columns) return w;
|
|
4132
|
+
const col = Math.min(p.col, grid.columns - 1);
|
|
4133
|
+
return { ...w, layout: { ...w.layout, [bp]: { ...p, col, cols: Math.min(p.cols, grid.columns - col) } } };
|
|
4134
|
+
})
|
|
4135
|
+
};
|
|
4136
|
+
next = resolveCollisions(next, bp, []);
|
|
2746
4137
|
}
|
|
2747
|
-
return
|
|
4138
|
+
return next;
|
|
2748
4139
|
}
|
|
2749
4140
|
function setCanvasSizing(def, sizing) {
|
|
2750
4141
|
return { ...def, canvas: { ...def.canvas, sizing } };
|
|
2751
4142
|
}
|
|
4143
|
+
function setCanvasDensity(def, density) {
|
|
4144
|
+
const { density: _prev, ...rest } = def.canvas;
|
|
4145
|
+
return { ...def, canvas: density === DEFAULT_CANVAS_DENSITY ? rest : { ...rest, density } };
|
|
4146
|
+
}
|
|
2752
4147
|
function updateWidget(def, id, next) {
|
|
2753
4148
|
return { ...def, widgets: def.widgets.map((w) => w.id === id ? next : w) };
|
|
2754
4149
|
}
|
|
2755
4150
|
function addWidget(def, descriptor) {
|
|
2756
|
-
const maxZ = def.widgets.reduce((m, w) => Math.max(m, baseBox(w).z), 0);
|
|
2757
4151
|
const id = generateWidgetId();
|
|
2758
|
-
const
|
|
4152
|
+
const span = descriptor.layout?.defaultSpan ?? FALLBACK_DEFAULT_SPAN;
|
|
4153
|
+
const columns = def.canvas.grid.columns;
|
|
4154
|
+
const cols = Math.min(span.cols, columns);
|
|
4155
|
+
const occupied = def.widgets.map(basePlacement).filter((p) => p.mode === "grid");
|
|
4156
|
+
const slot = firstFreeSlot(occupied, cols, span.rows, columns);
|
|
4157
|
+
const placement = { mode: "grid", col: slot.col, row: slot.row, cols, rows: span.rows };
|
|
2759
4158
|
const common = {
|
|
2760
4159
|
id,
|
|
2761
4160
|
widget: {
|
|
@@ -2763,44 +4162,414 @@ function addWidget(def, descriptor) {
|
|
|
2763
4162
|
optionsVersion: descriptor.optionsVersion,
|
|
2764
4163
|
sdkApiVersion: descriptor.sdkApiVersion
|
|
2765
4164
|
},
|
|
2766
|
-
layout: { [BASE_BREAKPOINT]:
|
|
4165
|
+
layout: { [BASE_BREAKPOINT]: placement },
|
|
2767
4166
|
options: { ...descriptor.defaults }
|
|
2768
4167
|
};
|
|
2769
4168
|
const widget = { ...common, type: descriptor.id };
|
|
2770
|
-
return { definition: { ...def, widgets: [...def.widgets, widget] }, id };
|
|
4169
|
+
return { definition: resolveEverywhere({ ...def, widgets: [...def.widgets, widget] }, [id]), id };
|
|
2771
4170
|
}
|
|
2772
|
-
function
|
|
2773
|
-
const colStride = geom.colWidth + geom.colGap;
|
|
2774
|
-
const rowStride = geom.rowHeight + geom.rowGap;
|
|
2775
|
-
const dx = box.offset?.x ?? 0;
|
|
2776
|
-
const dy = box.offset?.y ?? 0;
|
|
4171
|
+
function gridPlacementToPx(p, geom) {
|
|
2777
4172
|
return {
|
|
2778
|
-
x:
|
|
2779
|
-
y:
|
|
2780
|
-
w:
|
|
2781
|
-
h:
|
|
4173
|
+
x: p.col * (geom.cell + geom.gutter.x),
|
|
4174
|
+
y: p.row * (geom.cell + geom.gutter.y),
|
|
4175
|
+
w: spanPx(p.cols, geom.cell, geom.gutter.x),
|
|
4176
|
+
h: spanPx(p.rows, geom.cell, geom.gutter.y)
|
|
2782
4177
|
};
|
|
2783
4178
|
}
|
|
2784
|
-
function
|
|
2785
|
-
const colStride = Math.max(1, geom.
|
|
2786
|
-
const rowStride = Math.max(1, geom.
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
let col = Math.max(0, Math.round(x / colStride));
|
|
2790
|
-
let colSpan = Math.max(1, Math.round((px.w + geom.colGap) / colStride));
|
|
4179
|
+
function pxToGridPlacement(px, geom, columns) {
|
|
4180
|
+
const colStride = Math.max(1, geom.cell + geom.gutter.x);
|
|
4181
|
+
const rowStride = Math.max(1, geom.cell + geom.gutter.y);
|
|
4182
|
+
let col = Math.max(0, Math.round(px.x / colStride));
|
|
4183
|
+
let cols = Math.max(1, Math.round((px.w + geom.gutter.x) / colStride));
|
|
2791
4184
|
if (columns !== void 0) {
|
|
2792
4185
|
col = Math.min(col, columns - 1);
|
|
2793
|
-
|
|
4186
|
+
cols = Math.min(cols, columns - col);
|
|
2794
4187
|
}
|
|
2795
|
-
|
|
4188
|
+
return {
|
|
4189
|
+
mode: "grid",
|
|
2796
4190
|
col,
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
4191
|
+
row: Math.max(0, Math.round(px.y / rowStride)),
|
|
4192
|
+
cols,
|
|
4193
|
+
rows: Math.max(1, Math.round((px.h + geom.gutter.y) / rowStride))
|
|
4194
|
+
};
|
|
4195
|
+
}
|
|
4196
|
+
function formatSpan(span) {
|
|
4197
|
+
return `${span.cols}\xD7${span.rows}`;
|
|
4198
|
+
}
|
|
4199
|
+
var OFFSET_STEP = 1;
|
|
4200
|
+
var OFFSET_STEP_FINE = 0.25;
|
|
4201
|
+
function snapOffset(value, fine = false) {
|
|
4202
|
+
const step = fine ? OFFSET_STEP_FINE : OFFSET_STEP;
|
|
4203
|
+
const snapped = Math.round(value / step) * step;
|
|
4204
|
+
return snapped === 0 ? 0 : snapped;
|
|
4205
|
+
}
|
|
4206
|
+
function replaceBase(def, id, placement) {
|
|
4207
|
+
return {
|
|
4208
|
+
...def,
|
|
4209
|
+
widgets: def.widgets.map((w) => w.id === id ? { ...w, layout: { ...w.layout, [BASE_BREAKPOINT]: placement } } : w)
|
|
4210
|
+
};
|
|
4211
|
+
}
|
|
4212
|
+
function setRelativeOffset(def, id, offset, options = {}) {
|
|
4213
|
+
const w = def.widgets.find((x2) => x2.id === id);
|
|
4214
|
+
if (!w) return def;
|
|
4215
|
+
const p = basePlacement(w);
|
|
4216
|
+
if (p.mode !== "relative") return def;
|
|
4217
|
+
const step = options.fine ? OFFSET_STEP_FINE : OFFSET_STEP;
|
|
4218
|
+
let x = snapOffset(offset.x, options.fine);
|
|
4219
|
+
let y = snapOffset(offset.y, options.fine);
|
|
4220
|
+
const { bounds } = options;
|
|
4221
|
+
if (bounds) {
|
|
4222
|
+
const host = layoutRects(def.widgets, BASE_BREAKPOINT, bounds.cell, bounds.gutter).get(p.host);
|
|
4223
|
+
if (host) {
|
|
4224
|
+
const r = relativeRect({ ...p, offset: { x, y } }, host, bounds.cell, bounds.gutter);
|
|
4225
|
+
const px = (bounds.cell + bounds.gutter.x) * step;
|
|
4226
|
+
const py = (bounds.cell + bounds.gutter.y) * step;
|
|
4227
|
+
const width = spanPx(bounds.columns, bounds.cell, bounds.gutter.x);
|
|
4228
|
+
if (r.x < 0) x += Math.ceil(-r.x / px) * step;
|
|
4229
|
+
else if (r.x + r.w > width) x -= Math.ceil((r.x + r.w - width) / px) * step;
|
|
4230
|
+
if (r.y < 0) y += Math.ceil(-r.y / py) * step;
|
|
4231
|
+
}
|
|
4232
|
+
}
|
|
4233
|
+
const next = { ...p };
|
|
4234
|
+
if (x === 0 && y === 0) delete next.offset;
|
|
4235
|
+
else next.offset = { x, y };
|
|
4236
|
+
if ((next.offset?.x ?? 0) === (p.offset?.x ?? 0) && (next.offset?.y ?? 0) === (p.offset?.y ?? 0)) return def;
|
|
4237
|
+
return replaceBase(def, id, next);
|
|
4238
|
+
}
|
|
4239
|
+
function canHost(def, host, id) {
|
|
4240
|
+
if (!host || host.id === id) return false;
|
|
4241
|
+
return layoutBreakpoints(def.canvas.breakpoints, def.widgets).every(
|
|
4242
|
+
(bp) => resolvePlacement(host.layout, bp).mode === "grid"
|
|
4243
|
+
);
|
|
4244
|
+
}
|
|
4245
|
+
function canHostRelative(def, id, hostId) {
|
|
4246
|
+
return canHost(def, def.widgets.find((x) => x.id === hostId), id);
|
|
4247
|
+
}
|
|
4248
|
+
function placeRelative(def, id, hostId, anchor) {
|
|
4249
|
+
const w = def.widgets.find((x) => x.id === id);
|
|
4250
|
+
if (!w) return def;
|
|
4251
|
+
if (!canHost(def, def.widgets.find((x) => x.id === hostId), id)) return def;
|
|
4252
|
+
const p = basePlacement(w);
|
|
4253
|
+
const size = p.mode === "grid" ? { cols: p.cols, rows: p.rows } : p.size;
|
|
4254
|
+
return replaceBase(def, id, { mode: "relative", host: hostId, anchor, size });
|
|
4255
|
+
}
|
|
4256
|
+
function setRelativeAnchor(def, id, anchor) {
|
|
4257
|
+
const w = def.widgets.find((x) => x.id === id);
|
|
4258
|
+
const p = w && basePlacement(w);
|
|
4259
|
+
if (!p || p.mode !== "relative" || p.anchor === anchor) return def;
|
|
4260
|
+
return replaceBase(def, id, { ...p, anchor });
|
|
4261
|
+
}
|
|
4262
|
+
function placeOnGrid(def, id) {
|
|
4263
|
+
const w = def.widgets.find((x) => x.id === id);
|
|
4264
|
+
if (!w) return def;
|
|
4265
|
+
const p = basePlacement(w);
|
|
4266
|
+
if (p.mode !== "relative") return def;
|
|
4267
|
+
const columns = def.canvas.grid.columns;
|
|
4268
|
+
const size = "fill" in p.size ? hostSpan(p, def, BASE_BREAKPOINT) : p.size;
|
|
4269
|
+
const cols = Math.min(size.cols, columns);
|
|
4270
|
+
const occupied = def.widgets.filter((o) => o.id !== id).map(basePlacement).filter((o) => o.mode === "grid");
|
|
4271
|
+
const slot = firstFreeSlot(occupied, cols, size.rows, columns);
|
|
4272
|
+
const grid = { mode: "grid", col: slot.col, row: slot.row, cols, rows: size.rows };
|
|
4273
|
+
return resolveEverywhere(replaceBase(def, id, grid), [id]);
|
|
4274
|
+
}
|
|
4275
|
+
function fitToLimits(def, spanLimitsOf) {
|
|
4276
|
+
let changed = false;
|
|
4277
|
+
const columnsAt = (bp) => breakpointColumns(def.canvas, bp);
|
|
4278
|
+
const clamped = {
|
|
4279
|
+
...def,
|
|
4280
|
+
widgets: def.widgets.map((w) => {
|
|
4281
|
+
const limits = spanLimitsOf(w);
|
|
4282
|
+
const layout = {};
|
|
4283
|
+
let touched = false;
|
|
4284
|
+
for (const [bp, p] of Object.entries(w.layout)) {
|
|
4285
|
+
const columns = columnsAt(bp);
|
|
4286
|
+
let q = p;
|
|
4287
|
+
if (p.mode === "grid") {
|
|
4288
|
+
const span = clampSpan(p, limits, columns);
|
|
4289
|
+
const col = Math.max(0, Math.min(p.col, columns - span.cols));
|
|
4290
|
+
if (span.cols !== p.cols || span.rows !== p.rows || col !== p.col) q = { ...p, ...span, col };
|
|
4291
|
+
} else if (!("fill" in p.size)) {
|
|
4292
|
+
const size = clampSpan(p.size, limits, columns);
|
|
4293
|
+
if (size.cols !== p.size.cols || size.rows !== p.size.rows) q = { ...p, size };
|
|
4294
|
+
}
|
|
4295
|
+
if (q !== p) touched = true;
|
|
4296
|
+
layout[bp] = q;
|
|
4297
|
+
}
|
|
4298
|
+
if (!touched) return w;
|
|
4299
|
+
changed = true;
|
|
4300
|
+
return { ...w, layout };
|
|
4301
|
+
})
|
|
4302
|
+
};
|
|
4303
|
+
let next = {
|
|
4304
|
+
...clamped,
|
|
4305
|
+
widgets: clamped.widgets.map((w) => {
|
|
4306
|
+
const limits = spanLimitsOf(w);
|
|
4307
|
+
if (!limits) return w;
|
|
4308
|
+
let layout;
|
|
4309
|
+
for (const [bp, p] of Object.entries(w.layout)) {
|
|
4310
|
+
if (p.mode !== "relative" || !("fill" in p.size)) continue;
|
|
4311
|
+
const span = hostSpan(p, clamped, bp);
|
|
4312
|
+
const size = clampSpan(span, limits, columnsAt(bp));
|
|
4313
|
+
if (size.cols === span.cols && size.rows === span.rows) continue;
|
|
4314
|
+
layout = { ...layout ?? w.layout, [bp]: { ...p, size } };
|
|
4315
|
+
}
|
|
4316
|
+
if (!layout) return w;
|
|
4317
|
+
changed = true;
|
|
4318
|
+
return { ...w, layout };
|
|
4319
|
+
})
|
|
4320
|
+
};
|
|
4321
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
4322
|
+
const off = lintLayout(next, { columns: def.canvas.grid.columns, spanLimitsOf }).filter(
|
|
4323
|
+
(f) => f.code === "relative-off-canvas"
|
|
4324
|
+
);
|
|
4325
|
+
if (off.length === 0) break;
|
|
4326
|
+
for (const f of off) {
|
|
4327
|
+
const fixed = fitRelativeOnCanvas(next, f.widgetId, f.breakpoint ?? BASE_BREAKPOINT);
|
|
4328
|
+
if (fixed !== next) changed = true;
|
|
4329
|
+
next = fixed;
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
if (!changed) return def;
|
|
4333
|
+
return resolveEverywhere(next, []);
|
|
4334
|
+
}
|
|
4335
|
+
function fitRelativeOnCanvas(def, id, bp) {
|
|
4336
|
+
const w = def.widgets.find((x) => x.id === id);
|
|
4337
|
+
if (!w) return def;
|
|
4338
|
+
const p = resolvePlacement(w.layout, bp);
|
|
4339
|
+
if (p.mode !== "relative") return def;
|
|
4340
|
+
const columns = breakpointColumns(def.canvas, bp);
|
|
4341
|
+
const gutter = gutterPx(def.canvas.grid);
|
|
4342
|
+
const { offset, ...bare } = p;
|
|
4343
|
+
let loX = -Infinity;
|
|
4344
|
+
let hiX = Infinity;
|
|
4345
|
+
let loY = -Infinity;
|
|
4346
|
+
for (const cell of RELATIVE_PROBE_CELLS(def.canvas.grid.minCellSize)) {
|
|
4347
|
+
const host = layoutRects(def.widgets, bp, cell, gutter).get(p.host);
|
|
4348
|
+
if (!host) return def;
|
|
4349
|
+
const r = relativeRect(bare, host, cell, gutter);
|
|
4350
|
+
const width = spanPx(columns, cell, gutter.x);
|
|
4351
|
+
loX = Math.max(loX, -r.x / (cell + gutter.x));
|
|
4352
|
+
hiX = Math.min(hiX, (width - r.x - r.w) / (cell + gutter.x));
|
|
4353
|
+
loY = Math.max(loY, -r.y / (cell + gutter.y));
|
|
4354
|
+
}
|
|
4355
|
+
const step = OFFSET_STEP_FINE;
|
|
4356
|
+
const up = (v) => Math.ceil(v / step - 1e-9) * step;
|
|
4357
|
+
const down = (v) => Math.floor(v / step + 1e-9) * step;
|
|
4358
|
+
let placement;
|
|
4359
|
+
if (up(loX) <= down(hiX)) {
|
|
4360
|
+
const x = Math.min(Math.max(offset?.x ?? 0, up(loX)), down(hiX)) || 0;
|
|
4361
|
+
const y = Math.max(offset?.y ?? 0, up(loY)) || 0;
|
|
4362
|
+
placement = x === 0 && y === 0 ? bare : { ...bare, offset: { x, y } };
|
|
4363
|
+
} else {
|
|
4364
|
+
const size = "fill" in p.size ? hostSpan(p, def, bp) : p.size;
|
|
4365
|
+
const cols = Math.min(size.cols, columns);
|
|
4366
|
+
const occupied = def.widgets.filter((o) => o.id !== id).map((o) => resolvePlacement(o.layout, bp)).filter((o) => o.mode === "grid");
|
|
4367
|
+
const slot = firstFreeSlot(occupied, cols, size.rows, columns);
|
|
4368
|
+
placement = { mode: "grid", col: slot.col, row: slot.row, cols, rows: size.rows };
|
|
4369
|
+
}
|
|
4370
|
+
return {
|
|
4371
|
+
...def,
|
|
4372
|
+
widgets: def.widgets.map((x) => x.id === id ? { ...x, layout: { ...x.layout, [bp]: placement } } : x)
|
|
4373
|
+
};
|
|
4374
|
+
}
|
|
4375
|
+
|
|
4376
|
+
// src/editor-history.ts
|
|
4377
|
+
var DEFAULT_HISTORY_LIMIT = 100;
|
|
4378
|
+
function createEditHistory(present, options = {}) {
|
|
4379
|
+
return { past: [], present, future: [], limit: Math.max(1, Math.floor(options.limit ?? DEFAULT_HISTORY_LIMIT)) };
|
|
4380
|
+
}
|
|
4381
|
+
function commitEdit(h, next, label, options = {}) {
|
|
4382
|
+
const equals = options.equals ?? Object.is;
|
|
4383
|
+
if (next === h.present || equals(next, h.present)) return h;
|
|
4384
|
+
const last = h.past[h.past.length - 1];
|
|
4385
|
+
if (options.key !== void 0 && options.key === h.open && last) {
|
|
4386
|
+
if (equals(next, last.state)) return { past: h.past.slice(0, -1), present: next, future: [], limit: h.limit };
|
|
4387
|
+
return { ...h, present: next, future: [] };
|
|
4388
|
+
}
|
|
4389
|
+
const past = [...h.past, { state: h.present, label }];
|
|
4390
|
+
if (past.length > h.limit) past.splice(0, past.length - h.limit);
|
|
4391
|
+
return { past, present: next, future: [], open: options.key, limit: h.limit };
|
|
4392
|
+
}
|
|
4393
|
+
function sealHistory(h) {
|
|
4394
|
+
if (h.open === void 0) return h;
|
|
4395
|
+
const { open: _closed, ...rest } = h;
|
|
4396
|
+
return rest;
|
|
4397
|
+
}
|
|
4398
|
+
function undoEdit(h) {
|
|
4399
|
+
const last = h.past[h.past.length - 1];
|
|
4400
|
+
if (!last) return { history: h };
|
|
4401
|
+
return {
|
|
4402
|
+
history: {
|
|
4403
|
+
past: h.past.slice(0, -1),
|
|
4404
|
+
present: last.state,
|
|
4405
|
+
future: [...h.future, { state: h.present, label: last.label }],
|
|
4406
|
+
limit: h.limit
|
|
4407
|
+
},
|
|
4408
|
+
label: last.label
|
|
4409
|
+
};
|
|
4410
|
+
}
|
|
4411
|
+
function redoEdit(h) {
|
|
4412
|
+
const next = h.future[h.future.length - 1];
|
|
4413
|
+
if (!next) return { history: h };
|
|
4414
|
+
return {
|
|
4415
|
+
history: {
|
|
4416
|
+
past: [...h.past, { state: h.present, label: next.label }],
|
|
4417
|
+
present: next.state,
|
|
4418
|
+
future: h.future.slice(0, -1),
|
|
4419
|
+
limit: h.limit
|
|
4420
|
+
},
|
|
4421
|
+
label: next.label
|
|
4422
|
+
};
|
|
4423
|
+
}
|
|
4424
|
+
var canUndo = (h) => h.past.length > 0;
|
|
4425
|
+
var canRedo = (h) => h.future.length > 0;
|
|
4426
|
+
function rebaseHistory(h, fn) {
|
|
4427
|
+
const map = (e) => ({ ...e, state: fn(e.state) });
|
|
4428
|
+
return { ...h, past: h.past.map(map), present: fn(h.present), future: h.future.map(map) };
|
|
4429
|
+
}
|
|
4430
|
+
|
|
4431
|
+
// src/editor-clipboard.ts
|
|
4432
|
+
var WIDGET_CLIPBOARD_FORMAT = "devicechain/dashboard-widgets";
|
|
4433
|
+
var WIDGET_CLIPBOARD_VERSION = 1;
|
|
4434
|
+
function slotReferences(w) {
|
|
4435
|
+
const out = [];
|
|
4436
|
+
if (w.datasource?.kind === "slot" && w.datasource.slot) out.push(w.datasource.slot);
|
|
4437
|
+
const target = w.options?.selectionTarget;
|
|
4438
|
+
if (typeof target === "string" && target !== "" && !out.includes(target)) out.push(target);
|
|
4439
|
+
return out;
|
|
4440
|
+
}
|
|
4441
|
+
var own = (map, key) => map && Object.prototype.hasOwnProperty.call(map, key) ? map[key] : void 0;
|
|
4442
|
+
function copyWidget(def, id, origin) {
|
|
4443
|
+
const w = def.widgets.find((x) => x.id === id);
|
|
4444
|
+
if (!w || w.type === UNKNOWN_WIDGET_TYPE) return void 0;
|
|
4445
|
+
const p = basePlacement(w);
|
|
4446
|
+
const size = p.mode === "grid" ? { cols: p.cols, rows: p.rows } : "fill" in p.size ? hostSpan(p, def, BASE_BREAKPOINT) : p.size;
|
|
4447
|
+
const placement = { mode: "grid", col: 0, row: 0, ...size };
|
|
4448
|
+
const widget = structuredClone({ ...w, layout: { [BASE_BREAKPOINT]: placement } });
|
|
4449
|
+
const slots = {};
|
|
4450
|
+
for (const name of slotReferences(w)) {
|
|
4451
|
+
const slot = own(def.slots, name);
|
|
4452
|
+
if (slot) slots[name] = structuredClone(slot);
|
|
4453
|
+
}
|
|
4454
|
+
return { tenant: origin.tenant, board: origin.board, canvas: structuredClone(def.canvas), widgets: [widget], slots };
|
|
4455
|
+
}
|
|
4456
|
+
function serializeWidgetClipboard(clip) {
|
|
4457
|
+
const definition = storedDefinition({
|
|
4458
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
4459
|
+
title: "",
|
|
4460
|
+
canvas: clip.canvas,
|
|
4461
|
+
widgets: clip.widgets,
|
|
4462
|
+
...Object.keys(clip.slots).length > 0 ? { slots: clip.slots } : {}
|
|
4463
|
+
});
|
|
4464
|
+
return JSON.stringify({
|
|
4465
|
+
format: WIDGET_CLIPBOARD_FORMAT,
|
|
4466
|
+
version: WIDGET_CLIPBOARD_VERSION,
|
|
4467
|
+
tenant: clip.tenant,
|
|
4468
|
+
board: clip.board,
|
|
4469
|
+
definition
|
|
4470
|
+
});
|
|
4471
|
+
}
|
|
4472
|
+
function parseWidgetClipboard(text, options = {}) {
|
|
4473
|
+
let env;
|
|
4474
|
+
try {
|
|
4475
|
+
env = JSON.parse(text);
|
|
4476
|
+
} catch {
|
|
4477
|
+
return void 0;
|
|
4478
|
+
}
|
|
4479
|
+
if (typeof env !== "object" || env === null) return void 0;
|
|
4480
|
+
const { format, version, tenant, board, definition } = env;
|
|
4481
|
+
if (format !== WIDGET_CLIPBOARD_FORMAT || version !== WIDGET_CLIPBOARD_VERSION) return void 0;
|
|
4482
|
+
if (typeof tenant !== "string" || tenant === "" || typeof board !== "string") return void 0;
|
|
4483
|
+
let parsed;
|
|
4484
|
+
try {
|
|
4485
|
+
parsed = parseDashboardDefinition(definition, options);
|
|
4486
|
+
} catch (err) {
|
|
4487
|
+
if (err?.code === "schemaTooNew") throw err;
|
|
4488
|
+
return void 0;
|
|
4489
|
+
}
|
|
4490
|
+
const widgets = [];
|
|
4491
|
+
const refused = [];
|
|
4492
|
+
for (const w of parsed.widgets) {
|
|
4493
|
+
if (w.type !== UNKNOWN_WIDGET_TYPE) {
|
|
4494
|
+
widgets.push(w);
|
|
4495
|
+
continue;
|
|
4496
|
+
}
|
|
4497
|
+
const type = typeof w.raw.type === "string" ? w.raw.type : "";
|
|
4498
|
+
refused.push({ type, reason: w.reason?.message ?? "" });
|
|
4499
|
+
}
|
|
4500
|
+
return { clipboard: { tenant, board, canvas: parsed.canvas, widgets, slots: parsed.slots ?? {} }, refused };
|
|
4501
|
+
}
|
|
4502
|
+
var CONCRETE_KINDS = /* @__PURE__ */ new Set(["device", "anchor", "devices", "relatedTraversal"]);
|
|
4503
|
+
function pasteWidgets(def, clip, at) {
|
|
4504
|
+
const sameTenant = clip.tenant !== "" && clip.tenant === at.tenant;
|
|
4505
|
+
const sameBoard = sameTenant && clip.board === at.board;
|
|
4506
|
+
const columns = def.canvas.grid.columns;
|
|
4507
|
+
const slots = { ...def.slots ?? {} };
|
|
4508
|
+
const mapped = /* @__PURE__ */ new Map();
|
|
4509
|
+
const mapSlot = (from) => {
|
|
4510
|
+
const known = mapped.get(from);
|
|
4511
|
+
if (known) return known;
|
|
4512
|
+
const source = own(clip.slots, from);
|
|
4513
|
+
const target = own(slots, from);
|
|
4514
|
+
const keep = sameTenant && target !== void 0 && source !== void 0 && target.type === source.type && (sameBoard || sameBinding(target.defaultBinding, source.defaultBinding));
|
|
4515
|
+
let result;
|
|
4516
|
+
if (keep) result = { slot: from };
|
|
4517
|
+
else {
|
|
4518
|
+
const slot = nextSlotName(slots);
|
|
4519
|
+
const label = sameTenant ? source?.label : void 0;
|
|
4520
|
+
slots[slot] = { type: source?.type ?? "device", ...label !== void 0 ? { label } : {} };
|
|
4521
|
+
result = { slot, reason: !sameTenant ? "tenant" : target ? "different" : "missing" };
|
|
4522
|
+
}
|
|
4523
|
+
mapped.set(from, result);
|
|
4524
|
+
return result;
|
|
2801
4525
|
};
|
|
2802
|
-
|
|
2803
|
-
|
|
4526
|
+
const occupied = def.widgets.map(basePlacement).filter((p) => p.mode === "grid");
|
|
4527
|
+
const added = [];
|
|
4528
|
+
const ids = [];
|
|
4529
|
+
const unbound = [];
|
|
4530
|
+
for (const source of clip.widgets) {
|
|
4531
|
+
const id = generateWidgetId();
|
|
4532
|
+
const p = basePlacement(source);
|
|
4533
|
+
const span = p.mode === "grid" ? { cols: p.cols, rows: p.rows } : "fill" in p.size ? { cols: 1, rows: 1 } : p.size;
|
|
4534
|
+
const cols = Math.min(span.cols, columns);
|
|
4535
|
+
const slot = firstFreeSlot(occupied, cols, span.rows, columns);
|
|
4536
|
+
const placement = { mode: "grid", col: slot.col, row: slot.row, cols, rows: span.rows };
|
|
4537
|
+
occupied.push(placement);
|
|
4538
|
+
const w = structuredClone(source);
|
|
4539
|
+
w.id = id;
|
|
4540
|
+
w.layout = { [BASE_BREAKPOINT]: placement };
|
|
4541
|
+
for (const from of slotReferences(source)) {
|
|
4542
|
+
const m = mapSlot(from);
|
|
4543
|
+
if (m.reason) unbound.push({ widgetId: id, slot: m.slot, from, reason: m.reason });
|
|
4544
|
+
if (m.slot === from) continue;
|
|
4545
|
+
if (w.datasource?.kind === "slot" && w.datasource.slot === from) w.datasource = { ...w.datasource, slot: m.slot };
|
|
4546
|
+
if (w.options?.selectionTarget === from) w.options = { ...w.options, selectionTarget: m.slot };
|
|
4547
|
+
}
|
|
4548
|
+
const ds = w.datasource;
|
|
4549
|
+
if (!sameTenant && ds && CONCRETE_KINDS.has(ds.kind) && ds.kind !== "slot" && ds.kind !== "unbound") {
|
|
4550
|
+
const slot2 = nextSlotName(slots);
|
|
4551
|
+
slots[slot2] = { type: ds.kind === "anchor" ? "anchor" : "device" };
|
|
4552
|
+
w.datasource = {
|
|
4553
|
+
kind: "slot",
|
|
4554
|
+
slot: slot2,
|
|
4555
|
+
measurements: [...ds.measurements],
|
|
4556
|
+
...ds.location ? { location: { ...ds.location } } : {}
|
|
4557
|
+
};
|
|
4558
|
+
unbound.push({ widgetId: id, slot: slot2, from: "", reason: "tenant" });
|
|
4559
|
+
}
|
|
4560
|
+
added.push(w);
|
|
4561
|
+
ids.push(id);
|
|
4562
|
+
}
|
|
4563
|
+
const next = { ...def, widgets: [...def.widgets, ...added] };
|
|
4564
|
+
if (Object.keys(slots).length > 0) next.slots = slots;
|
|
4565
|
+
return { definition: resolveEverywhere(next, ids), ids, unbound };
|
|
4566
|
+
}
|
|
4567
|
+
function duplicateWidget(def, id) {
|
|
4568
|
+
const here = { tenant: "here", board: "here" };
|
|
4569
|
+
const clip = copyWidget(def, id, here);
|
|
4570
|
+
if (!clip) return void 0;
|
|
4571
|
+
const r = pasteWidgets(def, clip, here);
|
|
4572
|
+
return { definition: r.definition, id: r.ids[0], unbound: r.unbound };
|
|
2804
4573
|
}
|
|
2805
4574
|
|
|
2806
4575
|
// src/resolver.ts
|
|
@@ -2808,28 +4577,63 @@ import { gql as gql3 } from "@devicechain/client";
|
|
|
2808
4577
|
var ANCHOR_PAGE_SIZE = 500;
|
|
2809
4578
|
function createDeviceResolver() {
|
|
2810
4579
|
const anchorCache = /* @__PURE__ */ new Map();
|
|
4580
|
+
const memberCache = /* @__PURE__ */ new Map();
|
|
2811
4581
|
const existsCache = /* @__PURE__ */ new Map();
|
|
4582
|
+
function anchorKey(anchor) {
|
|
4583
|
+
return `${anchor.relationship}|${anchor.targetType}|${anchor.targetToken}`;
|
|
4584
|
+
}
|
|
4585
|
+
function anchorPage(anchor) {
|
|
4586
|
+
const key = anchorKey(anchor);
|
|
4587
|
+
let pending = anchorCache.get(key);
|
|
4588
|
+
if (!pending) {
|
|
4589
|
+
pending = gql3("device-management", DEVICES_FOR_ANCHOR, {
|
|
4590
|
+
criteria: {
|
|
4591
|
+
pageNumber: 1,
|
|
4592
|
+
pageSize: ANCHOR_PAGE_SIZE,
|
|
4593
|
+
sourceType: "device",
|
|
4594
|
+
targetType: anchor.targetType,
|
|
4595
|
+
target: anchor.targetToken,
|
|
4596
|
+
relationshipType: anchor.relationship
|
|
4597
|
+
}
|
|
4598
|
+
}).then((r) => {
|
|
4599
|
+
const tokens = r.entityRelationships.results.map((rel) => rel.source.token);
|
|
4600
|
+
const total = r.entityRelationships.pagination?.totalRecords ?? tokens.length;
|
|
4601
|
+
return { tokens, total: Math.max(total, tokens.length) };
|
|
4602
|
+
}).catch((err) => {
|
|
4603
|
+
anchorCache.delete(key);
|
|
4604
|
+
throw err;
|
|
4605
|
+
});
|
|
4606
|
+
anchorCache.set(key, pending);
|
|
4607
|
+
}
|
|
4608
|
+
return pending;
|
|
4609
|
+
}
|
|
2812
4610
|
return {
|
|
2813
4611
|
devicesForAnchor(anchor) {
|
|
2814
|
-
|
|
2815
|
-
|
|
4612
|
+
return anchorPage(anchor).then((page) => page.tokens);
|
|
4613
|
+
},
|
|
4614
|
+
// The members of an anchor with their names and the anchor's true size. One relationship page
|
|
4615
|
+
// (shared with devicesForAnchor) plus ONE batched name read, whatever the size of the fleet. A
|
|
4616
|
+
// failed name read is not a failed roster: the tokens still stream, and the widget labels
|
|
4617
|
+
// them by token.
|
|
4618
|
+
anchorMembers(anchor) {
|
|
4619
|
+
const key = anchorKey(anchor);
|
|
4620
|
+
let pending = memberCache.get(key);
|
|
2816
4621
|
if (!pending) {
|
|
2817
|
-
pending =
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
4622
|
+
pending = anchorPage(anchor).then(async ({ tokens, total }) => {
|
|
4623
|
+
const names = /* @__PURE__ */ new Map();
|
|
4624
|
+
if (tokens.length > 0) {
|
|
4625
|
+
try {
|
|
4626
|
+
const r = await gql3("device-management", DEVICE_NAMES, { tokens });
|
|
4627
|
+
for (const d of r.devicesByToken) names.set(d.token, d.name);
|
|
4628
|
+
} catch {
|
|
4629
|
+
}
|
|
2825
4630
|
}
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
anchorCache.delete(key);
|
|
4631
|
+
return { members: tokens.map((token) => ({ token, name: names.get(token) ?? null })), total };
|
|
4632
|
+
}).catch((err) => {
|
|
4633
|
+
memberCache.delete(key);
|
|
2830
4634
|
throw err;
|
|
2831
4635
|
});
|
|
2832
|
-
|
|
4636
|
+
memberCache.set(key, pending);
|
|
2833
4637
|
}
|
|
2834
4638
|
return pending;
|
|
2835
4639
|
},
|
|
@@ -2861,15 +4665,14 @@ function createDeviceResolver() {
|
|
|
2861
4665
|
|
|
2862
4666
|
// src/history.ts
|
|
2863
4667
|
import { gql as gql4 } from "@devicechain/client";
|
|
4668
|
+
function historyWindowFor(spanSeconds, end) {
|
|
4669
|
+
const { startTime, endTime } = rangeWindow({ spanSeconds, paused: false }, end);
|
|
4670
|
+
return { startTime, endTime, intervalSeconds: bucketSecondsFor(spanSeconds) };
|
|
4671
|
+
}
|
|
2864
4672
|
function defaultHistoryWindow() {
|
|
2865
|
-
|
|
2866
|
-
return {
|
|
2867
|
-
startTime: new Date(now - 60 * 60 * 1e3).toISOString(),
|
|
2868
|
-
endTime: new Date(now).toISOString(),
|
|
2869
|
-
intervalSeconds: 60
|
|
2870
|
-
};
|
|
4673
|
+
return historyWindowFor(DEFAULT_TIME_RANGE_SECONDS, Date.now());
|
|
2871
4674
|
}
|
|
2872
|
-
async function fetchWidgetHistory(widget,
|
|
4675
|
+
async function fetchWidgetHistory(widget, window2) {
|
|
2873
4676
|
const ds = widget.datasource;
|
|
2874
4677
|
if (!ds || ds.kind !== "device") return [];
|
|
2875
4678
|
try {
|
|
@@ -2881,9 +4684,9 @@ async function fetchWidgetHistory(widget, window) {
|
|
|
2881
4684
|
criteria: {
|
|
2882
4685
|
deviceToken,
|
|
2883
4686
|
name,
|
|
2884
|
-
startTime:
|
|
2885
|
-
endTime:
|
|
2886
|
-
intervalSeconds:
|
|
4687
|
+
startTime: window2.startTime,
|
|
4688
|
+
endTime: window2.endTime,
|
|
4689
|
+
intervalSeconds: window2.intervalSeconds
|
|
2887
4690
|
}
|
|
2888
4691
|
}).then((r) => r.bucketedMeasurements)
|
|
2889
4692
|
)
|
|
@@ -2990,6 +4793,36 @@ function checkValue(widgetId, key, spec, value) {
|
|
|
2990
4793
|
return issue("enum", `${key} must be one of ${spec.values.join(", ")}, got ${JSON.stringify(value)}`);
|
|
2991
4794
|
}
|
|
2992
4795
|
return [];
|
|
4796
|
+
case "list": {
|
|
4797
|
+
if (!Array.isArray(value)) return issue("type", `${key} must be a list, got ${describe(value)}`);
|
|
4798
|
+
if (spec.maxItems !== void 0 && value.length > spec.maxItems) {
|
|
4799
|
+
return issue("range", `${key} holds at most ${spec.maxItems} entries, got ${value.length}`);
|
|
4800
|
+
}
|
|
4801
|
+
const out = [];
|
|
4802
|
+
value.forEach((entry, i) => {
|
|
4803
|
+
const at = `${key}[${i}]`;
|
|
4804
|
+
const at_ = (path, code, message) => ({ widgetId, key: path, code, message });
|
|
4805
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
4806
|
+
out.push(at_(at, "type", `${at} must be an object, got ${describe(entry)}`));
|
|
4807
|
+
return;
|
|
4808
|
+
}
|
|
4809
|
+
const record = entry;
|
|
4810
|
+
for (const [field, fieldSpec] of Object.entries(spec.item)) {
|
|
4811
|
+
const fieldValue = record[field];
|
|
4812
|
+
if (fieldValue === void 0 || fieldSpec.required && fieldValue === "") {
|
|
4813
|
+
if (fieldSpec.required) out.push(at_(`${at}.${field}`, "missing", `${at}.${field} is required but not set`));
|
|
4814
|
+
continue;
|
|
4815
|
+
}
|
|
4816
|
+
out.push(...checkValue(widgetId, `${at}.${field}`, fieldSpec, fieldValue));
|
|
4817
|
+
}
|
|
4818
|
+
for (const field of Object.keys(record)) {
|
|
4819
|
+
if (!hasOwn(spec.item, field)) {
|
|
4820
|
+
out.push(at_(`${at}.${field}`, "unknown", `${at}.${field} is not a field of ${key} entries`));
|
|
4821
|
+
}
|
|
4822
|
+
}
|
|
4823
|
+
});
|
|
4824
|
+
return out;
|
|
4825
|
+
}
|
|
2993
4826
|
}
|
|
2994
4827
|
}
|
|
2995
4828
|
function parsesAsJson(value) {
|
|
@@ -3029,7 +4862,7 @@ function applyOptionPatch(specs, options, patch, label = "widget") {
|
|
|
3029
4862
|
for (const [key, value] of Object.entries(patch)) {
|
|
3030
4863
|
if (!hasOwn(specs, key)) throw new Error(`${label}: "${key}" is not a declared option`);
|
|
3031
4864
|
const spec = specs[key];
|
|
3032
|
-
if (value === void 0 || value === "" || spec.kind === "boolean" && value === false) {
|
|
4865
|
+
if (value === void 0 || value === "" || spec.kind === "boolean" && value === false || spec.kind === "list" && Array.isArray(value) && value.length === 0) {
|
|
3033
4866
|
delete next[key];
|
|
3034
4867
|
continue;
|
|
3035
4868
|
}
|
|
@@ -3059,25 +4892,17 @@ function applyOptionPatch(specs, options, patch, label = "widget") {
|
|
|
3059
4892
|
}
|
|
3060
4893
|
next[key] = value;
|
|
3061
4894
|
break;
|
|
4895
|
+
case "list": {
|
|
4896
|
+
const bad = checkValue(label, key, spec, value);
|
|
4897
|
+
if (bad.length > 0) refuse(`a list of ${key} entries (${bad[0].message})`);
|
|
4898
|
+
next[key] = value;
|
|
4899
|
+
break;
|
|
4900
|
+
}
|
|
3062
4901
|
}
|
|
3063
4902
|
}
|
|
3064
4903
|
return next;
|
|
3065
4904
|
}
|
|
3066
4905
|
|
|
3067
|
-
// src/widget-descriptor.ts
|
|
3068
|
-
var BUILTIN_IDS = new Set(WIDGET_TYPES);
|
|
3069
|
-
function isBuiltinWidgetId(id) {
|
|
3070
|
-
return typeof id === "string" && BUILTIN_IDS.has(id);
|
|
3071
|
-
}
|
|
3072
|
-
var NAMESPACED_ID = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
|
|
3073
|
-
function isNamespacedWidgetId(id) {
|
|
3074
|
-
return typeof id === "string" && NAMESPACED_ID.test(id);
|
|
3075
|
-
}
|
|
3076
|
-
var SDK_API_VERSION = 1;
|
|
3077
|
-
function defineWidgetDescriptor(descriptor) {
|
|
3078
|
-
return descriptor;
|
|
3079
|
-
}
|
|
3080
|
-
|
|
3081
4906
|
// src/widget-catalog.ts
|
|
3082
4907
|
var WidgetCatalogError = class extends Error {
|
|
3083
4908
|
constructor(issues, subject = "widget catalog") {
|
|
@@ -3091,28 +4916,26 @@ function describeIssues(subject, issues) {
|
|
|
3091
4916
|
return `invalid ${subject} (${issues.length} problem${issues.length === 1 ? "" : "s"}):
|
|
3092
4917
|
${lines.join("\n")}`;
|
|
3093
4918
|
}
|
|
3094
|
-
var CHANNELS = ["measurement", "alarm", "control", "selection", "location", "none"];
|
|
4919
|
+
var CHANNELS = ["measurement", "alarm", "control", "selection", "location", "aggregate", "none"];
|
|
3095
4920
|
var SEMVER2 = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
3096
4921
|
var SELECTOR_KINDS = ["device", "anchor", "slot"];
|
|
3097
4922
|
var BINDING_KEYS = {
|
|
3098
|
-
measurement: ["window"],
|
|
4923
|
+
measurement: ["window", "windowSeconds", "members"],
|
|
3099
4924
|
alarm: ["pageSize", "state", "severity", "acknowledged"],
|
|
3100
4925
|
control: ["pageSize"],
|
|
3101
4926
|
location: [],
|
|
4927
|
+
aggregate: ["windowSeconds"],
|
|
3102
4928
|
selection: [],
|
|
3103
4929
|
none: []
|
|
3104
4930
|
};
|
|
3105
|
-
function isRecord3(value) {
|
|
3106
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3107
|
-
}
|
|
3108
4931
|
function isPlainObject(value) {
|
|
3109
|
-
if (!
|
|
4932
|
+
if (!isRecord(value)) return false;
|
|
3110
4933
|
const proto = Object.getPrototypeOf(value);
|
|
3111
4934
|
return proto === Object.prototype || proto === null;
|
|
3112
4935
|
}
|
|
3113
4936
|
var join = (base, key) => base ? `${base}.${key}` : key;
|
|
3114
4937
|
function validateWidgetDescriptor(descriptor) {
|
|
3115
|
-
const rawId =
|
|
4938
|
+
const rawId = isRecord(descriptor) && typeof descriptor.id === "string" ? descriptor.id : "";
|
|
3116
4939
|
return collectDescriptorIssues(descriptor, rawId || "#?");
|
|
3117
4940
|
}
|
|
3118
4941
|
function collectDescriptorIssues(d, id) {
|
|
@@ -3139,7 +4962,7 @@ function collectDescriptorIssues(d, id) {
|
|
|
3139
4962
|
if (typeof d.optionsVersion !== "number" || !Number.isInteger(d.optionsVersion) || d.optionsVersion < 1) {
|
|
3140
4963
|
add("optionsVersion", `optionsVersion must be a positive integer, got ${JSON.stringify(d.optionsVersion)}`);
|
|
3141
4964
|
}
|
|
3142
|
-
if (!
|
|
4965
|
+
if (!isRecord(d.display)) {
|
|
3143
4966
|
add("display", "display must be an object");
|
|
3144
4967
|
} else {
|
|
3145
4968
|
if (typeof d.display.title !== "string" || d.display.title === "") {
|
|
@@ -3155,10 +4978,15 @@ function collectDescriptorIssues(d, id) {
|
|
|
3155
4978
|
const specs = checkOptions(d.options, add);
|
|
3156
4979
|
const constraints = checkConstraints(d.constraints, specs, add);
|
|
3157
4980
|
if (specs) checkDefaults(d.defaults, specs, constraints, typeof d.id === "string" ? d.id : id, add);
|
|
3158
|
-
else if (!
|
|
4981
|
+
else if (!isRecord(d.defaults)) add("defaults", "defaults must be an object");
|
|
3159
4982
|
checkDatasource(d.datasource, channel, add);
|
|
3160
4983
|
if (channel) checkBinding(d.binding, channel, specs, add);
|
|
3161
4984
|
checkInteractions(d.interactions, channel, add);
|
|
4985
|
+
checkLayout(d.layout, add);
|
|
4986
|
+
if (d.freshness !== void 0) {
|
|
4987
|
+
if (d.freshness !== true) add("freshness", "freshness must be true when present");
|
|
4988
|
+
else if (channel && channel !== "measurement") add("freshness", "freshness applies only to the measurement channel");
|
|
4989
|
+
}
|
|
3162
4990
|
return issues;
|
|
3163
4991
|
}
|
|
3164
4992
|
function jsonOnly(value, path, add, ancestors) {
|
|
@@ -3197,14 +5025,17 @@ function jsonOnly(value, path, add, ancestors) {
|
|
|
3197
5025
|
ancestors.delete(obj);
|
|
3198
5026
|
}
|
|
3199
5027
|
function checkOptions(options, add) {
|
|
3200
|
-
if (!
|
|
5028
|
+
if (!isRecord(options)) {
|
|
3201
5029
|
add("options", "options must be an object mapping option keys to specs");
|
|
3202
5030
|
return void 0;
|
|
3203
5031
|
}
|
|
5032
|
+
return checkOptionsAt(options, "options.", add) ? options : void 0;
|
|
5033
|
+
}
|
|
5034
|
+
function checkOptionsAt(options, prefix, add) {
|
|
3204
5035
|
let sound = true;
|
|
3205
5036
|
for (const [key, spec] of Object.entries(options)) {
|
|
3206
|
-
const at =
|
|
3207
|
-
if (!
|
|
5037
|
+
const at = `${prefix}${key}`;
|
|
5038
|
+
if (!isRecord(spec)) {
|
|
3208
5039
|
add(at, "an option spec must be an object");
|
|
3209
5040
|
sound = false;
|
|
3210
5041
|
continue;
|
|
@@ -3235,7 +5066,7 @@ function checkOptions(options, add) {
|
|
|
3235
5066
|
for (const table of ["valueLabels", "valueLabelIds"]) {
|
|
3236
5067
|
const labels = spec[table];
|
|
3237
5068
|
if (labels === void 0) continue;
|
|
3238
|
-
if (!
|
|
5069
|
+
if (!isRecord(labels)) {
|
|
3239
5070
|
add(`${at}.${table}`, `${table} must be an object mapping enum values to strings`);
|
|
3240
5071
|
continue;
|
|
3241
5072
|
}
|
|
@@ -3247,12 +5078,33 @@ function checkOptions(options, add) {
|
|
|
3247
5078
|
}
|
|
3248
5079
|
}
|
|
3249
5080
|
break;
|
|
5081
|
+
case "list": {
|
|
5082
|
+
if (spec.maxItems !== void 0 && (typeof spec.maxItems !== "number" || !Number.isInteger(spec.maxItems) || spec.maxItems < 1)) {
|
|
5083
|
+
add(`${at}.maxItems`, "maxItems must be a whole number of at least 1");
|
|
5084
|
+
}
|
|
5085
|
+
if (!isRecord(spec.item) || Object.keys(spec.item).length === 0) {
|
|
5086
|
+
add(`${at}.item`, "a list option needs an item object declaring at least one field");
|
|
5087
|
+
sound = false;
|
|
5088
|
+
break;
|
|
5089
|
+
}
|
|
5090
|
+
const fields = spec.item;
|
|
5091
|
+
const flat = Object.fromEntries(Object.entries(fields).filter(([, f]) => !isRecord(f) || f.kind !== "list"));
|
|
5092
|
+
for (const key2 of Object.keys(fields)) {
|
|
5093
|
+
if (!(key2 in flat)) add(`${at}.item.${key2}`, "a list item field cannot itself be a list");
|
|
5094
|
+
}
|
|
5095
|
+
if (Object.keys(flat).length !== Object.keys(fields).length) {
|
|
5096
|
+
sound = false;
|
|
5097
|
+
break;
|
|
5098
|
+
}
|
|
5099
|
+
if (!checkOptionsAt(flat, `${at}.item.`, add)) sound = false;
|
|
5100
|
+
break;
|
|
5101
|
+
}
|
|
3250
5102
|
default:
|
|
3251
|
-
add(`${at}.kind`, `kind must be string, number, boolean or
|
|
5103
|
+
add(`${at}.kind`, `kind must be string, number, boolean, enum or list, got ${JSON.stringify(spec.kind)}`);
|
|
3252
5104
|
sound = false;
|
|
3253
5105
|
}
|
|
3254
5106
|
}
|
|
3255
|
-
return sound
|
|
5107
|
+
return sound;
|
|
3256
5108
|
}
|
|
3257
5109
|
function checkConstraints(constraints, specs, add) {
|
|
3258
5110
|
if (constraints === void 0) return [];
|
|
@@ -3263,8 +5115,8 @@ function checkConstraints(constraints, specs, add) {
|
|
|
3263
5115
|
const valid = [];
|
|
3264
5116
|
constraints.forEach((c, i) => {
|
|
3265
5117
|
const at = `constraints[${i}]`;
|
|
3266
|
-
if (!
|
|
3267
|
-
add(`${at}.kind`, `constraint kind must be "greater-than", got ${JSON.stringify(
|
|
5118
|
+
if (!isRecord(c) || c.kind !== "greater-than") {
|
|
5119
|
+
add(`${at}.kind`, `constraint kind must be "greater-than", got ${JSON.stringify(isRecord(c) ? c.kind : c)}`);
|
|
3268
5120
|
return;
|
|
3269
5121
|
}
|
|
3270
5122
|
let ok = true;
|
|
@@ -3283,7 +5135,7 @@ function checkConstraints(constraints, specs, add) {
|
|
|
3283
5135
|
return valid;
|
|
3284
5136
|
}
|
|
3285
5137
|
function checkDefaults(defaults, specs, constraints, id, add) {
|
|
3286
|
-
if (!
|
|
5138
|
+
if (!isRecord(defaults)) {
|
|
3287
5139
|
add("defaults", "defaults must be an object");
|
|
3288
5140
|
return;
|
|
3289
5141
|
}
|
|
@@ -3293,7 +5145,7 @@ function checkDefaults(defaults, specs, constraints, id, add) {
|
|
|
3293
5145
|
for (const issue of found) add(`defaults.${issue.key}`, issue.message);
|
|
3294
5146
|
}
|
|
3295
5147
|
function checkDatasource(datasource, channel, add) {
|
|
3296
|
-
if (!
|
|
5148
|
+
if (!isRecord(datasource)) {
|
|
3297
5149
|
add("datasource", "datasource must be an object");
|
|
3298
5150
|
return;
|
|
3299
5151
|
}
|
|
@@ -3323,7 +5175,7 @@ function checkDatasource(datasource, channel, add) {
|
|
|
3323
5175
|
});
|
|
3324
5176
|
}
|
|
3325
5177
|
function checkBinding(binding, channel, specs, add) {
|
|
3326
|
-
if (!
|
|
5178
|
+
if (!isRecord(binding)) {
|
|
3327
5179
|
add("binding", "binding must be an object");
|
|
3328
5180
|
return;
|
|
3329
5181
|
}
|
|
@@ -3338,7 +5190,7 @@ function checkBinding(binding, channel, specs, add) {
|
|
|
3338
5190
|
const policy = binding[key];
|
|
3339
5191
|
if (policy === void 0) return;
|
|
3340
5192
|
const at = `binding.${key}`;
|
|
3341
|
-
if (!
|
|
5193
|
+
if (!isRecord(policy)) {
|
|
3342
5194
|
add(at, `${key} must be an object`);
|
|
3343
5195
|
return;
|
|
3344
5196
|
}
|
|
@@ -3365,7 +5217,7 @@ function checkBinding(binding, channel, specs, add) {
|
|
|
3365
5217
|
}
|
|
3366
5218
|
if (policy.min !== void 0 && typeof policy.min !== "number") add(`${at}.min`, "min must be a number");
|
|
3367
5219
|
if (policy.default !== void 0) {
|
|
3368
|
-
const wantsNumber = key === "window" || key === "pageSize";
|
|
5220
|
+
const wantsNumber = key === "window" || key === "windowSeconds" || key === "pageSize";
|
|
3369
5221
|
if (typeof policy.default !== (wantsNumber ? "number" : "string")) {
|
|
3370
5222
|
add(`${at}.default`, `default must be a ${wantsNumber ? "number" : "string"}`);
|
|
3371
5223
|
}
|
|
@@ -3375,13 +5227,83 @@ function checkBinding(binding, channel, specs, add) {
|
|
|
3375
5227
|
const isText = (s) => s.kind === "string" || s.kind === "enum";
|
|
3376
5228
|
const isBoolEnum = (s) => s.kind === "enum" && s.values.every((v) => v === "true" || v === "false");
|
|
3377
5229
|
ref("window", isNumber, "a number option", false);
|
|
5230
|
+
ref("windowSeconds", isNumber, "a number option", false);
|
|
3378
5231
|
ref("pageSize", isNumber, "a number option", true);
|
|
3379
5232
|
ref("state", isText, "a string or enum option", false);
|
|
3380
5233
|
ref("severity", isText, "a string or enum option", false);
|
|
3381
5234
|
ref("acknowledged", isBoolEnum, 'an enum option whose values are only "true" and "false"', false);
|
|
5235
|
+
const members = binding.members;
|
|
5236
|
+
if (members !== void 0) {
|
|
5237
|
+
const m = isRecord(members) ? members : {};
|
|
5238
|
+
const isConst = m.const !== void 0;
|
|
5239
|
+
if (isConst ? m.option !== void 0 || m.equals !== void 0 : typeof m.option !== "string" || typeof m.equals !== "string") {
|
|
5240
|
+
add("binding.members", "members must be { const: true } or { option, equals } naming a declared option and a value of it");
|
|
5241
|
+
} else if (isConst) {
|
|
5242
|
+
if (m.const !== true) add("binding.members.const", `const must be true, got ${JSON.stringify(m.const)}`);
|
|
5243
|
+
} else if (specs) {
|
|
5244
|
+
const spec = hasOwn(specs, m.option) ? specs[m.option] : void 0;
|
|
5245
|
+
if (!spec) add("binding.members.option", `option ${JSON.stringify(m.option)} is not a declared option key`);
|
|
5246
|
+
else if (!isText(spec)) add("binding.members.option", `option ${JSON.stringify(m.option)} must be a string or enum option`);
|
|
5247
|
+
else if (spec.kind === "enum" && !spec.values.includes(m.equals)) {
|
|
5248
|
+
add("binding.members.equals", `${JSON.stringify(m.equals)} is not one of the option's values`);
|
|
5249
|
+
}
|
|
5250
|
+
}
|
|
5251
|
+
}
|
|
5252
|
+
}
|
|
5253
|
+
function readSpan(value, path, add) {
|
|
5254
|
+
if (!isRecord(value)) {
|
|
5255
|
+
add(path, `${path} must be an object with cols and rows`);
|
|
5256
|
+
return void 0;
|
|
5257
|
+
}
|
|
5258
|
+
let ok = true;
|
|
5259
|
+
for (const axis of ["cols", "rows"]) {
|
|
5260
|
+
const n = value[axis];
|
|
5261
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < 1) {
|
|
5262
|
+
add(`${path}.${axis}`, `${axis} must be a positive integer, got ${JSON.stringify(n)}`);
|
|
5263
|
+
ok = false;
|
|
5264
|
+
}
|
|
5265
|
+
}
|
|
5266
|
+
for (const key of Object.keys(value)) {
|
|
5267
|
+
if (key !== "cols" && key !== "rows") add(`${path}.${key}`, `unknown span field "${key}"`);
|
|
5268
|
+
}
|
|
5269
|
+
return ok ? { cols: value.cols, rows: value.rows } : void 0;
|
|
5270
|
+
}
|
|
5271
|
+
function checkLayout(layout, add) {
|
|
5272
|
+
if (layout === void 0) return;
|
|
5273
|
+
if (!isRecord(layout)) {
|
|
5274
|
+
add("layout", "layout must be an object");
|
|
5275
|
+
return;
|
|
5276
|
+
}
|
|
5277
|
+
for (const key of Object.keys(layout)) {
|
|
5278
|
+
if (!["defaultSpan", "minSpan", "maxSpan", "resize"].includes(key)) add(`layout.${key}`, `unknown layout field "${key}"`);
|
|
5279
|
+
}
|
|
5280
|
+
let def;
|
|
5281
|
+
if (layout.defaultSpan === void 0) add("layout.defaultSpan", "layout needs a defaultSpan");
|
|
5282
|
+
else def = readSpan(layout.defaultSpan, "layout.defaultSpan", add);
|
|
5283
|
+
const min = layout.minSpan === void 0 ? void 0 : readSpan(layout.minSpan, "layout.minSpan", add);
|
|
5284
|
+
const max = layout.maxSpan === void 0 ? void 0 : readSpan(layout.maxSpan, "layout.maxSpan", add);
|
|
5285
|
+
const mode = layout.resize ?? "both";
|
|
5286
|
+
if (mode !== "both" && mode !== "horizontal" && mode !== "vertical" && mode !== "none") {
|
|
5287
|
+
add("layout.resize", `resize must be both, horizontal, vertical or none, got ${JSON.stringify(layout.resize)}`);
|
|
5288
|
+
return;
|
|
5289
|
+
}
|
|
5290
|
+
const floor = min ?? { cols: 1, rows: 1 };
|
|
5291
|
+
for (const axis of ["cols", "rows"]) {
|
|
5292
|
+
if (def && floor[axis] > def[axis]) add("layout.minSpan", `minSpan.${axis} (${floor[axis]}) is above defaultSpan.${axis} (${def[axis]})`);
|
|
5293
|
+
if (def && max && def[axis] > max[axis]) add("layout.maxSpan", `maxSpan.${axis} (${max[axis]}) is below defaultSpan.${axis} (${def[axis]})`);
|
|
5294
|
+
if (!def && min && max && min[axis] > max[axis]) add("layout.maxSpan", `maxSpan.${axis} is below minSpan.${axis}`);
|
|
5295
|
+
}
|
|
5296
|
+
if (!def) return;
|
|
5297
|
+
for (const axis of pinnedAxes(mode)) {
|
|
5298
|
+
for (const [name, bound] of [["minSpan", min], ["maxSpan", max]]) {
|
|
5299
|
+
if (bound && bound[axis] !== def[axis]) {
|
|
5300
|
+
add("layout.resize", `resize "${mode}" pins ${axis} at ${def[axis]}, but ${name}.${axis} is ${bound[axis]}`);
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5303
|
+
}
|
|
3382
5304
|
}
|
|
3383
5305
|
function checkInteractions(interactions, channel, add) {
|
|
3384
|
-
if (!
|
|
5306
|
+
if (!isRecord(interactions)) {
|
|
3385
5307
|
add("interactions", "interactions must be an object");
|
|
3386
5308
|
return;
|
|
3387
5309
|
}
|
|
@@ -3416,7 +5338,7 @@ function collectCatalogIssues(descriptors) {
|
|
|
3416
5338
|
const issues = [];
|
|
3417
5339
|
const seen = /* @__PURE__ */ new Map();
|
|
3418
5340
|
descriptors.forEach((d, index) => {
|
|
3419
|
-
const id =
|
|
5341
|
+
const id = isRecord(d) && typeof d.id === "string" && d.id !== "" ? d.id : `#${index}`;
|
|
3420
5342
|
issues.push(...collectDescriptorIssues(d, id));
|
|
3421
5343
|
if (id.startsWith("#")) return;
|
|
3422
5344
|
const first = seen.get(id);
|
|
@@ -3435,41 +5357,151 @@ function sealCatalog(descriptors) {
|
|
|
3435
5357
|
list: () => ordered
|
|
3436
5358
|
});
|
|
3437
5359
|
}
|
|
5360
|
+
|
|
5361
|
+
// src/reflow.ts
|
|
5362
|
+
function liveColumns(grid, containerWidth, authored, scale = 1) {
|
|
5363
|
+
const minCell = grid.minCellSize * scale;
|
|
5364
|
+
if (cellSizeAt(grid, containerWidth, authored, scale) >= minCell) return authored;
|
|
5365
|
+
const gx = gutterPx(grid, scale).x;
|
|
5366
|
+
const room = cellSizeAt(grid, containerWidth, 1, scale);
|
|
5367
|
+
const fit = Math.floor((room + gx) / (minCell + gx));
|
|
5368
|
+
return Math.min(authored, Math.max(PHONE_MIN_COLUMNS, fit));
|
|
5369
|
+
}
|
|
5370
|
+
var clampInto = (n, min, max) => Math.min(max ?? Infinity, Math.max(min ?? 1, n));
|
|
5371
|
+
var STACK_BELOW_WIDTH = 600;
|
|
5372
|
+
function reflowGrid(items, columns, options = {}) {
|
|
5373
|
+
const order = items.map((item, index) => ({ item, index })).sort(
|
|
5374
|
+
(a, b) => a.item.placement.row - b.item.placement.row || a.item.placement.col - b.item.placement.col || a.index - b.index
|
|
5375
|
+
);
|
|
5376
|
+
const placed = [];
|
|
5377
|
+
const out = /* @__PURE__ */ new Map();
|
|
5378
|
+
let floor = 0;
|
|
5379
|
+
let bottom = 0;
|
|
5380
|
+
for (const { item } of order) {
|
|
5381
|
+
const { placement: p, limits } = item;
|
|
5382
|
+
const want = options.stack ? columns : p.cols;
|
|
5383
|
+
const cols = Math.min(columns, clampInto(want, limits?.minSpan?.cols, limits?.maxSpan?.cols));
|
|
5384
|
+
const rows = clampInto(p.rows, limits?.minSpan?.rows, limits?.maxSpan?.rows);
|
|
5385
|
+
if (options.stack) {
|
|
5386
|
+
const spot2 = { mode: "grid", col: 0, row: bottom, cols, rows };
|
|
5387
|
+
out.set(item.id, spot2);
|
|
5388
|
+
bottom += rows;
|
|
5389
|
+
continue;
|
|
5390
|
+
}
|
|
5391
|
+
let spot;
|
|
5392
|
+
for (let row = floor; !spot; row++) {
|
|
5393
|
+
for (let col = 0; col + cols <= columns; col++) {
|
|
5394
|
+
const probe = { mode: "grid", col, row, cols, rows };
|
|
5395
|
+
if (!placed.some((q) => gridFootprintsOverlap(probe, q))) {
|
|
5396
|
+
spot = probe;
|
|
5397
|
+
break;
|
|
5398
|
+
}
|
|
5399
|
+
}
|
|
5400
|
+
}
|
|
5401
|
+
placed.push(spot);
|
|
5402
|
+
out.set(item.id, spot);
|
|
5403
|
+
floor = spot.row;
|
|
5404
|
+
}
|
|
5405
|
+
return out;
|
|
5406
|
+
}
|
|
5407
|
+
function layoutShape(canvas, containerWidth, scale = 1) {
|
|
5408
|
+
const measured = containerWidth > 0;
|
|
5409
|
+
const breakpoint = measured ? activeBreakpoint(canvas.breakpoints, containerWidth) : BASE_BREAKPOINT;
|
|
5410
|
+
const authored = breakpointColumns(canvas, breakpoint);
|
|
5411
|
+
const columns = measured ? liveColumns(canvas.grid, containerWidth, authored, scale) : authored;
|
|
5412
|
+
const reflowed = columns < authored;
|
|
5413
|
+
return { breakpoint, columns, reflowed, stacked: reflowed && containerWidth < STACK_BELOW_WIDTH };
|
|
5414
|
+
}
|
|
5415
|
+
function effectiveLayout(def, containerWidth, options = {}) {
|
|
5416
|
+
const shape = layoutShape(def.canvas, containerWidth, options.scale ?? 1);
|
|
5417
|
+
const items = [];
|
|
5418
|
+
for (const w of def.widgets) {
|
|
5419
|
+
const p = resolvePlacement(w.layout, shape.breakpoint);
|
|
5420
|
+
if (p.mode === "grid") items.push({ id: w.id, placement: p, limits: options.limitsOf?.(w) });
|
|
5421
|
+
}
|
|
5422
|
+
const grid = shape.reflowed ? reflowGrid(items, shape.columns, { stack: shape.stacked }) : new Map(items.map((i) => [i.id, i.placement]));
|
|
5423
|
+
return { ...shape, grid };
|
|
5424
|
+
}
|
|
3438
5425
|
export {
|
|
5426
|
+
ANCHOR_PAGE_SIZE,
|
|
3439
5427
|
BASE_BREAKPOINT,
|
|
5428
|
+
CANVAS_DENSITIES,
|
|
3440
5429
|
COMMAND_STATUSES,
|
|
5430
|
+
CURRENT_SCHEMA_VERSION,
|
|
5431
|
+
ConnectionTracker,
|
|
5432
|
+
DEFAULT_CANVAS_DENSITY,
|
|
5433
|
+
DEFAULT_GRID,
|
|
5434
|
+
DEFAULT_HISTORY_LIMIT,
|
|
5435
|
+
DEFAULT_TIME_RANGE_SECONDS,
|
|
5436
|
+
DENSITY_SCALE,
|
|
3441
5437
|
DashboardDefinitionError,
|
|
3442
5438
|
DashboardHub,
|
|
5439
|
+
FALLBACK_DEFAULT_SPAN,
|
|
5440
|
+
FRAME_BORDER_WIDTHS,
|
|
5441
|
+
FRAME_HEADERS,
|
|
5442
|
+
FRAME_RADII,
|
|
5443
|
+
FRAME_VARIANTS,
|
|
3443
5444
|
HOST_OPTION_CONTROLS,
|
|
5445
|
+
LEGACY_REFERENCE_WIDTH,
|
|
5446
|
+
MAX_SEED_BUCKETS,
|
|
5447
|
+
MAX_TIME_RANGE_SECONDS,
|
|
5448
|
+
MIN_TIME_RANGE_SECONDS,
|
|
3444
5449
|
NotInRecordingError,
|
|
5450
|
+
OFFLINE_AFTER_MS,
|
|
5451
|
+
OFFSET_STEP,
|
|
5452
|
+
OFFSET_STEP_FINE,
|
|
5453
|
+
PHONE_MIN_COLUMNS,
|
|
5454
|
+
PLACEMENT_ANCHORS,
|
|
5455
|
+
PLACEMENT_MODES,
|
|
3445
5456
|
PLAYBACK_RATES,
|
|
5457
|
+
PUBLISH_BLOCKING_LAYOUT_CODES,
|
|
3446
5458
|
RECORDED_HISTORY_WINDOW_MS,
|
|
3447
5459
|
RECORDING_FORMAT_VERSION,
|
|
3448
5460
|
RECORDING_KIND,
|
|
5461
|
+
RESERVED_PLACEMENT_MODES,
|
|
3449
5462
|
RecordedClock,
|
|
3450
5463
|
RecordedDataSource,
|
|
3451
5464
|
RecordingFormatError,
|
|
3452
5465
|
SDK_API_VERSION,
|
|
5466
|
+
SPACE_TOKEN_PX,
|
|
5467
|
+
STACK_BELOW_WIDTH,
|
|
3453
5468
|
SYNTHETIC_GENERATORS,
|
|
3454
5469
|
SyntheticDataSource,
|
|
5470
|
+
THEME_COLOR_TOKENS,
|
|
5471
|
+
TIME_RANGE_PRESETS,
|
|
3455
5472
|
UNKNOWN_WIDGET_TYPE,
|
|
5473
|
+
WIDGET_CLIPBOARD_FORMAT,
|
|
5474
|
+
WIDGET_CLIPBOARD_VERSION,
|
|
3456
5475
|
WIDGET_TYPES,
|
|
3457
5476
|
WidgetCatalogError,
|
|
3458
5477
|
activeBreakpoint,
|
|
3459
5478
|
addWidget,
|
|
5479
|
+
anchorFraction,
|
|
3460
5480
|
anchorSlotNames,
|
|
3461
5481
|
applyOptionPatch,
|
|
3462
5482
|
applySelection,
|
|
3463
|
-
|
|
5483
|
+
basePlacement,
|
|
3464
5484
|
bindWidgetSlot,
|
|
3465
5485
|
bindingsWithoutScopedSlots,
|
|
3466
|
-
|
|
5486
|
+
blockingLayoutFindings,
|
|
5487
|
+
breakpointColumns,
|
|
5488
|
+
breakpointMinWidth,
|
|
5489
|
+
bucketSecondsFor,
|
|
5490
|
+
canHostRelative,
|
|
5491
|
+
canRedo,
|
|
3467
5492
|
canScopeSlot,
|
|
5493
|
+
canUndo,
|
|
5494
|
+
cellSizeAt,
|
|
3468
5495
|
checkWidgetCompatibility,
|
|
3469
5496
|
clampNumberOption,
|
|
5497
|
+
clampSpan,
|
|
3470
5498
|
clearWidgetDatasource,
|
|
3471
5499
|
collectCatalogIssues,
|
|
5500
|
+
commitEdit,
|
|
5501
|
+
containerWidthFor,
|
|
5502
|
+
copyWidget,
|
|
3472
5503
|
createDeviceResolver,
|
|
5504
|
+
createEditHistory,
|
|
3473
5505
|
createEntityLister,
|
|
3474
5506
|
createRecordedClock,
|
|
3475
5507
|
createRecordedLister,
|
|
@@ -3479,51 +5511,108 @@ export {
|
|
|
3479
5511
|
defineWidgetDescriptor,
|
|
3480
5512
|
deleteWidget,
|
|
3481
5513
|
describeIssues,
|
|
5514
|
+
duplicateWidget,
|
|
3482
5515
|
effectiveBindings,
|
|
5516
|
+
effectiveLayout,
|
|
3483
5517
|
enumValues,
|
|
3484
5518
|
fetchWidgetHistory,
|
|
5519
|
+
firstFreeSlot,
|
|
5520
|
+
fitToLimits,
|
|
5521
|
+
formatSpan,
|
|
3485
5522
|
generateWidgetId,
|
|
3486
|
-
|
|
5523
|
+
gridFootprintsOverlap,
|
|
5524
|
+
gridLengthPx,
|
|
5525
|
+
gridPlacementToPx,
|
|
5526
|
+
gridRect,
|
|
5527
|
+
gutterPx,
|
|
3487
5528
|
hasScopedSlots,
|
|
5529
|
+
historyWindowFor,
|
|
5530
|
+
initialTimeRange,
|
|
3488
5531
|
isBuiltinWidget,
|
|
3489
5532
|
isBuiltinWidgetId,
|
|
3490
5533
|
isCancellableCommandStatus,
|
|
5534
|
+
isCanvasDensity,
|
|
3491
5535
|
isDirty,
|
|
3492
5536
|
isNamespacedWidgetId,
|
|
3493
5537
|
isResolvedWidget,
|
|
5538
|
+
isSpaceToken,
|
|
3494
5539
|
isTerminalCommandStatus,
|
|
5540
|
+
isValidSpanSeconds,
|
|
5541
|
+
layoutBreakpoints,
|
|
5542
|
+
layoutRects,
|
|
5543
|
+
layoutShape,
|
|
5544
|
+
lintLayout,
|
|
5545
|
+
liveColumns,
|
|
3495
5546
|
migrateToSlots,
|
|
5547
|
+
moveWidget,
|
|
3496
5548
|
numberSpec,
|
|
5549
|
+
paddingPx,
|
|
3497
5550
|
parseBindingManifest,
|
|
3498
5551
|
parseBoardRecording,
|
|
3499
5552
|
parseDashboardDefinition,
|
|
5553
|
+
parsePausedParam,
|
|
5554
|
+
parseRangeParam,
|
|
3500
5555
|
parseSlotBinding,
|
|
5556
|
+
parseWidgetClipboard,
|
|
3501
5557
|
parseWidgetRef,
|
|
5558
|
+
pasteWidgets,
|
|
5559
|
+
pauseRange,
|
|
5560
|
+
pinnedAxes,
|
|
5561
|
+
placeOnGrid,
|
|
5562
|
+
placeRelative,
|
|
5563
|
+
presetIdOf,
|
|
3502
5564
|
pruneSlots,
|
|
3503
|
-
|
|
5565
|
+
pushAside,
|
|
5566
|
+
pushDown,
|
|
5567
|
+
pxToGridPlacement,
|
|
5568
|
+
rangeEnd,
|
|
5569
|
+
rangeWindow,
|
|
5570
|
+
rebaseHistory,
|
|
3504
5571
|
recordedWallTimeMs,
|
|
3505
5572
|
recordingBounds,
|
|
5573
|
+
redoEdit,
|
|
5574
|
+
reflowGrid,
|
|
5575
|
+
relativeRect,
|
|
5576
|
+
resizeWidget,
|
|
3506
5577
|
resolveConcrete,
|
|
3507
5578
|
resolveContextBindings,
|
|
5579
|
+
resolveFrame,
|
|
5580
|
+
resolvePlacement,
|
|
3508
5581
|
resolveSlotCandidates,
|
|
3509
|
-
resolveWidgetBox,
|
|
3510
5582
|
resolveWidgetDatasource,
|
|
5583
|
+
resumeRange,
|
|
3511
5584
|
sameBinding,
|
|
5585
|
+
sameLabel,
|
|
5586
|
+
sealHistory,
|
|
5587
|
+
seedBucketCount,
|
|
3512
5588
|
serializeDefinition,
|
|
5589
|
+
serializeWidgetClipboard,
|
|
5590
|
+
setBoardTimeRange,
|
|
5591
|
+
setCanvasDensity,
|
|
3513
5592
|
setCanvasGrid,
|
|
3514
5593
|
setCanvasSizing,
|
|
5594
|
+
setRelativeAnchor,
|
|
5595
|
+
setRelativeOffset,
|
|
3515
5596
|
setSlotScope,
|
|
3516
5597
|
setTitle,
|
|
3517
|
-
|
|
5598
|
+
setWidgetPlacement,
|
|
3518
5599
|
simulationScaleAt,
|
|
5600
|
+
snapOffset,
|
|
5601
|
+
spanLabel,
|
|
5602
|
+
spanLimitsOfLayout,
|
|
5603
|
+
spanPx,
|
|
3519
5604
|
storedDefinition,
|
|
3520
5605
|
stripDefaultBindings,
|
|
5606
|
+
timeRangeFromSearch,
|
|
5607
|
+
undoEdit,
|
|
3521
5608
|
unresolvedWidgets,
|
|
3522
5609
|
updateWidget,
|
|
3523
5610
|
validateOptions,
|
|
3524
5611
|
validateWidgetDescriptor,
|
|
3525
5612
|
widgetBinding,
|
|
3526
5613
|
widgetSlotName,
|
|
3527
|
-
widgetVersionNotice
|
|
5614
|
+
widgetVersionNotice,
|
|
5615
|
+
withSpan,
|
|
5616
|
+
withoutConversionMark
|
|
3528
5617
|
};
|
|
3529
5618
|
//# sourceMappingURL=index.js.map
|