@richardmcquiston01/dashboard-widgets-toolkit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +324 -0
- package/dist/core.cjs +836 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +93 -0
- package/dist/core.d.ts +93 -0
- package/dist/core.js +778 -0
- package/dist/core.js.map +1 -0
- package/dist/index.cjs +836 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +778 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +1273 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +203 -0
- package/dist/react.d.ts +203 -0
- package/dist/react.js +1248 -0
- package/dist/react.js.map +1 -0
- package/dist/scale-WlRozDCf.d.cts +416 -0
- package/dist/scale-WlRozDCf.d.ts +416 -0
- package/dist/styles.css +550 -0
- package/package.json +121 -0
package/dist/core.cjs
ADDED
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/core/payload.ts
|
|
4
|
+
var WIDGET_KINDS = [
|
|
5
|
+
"TEXT",
|
|
6
|
+
"KPI",
|
|
7
|
+
"GAUGE",
|
|
8
|
+
"TABLE",
|
|
9
|
+
"BAR_LIST",
|
|
10
|
+
"ALERT_LIST",
|
|
11
|
+
"GRAPH"
|
|
12
|
+
];
|
|
13
|
+
function isWidgetKind(value) {
|
|
14
|
+
return typeof value === "string" && WIDGET_KINDS.includes(value);
|
|
15
|
+
}
|
|
16
|
+
var VALUE_FORMATS = ["number", "currency", "percent"];
|
|
17
|
+
var MAX_GRAPH_SERIES = 8;
|
|
18
|
+
var DEFAULT_EMPTY_TEXT = "Nothing to show yet.";
|
|
19
|
+
function emptyWidget(text = DEFAULT_EMPTY_TEXT) {
|
|
20
|
+
return { empty: true, text };
|
|
21
|
+
}
|
|
22
|
+
function isWidgetEmptyState(payload) {
|
|
23
|
+
return "empty" in payload && payload.empty === true;
|
|
24
|
+
}
|
|
25
|
+
function emptyTextFor(payload) {
|
|
26
|
+
if (isWidgetEmptyState(payload)) {
|
|
27
|
+
return payload.text;
|
|
28
|
+
}
|
|
29
|
+
switch (payload.kind) {
|
|
30
|
+
case "TABLE":
|
|
31
|
+
return payload.rows.length === 0 ? DEFAULT_EMPTY_TEXT : null;
|
|
32
|
+
case "BAR_LIST":
|
|
33
|
+
return payload.items.length === 0 ? DEFAULT_EMPTY_TEXT : null;
|
|
34
|
+
case "ALERT_LIST":
|
|
35
|
+
return payload.items.length === 0 ? payload.emptyText : null;
|
|
36
|
+
case "GRAPH":
|
|
37
|
+
return payload.series.every((series) => series.points.length === 0) ? DEFAULT_EMPTY_TEXT : null;
|
|
38
|
+
case "TEXT":
|
|
39
|
+
case "KPI":
|
|
40
|
+
case "GAUGE":
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/core/compat.ts
|
|
46
|
+
function isStringArray(value) {
|
|
47
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
48
|
+
}
|
|
49
|
+
function upgradeLegacyWidgetData(value) {
|
|
50
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
const record = value;
|
|
54
|
+
if (record["kind"] === "TABLE" && isStringArray(record["columns"]) && Array.isArray(record["rows"]) && record["rows"].every(isStringArray)) {
|
|
55
|
+
const rows = record["rows"];
|
|
56
|
+
return {
|
|
57
|
+
...record,
|
|
58
|
+
columns: record["columns"].map((label) => ({ label })),
|
|
59
|
+
rows: rows.map((row) => row.map((text) => ({ text })))
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (record["kind"] === "GRAPH" && Array.isArray(record["points"]) && record["series"] === void 0) {
|
|
63
|
+
const { points, ...rest } = record;
|
|
64
|
+
return {
|
|
65
|
+
...rest,
|
|
66
|
+
series: [{ name: "Value", points }],
|
|
67
|
+
chartType: "bar",
|
|
68
|
+
valueFormat: "number"
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
function fromLegacyDefinition(legacy) {
|
|
74
|
+
if (!isWidgetKind(legacy.viewType)) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
key: legacy.widgetKey,
|
|
79
|
+
title: legacy.title,
|
|
80
|
+
...legacy.description === null ? {} : { description: legacy.description },
|
|
81
|
+
kind: legacy.viewType,
|
|
82
|
+
sortOrder: legacy.sortOrder
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/core/definition.ts
|
|
87
|
+
var WIDGET_SIZES = ["small", "medium", "large", "full"];
|
|
88
|
+
function defineWidget(definition) {
|
|
89
|
+
return definition;
|
|
90
|
+
}
|
|
91
|
+
function sortDefinitions(definitions) {
|
|
92
|
+
return definitions.map((definition, index) => ({ definition, index })).sort(
|
|
93
|
+
(a, b) => (a.definition.sortOrder ?? 0) - (b.definition.sortOrder ?? 0) || a.index - b.index
|
|
94
|
+
).map(({ definition }) => definition);
|
|
95
|
+
}
|
|
96
|
+
function isWidgetVisibleTo(definition, roles) {
|
|
97
|
+
if (definition.active === false) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
if (definition.roles === void 0 || definition.roles.length === 0) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
if (roles === void 0) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return definition.roles.some((role) => roles.includes(role));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/core/format.ts
|
|
110
|
+
var DEFAULT_CURRENCY = "USD";
|
|
111
|
+
function localeArgument(locale) {
|
|
112
|
+
return locale === void 0 || typeof locale === "string" ? locale : [...locale];
|
|
113
|
+
}
|
|
114
|
+
function numberFormat(locale, options) {
|
|
115
|
+
try {
|
|
116
|
+
return new Intl.NumberFormat(localeArgument(locale), options);
|
|
117
|
+
} catch {
|
|
118
|
+
const { style: _style, currency: _currency, ...rest } = options;
|
|
119
|
+
return new Intl.NumberFormat(void 0, rest);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function formatValue(value, format, options = {}) {
|
|
123
|
+
if (!Number.isFinite(value)) {
|
|
124
|
+
return "\u2014";
|
|
125
|
+
}
|
|
126
|
+
const compact = options.compact ? { notation: "compact", maximumFractionDigits: 1 } : {};
|
|
127
|
+
switch (format) {
|
|
128
|
+
case "number":
|
|
129
|
+
return numberFormat(options.locale, {
|
|
130
|
+
maximumFractionDigits: 2,
|
|
131
|
+
...compact
|
|
132
|
+
}).format(value);
|
|
133
|
+
case "currency": {
|
|
134
|
+
const currency = (options.currency ?? DEFAULT_CURRENCY).toUpperCase();
|
|
135
|
+
const formatter = numberFormat(options.locale, {
|
|
136
|
+
style: "currency",
|
|
137
|
+
currency,
|
|
138
|
+
...compact
|
|
139
|
+
});
|
|
140
|
+
const text = formatter.format(value);
|
|
141
|
+
return formatter.resolvedOptions().style === "currency" ? text : `${text} ${currency}`;
|
|
142
|
+
}
|
|
143
|
+
case "percent":
|
|
144
|
+
return numberFormat(options.locale, {
|
|
145
|
+
style: "percent",
|
|
146
|
+
maximumFractionDigits: 1,
|
|
147
|
+
...compact
|
|
148
|
+
}).format(value);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function formatCount(value, locale) {
|
|
152
|
+
return formatValue(value, "number", { locale });
|
|
153
|
+
}
|
|
154
|
+
function shareOf(part, total) {
|
|
155
|
+
return total <= 0 ? 0 : Math.round(part / total * 100);
|
|
156
|
+
}
|
|
157
|
+
function percentChange(current, previous) {
|
|
158
|
+
if (previous === null || previous === void 0 || previous === 0 || !Number.isFinite(previous) || !Number.isFinite(current)) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
return (current - previous) / Math.abs(previous) * 100;
|
|
162
|
+
}
|
|
163
|
+
function formatPercentChange(change, locale) {
|
|
164
|
+
const digits = Math.abs(change) < 10 ? 1 : 0;
|
|
165
|
+
const rounded = Number(change.toFixed(digits));
|
|
166
|
+
if (rounded === 0) {
|
|
167
|
+
return "\xB10%";
|
|
168
|
+
}
|
|
169
|
+
return numberFormat(locale, {
|
|
170
|
+
style: "percent",
|
|
171
|
+
maximumFractionDigits: digits,
|
|
172
|
+
signDisplay: "exceptZero"
|
|
173
|
+
}).format(rounded / 100);
|
|
174
|
+
}
|
|
175
|
+
var DEFAULT_COMPARISON_LABEL = "previous period";
|
|
176
|
+
function kpiDelta(current, previous, options = {}) {
|
|
177
|
+
const percent = percentChange(current, previous);
|
|
178
|
+
if (percent === null) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
const text = formatPercentChange(percent, options.locale);
|
|
182
|
+
const direction = text === "\xB10%" ? "flat" : percent > 0 ? "up" : "down";
|
|
183
|
+
const higherIsBetter = options.higherIsBetter ?? true;
|
|
184
|
+
const sentiment = direction === "flat" ? "neutral" : direction === "up" === higherIsBetter ? "good" : "bad";
|
|
185
|
+
return {
|
|
186
|
+
percent,
|
|
187
|
+
direction,
|
|
188
|
+
sentiment,
|
|
189
|
+
text,
|
|
190
|
+
description: `${text} vs ${options.comparisonLabel ?? DEFAULT_COMPARISON_LABEL}`
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/core/layout.ts
|
|
195
|
+
var EMPTY_LAYOUT = Object.freeze({
|
|
196
|
+
order: Object.freeze([]),
|
|
197
|
+
hidden: Object.freeze([]),
|
|
198
|
+
minimized: Object.freeze([])
|
|
199
|
+
});
|
|
200
|
+
function uniqueStrings(value) {
|
|
201
|
+
if (!Array.isArray(value)) return [];
|
|
202
|
+
const seen = /* @__PURE__ */ new Set();
|
|
203
|
+
for (const entry of value) {
|
|
204
|
+
if (typeof entry === "string" && entry !== "") seen.add(entry);
|
|
205
|
+
}
|
|
206
|
+
return [...seen];
|
|
207
|
+
}
|
|
208
|
+
function parseLayout(raw) {
|
|
209
|
+
let value = raw;
|
|
210
|
+
if (typeof raw === "string") {
|
|
211
|
+
try {
|
|
212
|
+
value = JSON.parse(raw);
|
|
213
|
+
} catch {
|
|
214
|
+
return EMPTY_LAYOUT;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
218
|
+
return EMPTY_LAYOUT;
|
|
219
|
+
}
|
|
220
|
+
const record = value;
|
|
221
|
+
const order = uniqueStrings(record["order"]);
|
|
222
|
+
const hidden = uniqueStrings(record["hidden"]);
|
|
223
|
+
const minimized = uniqueStrings(record["minimized"]);
|
|
224
|
+
if (order.length === 0 && hidden.length === 0 && minimized.length === 0) {
|
|
225
|
+
return EMPTY_LAYOUT;
|
|
226
|
+
}
|
|
227
|
+
return { order, hidden, minimized };
|
|
228
|
+
}
|
|
229
|
+
function serializeLayout(layout) {
|
|
230
|
+
return JSON.stringify({
|
|
231
|
+
order: [...layout.order],
|
|
232
|
+
hidden: [...layout.hidden],
|
|
233
|
+
minimized: [...layout.minimized]
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
function bySortOrder(items) {
|
|
237
|
+
return items.map((item, index) => ({ item, index })).sort(
|
|
238
|
+
(a, b) => (a.item.sortOrder ?? 0) - (b.item.sortOrder ?? 0) || a.index - b.index
|
|
239
|
+
).map(({ item }) => item);
|
|
240
|
+
}
|
|
241
|
+
function visibleWidgets(definitions, layout) {
|
|
242
|
+
const hidden = new Set(layout.hidden);
|
|
243
|
+
const visible = definitions.filter((d) => !hidden.has(d.key));
|
|
244
|
+
const byKey = new Map(visible.map((d) => [d.key, d]));
|
|
245
|
+
const ordered = [];
|
|
246
|
+
for (const key of layout.order) {
|
|
247
|
+
const definition = byKey.get(key);
|
|
248
|
+
if (definition !== void 0) {
|
|
249
|
+
ordered.push(definition);
|
|
250
|
+
byKey.delete(key);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
for (const definition of bySortOrder(visible)) {
|
|
254
|
+
if (byKey.has(definition.key)) ordered.push(definition);
|
|
255
|
+
}
|
|
256
|
+
return ordered;
|
|
257
|
+
}
|
|
258
|
+
function hiddenWidgets(definitions, layout) {
|
|
259
|
+
const hidden = new Set(layout.hidden);
|
|
260
|
+
return bySortOrder(definitions.filter((d) => hidden.has(d.key)));
|
|
261
|
+
}
|
|
262
|
+
function moveWidget(definitions, layout, key, toIndex) {
|
|
263
|
+
const current = visibleWidgets(definitions, layout).map(
|
|
264
|
+
(d) => d.key
|
|
265
|
+
);
|
|
266
|
+
const fromIndex = current.indexOf(key);
|
|
267
|
+
if (fromIndex === -1 || !Number.isInteger(toIndex) || toIndex < 0 || toIndex >= current.length || toIndex === fromIndex) {
|
|
268
|
+
return layout;
|
|
269
|
+
}
|
|
270
|
+
const next = [...current];
|
|
271
|
+
next.splice(fromIndex, 1);
|
|
272
|
+
next.splice(toIndex, 0, key);
|
|
273
|
+
return { ...layout, order: next };
|
|
274
|
+
}
|
|
275
|
+
function moveWidgetBy(definitions, layout, key, offset) {
|
|
276
|
+
const current = visibleWidgets(definitions, layout).map(
|
|
277
|
+
(d) => d.key
|
|
278
|
+
);
|
|
279
|
+
const fromIndex = current.indexOf(key);
|
|
280
|
+
if (fromIndex === -1) return layout;
|
|
281
|
+
const toIndex = Math.max(
|
|
282
|
+
0,
|
|
283
|
+
Math.min(current.length - 1, fromIndex + Math.trunc(offset))
|
|
284
|
+
);
|
|
285
|
+
return moveWidget(definitions, layout, key, toIndex);
|
|
286
|
+
}
|
|
287
|
+
function moveWidgetUp(definitions, layout, key) {
|
|
288
|
+
return moveWidgetBy(definitions, layout, key, -1);
|
|
289
|
+
}
|
|
290
|
+
function moveWidgetDown(definitions, layout, key) {
|
|
291
|
+
return moveWidgetBy(definitions, layout, key, 1);
|
|
292
|
+
}
|
|
293
|
+
function isHidden(layout, key) {
|
|
294
|
+
return layout.hidden.includes(key);
|
|
295
|
+
}
|
|
296
|
+
function isMinimized(layout, key) {
|
|
297
|
+
return layout.minimized.includes(key);
|
|
298
|
+
}
|
|
299
|
+
function hideWidget(layout, key) {
|
|
300
|
+
if (isHidden(layout, key)) return layout;
|
|
301
|
+
return { ...layout, hidden: [...layout.hidden, key] };
|
|
302
|
+
}
|
|
303
|
+
function showWidget(layout, key) {
|
|
304
|
+
if (!isHidden(layout, key)) return layout;
|
|
305
|
+
return {
|
|
306
|
+
...layout,
|
|
307
|
+
hidden: layout.hidden.filter((k) => k !== key),
|
|
308
|
+
minimized: layout.minimized.filter((k) => k !== key)
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function toggleHidden(layout, key) {
|
|
312
|
+
return isHidden(layout, key) ? showWidget(layout, key) : hideWidget(layout, key);
|
|
313
|
+
}
|
|
314
|
+
function minimizeWidget(layout, key) {
|
|
315
|
+
if (isMinimized(layout, key)) return layout;
|
|
316
|
+
return { ...layout, minimized: [...layout.minimized, key] };
|
|
317
|
+
}
|
|
318
|
+
function restoreWidget(layout, key) {
|
|
319
|
+
if (!isMinimized(layout, key)) return layout;
|
|
320
|
+
return { ...layout, minimized: layout.minimized.filter((k) => k !== key) };
|
|
321
|
+
}
|
|
322
|
+
function toggleMinimized(layout, key) {
|
|
323
|
+
return isMinimized(layout, key) ? restoreWidget(layout, key) : minimizeWidget(layout, key);
|
|
324
|
+
}
|
|
325
|
+
function pruneLayout(definitions, layout) {
|
|
326
|
+
const known = new Set(definitions.map((d) => d.key));
|
|
327
|
+
const keep = (keys) => keys.filter((k) => known.has(k));
|
|
328
|
+
const next = {
|
|
329
|
+
order: keep(layout.order),
|
|
330
|
+
hidden: keep(layout.hidden),
|
|
331
|
+
minimized: keep(layout.minimized)
|
|
332
|
+
};
|
|
333
|
+
return next.order.length === layout.order.length && next.hidden.length === layout.hidden.length && next.minimized.length === layout.minimized.length ? layout : next;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/core/result.ts
|
|
337
|
+
function ok(value) {
|
|
338
|
+
return { ok: true, value };
|
|
339
|
+
}
|
|
340
|
+
function err(error) {
|
|
341
|
+
return { ok: false, error };
|
|
342
|
+
}
|
|
343
|
+
function describeUnknownError(cause) {
|
|
344
|
+
if (cause instanceof Error) {
|
|
345
|
+
return cause.message === "" ? cause.name : cause.message;
|
|
346
|
+
}
|
|
347
|
+
if (typeof cause === "string") {
|
|
348
|
+
return cause;
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
const json = JSON.stringify(cause);
|
|
352
|
+
return json ?? String(cause);
|
|
353
|
+
} catch {
|
|
354
|
+
return String(cause);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/core/url.ts
|
|
359
|
+
var SCHEME_PATTERN = /^([a-z][a-z0-9+.-]*):/i;
|
|
360
|
+
var IGNORED_IN_SCHEME = /[\u0000- \u007f]/g;
|
|
361
|
+
function schemeOf(url) {
|
|
362
|
+
const match = SCHEME_PATTERN.exec(
|
|
363
|
+
url.replace(IGNORED_IN_SCHEME, "")
|
|
364
|
+
);
|
|
365
|
+
return match === null ? null : (match[1] ?? "").toLowerCase();
|
|
366
|
+
}
|
|
367
|
+
function isSafeHref(url) {
|
|
368
|
+
if (url.trim() === "") {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
const scheme = schemeOf(url);
|
|
372
|
+
return scheme === null || scheme === "http" || scheme === "https" || scheme === "mailto";
|
|
373
|
+
}
|
|
374
|
+
function isSafeImageUrl(url) {
|
|
375
|
+
if (url.trim() === "") {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
const scheme = schemeOf(url);
|
|
379
|
+
return scheme === null || scheme === "http" || scheme === "https";
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/core/validate.ts
|
|
383
|
+
var MAX_REPORTED_PROBLEMS = 10;
|
|
384
|
+
function describeType(value) {
|
|
385
|
+
if (value === null) return "null";
|
|
386
|
+
if (Array.isArray(value)) return "array";
|
|
387
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
388
|
+
return String(value);
|
|
389
|
+
}
|
|
390
|
+
return typeof value;
|
|
391
|
+
}
|
|
392
|
+
function isRecord(value) {
|
|
393
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
394
|
+
}
|
|
395
|
+
function join(path, field) {
|
|
396
|
+
return path === "" ? field : `${path}.${field}`;
|
|
397
|
+
}
|
|
398
|
+
function requireString(record, field, path, problems) {
|
|
399
|
+
const value = record[field];
|
|
400
|
+
if (typeof value !== "string") {
|
|
401
|
+
problems.push(
|
|
402
|
+
`${join(path, field)} must be a string, got ${describeType(value)}.`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function optionalString(record, field, path, problems) {
|
|
407
|
+
if (record[field] !== void 0) {
|
|
408
|
+
requireString(record, field, path, problems);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function requireNumber(record, field, path, problems) {
|
|
412
|
+
const value = record[field];
|
|
413
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
414
|
+
problems.push(
|
|
415
|
+
`${join(path, field)} must be a finite number, got ${describeType(value)}.`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function optionalNumber(record, field, path, problems) {
|
|
420
|
+
if (record[field] !== void 0) {
|
|
421
|
+
requireNumber(record, field, path, problems);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function optionalBoolean(record, field, path, problems) {
|
|
425
|
+
const value = record[field];
|
|
426
|
+
if (value !== void 0 && typeof value !== "boolean") {
|
|
427
|
+
problems.push(
|
|
428
|
+
`${join(path, field)} must be a boolean, got ${describeType(value)}.`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function optionalUrl(record, field, path, problems, isSafe, allowed) {
|
|
433
|
+
const value = record[field];
|
|
434
|
+
if (value === void 0) return;
|
|
435
|
+
if (typeof value !== "string") {
|
|
436
|
+
problems.push(
|
|
437
|
+
`${join(path, field)} must be a string, got ${describeType(value)}.`
|
|
438
|
+
);
|
|
439
|
+
} else if (!isSafe(value)) {
|
|
440
|
+
problems.push(
|
|
441
|
+
`${join(path, field)} must be ${allowed}, got ${JSON.stringify(value.slice(0, 80))}.`
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function requireArray(record, field, path, problems) {
|
|
446
|
+
const value = record[field];
|
|
447
|
+
if (!Array.isArray(value)) {
|
|
448
|
+
problems.push(
|
|
449
|
+
`${join(path, field)} must be an array, got ${describeType(value)}.`
|
|
450
|
+
);
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
return value;
|
|
454
|
+
}
|
|
455
|
+
function requireOneOf(record, field, options, path, problems) {
|
|
456
|
+
const value = record[field];
|
|
457
|
+
if (typeof value !== "string" || !options.includes(value)) {
|
|
458
|
+
problems.push(
|
|
459
|
+
`${join(path, field)} must be one of ${options.map((o) => `"${o}"`).join(", ")}, got ${typeof value === "string" ? JSON.stringify(value) : describeType(value)}.`
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function eachRecord(items, path, problems, check) {
|
|
464
|
+
items.forEach((item, index) => {
|
|
465
|
+
const itemPath = `${path}[${index}]`;
|
|
466
|
+
if (isRecord(item)) {
|
|
467
|
+
check(item, itemPath);
|
|
468
|
+
} else {
|
|
469
|
+
problems.push(
|
|
470
|
+
`${itemPath} must be an object, got ${describeType(item)}.`
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
var HREF_RULE = "an http(s), mailto or relative URL";
|
|
476
|
+
var IMAGE_RULE = "an http(s) or relative URL";
|
|
477
|
+
function checkPayloadFields(kind, data, problems) {
|
|
478
|
+
switch (kind) {
|
|
479
|
+
case "TEXT":
|
|
480
|
+
requireString(data, "value", "", problems);
|
|
481
|
+
requireString(data, "label", "", problems);
|
|
482
|
+
return;
|
|
483
|
+
case "KPI":
|
|
484
|
+
requireNumber(data, "value", "", problems);
|
|
485
|
+
if (data["previous"] !== null) {
|
|
486
|
+
optionalNumber(data, "previous", "", problems);
|
|
487
|
+
}
|
|
488
|
+
requireOneOf(data, "format", VALUE_FORMATS, "", problems);
|
|
489
|
+
optionalString(data, "currency", "", problems);
|
|
490
|
+
requireString(data, "label", "", problems);
|
|
491
|
+
optionalString(data, "hint", "", problems);
|
|
492
|
+
optionalBoolean(data, "higherIsBetter", "", problems);
|
|
493
|
+
return;
|
|
494
|
+
case "GAUGE":
|
|
495
|
+
requireNumber(data, "value", "", problems);
|
|
496
|
+
requireNumber(data, "max", "", problems);
|
|
497
|
+
requireString(data, "label", "", problems);
|
|
498
|
+
if (typeof data["max"] === "number" && data["max"] <= 0) {
|
|
499
|
+
problems.push(`max must be greater than 0, got ${data["max"]}.`);
|
|
500
|
+
}
|
|
501
|
+
return;
|
|
502
|
+
case "TABLE": {
|
|
503
|
+
const columns = requireArray(data, "columns", "", problems);
|
|
504
|
+
if (columns !== null) {
|
|
505
|
+
eachRecord(columns, "columns", problems, (column, path) => {
|
|
506
|
+
requireString(column, "label", path, problems);
|
|
507
|
+
optionalBoolean(column, "numeric", path, problems);
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
const rows = requireArray(data, "rows", "", problems);
|
|
511
|
+
if (rows !== null) {
|
|
512
|
+
rows.forEach((row, rowIndex) => {
|
|
513
|
+
const rowPath = `rows[${rowIndex}]`;
|
|
514
|
+
if (!Array.isArray(row)) {
|
|
515
|
+
problems.push(
|
|
516
|
+
`${rowPath} must be an array of cells, got ${describeType(row)}.`
|
|
517
|
+
);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (columns !== null && row.length !== columns.length) {
|
|
521
|
+
problems.push(
|
|
522
|
+
`${rowPath} has ${row.length} cell(s) but there are ${columns.length} column(s).`
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
eachRecord(row, rowPath, problems, (cell, cellPath) => {
|
|
526
|
+
requireString(cell, "text", cellPath, problems);
|
|
527
|
+
optionalUrl(
|
|
528
|
+
cell,
|
|
529
|
+
"href",
|
|
530
|
+
cellPath,
|
|
531
|
+
problems,
|
|
532
|
+
isSafeHref,
|
|
533
|
+
HREF_RULE
|
|
534
|
+
);
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
optionalString(data, "footer", "", problems);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
case "BAR_LIST": {
|
|
542
|
+
const items = requireArray(data, "items", "", problems);
|
|
543
|
+
if (items !== null) {
|
|
544
|
+
eachRecord(items, "items", problems, (item, path) => {
|
|
545
|
+
requireString(item, "label", path, problems);
|
|
546
|
+
requireNumber(item, "value", path, problems);
|
|
547
|
+
optionalString(item, "display", path, problems);
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
optionalNumber(data, "total", "", problems);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
case "ALERT_LIST": {
|
|
554
|
+
const items = requireArray(data, "items", "", problems);
|
|
555
|
+
if (items !== null) {
|
|
556
|
+
eachRecord(items, "items", problems, (item, path) => {
|
|
557
|
+
requireString(item, "title", path, problems);
|
|
558
|
+
optionalUrl(item, "href", path, problems, isSafeHref, HREF_RULE);
|
|
559
|
+
optionalUrl(
|
|
560
|
+
item,
|
|
561
|
+
"thumbnailUrl",
|
|
562
|
+
path,
|
|
563
|
+
problems,
|
|
564
|
+
isSafeImageUrl,
|
|
565
|
+
IMAGE_RULE
|
|
566
|
+
);
|
|
567
|
+
requireString(item, "valueLabel", path, problems);
|
|
568
|
+
optionalString(item, "detail", path, problems);
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
requireNumber(data, "total", "", problems);
|
|
572
|
+
requireString(data, "emptyText", "", problems);
|
|
573
|
+
if (items !== null && typeof data["total"] === "number" && data["total"] < items.length) {
|
|
574
|
+
problems.push(
|
|
575
|
+
`total (${data["total"]}) must be at least the number of items (${items.length}).`
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
case "GRAPH": {
|
|
581
|
+
const series = requireArray(data, "series", "", problems);
|
|
582
|
+
if (series !== null) {
|
|
583
|
+
if (series.length > MAX_GRAPH_SERIES) {
|
|
584
|
+
problems.push(
|
|
585
|
+
`series has ${series.length} entries; at most ${MAX_GRAPH_SERIES} are allowed (fold the rest into "Other").`
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
eachRecord(series, "series", problems, (entry, path) => {
|
|
589
|
+
requireString(entry, "name", path, problems);
|
|
590
|
+
const points = requireArray(entry, "points", path, problems);
|
|
591
|
+
if (points !== null) {
|
|
592
|
+
eachRecord(
|
|
593
|
+
points,
|
|
594
|
+
`${path}.points`,
|
|
595
|
+
problems,
|
|
596
|
+
(point, pointPath) => {
|
|
597
|
+
requireString(point, "label", pointPath, problems);
|
|
598
|
+
requireNumber(point, "value", pointPath, problems);
|
|
599
|
+
}
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
requireOneOf(data, "chartType", ["bar", "line"], "", problems);
|
|
605
|
+
requireOneOf(data, "valueFormat", VALUE_FORMATS, "", problems);
|
|
606
|
+
optionalString(data, "currency", "", problems);
|
|
607
|
+
optionalString(data, "xLabel", "", problems);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function formatProblems(prefix, problems) {
|
|
613
|
+
const shown = problems.slice(0, MAX_REPORTED_PROBLEMS);
|
|
614
|
+
const more = problems.length - shown.length;
|
|
615
|
+
return `${prefix}: ${shown.join(" ")}${more > 0 ? ` (and ${more} more problem(s))` : ""}`;
|
|
616
|
+
}
|
|
617
|
+
function validateWidgetData(value, options = {}) {
|
|
618
|
+
const subject = options.widgetKey === void 0 ? "Widget data" : `Widget ${JSON.stringify(options.widgetKey)}`;
|
|
619
|
+
if (!isRecord(value)) {
|
|
620
|
+
return err(`${subject}: must be an object, got ${describeType(value)}.`);
|
|
621
|
+
}
|
|
622
|
+
if (value["empty"] === true) {
|
|
623
|
+
if (typeof value["text"] !== "string") {
|
|
624
|
+
return err(
|
|
625
|
+
`${subject} (empty): text must be a string, got ${describeType(value["text"])}.`
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
return ok(value);
|
|
629
|
+
}
|
|
630
|
+
const kind = value["kind"];
|
|
631
|
+
if (!isWidgetKind(kind)) {
|
|
632
|
+
return err(
|
|
633
|
+
`${subject}: kind must be one of ${WIDGET_KINDS.join(", ")} (or the payload must be {"empty": true, "text": ...}), got ${typeof kind === "string" ? JSON.stringify(kind) : describeType(kind)}.`
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
if (options.expectedKind !== void 0 && kind !== options.expectedKind) {
|
|
637
|
+
return err(
|
|
638
|
+
`${subject}: expected kind ${options.expectedKind} from its definition, got ${kind}.`
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
const problems = [];
|
|
642
|
+
checkPayloadFields(kind, value, problems);
|
|
643
|
+
if (problems.length > 0) {
|
|
644
|
+
return err(formatProblems(`${subject} (${kind})`, problems));
|
|
645
|
+
}
|
|
646
|
+
return ok(value);
|
|
647
|
+
}
|
|
648
|
+
function validateWidgetDefinition(value) {
|
|
649
|
+
if (!isRecord(value)) {
|
|
650
|
+
return err(
|
|
651
|
+
`Widget definition: must be an object, got ${describeType(value)}.`
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
const subject = typeof value["key"] === "string" ? `Widget definition ${JSON.stringify(value["key"])}` : "Widget definition";
|
|
655
|
+
const problems = [];
|
|
656
|
+
requireString(value, "key", "", problems);
|
|
657
|
+
if (value["key"] === "") {
|
|
658
|
+
problems.push("key must not be empty.");
|
|
659
|
+
}
|
|
660
|
+
requireString(value, "title", "", problems);
|
|
661
|
+
optionalString(value, "description", "", problems);
|
|
662
|
+
requireOneOf(value, "kind", WIDGET_KINDS, "", problems);
|
|
663
|
+
optionalNumber(value, "sortOrder", "", problems);
|
|
664
|
+
if (value["roles"] !== void 0) {
|
|
665
|
+
const roles = requireArray(value, "roles", "", problems);
|
|
666
|
+
roles?.forEach((role, index) => {
|
|
667
|
+
if (typeof role !== "string") {
|
|
668
|
+
problems.push(
|
|
669
|
+
`roles[${index}] must be a string, got ${describeType(role)}.`
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
optionalBoolean(value, "active", "", problems);
|
|
675
|
+
if (value["defaultSize"] !== void 0) {
|
|
676
|
+
requireOneOf(value, "defaultSize", WIDGET_SIZES, "", problems);
|
|
677
|
+
}
|
|
678
|
+
if (problems.length > 0) {
|
|
679
|
+
return err(formatProblems(subject, problems));
|
|
680
|
+
}
|
|
681
|
+
return ok(value);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// src/core/resolve.ts
|
|
685
|
+
function resolvePayload(definition, payload, options = {}) {
|
|
686
|
+
let checked;
|
|
687
|
+
if (options.validate === false) {
|
|
688
|
+
checked = payload;
|
|
689
|
+
} else {
|
|
690
|
+
const result = validateWidgetData(payload, {
|
|
691
|
+
widgetKey: definition.key,
|
|
692
|
+
expectedKind: definition.kind
|
|
693
|
+
});
|
|
694
|
+
if (!result.ok) {
|
|
695
|
+
return { definition, status: "error", error: result.error };
|
|
696
|
+
}
|
|
697
|
+
checked = result.value;
|
|
698
|
+
}
|
|
699
|
+
const emptyText = emptyTextFor(checked);
|
|
700
|
+
if (emptyText !== null) {
|
|
701
|
+
return { definition, status: "empty", emptyText };
|
|
702
|
+
}
|
|
703
|
+
return { definition, status: "ok", data: checked };
|
|
704
|
+
}
|
|
705
|
+
function failedWidget(definition, error) {
|
|
706
|
+
return { definition, status: "error", error };
|
|
707
|
+
}
|
|
708
|
+
function loadingWidgets(definitions) {
|
|
709
|
+
return definitions.map((definition) => ({ definition, status: "loading" }));
|
|
710
|
+
}
|
|
711
|
+
async function resolveWidget(definition, provider, context, options = {}) {
|
|
712
|
+
if (provider === void 0) {
|
|
713
|
+
return failedWidget(
|
|
714
|
+
definition,
|
|
715
|
+
`Widget ${JSON.stringify(definition.key)}: no provider is registered for this key.`
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
let payload;
|
|
719
|
+
try {
|
|
720
|
+
payload = await provider(context, definition);
|
|
721
|
+
} catch (cause) {
|
|
722
|
+
return failedWidget(
|
|
723
|
+
definition,
|
|
724
|
+
`Widget ${JSON.stringify(definition.key)}: provider failed: ${describeUnknownError(cause)}`
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
return resolvePayload(definition, payload, options);
|
|
728
|
+
}
|
|
729
|
+
function widgetsFor(definitions, roles) {
|
|
730
|
+
return sortDefinitions(
|
|
731
|
+
definitions.filter((definition) => isWidgetVisibleTo(definition, roles))
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
async function resolveWidgets(definitions, providers, context, options = {}) {
|
|
735
|
+
const visible = widgetsFor(definitions, context.roles);
|
|
736
|
+
return Promise.all(
|
|
737
|
+
visible.map(
|
|
738
|
+
(definition) => resolveWidget(
|
|
739
|
+
definition,
|
|
740
|
+
Object.prototype.hasOwnProperty.call(providers, definition.key) ? providers[definition.key] : void 0,
|
|
741
|
+
context,
|
|
742
|
+
options
|
|
743
|
+
)
|
|
744
|
+
)
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// src/core/scale.ts
|
|
749
|
+
function niceStep(value) {
|
|
750
|
+
if (!(value > 0) || !Number.isFinite(value)) return 1;
|
|
751
|
+
const exponent = Math.floor(Math.log10(value));
|
|
752
|
+
const fraction = value / 10 ** exponent;
|
|
753
|
+
const nice = fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 5 ? 5 : 10;
|
|
754
|
+
return nice * 10 ** exponent;
|
|
755
|
+
}
|
|
756
|
+
function niceDomain(values, tickCount = 4) {
|
|
757
|
+
const finite = values.filter((v) => Number.isFinite(v));
|
|
758
|
+
let low = Math.min(0, ...finite);
|
|
759
|
+
let high = Math.max(0, ...finite);
|
|
760
|
+
if (low === high) {
|
|
761
|
+
high = 1;
|
|
762
|
+
low = 0;
|
|
763
|
+
}
|
|
764
|
+
const step = niceStep((high - low) / Math.max(1, tickCount));
|
|
765
|
+
const min = Math.floor(low / step) * step;
|
|
766
|
+
const max = Math.ceil(high / step) * step;
|
|
767
|
+
const ticks = [];
|
|
768
|
+
const count = Math.round((max - min) / step);
|
|
769
|
+
for (let index = 0; index <= count; index++) {
|
|
770
|
+
ticks.push(Number((min + index * step).toPrecision(12)));
|
|
771
|
+
}
|
|
772
|
+
return { min, max, ticks };
|
|
773
|
+
}
|
|
774
|
+
function labelStride(count, maxLabels = 6) {
|
|
775
|
+
return Math.max(1, Math.ceil(count / Math.max(1, maxLabels)));
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
exports.DEFAULT_COMPARISON_LABEL = DEFAULT_COMPARISON_LABEL;
|
|
779
|
+
exports.DEFAULT_CURRENCY = DEFAULT_CURRENCY;
|
|
780
|
+
exports.DEFAULT_EMPTY_TEXT = DEFAULT_EMPTY_TEXT;
|
|
781
|
+
exports.EMPTY_LAYOUT = EMPTY_LAYOUT;
|
|
782
|
+
exports.MAX_GRAPH_SERIES = MAX_GRAPH_SERIES;
|
|
783
|
+
exports.MAX_REPORTED_PROBLEMS = MAX_REPORTED_PROBLEMS;
|
|
784
|
+
exports.VALUE_FORMATS = VALUE_FORMATS;
|
|
785
|
+
exports.WIDGET_KINDS = WIDGET_KINDS;
|
|
786
|
+
exports.WIDGET_SIZES = WIDGET_SIZES;
|
|
787
|
+
exports.defineWidget = defineWidget;
|
|
788
|
+
exports.describeUnknownError = describeUnknownError;
|
|
789
|
+
exports.emptyTextFor = emptyTextFor;
|
|
790
|
+
exports.emptyWidget = emptyWidget;
|
|
791
|
+
exports.err = err;
|
|
792
|
+
exports.failedWidget = failedWidget;
|
|
793
|
+
exports.formatCount = formatCount;
|
|
794
|
+
exports.formatPercentChange = formatPercentChange;
|
|
795
|
+
exports.formatValue = formatValue;
|
|
796
|
+
exports.fromLegacyDefinition = fromLegacyDefinition;
|
|
797
|
+
exports.hiddenWidgets = hiddenWidgets;
|
|
798
|
+
exports.hideWidget = hideWidget;
|
|
799
|
+
exports.isHidden = isHidden;
|
|
800
|
+
exports.isMinimized = isMinimized;
|
|
801
|
+
exports.isSafeHref = isSafeHref;
|
|
802
|
+
exports.isSafeImageUrl = isSafeImageUrl;
|
|
803
|
+
exports.isWidgetEmptyState = isWidgetEmptyState;
|
|
804
|
+
exports.isWidgetKind = isWidgetKind;
|
|
805
|
+
exports.isWidgetVisibleTo = isWidgetVisibleTo;
|
|
806
|
+
exports.kpiDelta = kpiDelta;
|
|
807
|
+
exports.labelStride = labelStride;
|
|
808
|
+
exports.loadingWidgets = loadingWidgets;
|
|
809
|
+
exports.minimizeWidget = minimizeWidget;
|
|
810
|
+
exports.moveWidget = moveWidget;
|
|
811
|
+
exports.moveWidgetBy = moveWidgetBy;
|
|
812
|
+
exports.moveWidgetDown = moveWidgetDown;
|
|
813
|
+
exports.moveWidgetUp = moveWidgetUp;
|
|
814
|
+
exports.niceDomain = niceDomain;
|
|
815
|
+
exports.niceStep = niceStep;
|
|
816
|
+
exports.ok = ok;
|
|
817
|
+
exports.parseLayout = parseLayout;
|
|
818
|
+
exports.percentChange = percentChange;
|
|
819
|
+
exports.pruneLayout = pruneLayout;
|
|
820
|
+
exports.resolvePayload = resolvePayload;
|
|
821
|
+
exports.resolveWidget = resolveWidget;
|
|
822
|
+
exports.resolveWidgets = resolveWidgets;
|
|
823
|
+
exports.restoreWidget = restoreWidget;
|
|
824
|
+
exports.serializeLayout = serializeLayout;
|
|
825
|
+
exports.shareOf = shareOf;
|
|
826
|
+
exports.showWidget = showWidget;
|
|
827
|
+
exports.sortDefinitions = sortDefinitions;
|
|
828
|
+
exports.toggleHidden = toggleHidden;
|
|
829
|
+
exports.toggleMinimized = toggleMinimized;
|
|
830
|
+
exports.upgradeLegacyWidgetData = upgradeLegacyWidgetData;
|
|
831
|
+
exports.validateWidgetData = validateWidgetData;
|
|
832
|
+
exports.validateWidgetDefinition = validateWidgetDefinition;
|
|
833
|
+
exports.visibleWidgets = visibleWidgets;
|
|
834
|
+
exports.widgetsFor = widgetsFor;
|
|
835
|
+
//# sourceMappingURL=core.cjs.map
|
|
836
|
+
//# sourceMappingURL=core.cjs.map
|