@solidxai/core-ui 0.1.9-beta.14 → 0.1.9-beta.16

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.
Files changed (89) hide show
  1. package/dist/components/core/dashboard/mappers/echartsOptionMapper.d.ts +4 -0
  2. package/dist/components/core/dashboard/mappers/echartsOptionMapper.d.ts.map +1 -0
  3. package/dist/components/core/dashboard/mappers/echartsOptionMapper.js +76 -0
  4. package/dist/components/core/dashboard/mappers/echartsOptionMapper.js.map +1 -0
  5. package/dist/components/core/dashboard/mappers/echartsOptionMapper.ts +78 -0
  6. package/dist/components/core/dashboard/widgets/DefaultDashboardBarChartWidget.d.ts +3 -0
  7. package/dist/components/core/dashboard/widgets/DefaultDashboardBarChartWidget.d.ts.map +1 -0
  8. package/dist/components/core/dashboard/widgets/DefaultDashboardBarChartWidget.js +17 -0
  9. package/dist/components/core/dashboard/widgets/DefaultDashboardBarChartWidget.js.map +1 -0
  10. package/dist/components/core/dashboard/widgets/DefaultDashboardBarChartWidget.tsx +6 -0
  11. package/dist/components/core/dashboard/widgets/DefaultDashboardChartWidget.d.ts +3 -0
  12. package/dist/components/core/dashboard/widgets/DefaultDashboardChartWidget.d.ts.map +1 -0
  13. package/dist/components/core/dashboard/widgets/DefaultDashboardChartWidget.js +41 -0
  14. package/dist/components/core/dashboard/widgets/DefaultDashboardChartWidget.js.map +1 -0
  15. package/dist/components/core/dashboard/widgets/DefaultDashboardChartWidget.tsx +56 -0
  16. package/dist/components/core/dashboard/widgets/DefaultDashboardKpiWidget.d.ts +3 -0
  17. package/dist/components/core/dashboard/widgets/DefaultDashboardKpiWidget.d.ts.map +1 -0
  18. package/dist/components/core/dashboard/widgets/DefaultDashboardKpiWidget.js +9 -0
  19. package/dist/components/core/dashboard/widgets/DefaultDashboardKpiWidget.js.map +1 -0
  20. package/dist/components/core/dashboard/widgets/DefaultDashboardKpiWidget.tsx +7 -0
  21. package/dist/components/core/dashboard/widgets/DefaultDashboardLineChartWidget.d.ts +3 -0
  22. package/dist/components/core/dashboard/widgets/DefaultDashboardLineChartWidget.d.ts.map +1 -0
  23. package/dist/components/core/dashboard/widgets/DefaultDashboardLineChartWidget.js +17 -0
  24. package/dist/components/core/dashboard/widgets/DefaultDashboardLineChartWidget.js.map +1 -0
  25. package/dist/components/core/dashboard/widgets/DefaultDashboardLineChartWidget.tsx +6 -0
  26. package/dist/components/core/dashboard/widgets/DefaultDashboardPieChartWidget.d.ts +3 -0
  27. package/dist/components/core/dashboard/widgets/DefaultDashboardPieChartWidget.d.ts.map +1 -0
  28. package/dist/components/core/dashboard/widgets/DefaultDashboardPieChartWidget.js +17 -0
  29. package/dist/components/core/dashboard/widgets/DefaultDashboardPieChartWidget.js.map +1 -0
  30. package/dist/components/core/dashboard/widgets/DefaultDashboardPieChartWidget.tsx +6 -0
  31. package/dist/components/core/dashboard/widgets/DefaultDashboardTableWidget.d.ts +3 -0
  32. package/dist/components/core/dashboard/widgets/DefaultDashboardTableWidget.d.ts.map +1 -0
  33. package/dist/components/core/dashboard/widgets/DefaultDashboardTableWidget.js +12 -0
  34. package/dist/components/core/dashboard/widgets/DefaultDashboardTableWidget.js.map +1 -0
  35. package/dist/components/core/dashboard/widgets/DefaultDashboardTableWidget.tsx +39 -0
  36. package/dist/components/core/dashboard/widgets/DefaultDashboardUnknownWidget.d.ts +3 -0
  37. package/dist/components/core/dashboard/widgets/DefaultDashboardUnknownWidget.d.ts.map +1 -0
  38. package/dist/components/core/dashboard/widgets/DefaultDashboardUnknownWidget.js +7 -0
  39. package/dist/components/core/dashboard/widgets/DefaultDashboardUnknownWidget.js.map +1 -0
  40. package/dist/components/core/dashboard/widgets/DefaultDashboardUnknownWidget.tsx +9 -0
  41. package/dist/components/core/dashboard/widgets/QueueSlaHeatmapWidget.d.ts +3 -0
  42. package/dist/components/core/dashboard/widgets/QueueSlaHeatmapWidget.d.ts.map +1 -0
  43. package/dist/components/core/dashboard/widgets/QueueSlaHeatmapWidget.js +159 -0
  44. package/dist/components/core/dashboard/widgets/QueueSlaHeatmapWidget.js.map +1 -0
  45. package/dist/components/core/dashboard/widgets/QueueSlaHeatmapWidget.tsx +205 -0
  46. package/dist/helpers/registry.d.ts.map +1 -1
  47. package/dist/helpers/registry.js +15 -10
  48. package/dist/helpers/registry.js.map +1 -1
  49. package/dist/helpers/registry.ts +16 -12
  50. package/dist/index.d.ts +4 -7
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +3 -7
  53. package/dist/index.js.map +1 -1
  54. package/dist/index.ts +12 -13
  55. package/dist/redux/api/dashboardRuntimeApi.d.ts +34 -0
  56. package/dist/redux/api/dashboardRuntimeApi.d.ts.map +1 -0
  57. package/dist/redux/api/dashboardRuntimeApi.js +79 -0
  58. package/dist/redux/api/dashboardRuntimeApi.js.map +1 -0
  59. package/dist/redux/api/dashboardRuntimeApi.ts +75 -0
  60. package/dist/redux/store/defaultStoreConfig.d.ts +34 -9
  61. package/dist/redux/store/defaultStoreConfig.d.ts.map +1 -1
  62. package/dist/redux/store/defaultStoreConfig.js +2 -6
  63. package/dist/redux/store/defaultStoreConfig.js.map +1 -1
  64. package/dist/redux/store/defaultStoreConfig.ts +2 -6
  65. package/dist/routes/pages/admin/core/DashboardPage.d.ts +1 -0
  66. package/dist/routes/pages/admin/core/DashboardPage.d.ts.map +1 -1
  67. package/dist/routes/pages/admin/core/DashboardPage.js +780 -5
  68. package/dist/routes/pages/admin/core/DashboardPage.js.map +1 -1
  69. package/dist/routes/pages/admin/core/DashboardPage.module.css +186 -0
  70. package/dist/routes/pages/admin/core/DashboardPage.tsx +898 -7
  71. package/dist/routes/solidRoutes.d.ts.map +1 -1
  72. package/dist/routes/solidRoutes.js +2 -2
  73. package/dist/routes/solidRoutes.js.map +1 -1
  74. package/dist/routes/solidRoutes.tsx +2 -2
  75. package/dist/routes/types.d.ts +1 -1
  76. package/dist/routes/types.d.ts.map +1 -1
  77. package/dist/routes/types.js.map +1 -1
  78. package/dist/routes/types.ts +1 -1
  79. package/dist/types/dashboard.d.ts +16 -0
  80. package/dist/types/dashboard.d.ts.map +1 -0
  81. package/dist/types/dashboard.js +2 -0
  82. package/dist/types/dashboard.js.map +1 -0
  83. package/dist/types/dashboard.ts +23 -0
  84. package/dist/types/extension-registry.d.ts +1 -0
  85. package/dist/types/extension-registry.d.ts.map +1 -1
  86. package/dist/types/extension-registry.js +1 -0
  87. package/dist/types/extension-registry.js.map +1 -1
  88. package/dist/types/extension-registry.ts +1 -0
  89. package/package.json +3 -1
@@ -1,14 +1,905 @@
1
- import SolidDashboard from '../../../../components/core/dashboard/SolidDashboard'
2
- import { useParams, useSearchParams } from "react-router-dom";
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
+ import { useParams } from "react-router-dom";
3
+ import { AlertTriangle, Filter, RefreshCw, Save } from "lucide-react";
4
+ import { GridStack, type GridStackNode } from "gridstack";
5
+ import "gridstack/dist/gridstack.min.css";
6
+ import { getExtensionComponent } from "../../../../helpers/registry";
7
+ import {
8
+ SolidAutocomplete,
9
+ SolidButton,
10
+ SolidDatePicker,
11
+ SolidDialog,
12
+ SolidDialogBody,
13
+ SolidDialogFooter,
14
+ SolidSelect,
15
+ SolidSpinner,
16
+ } from "../../../../components/shad-cn-ui";
17
+ import { resolveChartType } from "../../../../components/core/dashboard/mappers/echartsOptionMapper";
18
+ import {
19
+ useGetDashboardDataMutation,
20
+ useGetDashboardDefinitionQuery,
21
+ useGetDashboardLayoutQuery,
22
+ useLazyGetDashboardVariableOptionsQuery,
23
+ useSaveDashboardLayoutMutation,
24
+ } from "../../../../redux/api/dashboardRuntimeApi";
25
+ import type { DashboardGridLayoutItem, DashboardWidgetComponentProps } from "../../../../types/dashboard";
26
+ import styles from "./DashboardPage.module.css";
27
+
28
+ type Option = {
29
+ label: string;
30
+ value: string;
31
+ };
32
+
33
+ type DateRangeValue = {
34
+ start: Date | null;
35
+ end: Date | null;
36
+ preset?: string;
37
+ };
38
+
39
+ type DashboardVariableValue = string | string[] | DateRangeValue | null | undefined;
40
+
41
+ const normalizeType = (value: unknown): string => `${value ?? ""}`.toLowerCase();
42
+
43
+ const isDateRangeVariable = (variable: any): boolean => {
44
+ const type = normalizeType(variable?.type);
45
+ return type === "date" || type === "daterange" || type === "date_range";
46
+ };
47
+
48
+ const isDynamicVariable = (variable: any): boolean => {
49
+ const type = normalizeType(variable?.type);
50
+ return type === "selectiondynamic" || type === "dynamicselect" || type === "selectdynamic";
51
+ };
52
+
53
+ const isStaticVariable = (variable: any): boolean => {
54
+ const type = normalizeType(variable?.type);
55
+ return type === "selectionstatic" || type === "select" || type === "multiselect";
56
+ };
57
+
58
+ const isMultiValueVariable = (variable: any): boolean => {
59
+ const type = normalizeType(variable?.type);
60
+ return type === "multiselect" || !!variable?.multiple || !!variable?.allowMultiple || !!variable?.isMultiSelect;
61
+ };
62
+
63
+ const toOptions = (options: any[]): Option[] =>
64
+ (options ?? [])
65
+ .map((entry: any) => {
66
+ if (typeof entry === "string") {
67
+ const [value, label] = entry.split(":");
68
+ return { value, label: label ?? value };
69
+ }
70
+ return {
71
+ value: `${entry?.value ?? entry?.id ?? ""}`,
72
+ label: `${entry?.label ?? entry?.name ?? entry?.displayName ?? entry?.value ?? entry?.id ?? ""}`,
73
+ };
74
+ })
75
+ .filter((entry) => !!entry.value);
76
+
77
+ const toIso = (value: Date | null | undefined): string | null => {
78
+ if (!value || Number.isNaN(value.getTime())) return null;
79
+ return value.toISOString();
80
+ };
81
+
82
+ const extractWidgetName = (widgetData: any): string =>
83
+ widgetData?.meta?.widgetName ?? widgetData?.widgetName ?? widgetData?.id ?? "";
84
+
85
+ const sortLayoutItems = (items: DashboardGridLayoutItem[]): DashboardGridLayoutItem[] =>
86
+ [...(items ?? [])].sort((a, b) => {
87
+ const byY = (a?.y ?? 0) - (b?.y ?? 0);
88
+ if (byY !== 0) return byY;
89
+ return (a?.x ?? 0) - (b?.x ?? 0);
90
+ });
91
+
92
+ const toLayoutItems = (items: any[]): DashboardGridLayoutItem[] =>
93
+ (items ?? [])
94
+ .filter((item) => !!(item?.widgetId ?? item?.id))
95
+ .map((item) => ({
96
+ widgetId: item?.widgetId ?? item?.id,
97
+ x: Number(item?.x ?? 0),
98
+ y: Number(item?.y ?? 0),
99
+ w: Number(item?.w ?? 4),
100
+ h: Number(item?.h ?? 3),
101
+ minW: item?.minW !== undefined ? Number(item?.minW) : undefined,
102
+ minH: item?.minH !== undefined ? Number(item?.minH) : undefined,
103
+ }));
104
+
105
+ const getAllLayoutItemsFromGrid = (grid: GridStack | null): DashboardGridLayoutItem[] => {
106
+ if (!grid) return [];
107
+ const snapshot = grid.save(false, false, (node: any, widget: any) => {
108
+ if (!widget.id) {
109
+ widget.id =
110
+ node?.id ??
111
+ node?.el?.getAttribute?.("gs-id") ??
112
+ node?.el?.getAttribute?.("id") ??
113
+ "";
114
+ }
115
+ });
116
+ const widgets = Array.isArray(snapshot) ? snapshot : [];
117
+ const fromSave = toLayoutItems(
118
+ widgets.map((item: any) => ({
119
+ widgetId: item?.id ?? item?.widgetId,
120
+ x: item?.x,
121
+ y: item?.y,
122
+ w: item?.w ?? item?.width,
123
+ h: item?.h ?? item?.height,
124
+ minW: item?.minW,
125
+ minH: item?.minH,
126
+ }))
127
+ );
128
+
129
+ if (fromSave.length > 0) return fromSave;
130
+
131
+ const nodes = Array.isArray((grid as any)?.engine?.nodes) ? (grid as any).engine.nodes : [];
132
+ return toLayoutItems(
133
+ nodes.map((node: any) => ({
134
+ widgetId:
135
+ node?.id ??
136
+ node?.el?.getAttribute?.("gs-id") ??
137
+ node?.el?.getAttribute?.("id"),
138
+ x: node?.x,
139
+ y: node?.y,
140
+ w: node?.w ?? node?.width,
141
+ h: node?.h ?? node?.height,
142
+ minW: node?.minW,
143
+ minH: node?.minH,
144
+ }))
145
+ );
146
+ };
147
+
148
+ const resolveDefaultDashboardWidgetComponentName = (definition: any, runtime: any): string => {
149
+ const runtimeData = runtime?.data ?? runtime ?? {};
150
+ if (runtimeData?.value !== undefined) return "DefaultDashboardKpiWidget";
151
+ if (Array.isArray(runtimeData?.columns) && Array.isArray(runtimeData?.records)) return "DefaultDashboardTableWidget";
152
+
153
+ const chartType = resolveChartType(definition);
154
+ if (chartType === "line") return "DefaultDashboardLineChartWidget";
155
+ if (chartType === "bar") return "DefaultDashboardBarChartWidget";
156
+ if (chartType === "pie") return "DefaultDashboardPieChartWidget";
157
+ return "DefaultDashboardUnknownWidget";
158
+ };
159
+
160
+ const getPreferredWidgetComponentName = (definition: any, runtime: any): string => {
161
+ const explicit =
162
+ definition?.componentName ??
163
+ definition?.widgetComponentName ??
164
+ definition?.renderer?.componentName ??
165
+ definition?.ui?.componentName;
166
+ return explicit || resolveDefaultDashboardWidgetComponentName(definition, runtime);
167
+ };
168
+
169
+ const notifyDashboardChartsToResize = () => {
170
+ if (typeof window === "undefined") return;
171
+ window.requestAnimationFrame(() => {
172
+ window.dispatchEvent(new Event("resize"));
173
+ });
174
+ };
175
+
176
+ const resolvePresetRange = (preset: string | undefined): DateRangeValue => {
177
+ if (!preset) return { start: null, end: null };
178
+ const now = new Date();
179
+ const end = new Date(now);
180
+
181
+ if (preset === "today") {
182
+ const start = new Date(now);
183
+ start.setHours(0, 0, 0, 0);
184
+ return { start, end };
185
+ }
186
+
187
+ if (preset === "last_24_hours") {
188
+ const start = new Date(now);
189
+ start.setHours(start.getHours() - 24);
190
+ return { start, end };
191
+ }
192
+
193
+ if (preset === "last_7_days") {
194
+ const start = new Date(now);
195
+ start.setDate(start.getDate() - 7);
196
+ return { start, end };
197
+ }
198
+
199
+ if (preset === "last_30_days") {
200
+ const start = new Date(now);
201
+ start.setDate(start.getDate() - 30);
202
+ return { start, end };
203
+ }
204
+
205
+ return { start: null, end: null };
206
+ };
207
+
208
+ const parseDateValue = (value: any): Date | null => {
209
+ if (!value) return null;
210
+ const date = new Date(value);
211
+ return Number.isNaN(date.getTime()) ? null : date;
212
+ };
213
+
214
+ const getDatePresetOptions = (variable: any): Option[] => {
215
+ const metadataPresets = variable?.presetValues ?? variable?.presets ?? variable?.datePresets;
216
+ const builtIns: Option[] = [
217
+ { value: "today", label: "Today" },
218
+ { value: "last_24_hours", label: "Last 24 Hours" },
219
+ { value: "last_7_days", label: "Last 7 Days" },
220
+ { value: "last_30_days", label: "Last 30 Days" },
221
+ ];
222
+
223
+ const source = Array.isArray(metadataPresets) && metadataPresets.length ? metadataPresets : builtIns;
224
+ const normalized = source
225
+ .map((entry: any) => {
226
+ if (typeof entry === "string") {
227
+ const [value, label] = entry.split(":");
228
+ const cleaned = `${value ?? ""}`.trim();
229
+ if (!cleaned) return null;
230
+ return { value: cleaned, label: `${label ?? cleaned}` };
231
+ }
232
+ const value = `${entry?.value ?? entry?.id ?? ""}`.trim();
233
+ if (!value) return null;
234
+ const label = `${entry?.label ?? entry?.displayName ?? value}`;
235
+ return { value, label };
236
+ })
237
+ .filter(Boolean) as Option[];
238
+
239
+ if (!normalized.find((entry) => entry.value === "custom")) {
240
+ normalized.push({ value: "custom", label: "Custom" });
241
+ }
242
+ return normalized;
243
+ };
244
+
245
+ const buildDefaultFilterValues = (variables: any[]): Record<string, DashboardVariableValue> => {
246
+ const defaults: Record<string, DashboardVariableValue> = {};
247
+
248
+ variables.forEach((variable: any) => {
249
+ const name = variable?.name;
250
+ if (!name) return;
251
+
252
+ if (isDateRangeVariable(variable)) {
253
+ const presetRange = resolvePresetRange(variable?.defaultValue?.preset ?? variable?.defaultValuePreset);
254
+ const start = parseDateValue(variable?.defaultValue?.start ?? variable?.defaultValue?.from) ?? presetRange.start;
255
+ const end = parseDateValue(variable?.defaultValue?.end ?? variable?.defaultValue?.to) ?? presetRange.end;
256
+ const preset =
257
+ `${variable?.defaultValue?.preset ?? variable?.defaultValuePreset ?? ""}`.trim() ||
258
+ (start || end ? "custom" : "custom");
259
+ defaults[name] = { start, end, preset };
260
+ return;
261
+ }
262
+
263
+ if (isMultiValueVariable(variable)) {
264
+ const base = variable?.defaultValue;
265
+ defaults[name] = Array.isArray(base) ? base.map((entry: any) => `${entry}`) : [];
266
+ return;
267
+ }
268
+
269
+ if (variable?.defaultValue !== undefined && variable?.defaultValue !== null) {
270
+ defaults[name] = `${variable.defaultValue}`;
271
+ return;
272
+ }
273
+
274
+ defaults[name] = "";
275
+ });
276
+
277
+ return defaults;
278
+ };
279
+
280
+ const buildClearedFilterValues = (variables: any[]): Record<string, DashboardVariableValue> => {
281
+ const cleared: Record<string, DashboardVariableValue> = {};
282
+
283
+ variables.forEach((variable: any) => {
284
+ const name = variable?.name;
285
+ if (!name) return;
286
+
287
+ if (isDateRangeVariable(variable)) {
288
+ cleared[name] = { start: null, end: null, preset: "custom" };
289
+ return;
290
+ }
291
+
292
+ if (isMultiValueVariable(variable)) {
293
+ cleared[name] = [];
294
+ return;
295
+ }
296
+
297
+ cleared[name] = "";
298
+ });
299
+
300
+ return cleared;
301
+ };
302
+
303
+ const isFilterValueApplied = (variable: any, value: DashboardVariableValue): boolean => {
304
+ if (isDateRangeVariable(variable)) {
305
+ const range = value as DateRangeValue | undefined;
306
+ return !!range?.start || !!range?.end;
307
+ }
308
+
309
+ if (isMultiValueVariable(variable)) {
310
+ return Array.isArray(value) && value.length > 0;
311
+ }
312
+
313
+ return value !== undefined && value !== null && `${value}`.trim() !== "";
314
+ };
315
+
316
+ const isSameFilterValue = (variable: any, left: DashboardVariableValue, right: DashboardVariableValue): boolean => {
317
+ if (isDateRangeVariable(variable)) {
318
+ const leftRange = (left as DateRangeValue | undefined) ?? { start: null, end: null };
319
+ const rightRange = (right as DateRangeValue | undefined) ?? { start: null, end: null };
320
+ return toIso(leftRange.start) === toIso(rightRange.start) && toIso(leftRange.end) === toIso(rightRange.end);
321
+ }
322
+
323
+ if (isMultiValueVariable(variable)) {
324
+ const leftArr = Array.isArray(left) ? left.map((v) => `${v}`).sort() : [];
325
+ const rightArr = Array.isArray(right) ? right.map((v) => `${v}`).sort() : [];
326
+ if (leftArr.length !== rightArr.length) return false;
327
+ return leftArr.every((value, index) => value === rightArr[index]);
328
+ }
329
+
330
+ return `${left ?? ""}` === `${right ?? ""}`;
331
+ };
332
+
333
+ const getDashboardTitle = (definition: any, fallback: string) =>
334
+ definition?.displayName ?? definition?.title ?? definition?.name ?? fallback;
335
+
336
+ const getWidgetTitle = (widget: any, fallback: string) =>
337
+ widget?.displayName ?? widget?.title ?? widget?.name ?? fallback;
338
+
339
+ const isUnauthorizedWidgetRuntime = (runtime: any): boolean =>
340
+ !!(runtime?.meta?.unauthorized || runtime?.uiHints?.state === "unauthorized");
3
341
 
4
342
  export function DashboardPage() {
5
- const params = useParams();
343
+ const params = useParams();
344
+ const moduleName = params.moduleName ?? "";
345
+ const dashboardName = params.dashboardName ?? "";
346
+ const gridRef = useRef<HTMLDivElement | null>(null);
347
+ const gridInstanceRef = useRef<GridStack | null>(null);
348
+
349
+ const { data: definition, isLoading: definitionLoading } = useGetDashboardDefinitionQuery(
350
+ { moduleName, dashboardName },
351
+ { skip: !moduleName || !dashboardName }
352
+ );
353
+
354
+ const { data: layoutData } = useGetDashboardLayoutQuery(
355
+ { moduleName, dashboardName },
356
+ { skip: !moduleName || !dashboardName }
357
+ );
358
+
359
+ const [getDashboardData, { data: dashboardData, isLoading: dataLoading, error: dataError }] = useGetDashboardDataMutation();
360
+ const [saveDashboardLayout, { isLoading: saveLayoutLoading }] = useSaveDashboardLayoutMutation();
361
+ const [getVariableOptions] = useLazyGetDashboardVariableOptionsQuery();
362
+
363
+ const [appliedVariableValues, setAppliedVariableValues] = useState<Record<string, DashboardVariableValue>>({});
364
+ const [filterDraftValues, setFilterDraftValues] = useState<Record<string, DashboardVariableValue>>({});
365
+ const [dynamicOptions, setDynamicOptions] = useState<Record<string, Option[]>>({});
366
+ const [draftLayoutItems, setDraftLayoutItems] = useState<DashboardGridLayoutItem[] | null>(null);
367
+ const [isFilterDialogOpen, setIsFilterDialogOpen] = useState(false);
368
+ const bootstrappedRef = useRef(false);
369
+
370
+ const variables = useMemo(() => (Array.isArray(definition?.variables) ? definition.variables : []), [definition]);
371
+ const definitionWidgets = useMemo(() => (Array.isArray(definition?.widgets) ? definition.widgets : []), [definition]);
372
+ const effectiveLayoutItems = useMemo(() => toLayoutItems(layoutData?.effectiveLayout?.items ?? []), [layoutData]);
373
+ const gridColumns = Number(layoutData?.effectiveLayout?.columns ?? 12) || 12;
374
+
375
+ const defaultFilterValues = useMemo(() => buildDefaultFilterValues(variables), [variables]);
376
+
377
+ useEffect(() => {
378
+ if (!definition || bootstrappedRef.current) return;
379
+ setAppliedVariableValues(defaultFilterValues);
380
+ setFilterDraftValues(defaultFilterValues);
381
+ bootstrappedRef.current = true;
382
+ }, [definition, defaultFilterValues]);
383
+
384
+ useEffect(() => {
385
+ if (!isFilterDialogOpen) return;
386
+ setFilterDraftValues(appliedVariableValues);
387
+ }, [appliedVariableValues, isFilterDialogOpen]);
388
+
389
+ useEffect(() => {
390
+ if (!moduleName || !dashboardName || variables.length === 0) return;
391
+ const dynamicVariables = variables.filter((variable: any) => isDynamicVariable(variable));
392
+ dynamicVariables.forEach((variable: any) => {
393
+ const variableName = variable?.name;
394
+ if (!variableName) return;
395
+ void getVariableOptions({ moduleName, dashboardName, variableName, limit: 100 })
396
+ .unwrap()
397
+ .then((response) => {
398
+ setDynamicOptions((prev) => ({ ...prev, [variableName]: toOptions(Array.isArray(response) ? response : []) }));
399
+ })
400
+ .catch(() => {
401
+ setDynamicOptions((prev) => ({ ...prev, [variableName]: [] }));
402
+ });
403
+ });
404
+ }, [dashboardName, getVariableOptions, moduleName, variables]);
405
+
406
+ const handleDynamicOptionsSearch = async (variableName: string, query: string) => {
407
+ if (!moduleName || !dashboardName || !variableName) return;
408
+ try {
409
+ const response = await getVariableOptions({ moduleName, dashboardName, variableName, query, limit: 100 }).unwrap();
410
+ setDynamicOptions((prev) => ({ ...prev, [variableName]: toOptions(Array.isArray(response) ? response : []) }));
411
+ } catch {
412
+ setDynamicOptions((prev) => ({ ...prev, [variableName]: [] }));
413
+ }
414
+ };
415
+
416
+ useEffect(() => {
417
+ if (!bootstrappedRef.current || !moduleName || !dashboardName) return;
418
+ void handleRefresh(appliedVariableValues);
419
+ // eslint-disable-next-line react-hooks/exhaustive-deps
420
+ }, [moduleName, dashboardName, bootstrappedRef.current]);
421
+
422
+ const widgetDefinitionMap = useMemo(() => {
423
+ const map = new Map<string, any>();
424
+ definitionWidgets.forEach((widget: any) => {
425
+ if (widget?.id) map.set(widget.id, widget);
426
+ if (widget?.name) map.set(widget.name, widget);
427
+ });
428
+ return map;
429
+ }, [definitionWidgets]);
430
+
431
+ const widgetDataMap = useMemo(() => {
432
+ const map = new Map<string, any>();
433
+ const rows = Array.isArray(dashboardData?.widgets) ? dashboardData.widgets : [];
434
+ rows.forEach((row: any) => {
435
+ const name = extractWidgetName(row);
436
+ if (name) map.set(name, row);
437
+ });
438
+ return map;
439
+ }, [dashboardData]);
440
+
441
+ const orderedWidgets = useMemo(() => {
442
+ const layoutItems = sortLayoutItems(effectiveLayoutItems);
443
+ if (!layoutItems.length) return definitionWidgets;
444
+
445
+ const seen = new Set<string>();
446
+ const arranged: any[] = [];
447
+ layoutItems.forEach((item: DashboardGridLayoutItem) => {
448
+ const key = item?.widgetId;
449
+ const definitionWidget = widgetDefinitionMap.get(key);
450
+ if (!definitionWidget || seen.has(key)) return;
451
+ arranged.push(definitionWidget);
452
+ seen.add(key);
453
+ });
454
+
455
+ definitionWidgets.forEach((widget: any) => {
456
+ const key = widget?.id ?? widget?.name;
457
+ if (!key || seen.has(key)) return;
458
+ arranged.push(widget);
459
+ });
460
+
461
+ return arranged;
462
+ }, [definitionWidgets, effectiveLayoutItems, widgetDefinitionMap]);
463
+
464
+ const widgetLayoutMap = useMemo(() => {
465
+ const map = new Map<string, DashboardGridLayoutItem>();
466
+ effectiveLayoutItems.forEach((item) => map.set(item.widgetId, item));
467
+ return map;
468
+ }, [effectiveLayoutItems]);
469
+
470
+ const appliedFilterCount = useMemo(() => {
471
+ return variables.reduce((count: number, variable: any) => {
472
+ const name = variable?.name;
473
+ if (!name) return count;
474
+ const value = appliedVariableValues[name];
475
+ const defaultValue = defaultFilterValues[name];
476
+ if (!isFilterValueApplied(variable, value)) return count;
477
+ return isSameFilterValue(variable, value, defaultValue) ? count : count + 1;
478
+ }, 0);
479
+ }, [appliedVariableValues, defaultFilterValues, variables]);
480
+
481
+ useEffect(() => {
482
+ if (!gridRef.current || orderedWidgets.length === 0) return;
483
+
484
+ if (gridInstanceRef.current) {
485
+ gridInstanceRef.current.destroy(false);
486
+ gridInstanceRef.current = null;
487
+ }
488
+
489
+ const initialLayout: DashboardGridLayoutItem[] = effectiveLayoutItems.length
490
+ ? effectiveLayoutItems
491
+ : orderedWidgets.map((widget: any, index: number) => ({
492
+ widgetId: widget?.id ?? widget?.name ?? `widget-${index}`,
493
+ x: 0,
494
+ y: index * 3,
495
+ w: 4,
496
+ h: 3,
497
+ }));
498
+
499
+ const gridWidgets = initialLayout.map((item: DashboardGridLayoutItem) => ({
500
+ id: item.widgetId,
501
+ x: item.x,
502
+ y: item.y,
503
+ w: item.w,
504
+ h: item.h,
505
+ minW: item.minW,
506
+ minH: item.minH,
507
+ }));
508
+
509
+ const instance = GridStack.init(
510
+ {
511
+ column: gridColumns,
512
+ cellHeight: 92,
513
+ margin: 8,
514
+ float: true,
515
+ },
516
+ gridRef.current
517
+ );
518
+
519
+ // React renders the DOM; make sure Gridstack converts every item node to a widget,
520
+ // then explicitly apply layout from API to avoid attribute-parsing or timing issues.
521
+ instance.batchUpdate();
522
+ const domItems = Array.from(gridRef.current.querySelectorAll(".grid-stack-item")) as HTMLElement[];
523
+ domItems.forEach((itemEl) => {
524
+ if (!(itemEl as any).gridstackNode) {
525
+ instance.makeWidget(itemEl as any);
526
+ }
527
+ });
528
+ instance.column(gridColumns, "none");
529
+ instance.load(gridWidgets as any, false);
530
+ instance.batchUpdate(false);
531
+ notifyDashboardChartsToResize();
532
+ window.setTimeout(notifyDashboardChartsToResize, 60);
533
+
534
+ instance.on("change", (_event: Event, _items: GridStackNode[]) => {
535
+ const normalized = getAllLayoutItemsFromGrid(instance);
536
+ setDraftLayoutItems(normalized);
537
+ notifyDashboardChartsToResize();
538
+ });
539
+ instance.on("resizestop", () => notifyDashboardChartsToResize());
540
+ instance.on("dragstop", () => notifyDashboardChartsToResize());
541
+
542
+ gridInstanceRef.current = instance;
543
+ return () => {
544
+ instance.destroy(false);
545
+ if (gridInstanceRef.current === instance) gridInstanceRef.current = null;
546
+ };
547
+ }, [orderedWidgets, gridColumns, effectiveLayoutItems]);
548
+
549
+ const handleDraftVariableChange = (variableName: string, value: DashboardVariableValue) => {
550
+ setFilterDraftValues((prev) => ({ ...prev, [variableName]: value }));
551
+ };
552
+
553
+ const buildPayloadVariables = (values: Record<string, DashboardVariableValue>): Record<string, any> => {
554
+ const payload: Record<string, any> = {};
555
+ variables.forEach((variable: any) => {
556
+ const name = variable?.name;
557
+ if (!name) return;
558
+ const value = values[name];
559
+
560
+ if (isDateRangeVariable(variable)) {
561
+ const range = value as DateRangeValue | undefined;
562
+ const from = toIso(range?.start ?? null);
563
+ const to = toIso(range?.end ?? null);
564
+ if (!from && !to) return;
565
+ payload[name] = { from, to };
566
+ return;
567
+ }
568
+
569
+ if (isMultiValueVariable(variable)) {
570
+ const list = Array.isArray(value) ? value.filter(Boolean) : [];
571
+ if (!list.length) return;
572
+ payload[name] = list;
573
+ return;
574
+ }
575
+
576
+ if (value === undefined || value === null || value === "") return;
577
+ payload[name] = value;
578
+ });
579
+ return payload;
580
+ };
581
+
582
+ async function handleRefresh(values = appliedVariableValues) {
583
+ if (!moduleName || !dashboardName) return;
584
+ await getDashboardData({
585
+ moduleName,
586
+ dashboardName,
587
+ variables: buildPayloadVariables(values),
588
+ }).unwrap();
589
+ }
590
+
591
+ async function handleApplyFilters() {
592
+ const nextValues = { ...filterDraftValues };
593
+ setAppliedVariableValues(nextValues);
594
+ setIsFilterDialogOpen(false);
595
+ await handleRefresh(nextValues);
596
+ }
597
+
598
+ async function handleClearFilters() {
599
+ const clearedValues = buildClearedFilterValues(variables);
600
+ setFilterDraftValues(clearedValues);
601
+ setAppliedVariableValues(clearedValues);
602
+ setIsFilterDialogOpen(false);
603
+ await handleRefresh(clearedValues);
604
+ }
605
+
606
+ async function handleSaveLayout() {
607
+ if (!moduleName || !dashboardName) return;
608
+ const liveGridLayout = getAllLayoutItemsFromGrid(gridInstanceRef.current);
609
+ const baseLayout = effectiveLayoutItems.length ? effectiveLayoutItems : [];
610
+ const items = liveGridLayout.length
611
+ ? liveGridLayout
612
+ : draftLayoutItems && draftLayoutItems.length
613
+ ? draftLayoutItems
614
+ : baseLayout;
615
+
616
+ await saveDashboardLayout({
617
+ moduleName,
618
+ dashboardName,
619
+ layout: {
620
+ engine: "gridstack",
621
+ columns: gridColumns,
622
+ items,
623
+ },
624
+ }).unwrap();
625
+
626
+ setDraftLayoutItems(null);
627
+ }
628
+
629
+ const renderVariable = (variable: any) => {
630
+ const variableName = variable?.name;
631
+ if (!variableName) return null;
632
+
633
+ const value = filterDraftValues[variableName];
634
+ const label = variable?.displayName ?? variable?.label ?? variableName;
635
+
636
+ if (isDateRangeVariable(variable)) {
637
+ const range = (value as DateRangeValue | undefined) ?? { start: null, end: null };
638
+ const presetOptions = getDatePresetOptions(variable);
639
+ const preset = range?.preset && presetOptions.some((entry) => entry.value === range.preset) ? range.preset : "custom";
640
+ return (
641
+ <div key={variableName} className={styles.filterField}>
642
+ <label className={styles.filterLabel}>{label}</label>
643
+ <SolidSelect
644
+ value={preset}
645
+ options={presetOptions}
646
+ optionLabel="label"
647
+ optionValue="value"
648
+ placeholder={`Select ${label} preset`}
649
+ onChange={(event) => {
650
+ const selectedPreset = `${event?.value ?? "custom"}`;
651
+ if (selectedPreset === "custom") {
652
+ handleDraftVariableChange(variableName, {
653
+ start: range.start ?? null,
654
+ end: range.end ?? null,
655
+ preset: "custom",
656
+ });
657
+ return;
658
+ }
659
+ const nextRange = resolvePresetRange(selectedPreset);
660
+ handleDraftVariableChange(variableName, {
661
+ start: nextRange.start ?? null,
662
+ end: nextRange.end ?? null,
663
+ preset: selectedPreset,
664
+ });
665
+ }}
666
+ />
667
+ {preset === "custom" ? (
668
+ <div className={styles.dateRange}>
669
+ <SolidDatePicker
670
+ selected={range.start ?? null}
671
+ inputClassName="w-full"
672
+ onChange={(next: Date | null) =>
673
+ handleDraftVariableChange(variableName, {
674
+ start: next ?? null,
675
+ end: range.end ?? null,
676
+ preset: "custom",
677
+ })
678
+ }
679
+ placeholderText="Start date"
680
+ dateFormat="yyyy-MM-dd"
681
+ />
682
+ <SolidDatePicker
683
+ selected={range.end ?? null}
684
+ inputClassName="w-full"
685
+ onChange={(next: Date | null) =>
686
+ handleDraftVariableChange(variableName, {
687
+ start: range.start ?? null,
688
+ end: next ?? null,
689
+ preset: "custom",
690
+ })
691
+ }
692
+ placeholderText="End date"
693
+ dateFormat="yyyy-MM-dd"
694
+ />
695
+ </div>
696
+ ) : null}
697
+ </div>
698
+ );
699
+ }
700
+
701
+ if (isMultiValueVariable(variable)) {
702
+ const baseOptions = isDynamicVariable(variable)
703
+ ? (dynamicOptions[variableName] ?? [])
704
+ : toOptions(variable?.selectionStaticValues ?? variable?.options ?? []);
705
+ const selectedValues = Array.isArray(value) ? value : [];
706
+ return (
707
+ <div key={variableName} className={styles.filterField}>
708
+ <label className={styles.filterLabel}>{label}</label>
709
+ <SolidAutocomplete
710
+ multiple
711
+ dropdown
712
+ field="label"
713
+ className="w-full"
714
+ inputClassName="w-full"
715
+ value={baseOptions.filter((option) => selectedValues.includes(option.value))}
716
+ suggestions={baseOptions}
717
+ completeMethod={() => Promise.resolve()}
718
+ placeholder={`Select ${label}`}
719
+ onChange={(event: { value: any }) => {
720
+ const selected = Array.isArray(event?.value)
721
+ ? event.value
722
+ .map((entry: any) => `${entry?.value ?? entry?.id ?? entry ?? ""}`)
723
+ .filter((entry: string) => !!entry)
724
+ : [];
725
+ handleDraftVariableChange(variableName, selected);
726
+ }}
727
+ />
728
+ </div>
729
+ );
730
+ }
731
+
732
+ if (isDynamicVariable(variable)) {
733
+ const options = dynamicOptions[variableName] ?? [];
734
+ const selectedValue = typeof value === "string" ? value : "";
735
+ const selectedOption = options.find((entry) => `${entry.value}` === selectedValue) ?? null;
736
+
737
+ return (
738
+ <div key={variableName} className={styles.filterField}>
739
+ <label className={styles.filterLabel}>{label}</label>
740
+ <SolidAutocomplete
741
+ dropdown
742
+ field="label"
743
+ className="w-full"
744
+ inputClassName="w-full"
745
+ value={selectedOption}
746
+ suggestions={options}
747
+ completeMethod={({ query }) => handleDynamicOptionsSearch(variableName, query)}
748
+ placeholder={`Select ${label}`}
749
+ onChange={(event: { value: any }) => {
750
+ const selected = event?.value;
751
+ const nextValue = selected?.value ?? selected?.id ?? "";
752
+ handleDraftVariableChange(variableName, nextValue ? `${nextValue}` : "");
753
+ }}
754
+ />
755
+ </div>
756
+ );
757
+ }
758
+
759
+ const options = isDynamicVariable(variable)
760
+ ? (dynamicOptions[variableName] ?? [])
761
+ : isStaticVariable(variable)
762
+ ? toOptions(variable?.selectionStaticValues ?? variable?.options ?? [])
763
+ : [];
6
764
 
7
- const dashboardName = params.dashboardName;
8
- const moduleName = params.moduleName as string;
9
765
  return (
10
- <div>
11
- <SolidDashboard moduleName={moduleName} dashboardName={dashboardName} />
766
+ <div key={variableName} className={styles.filterField}>
767
+ <label className={styles.filterLabel}>{label}</label>
768
+ <SolidSelect
769
+ value={typeof value === "string" ? value : ""}
770
+ options={options}
771
+ optionLabel="label"
772
+ optionValue="value"
773
+ placeholder={`Select ${label}`}
774
+ onChange={(event) => handleDraftVariableChange(variableName, event.value ? `${event.value}` : "")}
775
+ />
776
+ </div>
777
+ );
778
+ };
779
+
780
+ const renderWidgetBody = (widgetDefinition: any, runtime: any) => {
781
+ if (isUnauthorizedWidgetRuntime(runtime)) {
782
+ return (
783
+ <div className={styles.widgetUnauthorizedState}>
784
+ <AlertTriangle size={18} className={styles.widgetUnauthorizedIcon} />
785
+ <span className={styles.widgetUnauthorizedText}>
786
+ {runtime?.uiHints?.message ?? "Unauthorized"}
787
+ </span>
12
788
  </div>
789
+ );
790
+ }
791
+
792
+ const componentName = getPreferredWidgetComponentName(widgetDefinition, runtime);
793
+ const ExtensionWidget = getExtensionComponent(componentName);
794
+ if (!ExtensionWidget) {
795
+ const UnknownWidget = getExtensionComponent("DefaultDashboardUnknownWidget");
796
+ if (!UnknownWidget) return <pre>{JSON.stringify(runtime?.data ?? runtime ?? {}, null, 2)}</pre>;
797
+ return <UnknownWidget definition={widgetDefinition} runtime={runtime} variables={appliedVariableValues} />;
798
+ }
799
+ return (
800
+ <ExtensionWidget
801
+ {...({ definition: widgetDefinition, runtime, variables: appliedVariableValues } satisfies DashboardWidgetComponentProps)}
802
+ />
803
+ );
804
+ };
805
+
806
+ if (definitionLoading) {
807
+ return (
808
+ <div className={styles.page}>
809
+ <SolidSpinner />
810
+ </div>
13
811
  );
812
+ }
813
+
814
+ return (
815
+ <div className={styles.page}>
816
+ <div className={styles.header}>
817
+ <div className={styles.titleBlock}>
818
+ <h1 className={styles.title}>{getDashboardTitle(definition, dashboardName)}</h1>
819
+ <p className={styles.subtitle}>{definition?.description ?? `${moduleName} / ${dashboardName}`}</p>
820
+ </div>
821
+
822
+ <div className={styles.headerActions}>
823
+ <div className={styles.filterButtonWrap}>
824
+ <SolidButton
825
+ className={styles.iconButton}
826
+ leftIcon={<Filter size={16} />}
827
+ onClick={() => setIsFilterDialogOpen(true)}
828
+ tooltip="Filters"
829
+ aria-label="Filters"
830
+ />
831
+ {appliedFilterCount > 0 ? <span className={styles.filterCountBadge}>{appliedFilterCount}</span> : null}
832
+ </div>
833
+
834
+ <SolidButton
835
+ className={styles.iconButton}
836
+ leftIcon={<RefreshCw size={16} />}
837
+ onClick={() => void handleRefresh()}
838
+ disabled={dataLoading}
839
+ tooltip="Refresh"
840
+ aria-label="Refresh"
841
+ />
842
+
843
+ <SolidButton
844
+ className={styles.iconButton}
845
+ leftIcon={<Save size={16} />}
846
+ onClick={() => void handleSaveLayout()}
847
+ disabled={saveLayoutLoading}
848
+ tooltip="Save Layout"
849
+ aria-label="Save Layout"
850
+ />
851
+ </div>
852
+ </div>
853
+
854
+ <SolidDialog
855
+ open={isFilterDialogOpen}
856
+ onOpenChange={setIsFilterDialogOpen}
857
+ header="Dashboard Filters"
858
+ contentClassName={styles.filterDialogContent}
859
+ >
860
+ <SolidDialogBody>
861
+ <div className={styles.filterModalColumn}>{variables.map(renderVariable)}</div>
862
+ </SolidDialogBody>
863
+ <SolidDialogFooter className={styles.filterModalActions}>
864
+ <SolidButton variant="outline" onClick={() => void handleClearFilters()}>
865
+ Clear
866
+ </SolidButton>
867
+ <SolidButton onClick={() => void handleApplyFilters()}>
868
+ Apply
869
+ </SolidButton>
870
+ </SolidDialogFooter>
871
+ </SolidDialog>
872
+
873
+ {dataError ? <p className={styles.error}>Failed to load dashboard data.</p> : null}
874
+
875
+ <div ref={gridRef} className={`grid-stack ${styles.gridStack}`}>
876
+ {orderedWidgets.map((widget: any) => {
877
+ const widgetName = widget?.id ?? widget?.name;
878
+ const runtime = widgetDataMap.get(widgetName);
879
+ const slot = widgetLayoutMap.get(widgetName);
880
+
881
+ return (
882
+ <div
883
+ key={widgetName}
884
+ className="grid-stack-item"
885
+ gs-id={widgetName}
886
+ gs-x={slot?.x ?? 0}
887
+ gs-y={slot?.y ?? 0}
888
+ gs-w={slot?.w ?? 4}
889
+ gs-h={slot?.h ?? 3}
890
+ gs-min-w={slot?.minW ?? 2}
891
+ gs-min-h={slot?.minH ?? 2}
892
+ >
893
+ <div className={`grid-stack-item-content ${styles.widgetCard}`}>
894
+ <h3 className={styles.widgetTitle}>{getWidgetTitle(widget, widgetName)}</h3>
895
+ <div className={styles.widgetBody}>
896
+ {runtime ? renderWidgetBody(widget, runtime) : <p className={styles.muted}>No data</p>}
897
+ </div>
898
+ </div>
899
+ </div>
900
+ );
901
+ })}
902
+ </div>
903
+ </div>
904
+ );
14
905
  }