@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.
- package/dist/components/core/dashboard/mappers/echartsOptionMapper.d.ts +4 -0
- package/dist/components/core/dashboard/mappers/echartsOptionMapper.d.ts.map +1 -0
- package/dist/components/core/dashboard/mappers/echartsOptionMapper.js +76 -0
- package/dist/components/core/dashboard/mappers/echartsOptionMapper.js.map +1 -0
- package/dist/components/core/dashboard/mappers/echartsOptionMapper.ts +78 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardBarChartWidget.d.ts +3 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardBarChartWidget.d.ts.map +1 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardBarChartWidget.js +17 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardBarChartWidget.js.map +1 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardBarChartWidget.tsx +6 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardChartWidget.d.ts +3 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardChartWidget.d.ts.map +1 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardChartWidget.js +41 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardChartWidget.js.map +1 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardChartWidget.tsx +56 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardKpiWidget.d.ts +3 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardKpiWidget.d.ts.map +1 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardKpiWidget.js +9 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardKpiWidget.js.map +1 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardKpiWidget.tsx +7 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardLineChartWidget.d.ts +3 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardLineChartWidget.d.ts.map +1 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardLineChartWidget.js +17 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardLineChartWidget.js.map +1 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardLineChartWidget.tsx +6 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardPieChartWidget.d.ts +3 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardPieChartWidget.d.ts.map +1 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardPieChartWidget.js +17 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardPieChartWidget.js.map +1 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardPieChartWidget.tsx +6 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardTableWidget.d.ts +3 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardTableWidget.d.ts.map +1 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardTableWidget.js +12 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardTableWidget.js.map +1 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardTableWidget.tsx +39 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardUnknownWidget.d.ts +3 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardUnknownWidget.d.ts.map +1 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardUnknownWidget.js +7 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardUnknownWidget.js.map +1 -0
- package/dist/components/core/dashboard/widgets/DefaultDashboardUnknownWidget.tsx +9 -0
- package/dist/components/core/dashboard/widgets/QueueSlaHeatmapWidget.d.ts +3 -0
- package/dist/components/core/dashboard/widgets/QueueSlaHeatmapWidget.d.ts.map +1 -0
- package/dist/components/core/dashboard/widgets/QueueSlaHeatmapWidget.js +159 -0
- package/dist/components/core/dashboard/widgets/QueueSlaHeatmapWidget.js.map +1 -0
- package/dist/components/core/dashboard/widgets/QueueSlaHeatmapWidget.tsx +205 -0
- package/dist/helpers/registry.d.ts.map +1 -1
- package/dist/helpers/registry.js +15 -10
- package/dist/helpers/registry.js.map +1 -1
- package/dist/helpers/registry.ts +16 -12
- package/dist/index.d.ts +4 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -7
- package/dist/index.js.map +1 -1
- package/dist/index.ts +12 -13
- package/dist/redux/api/dashboardRuntimeApi.d.ts +34 -0
- package/dist/redux/api/dashboardRuntimeApi.d.ts.map +1 -0
- package/dist/redux/api/dashboardRuntimeApi.js +79 -0
- package/dist/redux/api/dashboardRuntimeApi.js.map +1 -0
- package/dist/redux/api/dashboardRuntimeApi.ts +75 -0
- package/dist/redux/store/defaultStoreConfig.d.ts +34 -9
- package/dist/redux/store/defaultStoreConfig.d.ts.map +1 -1
- package/dist/redux/store/defaultStoreConfig.js +2 -6
- package/dist/redux/store/defaultStoreConfig.js.map +1 -1
- package/dist/redux/store/defaultStoreConfig.ts +2 -6
- package/dist/routes/pages/admin/core/DashboardPage.d.ts +1 -0
- package/dist/routes/pages/admin/core/DashboardPage.d.ts.map +1 -1
- package/dist/routes/pages/admin/core/DashboardPage.js +780 -5
- package/dist/routes/pages/admin/core/DashboardPage.js.map +1 -1
- package/dist/routes/pages/admin/core/DashboardPage.module.css +186 -0
- package/dist/routes/pages/admin/core/DashboardPage.tsx +898 -7
- package/dist/routes/solidRoutes.d.ts.map +1 -1
- package/dist/routes/solidRoutes.js +2 -2
- package/dist/routes/solidRoutes.js.map +1 -1
- package/dist/routes/solidRoutes.tsx +2 -2
- package/dist/routes/types.d.ts +1 -1
- package/dist/routes/types.d.ts.map +1 -1
- package/dist/routes/types.js.map +1 -1
- package/dist/routes/types.ts +1 -1
- package/dist/types/dashboard.d.ts +16 -0
- package/dist/types/dashboard.d.ts.map +1 -0
- package/dist/types/dashboard.js +2 -0
- package/dist/types/dashboard.js.map +1 -0
- package/dist/types/dashboard.ts +23 -0
- package/dist/types/extension-registry.d.ts +1 -0
- package/dist/types/extension-registry.d.ts.map +1 -1
- package/dist/types/extension-registry.js +1 -0
- package/dist/types/extension-registry.js.map +1 -1
- package/dist/types/extension-registry.ts +1 -0
- package/package.json +3 -1
|
@@ -1,14 +1,905 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { useParams
|
|
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
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
}
|