@richardmcquiston01/dashboard-widgets-toolkit 0.1.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -8
- package/dist/core.cjs +690 -64
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +87 -18
- package/dist/core.d.ts +87 -18
- package/dist/core.js +666 -65
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +690 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +666 -65
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +1594 -72
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +164 -14
- package/dist/react.d.ts +164 -14
- package/dist/react.js +1592 -74
- package/dist/react.js.map +1 -1
- package/dist/{scale-WlRozDCf.d.cts → scale-dWDGE1zW.d.cts} +327 -108
- package/dist/{scale-WlRozDCf.d.ts → scale-dWDGE1zW.d.ts} +327 -108
- package/dist/styles.css +156 -6
- package/package.json +1 -1
package/dist/react.cjs
CHANGED
|
@@ -5,6 +5,80 @@ var jsxRuntime = require('react/jsx-runtime');
|
|
|
5
5
|
|
|
6
6
|
// src/react/card.tsx
|
|
7
7
|
|
|
8
|
+
// src/core/grid.ts
|
|
9
|
+
var WIDTH_COLUMNS = 12;
|
|
10
|
+
var MIN_WIDGET_WIDTH = 2;
|
|
11
|
+
var MAX_WIDGET_WIDTH = 12;
|
|
12
|
+
function isWidgetWidth(value) {
|
|
13
|
+
return typeof value === "number" && Number.isInteger(value) && value >= MIN_WIDGET_WIDTH && value <= MAX_WIDGET_WIDTH;
|
|
14
|
+
}
|
|
15
|
+
function widthForSize(size) {
|
|
16
|
+
switch (size) {
|
|
17
|
+
case "small":
|
|
18
|
+
return 3;
|
|
19
|
+
case "large":
|
|
20
|
+
return 6;
|
|
21
|
+
case "full":
|
|
22
|
+
return 12;
|
|
23
|
+
default:
|
|
24
|
+
return 4;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function itemWidth(item) {
|
|
28
|
+
return isWidgetWidth(item.width) ? item.width : widthForSize(item.defaultSize);
|
|
29
|
+
}
|
|
30
|
+
function usesWidthColumns(items) {
|
|
31
|
+
return items.some((item) => item.width !== void 0);
|
|
32
|
+
}
|
|
33
|
+
function fillsWidth(fill) {
|
|
34
|
+
return fill === "width" || fill === "both";
|
|
35
|
+
}
|
|
36
|
+
function fillsHeight(fill) {
|
|
37
|
+
return fill === "height" || fill === "both";
|
|
38
|
+
}
|
|
39
|
+
function baseColumnSpan(size, columns, largeSpan = 2) {
|
|
40
|
+
if (size === "full") return columns;
|
|
41
|
+
if (size === "large") return Math.min(largeSpan, columns);
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
function fillColumnSpans(items, columns, largeSpan = 2) {
|
|
45
|
+
return placeAndFill(
|
|
46
|
+
items,
|
|
47
|
+
columns,
|
|
48
|
+
(item, trackCount) => baseColumnSpan(item.defaultSize, trackCount, largeSpan)
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
function fillWidthSpans(items) {
|
|
52
|
+
return placeAndFill(items, WIDTH_COLUMNS, itemWidth);
|
|
53
|
+
}
|
|
54
|
+
function placeAndFill(items, columns, spanOf) {
|
|
55
|
+
const trackCount = Math.max(1, Math.floor(columns));
|
|
56
|
+
const spans = /* @__PURE__ */ new Map();
|
|
57
|
+
let row = [];
|
|
58
|
+
let used = 0;
|
|
59
|
+
function closeRow() {
|
|
60
|
+
const fillers = row.filter(({ item }) => fillsWidth(item.fill));
|
|
61
|
+
const leftover = trackCount - used;
|
|
62
|
+
if (fillers.length > 0) {
|
|
63
|
+
const share = Math.floor(leftover / fillers.length);
|
|
64
|
+
const remainder = leftover % fillers.length;
|
|
65
|
+
fillers.forEach(({ item, span }, index) => {
|
|
66
|
+
spans.set(item.key, span + share + (index < remainder ? 1 : 0));
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
row = [];
|
|
70
|
+
used = 0;
|
|
71
|
+
}
|
|
72
|
+
for (const item of items) {
|
|
73
|
+
const span = Math.min(spanOf(item, trackCount), trackCount);
|
|
74
|
+
if (used + span > trackCount && row.length > 0) closeRow();
|
|
75
|
+
row.push({ item, span });
|
|
76
|
+
used += span;
|
|
77
|
+
}
|
|
78
|
+
closeRow();
|
|
79
|
+
return spans;
|
|
80
|
+
}
|
|
81
|
+
|
|
8
82
|
// src/core/format.ts
|
|
9
83
|
var DEFAULT_CURRENCY = "USD";
|
|
10
84
|
function localeArgument(locale) {
|
|
@@ -148,11 +222,24 @@ var DEFAULT_LABELS = {
|
|
|
148
222
|
viewAsTable: "View as table",
|
|
149
223
|
hide: (title) => `Hide ${title}`,
|
|
150
224
|
show: (title) => `Show ${title}`,
|
|
151
|
-
minimize: (title) => `
|
|
225
|
+
minimize: (title) => `Minimize ${title}`,
|
|
152
226
|
expand: (title) => `Expand ${title}`,
|
|
153
227
|
moveEarlier: (title) => `Move ${title} earlier`,
|
|
154
228
|
moveLater: (title) => `Move ${title} later`,
|
|
155
229
|
hiddenWidgets: "Hidden:",
|
|
230
|
+
minimizedWidgets: "Minimized:",
|
|
231
|
+
opensInNewTab: "(opens in a new tab)",
|
|
232
|
+
view: (title) => `View ${title}`,
|
|
233
|
+
close: "Close",
|
|
234
|
+
search: "Search",
|
|
235
|
+
filterBy: (column) => `Filter ${column}`,
|
|
236
|
+
sortBy: (column) => `Sort by ${column}`,
|
|
237
|
+
noResults: "No matching results.",
|
|
238
|
+
showingRows: (from, to, total) => `Showing ${from}\u2013${to} of ${total} results`,
|
|
239
|
+
previousPage: "Previous",
|
|
240
|
+
nextPage: "Next",
|
|
241
|
+
pageOf: (page, pageCount) => `Page ${page} of ${pageCount}`,
|
|
242
|
+
pagination: "Pagination",
|
|
156
243
|
allHidden: "Every widget is hidden. Show one from the list above.",
|
|
157
244
|
noWidgets: "No widgets to show.",
|
|
158
245
|
andMore: (count) => `and ${count} more`,
|
|
@@ -667,16 +754,50 @@ function isSafeImageUrl(url) {
|
|
|
667
754
|
const scheme = schemeOf(url);
|
|
668
755
|
return scheme === null || scheme === "http" || scheme === "https";
|
|
669
756
|
}
|
|
757
|
+
function isNewTabTarget(target) {
|
|
758
|
+
return target !== void 0 && target !== "" && target !== "_self";
|
|
759
|
+
}
|
|
670
760
|
function WidgetLink({
|
|
671
761
|
href,
|
|
672
762
|
className,
|
|
673
763
|
children
|
|
674
764
|
}) {
|
|
675
|
-
const { linkTarget } = useWidgetSettings();
|
|
765
|
+
const { linkTarget, labels } = useWidgetSettings();
|
|
676
766
|
if (href === void 0 || !isSafeHref(href)) {
|
|
677
767
|
return /* @__PURE__ */ jsxRuntime.jsx("span", { className, children });
|
|
678
768
|
}
|
|
679
|
-
|
|
769
|
+
const opensNewTab = isNewTabTarget(linkTarget);
|
|
770
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("a", { href, target: linkTarget, rel: "noreferrer", className, children: [
|
|
771
|
+
children,
|
|
772
|
+
opensNewTab && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
773
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
774
|
+
"svg",
|
|
775
|
+
{
|
|
776
|
+
className: "dwt-external-icon",
|
|
777
|
+
viewBox: "0 0 12 12",
|
|
778
|
+
width: "1em",
|
|
779
|
+
height: "1em",
|
|
780
|
+
"aria-hidden": "true",
|
|
781
|
+
focusable: "false",
|
|
782
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
783
|
+
"path",
|
|
784
|
+
{
|
|
785
|
+
d: "M7 1h4v4M11 1 5.5 6.5M9 7.5V10a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h2.5",
|
|
786
|
+
fill: "none",
|
|
787
|
+
stroke: "currentColor",
|
|
788
|
+
strokeWidth: "1.4",
|
|
789
|
+
strokeLinecap: "round",
|
|
790
|
+
strokeLinejoin: "round"
|
|
791
|
+
}
|
|
792
|
+
)
|
|
793
|
+
}
|
|
794
|
+
),
|
|
795
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "dwt-visually-hidden", children: [
|
|
796
|
+
" ",
|
|
797
|
+
labels.opensInNewTab
|
|
798
|
+
] })
|
|
799
|
+
] })
|
|
800
|
+
] });
|
|
680
801
|
}
|
|
681
802
|
function Thumbnail({
|
|
682
803
|
url
|
|
@@ -898,12 +1019,17 @@ function WidgetCard({
|
|
|
898
1019
|
actions,
|
|
899
1020
|
minimized = false,
|
|
900
1021
|
onToggleMinimized,
|
|
1022
|
+
onView,
|
|
901
1023
|
status = "ok",
|
|
902
1024
|
emptyText,
|
|
903
1025
|
error,
|
|
904
1026
|
onRetry,
|
|
905
1027
|
footer,
|
|
906
1028
|
size,
|
|
1029
|
+
fill,
|
|
1030
|
+
columnSpan,
|
|
1031
|
+
width,
|
|
1032
|
+
widgetKey,
|
|
907
1033
|
headingLevel = 2,
|
|
908
1034
|
className,
|
|
909
1035
|
children
|
|
@@ -949,8 +1075,17 @@ function WidgetCard({
|
|
|
949
1075
|
`dwt-card--${status}`,
|
|
950
1076
|
minimized && "dwt-card--minimized",
|
|
951
1077
|
size !== void 0 && `dwt-card--size-${size}`,
|
|
1078
|
+
fillsHeight(fill) && "dwt-card--fill-height",
|
|
1079
|
+
fillsWidth(fill) && "dwt-card--fill-width",
|
|
952
1080
|
className
|
|
953
1081
|
),
|
|
1082
|
+
...widgetKey === void 0 ? {} : { "data-widget-key": widgetKey },
|
|
1083
|
+
...columnSpan === void 0 && width === void 0 ? {} : {
|
|
1084
|
+
style: {
|
|
1085
|
+
...width === void 0 ? {} : { "--dwt-width": width },
|
|
1086
|
+
...columnSpan === void 0 ? {} : { gridColumn: `span ${columnSpan}` }
|
|
1087
|
+
}
|
|
1088
|
+
},
|
|
954
1089
|
children: [
|
|
955
1090
|
/* @__PURE__ */ jsxRuntime.jsxs("header", { className: slot("cardHeader", "dwt-card-header"), children: [
|
|
956
1091
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "dwt-card-heading", children: [
|
|
@@ -959,12 +1094,54 @@ function WidgetCard({
|
|
|
959
1094
|
{
|
|
960
1095
|
id: `${id}-title`,
|
|
961
1096
|
className: slot("cardTitle", "dwt-card-title"),
|
|
962
|
-
children: title
|
|
1097
|
+
children: onView === void 0 ? title : /* @__PURE__ */ jsxRuntime.jsx(
|
|
1098
|
+
"button",
|
|
1099
|
+
{
|
|
1100
|
+
type: "button",
|
|
1101
|
+
tabIndex: -1,
|
|
1102
|
+
className: "dwt-card-title-button",
|
|
1103
|
+
onClick: onView,
|
|
1104
|
+
children: title
|
|
1105
|
+
}
|
|
1106
|
+
)
|
|
963
1107
|
}
|
|
964
1108
|
),
|
|
965
1109
|
description !== void 0 && !minimized && /* @__PURE__ */ jsxRuntime.jsx("p", { className: slot("cardDescription", "dwt-card-description"), children: description })
|
|
966
1110
|
] }),
|
|
967
|
-
(actions !== void 0 || onToggleMinimized !== void 0) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: slot("cardActions", "dwt-card-actions"), children: [
|
|
1111
|
+
(actions !== void 0 || onToggleMinimized !== void 0 || onView !== void 0) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: slot("cardActions", "dwt-card-actions"), children: [
|
|
1112
|
+
onView !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1113
|
+
"button",
|
|
1114
|
+
{
|
|
1115
|
+
type: "button",
|
|
1116
|
+
className: slot("button", "dwt-button", "dwt-icon-button"),
|
|
1117
|
+
"aria-label": labels.view(title),
|
|
1118
|
+
title: labels.view(title),
|
|
1119
|
+
onClick: onView,
|
|
1120
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1121
|
+
"svg",
|
|
1122
|
+
{
|
|
1123
|
+
viewBox: "0 0 16 16",
|
|
1124
|
+
width: "16",
|
|
1125
|
+
height: "16",
|
|
1126
|
+
"aria-hidden": "true",
|
|
1127
|
+
focusable: "false",
|
|
1128
|
+
children: [
|
|
1129
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1130
|
+
"path",
|
|
1131
|
+
{
|
|
1132
|
+
d: "M1 8s2.6-4.5 7-4.5S15 8 15 8s-2.6 4.5-7 4.5S1 8 1 8Z",
|
|
1133
|
+
fill: "none",
|
|
1134
|
+
stroke: "currentColor",
|
|
1135
|
+
strokeWidth: "1.4",
|
|
1136
|
+
strokeLinejoin: "round"
|
|
1137
|
+
}
|
|
1138
|
+
),
|
|
1139
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "8", cy: "8", r: "2.1", fill: "currentColor" })
|
|
1140
|
+
]
|
|
1141
|
+
}
|
|
1142
|
+
)
|
|
1143
|
+
}
|
|
1144
|
+
),
|
|
968
1145
|
actions,
|
|
969
1146
|
onToggleMinimized !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
970
1147
|
"button",
|
|
@@ -1010,10 +1187,12 @@ function ResolvedWidgetCard({
|
|
|
1010
1187
|
WidgetCard,
|
|
1011
1188
|
{
|
|
1012
1189
|
...props,
|
|
1190
|
+
widgetKey: definition.key,
|
|
1013
1191
|
title: widgetTitle(widget),
|
|
1014
1192
|
status: widget.status,
|
|
1015
1193
|
...definition.description === void 0 ? {} : { description: definition.description },
|
|
1016
1194
|
...definition.defaultSize === void 0 ? {} : { size: definition.defaultSize },
|
|
1195
|
+
...definition.fill === void 0 ? {} : { fill: definition.fill },
|
|
1017
1196
|
...widget.status === "empty" ? { emptyText: widget.emptyText } : {},
|
|
1018
1197
|
...widget.status === "error" ? { error: widget.error } : {},
|
|
1019
1198
|
children: widget.status === "ok" ? /* @__PURE__ */ jsxRuntime.jsx(WidgetContent, { data: widget.data }) : null
|
|
@@ -1107,6 +1286,583 @@ function restoreWidget(layout, key) {
|
|
|
1107
1286
|
function toggleMinimized(layout, key) {
|
|
1108
1287
|
return isMinimized(layout, key) ? restoreWidget(layout, key) : minimizeWidget(layout, key);
|
|
1109
1288
|
}
|
|
1289
|
+
|
|
1290
|
+
// src/core/result.ts
|
|
1291
|
+
function ok(value) {
|
|
1292
|
+
return { ok: true, value };
|
|
1293
|
+
}
|
|
1294
|
+
function err(error) {
|
|
1295
|
+
return { ok: false, error };
|
|
1296
|
+
}
|
|
1297
|
+
function describeUnknownError(cause) {
|
|
1298
|
+
if (cause instanceof Error) {
|
|
1299
|
+
return cause.message === "" ? cause.name : cause.message;
|
|
1300
|
+
}
|
|
1301
|
+
if (typeof cause === "string") {
|
|
1302
|
+
return cause;
|
|
1303
|
+
}
|
|
1304
|
+
try {
|
|
1305
|
+
const json = JSON.stringify(cause);
|
|
1306
|
+
return json ?? String(cause);
|
|
1307
|
+
} catch {
|
|
1308
|
+
return String(cause);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// src/core/detail.ts
|
|
1313
|
+
var DEFAULT_DETAIL_PAGE_SIZE = 25;
|
|
1314
|
+
function resolveDetailOptions(definition) {
|
|
1315
|
+
const detail = definition.detail;
|
|
1316
|
+
if (detail === void 0 || detail === false) return void 0;
|
|
1317
|
+
const options = detail === true ? {} : detail;
|
|
1318
|
+
return {
|
|
1319
|
+
title: options.title ?? definition.title,
|
|
1320
|
+
pageSize: options.pageSize ?? DEFAULT_DETAIL_PAGE_SIZE,
|
|
1321
|
+
mode: options.mode ?? "client"
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
function defaultDetailQuery(pageSize = DEFAULT_DETAIL_PAGE_SIZE) {
|
|
1325
|
+
return { page: 1, pageSize };
|
|
1326
|
+
}
|
|
1327
|
+
function fold(text) {
|
|
1328
|
+
return text.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
|
|
1329
|
+
}
|
|
1330
|
+
function cellValue(cell) {
|
|
1331
|
+
return cell.value ?? cell.text;
|
|
1332
|
+
}
|
|
1333
|
+
function cellMatches(cell, needle) {
|
|
1334
|
+
return fold(String(cellValue(cell))).includes(needle);
|
|
1335
|
+
}
|
|
1336
|
+
function compareCells(a, b, collator) {
|
|
1337
|
+
const left = cellValue(a);
|
|
1338
|
+
const right = cellValue(b);
|
|
1339
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
1340
|
+
return left - right;
|
|
1341
|
+
}
|
|
1342
|
+
if (typeof left === "number") return -1;
|
|
1343
|
+
if (typeof right === "number") return 1;
|
|
1344
|
+
return collator.compare(left, right);
|
|
1345
|
+
}
|
|
1346
|
+
function queryRows(data, query, locale) {
|
|
1347
|
+
const indexOf = new Map(
|
|
1348
|
+
data.columns.map((column, index) => [column.key, index])
|
|
1349
|
+
);
|
|
1350
|
+
const filterEntries = [];
|
|
1351
|
+
for (const [key, text] of Object.entries(query.filters ?? {})) {
|
|
1352
|
+
const index = indexOf.get(key);
|
|
1353
|
+
if (index === void 0) {
|
|
1354
|
+
return err(
|
|
1355
|
+
`Detail query: filter column "${key}" does not exist. Columns: ${listKeys(data)}.`
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
const needle = fold(text.trim());
|
|
1359
|
+
if (needle !== "") filterEntries.push({ index, needle });
|
|
1360
|
+
}
|
|
1361
|
+
let sortIndex = -1;
|
|
1362
|
+
if (query.sort !== void 0) {
|
|
1363
|
+
const found = indexOf.get(query.sort.column);
|
|
1364
|
+
const column = found === void 0 ? void 0 : data.columns[found];
|
|
1365
|
+
if (found === void 0 || column === void 0) {
|
|
1366
|
+
return err(
|
|
1367
|
+
`Detail query: sort column "${query.sort.column}" does not exist. Columns: ${listKeys(data)}.`
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
if (column.sortable === false) {
|
|
1371
|
+
return err(
|
|
1372
|
+
`Detail query: column "${column.key}" is not sortable (sortable: false).`
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
sortIndex = found;
|
|
1376
|
+
}
|
|
1377
|
+
const search = fold((query.search ?? "").trim());
|
|
1378
|
+
let rows = data.rows.filter(
|
|
1379
|
+
(row) => filterEntries.every(({ index, needle }) => {
|
|
1380
|
+
const cell = row[index];
|
|
1381
|
+
return cell !== void 0 && cellMatches(cell, needle);
|
|
1382
|
+
}) && (search === "" || row.some((cell) => cellMatches(cell, search)))
|
|
1383
|
+
);
|
|
1384
|
+
if (query.sort !== void 0 && sortIndex >= 0) {
|
|
1385
|
+
const collator = new Intl.Collator(locale, {
|
|
1386
|
+
numeric: true,
|
|
1387
|
+
sensitivity: "base"
|
|
1388
|
+
});
|
|
1389
|
+
const sign = query.sort.direction === "desc" ? -1 : 1;
|
|
1390
|
+
rows = rows.map((row, position) => ({ row, position })).sort((a, b) => {
|
|
1391
|
+
const left = a.row[sortIndex];
|
|
1392
|
+
const right = b.row[sortIndex];
|
|
1393
|
+
const order = left === void 0 || right === void 0 ? 0 : sign * compareCells(left, right, collator);
|
|
1394
|
+
return order || a.position - b.position;
|
|
1395
|
+
}).map(({ row }) => row);
|
|
1396
|
+
}
|
|
1397
|
+
const pageSize = Math.max(1, Math.floor(query.pageSize));
|
|
1398
|
+
const pageCount = Math.max(1, Math.ceil(rows.length / pageSize));
|
|
1399
|
+
const page = Math.min(Math.max(1, Math.floor(query.page)), pageCount);
|
|
1400
|
+
return ok({
|
|
1401
|
+
rows: rows.slice((page - 1) * pageSize, page * pageSize),
|
|
1402
|
+
totalRows: rows.length,
|
|
1403
|
+
page,
|
|
1404
|
+
pageCount
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
function listKeys(data) {
|
|
1408
|
+
return data.columns.map((column) => `"${column.key}"`).join(", ");
|
|
1409
|
+
}
|
|
1410
|
+
function describeType(value) {
|
|
1411
|
+
if (value === null) return "null";
|
|
1412
|
+
if (Array.isArray(value)) return "array";
|
|
1413
|
+
return typeof value;
|
|
1414
|
+
}
|
|
1415
|
+
function validateDetailData(data, subject = "Detail data") {
|
|
1416
|
+
const problems = [];
|
|
1417
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
1418
|
+
return err(`${subject}: must be an object, got ${describeType(data)}.`);
|
|
1419
|
+
}
|
|
1420
|
+
const record = data;
|
|
1421
|
+
const columns = record["columns"];
|
|
1422
|
+
const rows = record["rows"];
|
|
1423
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1424
|
+
let columnCount = 0;
|
|
1425
|
+
if (!Array.isArray(columns) || columns.length === 0) {
|
|
1426
|
+
problems.push(
|
|
1427
|
+
`columns must be a non-empty array, got ${describeType(columns)}.`
|
|
1428
|
+
);
|
|
1429
|
+
} else {
|
|
1430
|
+
columnCount = columns.length;
|
|
1431
|
+
columns.forEach((column, index) => {
|
|
1432
|
+
const at = `columns[${index}]`;
|
|
1433
|
+
if (typeof column !== "object" || column === null) {
|
|
1434
|
+
problems.push(`${at} must be an object, got ${describeType(column)}.`);
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
const fields = column;
|
|
1438
|
+
const key = fields["key"];
|
|
1439
|
+
if (typeof key !== "string" || key === "") {
|
|
1440
|
+
problems.push(`${at}.key must be a non-empty string.`);
|
|
1441
|
+
} else if (keys.has(key)) {
|
|
1442
|
+
problems.push(`${at}.key "${key}" is used by an earlier column.`);
|
|
1443
|
+
} else {
|
|
1444
|
+
keys.add(key);
|
|
1445
|
+
}
|
|
1446
|
+
if (typeof fields["label"] !== "string") {
|
|
1447
|
+
problems.push(
|
|
1448
|
+
`${at}.label must be a string, got ${describeType(fields["label"])}.`
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
for (const flag of ["numeric", "sortable", "filterable"]) {
|
|
1452
|
+
const flagValue = fields[flag];
|
|
1453
|
+
if (flagValue !== void 0 && typeof flagValue !== "boolean") {
|
|
1454
|
+
problems.push(
|
|
1455
|
+
`${at}.${flag} must be a boolean, got ${describeType(flagValue)}.`
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
if (!Array.isArray(rows)) {
|
|
1462
|
+
problems.push(`rows must be an array, got ${describeType(rows)}.`);
|
|
1463
|
+
} else {
|
|
1464
|
+
rows.forEach((row, rowIndex) => {
|
|
1465
|
+
if (!Array.isArray(row)) {
|
|
1466
|
+
problems.push(
|
|
1467
|
+
`rows[${rowIndex}] must be an array of cells, got ${describeType(row)}.`
|
|
1468
|
+
);
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
if (columnCount > 0 && row.length !== columnCount) {
|
|
1472
|
+
problems.push(
|
|
1473
|
+
`rows[${rowIndex}] has ${row.length} cells but there are ${columnCount} columns.`
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
row.forEach((cell, cellIndex) => {
|
|
1477
|
+
const at = `rows[${rowIndex}][${cellIndex}]`;
|
|
1478
|
+
if (typeof cell !== "object" || cell === null) {
|
|
1479
|
+
problems.push(`${at} must be an object, got ${describeType(cell)}.`);
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
const fields = cell;
|
|
1483
|
+
if (typeof fields["text"] !== "string") {
|
|
1484
|
+
problems.push(
|
|
1485
|
+
`${at}.text must be a string, got ${describeType(fields["text"])}.`
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
const value = fields["value"];
|
|
1489
|
+
if (value !== void 0 && typeof value !== "string" && !(typeof value === "number" && Number.isFinite(value))) {
|
|
1490
|
+
problems.push(
|
|
1491
|
+
`${at}.value must be a string or a finite number, got ${describeType(value)}.`
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1494
|
+
const href = fields["href"];
|
|
1495
|
+
if (href !== void 0) {
|
|
1496
|
+
if (typeof href !== "string") {
|
|
1497
|
+
problems.push(
|
|
1498
|
+
`${at}.href must be a string, got ${describeType(href)}.`
|
|
1499
|
+
);
|
|
1500
|
+
} else if (!isSafeHref(href)) {
|
|
1501
|
+
problems.push(
|
|
1502
|
+
`${at}.href "${href}" is not allowed (only http, https, mailto and relative URLs).`
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
const totalRows = record["totalRows"];
|
|
1510
|
+
if (totalRows !== void 0 && !(typeof totalRows === "number" && Number.isInteger(totalRows) && totalRows >= 0)) {
|
|
1511
|
+
problems.push(
|
|
1512
|
+
`totalRows must be a non-negative whole number, got ${describeType(totalRows)}.`
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
if (problems.length > 0) {
|
|
1516
|
+
return err(`${subject}: ${problems.join(" ")}`);
|
|
1517
|
+
}
|
|
1518
|
+
return ok(data);
|
|
1519
|
+
}
|
|
1520
|
+
function deriveDetailData(data, locale) {
|
|
1521
|
+
if (data.kind === "TABLE") {
|
|
1522
|
+
if (data.footer !== void 0) return void 0;
|
|
1523
|
+
return {
|
|
1524
|
+
columns: data.columns.map((column, index) => ({
|
|
1525
|
+
key: `c${index}`,
|
|
1526
|
+
label: column.label,
|
|
1527
|
+
...column.numeric === void 0 ? {} : { numeric: column.numeric }
|
|
1528
|
+
})),
|
|
1529
|
+
rows: data.rows
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
if (data.kind === "BAR_LIST") {
|
|
1533
|
+
return {
|
|
1534
|
+
columns: [
|
|
1535
|
+
{ key: "label", label: "Name" },
|
|
1536
|
+
{ key: "value", label: "Value", numeric: true }
|
|
1537
|
+
],
|
|
1538
|
+
rows: data.items.map((item) => [
|
|
1539
|
+
{ text: item.label },
|
|
1540
|
+
{
|
|
1541
|
+
text: item.display ?? formatValue(item.value, "number", { locale }),
|
|
1542
|
+
value: item.value
|
|
1543
|
+
}
|
|
1544
|
+
])
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
return void 0;
|
|
1548
|
+
}
|
|
1549
|
+
function nextSort(current, column) {
|
|
1550
|
+
if (current?.column !== column) return { column, direction: "asc" };
|
|
1551
|
+
return current.direction === "asc" ? { column, direction: "desc" } : void 0;
|
|
1552
|
+
}
|
|
1553
|
+
function withSort(query, sort) {
|
|
1554
|
+
const { sort: _previous, ...rest } = query;
|
|
1555
|
+
return sort === void 0 ? { ...rest, page: 1 } : { ...rest, sort, page: 1 };
|
|
1556
|
+
}
|
|
1557
|
+
function SortGlyph({
|
|
1558
|
+
sort,
|
|
1559
|
+
column
|
|
1560
|
+
}) {
|
|
1561
|
+
if (sort?.column !== column) return /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: " \u2195" });
|
|
1562
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: sort.direction === "asc" ? " \u2191" : " \u2193" });
|
|
1563
|
+
}
|
|
1564
|
+
function Cell({
|
|
1565
|
+
cell,
|
|
1566
|
+
column
|
|
1567
|
+
}) {
|
|
1568
|
+
return /* @__PURE__ */ jsxRuntime.jsx("td", { className: column?.numeric === true ? "dwt-numeric" : void 0, children: cell.href === void 0 ? cell.text : /* @__PURE__ */ jsxRuntime.jsx(WidgetLink, { href: cell.href, className: "dwt-link", children: cell.text }) });
|
|
1569
|
+
}
|
|
1570
|
+
function WidgetDetail({
|
|
1571
|
+
title,
|
|
1572
|
+
data,
|
|
1573
|
+
query,
|
|
1574
|
+
onQueryChange,
|
|
1575
|
+
status = "ok",
|
|
1576
|
+
error,
|
|
1577
|
+
onRetry,
|
|
1578
|
+
headingLevel = 2,
|
|
1579
|
+
headingId,
|
|
1580
|
+
className
|
|
1581
|
+
}) {
|
|
1582
|
+
const slot = useSlotClassName();
|
|
1583
|
+
const { labels, locale } = useWidgetSettings();
|
|
1584
|
+
const id = react.useId();
|
|
1585
|
+
const Heading = `h${headingLevel}`;
|
|
1586
|
+
const localeTag = typeof locale === "string" ? locale : locale?.[0];
|
|
1587
|
+
let page;
|
|
1588
|
+
let problem;
|
|
1589
|
+
if (status === "ok" && data !== void 0) {
|
|
1590
|
+
const result = queryRows(data, query, localeTag);
|
|
1591
|
+
if (result.ok) page = result.value;
|
|
1592
|
+
else problem = result.error;
|
|
1593
|
+
}
|
|
1594
|
+
function change(patch) {
|
|
1595
|
+
onQueryChange({ ...query, ...patch, page: patch.page ?? 1 });
|
|
1596
|
+
}
|
|
1597
|
+
let body;
|
|
1598
|
+
if (status === "loading") {
|
|
1599
|
+
body = /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", className: slot("status", "dwt-status"), children: labels.loading });
|
|
1600
|
+
} else if (status === "error" || problem !== void 0) {
|
|
1601
|
+
body = /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "alert", className: slot("error", "dwt-error"), children: [
|
|
1602
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "dwt-error-message", children: error ?? problem ?? "" }),
|
|
1603
|
+
onRetry !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1604
|
+
"button",
|
|
1605
|
+
{
|
|
1606
|
+
type: "button",
|
|
1607
|
+
className: slot("button", "dwt-button"),
|
|
1608
|
+
onClick: onRetry,
|
|
1609
|
+
children: labels.retry
|
|
1610
|
+
}
|
|
1611
|
+
)
|
|
1612
|
+
] });
|
|
1613
|
+
} else if (data !== void 0 && page !== void 0) {
|
|
1614
|
+
const columns = data.columns;
|
|
1615
|
+
const from = (page.page - 1) * query.pageSize + 1;
|
|
1616
|
+
const to = from + page.rows.length - 1;
|
|
1617
|
+
body = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1618
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "dwt-detail-controls", children: [
|
|
1619
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "dwt-detail-field", children: [
|
|
1620
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: labels.search }),
|
|
1621
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1622
|
+
"input",
|
|
1623
|
+
{
|
|
1624
|
+
type: "search",
|
|
1625
|
+
value: query.search ?? "",
|
|
1626
|
+
onChange: (event) => change({ search: event.target.value })
|
|
1627
|
+
}
|
|
1628
|
+
)
|
|
1629
|
+
] }),
|
|
1630
|
+
columns.filter((column) => column.filterable === true).map((column) => /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "dwt-detail-field", children: [
|
|
1631
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: labels.filterBy(column.label) }),
|
|
1632
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1633
|
+
"input",
|
|
1634
|
+
{
|
|
1635
|
+
type: "search",
|
|
1636
|
+
value: query.filters?.[column.key] ?? "",
|
|
1637
|
+
onChange: (event) => change({
|
|
1638
|
+
filters: {
|
|
1639
|
+
...query.filters,
|
|
1640
|
+
[column.key]: event.target.value
|
|
1641
|
+
}
|
|
1642
|
+
})
|
|
1643
|
+
}
|
|
1644
|
+
)
|
|
1645
|
+
] }, column.key))
|
|
1646
|
+
] }),
|
|
1647
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1648
|
+
"p",
|
|
1649
|
+
{
|
|
1650
|
+
className: "dwt-detail-summary",
|
|
1651
|
+
role: "status",
|
|
1652
|
+
"aria-live": "polite",
|
|
1653
|
+
"aria-atomic": "true",
|
|
1654
|
+
children: page.totalRows === 0 ? labels.noResults : labels.showingRows(from, to, page.totalRows)
|
|
1655
|
+
}
|
|
1656
|
+
),
|
|
1657
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "dwt-table-wrap", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: slot("table", "dwt-table", "dwt-detail-table"), children: [
|
|
1658
|
+
/* @__PURE__ */ jsxRuntime.jsx("caption", { className: "dwt-visually-hidden", children: title }),
|
|
1659
|
+
/* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsx("tr", { children: columns.map((column) => {
|
|
1660
|
+
const sorted = query.sort?.column === column.key;
|
|
1661
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1662
|
+
"th",
|
|
1663
|
+
{
|
|
1664
|
+
scope: "col",
|
|
1665
|
+
className: column.numeric === true ? "dwt-numeric" : void 0,
|
|
1666
|
+
"aria-sort": sorted ? query.sort?.direction === "asc" ? "ascending" : "descending" : void 0,
|
|
1667
|
+
children: column.sortable === false ? column.label : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1668
|
+
"button",
|
|
1669
|
+
{
|
|
1670
|
+
type: "button",
|
|
1671
|
+
className: "dwt-detail-sort",
|
|
1672
|
+
"aria-label": labels.sortBy(column.label),
|
|
1673
|
+
onClick: () => onQueryChange(
|
|
1674
|
+
withSort(query, nextSort(query.sort, column.key))
|
|
1675
|
+
),
|
|
1676
|
+
children: [
|
|
1677
|
+
column.label,
|
|
1678
|
+
/* @__PURE__ */ jsxRuntime.jsx(SortGlyph, { sort: query.sort, column: column.key })
|
|
1679
|
+
]
|
|
1680
|
+
}
|
|
1681
|
+
)
|
|
1682
|
+
},
|
|
1683
|
+
column.key
|
|
1684
|
+
);
|
|
1685
|
+
}) }) }),
|
|
1686
|
+
/* @__PURE__ */ jsxRuntime.jsx("tbody", { children: page.rows.map((row, rowIndex) => /* @__PURE__ */ jsxRuntime.jsx("tr", { children: row.map((cell, cellIndex) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1687
|
+
Cell,
|
|
1688
|
+
{
|
|
1689
|
+
cell,
|
|
1690
|
+
column: columns[cellIndex]
|
|
1691
|
+
},
|
|
1692
|
+
cellIndex
|
|
1693
|
+
)) }, rowIndex)) })
|
|
1694
|
+
] }) }),
|
|
1695
|
+
page.pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsxs("nav", { className: "dwt-detail-pager", "aria-label": labels.pagination, children: [
|
|
1696
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1697
|
+
"button",
|
|
1698
|
+
{
|
|
1699
|
+
type: "button",
|
|
1700
|
+
className: slot("button", "dwt-button"),
|
|
1701
|
+
disabled: page.page <= 1,
|
|
1702
|
+
onClick: () => onQueryChange({ ...query, page: page.page - 1 }),
|
|
1703
|
+
children: labels.previousPage
|
|
1704
|
+
}
|
|
1705
|
+
),
|
|
1706
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: labels.pageOf(page.page, page.pageCount) }),
|
|
1707
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1708
|
+
"button",
|
|
1709
|
+
{
|
|
1710
|
+
type: "button",
|
|
1711
|
+
className: slot("button", "dwt-button"),
|
|
1712
|
+
disabled: page.page >= page.pageCount,
|
|
1713
|
+
onClick: () => onQueryChange({ ...query, page: page.page + 1 }),
|
|
1714
|
+
children: labels.nextPage
|
|
1715
|
+
}
|
|
1716
|
+
)
|
|
1717
|
+
] })
|
|
1718
|
+
] });
|
|
1719
|
+
}
|
|
1720
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1721
|
+
"section",
|
|
1722
|
+
{
|
|
1723
|
+
"aria-labelledby": headingId ?? `${id}-title`,
|
|
1724
|
+
className: slot("detail", "dwt-detail", className),
|
|
1725
|
+
children: [
|
|
1726
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1727
|
+
Heading,
|
|
1728
|
+
{
|
|
1729
|
+
id: headingId ?? `${id}-title`,
|
|
1730
|
+
className: slot("cardTitle", "dwt-card-title", "dwt-detail-title"),
|
|
1731
|
+
children: title
|
|
1732
|
+
}
|
|
1733
|
+
),
|
|
1734
|
+
body
|
|
1735
|
+
]
|
|
1736
|
+
}
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
function WidgetDetailDialog({
|
|
1740
|
+
title,
|
|
1741
|
+
open,
|
|
1742
|
+
onClose,
|
|
1743
|
+
children
|
|
1744
|
+
}) {
|
|
1745
|
+
const slot = useSlotClassName();
|
|
1746
|
+
const { labels } = useWidgetSettings();
|
|
1747
|
+
const dialogRef = react.useRef(null);
|
|
1748
|
+
const titleId = react.useId();
|
|
1749
|
+
react.useEffect(() => {
|
|
1750
|
+
const dialog = dialogRef.current;
|
|
1751
|
+
if (dialog === null || !open) return void 0;
|
|
1752
|
+
const opener = document.activeElement;
|
|
1753
|
+
if (!dialog.open) dialog.showModal();
|
|
1754
|
+
return () => {
|
|
1755
|
+
if (dialog.open) dialog.close();
|
|
1756
|
+
if (opener instanceof HTMLElement) opener.focus();
|
|
1757
|
+
};
|
|
1758
|
+
}, [open]);
|
|
1759
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1760
|
+
"dialog",
|
|
1761
|
+
{
|
|
1762
|
+
ref: dialogRef,
|
|
1763
|
+
className: "dwt-detail-dialog",
|
|
1764
|
+
"aria-labelledby": titleId,
|
|
1765
|
+
onCancel: (event) => {
|
|
1766
|
+
event.preventDefault();
|
|
1767
|
+
onClose();
|
|
1768
|
+
},
|
|
1769
|
+
onClick: (event) => {
|
|
1770
|
+
if (event.target === dialogRef.current) onClose();
|
|
1771
|
+
},
|
|
1772
|
+
children: open && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "dwt-detail-dialog-body", children: [
|
|
1773
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1774
|
+
"button",
|
|
1775
|
+
{
|
|
1776
|
+
type: "button",
|
|
1777
|
+
className: slot("button", "dwt-button", "dwt-detail-close"),
|
|
1778
|
+
onClick: onClose,
|
|
1779
|
+
children: labels.close
|
|
1780
|
+
}
|
|
1781
|
+
),
|
|
1782
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { id: titleId, className: "dwt-visually-hidden", children: title }),
|
|
1783
|
+
children
|
|
1784
|
+
] })
|
|
1785
|
+
}
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1788
|
+
function useDetailData(definition, load, enabled) {
|
|
1789
|
+
const [state, setState] = react.useState({ status: "loading" });
|
|
1790
|
+
const [attempt, setAttempt] = react.useState(0);
|
|
1791
|
+
react.useEffect(() => {
|
|
1792
|
+
if (!enabled || load === void 0) return void 0;
|
|
1793
|
+
const controller = new AbortController();
|
|
1794
|
+
setState({ status: "loading" });
|
|
1795
|
+
Promise.resolve().then(
|
|
1796
|
+
() => load(definition, {
|
|
1797
|
+
signal: controller.signal,
|
|
1798
|
+
query: defaultDetailQuery()
|
|
1799
|
+
})
|
|
1800
|
+
).then((raw) => {
|
|
1801
|
+
if (controller.signal.aborted) return;
|
|
1802
|
+
const checked = validateDetailData(
|
|
1803
|
+
raw,
|
|
1804
|
+
`Widget "${definition.key}" detail`
|
|
1805
|
+
);
|
|
1806
|
+
setState(
|
|
1807
|
+
checked.ok ? { status: "ok", data: checked.value } : { status: "error", error: checked.error }
|
|
1808
|
+
);
|
|
1809
|
+
}).catch((cause) => {
|
|
1810
|
+
if (controller.signal.aborted) return;
|
|
1811
|
+
setState({
|
|
1812
|
+
status: "error",
|
|
1813
|
+
error: `Widget "${definition.key}" detail: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
1814
|
+
});
|
|
1815
|
+
});
|
|
1816
|
+
return () => controller.abort();
|
|
1817
|
+
}, [enabled, load, definition, attempt]);
|
|
1818
|
+
return { state, reload: () => setAttempt((count) => count + 1) };
|
|
1819
|
+
}
|
|
1820
|
+
var NARROW_VIEWPORT_QUERY = "(max-width: 640px)";
|
|
1821
|
+
var WIDE_GRID_PX = 900;
|
|
1822
|
+
function useFillSpans(definitions, gridRef, twelve) {
|
|
1823
|
+
const [metrics, setMetrics] = react.useState(void 0);
|
|
1824
|
+
const needsSpans = definitions.some(
|
|
1825
|
+
(definition) => fillsWidth(definition.fill)
|
|
1826
|
+
);
|
|
1827
|
+
react.useEffect(() => {
|
|
1828
|
+
const grid = gridRef.current;
|
|
1829
|
+
if (!needsSpans || grid === null || typeof ResizeObserver === "undefined") {
|
|
1830
|
+
setMetrics(void 0);
|
|
1831
|
+
return void 0;
|
|
1832
|
+
}
|
|
1833
|
+
function measure() {
|
|
1834
|
+
if (grid === null) return;
|
|
1835
|
+
if (twelve) {
|
|
1836
|
+
const wide = grid.clientWidth > WIDE_GRID_PX;
|
|
1837
|
+
setMetrics(
|
|
1838
|
+
(previous) => !wide ? void 0 : previous?.columns === WIDTH_COLUMNS ? previous : { columns: WIDTH_COLUMNS, largeSpan: 2 }
|
|
1839
|
+
);
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
const tracks = getComputedStyle(grid).gridTemplateColumns;
|
|
1843
|
+
const columns = tracks === "" || tracks === "none" ? 0 : tracks.trim().split(/\s+/).length;
|
|
1844
|
+
const largeSpan = window.matchMedia(NARROW_VIEWPORT_QUERY).matches ? 1 : 2;
|
|
1845
|
+
setMetrics(
|
|
1846
|
+
(previous) => columns === 0 ? void 0 : previous?.columns === columns && previous.largeSpan === largeSpan ? previous : { columns, largeSpan }
|
|
1847
|
+
);
|
|
1848
|
+
}
|
|
1849
|
+
measure();
|
|
1850
|
+
const observer = new ResizeObserver(measure);
|
|
1851
|
+
observer.observe(grid);
|
|
1852
|
+
return () => observer.disconnect();
|
|
1853
|
+
}, [needsSpans, gridRef, twelve]);
|
|
1854
|
+
return react.useMemo(
|
|
1855
|
+
() => metrics === void 0 ? /* @__PURE__ */ new Map() : twelve ? fillWidthSpans(definitions) : fillColumnSpans(definitions, metrics.columns, metrics.largeSpan),
|
|
1856
|
+
[definitions, metrics, twelve]
|
|
1857
|
+
);
|
|
1858
|
+
}
|
|
1859
|
+
function widthProps(twelve, definition) {
|
|
1860
|
+
return twelve ? { width: itemWidth(definition) } : {};
|
|
1861
|
+
}
|
|
1862
|
+
function spanProps(spans, key) {
|
|
1863
|
+
const columnSpan = spans.get(key);
|
|
1864
|
+
return columnSpan === void 0 ? {} : { columnSpan };
|
|
1865
|
+
}
|
|
1110
1866
|
function WidgetGrid({
|
|
1111
1867
|
widgets,
|
|
1112
1868
|
onRetry,
|
|
@@ -1114,21 +1870,78 @@ function WidgetGrid({
|
|
|
1114
1870
|
className
|
|
1115
1871
|
}) {
|
|
1116
1872
|
const slot = useSlotClassName();
|
|
1117
|
-
|
|
1118
|
-
|
|
1873
|
+
const gridRef = react.useRef(null);
|
|
1874
|
+
const definitions = widgets.map((w) => w.definition);
|
|
1875
|
+
const twelve = usesWidthColumns(definitions);
|
|
1876
|
+
const spans = useFillSpans(
|
|
1877
|
+
definitions,
|
|
1878
|
+
gridRef,
|
|
1879
|
+
twelve
|
|
1880
|
+
);
|
|
1881
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1882
|
+
"div",
|
|
1883
|
+
{
|
|
1884
|
+
ref: gridRef,
|
|
1885
|
+
className: slot(
|
|
1886
|
+
"grid",
|
|
1887
|
+
"dwt-grid",
|
|
1888
|
+
twelve && "dwt-grid--twelve",
|
|
1889
|
+
className
|
|
1890
|
+
),
|
|
1891
|
+
children: widgets.map((widget) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1892
|
+
ResolvedWidgetCard,
|
|
1893
|
+
{
|
|
1894
|
+
widget,
|
|
1895
|
+
...spanProps(spans, widget.definition.key),
|
|
1896
|
+
...widthProps(twelve, widget.definition),
|
|
1897
|
+
...headingLevel === void 0 ? {} : { headingLevel },
|
|
1898
|
+
...onRetry === void 0 || widget.status !== "error" ? {} : { onRetry: () => onRetry(widget.definition.key) }
|
|
1899
|
+
},
|
|
1900
|
+
widget.definition.key
|
|
1901
|
+
))
|
|
1902
|
+
}
|
|
1903
|
+
);
|
|
1904
|
+
}
|
|
1905
|
+
function DetailHost({
|
|
1906
|
+
widget,
|
|
1907
|
+
loadDetail,
|
|
1908
|
+
onClose
|
|
1909
|
+
}) {
|
|
1910
|
+
const { locale } = useWidgetSettings();
|
|
1911
|
+
const { definition } = widget;
|
|
1912
|
+
const options = resolveDetailOptions(definition);
|
|
1913
|
+
const pageSize = options?.pageSize ?? 25;
|
|
1914
|
+
const [query, setQuery] = react.useState(defaultDetailQuery(pageSize));
|
|
1915
|
+
const derived = widget.status === "ok" ? deriveDetailData(widget.data, locale) : void 0;
|
|
1916
|
+
const load = react.useMemo(
|
|
1917
|
+
() => loadDetail ?? (derived === void 0 ? void 0 : () => derived),
|
|
1918
|
+
[loadDetail, derived]
|
|
1919
|
+
);
|
|
1920
|
+
const { state, reload } = useDetailData(definition, load, true);
|
|
1921
|
+
const title = options?.title ?? definition.title;
|
|
1922
|
+
return /* @__PURE__ */ jsxRuntime.jsx(WidgetDetailDialog, { title, open: true, onClose, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1923
|
+
WidgetDetail,
|
|
1119
1924
|
{
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1925
|
+
title,
|
|
1926
|
+
query,
|
|
1927
|
+
onQueryChange: setQuery,
|
|
1928
|
+
status: load === void 0 ? "error" : state.status,
|
|
1929
|
+
...load === void 0 ? {
|
|
1930
|
+
error: `Widget "${definition.key}" has no detail data: pass loadDetail, or use a TABLE without a footer or a BAR_LIST.`
|
|
1931
|
+
} : {},
|
|
1932
|
+
...state.data === void 0 ? {} : { data: state.data },
|
|
1933
|
+
...state.error === void 0 ? {} : { error: state.error },
|
|
1934
|
+
onRetry: reload
|
|
1935
|
+
}
|
|
1936
|
+
) });
|
|
1126
1937
|
}
|
|
1127
1938
|
function DashboardInner({
|
|
1128
1939
|
widgets,
|
|
1129
1940
|
layout = EMPTY_LAYOUT,
|
|
1130
1941
|
onLayoutChange,
|
|
1131
1942
|
onRetry,
|
|
1943
|
+
loadDetail,
|
|
1944
|
+
onOpenDetail,
|
|
1132
1945
|
headingLevel,
|
|
1133
1946
|
className
|
|
1134
1947
|
}) {
|
|
@@ -1141,6 +1954,16 @@ function DashboardInner({
|
|
|
1141
1954
|
const visible = visibleWidgets(definitions, layout);
|
|
1142
1955
|
const hidden = hiddenWidgets(definitions, layout);
|
|
1143
1956
|
const editable = onLayoutChange !== void 0;
|
|
1957
|
+
const minimized = editable ? visible.filter((definition) => isMinimized(layout, definition.key)) : [];
|
|
1958
|
+
const shown = editable ? visible.filter((definition) => !isMinimized(layout, definition.key)) : visible;
|
|
1959
|
+
const [detailKey, setDetailKey] = react.useState(void 0);
|
|
1960
|
+
const gridRef = react.useRef(null);
|
|
1961
|
+
const twelve = usesWidthColumns(shown);
|
|
1962
|
+
const spans = useFillSpans(
|
|
1963
|
+
shown,
|
|
1964
|
+
gridRef,
|
|
1965
|
+
twelve
|
|
1966
|
+
);
|
|
1144
1967
|
function change(next) {
|
|
1145
1968
|
if (next !== layout) onLayoutChange?.(next);
|
|
1146
1969
|
}
|
|
@@ -1162,65 +1985,110 @@ function DashboardInner({
|
|
|
1162
1985
|
definition.key
|
|
1163
1986
|
))
|
|
1164
1987
|
] }),
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1988
|
+
minimized.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1989
|
+
"div",
|
|
1990
|
+
{
|
|
1991
|
+
className: slot("hiddenBar", "dwt-hidden-bar", "dwt-minimized-bar"),
|
|
1992
|
+
children: [
|
|
1993
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "dwt-hidden-label", children: labels.minimizedWidgets }),
|
|
1994
|
+
minimized.map((definition) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1995
|
+
"button",
|
|
1996
|
+
{
|
|
1997
|
+
type: "button",
|
|
1998
|
+
className: slot("button", "dwt-button", "dwt-show-button"),
|
|
1999
|
+
"aria-label": labels.expand(definition.title),
|
|
2000
|
+
onClick: () => change(toggleMinimized(layout, definition.key)),
|
|
2001
|
+
children: [
|
|
2002
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u25B8 " }),
|
|
2003
|
+
definition.title
|
|
2004
|
+
]
|
|
2005
|
+
},
|
|
2006
|
+
definition.key
|
|
2007
|
+
))
|
|
2008
|
+
]
|
|
2009
|
+
}
|
|
2010
|
+
),
|
|
2011
|
+
visible.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: slot("empty", "dwt-empty", "dwt-dashboard-empty"), children: definitions.length === 0 ? labels.noWidgets : labels.allHidden }) : shown.length === 0 ? null : /* @__PURE__ */ jsxRuntime.jsx(
|
|
2012
|
+
"div",
|
|
2013
|
+
{
|
|
2014
|
+
ref: gridRef,
|
|
2015
|
+
className: slot("grid", "dwt-grid", twelve && "dwt-grid--twelve"),
|
|
2016
|
+
children: shown.map((definition, index) => {
|
|
2017
|
+
const widget = byKey.get(
|
|
2018
|
+
definition.key
|
|
2019
|
+
);
|
|
2020
|
+
if (widget === void 0) return null;
|
|
2021
|
+
const key = definition.key;
|
|
2022
|
+
const title = definition.title;
|
|
2023
|
+
const actions = editable ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2024
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2025
|
+
"button",
|
|
2026
|
+
{
|
|
2027
|
+
type: "button",
|
|
2028
|
+
className: slot("button", "dwt-button", "dwt-icon-button"),
|
|
2029
|
+
"aria-label": labels.moveEarlier(title),
|
|
2030
|
+
title: labels.moveEarlier(title),
|
|
2031
|
+
disabled: index === 0,
|
|
2032
|
+
onClick: () => change(moveWidgetBy(definitions, layout, key, -1)),
|
|
2033
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2191" })
|
|
2034
|
+
}
|
|
2035
|
+
),
|
|
2036
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2037
|
+
"button",
|
|
2038
|
+
{
|
|
2039
|
+
type: "button",
|
|
2040
|
+
className: slot("button", "dwt-button", "dwt-icon-button"),
|
|
2041
|
+
"aria-label": labels.moveLater(title),
|
|
2042
|
+
title: labels.moveLater(title),
|
|
2043
|
+
disabled: index === shown.length - 1,
|
|
2044
|
+
onClick: () => change(moveWidgetBy(definitions, layout, key, 1)),
|
|
2045
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2193" })
|
|
2046
|
+
}
|
|
2047
|
+
),
|
|
2048
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2049
|
+
"button",
|
|
2050
|
+
{
|
|
2051
|
+
type: "button",
|
|
2052
|
+
className: slot("button", "dwt-button", "dwt-icon-button"),
|
|
2053
|
+
"aria-label": labels.hide(title),
|
|
2054
|
+
title: labels.hide(title),
|
|
2055
|
+
onClick: () => change(hideWidget(layout, key)),
|
|
2056
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\xD7" })
|
|
2057
|
+
}
|
|
2058
|
+
)
|
|
2059
|
+
] }) : void 0;
|
|
2060
|
+
const viewable = resolveDetailOptions(definition) !== void 0 && (onOpenDetail !== void 0 || loadDetail !== void 0 || widget.status === "ok" && deriveDetailData(widget.data) !== void 0);
|
|
2061
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2062
|
+
ResolvedWidgetCard,
|
|
2063
|
+
{
|
|
2064
|
+
widget,
|
|
2065
|
+
...viewable ? {
|
|
2066
|
+
onView: () => onOpenDetail === void 0 ? setDetailKey(key) : onOpenDetail(key)
|
|
2067
|
+
} : {},
|
|
2068
|
+
...spanProps(spans, key),
|
|
2069
|
+
...widthProps(twelve, definition),
|
|
2070
|
+
minimized: isMinimized(layout, key),
|
|
2071
|
+
...actions === void 0 ? {} : { actions },
|
|
2072
|
+
...editable ? {
|
|
2073
|
+
onToggleMinimized: () => change(toggleMinimized(layout, key))
|
|
2074
|
+
} : {},
|
|
2075
|
+
...headingLevel === void 0 ? {} : { headingLevel },
|
|
2076
|
+
...onRetry === void 0 || widget.status !== "error" ? {} : { onRetry: () => onRetry(key) }
|
|
2077
|
+
},
|
|
2078
|
+
key
|
|
2079
|
+
);
|
|
2080
|
+
})
|
|
2081
|
+
}
|
|
2082
|
+
),
|
|
2083
|
+
detailKey !== void 0 && byKey.get(detailKey) !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2084
|
+
DetailHost,
|
|
2085
|
+
{
|
|
2086
|
+
widget: byKey.get(detailKey),
|
|
2087
|
+
loadDetail,
|
|
2088
|
+
onClose: () => setDetailKey(void 0)
|
|
2089
|
+
},
|
|
2090
|
+
detailKey
|
|
2091
|
+
)
|
|
1224
2092
|
] });
|
|
1225
2093
|
}
|
|
1226
2094
|
function Dashboard({
|
|
@@ -1245,6 +2113,656 @@ function Dashboard({
|
|
|
1245
2113
|
);
|
|
1246
2114
|
}
|
|
1247
2115
|
|
|
2116
|
+
// src/core/definition.ts
|
|
2117
|
+
function sortDefinitions(definitions) {
|
|
2118
|
+
return definitions.map((definition, index) => ({ definition, index })).sort(
|
|
2119
|
+
(a, b) => (a.definition.sortOrder ?? 0) - (b.definition.sortOrder ?? 0) || a.index - b.index
|
|
2120
|
+
).map(({ definition }) => definition);
|
|
2121
|
+
}
|
|
2122
|
+
function isWidgetVisibleTo(definition, roles) {
|
|
2123
|
+
if (definition.active === false) {
|
|
2124
|
+
return false;
|
|
2125
|
+
}
|
|
2126
|
+
if (definition.roles === void 0 || definition.roles.length === 0) {
|
|
2127
|
+
return true;
|
|
2128
|
+
}
|
|
2129
|
+
if (roles === void 0) {
|
|
2130
|
+
return false;
|
|
2131
|
+
}
|
|
2132
|
+
return definition.roles.some((role) => roles.includes(role));
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// src/core/payload.ts
|
|
2136
|
+
var WIDGET_KINDS = [
|
|
2137
|
+
"TEXT",
|
|
2138
|
+
"KPI",
|
|
2139
|
+
"GAUGE",
|
|
2140
|
+
"TABLE",
|
|
2141
|
+
"BAR_LIST",
|
|
2142
|
+
"ALERT_LIST",
|
|
2143
|
+
"GRAPH"
|
|
2144
|
+
];
|
|
2145
|
+
function isWidgetKind(value) {
|
|
2146
|
+
return typeof value === "string" && WIDGET_KINDS.includes(value);
|
|
2147
|
+
}
|
|
2148
|
+
var VALUE_FORMATS = ["number", "currency", "percent"];
|
|
2149
|
+
var MAX_GRAPH_SERIES = 8;
|
|
2150
|
+
var DEFAULT_EMPTY_TEXT = "Nothing to show yet.";
|
|
2151
|
+
function isWidgetEmptyState(payload) {
|
|
2152
|
+
return "empty" in payload && payload.empty === true;
|
|
2153
|
+
}
|
|
2154
|
+
function emptyTextFor(payload) {
|
|
2155
|
+
if (isWidgetEmptyState(payload)) {
|
|
2156
|
+
return payload.text;
|
|
2157
|
+
}
|
|
2158
|
+
switch (payload.kind) {
|
|
2159
|
+
case "TABLE":
|
|
2160
|
+
return payload.rows.length === 0 ? DEFAULT_EMPTY_TEXT : null;
|
|
2161
|
+
case "BAR_LIST":
|
|
2162
|
+
return payload.items.length === 0 ? DEFAULT_EMPTY_TEXT : null;
|
|
2163
|
+
case "ALERT_LIST":
|
|
2164
|
+
return payload.items.length === 0 ? payload.emptyText : null;
|
|
2165
|
+
case "GRAPH":
|
|
2166
|
+
return payload.series.every((series) => series.points.length === 0) ? DEFAULT_EMPTY_TEXT : null;
|
|
2167
|
+
case "TEXT":
|
|
2168
|
+
case "KPI":
|
|
2169
|
+
case "GAUGE":
|
|
2170
|
+
return null;
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
// src/core/validate.ts
|
|
2175
|
+
var MAX_REPORTED_PROBLEMS = 10;
|
|
2176
|
+
function describeType2(value) {
|
|
2177
|
+
if (value === null) return "null";
|
|
2178
|
+
if (Array.isArray(value)) return "array";
|
|
2179
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
2180
|
+
return String(value);
|
|
2181
|
+
}
|
|
2182
|
+
return typeof value;
|
|
2183
|
+
}
|
|
2184
|
+
function isRecord(value) {
|
|
2185
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2186
|
+
}
|
|
2187
|
+
function join(path, field) {
|
|
2188
|
+
return path === "" ? field : `${path}.${field}`;
|
|
2189
|
+
}
|
|
2190
|
+
function requireString(record, field, path, problems) {
|
|
2191
|
+
const value = record[field];
|
|
2192
|
+
if (typeof value !== "string") {
|
|
2193
|
+
problems.push(
|
|
2194
|
+
`${join(path, field)} must be a string, got ${describeType2(value)}.`
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
function optionalString(record, field, path, problems) {
|
|
2199
|
+
if (record[field] !== void 0) {
|
|
2200
|
+
requireString(record, field, path, problems);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
function requireNumber(record, field, path, problems) {
|
|
2204
|
+
const value = record[field];
|
|
2205
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
2206
|
+
problems.push(
|
|
2207
|
+
`${join(path, field)} must be a finite number, got ${describeType2(value)}.`
|
|
2208
|
+
);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
function optionalNumber(record, field, path, problems) {
|
|
2212
|
+
if (record[field] !== void 0) {
|
|
2213
|
+
requireNumber(record, field, path, problems);
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
function optionalBoolean(record, field, path, problems) {
|
|
2217
|
+
const value = record[field];
|
|
2218
|
+
if (value !== void 0 && typeof value !== "boolean") {
|
|
2219
|
+
problems.push(
|
|
2220
|
+
`${join(path, field)} must be a boolean, got ${describeType2(value)}.`
|
|
2221
|
+
);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
function optionalUrl(record, field, path, problems, isSafe, allowed) {
|
|
2225
|
+
const value = record[field];
|
|
2226
|
+
if (value === void 0) return;
|
|
2227
|
+
if (typeof value !== "string") {
|
|
2228
|
+
problems.push(
|
|
2229
|
+
`${join(path, field)} must be a string, got ${describeType2(value)}.`
|
|
2230
|
+
);
|
|
2231
|
+
} else if (!isSafe(value)) {
|
|
2232
|
+
problems.push(
|
|
2233
|
+
`${join(path, field)} must be ${allowed}, got ${JSON.stringify(value.slice(0, 80))}.`
|
|
2234
|
+
);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
function requireArray(record, field, path, problems) {
|
|
2238
|
+
const value = record[field];
|
|
2239
|
+
if (!Array.isArray(value)) {
|
|
2240
|
+
problems.push(
|
|
2241
|
+
`${join(path, field)} must be an array, got ${describeType2(value)}.`
|
|
2242
|
+
);
|
|
2243
|
+
return null;
|
|
2244
|
+
}
|
|
2245
|
+
return value;
|
|
2246
|
+
}
|
|
2247
|
+
function requireOneOf(record, field, options, path, problems) {
|
|
2248
|
+
const value = record[field];
|
|
2249
|
+
if (typeof value !== "string" || !options.includes(value)) {
|
|
2250
|
+
problems.push(
|
|
2251
|
+
`${join(path, field)} must be one of ${options.map((o) => `"${o}"`).join(", ")}, got ${typeof value === "string" ? JSON.stringify(value) : describeType2(value)}.`
|
|
2252
|
+
);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
function eachRecord(items, path, problems, check) {
|
|
2256
|
+
items.forEach((item, index) => {
|
|
2257
|
+
const itemPath = `${path}[${index}]`;
|
|
2258
|
+
if (isRecord(item)) {
|
|
2259
|
+
check(item, itemPath);
|
|
2260
|
+
} else {
|
|
2261
|
+
problems.push(
|
|
2262
|
+
`${itemPath} must be an object, got ${describeType2(item)}.`
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
var HREF_RULE = "an http(s), mailto or relative URL";
|
|
2268
|
+
var IMAGE_RULE = "an http(s) or relative URL";
|
|
2269
|
+
function checkPayloadFields(kind, data, problems) {
|
|
2270
|
+
switch (kind) {
|
|
2271
|
+
case "TEXT":
|
|
2272
|
+
requireString(data, "value", "", problems);
|
|
2273
|
+
requireString(data, "label", "", problems);
|
|
2274
|
+
return;
|
|
2275
|
+
case "KPI":
|
|
2276
|
+
requireNumber(data, "value", "", problems);
|
|
2277
|
+
if (data["previous"] !== null) {
|
|
2278
|
+
optionalNumber(data, "previous", "", problems);
|
|
2279
|
+
}
|
|
2280
|
+
requireOneOf(data, "format", VALUE_FORMATS, "", problems);
|
|
2281
|
+
optionalString(data, "currency", "", problems);
|
|
2282
|
+
requireString(data, "label", "", problems);
|
|
2283
|
+
optionalString(data, "hint", "", problems);
|
|
2284
|
+
optionalBoolean(data, "higherIsBetter", "", problems);
|
|
2285
|
+
return;
|
|
2286
|
+
case "GAUGE":
|
|
2287
|
+
requireNumber(data, "value", "", problems);
|
|
2288
|
+
requireNumber(data, "max", "", problems);
|
|
2289
|
+
requireString(data, "label", "", problems);
|
|
2290
|
+
if (typeof data["max"] === "number" && data["max"] <= 0) {
|
|
2291
|
+
problems.push(`max must be greater than 0, got ${data["max"]}.`);
|
|
2292
|
+
}
|
|
2293
|
+
return;
|
|
2294
|
+
case "TABLE": {
|
|
2295
|
+
const columns = requireArray(data, "columns", "", problems);
|
|
2296
|
+
if (columns !== null) {
|
|
2297
|
+
eachRecord(columns, "columns", problems, (column, path) => {
|
|
2298
|
+
requireString(column, "label", path, problems);
|
|
2299
|
+
optionalBoolean(column, "numeric", path, problems);
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
const rows = requireArray(data, "rows", "", problems);
|
|
2303
|
+
if (rows !== null) {
|
|
2304
|
+
rows.forEach((row, rowIndex) => {
|
|
2305
|
+
const rowPath = `rows[${rowIndex}]`;
|
|
2306
|
+
if (!Array.isArray(row)) {
|
|
2307
|
+
problems.push(
|
|
2308
|
+
`${rowPath} must be an array of cells, got ${describeType2(row)}.`
|
|
2309
|
+
);
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
if (columns !== null && row.length !== columns.length) {
|
|
2313
|
+
problems.push(
|
|
2314
|
+
`${rowPath} has ${row.length} cell(s) but there are ${columns.length} column(s).`
|
|
2315
|
+
);
|
|
2316
|
+
}
|
|
2317
|
+
eachRecord(row, rowPath, problems, (cell, cellPath) => {
|
|
2318
|
+
requireString(cell, "text", cellPath, problems);
|
|
2319
|
+
optionalUrl(
|
|
2320
|
+
cell,
|
|
2321
|
+
"href",
|
|
2322
|
+
cellPath,
|
|
2323
|
+
problems,
|
|
2324
|
+
isSafeHref,
|
|
2325
|
+
HREF_RULE
|
|
2326
|
+
);
|
|
2327
|
+
});
|
|
2328
|
+
});
|
|
2329
|
+
}
|
|
2330
|
+
optionalString(data, "footer", "", problems);
|
|
2331
|
+
return;
|
|
2332
|
+
}
|
|
2333
|
+
case "BAR_LIST": {
|
|
2334
|
+
const items = requireArray(data, "items", "", problems);
|
|
2335
|
+
if (items !== null) {
|
|
2336
|
+
eachRecord(items, "items", problems, (item, path) => {
|
|
2337
|
+
requireString(item, "label", path, problems);
|
|
2338
|
+
requireNumber(item, "value", path, problems);
|
|
2339
|
+
optionalString(item, "display", path, problems);
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
optionalNumber(data, "total", "", problems);
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2345
|
+
case "ALERT_LIST": {
|
|
2346
|
+
const items = requireArray(data, "items", "", problems);
|
|
2347
|
+
if (items !== null) {
|
|
2348
|
+
eachRecord(items, "items", problems, (item, path) => {
|
|
2349
|
+
requireString(item, "title", path, problems);
|
|
2350
|
+
optionalUrl(item, "href", path, problems, isSafeHref, HREF_RULE);
|
|
2351
|
+
optionalUrl(
|
|
2352
|
+
item,
|
|
2353
|
+
"thumbnailUrl",
|
|
2354
|
+
path,
|
|
2355
|
+
problems,
|
|
2356
|
+
isSafeImageUrl,
|
|
2357
|
+
IMAGE_RULE
|
|
2358
|
+
);
|
|
2359
|
+
requireString(item, "valueLabel", path, problems);
|
|
2360
|
+
optionalString(item, "detail", path, problems);
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
requireNumber(data, "total", "", problems);
|
|
2364
|
+
requireString(data, "emptyText", "", problems);
|
|
2365
|
+
if (items !== null && typeof data["total"] === "number" && data["total"] < items.length) {
|
|
2366
|
+
problems.push(
|
|
2367
|
+
`total (${data["total"]}) must be at least the number of items (${items.length}).`
|
|
2368
|
+
);
|
|
2369
|
+
}
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
case "GRAPH": {
|
|
2373
|
+
const series = requireArray(data, "series", "", problems);
|
|
2374
|
+
if (series !== null) {
|
|
2375
|
+
if (series.length > MAX_GRAPH_SERIES) {
|
|
2376
|
+
problems.push(
|
|
2377
|
+
`series has ${series.length} entries; at most ${MAX_GRAPH_SERIES} are allowed (fold the rest into "Other").`
|
|
2378
|
+
);
|
|
2379
|
+
}
|
|
2380
|
+
eachRecord(series, "series", problems, (entry, path) => {
|
|
2381
|
+
requireString(entry, "name", path, problems);
|
|
2382
|
+
const points = requireArray(entry, "points", path, problems);
|
|
2383
|
+
if (points !== null) {
|
|
2384
|
+
eachRecord(
|
|
2385
|
+
points,
|
|
2386
|
+
`${path}.points`,
|
|
2387
|
+
problems,
|
|
2388
|
+
(point, pointPath) => {
|
|
2389
|
+
requireString(point, "label", pointPath, problems);
|
|
2390
|
+
requireNumber(point, "value", pointPath, problems);
|
|
2391
|
+
}
|
|
2392
|
+
);
|
|
2393
|
+
}
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2396
|
+
requireOneOf(data, "chartType", ["bar", "line"], "", problems);
|
|
2397
|
+
requireOneOf(data, "valueFormat", VALUE_FORMATS, "", problems);
|
|
2398
|
+
optionalString(data, "currency", "", problems);
|
|
2399
|
+
optionalString(data, "xLabel", "", problems);
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
function formatProblems(prefix, problems) {
|
|
2405
|
+
const shown = problems.slice(0, MAX_REPORTED_PROBLEMS);
|
|
2406
|
+
const more = problems.length - shown.length;
|
|
2407
|
+
return `${prefix}: ${shown.join(" ")}${more > 0 ? ` (and ${more} more problem(s))` : ""}`;
|
|
2408
|
+
}
|
|
2409
|
+
function validateWidgetData(value, options = {}) {
|
|
2410
|
+
const subject = options.widgetKey === void 0 ? "Widget data" : `Widget ${JSON.stringify(options.widgetKey)}`;
|
|
2411
|
+
if (!isRecord(value)) {
|
|
2412
|
+
return err(`${subject}: must be an object, got ${describeType2(value)}.`);
|
|
2413
|
+
}
|
|
2414
|
+
if (value["empty"] === true) {
|
|
2415
|
+
if (typeof value["text"] !== "string") {
|
|
2416
|
+
return err(
|
|
2417
|
+
`${subject} (empty): text must be a string, got ${describeType2(value["text"])}.`
|
|
2418
|
+
);
|
|
2419
|
+
}
|
|
2420
|
+
return ok(value);
|
|
2421
|
+
}
|
|
2422
|
+
const kind = value["kind"];
|
|
2423
|
+
if (!isWidgetKind(kind)) {
|
|
2424
|
+
return err(
|
|
2425
|
+
`${subject}: kind must be one of ${WIDGET_KINDS.join(", ")} (or the payload must be {"empty": true, "text": ...}), got ${typeof kind === "string" ? JSON.stringify(kind) : describeType2(kind)}.`
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
if (options.expectedKind !== void 0 && kind !== options.expectedKind) {
|
|
2429
|
+
return err(
|
|
2430
|
+
`${subject}: expected kind ${options.expectedKind} from its definition, got ${kind}.`
|
|
2431
|
+
);
|
|
2432
|
+
}
|
|
2433
|
+
const problems = [];
|
|
2434
|
+
checkPayloadFields(kind, value, problems);
|
|
2435
|
+
if (problems.length > 0) {
|
|
2436
|
+
return err(formatProblems(`${subject} (${kind})`, problems));
|
|
2437
|
+
}
|
|
2438
|
+
return ok(value);
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
// src/core/resolve.ts
|
|
2442
|
+
function resolvePayload(definition, payload, options = {}) {
|
|
2443
|
+
let checked;
|
|
2444
|
+
if (options.validate === false) {
|
|
2445
|
+
checked = payload;
|
|
2446
|
+
} else {
|
|
2447
|
+
const result = validateWidgetData(payload, {
|
|
2448
|
+
widgetKey: definition.key,
|
|
2449
|
+
expectedKind: definition.kind
|
|
2450
|
+
});
|
|
2451
|
+
if (!result.ok) {
|
|
2452
|
+
return { definition, status: "error", error: result.error };
|
|
2453
|
+
}
|
|
2454
|
+
checked = result.value;
|
|
2455
|
+
}
|
|
2456
|
+
const emptyText = emptyTextFor(checked);
|
|
2457
|
+
if (emptyText !== null) {
|
|
2458
|
+
return { definition, status: "empty", emptyText };
|
|
2459
|
+
}
|
|
2460
|
+
return { definition, status: "ok", data: checked };
|
|
2461
|
+
}
|
|
2462
|
+
function failedWidget(definition, error) {
|
|
2463
|
+
return { definition, status: "error", error };
|
|
2464
|
+
}
|
|
2465
|
+
function loadingWidgets(definitions) {
|
|
2466
|
+
return definitions.map((definition) => ({ definition, status: "loading" }));
|
|
2467
|
+
}
|
|
2468
|
+
function cacheKeyFor(definition, options) {
|
|
2469
|
+
return options.cacheKey === void 0 ? definition.key : options.cacheKey(definition);
|
|
2470
|
+
}
|
|
2471
|
+
var LoadInterruption = class extends Error {
|
|
2472
|
+
};
|
|
2473
|
+
async function resolveWidget(definition, provider, context, options = {}) {
|
|
2474
|
+
const quotedKey = JSON.stringify(definition.key);
|
|
2475
|
+
if (provider === void 0) {
|
|
2476
|
+
return failedWidget(
|
|
2477
|
+
definition,
|
|
2478
|
+
`Widget ${quotedKey}: no provider is registered for this key.`
|
|
2479
|
+
);
|
|
2480
|
+
}
|
|
2481
|
+
if (options.signal?.aborted === true) {
|
|
2482
|
+
return failedWidget(
|
|
2483
|
+
definition,
|
|
2484
|
+
`Widget ${quotedKey}: loading was canceled before it started.`
|
|
2485
|
+
);
|
|
2486
|
+
}
|
|
2487
|
+
const controller = new AbortController();
|
|
2488
|
+
const onExternalAbort = () => controller.abort(options.signal?.reason);
|
|
2489
|
+
options.signal?.addEventListener("abort", onExternalAbort, { once: true });
|
|
2490
|
+
let timedOut = false;
|
|
2491
|
+
const timeoutMs = options.timeoutMs !== void 0 && options.timeoutMs > 0 ? options.timeoutMs : void 0;
|
|
2492
|
+
const timer = timeoutMs === void 0 ? void 0 : setTimeout(() => {
|
|
2493
|
+
timedOut = true;
|
|
2494
|
+
controller.abort(new Error(`Timed out after ${timeoutMs} ms.`));
|
|
2495
|
+
}, timeoutMs);
|
|
2496
|
+
const interrupted = new Promise((_resolve, reject) => {
|
|
2497
|
+
controller.signal.addEventListener(
|
|
2498
|
+
"abort",
|
|
2499
|
+
() => reject(new LoadInterruption()),
|
|
2500
|
+
{ once: true }
|
|
2501
|
+
);
|
|
2502
|
+
});
|
|
2503
|
+
let payload;
|
|
2504
|
+
try {
|
|
2505
|
+
payload = await Promise.race([
|
|
2506
|
+
Promise.resolve().then(() => {
|
|
2507
|
+
if (controller.signal.aborted) throw new LoadInterruption();
|
|
2508
|
+
return provider(context, definition, { signal: controller.signal });
|
|
2509
|
+
}),
|
|
2510
|
+
interrupted
|
|
2511
|
+
]);
|
|
2512
|
+
} catch (cause) {
|
|
2513
|
+
if (cause instanceof LoadInterruption) {
|
|
2514
|
+
return failedWidget(
|
|
2515
|
+
definition,
|
|
2516
|
+
timedOut ? `Widget ${quotedKey}: provider timed out after ${timeoutMs} ms.` : `Widget ${quotedKey}: loading was canceled.`
|
|
2517
|
+
);
|
|
2518
|
+
}
|
|
2519
|
+
return failedWidget(
|
|
2520
|
+
definition,
|
|
2521
|
+
`Widget ${quotedKey}: provider failed: ${describeUnknownError(cause)}`
|
|
2522
|
+
);
|
|
2523
|
+
} finally {
|
|
2524
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
2525
|
+
options.signal?.removeEventListener("abort", onExternalAbort);
|
|
2526
|
+
}
|
|
2527
|
+
const resolved = resolvePayload(definition, payload, options);
|
|
2528
|
+
if (resolved.status !== "ok") return resolved;
|
|
2529
|
+
const storedAt = Date.now();
|
|
2530
|
+
if (options.cache !== void 0) {
|
|
2531
|
+
try {
|
|
2532
|
+
await options.cache.set(cacheKeyFor(definition, options), {
|
|
2533
|
+
payload: resolved.data,
|
|
2534
|
+
storedAt
|
|
2535
|
+
});
|
|
2536
|
+
} catch {
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
return { ...resolved, updatedAt: storedAt };
|
|
2540
|
+
}
|
|
2541
|
+
function widgetsFor(definitions, roles) {
|
|
2542
|
+
return sortDefinitions(
|
|
2543
|
+
definitions.filter((definition) => isWidgetVisibleTo(definition, roles))
|
|
2544
|
+
);
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
// src/core/loader.ts
|
|
2548
|
+
function createWidgetLoader(options) {
|
|
2549
|
+
const { definitions, providers, context, ...resolveOptions } = options;
|
|
2550
|
+
const visible = widgetsFor(definitions, context.roles);
|
|
2551
|
+
let snapshot = loadingWidgets(visible);
|
|
2552
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2553
|
+
const started = /* @__PURE__ */ new Set();
|
|
2554
|
+
const generations = /* @__PURE__ */ new Map();
|
|
2555
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
2556
|
+
let disposed = false;
|
|
2557
|
+
function publish(next) {
|
|
2558
|
+
snapshot = next;
|
|
2559
|
+
for (const listener of [...listeners]) {
|
|
2560
|
+
try {
|
|
2561
|
+
listener();
|
|
2562
|
+
} catch {
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
function replace(key, widget) {
|
|
2567
|
+
publish(
|
|
2568
|
+
snapshot.map(
|
|
2569
|
+
(current) => current.definition.key === key ? widget : current
|
|
2570
|
+
)
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
2573
|
+
function abortAll(reason) {
|
|
2574
|
+
for (const controller of inFlight.values()) controller.abort(reason);
|
|
2575
|
+
inFlight.clear();
|
|
2576
|
+
}
|
|
2577
|
+
resolveOptions.signal?.addEventListener(
|
|
2578
|
+
"abort",
|
|
2579
|
+
() => abortAll(resolveOptions.signal?.reason),
|
|
2580
|
+
{ once: true }
|
|
2581
|
+
);
|
|
2582
|
+
async function run(definition, readCache) {
|
|
2583
|
+
const key = definition.key;
|
|
2584
|
+
const generation = (generations.get(key) ?? 0) + 1;
|
|
2585
|
+
generations.set(key, generation);
|
|
2586
|
+
inFlight.get(key)?.abort(new Error("Superseded by a newer load."));
|
|
2587
|
+
const controller = new AbortController();
|
|
2588
|
+
inFlight.set(key, controller);
|
|
2589
|
+
const isCurrent = () => !disposed && generations.get(key) === generation;
|
|
2590
|
+
if (readCache && resolveOptions.cache !== void 0) {
|
|
2591
|
+
try {
|
|
2592
|
+
const entry = await resolveOptions.cache.get(
|
|
2593
|
+
cacheKeyFor(definition, resolveOptions)
|
|
2594
|
+
);
|
|
2595
|
+
if (entry !== void 0 && isCurrent()) {
|
|
2596
|
+
const cached = resolvePayload(
|
|
2597
|
+
definition,
|
|
2598
|
+
entry.payload,
|
|
2599
|
+
resolveOptions
|
|
2600
|
+
);
|
|
2601
|
+
if (cached.status === "ok") {
|
|
2602
|
+
replace(key, { ...cached, updatedAt: entry.storedAt, stale: true });
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
} catch {
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
if (!isCurrent()) return;
|
|
2609
|
+
const provider = Object.prototype.hasOwnProperty.call(providers, key) ? providers[key] : void 0;
|
|
2610
|
+
const result = await resolveWidget(definition, provider, context, {
|
|
2611
|
+
...resolveOptions,
|
|
2612
|
+
signal: controller.signal
|
|
2613
|
+
});
|
|
2614
|
+
if (!isCurrent()) return;
|
|
2615
|
+
inFlight.delete(key);
|
|
2616
|
+
replace(key, result);
|
|
2617
|
+
}
|
|
2618
|
+
function start(definition, readCache) {
|
|
2619
|
+
started.add(definition.key);
|
|
2620
|
+
void run(definition, readCache);
|
|
2621
|
+
}
|
|
2622
|
+
function definitionFor(key) {
|
|
2623
|
+
return visible.find((definition) => definition.key === key);
|
|
2624
|
+
}
|
|
2625
|
+
function load(key) {
|
|
2626
|
+
if (disposed || started.has(key)) return;
|
|
2627
|
+
const definition = definitionFor(key);
|
|
2628
|
+
if (definition !== void 0) start(definition, true);
|
|
2629
|
+
}
|
|
2630
|
+
function loadAll() {
|
|
2631
|
+
for (const definition of visible) load(definition.key);
|
|
2632
|
+
}
|
|
2633
|
+
function refresh(key, refreshOptions) {
|
|
2634
|
+
if (disposed) return;
|
|
2635
|
+
const keys = key === void 0 ? [...started] : [key];
|
|
2636
|
+
for (const target of keys) {
|
|
2637
|
+
const definition = definitionFor(target);
|
|
2638
|
+
if (definition === void 0) continue;
|
|
2639
|
+
if (refreshOptions?.skipInFlight === true && inFlight.has(target)) {
|
|
2640
|
+
continue;
|
|
2641
|
+
}
|
|
2642
|
+
const current = snapshot.find(
|
|
2643
|
+
(widget) => widget.definition.key === target
|
|
2644
|
+
);
|
|
2645
|
+
if (current?.status === "error") {
|
|
2646
|
+
replace(target, { definition, status: "loading" });
|
|
2647
|
+
}
|
|
2648
|
+
start(definition, false);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
function dispose() {
|
|
2652
|
+
disposed = true;
|
|
2653
|
+
abortAll(new Error("The widget loader was disposed."));
|
|
2654
|
+
listeners.clear();
|
|
2655
|
+
}
|
|
2656
|
+
function subscribe(listener) {
|
|
2657
|
+
listeners.add(listener);
|
|
2658
|
+
return () => {
|
|
2659
|
+
listeners.delete(listener);
|
|
2660
|
+
};
|
|
2661
|
+
}
|
|
2662
|
+
return {
|
|
2663
|
+
getSnapshot: () => snapshot,
|
|
2664
|
+
subscribe,
|
|
2665
|
+
load,
|
|
2666
|
+
loadAll,
|
|
2667
|
+
refresh,
|
|
2668
|
+
dispose
|
|
2669
|
+
};
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
// src/react/use-widgets.ts
|
|
2673
|
+
function useWidgets(definitions, providers, context, options = {}) {
|
|
2674
|
+
const {
|
|
2675
|
+
loadWhen = "mount",
|
|
2676
|
+
rootMargin = "200px",
|
|
2677
|
+
refreshMs = 0,
|
|
2678
|
+
validate,
|
|
2679
|
+
timeoutMs,
|
|
2680
|
+
signal,
|
|
2681
|
+
cache,
|
|
2682
|
+
cacheKey
|
|
2683
|
+
} = options;
|
|
2684
|
+
const placeholders = react.useMemo(
|
|
2685
|
+
() => loadingWidgets(widgetsFor(definitions, context.roles)),
|
|
2686
|
+
[definitions, context.roles]
|
|
2687
|
+
);
|
|
2688
|
+
const [widgets, setWidgets] = react.useState(placeholders);
|
|
2689
|
+
const [loader, setLoader] = react.useState(null);
|
|
2690
|
+
const loaderRef = react.useRef(null);
|
|
2691
|
+
const gridRef = react.useRef(null);
|
|
2692
|
+
react.useEffect(() => {
|
|
2693
|
+
const created = createWidgetLoader({
|
|
2694
|
+
definitions,
|
|
2695
|
+
providers,
|
|
2696
|
+
context,
|
|
2697
|
+
...validate === void 0 ? {} : { validate },
|
|
2698
|
+
...timeoutMs === void 0 ? {} : { timeoutMs },
|
|
2699
|
+
...signal === void 0 ? {} : { signal },
|
|
2700
|
+
...cache === void 0 ? {} : { cache },
|
|
2701
|
+
...cacheKey === void 0 ? {} : { cacheKey }
|
|
2702
|
+
});
|
|
2703
|
+
loaderRef.current = created;
|
|
2704
|
+
setWidgets(created.getSnapshot());
|
|
2705
|
+
const unsubscribe = created.subscribe(
|
|
2706
|
+
() => setWidgets(created.getSnapshot())
|
|
2707
|
+
);
|
|
2708
|
+
setLoader(created);
|
|
2709
|
+
if (loadWhen === "mount" || typeof IntersectionObserver === "undefined") {
|
|
2710
|
+
created.loadAll();
|
|
2711
|
+
}
|
|
2712
|
+
return () => {
|
|
2713
|
+
unsubscribe();
|
|
2714
|
+
created.dispose();
|
|
2715
|
+
loaderRef.current = null;
|
|
2716
|
+
setLoader(null);
|
|
2717
|
+
};
|
|
2718
|
+
}, [
|
|
2719
|
+
definitions,
|
|
2720
|
+
providers,
|
|
2721
|
+
context,
|
|
2722
|
+
validate,
|
|
2723
|
+
timeoutMs,
|
|
2724
|
+
signal,
|
|
2725
|
+
cache,
|
|
2726
|
+
cacheKey,
|
|
2727
|
+
loadWhen
|
|
2728
|
+
]);
|
|
2729
|
+
const cardKeys = widgets.map((widget) => widget.definition.key).join("\0");
|
|
2730
|
+
react.useEffect(() => {
|
|
2731
|
+
const grid = gridRef.current;
|
|
2732
|
+
if (loader === null || grid === null || loadWhen !== "visible" || typeof IntersectionObserver === "undefined") {
|
|
2733
|
+
return void 0;
|
|
2734
|
+
}
|
|
2735
|
+
const observer = new IntersectionObserver(
|
|
2736
|
+
(entries) => {
|
|
2737
|
+
for (const entry of entries) {
|
|
2738
|
+
const key = entry.target.dataset["widgetKey"];
|
|
2739
|
+
if (entry.isIntersecting && key !== void 0) {
|
|
2740
|
+
loader.load(key);
|
|
2741
|
+
observer.unobserve(entry.target);
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
},
|
|
2745
|
+
{ rootMargin }
|
|
2746
|
+
);
|
|
2747
|
+
grid.querySelectorAll("[data-widget-key]").forEach((card) => observer.observe(card));
|
|
2748
|
+
return () => observer.disconnect();
|
|
2749
|
+
}, [loader, loadWhen, rootMargin, cardKeys]);
|
|
2750
|
+
react.useEffect(() => {
|
|
2751
|
+
if (loader === null || !(refreshMs > 0)) return void 0;
|
|
2752
|
+
const timer = setInterval(() => {
|
|
2753
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
2754
|
+
return;
|
|
2755
|
+
}
|
|
2756
|
+
loader.refresh(void 0, { skipInFlight: true });
|
|
2757
|
+
}, refreshMs);
|
|
2758
|
+
return () => clearInterval(timer);
|
|
2759
|
+
}, [loader, refreshMs]);
|
|
2760
|
+
const refresh = react.useCallback((key) => {
|
|
2761
|
+
loaderRef.current?.refresh(key);
|
|
2762
|
+
}, []);
|
|
2763
|
+
return { widgets, refresh, gridRef };
|
|
2764
|
+
}
|
|
2765
|
+
|
|
1248
2766
|
exports.AlertListWidget = AlertListWidget;
|
|
1249
2767
|
exports.BarListWidget = BarListWidget;
|
|
1250
2768
|
exports.DEFAULT_LABELS = DEFAULT_LABELS;
|
|
@@ -1259,6 +2777,8 @@ exports.TextWidget = TextWidget;
|
|
|
1259
2777
|
exports.Thumbnail = Thumbnail;
|
|
1260
2778
|
exports.WidgetCard = WidgetCard;
|
|
1261
2779
|
exports.WidgetContent = WidgetContent;
|
|
2780
|
+
exports.WidgetDetail = WidgetDetail;
|
|
2781
|
+
exports.WidgetDetailDialog = WidgetDetailDialog;
|
|
1262
2782
|
exports.WidgetGrid = WidgetGrid;
|
|
1263
2783
|
exports.WidgetLink = WidgetLink;
|
|
1264
2784
|
exports.WidgetSettingsProvider = WidgetSettingsProvider;
|
|
@@ -1267,7 +2787,9 @@ exports.buildChartModel = buildChartModel;
|
|
|
1267
2787
|
exports.describeChart = describeChart;
|
|
1268
2788
|
exports.joinClassNames = joinClassNames;
|
|
1269
2789
|
exports.seriesColor = seriesColor;
|
|
2790
|
+
exports.useDetailData = useDetailData;
|
|
1270
2791
|
exports.useWidgetSettings = useWidgetSettings;
|
|
2792
|
+
exports.useWidgets = useWidgets;
|
|
1271
2793
|
exports.widgetTitle = widgetTitle;
|
|
1272
2794
|
//# sourceMappingURL=react.cjs.map
|
|
1273
2795
|
//# sourceMappingURL=react.cjs.map
|