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