@nubitio/dashboard 0.5.19
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/LICENSE +21 -0
- package/dist/index.cjs +537 -0
- package/dist/index.d.cts +147 -0
- package/dist/index.d.mts +147 -0
- package/dist/index.mjs +527 -0
- package/dist/style.css +315 -0
- package/package.json +58 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import { AppToolbar, Badge, EmptyState, StatCard } from "@nubitio/ui";
|
|
2
|
+
import { useCallback, useEffect, useState } from "react";
|
|
3
|
+
import { getCoreApiBaseUrl, getCoreCurrency, getCoreLocale } from "@nubitio/core";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { Link } from "react-router-dom";
|
|
6
|
+
//#region packages/dashboard/dashboard/defineDashboard.ts
|
|
7
|
+
const dashboardCache = /* @__PURE__ */ new Map();
|
|
8
|
+
function cacheKey(config) {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.stringify({
|
|
11
|
+
id: config.id,
|
|
12
|
+
title: config.title,
|
|
13
|
+
dataUrl: config.dataUrl,
|
|
14
|
+
sections: config.sections
|
|
15
|
+
});
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Declarative dashboard definition — mirrors `defineResource` ergonomics.
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* const overview = defineDashboard({
|
|
25
|
+
* title: 'Dashboard',
|
|
26
|
+
* dataUrl: '/api/dashboard/overview',
|
|
27
|
+
* sections: [
|
|
28
|
+
* { layout: 'stats', widgets: [statWidget({ id: 'revenue', title: 'Revenue', valuePath: 'stats.revenue', format: 'currency' })] },
|
|
29
|
+
* ],
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* export const DashboardRoute = () => <DashboardPage config={overview} />;
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
function defineDashboard(config) {
|
|
36
|
+
const key = cacheKey(config);
|
|
37
|
+
if (key !== null && dashboardCache.has(key)) return dashboardCache.get(key);
|
|
38
|
+
const dashboard = {
|
|
39
|
+
id: "dashboard",
|
|
40
|
+
...config
|
|
41
|
+
};
|
|
42
|
+
if (key !== null) dashboardCache.set(key, dashboard);
|
|
43
|
+
return dashboard;
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region packages/dashboard/dashboard/useDashboardData.ts
|
|
47
|
+
function resolveDataUrl(dataUrl) {
|
|
48
|
+
if (/^https?:\/\//i.test(dataUrl)) return dataUrl;
|
|
49
|
+
return `${getCoreApiBaseUrl().replace(/\/+$/, "")}${dataUrl.startsWith("/") ? dataUrl : `/${dataUrl}`}`;
|
|
50
|
+
}
|
|
51
|
+
const EMPTY = {
|
|
52
|
+
data: {},
|
|
53
|
+
loading: false,
|
|
54
|
+
error: null
|
|
55
|
+
};
|
|
56
|
+
function useDashboardData(dataUrl, refreshInterval) {
|
|
57
|
+
const [state, setState] = useState({
|
|
58
|
+
...EMPTY,
|
|
59
|
+
loading: !!dataUrl
|
|
60
|
+
});
|
|
61
|
+
const refetch = useCallback(async () => {
|
|
62
|
+
if (!dataUrl) {
|
|
63
|
+
setState(EMPTY);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
setState((prev) => ({
|
|
67
|
+
...prev,
|
|
68
|
+
loading: true,
|
|
69
|
+
error: null
|
|
70
|
+
}));
|
|
71
|
+
try {
|
|
72
|
+
const response = await fetch(resolveDataUrl(dataUrl), { credentials: "include" });
|
|
73
|
+
if (!response.ok) throw new Error(`Dashboard request failed (${response.status})`);
|
|
74
|
+
setState({
|
|
75
|
+
data: await response.json(),
|
|
76
|
+
loading: false,
|
|
77
|
+
error: null
|
|
78
|
+
});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
setState({
|
|
81
|
+
data: {},
|
|
82
|
+
loading: false,
|
|
83
|
+
error: error instanceof Error ? error.message : "Failed to load dashboard"
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}, [dataUrl]);
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
refetch();
|
|
89
|
+
}, [refetch]);
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (!refreshInterval || !dataUrl) return void 0;
|
|
92
|
+
const timer = window.setInterval(() => {
|
|
93
|
+
refetch();
|
|
94
|
+
}, refreshInterval);
|
|
95
|
+
return () => window.clearInterval(timer);
|
|
96
|
+
}, [
|
|
97
|
+
dataUrl,
|
|
98
|
+
refreshInterval,
|
|
99
|
+
refetch
|
|
100
|
+
]);
|
|
101
|
+
return {
|
|
102
|
+
...state,
|
|
103
|
+
refetch
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region packages/dashboard/dashboard/formatValue.ts
|
|
108
|
+
function formatDashboardValue(value, format = "text") {
|
|
109
|
+
if (value === null || value === void 0) return "—";
|
|
110
|
+
if (format === "text") return String(value);
|
|
111
|
+
if (format === "date" || format === "datetime") {
|
|
112
|
+
const date = value instanceof Date ? value : new Date(String(value));
|
|
113
|
+
if (Number.isNaN(date.getTime())) return String(value);
|
|
114
|
+
return new Intl.DateTimeFormat(getCoreLocale(), {
|
|
115
|
+
dateStyle: format === "date" ? "medium" : void 0,
|
|
116
|
+
timeStyle: format === "datetime" ? "short" : void 0
|
|
117
|
+
}).format(date);
|
|
118
|
+
}
|
|
119
|
+
const numeric = typeof value === "number" ? value : Number(value);
|
|
120
|
+
if (!Number.isFinite(numeric)) return String(value);
|
|
121
|
+
const baseOptions = {
|
|
122
|
+
minimumFractionDigits: format === "currency" ? 2 : void 0,
|
|
123
|
+
maximumFractionDigits: format === "currency" ? 2 : void 0
|
|
124
|
+
};
|
|
125
|
+
if (format === "currency") {
|
|
126
|
+
const currency = getCoreCurrency();
|
|
127
|
+
if (!currency) return new Intl.NumberFormat(getCoreLocale(), baseOptions).format(numeric);
|
|
128
|
+
return new Intl.NumberFormat(getCoreLocale(), {
|
|
129
|
+
...baseOptions,
|
|
130
|
+
style: "currency",
|
|
131
|
+
currency,
|
|
132
|
+
currencyDisplay: "narrowSymbol"
|
|
133
|
+
}).format(numeric);
|
|
134
|
+
}
|
|
135
|
+
if (format === "percent") {
|
|
136
|
+
const normalized = Math.abs(numeric) <= 1 ? numeric : numeric / 100;
|
|
137
|
+
return new Intl.NumberFormat(getCoreLocale(), {
|
|
138
|
+
style: "percent",
|
|
139
|
+
minimumFractionDigits: 1,
|
|
140
|
+
maximumFractionDigits: 1
|
|
141
|
+
}).format(normalized);
|
|
142
|
+
}
|
|
143
|
+
return new Intl.NumberFormat(getCoreLocale()).format(numeric);
|
|
144
|
+
}
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region packages/dashboard/dashboard/resolvePath.ts
|
|
147
|
+
function resolvePath(source, path) {
|
|
148
|
+
if (path === void 0 || path === "") return source;
|
|
149
|
+
if (source === null || source === void 0) return void 0;
|
|
150
|
+
return path.split(".").reduce((current, segment) => {
|
|
151
|
+
if (current === null || current === void 0) return void 0;
|
|
152
|
+
if (typeof current !== "object") return void 0;
|
|
153
|
+
return current[segment];
|
|
154
|
+
}, source);
|
|
155
|
+
}
|
|
156
|
+
function resolveArray(source, path) {
|
|
157
|
+
const value = resolvePath(source, path);
|
|
158
|
+
if (!Array.isArray(value)) return [];
|
|
159
|
+
return value.filter((item) => item !== null && typeof item === "object");
|
|
160
|
+
}
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region packages/dashboard/dashboard/widgets/BarChartWidgetView.tsx
|
|
163
|
+
function toNumber$1(value) {
|
|
164
|
+
const numeric = typeof value === "number" ? value : Number(value);
|
|
165
|
+
return Number.isFinite(numeric) ? numeric : 0;
|
|
166
|
+
}
|
|
167
|
+
function BarChartWidgetView({ widget, data, loading }) {
|
|
168
|
+
const rows = resolveArray(data, widget.dataPath);
|
|
169
|
+
const max = rows.reduce((peak, row) => Math.max(peak, toNumber$1(row[widget.yKey])), 0) || 1;
|
|
170
|
+
const height = widget.height ?? 200;
|
|
171
|
+
return /* @__PURE__ */ jsx(StatCard, {
|
|
172
|
+
title: widget.title,
|
|
173
|
+
headerExtra: widget.subtitle ? /* @__PURE__ */ jsx("span", {
|
|
174
|
+
className: "nb-dashboard-widget__subtitle",
|
|
175
|
+
children: widget.subtitle
|
|
176
|
+
}) : void 0,
|
|
177
|
+
menuVisible: widget.menuVisible,
|
|
178
|
+
isLoading: loading,
|
|
179
|
+
className: "nb-dashboard-chart-card",
|
|
180
|
+
children: rows.length === 0 ? /* @__PURE__ */ jsx("div", {
|
|
181
|
+
className: "nb-dashboard-chart-empty",
|
|
182
|
+
children: "No data"
|
|
183
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
184
|
+
className: "nb-dashboard-bar-chart",
|
|
185
|
+
style: { height },
|
|
186
|
+
role: "img",
|
|
187
|
+
"aria-label": widget.title,
|
|
188
|
+
children: rows.map((row, index) => {
|
|
189
|
+
const label = String(row[widget.xKey] ?? "");
|
|
190
|
+
const value = toNumber$1(row[widget.yKey]);
|
|
191
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
192
|
+
className: "nb-dashboard-bar-chart__item",
|
|
193
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
194
|
+
className: "nb-dashboard-bar-chart__bar-wrap",
|
|
195
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
196
|
+
className: "nb-dashboard-bar-chart__bar",
|
|
197
|
+
style: { height: `${Math.max(4, value / max * 100)}%` },
|
|
198
|
+
title: `${label}: ${formatDashboardValue(value, widget.valueFormat)}`
|
|
199
|
+
})
|
|
200
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
201
|
+
className: "nb-dashboard-bar-chart__label",
|
|
202
|
+
children: label
|
|
203
|
+
})]
|
|
204
|
+
}, `${label}-${index}`);
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region packages/dashboard/dashboard/widgets/DonutChartWidgetView.tsx
|
|
211
|
+
const DEFAULT_COLORS = [
|
|
212
|
+
"var(--accent-color)",
|
|
213
|
+
"var(--success-color)",
|
|
214
|
+
"var(--warning-color)",
|
|
215
|
+
"var(--info-color)",
|
|
216
|
+
"#6b4fc8",
|
|
217
|
+
"#c2410c"
|
|
218
|
+
];
|
|
219
|
+
function toNumber(value) {
|
|
220
|
+
const numeric = typeof value === "number" ? value : Number(value);
|
|
221
|
+
return Number.isFinite(numeric) ? numeric : 0;
|
|
222
|
+
}
|
|
223
|
+
function DonutChartWidgetView({ widget, data, loading }) {
|
|
224
|
+
const rows = resolveArray(data, widget.dataPath);
|
|
225
|
+
const colors = widget.colors ?? DEFAULT_COLORS;
|
|
226
|
+
const total = rows.reduce((sum, row) => sum + toNumber(row[widget.valueKey]), 0);
|
|
227
|
+
const centerValue = widget.centerValuePath !== void 0 ? resolvePath(data, widget.centerValuePath) : total;
|
|
228
|
+
const radius = 38;
|
|
229
|
+
const stroke = 14;
|
|
230
|
+
const circumference = 2 * Math.PI * radius;
|
|
231
|
+
const arcs = rows.reduce((segments, row, index) => {
|
|
232
|
+
const pct = toNumber(row[widget.valueKey]) / total;
|
|
233
|
+
const offset = segments.reduce((sum, segment) => sum + segment.dash, 0);
|
|
234
|
+
segments.push({
|
|
235
|
+
key: `${String(row[widget.labelKey])}-${index}`,
|
|
236
|
+
dash: pct * circumference,
|
|
237
|
+
offset,
|
|
238
|
+
color: colors[index % colors.length]
|
|
239
|
+
});
|
|
240
|
+
return segments;
|
|
241
|
+
}, []);
|
|
242
|
+
return /* @__PURE__ */ jsx(StatCard, {
|
|
243
|
+
title: widget.title,
|
|
244
|
+
headerExtra: widget.subtitle ? /* @__PURE__ */ jsx("span", {
|
|
245
|
+
className: "nb-dashboard-widget__subtitle",
|
|
246
|
+
children: widget.subtitle
|
|
247
|
+
}) : void 0,
|
|
248
|
+
menuVisible: widget.menuVisible,
|
|
249
|
+
isLoading: loading,
|
|
250
|
+
className: "nb-dashboard-chart-card",
|
|
251
|
+
children: rows.length === 0 || total <= 0 ? /* @__PURE__ */ jsx("div", {
|
|
252
|
+
className: "nb-dashboard-chart-empty",
|
|
253
|
+
children: "No data"
|
|
254
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
255
|
+
className: "nb-dashboard-donut",
|
|
256
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
257
|
+
className: "nb-dashboard-donut__chart",
|
|
258
|
+
role: "img",
|
|
259
|
+
"aria-label": widget.title,
|
|
260
|
+
children: [/* @__PURE__ */ jsxs("svg", {
|
|
261
|
+
viewBox: "0 0 100 100",
|
|
262
|
+
className: "nb-dashboard-donut__svg",
|
|
263
|
+
children: [/* @__PURE__ */ jsx("circle", {
|
|
264
|
+
cx: "50",
|
|
265
|
+
cy: "50",
|
|
266
|
+
r: radius,
|
|
267
|
+
fill: "none",
|
|
268
|
+
stroke: "var(--surface-3)",
|
|
269
|
+
strokeWidth: stroke
|
|
270
|
+
}), arcs.map((arc) => /* @__PURE__ */ jsx("circle", {
|
|
271
|
+
cx: "50",
|
|
272
|
+
cy: "50",
|
|
273
|
+
r: radius,
|
|
274
|
+
fill: "none",
|
|
275
|
+
stroke: arc.color,
|
|
276
|
+
strokeWidth: stroke,
|
|
277
|
+
strokeDasharray: `${arc.dash} ${circumference - arc.dash}`,
|
|
278
|
+
strokeDashoffset: -arc.offset,
|
|
279
|
+
transform: "rotate(-90 50 50)",
|
|
280
|
+
strokeLinecap: "butt"
|
|
281
|
+
}, arc.key))]
|
|
282
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
283
|
+
className: "nb-dashboard-donut__center",
|
|
284
|
+
children: [widget.centerLabel && /* @__PURE__ */ jsx("span", {
|
|
285
|
+
className: "nb-dashboard-donut__center-label",
|
|
286
|
+
children: widget.centerLabel
|
|
287
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
288
|
+
className: "nb-dashboard-donut__center-value",
|
|
289
|
+
children: formatDashboardValue(centerValue, widget.valueFormat)
|
|
290
|
+
})]
|
|
291
|
+
})]
|
|
292
|
+
}), widget.showLegend !== false && /* @__PURE__ */ jsx("ul", {
|
|
293
|
+
className: "nb-dashboard-donut__legend",
|
|
294
|
+
children: rows.map((row, index) => {
|
|
295
|
+
const label = String(row[widget.labelKey] ?? "");
|
|
296
|
+
const value = toNumber(row[widget.valueKey]);
|
|
297
|
+
const pct = total > 0 ? value / total * 100 : 0;
|
|
298
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
299
|
+
className: "nb-dashboard-donut__legend-item",
|
|
300
|
+
children: [
|
|
301
|
+
/* @__PURE__ */ jsx("span", {
|
|
302
|
+
className: "nb-dashboard-donut__legend-swatch",
|
|
303
|
+
style: { background: colors[index % colors.length] }
|
|
304
|
+
}),
|
|
305
|
+
/* @__PURE__ */ jsx("span", {
|
|
306
|
+
className: "nb-dashboard-donut__legend-label",
|
|
307
|
+
children: label
|
|
308
|
+
}),
|
|
309
|
+
/* @__PURE__ */ jsxs("span", {
|
|
310
|
+
className: "nb-dashboard-donut__legend-value",
|
|
311
|
+
children: [pct.toFixed(0), "%"]
|
|
312
|
+
})
|
|
313
|
+
]
|
|
314
|
+
}, `${label}-${index}`);
|
|
315
|
+
})
|
|
316
|
+
})]
|
|
317
|
+
})
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
//#endregion
|
|
321
|
+
//#region packages/dashboard/dashboard/widgets/StatWidgetView.tsx
|
|
322
|
+
function resolveTrend(widget, data) {
|
|
323
|
+
if (!widget.trend) return { value: null };
|
|
324
|
+
const raw = widget.trend.valuePath !== void 0 ? resolvePath(data, widget.trend.valuePath) : widget.trend.value;
|
|
325
|
+
const numeric = typeof raw === "number" ? raw : Number(raw);
|
|
326
|
+
return {
|
|
327
|
+
value: Number.isFinite(numeric) ? numeric : null,
|
|
328
|
+
label: widget.trend.label,
|
|
329
|
+
invert: widget.trend.invertColors
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function StatWidgetView({ widget, data, loading }) {
|
|
333
|
+
const valueText = formatDashboardValue(widget.valuePath !== void 0 ? resolvePath(data, widget.valuePath) : widget.value, widget.format ?? "text");
|
|
334
|
+
const trend = resolveTrend(widget, data);
|
|
335
|
+
const trendDirection = trend.value === null || trend.value === 0 ? "flat" : trend.value > 0 ? "up" : "down";
|
|
336
|
+
const trendClass = trendDirection === "flat" ? "nb-stat-card__delta--flat" : trendDirection === "up" ? trend.invert ? "nb-stat-card__delta--down" : "nb-stat-card__delta--up" : trend.invert ? "nb-stat-card__delta--up" : "nb-stat-card__delta--down";
|
|
337
|
+
const trendArrow = trendDirection === "up" ? "▲" : trendDirection === "down" ? "▼" : "—";
|
|
338
|
+
const trendText = trend.value === null ? null : `${trendArrow} ${formatDashboardValue(Math.abs(trend.value), "percent")}${trend.label ? ` ${trend.label}` : ""}`;
|
|
339
|
+
return /* @__PURE__ */ jsx(StatCard, {
|
|
340
|
+
title: widget.title,
|
|
341
|
+
menuVisible: widget.menuVisible,
|
|
342
|
+
isLoading: loading,
|
|
343
|
+
className: `nb-dashboard-stat nb-dashboard-stat--${widget.iconTone ?? "accent"}`,
|
|
344
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
345
|
+
className: "nb-dashboard-stat__content",
|
|
346
|
+
children: [widget.icon && /* @__PURE__ */ jsx("span", {
|
|
347
|
+
className: `nb-dashboard-stat__icon nb-dashboard-stat__icon--${widget.iconTone ?? "accent"}`,
|
|
348
|
+
children: /* @__PURE__ */ jsx("i", {
|
|
349
|
+
className: `ph ${widget.icon}`,
|
|
350
|
+
"aria-hidden": "true"
|
|
351
|
+
})
|
|
352
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
353
|
+
className: "nb-dashboard-stat__metrics",
|
|
354
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
355
|
+
className: "nb-stat-card__value nb-stat-card__value--2xl",
|
|
356
|
+
children: valueText
|
|
357
|
+
}), trendText && /* @__PURE__ */ jsx("span", {
|
|
358
|
+
className: `nb-stat-card__delta ${trendClass}`,
|
|
359
|
+
children: trendText
|
|
360
|
+
})]
|
|
361
|
+
})]
|
|
362
|
+
})
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
//#endregion
|
|
366
|
+
//#region packages/dashboard/dashboard/widgets/TableWidgetView.tsx
|
|
367
|
+
function renderCell(row, column) {
|
|
368
|
+
const raw = row[column.key];
|
|
369
|
+
if (column.badge) {
|
|
370
|
+
const key = raw == null ? "" : String(raw);
|
|
371
|
+
return /* @__PURE__ */ jsx(Badge, {
|
|
372
|
+
variant: column.badge[key] ?? "secondary",
|
|
373
|
+
size: "sm",
|
|
374
|
+
children: column.badgeLabels?.[key] ?? key.replace(/_/g, " ")
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return formatDashboardValue(raw, column.format ?? "text");
|
|
378
|
+
}
|
|
379
|
+
function TableWidgetView({ widget, data, loading }) {
|
|
380
|
+
const rows = resolveArray(data, widget.dataPath);
|
|
381
|
+
return /* @__PURE__ */ jsx(StatCard, {
|
|
382
|
+
title: widget.title,
|
|
383
|
+
headerExtra: /* @__PURE__ */ jsxs("div", {
|
|
384
|
+
className: "nb-dashboard-table__header-extra",
|
|
385
|
+
children: [widget.subtitle && /* @__PURE__ */ jsx("span", {
|
|
386
|
+
className: "nb-dashboard-widget__subtitle",
|
|
387
|
+
children: widget.subtitle
|
|
388
|
+
}), widget.viewAll && /* @__PURE__ */ jsx(Link, {
|
|
389
|
+
to: widget.viewAll.to,
|
|
390
|
+
className: "nb-dashboard-table__view-all",
|
|
391
|
+
children: widget.viewAll.label ?? "View all"
|
|
392
|
+
})]
|
|
393
|
+
}),
|
|
394
|
+
menuVisible: widget.menuVisible,
|
|
395
|
+
isLoading: loading,
|
|
396
|
+
className: "nb-dashboard-table-card",
|
|
397
|
+
children: rows.length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
|
|
398
|
+
size: "sm",
|
|
399
|
+
icon: "ph ph-table",
|
|
400
|
+
title: widget.emptyTitle ?? "No records yet",
|
|
401
|
+
description: widget.emptyDescription
|
|
402
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
403
|
+
className: "nb-dashboard-table-wrap",
|
|
404
|
+
children: /* @__PURE__ */ jsxs("table", {
|
|
405
|
+
className: "nb-dashboard-table",
|
|
406
|
+
children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: widget.columns.map((column) => /* @__PURE__ */ jsx("th", {
|
|
407
|
+
className: column.align ? `nb-dashboard-table__cell--${column.align}` : void 0,
|
|
408
|
+
children: column.label
|
|
409
|
+
}, column.key)) }) }), /* @__PURE__ */ jsx("tbody", { children: rows.map((row, rowIndex) => /* @__PURE__ */ jsx("tr", { children: widget.columns.map((column) => /* @__PURE__ */ jsx("td", {
|
|
410
|
+
className: column.align ? `nb-dashboard-table__cell--${column.align}` : void 0,
|
|
411
|
+
children: renderCell(row, column)
|
|
412
|
+
}, column.key)) }, rowIndex)) })]
|
|
413
|
+
})
|
|
414
|
+
})
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
//#endregion
|
|
418
|
+
//#region packages/dashboard/dashboard/widgets/WidgetRenderer.tsx
|
|
419
|
+
function WidgetRenderer({ widget, data, loading }) {
|
|
420
|
+
switch (widget.type) {
|
|
421
|
+
case "stat": return /* @__PURE__ */ jsx(StatWidgetView, {
|
|
422
|
+
widget,
|
|
423
|
+
data,
|
|
424
|
+
loading
|
|
425
|
+
});
|
|
426
|
+
case "bar-chart": return /* @__PURE__ */ jsx(BarChartWidgetView, {
|
|
427
|
+
widget,
|
|
428
|
+
data,
|
|
429
|
+
loading
|
|
430
|
+
});
|
|
431
|
+
case "donut-chart": return /* @__PURE__ */ jsx(DonutChartWidgetView, {
|
|
432
|
+
widget,
|
|
433
|
+
data,
|
|
434
|
+
loading
|
|
435
|
+
});
|
|
436
|
+
case "table": return /* @__PURE__ */ jsx(TableWidgetView, {
|
|
437
|
+
widget,
|
|
438
|
+
data,
|
|
439
|
+
loading
|
|
440
|
+
});
|
|
441
|
+
default: return null;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
//#endregion
|
|
445
|
+
//#region packages/dashboard/dashboard/DashboardPage.tsx
|
|
446
|
+
function sectionClassName(section) {
|
|
447
|
+
const layout = section.layout ?? "grid";
|
|
448
|
+
if (layout === "grid") return (typeof section.columns === "number" ? `repeat(${section.columns}, minmax(0, 1fr))` : section.columns) ? "nb-dashboard-section nb-dashboard-section--grid" : "nb-dashboard-section nb-dashboard-section--grid";
|
|
449
|
+
return `nb-dashboard-section nb-dashboard-section--${layout}`;
|
|
450
|
+
}
|
|
451
|
+
function sectionStyle(section) {
|
|
452
|
+
if (section.layout !== "grid" && section.layout !== void 0) return void 0;
|
|
453
|
+
const columns = typeof section.columns === "number" ? `repeat(${section.columns}, minmax(0, 1fr))` : section.columns;
|
|
454
|
+
if (!columns) return void 0;
|
|
455
|
+
return { gridTemplateColumns: String(columns) };
|
|
456
|
+
}
|
|
457
|
+
function DashboardSectionView({ section, data, loading }) {
|
|
458
|
+
return /* @__PURE__ */ jsx("section", {
|
|
459
|
+
className: sectionClassName(section),
|
|
460
|
+
style: sectionStyle(section),
|
|
461
|
+
"data-section": section.id,
|
|
462
|
+
children: section.widgets.map((widget) => /* @__PURE__ */ jsx(WidgetRenderer, {
|
|
463
|
+
widget,
|
|
464
|
+
data,
|
|
465
|
+
loading
|
|
466
|
+
}, widget.id))
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
function DashboardPage({ config }) {
|
|
470
|
+
const fetched = useDashboardData(config.dataUrl, config.refreshInterval);
|
|
471
|
+
const { data, loading, error, refetch } = config.useData?.() ?? fetched;
|
|
472
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
473
|
+
className: "view-wrapper-scroll nb-dashboard-page",
|
|
474
|
+
children: [/* @__PURE__ */ jsxs(AppToolbar, {
|
|
475
|
+
title: config.title,
|
|
476
|
+
onRefresh: refetch,
|
|
477
|
+
children: [error && /* @__PURE__ */ jsx("div", {
|
|
478
|
+
className: "nb-dashboard-page__error",
|
|
479
|
+
children: error
|
|
480
|
+
}), config.sections.map((section, index) => /* @__PURE__ */ jsx(DashboardSectionView, {
|
|
481
|
+
section,
|
|
482
|
+
data,
|
|
483
|
+
loading
|
|
484
|
+
}, section.id ?? `section-${index}`))]
|
|
485
|
+
}), loading && /* @__PURE__ */ jsx("div", {
|
|
486
|
+
className: "nb-dashboard-page__loading",
|
|
487
|
+
"aria-hidden": "true",
|
|
488
|
+
children: /* @__PURE__ */ jsx("span", { className: "nb-dashboard-page__spinner" })
|
|
489
|
+
})]
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
//#endregion
|
|
493
|
+
//#region packages/dashboard/dashboard/widgetBuilders.ts
|
|
494
|
+
function statWidget(config) {
|
|
495
|
+
return {
|
|
496
|
+
type: "stat",
|
|
497
|
+
menuVisible: false,
|
|
498
|
+
...config
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function barChartWidget(config) {
|
|
502
|
+
return {
|
|
503
|
+
type: "bar-chart",
|
|
504
|
+
menuVisible: false,
|
|
505
|
+
valueFormat: "currency",
|
|
506
|
+
height: 200,
|
|
507
|
+
...config
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
function donutChartWidget(config) {
|
|
511
|
+
return {
|
|
512
|
+
type: "donut-chart",
|
|
513
|
+
menuVisible: false,
|
|
514
|
+
showLegend: true,
|
|
515
|
+
valueFormat: "currency",
|
|
516
|
+
...config
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function tableWidget(config) {
|
|
520
|
+
return {
|
|
521
|
+
type: "table",
|
|
522
|
+
menuVisible: false,
|
|
523
|
+
...config
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
//#endregion
|
|
527
|
+
export { DashboardPage, barChartWidget, defineDashboard, donutChartWidget, formatDashboardValue, resolveArray, resolvePath, statWidget, tableWidget, useDashboardData };
|