@richardmcquiston01/dashboard-widgets-toolkit 0.10.0 → 0.11.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/dist/core.cjs +1222 -17
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +13 -179
- package/dist/core.d.ts +13 -179
- package/dist/core.js +1191 -18
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +1222 -17
- 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 +1191 -18
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +587 -32
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +75 -5
- package/dist/react.d.ts +75 -5
- package/dist/react.js +588 -34
- package/dist/react.js.map +1 -1
- package/dist/storage-VaaysVnZ.d.cts +1333 -0
- package/dist/storage-VaaysVnZ.d.ts +1333 -0
- package/package.json +1 -1
- package/dist/scale-N69bo7wo.d.cts +0 -758
- package/dist/scale-N69bo7wo.d.ts +0 -758
package/dist/react.cjs
CHANGED
|
@@ -472,6 +472,265 @@ function placeAndFillRows(items, columns, spanOf) {
|
|
|
472
472
|
return { spans, rows };
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
+
// src/core/options.ts
|
|
476
|
+
var DATE_RANGE_PRESETS = [
|
|
477
|
+
"today",
|
|
478
|
+
"yesterday",
|
|
479
|
+
"last7",
|
|
480
|
+
"last30",
|
|
481
|
+
"last90",
|
|
482
|
+
"thisMonth",
|
|
483
|
+
"lastMonth",
|
|
484
|
+
"thisYear",
|
|
485
|
+
"lastYear"
|
|
486
|
+
];
|
|
487
|
+
var COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
|
|
488
|
+
var INTERVAL_PATTERN = /^(\d{4}-\d{2}-\d{2})\/(\d{4}-\d{2}-\d{2})$/;
|
|
489
|
+
var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
|
|
490
|
+
function isClientApplied(option) {
|
|
491
|
+
return (option.type === "sort" || option.type === "columns") && option.apply === "client";
|
|
492
|
+
}
|
|
493
|
+
function civilToday(context) {
|
|
494
|
+
const now = context.now ?? Date.now();
|
|
495
|
+
let timeZone = context.timeZone ?? "UTC";
|
|
496
|
+
try {
|
|
497
|
+
new Intl.DateTimeFormat("en-US", { timeZone });
|
|
498
|
+
} catch {
|
|
499
|
+
timeZone = "UTC";
|
|
500
|
+
}
|
|
501
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
502
|
+
timeZone,
|
|
503
|
+
year: "numeric",
|
|
504
|
+
month: "2-digit",
|
|
505
|
+
day: "2-digit"
|
|
506
|
+
}).formatToParts(new Date(now));
|
|
507
|
+
const pick = (type) => Number(parts.find((part) => part.type === type)?.value ?? "0");
|
|
508
|
+
return { year: pick("year"), month: pick("month"), day: pick("day") };
|
|
509
|
+
}
|
|
510
|
+
function toIso(epochMs) {
|
|
511
|
+
return new Date(epochMs).toISOString().slice(0, 10);
|
|
512
|
+
}
|
|
513
|
+
var DAY_MS = 864e5;
|
|
514
|
+
function parseIsoDay(text) {
|
|
515
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
|
|
516
|
+
if (match === null) return void 0;
|
|
517
|
+
const epoch = Date.UTC(
|
|
518
|
+
Number(match[1]),
|
|
519
|
+
Number(match[2]) - 1,
|
|
520
|
+
Number(match[3])
|
|
521
|
+
);
|
|
522
|
+
return toIso(epoch) === text ? epoch : void 0;
|
|
523
|
+
}
|
|
524
|
+
function isIsoInterval(value) {
|
|
525
|
+
const match = INTERVAL_PATTERN.exec(value);
|
|
526
|
+
if (match === null) return false;
|
|
527
|
+
const start = parseIsoDay(match[1]);
|
|
528
|
+
const end = parseIsoDay(match[2]);
|
|
529
|
+
return start !== void 0 && end !== void 0 && start <= end;
|
|
530
|
+
}
|
|
531
|
+
function isPreset(value) {
|
|
532
|
+
return DATE_RANGE_PRESETS.includes(value);
|
|
533
|
+
}
|
|
534
|
+
function resolveDateRange(value, context = {}) {
|
|
535
|
+
if (isIsoInterval(value)) return value;
|
|
536
|
+
if (!isPreset(value)) return void 0;
|
|
537
|
+
const today = civilToday(context);
|
|
538
|
+
const todayMs = Date.UTC(today.year, today.month - 1, today.day);
|
|
539
|
+
const range = (startMs, endMs) => `${toIso(startMs)}/${toIso(endMs)}`;
|
|
540
|
+
switch (value) {
|
|
541
|
+
case "today":
|
|
542
|
+
return range(todayMs, todayMs);
|
|
543
|
+
case "yesterday":
|
|
544
|
+
return range(todayMs - DAY_MS, todayMs - DAY_MS);
|
|
545
|
+
case "last7":
|
|
546
|
+
return range(todayMs - 6 * DAY_MS, todayMs);
|
|
547
|
+
case "last30":
|
|
548
|
+
return range(todayMs - 29 * DAY_MS, todayMs);
|
|
549
|
+
case "last90":
|
|
550
|
+
return range(todayMs - 89 * DAY_MS, todayMs);
|
|
551
|
+
case "thisMonth":
|
|
552
|
+
return range(Date.UTC(today.year, today.month - 1, 1), todayMs);
|
|
553
|
+
case "lastMonth":
|
|
554
|
+
return range(
|
|
555
|
+
Date.UTC(today.year, today.month - 2, 1),
|
|
556
|
+
Date.UTC(today.year, today.month - 1, 1) - DAY_MS
|
|
557
|
+
);
|
|
558
|
+
case "thisYear":
|
|
559
|
+
return range(Date.UTC(today.year, 0, 1), todayMs);
|
|
560
|
+
case "lastYear":
|
|
561
|
+
return range(
|
|
562
|
+
Date.UTC(today.year - 1, 0, 1),
|
|
563
|
+
Date.UTC(today.year, 0, 1) - DAY_MS
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function onStepGrid(option, value) {
|
|
568
|
+
if (option.step === void 0) return true;
|
|
569
|
+
const steps = (value - option.min) / option.step;
|
|
570
|
+
return Math.abs(steps - Math.round(steps)) < 1e-9;
|
|
571
|
+
}
|
|
572
|
+
function parseSortValue(value) {
|
|
573
|
+
const separator = value.lastIndexOf(":");
|
|
574
|
+
if (separator <= 0) return void 0;
|
|
575
|
+
const column = value.slice(0, separator);
|
|
576
|
+
const direction = value.slice(separator + 1);
|
|
577
|
+
if (direction !== "asc" && direction !== "desc") return void 0;
|
|
578
|
+
return { column, direction };
|
|
579
|
+
}
|
|
580
|
+
function sortFromOptions(definition, values) {
|
|
581
|
+
if (values === void 0) return void 0;
|
|
582
|
+
const sortOption = definition.options?.find(
|
|
583
|
+
(option) => option.type === "sort"
|
|
584
|
+
);
|
|
585
|
+
if (sortOption === void 0) return void 0;
|
|
586
|
+
const chosen = values[sortOption.key];
|
|
587
|
+
return typeof chosen === "string" ? parseSortValue(chosen) : void 0;
|
|
588
|
+
}
|
|
589
|
+
function coerceOptionValue(option, value, context = {}) {
|
|
590
|
+
switch (option.type) {
|
|
591
|
+
case "choice":
|
|
592
|
+
return typeof value === "string" && option.choices.some((choice) => choice.value === value) ? value : void 0;
|
|
593
|
+
case "number":
|
|
594
|
+
return typeof value === "number" && Number.isFinite(value) && value >= option.min && value <= option.max && onStepGrid(option, value) ? value : void 0;
|
|
595
|
+
case "boolean":
|
|
596
|
+
return typeof value === "boolean" ? value : void 0;
|
|
597
|
+
case "text": {
|
|
598
|
+
if (typeof value !== "string") return void 0;
|
|
599
|
+
const trimmed = value.trim();
|
|
600
|
+
return trimmed.length <= option.maxLength && !CONTROL_CHARACTERS.test(trimmed) ? trimmed : void 0;
|
|
601
|
+
}
|
|
602
|
+
case "dateRange": {
|
|
603
|
+
if (typeof value !== "string") return void 0;
|
|
604
|
+
if (isPreset(value) && option.presets !== void 0 && !option.presets.some((preset) => preset.value === value)) {
|
|
605
|
+
return void 0;
|
|
606
|
+
}
|
|
607
|
+
return resolveDateRange(value, context);
|
|
608
|
+
}
|
|
609
|
+
case "color":
|
|
610
|
+
return typeof value === "string" && COLOR_PATTERN.test(value) ? value.toLowerCase() : void 0;
|
|
611
|
+
case "sort": {
|
|
612
|
+
if (typeof value !== "string") return void 0;
|
|
613
|
+
const sort = parseSortValue(value);
|
|
614
|
+
return sort !== void 0 && option.columns.some((column) => column.key === sort.column) ? value : void 0;
|
|
615
|
+
}
|
|
616
|
+
case "columns": {
|
|
617
|
+
if (!Array.isArray(value) || value.length === 0) return void 0;
|
|
618
|
+
const keys = value;
|
|
619
|
+
const allowed = new Set(option.columns.map((c) => c.key));
|
|
620
|
+
const seen = /* @__PURE__ */ new Set();
|
|
621
|
+
for (const key of keys) {
|
|
622
|
+
if (typeof key !== "string" || !allowed.has(key) || seen.has(key)) {
|
|
623
|
+
return void 0;
|
|
624
|
+
}
|
|
625
|
+
seen.add(key);
|
|
626
|
+
}
|
|
627
|
+
return [...seen];
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
function defaultOptionValue(option, context = {}) {
|
|
632
|
+
switch (option.type) {
|
|
633
|
+
case "dateRange":
|
|
634
|
+
return resolveDateRange(option.default, context);
|
|
635
|
+
case "sort":
|
|
636
|
+
return option.default;
|
|
637
|
+
case "columns":
|
|
638
|
+
return [...option.default];
|
|
639
|
+
default:
|
|
640
|
+
return option.default;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
var NO_VALUES = Object.freeze({});
|
|
644
|
+
function resolveOptionValues(definition, chosen, context = {}) {
|
|
645
|
+
const options = definition.options;
|
|
646
|
+
if (options === void 0 || options.length === 0) return NO_VALUES;
|
|
647
|
+
const values = {};
|
|
648
|
+
for (const option of options) {
|
|
649
|
+
const picked = chosen?.[option.key];
|
|
650
|
+
const value = (picked === void 0 ? void 0 : coerceOptionValue(option, picked, context)) ?? defaultOptionValue(option, context);
|
|
651
|
+
if (value !== void 0) values[option.key] = value;
|
|
652
|
+
}
|
|
653
|
+
return values;
|
|
654
|
+
}
|
|
655
|
+
function sameValue(a, b) {
|
|
656
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
657
|
+
return a.length === b.length && a.every((item, i) => item === b[i]);
|
|
658
|
+
}
|
|
659
|
+
return a === b;
|
|
660
|
+
}
|
|
661
|
+
function sameOptionValues(a, b) {
|
|
662
|
+
const keys = Object.keys({ ...a, ...b });
|
|
663
|
+
return keys.every((key) => sameValue(a[key], b[key]));
|
|
664
|
+
}
|
|
665
|
+
function optionsCacheSuffix(definition, values, context = {}) {
|
|
666
|
+
const parts = [];
|
|
667
|
+
for (const option of definition.options ?? []) {
|
|
668
|
+
if (isClientApplied(option)) continue;
|
|
669
|
+
const value = values[option.key];
|
|
670
|
+
if (sameValue(value, defaultOptionValue(option, context))) continue;
|
|
671
|
+
parts.push(
|
|
672
|
+
`${option.key}=${encodeURIComponent(JSON.stringify(value ?? null))}`
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
parts.sort();
|
|
676
|
+
return parts.length === 0 ? "" : `?${parts.join("&")}`;
|
|
677
|
+
}
|
|
678
|
+
function tableColumnIndex(key) {
|
|
679
|
+
const match = /^c(\d+)$/.exec(key);
|
|
680
|
+
return match === null ? void 0 : Number(match[1]);
|
|
681
|
+
}
|
|
682
|
+
function sortBarItems(items, sort) {
|
|
683
|
+
const sign = sort.direction === "desc" ? -1 : 1;
|
|
684
|
+
return items.map((item, index) => ({ item, index })).sort((a, b) => {
|
|
685
|
+
const order = sort.column === "value" ? a.item.value - b.item.value : a.item.label.localeCompare(b.item.label);
|
|
686
|
+
return order * sign || a.index - b.index;
|
|
687
|
+
}).map(({ item }) => item);
|
|
688
|
+
}
|
|
689
|
+
function applyClientOptions(definition, data, values) {
|
|
690
|
+
const options = definition.options ?? [];
|
|
691
|
+
const sortOption = options.find(
|
|
692
|
+
(o) => o.type === "sort" && o.apply === "client"
|
|
693
|
+
);
|
|
694
|
+
const columnsOption = options.find(
|
|
695
|
+
(o) => o.type === "columns" && o.apply === "client"
|
|
696
|
+
);
|
|
697
|
+
const sortValue = sortOption === void 0 ? void 0 : values[sortOption.key];
|
|
698
|
+
const columnsValue = columnsOption === void 0 ? void 0 : values[columnsOption.key];
|
|
699
|
+
let next = data;
|
|
700
|
+
if (typeof sortValue === "string") {
|
|
701
|
+
const sort = parseSortValue(sortValue);
|
|
702
|
+
if (sort !== void 0 && next.kind === "BAR_LIST") {
|
|
703
|
+
if (sort.column === "label" || sort.column === "value") {
|
|
704
|
+
next = { ...next, items: sortBarItems(next.items, sort) };
|
|
705
|
+
}
|
|
706
|
+
} else if (sort !== void 0 && next.kind === "TABLE") {
|
|
707
|
+
const index = tableColumnIndex(sort.column);
|
|
708
|
+
if (index !== void 0 && index < next.columns.length) {
|
|
709
|
+
const sorted = queryRows(tableDetailData(next), {
|
|
710
|
+
page: 1,
|
|
711
|
+
pageSize: Math.max(1, next.rows.length),
|
|
712
|
+
sort
|
|
713
|
+
});
|
|
714
|
+
if (sorted.ok) next = { ...next, rows: sorted.value.rows };
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (Array.isArray(columnsValue) && next.kind === "TABLE") {
|
|
719
|
+
const table = next;
|
|
720
|
+
const indexes = columnsValue.map(tableColumnIndex).filter(
|
|
721
|
+
(index) => index !== void 0 && index < table.columns.length
|
|
722
|
+
);
|
|
723
|
+
if (indexes.length > 0) {
|
|
724
|
+
next = {
|
|
725
|
+
...table,
|
|
726
|
+
columns: indexes.map((i) => table.columns[i]),
|
|
727
|
+
rows: table.rows.map((row) => indexes.map((i) => row[i]))
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return next;
|
|
732
|
+
}
|
|
733
|
+
|
|
475
734
|
// src/core/scale.ts
|
|
476
735
|
function niceStep(value) {
|
|
477
736
|
if (!(value > 0) || !Number.isFinite(value)) return 1;
|
|
@@ -1238,7 +1497,8 @@ function GaugeWidget({
|
|
|
1238
1497
|
}
|
|
1239
1498
|
function TableWidget({
|
|
1240
1499
|
data,
|
|
1241
|
-
controls
|
|
1500
|
+
controls,
|
|
1501
|
+
initialSort
|
|
1242
1502
|
}) {
|
|
1243
1503
|
if (controls === void 0) {
|
|
1244
1504
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "dwt-table-wrap", children: [
|
|
@@ -1246,7 +1506,14 @@ function TableWidget({
|
|
|
1246
1506
|
data.footer !== void 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "dwt-note", children: data.footer })
|
|
1247
1507
|
] });
|
|
1248
1508
|
}
|
|
1249
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1509
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1510
|
+
InteractiveTable,
|
|
1511
|
+
{
|
|
1512
|
+
data,
|
|
1513
|
+
controls,
|
|
1514
|
+
initialSort
|
|
1515
|
+
}
|
|
1516
|
+
);
|
|
1250
1517
|
}
|
|
1251
1518
|
function TableView({
|
|
1252
1519
|
data,
|
|
@@ -1299,13 +1566,14 @@ function nextTableSort(current, column) {
|
|
|
1299
1566
|
}
|
|
1300
1567
|
function InteractiveTable({
|
|
1301
1568
|
data,
|
|
1302
|
-
controls
|
|
1569
|
+
controls,
|
|
1570
|
+
initialSort
|
|
1303
1571
|
}) {
|
|
1304
1572
|
const slot = useSlotClassName();
|
|
1305
1573
|
const { labels, locale } = useWidgetSettings();
|
|
1306
1574
|
const searchId = react.useId();
|
|
1307
1575
|
const [search, setSearch] = react.useState("");
|
|
1308
|
-
const [sort, setSort] = react.useState(
|
|
1576
|
+
const [sort, setSort] = react.useState(initialSort);
|
|
1309
1577
|
const localeTag = typeof locale === "string" ? locale : locale?.[0];
|
|
1310
1578
|
const query = {
|
|
1311
1579
|
page: 1,
|
|
@@ -1407,7 +1675,8 @@ function AlertListWidget({
|
|
|
1407
1675
|
}
|
|
1408
1676
|
function WidgetContent({
|
|
1409
1677
|
data,
|
|
1410
|
-
tableControls
|
|
1678
|
+
tableControls,
|
|
1679
|
+
initialSort
|
|
1411
1680
|
}) {
|
|
1412
1681
|
switch (data.kind) {
|
|
1413
1682
|
case "TEXT":
|
|
@@ -1417,7 +1686,14 @@ function WidgetContent({
|
|
|
1417
1686
|
case "GAUGE":
|
|
1418
1687
|
return /* @__PURE__ */ jsxRuntime.jsx(GaugeWidget, { data });
|
|
1419
1688
|
case "TABLE":
|
|
1420
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1689
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1690
|
+
TableWidget,
|
|
1691
|
+
{
|
|
1692
|
+
data,
|
|
1693
|
+
controls: tableControls,
|
|
1694
|
+
initialSort
|
|
1695
|
+
}
|
|
1696
|
+
);
|
|
1421
1697
|
case "BAR_LIST":
|
|
1422
1698
|
return /* @__PURE__ */ jsxRuntime.jsx(BarListWidget, { data });
|
|
1423
1699
|
case "ALERT_LIST":
|
|
@@ -1614,7 +1890,8 @@ function ResolvedWidgetCard({
|
|
|
1614
1890
|
WidgetContent,
|
|
1615
1891
|
{
|
|
1616
1892
|
data: widget.data,
|
|
1617
|
-
tableControls: resolveTableControls(definition)
|
|
1893
|
+
tableControls: resolveTableControls(definition),
|
|
1894
|
+
initialSort: sortFromOptions(definition, widget.options)
|
|
1618
1895
|
}
|
|
1619
1896
|
) : null
|
|
1620
1897
|
}
|
|
@@ -2376,7 +2653,8 @@ function WidgetDetailDialog({
|
|
|
2376
2653
|
}
|
|
2377
2654
|
);
|
|
2378
2655
|
}
|
|
2379
|
-
|
|
2656
|
+
var NO_OPTION_VALUES = Object.freeze({});
|
|
2657
|
+
function useDetailData(definition, load, enabled, fallback, optionValues = NO_OPTION_VALUES) {
|
|
2380
2658
|
const [state, setState] = react.useState({ status: "loading" });
|
|
2381
2659
|
const [attempt, setAttempt] = react.useState(0);
|
|
2382
2660
|
react.useEffect(() => {
|
|
@@ -2386,6 +2664,7 @@ function useDetailData(definition, load, enabled, fallback) {
|
|
|
2386
2664
|
Promise.resolve().then(
|
|
2387
2665
|
() => load === void 0 ? void 0 : load(definition, {
|
|
2388
2666
|
signal: controller.signal,
|
|
2667
|
+
options: optionValues,
|
|
2389
2668
|
query: defaultDetailQuery()
|
|
2390
2669
|
})
|
|
2391
2670
|
).then((raw) => {
|
|
@@ -2413,7 +2692,7 @@ function useDetailData(definition, load, enabled, fallback) {
|
|
|
2413
2692
|
});
|
|
2414
2693
|
});
|
|
2415
2694
|
return () => controller.abort();
|
|
2416
|
-
}, [enabled, load, fallback, definition, attempt]);
|
|
2695
|
+
}, [enabled, load, fallback, definition, optionValues, attempt]);
|
|
2417
2696
|
return { state, reload: () => setAttempt((count) => count + 1) };
|
|
2418
2697
|
}
|
|
2419
2698
|
var NEW_PAGE_VALUE = "__new-page__";
|
|
@@ -2529,7 +2808,14 @@ function DetailHost({
|
|
|
2529
2808
|
const { definition } = widget;
|
|
2530
2809
|
const options = resolveDetailOptions(definition);
|
|
2531
2810
|
const pageSize = options?.pageSize ?? 25;
|
|
2532
|
-
const
|
|
2811
|
+
const optionSort = sortFromOptions(
|
|
2812
|
+
definition,
|
|
2813
|
+
widget.options
|
|
2814
|
+
);
|
|
2815
|
+
const [query, setQuery] = react.useState({
|
|
2816
|
+
...defaultDetailQuery(pageSize),
|
|
2817
|
+
...optionSort === void 0 ? {} : { sort: optionSort }
|
|
2818
|
+
});
|
|
2533
2819
|
const derived = react.useMemo(
|
|
2534
2820
|
() => widget.status === "ok" ? deriveDetailData(widget.data, locale) : void 0,
|
|
2535
2821
|
[widget, locale]
|
|
@@ -2538,7 +2824,8 @@ function DetailHost({
|
|
|
2538
2824
|
definition,
|
|
2539
2825
|
loadDetail,
|
|
2540
2826
|
true,
|
|
2541
|
-
derived
|
|
2827
|
+
derived,
|
|
2828
|
+
widget.options
|
|
2542
2829
|
);
|
|
2543
2830
|
const title = options?.title ?? definition.title;
|
|
2544
2831
|
return /* @__PURE__ */ jsxRuntime.jsx(WidgetDetailDialog, { title, open: true, onClose, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -3392,6 +3679,196 @@ function Dashboard({
|
|
|
3392
3679
|
}
|
|
3393
3680
|
);
|
|
3394
3681
|
}
|
|
3682
|
+
function useStoredLayout(options) {
|
|
3683
|
+
const {
|
|
3684
|
+
persistence,
|
|
3685
|
+
scope,
|
|
3686
|
+
definitions,
|
|
3687
|
+
defaultLayout,
|
|
3688
|
+
saveDelayMs = 500,
|
|
3689
|
+
initialLayout,
|
|
3690
|
+
normalizeOptions
|
|
3691
|
+
} = options;
|
|
3692
|
+
const scopeKey = persistence.keyFor(scope);
|
|
3693
|
+
const [layout, setLayoutState] = react.useState(
|
|
3694
|
+
initialLayout ?? defaultLayout
|
|
3695
|
+
);
|
|
3696
|
+
const [backup, setBackupState] = react.useState(null);
|
|
3697
|
+
const [status, setStatus] = react.useState("loading");
|
|
3698
|
+
const [error, setError] = react.useState(null);
|
|
3699
|
+
const [conflict, setConflict] = react.useState(null);
|
|
3700
|
+
const [remoteLayout, setRemoteLayout] = react.useState(
|
|
3701
|
+
null
|
|
3702
|
+
);
|
|
3703
|
+
const latest = react.useRef(layout);
|
|
3704
|
+
const scopeRef = react.useRef(scope);
|
|
3705
|
+
scopeRef.current = scope;
|
|
3706
|
+
const normalizeRef = react.useRef(normalizeOptions);
|
|
3707
|
+
normalizeRef.current = normalizeOptions;
|
|
3708
|
+
const timer = react.useRef(void 0);
|
|
3709
|
+
const dirty = react.useRef(false);
|
|
3710
|
+
const apply = react.useCallback((next) => {
|
|
3711
|
+
latest.current = next;
|
|
3712
|
+
setLayoutState(next);
|
|
3713
|
+
}, []);
|
|
3714
|
+
const finishSave = react.useCallback(
|
|
3715
|
+
(result, mine) => {
|
|
3716
|
+
if (!result.ok) {
|
|
3717
|
+
setError(result.error);
|
|
3718
|
+
setStatus("error");
|
|
3719
|
+
return;
|
|
3720
|
+
}
|
|
3721
|
+
const saved = result.value;
|
|
3722
|
+
if (saved.outcome === "saved") {
|
|
3723
|
+
dirty.current = latest.current !== mine;
|
|
3724
|
+
setError(null);
|
|
3725
|
+
setStatus(dirty.current ? "saving" : "ready");
|
|
3726
|
+
return;
|
|
3727
|
+
}
|
|
3728
|
+
if (saved.outcome === "kept-theirs") {
|
|
3729
|
+
dirty.current = false;
|
|
3730
|
+
apply(saved.layout);
|
|
3731
|
+
setError(null);
|
|
3732
|
+
setStatus("ready");
|
|
3733
|
+
return;
|
|
3734
|
+
}
|
|
3735
|
+
setConflict({ mine, theirs: saved.layout });
|
|
3736
|
+
setStatus("conflict");
|
|
3737
|
+
},
|
|
3738
|
+
[apply]
|
|
3739
|
+
);
|
|
3740
|
+
const flush = react.useCallback(() => {
|
|
3741
|
+
if (timer.current !== void 0) {
|
|
3742
|
+
clearTimeout(timer.current);
|
|
3743
|
+
timer.current = void 0;
|
|
3744
|
+
}
|
|
3745
|
+
if (!dirty.current) return;
|
|
3746
|
+
const mine = latest.current;
|
|
3747
|
+
setStatus("saving");
|
|
3748
|
+
void persistence.save(scopeRef.current, mine).then((result) => finishSave(result, mine));
|
|
3749
|
+
}, [persistence, finishSave]);
|
|
3750
|
+
react.useEffect(() => {
|
|
3751
|
+
let current = true;
|
|
3752
|
+
setStatus("loading");
|
|
3753
|
+
setConflict(null);
|
|
3754
|
+
setRemoteLayout(null);
|
|
3755
|
+
void persistence.load(scopeRef.current, definitions, normalizeRef.current).then((result) => {
|
|
3756
|
+
if (!current) return;
|
|
3757
|
+
if (!result.ok) {
|
|
3758
|
+
setError(result.error);
|
|
3759
|
+
setStatus("error");
|
|
3760
|
+
return;
|
|
3761
|
+
}
|
|
3762
|
+
setError(null);
|
|
3763
|
+
if (!dirty.current && result.value.layout !== null) {
|
|
3764
|
+
apply(result.value.layout);
|
|
3765
|
+
}
|
|
3766
|
+
setStatus(dirty.current ? "saving" : "ready");
|
|
3767
|
+
});
|
|
3768
|
+
void persistence.loadBackup(scopeRef.current).then((result) => {
|
|
3769
|
+
if (current && result.ok) setBackupState(result.value);
|
|
3770
|
+
});
|
|
3771
|
+
return () => {
|
|
3772
|
+
current = false;
|
|
3773
|
+
};
|
|
3774
|
+
}, [persistence, scopeKey, definitions, apply]);
|
|
3775
|
+
react.useEffect(() => {
|
|
3776
|
+
return persistence.watch(
|
|
3777
|
+
scopeRef.current,
|
|
3778
|
+
definitions,
|
|
3779
|
+
(loaded) => {
|
|
3780
|
+
if (loaded.layout === null) return;
|
|
3781
|
+
if (dirty.current) {
|
|
3782
|
+
setRemoteLayout(loaded.layout);
|
|
3783
|
+
return;
|
|
3784
|
+
}
|
|
3785
|
+
apply(loaded.layout);
|
|
3786
|
+
},
|
|
3787
|
+
normalizeRef.current
|
|
3788
|
+
);
|
|
3789
|
+
}, [persistence, scopeKey, definitions, apply]);
|
|
3790
|
+
react.useEffect(() => {
|
|
3791
|
+
const onHide = () => {
|
|
3792
|
+
if (typeof document === "undefined" || document.visibilityState !== "hidden") {
|
|
3793
|
+
return;
|
|
3794
|
+
}
|
|
3795
|
+
flush();
|
|
3796
|
+
};
|
|
3797
|
+
if (typeof window !== "undefined") {
|
|
3798
|
+
window.addEventListener("pagehide", flush);
|
|
3799
|
+
document.addEventListener("visibilitychange", onHide);
|
|
3800
|
+
}
|
|
3801
|
+
return () => {
|
|
3802
|
+
if (typeof window !== "undefined") {
|
|
3803
|
+
window.removeEventListener("pagehide", flush);
|
|
3804
|
+
document.removeEventListener("visibilitychange", onHide);
|
|
3805
|
+
}
|
|
3806
|
+
flush();
|
|
3807
|
+
};
|
|
3808
|
+
}, [flush]);
|
|
3809
|
+
const setLayout = react.useCallback(
|
|
3810
|
+
(next) => {
|
|
3811
|
+
apply(next);
|
|
3812
|
+
dirty.current = true;
|
|
3813
|
+
if (timer.current !== void 0) clearTimeout(timer.current);
|
|
3814
|
+
timer.current = setTimeout(flush, Math.max(0, saveDelayMs));
|
|
3815
|
+
},
|
|
3816
|
+
[apply, flush, saveDelayMs]
|
|
3817
|
+
);
|
|
3818
|
+
const setBackup = react.useCallback(
|
|
3819
|
+
(next) => {
|
|
3820
|
+
setBackupState(next);
|
|
3821
|
+
if (next === null) return;
|
|
3822
|
+
void persistence.saveBackup(scopeRef.current, next).then((result) => {
|
|
3823
|
+
if (!result.ok) setError(result.error);
|
|
3824
|
+
});
|
|
3825
|
+
},
|
|
3826
|
+
[persistence]
|
|
3827
|
+
);
|
|
3828
|
+
const resolveConflict = react.useCallback(
|
|
3829
|
+
(choice) => {
|
|
3830
|
+
const pending = conflict;
|
|
3831
|
+
if (pending === null) return;
|
|
3832
|
+
setConflict(null);
|
|
3833
|
+
if (choice === "theirs") {
|
|
3834
|
+
dirty.current = false;
|
|
3835
|
+
apply(pending.theirs);
|
|
3836
|
+
setStatus("ready");
|
|
3837
|
+
return;
|
|
3838
|
+
}
|
|
3839
|
+
setStatus("saving");
|
|
3840
|
+
void persistence.save(scopeRef.current, pending.mine, { force: true }).then((result) => finishSave(result, pending.mine));
|
|
3841
|
+
},
|
|
3842
|
+
[conflict, persistence, apply, finishSave]
|
|
3843
|
+
);
|
|
3844
|
+
const resolveRemote = react.useCallback(
|
|
3845
|
+
(choice) => {
|
|
3846
|
+
const remote = remoteLayout;
|
|
3847
|
+
setRemoteLayout(null);
|
|
3848
|
+
if (choice === "use" && remote !== null) {
|
|
3849
|
+
if (timer.current !== void 0) {
|
|
3850
|
+
clearTimeout(timer.current);
|
|
3851
|
+
timer.current = void 0;
|
|
3852
|
+
}
|
|
3853
|
+
dirty.current = false;
|
|
3854
|
+
apply(remote);
|
|
3855
|
+
}
|
|
3856
|
+
},
|
|
3857
|
+
[remoteLayout, apply]
|
|
3858
|
+
);
|
|
3859
|
+
return {
|
|
3860
|
+
layout,
|
|
3861
|
+
setLayout,
|
|
3862
|
+
backup,
|
|
3863
|
+
setBackup,
|
|
3864
|
+
status,
|
|
3865
|
+
error,
|
|
3866
|
+
conflict,
|
|
3867
|
+
resolveConflict,
|
|
3868
|
+
remoteLayout,
|
|
3869
|
+
resolveRemote
|
|
3870
|
+
};
|
|
3871
|
+
}
|
|
3395
3872
|
|
|
3396
3873
|
// src/core/payload.ts
|
|
3397
3874
|
var WIDGET_KINDS = [
|
|
@@ -3735,12 +4212,33 @@ function failedWidget(definition, error) {
|
|
|
3735
4212
|
function loadingWidgets(definitions) {
|
|
3736
4213
|
return definitions.map((definition) => ({ definition, status: "loading" }));
|
|
3737
4214
|
}
|
|
3738
|
-
function cacheKeyFor(definition, options) {
|
|
3739
|
-
|
|
4215
|
+
function cacheKeyFor(definition, options, values) {
|
|
4216
|
+
const base = options.cacheKey === void 0 ? definition.key : options.cacheKey(definition);
|
|
4217
|
+
return values === void 0 ? base : `${base}${optionsCacheSuffix(definition, values, options)}`;
|
|
4218
|
+
}
|
|
4219
|
+
function optionValuesFor(definition, options) {
|
|
4220
|
+
return resolveOptionValues(
|
|
4221
|
+
definition,
|
|
4222
|
+
options.optionValues?.[definition.key],
|
|
4223
|
+
options
|
|
4224
|
+
);
|
|
4225
|
+
}
|
|
4226
|
+
function finalizeWidget(widget, values) {
|
|
4227
|
+
if (Object.keys(values).length === 0) return widget;
|
|
4228
|
+
if (widget.status === "ok") {
|
|
4229
|
+
const data = applyClientOptions(
|
|
4230
|
+
widget.definition,
|
|
4231
|
+
widget.data,
|
|
4232
|
+
values
|
|
4233
|
+
);
|
|
4234
|
+
return { ...widget, data, options: values };
|
|
4235
|
+
}
|
|
4236
|
+
return { ...widget, options: values };
|
|
3740
4237
|
}
|
|
3741
4238
|
var LoadInterruption = class extends Error {
|
|
3742
4239
|
};
|
|
3743
|
-
async function
|
|
4240
|
+
async function resolveWidgetUnapplied(definition, provider, context, options = {}) {
|
|
4241
|
+
const values = optionValuesFor(definition, options);
|
|
3744
4242
|
const quotedKey = JSON.stringify(definition.key);
|
|
3745
4243
|
if (provider === void 0) {
|
|
3746
4244
|
return failedWidget(
|
|
@@ -3775,7 +4273,10 @@ async function resolveWidget(definition, provider, context, options = {}) {
|
|
|
3775
4273
|
payload = await Promise.race([
|
|
3776
4274
|
Promise.resolve().then(() => {
|
|
3777
4275
|
if (controller.signal.aborted) throw new LoadInterruption();
|
|
3778
|
-
return provider(context, definition, {
|
|
4276
|
+
return provider(context, definition, {
|
|
4277
|
+
signal: controller.signal,
|
|
4278
|
+
options: values
|
|
4279
|
+
});
|
|
3779
4280
|
}),
|
|
3780
4281
|
interrupted
|
|
3781
4282
|
]);
|
|
@@ -3799,7 +4300,7 @@ async function resolveWidget(definition, provider, context, options = {}) {
|
|
|
3799
4300
|
const storedAt = Date.now();
|
|
3800
4301
|
if (options.cache !== void 0) {
|
|
3801
4302
|
try {
|
|
3802
|
-
await options.cache.set(cacheKeyFor(definition, options), {
|
|
4303
|
+
await options.cache.set(cacheKeyFor(definition, options, values), {
|
|
3803
4304
|
payload: resolved.data,
|
|
3804
4305
|
storedAt
|
|
3805
4306
|
});
|
|
@@ -3816,7 +4317,17 @@ function widgetsFor(definitions, roles) {
|
|
|
3816
4317
|
|
|
3817
4318
|
// src/core/loader.ts
|
|
3818
4319
|
function createWidgetLoader(options) {
|
|
3819
|
-
const { definitions, providers, context, ...
|
|
4320
|
+
const { definitions, providers, context, ...baseOptions } = options;
|
|
4321
|
+
const chosen = new Map(
|
|
4322
|
+
Object.entries(baseOptions.optionValues ?? {}).flatMap(
|
|
4323
|
+
([key, values]) => values === void 0 ? [] : [[key, values]]
|
|
4324
|
+
)
|
|
4325
|
+
);
|
|
4326
|
+
const resolveOptions = baseOptions;
|
|
4327
|
+
function currentOptions() {
|
|
4328
|
+
return { ...resolveOptions, optionValues: Object.fromEntries(chosen) };
|
|
4329
|
+
}
|
|
4330
|
+
const unapplied = /* @__PURE__ */ new Map();
|
|
3820
4331
|
const visible = widgetsFor(definitions, context.roles);
|
|
3821
4332
|
let snapshot = loadingWidgets(visible);
|
|
3822
4333
|
const listeners = /* @__PURE__ */ new Set();
|
|
@@ -3857,19 +4368,19 @@ function createWidgetLoader(options) {
|
|
|
3857
4368
|
const controller = new AbortController();
|
|
3858
4369
|
inFlight.set(key, controller);
|
|
3859
4370
|
const isCurrent = () => !disposed && generations.get(key) === generation;
|
|
3860
|
-
|
|
4371
|
+
const runOptions = currentOptions();
|
|
4372
|
+
const values = optionValuesFor(definition, runOptions);
|
|
4373
|
+
if (readCache && runOptions.cache !== void 0) {
|
|
3861
4374
|
try {
|
|
3862
|
-
const entry = await
|
|
3863
|
-
cacheKeyFor(definition,
|
|
4375
|
+
const entry = await runOptions.cache.get(
|
|
4376
|
+
cacheKeyFor(definition, runOptions, values)
|
|
3864
4377
|
);
|
|
3865
4378
|
if (entry !== void 0 && isCurrent()) {
|
|
3866
|
-
const cached = resolvePayload(
|
|
3867
|
-
definition,
|
|
3868
|
-
entry.payload,
|
|
3869
|
-
resolveOptions
|
|
3870
|
-
);
|
|
4379
|
+
const cached = resolvePayload(definition, entry.payload, runOptions);
|
|
3871
4380
|
if (cached.status === "ok") {
|
|
3872
|
-
|
|
4381
|
+
const stale = { ...cached, updatedAt: entry.storedAt, stale: true };
|
|
4382
|
+
unapplied.set(key, stale);
|
|
4383
|
+
replace(key, finalizeWidget(stale, values));
|
|
3873
4384
|
}
|
|
3874
4385
|
}
|
|
3875
4386
|
} catch {
|
|
@@ -3877,13 +4388,15 @@ function createWidgetLoader(options) {
|
|
|
3877
4388
|
}
|
|
3878
4389
|
if (!isCurrent()) return;
|
|
3879
4390
|
const provider = Object.prototype.hasOwnProperty.call(providers, key) ? providers[key] : void 0;
|
|
3880
|
-
const result = await
|
|
3881
|
-
...
|
|
4391
|
+
const result = await resolveWidgetUnapplied(definition, provider, context, {
|
|
4392
|
+
...runOptions,
|
|
3882
4393
|
signal: controller.signal
|
|
3883
4394
|
});
|
|
3884
4395
|
if (!isCurrent()) return;
|
|
3885
4396
|
inFlight.delete(key);
|
|
3886
|
-
|
|
4397
|
+
if (result.status === "ok") unapplied.set(key, result);
|
|
4398
|
+
else unapplied.delete(key);
|
|
4399
|
+
replace(key, finalizeWidget(result, values));
|
|
3887
4400
|
}
|
|
3888
4401
|
function start(definition, readCache) {
|
|
3889
4402
|
started.add(definition.key);
|
|
@@ -3918,6 +4431,29 @@ function createWidgetLoader(options) {
|
|
|
3918
4431
|
start(definition, false);
|
|
3919
4432
|
}
|
|
3920
4433
|
}
|
|
4434
|
+
function setOptions(key, values) {
|
|
4435
|
+
if (disposed) return;
|
|
4436
|
+
const definition = definitionFor(key);
|
|
4437
|
+
if (definition === void 0) return;
|
|
4438
|
+
const before = optionValuesFor(definition, currentOptions());
|
|
4439
|
+
chosen.set(key, { ...values });
|
|
4440
|
+
const runOptions = currentOptions();
|
|
4441
|
+
const after = optionValuesFor(definition, runOptions);
|
|
4442
|
+
if (sameOptionValues(before, after) || !started.has(key)) return;
|
|
4443
|
+
const kept = unapplied.get(key);
|
|
4444
|
+
if (kept !== void 0 && optionsCacheSuffix(definition, before, runOptions) === optionsCacheSuffix(definition, after, runOptions)) {
|
|
4445
|
+
replace(key, finalizeWidget(kept, after));
|
|
4446
|
+
return;
|
|
4447
|
+
}
|
|
4448
|
+
const current = snapshot.find(
|
|
4449
|
+
(widget) => widget.definition.key === key
|
|
4450
|
+
);
|
|
4451
|
+
if (current?.status === "ok") replace(key, { ...current, stale: true });
|
|
4452
|
+
else if (current?.status === "error") {
|
|
4453
|
+
replace(key, { definition, status: "loading" });
|
|
4454
|
+
}
|
|
4455
|
+
start(definition, true);
|
|
4456
|
+
}
|
|
3921
4457
|
function dispose() {
|
|
3922
4458
|
disposed = true;
|
|
3923
4459
|
abortAll(new Error("The widget loader was disposed."));
|
|
@@ -3935,6 +4471,7 @@ function createWidgetLoader(options) {
|
|
|
3935
4471
|
load,
|
|
3936
4472
|
loadAll,
|
|
3937
4473
|
refresh,
|
|
4474
|
+
setOptions,
|
|
3938
4475
|
dispose
|
|
3939
4476
|
};
|
|
3940
4477
|
}
|
|
@@ -3949,7 +4486,8 @@ function useWidgets(definitions, providers, context, options = {}) {
|
|
|
3949
4486
|
timeoutMs,
|
|
3950
4487
|
signal,
|
|
3951
4488
|
cache,
|
|
3952
|
-
cacheKey
|
|
4489
|
+
cacheKey,
|
|
4490
|
+
optionValues
|
|
3953
4491
|
} = options;
|
|
3954
4492
|
const placeholders = react.useMemo(
|
|
3955
4493
|
() => loadingWidgets(widgetsFor(definitions, context.roles)),
|
|
@@ -3959,6 +4497,8 @@ function useWidgets(definitions, providers, context, options = {}) {
|
|
|
3959
4497
|
const [loader, setLoader] = react.useState(null);
|
|
3960
4498
|
const loaderRef = react.useRef(null);
|
|
3961
4499
|
const gridRef = react.useRef(null);
|
|
4500
|
+
const optionValuesRef = react.useRef(optionValues);
|
|
4501
|
+
optionValuesRef.current = optionValues;
|
|
3962
4502
|
react.useEffect(() => {
|
|
3963
4503
|
const created = createWidgetLoader({
|
|
3964
4504
|
definitions,
|
|
@@ -3968,7 +4508,8 @@ function useWidgets(definitions, providers, context, options = {}) {
|
|
|
3968
4508
|
...timeoutMs === void 0 ? {} : { timeoutMs },
|
|
3969
4509
|
...signal === void 0 ? {} : { signal },
|
|
3970
4510
|
...cache === void 0 ? {} : { cache },
|
|
3971
|
-
...cacheKey === void 0 ? {} : { cacheKey }
|
|
4511
|
+
...cacheKey === void 0 ? {} : { cacheKey },
|
|
4512
|
+
...optionValuesRef.current === void 0 ? {} : { optionValues: optionValuesRef.current }
|
|
3972
4513
|
});
|
|
3973
4514
|
loaderRef.current = created;
|
|
3974
4515
|
setWidgets(created.getSnapshot());
|
|
@@ -4040,10 +4581,23 @@ function useWidgets(definitions, providers, context, options = {}) {
|
|
|
4040
4581
|
}, refreshMs);
|
|
4041
4582
|
return () => clearInterval(timer);
|
|
4042
4583
|
}, [loader, refreshMs]);
|
|
4584
|
+
const optionSignature = JSON.stringify(optionValues ?? {});
|
|
4585
|
+
react.useEffect(() => {
|
|
4586
|
+
if (loader === null) return;
|
|
4587
|
+
for (const [key, chosen] of Object.entries(optionValuesRef.current ?? {})) {
|
|
4588
|
+
if (chosen !== void 0) loader.setOptions(key, chosen);
|
|
4589
|
+
}
|
|
4590
|
+
}, [loader, optionSignature]);
|
|
4591
|
+
const setOptions = react.useCallback(
|
|
4592
|
+
(key, chosen) => {
|
|
4593
|
+
loaderRef.current?.setOptions(key, chosen);
|
|
4594
|
+
},
|
|
4595
|
+
[]
|
|
4596
|
+
);
|
|
4043
4597
|
const refresh = react.useCallback((key) => {
|
|
4044
4598
|
loaderRef.current?.refresh(key);
|
|
4045
4599
|
}, []);
|
|
4046
|
-
return { widgets, refresh, gridRef };
|
|
4600
|
+
return { widgets, refresh, setOptions, gridRef };
|
|
4047
4601
|
}
|
|
4048
4602
|
|
|
4049
4603
|
exports.AlertListWidget = AlertListWidget;
|
|
@@ -4071,6 +4625,7 @@ exports.describeChart = describeChart;
|
|
|
4071
4625
|
exports.joinClassNames = joinClassNames;
|
|
4072
4626
|
exports.seriesColor = seriesColor;
|
|
4073
4627
|
exports.useDetailData = useDetailData;
|
|
4628
|
+
exports.useStoredLayout = useStoredLayout;
|
|
4074
4629
|
exports.useWidgetSettings = useWidgetSettings;
|
|
4075
4630
|
exports.useWidgets = useWidgets;
|
|
4076
4631
|
exports.widgetTitle = widgetTitle;
|