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