@richardmcquiston01/dashboard-widgets-toolkit 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +593 -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 +594 -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,20 @@ 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);
|
|
1577
|
+
const seed = initialSort === void 0 ? "" : `${initialSort.column}:${initialSort.direction}`;
|
|
1578
|
+
const [seededWith, setSeededWith] = react.useState(seed);
|
|
1579
|
+
if (seededWith !== seed) {
|
|
1580
|
+
setSeededWith(seed);
|
|
1581
|
+
setSort(initialSort);
|
|
1582
|
+
}
|
|
1309
1583
|
const localeTag = typeof locale === "string" ? locale : locale?.[0];
|
|
1310
1584
|
const query = {
|
|
1311
1585
|
page: 1,
|
|
@@ -1407,7 +1681,8 @@ function AlertListWidget({
|
|
|
1407
1681
|
}
|
|
1408
1682
|
function WidgetContent({
|
|
1409
1683
|
data,
|
|
1410
|
-
tableControls
|
|
1684
|
+
tableControls,
|
|
1685
|
+
initialSort
|
|
1411
1686
|
}) {
|
|
1412
1687
|
switch (data.kind) {
|
|
1413
1688
|
case "TEXT":
|
|
@@ -1417,7 +1692,14 @@ function WidgetContent({
|
|
|
1417
1692
|
case "GAUGE":
|
|
1418
1693
|
return /* @__PURE__ */ jsxRuntime.jsx(GaugeWidget, { data });
|
|
1419
1694
|
case "TABLE":
|
|
1420
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1695
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1696
|
+
TableWidget,
|
|
1697
|
+
{
|
|
1698
|
+
data,
|
|
1699
|
+
controls: tableControls,
|
|
1700
|
+
initialSort
|
|
1701
|
+
}
|
|
1702
|
+
);
|
|
1421
1703
|
case "BAR_LIST":
|
|
1422
1704
|
return /* @__PURE__ */ jsxRuntime.jsx(BarListWidget, { data });
|
|
1423
1705
|
case "ALERT_LIST":
|
|
@@ -1614,7 +1896,8 @@ function ResolvedWidgetCard({
|
|
|
1614
1896
|
WidgetContent,
|
|
1615
1897
|
{
|
|
1616
1898
|
data: widget.data,
|
|
1617
|
-
tableControls: resolveTableControls(definition)
|
|
1899
|
+
tableControls: resolveTableControls(definition),
|
|
1900
|
+
initialSort: sortFromOptions(definition, widget.options)
|
|
1618
1901
|
}
|
|
1619
1902
|
) : null
|
|
1620
1903
|
}
|
|
@@ -2376,7 +2659,8 @@ function WidgetDetailDialog({
|
|
|
2376
2659
|
}
|
|
2377
2660
|
);
|
|
2378
2661
|
}
|
|
2379
|
-
|
|
2662
|
+
var NO_OPTION_VALUES = Object.freeze({});
|
|
2663
|
+
function useDetailData(definition, load, enabled, fallback, optionValues = NO_OPTION_VALUES) {
|
|
2380
2664
|
const [state, setState] = react.useState({ status: "loading" });
|
|
2381
2665
|
const [attempt, setAttempt] = react.useState(0);
|
|
2382
2666
|
react.useEffect(() => {
|
|
@@ -2386,6 +2670,7 @@ function useDetailData(definition, load, enabled, fallback) {
|
|
|
2386
2670
|
Promise.resolve().then(
|
|
2387
2671
|
() => load === void 0 ? void 0 : load(definition, {
|
|
2388
2672
|
signal: controller.signal,
|
|
2673
|
+
options: optionValues,
|
|
2389
2674
|
query: defaultDetailQuery()
|
|
2390
2675
|
})
|
|
2391
2676
|
).then((raw) => {
|
|
@@ -2413,7 +2698,7 @@ function useDetailData(definition, load, enabled, fallback) {
|
|
|
2413
2698
|
});
|
|
2414
2699
|
});
|
|
2415
2700
|
return () => controller.abort();
|
|
2416
|
-
}, [enabled, load, fallback, definition, attempt]);
|
|
2701
|
+
}, [enabled, load, fallback, definition, optionValues, attempt]);
|
|
2417
2702
|
return { state, reload: () => setAttempt((count) => count + 1) };
|
|
2418
2703
|
}
|
|
2419
2704
|
var NEW_PAGE_VALUE = "__new-page__";
|
|
@@ -2529,7 +2814,14 @@ function DetailHost({
|
|
|
2529
2814
|
const { definition } = widget;
|
|
2530
2815
|
const options = resolveDetailOptions(definition);
|
|
2531
2816
|
const pageSize = options?.pageSize ?? 25;
|
|
2532
|
-
const
|
|
2817
|
+
const optionSort = sortFromOptions(
|
|
2818
|
+
definition,
|
|
2819
|
+
widget.options
|
|
2820
|
+
);
|
|
2821
|
+
const [query, setQuery] = react.useState({
|
|
2822
|
+
...defaultDetailQuery(pageSize),
|
|
2823
|
+
...optionSort === void 0 ? {} : { sort: optionSort }
|
|
2824
|
+
});
|
|
2533
2825
|
const derived = react.useMemo(
|
|
2534
2826
|
() => widget.status === "ok" ? deriveDetailData(widget.data, locale) : void 0,
|
|
2535
2827
|
[widget, locale]
|
|
@@ -2538,7 +2830,8 @@ function DetailHost({
|
|
|
2538
2830
|
definition,
|
|
2539
2831
|
loadDetail,
|
|
2540
2832
|
true,
|
|
2541
|
-
derived
|
|
2833
|
+
derived,
|
|
2834
|
+
widget.options
|
|
2542
2835
|
);
|
|
2543
2836
|
const title = options?.title ?? definition.title;
|
|
2544
2837
|
return /* @__PURE__ */ jsxRuntime.jsx(WidgetDetailDialog, { title, open: true, onClose, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -3392,6 +3685,196 @@ function Dashboard({
|
|
|
3392
3685
|
}
|
|
3393
3686
|
);
|
|
3394
3687
|
}
|
|
3688
|
+
function useStoredLayout(options) {
|
|
3689
|
+
const {
|
|
3690
|
+
persistence,
|
|
3691
|
+
scope,
|
|
3692
|
+
definitions,
|
|
3693
|
+
defaultLayout,
|
|
3694
|
+
saveDelayMs = 500,
|
|
3695
|
+
initialLayout,
|
|
3696
|
+
normalizeOptions
|
|
3697
|
+
} = options;
|
|
3698
|
+
const scopeKey = persistence.keyFor(scope);
|
|
3699
|
+
const [layout, setLayoutState] = react.useState(
|
|
3700
|
+
initialLayout ?? defaultLayout
|
|
3701
|
+
);
|
|
3702
|
+
const [backup, setBackupState] = react.useState(null);
|
|
3703
|
+
const [status, setStatus] = react.useState("loading");
|
|
3704
|
+
const [error, setError] = react.useState(null);
|
|
3705
|
+
const [conflict, setConflict] = react.useState(null);
|
|
3706
|
+
const [remoteLayout, setRemoteLayout] = react.useState(
|
|
3707
|
+
null
|
|
3708
|
+
);
|
|
3709
|
+
const latest = react.useRef(layout);
|
|
3710
|
+
const scopeRef = react.useRef(scope);
|
|
3711
|
+
scopeRef.current = scope;
|
|
3712
|
+
const normalizeRef = react.useRef(normalizeOptions);
|
|
3713
|
+
normalizeRef.current = normalizeOptions;
|
|
3714
|
+
const timer = react.useRef(void 0);
|
|
3715
|
+
const dirty = react.useRef(false);
|
|
3716
|
+
const apply = react.useCallback((next) => {
|
|
3717
|
+
latest.current = next;
|
|
3718
|
+
setLayoutState(next);
|
|
3719
|
+
}, []);
|
|
3720
|
+
const finishSave = react.useCallback(
|
|
3721
|
+
(result, mine) => {
|
|
3722
|
+
if (!result.ok) {
|
|
3723
|
+
setError(result.error);
|
|
3724
|
+
setStatus("error");
|
|
3725
|
+
return;
|
|
3726
|
+
}
|
|
3727
|
+
const saved = result.value;
|
|
3728
|
+
if (saved.outcome === "saved") {
|
|
3729
|
+
dirty.current = latest.current !== mine;
|
|
3730
|
+
setError(null);
|
|
3731
|
+
setStatus(dirty.current ? "saving" : "ready");
|
|
3732
|
+
return;
|
|
3733
|
+
}
|
|
3734
|
+
if (saved.outcome === "kept-theirs") {
|
|
3735
|
+
dirty.current = false;
|
|
3736
|
+
apply(saved.layout);
|
|
3737
|
+
setError(null);
|
|
3738
|
+
setStatus("ready");
|
|
3739
|
+
return;
|
|
3740
|
+
}
|
|
3741
|
+
setConflict({ mine, theirs: saved.layout });
|
|
3742
|
+
setStatus("conflict");
|
|
3743
|
+
},
|
|
3744
|
+
[apply]
|
|
3745
|
+
);
|
|
3746
|
+
const flush = react.useCallback(() => {
|
|
3747
|
+
if (timer.current !== void 0) {
|
|
3748
|
+
clearTimeout(timer.current);
|
|
3749
|
+
timer.current = void 0;
|
|
3750
|
+
}
|
|
3751
|
+
if (!dirty.current) return;
|
|
3752
|
+
const mine = latest.current;
|
|
3753
|
+
setStatus("saving");
|
|
3754
|
+
void persistence.save(scopeRef.current, mine).then((result) => finishSave(result, mine));
|
|
3755
|
+
}, [persistence, finishSave]);
|
|
3756
|
+
react.useEffect(() => {
|
|
3757
|
+
let current = true;
|
|
3758
|
+
setStatus("loading");
|
|
3759
|
+
setConflict(null);
|
|
3760
|
+
setRemoteLayout(null);
|
|
3761
|
+
void persistence.load(scopeRef.current, definitions, normalizeRef.current).then((result) => {
|
|
3762
|
+
if (!current) return;
|
|
3763
|
+
if (!result.ok) {
|
|
3764
|
+
setError(result.error);
|
|
3765
|
+
setStatus("error");
|
|
3766
|
+
return;
|
|
3767
|
+
}
|
|
3768
|
+
setError(null);
|
|
3769
|
+
if (!dirty.current && result.value.layout !== null) {
|
|
3770
|
+
apply(result.value.layout);
|
|
3771
|
+
}
|
|
3772
|
+
setStatus(dirty.current ? "saving" : "ready");
|
|
3773
|
+
});
|
|
3774
|
+
void persistence.loadBackup(scopeRef.current).then((result) => {
|
|
3775
|
+
if (current && result.ok) setBackupState(result.value);
|
|
3776
|
+
});
|
|
3777
|
+
return () => {
|
|
3778
|
+
current = false;
|
|
3779
|
+
};
|
|
3780
|
+
}, [persistence, scopeKey, definitions, apply]);
|
|
3781
|
+
react.useEffect(() => {
|
|
3782
|
+
return persistence.watch(
|
|
3783
|
+
scopeRef.current,
|
|
3784
|
+
definitions,
|
|
3785
|
+
(loaded) => {
|
|
3786
|
+
if (loaded.layout === null) return;
|
|
3787
|
+
if (dirty.current) {
|
|
3788
|
+
setRemoteLayout(loaded.layout);
|
|
3789
|
+
return;
|
|
3790
|
+
}
|
|
3791
|
+
apply(loaded.layout);
|
|
3792
|
+
},
|
|
3793
|
+
normalizeRef.current
|
|
3794
|
+
);
|
|
3795
|
+
}, [persistence, scopeKey, definitions, apply]);
|
|
3796
|
+
react.useEffect(() => {
|
|
3797
|
+
const onHide = () => {
|
|
3798
|
+
if (typeof document === "undefined" || document.visibilityState !== "hidden") {
|
|
3799
|
+
return;
|
|
3800
|
+
}
|
|
3801
|
+
flush();
|
|
3802
|
+
};
|
|
3803
|
+
if (typeof window !== "undefined") {
|
|
3804
|
+
window.addEventListener("pagehide", flush);
|
|
3805
|
+
document.addEventListener("visibilitychange", onHide);
|
|
3806
|
+
}
|
|
3807
|
+
return () => {
|
|
3808
|
+
if (typeof window !== "undefined") {
|
|
3809
|
+
window.removeEventListener("pagehide", flush);
|
|
3810
|
+
document.removeEventListener("visibilitychange", onHide);
|
|
3811
|
+
}
|
|
3812
|
+
flush();
|
|
3813
|
+
};
|
|
3814
|
+
}, [flush]);
|
|
3815
|
+
const setLayout = react.useCallback(
|
|
3816
|
+
(next) => {
|
|
3817
|
+
apply(next);
|
|
3818
|
+
dirty.current = true;
|
|
3819
|
+
if (timer.current !== void 0) clearTimeout(timer.current);
|
|
3820
|
+
timer.current = setTimeout(flush, Math.max(0, saveDelayMs));
|
|
3821
|
+
},
|
|
3822
|
+
[apply, flush, saveDelayMs]
|
|
3823
|
+
);
|
|
3824
|
+
const setBackup = react.useCallback(
|
|
3825
|
+
(next) => {
|
|
3826
|
+
setBackupState(next);
|
|
3827
|
+
if (next === null) return;
|
|
3828
|
+
void persistence.saveBackup(scopeRef.current, next).then((result) => {
|
|
3829
|
+
if (!result.ok) setError(result.error);
|
|
3830
|
+
});
|
|
3831
|
+
},
|
|
3832
|
+
[persistence]
|
|
3833
|
+
);
|
|
3834
|
+
const resolveConflict = react.useCallback(
|
|
3835
|
+
(choice) => {
|
|
3836
|
+
const pending = conflict;
|
|
3837
|
+
if (pending === null) return;
|
|
3838
|
+
setConflict(null);
|
|
3839
|
+
if (choice === "theirs") {
|
|
3840
|
+
dirty.current = false;
|
|
3841
|
+
apply(pending.theirs);
|
|
3842
|
+
setStatus("ready");
|
|
3843
|
+
return;
|
|
3844
|
+
}
|
|
3845
|
+
setStatus("saving");
|
|
3846
|
+
void persistence.save(scopeRef.current, pending.mine, { force: true }).then((result) => finishSave(result, pending.mine));
|
|
3847
|
+
},
|
|
3848
|
+
[conflict, persistence, apply, finishSave]
|
|
3849
|
+
);
|
|
3850
|
+
const resolveRemote = react.useCallback(
|
|
3851
|
+
(choice) => {
|
|
3852
|
+
const remote = remoteLayout;
|
|
3853
|
+
setRemoteLayout(null);
|
|
3854
|
+
if (choice === "use" && remote !== null) {
|
|
3855
|
+
if (timer.current !== void 0) {
|
|
3856
|
+
clearTimeout(timer.current);
|
|
3857
|
+
timer.current = void 0;
|
|
3858
|
+
}
|
|
3859
|
+
dirty.current = false;
|
|
3860
|
+
apply(remote);
|
|
3861
|
+
}
|
|
3862
|
+
},
|
|
3863
|
+
[remoteLayout, apply]
|
|
3864
|
+
);
|
|
3865
|
+
return {
|
|
3866
|
+
layout,
|
|
3867
|
+
setLayout,
|
|
3868
|
+
backup,
|
|
3869
|
+
setBackup,
|
|
3870
|
+
status,
|
|
3871
|
+
error,
|
|
3872
|
+
conflict,
|
|
3873
|
+
resolveConflict,
|
|
3874
|
+
remoteLayout,
|
|
3875
|
+
resolveRemote
|
|
3876
|
+
};
|
|
3877
|
+
}
|
|
3395
3878
|
|
|
3396
3879
|
// src/core/payload.ts
|
|
3397
3880
|
var WIDGET_KINDS = [
|
|
@@ -3735,12 +4218,33 @@ function failedWidget(definition, error) {
|
|
|
3735
4218
|
function loadingWidgets(definitions) {
|
|
3736
4219
|
return definitions.map((definition) => ({ definition, status: "loading" }));
|
|
3737
4220
|
}
|
|
3738
|
-
function cacheKeyFor(definition, options) {
|
|
3739
|
-
|
|
4221
|
+
function cacheKeyFor(definition, options, values) {
|
|
4222
|
+
const base = options.cacheKey === void 0 ? definition.key : options.cacheKey(definition);
|
|
4223
|
+
return values === void 0 ? base : `${base}${optionsCacheSuffix(definition, values, options)}`;
|
|
4224
|
+
}
|
|
4225
|
+
function optionValuesFor(definition, options) {
|
|
4226
|
+
return resolveOptionValues(
|
|
4227
|
+
definition,
|
|
4228
|
+
options.optionValues?.[definition.key],
|
|
4229
|
+
options
|
|
4230
|
+
);
|
|
4231
|
+
}
|
|
4232
|
+
function finalizeWidget(widget, values) {
|
|
4233
|
+
if (Object.keys(values).length === 0) return widget;
|
|
4234
|
+
if (widget.status === "ok") {
|
|
4235
|
+
const data = applyClientOptions(
|
|
4236
|
+
widget.definition,
|
|
4237
|
+
widget.data,
|
|
4238
|
+
values
|
|
4239
|
+
);
|
|
4240
|
+
return { ...widget, data, options: values };
|
|
4241
|
+
}
|
|
4242
|
+
return { ...widget, options: values };
|
|
3740
4243
|
}
|
|
3741
4244
|
var LoadInterruption = class extends Error {
|
|
3742
4245
|
};
|
|
3743
|
-
async function
|
|
4246
|
+
async function resolveWidgetUnapplied(definition, provider, context, options = {}) {
|
|
4247
|
+
const values = optionValuesFor(definition, options);
|
|
3744
4248
|
const quotedKey = JSON.stringify(definition.key);
|
|
3745
4249
|
if (provider === void 0) {
|
|
3746
4250
|
return failedWidget(
|
|
@@ -3775,7 +4279,10 @@ async function resolveWidget(definition, provider, context, options = {}) {
|
|
|
3775
4279
|
payload = await Promise.race([
|
|
3776
4280
|
Promise.resolve().then(() => {
|
|
3777
4281
|
if (controller.signal.aborted) throw new LoadInterruption();
|
|
3778
|
-
return provider(context, definition, {
|
|
4282
|
+
return provider(context, definition, {
|
|
4283
|
+
signal: controller.signal,
|
|
4284
|
+
options: values
|
|
4285
|
+
});
|
|
3779
4286
|
}),
|
|
3780
4287
|
interrupted
|
|
3781
4288
|
]);
|
|
@@ -3799,7 +4306,7 @@ async function resolveWidget(definition, provider, context, options = {}) {
|
|
|
3799
4306
|
const storedAt = Date.now();
|
|
3800
4307
|
if (options.cache !== void 0) {
|
|
3801
4308
|
try {
|
|
3802
|
-
await options.cache.set(cacheKeyFor(definition, options), {
|
|
4309
|
+
await options.cache.set(cacheKeyFor(definition, options, values), {
|
|
3803
4310
|
payload: resolved.data,
|
|
3804
4311
|
storedAt
|
|
3805
4312
|
});
|
|
@@ -3816,7 +4323,17 @@ function widgetsFor(definitions, roles) {
|
|
|
3816
4323
|
|
|
3817
4324
|
// src/core/loader.ts
|
|
3818
4325
|
function createWidgetLoader(options) {
|
|
3819
|
-
const { definitions, providers, context, ...
|
|
4326
|
+
const { definitions, providers, context, ...baseOptions } = options;
|
|
4327
|
+
const chosen = new Map(
|
|
4328
|
+
Object.entries(baseOptions.optionValues ?? {}).flatMap(
|
|
4329
|
+
([key, values]) => values === void 0 ? [] : [[key, values]]
|
|
4330
|
+
)
|
|
4331
|
+
);
|
|
4332
|
+
const resolveOptions = baseOptions;
|
|
4333
|
+
function currentOptions() {
|
|
4334
|
+
return { ...resolveOptions, optionValues: Object.fromEntries(chosen) };
|
|
4335
|
+
}
|
|
4336
|
+
const unapplied = /* @__PURE__ */ new Map();
|
|
3820
4337
|
const visible = widgetsFor(definitions, context.roles);
|
|
3821
4338
|
let snapshot = loadingWidgets(visible);
|
|
3822
4339
|
const listeners = /* @__PURE__ */ new Set();
|
|
@@ -3857,19 +4374,19 @@ function createWidgetLoader(options) {
|
|
|
3857
4374
|
const controller = new AbortController();
|
|
3858
4375
|
inFlight.set(key, controller);
|
|
3859
4376
|
const isCurrent = () => !disposed && generations.get(key) === generation;
|
|
3860
|
-
|
|
4377
|
+
const runOptions = currentOptions();
|
|
4378
|
+
const values = optionValuesFor(definition, runOptions);
|
|
4379
|
+
if (readCache && runOptions.cache !== void 0) {
|
|
3861
4380
|
try {
|
|
3862
|
-
const entry = await
|
|
3863
|
-
cacheKeyFor(definition,
|
|
4381
|
+
const entry = await runOptions.cache.get(
|
|
4382
|
+
cacheKeyFor(definition, runOptions, values)
|
|
3864
4383
|
);
|
|
3865
4384
|
if (entry !== void 0 && isCurrent()) {
|
|
3866
|
-
const cached = resolvePayload(
|
|
3867
|
-
definition,
|
|
3868
|
-
entry.payload,
|
|
3869
|
-
resolveOptions
|
|
3870
|
-
);
|
|
4385
|
+
const cached = resolvePayload(definition, entry.payload, runOptions);
|
|
3871
4386
|
if (cached.status === "ok") {
|
|
3872
|
-
|
|
4387
|
+
const stale = { ...cached, updatedAt: entry.storedAt, stale: true };
|
|
4388
|
+
unapplied.set(key, stale);
|
|
4389
|
+
replace(key, finalizeWidget(stale, values));
|
|
3873
4390
|
}
|
|
3874
4391
|
}
|
|
3875
4392
|
} catch {
|
|
@@ -3877,13 +4394,15 @@ function createWidgetLoader(options) {
|
|
|
3877
4394
|
}
|
|
3878
4395
|
if (!isCurrent()) return;
|
|
3879
4396
|
const provider = Object.prototype.hasOwnProperty.call(providers, key) ? providers[key] : void 0;
|
|
3880
|
-
const result = await
|
|
3881
|
-
...
|
|
4397
|
+
const result = await resolveWidgetUnapplied(definition, provider, context, {
|
|
4398
|
+
...runOptions,
|
|
3882
4399
|
signal: controller.signal
|
|
3883
4400
|
});
|
|
3884
4401
|
if (!isCurrent()) return;
|
|
3885
4402
|
inFlight.delete(key);
|
|
3886
|
-
|
|
4403
|
+
if (result.status === "ok") unapplied.set(key, result);
|
|
4404
|
+
else unapplied.delete(key);
|
|
4405
|
+
replace(key, finalizeWidget(result, values));
|
|
3887
4406
|
}
|
|
3888
4407
|
function start(definition, readCache) {
|
|
3889
4408
|
started.add(definition.key);
|
|
@@ -3918,6 +4437,29 @@ function createWidgetLoader(options) {
|
|
|
3918
4437
|
start(definition, false);
|
|
3919
4438
|
}
|
|
3920
4439
|
}
|
|
4440
|
+
function setOptions(key, values) {
|
|
4441
|
+
if (disposed) return;
|
|
4442
|
+
const definition = definitionFor(key);
|
|
4443
|
+
if (definition === void 0) return;
|
|
4444
|
+
const before = optionValuesFor(definition, currentOptions());
|
|
4445
|
+
chosen.set(key, { ...values });
|
|
4446
|
+
const runOptions = currentOptions();
|
|
4447
|
+
const after = optionValuesFor(definition, runOptions);
|
|
4448
|
+
if (sameOptionValues(before, after) || !started.has(key)) return;
|
|
4449
|
+
const kept = unapplied.get(key);
|
|
4450
|
+
if (kept !== void 0 && optionsCacheSuffix(definition, before, runOptions) === optionsCacheSuffix(definition, after, runOptions)) {
|
|
4451
|
+
replace(key, finalizeWidget(kept, after));
|
|
4452
|
+
return;
|
|
4453
|
+
}
|
|
4454
|
+
const current = snapshot.find(
|
|
4455
|
+
(widget) => widget.definition.key === key
|
|
4456
|
+
);
|
|
4457
|
+
if (current?.status === "ok") replace(key, { ...current, stale: true });
|
|
4458
|
+
else if (current?.status === "error") {
|
|
4459
|
+
replace(key, { definition, status: "loading" });
|
|
4460
|
+
}
|
|
4461
|
+
start(definition, true);
|
|
4462
|
+
}
|
|
3921
4463
|
function dispose() {
|
|
3922
4464
|
disposed = true;
|
|
3923
4465
|
abortAll(new Error("The widget loader was disposed."));
|
|
@@ -3935,6 +4477,7 @@ function createWidgetLoader(options) {
|
|
|
3935
4477
|
load,
|
|
3936
4478
|
loadAll,
|
|
3937
4479
|
refresh,
|
|
4480
|
+
setOptions,
|
|
3938
4481
|
dispose
|
|
3939
4482
|
};
|
|
3940
4483
|
}
|
|
@@ -3949,7 +4492,8 @@ function useWidgets(definitions, providers, context, options = {}) {
|
|
|
3949
4492
|
timeoutMs,
|
|
3950
4493
|
signal,
|
|
3951
4494
|
cache,
|
|
3952
|
-
cacheKey
|
|
4495
|
+
cacheKey,
|
|
4496
|
+
optionValues
|
|
3953
4497
|
} = options;
|
|
3954
4498
|
const placeholders = react.useMemo(
|
|
3955
4499
|
() => loadingWidgets(widgetsFor(definitions, context.roles)),
|
|
@@ -3959,6 +4503,8 @@ function useWidgets(definitions, providers, context, options = {}) {
|
|
|
3959
4503
|
const [loader, setLoader] = react.useState(null);
|
|
3960
4504
|
const loaderRef = react.useRef(null);
|
|
3961
4505
|
const gridRef = react.useRef(null);
|
|
4506
|
+
const optionValuesRef = react.useRef(optionValues);
|
|
4507
|
+
optionValuesRef.current = optionValues;
|
|
3962
4508
|
react.useEffect(() => {
|
|
3963
4509
|
const created = createWidgetLoader({
|
|
3964
4510
|
definitions,
|
|
@@ -3968,7 +4514,8 @@ function useWidgets(definitions, providers, context, options = {}) {
|
|
|
3968
4514
|
...timeoutMs === void 0 ? {} : { timeoutMs },
|
|
3969
4515
|
...signal === void 0 ? {} : { signal },
|
|
3970
4516
|
...cache === void 0 ? {} : { cache },
|
|
3971
|
-
...cacheKey === void 0 ? {} : { cacheKey }
|
|
4517
|
+
...cacheKey === void 0 ? {} : { cacheKey },
|
|
4518
|
+
...optionValuesRef.current === void 0 ? {} : { optionValues: optionValuesRef.current }
|
|
3972
4519
|
});
|
|
3973
4520
|
loaderRef.current = created;
|
|
3974
4521
|
setWidgets(created.getSnapshot());
|
|
@@ -4040,10 +4587,23 @@ function useWidgets(definitions, providers, context, options = {}) {
|
|
|
4040
4587
|
}, refreshMs);
|
|
4041
4588
|
return () => clearInterval(timer);
|
|
4042
4589
|
}, [loader, refreshMs]);
|
|
4590
|
+
const optionSignature = JSON.stringify(optionValues ?? {});
|
|
4591
|
+
react.useEffect(() => {
|
|
4592
|
+
if (loader === null) return;
|
|
4593
|
+
for (const [key, chosen] of Object.entries(optionValuesRef.current ?? {})) {
|
|
4594
|
+
if (chosen !== void 0) loader.setOptions(key, chosen);
|
|
4595
|
+
}
|
|
4596
|
+
}, [loader, optionSignature]);
|
|
4597
|
+
const setOptions = react.useCallback(
|
|
4598
|
+
(key, chosen) => {
|
|
4599
|
+
loaderRef.current?.setOptions(key, chosen);
|
|
4600
|
+
},
|
|
4601
|
+
[]
|
|
4602
|
+
);
|
|
4043
4603
|
const refresh = react.useCallback((key) => {
|
|
4044
4604
|
loaderRef.current?.refresh(key);
|
|
4045
4605
|
}, []);
|
|
4046
|
-
return { widgets, refresh, gridRef };
|
|
4606
|
+
return { widgets, refresh, setOptions, gridRef };
|
|
4047
4607
|
}
|
|
4048
4608
|
|
|
4049
4609
|
exports.AlertListWidget = AlertListWidget;
|
|
@@ -4071,6 +4631,7 @@ exports.describeChart = describeChart;
|
|
|
4071
4631
|
exports.joinClassNames = joinClassNames;
|
|
4072
4632
|
exports.seriesColor = seriesColor;
|
|
4073
4633
|
exports.useDetailData = useDetailData;
|
|
4634
|
+
exports.useStoredLayout = useStoredLayout;
|
|
4074
4635
|
exports.useWidgetSettings = useWidgetSettings;
|
|
4075
4636
|
exports.useWidgets = useWidgets;
|
|
4076
4637
|
exports.widgetTitle = widgetTitle;
|