@adminforth/dashboard 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -54
- package/custom/api/dashboardApi.ts +9 -0
- package/custom/model/dashboard.types.ts +353 -2
- package/custom/queries/useWidgetData.ts +8 -4
- package/custom/runtime/DashboardRuntime.vue +2 -1
- package/custom/runtime/WidgetRenderer.vue +2 -1
- package/custom/runtime/WidgetShell.vue +8 -4
- package/custom/skills/adminforth-dashboard/SKILL.md +4 -4
- package/custom/widgets/chart/ChartWidget.vue +45 -12
- package/custom/widgets/chart/chart.types.ts +83 -0
- package/custom/widgets/chart/chart.utils.ts +2 -2
- package/custom/widgets/gauge-card/GaugeCardWidget.vue +63 -12
- package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
- package/custom/widgets/pivot-table/PivotTableWidget.vue +32 -12
- package/custom/widgets/table/TableWidget.vue +155 -30
- package/dist/custom/api/dashboardApi.d.ts +7 -1
- package/dist/custom/api/dashboardApi.js +4 -6
- package/dist/custom/api/dashboardApi.ts +9 -0
- package/dist/custom/model/dashboard.types.d.ts +70 -1
- package/dist/custom/model/dashboard.types.js +173 -1
- package/dist/custom/model/dashboard.types.ts +353 -2
- package/dist/custom/queries/useDashboardConfig.d.ts +42 -2
- package/dist/custom/queries/useWidgetData.d.ts +44 -3
- package/dist/custom/queries/useWidgetData.js +3 -3
- package/dist/custom/queries/useWidgetData.ts +8 -4
- package/dist/custom/runtime/DashboardRuntime.vue +2 -1
- package/dist/custom/runtime/WidgetRenderer.vue +2 -1
- package/dist/custom/runtime/WidgetShell.vue +8 -4
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +4 -4
- package/dist/custom/widgets/chart/ChartWidget.vue +45 -12
- package/dist/custom/widgets/chart/chart.types.d.ts +15 -0
- package/dist/custom/widgets/chart/chart.types.js +46 -0
- package/dist/custom/widgets/chart/chart.types.ts +83 -0
- package/dist/custom/widgets/chart/chart.utils.d.ts +1 -1
- package/dist/custom/widgets/chart/chart.utils.js +2 -2
- package/dist/custom/widgets/chart/chart.utils.ts +2 -2
- package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +63 -12
- package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
- package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +32 -12
- package/dist/custom/widgets/table/TableWidget.vue +155 -30
- package/dist/endpoint/widgets.d.ts +6 -1
- package/dist/endpoint/widgets.js +41 -6
- package/dist/schema/api.d.ts +874 -444
- package/dist/schema/api.js +11 -2
- package/dist/schema/widget.d.ts +538 -132
- package/dist/schema/widget.js +138 -14
- package/dist/services/widgetConfigValidator.js +26 -40
- package/dist/services/widgetDataService.d.ts +7 -14
- package/dist/services/widgetDataService.js +115 -11
- package/endpoint/widgets.ts +56 -6
- package/package.json +1 -1
- package/schema/api.ts +11 -1
- package/schema/widget.ts +145 -15
- package/services/widgetConfigValidator.ts +36 -44
- package/services/widgetDataService.ts +175 -28
package/dist/schema/widget.js
CHANGED
|
@@ -6,6 +6,56 @@ const DashboardWidgetSizeSchema = z.enum([
|
|
|
6
6
|
'wide',
|
|
7
7
|
'full',
|
|
8
8
|
]);
|
|
9
|
+
export const AggregationOperationZodSchema = z.enum([
|
|
10
|
+
'sum',
|
|
11
|
+
'count',
|
|
12
|
+
'avg',
|
|
13
|
+
'min',
|
|
14
|
+
'max',
|
|
15
|
+
'median',
|
|
16
|
+
]);
|
|
17
|
+
export const AggregationRuleZodSchema = z.object({
|
|
18
|
+
operation: AggregationOperationZodSchema,
|
|
19
|
+
field: z.string().optional(),
|
|
20
|
+
}).superRefine((rule, ctx) => {
|
|
21
|
+
if (rule.operation !== 'count' && !rule.field) {
|
|
22
|
+
ctx.addIssue({
|
|
23
|
+
code: z.ZodIssueCode.custom,
|
|
24
|
+
path: ['field'],
|
|
25
|
+
message: `field is required for ${rule.operation}`,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
export const GroupByRuleZodSchema = z.discriminatedUnion('type', [
|
|
30
|
+
z.object({
|
|
31
|
+
type: z.literal('field'),
|
|
32
|
+
field: z.string(),
|
|
33
|
+
}),
|
|
34
|
+
z.object({
|
|
35
|
+
type: z.literal('date_trunc'),
|
|
36
|
+
field: z.string(),
|
|
37
|
+
truncation: z.enum(['day', 'week', 'month', 'year']),
|
|
38
|
+
timezone: z.string().optional(),
|
|
39
|
+
}),
|
|
40
|
+
]);
|
|
41
|
+
export const AggregateDataSourceZodSchema = z.object({
|
|
42
|
+
type: z.literal('aggregate'),
|
|
43
|
+
resourceId: z.string(),
|
|
44
|
+
aggregations: z.record(z.string(), AggregationRuleZodSchema),
|
|
45
|
+
groupBy: GroupByRuleZodSchema.optional(),
|
|
46
|
+
filters: z.unknown().optional(),
|
|
47
|
+
}).strict();
|
|
48
|
+
export const ResourceDataSourceZodSchema = z.object({
|
|
49
|
+
type: z.literal('resource'),
|
|
50
|
+
resourceId: z.string(),
|
|
51
|
+
columns: z.array(z.string()).optional(),
|
|
52
|
+
filters: z.unknown().optional(),
|
|
53
|
+
sort: z.unknown().optional(),
|
|
54
|
+
}).strict();
|
|
55
|
+
export const WidgetDataSourceZodSchema = z.discriminatedUnion('type', [
|
|
56
|
+
ResourceDataSourceZodSchema,
|
|
57
|
+
AggregateDataSourceZodSchema,
|
|
58
|
+
]);
|
|
9
59
|
const WidgetBaseSchema = z.object({
|
|
10
60
|
id: z.string().optional(),
|
|
11
61
|
group_id: z.string().optional(),
|
|
@@ -16,6 +66,7 @@ const WidgetBaseSchema = z.object({
|
|
|
16
66
|
minWidth: z.number().nonnegative('Min width must be a non-negative number').optional(),
|
|
17
67
|
maxWidth: z.number().nonnegative('Max width must be a non-negative number').nullable().optional(),
|
|
18
68
|
order: z.number().optional(),
|
|
69
|
+
dataSource: WidgetDataSourceZodSchema.optional(),
|
|
19
70
|
});
|
|
20
71
|
const ChartBaseSchema = z.object({
|
|
21
72
|
title: z.string().optional(),
|
|
@@ -79,42 +130,115 @@ export const ChartConfigSchema = z.discriminatedUnion('type', [
|
|
|
79
130
|
HistogramChartSchema,
|
|
80
131
|
FunnelChartSchema,
|
|
81
132
|
]);
|
|
82
|
-
export const DashboardWidgetQuerySchema = z.object({
|
|
83
|
-
resource: z.string().min(1, 'Query resource must be a non-empty string'),
|
|
84
|
-
select: z.array(z.string()).optional(),
|
|
85
|
-
order: z.object({
|
|
86
|
-
field: z.string().min(1, 'Order field is required'),
|
|
87
|
-
direction: z.enum(['asc', 'desc']),
|
|
88
|
-
}).optional(),
|
|
89
|
-
limit: z.number().optional(),
|
|
90
|
-
});
|
|
91
133
|
export const EmptyWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
92
134
|
target: z.literal('empty'),
|
|
93
135
|
});
|
|
94
136
|
const TableWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
95
137
|
target: z.literal('table'),
|
|
96
138
|
table: z.unknown().optional(),
|
|
97
|
-
|
|
139
|
+
}).superRefine((widget, ctx) => {
|
|
140
|
+
var _a;
|
|
141
|
+
if (!widget.dataSource) {
|
|
142
|
+
ctx.addIssue({
|
|
143
|
+
code: z.ZodIssueCode.custom,
|
|
144
|
+
path: ['dataSource'],
|
|
145
|
+
message: 'Table widget must have dataSource config',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (((_a = widget.dataSource) === null || _a === void 0 ? void 0 : _a.type) === 'aggregate') {
|
|
149
|
+
ctx.addIssue({
|
|
150
|
+
code: z.ZodIssueCode.custom,
|
|
151
|
+
path: ['dataSource'],
|
|
152
|
+
message: 'Table widget dataSource must use resource type',
|
|
153
|
+
});
|
|
154
|
+
}
|
|
98
155
|
});
|
|
99
156
|
const ChartWidgetTargetConfigSchema = WidgetBaseSchema.extend({
|
|
100
157
|
target: z.literal('chart'),
|
|
101
158
|
chart: ChartConfigSchema,
|
|
102
|
-
|
|
159
|
+
}).superRefine((widget, ctx) => {
|
|
160
|
+
var _a, _b;
|
|
161
|
+
if (!widget.dataSource) {
|
|
162
|
+
ctx.addIssue({
|
|
163
|
+
code: z.ZodIssueCode.custom,
|
|
164
|
+
path: ['dataSource'],
|
|
165
|
+
message: 'Chart widget must have dataSource config',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if (((_a = widget.dataSource) === null || _a === void 0 ? void 0 : _a.type) === 'resource') {
|
|
169
|
+
ctx.addIssue({
|
|
170
|
+
code: z.ZodIssueCode.custom,
|
|
171
|
+
path: ['dataSource'],
|
|
172
|
+
message: 'Chart widget dataSource must use aggregate type',
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (((_b = widget.dataSource) === null || _b === void 0 ? void 0 : _b.type) === 'aggregate' && !widget.dataSource.groupBy) {
|
|
176
|
+
ctx.addIssue({
|
|
177
|
+
code: z.ZodIssueCode.custom,
|
|
178
|
+
path: ['dataSource', 'groupBy'],
|
|
179
|
+
message: 'Chart widget aggregate dataSource must define groupBy',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
103
182
|
});
|
|
104
183
|
const KpiCardWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
105
184
|
target: z.literal('kpi_card'),
|
|
106
185
|
kpi_card: z.unknown().optional(),
|
|
107
|
-
|
|
186
|
+
}).superRefine((widget, ctx) => {
|
|
187
|
+
var _a;
|
|
188
|
+
if (!widget.dataSource) {
|
|
189
|
+
ctx.addIssue({
|
|
190
|
+
code: z.ZodIssueCode.custom,
|
|
191
|
+
path: ['dataSource'],
|
|
192
|
+
message: 'KPI card widget must have dataSource config',
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if (((_a = widget.dataSource) === null || _a === void 0 ? void 0 : _a.type) === 'aggregate' && widget.dataSource.groupBy) {
|
|
196
|
+
ctx.addIssue({
|
|
197
|
+
code: z.ZodIssueCode.custom,
|
|
198
|
+
path: ['dataSource', 'groupBy'],
|
|
199
|
+
message: 'KPI card aggregate dataSource must not define groupBy',
|
|
200
|
+
});
|
|
201
|
+
}
|
|
108
202
|
});
|
|
109
203
|
const GaugeCardWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
110
204
|
target: z.literal('gauge_card'),
|
|
111
205
|
gauge_card: z.unknown().optional(),
|
|
112
|
-
|
|
206
|
+
}).superRefine((widget, ctx) => {
|
|
207
|
+
var _a;
|
|
208
|
+
if (!widget.dataSource) {
|
|
209
|
+
ctx.addIssue({
|
|
210
|
+
code: z.ZodIssueCode.custom,
|
|
211
|
+
path: ['dataSource'],
|
|
212
|
+
message: 'Gauge card widget must have dataSource config',
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (((_a = widget.dataSource) === null || _a === void 0 ? void 0 : _a.type) === 'aggregate' && widget.dataSource.groupBy) {
|
|
216
|
+
ctx.addIssue({
|
|
217
|
+
code: z.ZodIssueCode.custom,
|
|
218
|
+
path: ['dataSource', 'groupBy'],
|
|
219
|
+
message: 'Gauge card aggregate dataSource must not define groupBy',
|
|
220
|
+
});
|
|
221
|
+
}
|
|
113
222
|
});
|
|
114
223
|
const PivotTableWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
115
224
|
target: z.literal('pivot_table'),
|
|
116
225
|
pivot_table: z.unknown().optional(),
|
|
117
|
-
|
|
226
|
+
}).superRefine((widget, ctx) => {
|
|
227
|
+
var _a;
|
|
228
|
+
if (!widget.dataSource) {
|
|
229
|
+
ctx.addIssue({
|
|
230
|
+
code: z.ZodIssueCode.custom,
|
|
231
|
+
path: ['dataSource'],
|
|
232
|
+
message: 'Pivot table widget must have dataSource config',
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
if (((_a = widget.dataSource) === null || _a === void 0 ? void 0 : _a.type) === 'aggregate' && !widget.dataSource.groupBy) {
|
|
236
|
+
ctx.addIssue({
|
|
237
|
+
code: z.ZodIssueCode.custom,
|
|
238
|
+
path: ['dataSource', 'groupBy'],
|
|
239
|
+
message: 'Pivot table aggregate dataSource must define groupBy',
|
|
240
|
+
});
|
|
241
|
+
}
|
|
118
242
|
});
|
|
119
243
|
export const WidgetConfigSchema = z.discriminatedUnion('target', [
|
|
120
244
|
TableWidgetConfigSchema,
|
|
@@ -1,61 +1,38 @@
|
|
|
1
|
+
import { normalizeChartWidgetConfig } from '../custom/widgets/chart/chart.types.js';
|
|
1
2
|
export function validateDashboardWidgetApiConfig(adminforth, widget) {
|
|
2
|
-
var _a, _b;
|
|
3
3
|
if (widget.target !== 'chart') {
|
|
4
4
|
return [];
|
|
5
5
|
}
|
|
6
6
|
const errors = [];
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
field: 'query',
|
|
10
|
-
message: 'Chart widget must have query config',
|
|
11
|
-
});
|
|
12
|
-
return errors;
|
|
13
|
-
}
|
|
14
|
-
if (!widget.chart) {
|
|
7
|
+
const chart = normalizeChartWidgetConfig(widget.chart);
|
|
8
|
+
if (!chart) {
|
|
15
9
|
errors.push({
|
|
16
10
|
field: 'chart',
|
|
17
11
|
message: 'Chart widget must have chart config',
|
|
18
12
|
});
|
|
19
13
|
return errors;
|
|
20
14
|
}
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
errors.push({
|
|
26
|
-
field: 'query.resource',
|
|
27
|
-
message: `Resource "${query.resource}" is not registered`,
|
|
28
|
-
});
|
|
29
|
-
return errors;
|
|
30
|
-
}
|
|
31
|
-
if (!query.select) {
|
|
32
|
-
return errors;
|
|
33
|
-
}
|
|
34
|
-
const resourceFields = resource.columns.map((column) => column.name);
|
|
35
|
-
for (const field of query.select) {
|
|
36
|
-
if (!resourceFields.includes(field)) {
|
|
15
|
+
const aggregateDataSource = getAggregateDataSource(widget.dataSource);
|
|
16
|
+
if (aggregateDataSource) {
|
|
17
|
+
const resource = adminforth.config.resources.find((item) => item.resourceId === aggregateDataSource.resourceId);
|
|
18
|
+
if (!resource) {
|
|
37
19
|
errors.push({
|
|
38
|
-
field: '
|
|
39
|
-
message: `
|
|
20
|
+
field: 'data_source.resource_id',
|
|
21
|
+
message: `Resource "${aggregateDataSource.resourceId}" is not registered`,
|
|
40
22
|
});
|
|
41
23
|
}
|
|
42
|
-
|
|
43
|
-
const chartFields = [
|
|
44
|
-
chart.x_field,
|
|
45
|
-
chart.y_field,
|
|
46
|
-
chart.label_field,
|
|
47
|
-
chart.value_field,
|
|
48
|
-
chart.bucket_field,
|
|
49
|
-
...((_b = (_a = chart.series) === null || _a === void 0 ? void 0 : _a.map((series) => series.field)) !== null && _b !== void 0 ? _b : []),
|
|
50
|
-
].filter((field) => typeof field === 'string');
|
|
51
|
-
for (const field of chartFields) {
|
|
52
|
-
if (!query.select.includes(field)) {
|
|
24
|
+
if (!aggregateDataSource.groupBy) {
|
|
53
25
|
errors.push({
|
|
54
|
-
field: '
|
|
55
|
-
message:
|
|
26
|
+
field: 'data_source.group_by',
|
|
27
|
+
message: 'Chart aggregate dataSource must define groupBy',
|
|
56
28
|
});
|
|
57
29
|
}
|
|
30
|
+
return errors;
|
|
58
31
|
}
|
|
32
|
+
errors.push({
|
|
33
|
+
field: 'data_source',
|
|
34
|
+
message: 'Chart widget must have aggregate dataSource config',
|
|
35
|
+
});
|
|
59
36
|
return errors;
|
|
60
37
|
}
|
|
61
38
|
export function createWidgetConfigValidatorService(adminforth) {
|
|
@@ -63,3 +40,12 @@ export function createWidgetConfigValidatorService(adminforth) {
|
|
|
63
40
|
validateDashboardWidgetApiConfig: (widget) => validateDashboardWidgetApiConfig(adminforth, widget),
|
|
64
41
|
};
|
|
65
42
|
}
|
|
43
|
+
function getAggregateDataSource(dataSource) {
|
|
44
|
+
if (typeof dataSource !== 'object'
|
|
45
|
+
|| dataSource === null
|
|
46
|
+
|| dataSource.type !== 'aggregate'
|
|
47
|
+
|| typeof dataSource.resourceId !== 'string') {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return dataSource;
|
|
51
|
+
}
|
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
import type { IAdminForth } from 'adminforth';
|
|
2
|
-
import type { DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
|
|
3
|
-
export type
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
field: string;
|
|
8
|
-
direction: 'asc' | 'desc';
|
|
2
|
+
import type { DashboardWidgetConfig, DashboardWidgetData } from '../custom/model/dashboard.types.js';
|
|
3
|
+
export type DashboardWidgetDataOptions = {
|
|
4
|
+
pagination?: {
|
|
5
|
+
page: number;
|
|
6
|
+
pageSize: number;
|
|
9
7
|
};
|
|
10
|
-
limit?: number;
|
|
11
|
-
};
|
|
12
|
-
export type DashboardWidgetData = {
|
|
13
|
-
columns: string[];
|
|
14
|
-
rows: Record<string, unknown>[];
|
|
15
8
|
};
|
|
16
9
|
export type WidgetDataService = {
|
|
17
|
-
getWidgetData: (widget: DashboardWidgetConfig) => Promise<DashboardWidgetData | null>;
|
|
10
|
+
getWidgetData: (widget: DashboardWidgetConfig, options?: DashboardWidgetDataOptions) => Promise<DashboardWidgetData | null>;
|
|
18
11
|
};
|
|
19
|
-
export declare function getWidgetData(adminforth: IAdminForth, widget: DashboardWidgetConfig): Promise<DashboardWidgetData | null>;
|
|
12
|
+
export declare function getWidgetData(adminforth: IAdminForth, widget: DashboardWidgetConfig, options?: DashboardWidgetDataOptions): Promise<DashboardWidgetData | null>;
|
|
20
13
|
export declare function createWidgetDataService(adminforth: IAdminForth): WidgetDataService;
|
|
@@ -7,26 +7,130 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
import { Sorts } from 'adminforth';
|
|
11
|
-
export function getWidgetData(
|
|
10
|
+
import { Aggregates, GroupBy, Sorts } from 'adminforth';
|
|
11
|
+
export function getWidgetData(adminforth_1, widget_1) {
|
|
12
|
+
return __awaiter(this, arguments, void 0, function* (adminforth, widget, options = {}) {
|
|
13
|
+
const dataSource = getWidgetDataSource(widget.dataSource);
|
|
14
|
+
if (!dataSource) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
if (dataSource.type === 'aggregate') {
|
|
18
|
+
return getAggregateWidgetData(adminforth, dataSource);
|
|
19
|
+
}
|
|
20
|
+
return getResourceWidgetData(adminforth, dataSource, options);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function getResourceWidgetData(adminforth, dataSource, options) {
|
|
12
24
|
return __awaiter(this, void 0, void 0, function* () {
|
|
13
25
|
var _a, _b;
|
|
14
|
-
|
|
15
|
-
|
|
26
|
+
const resource = adminforth.resource(dataSource.resourceId);
|
|
27
|
+
const filters = normalizeFilters(dataSource.filters);
|
|
28
|
+
const sort = normalizeSort(dataSource.sort);
|
|
29
|
+
const pagination = options.pagination;
|
|
30
|
+
const offset = pagination ? (pagination.page - 1) * pagination.pageSize : 0;
|
|
31
|
+
const limit = pagination ? pagination.pageSize : undefined;
|
|
32
|
+
const rows = yield resource.list(filters, limit !== null && limit !== void 0 ? limit : undefined, offset, sort);
|
|
33
|
+
const columns = (_a = dataSource.columns) !== null && _a !== void 0 ? _a : Object.keys((_b = rows[0]) !== null && _b !== void 0 ? _b : {});
|
|
34
|
+
const total = pagination ? yield resource.count(filters) : 0;
|
|
35
|
+
return Object.assign({ columns, rows: rows.map((row) => (Object.fromEntries(columns.map((column) => [column, row[column]])))) }, (pagination ? {
|
|
36
|
+
pagination: {
|
|
37
|
+
page: pagination.page,
|
|
38
|
+
pageSize: pagination.pageSize,
|
|
39
|
+
total,
|
|
40
|
+
totalPages: Math.max(Math.ceil(total / pagination.pageSize), 1),
|
|
41
|
+
},
|
|
42
|
+
} : {}));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function getAggregateWidgetData(adminforth, dataSource) {
|
|
46
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
47
|
+
var _a, _b;
|
|
48
|
+
const resource = adminforth.resource(dataSource.resourceId);
|
|
49
|
+
const rows = yield resource.aggregate(normalizeFilters(dataSource.filters), Object.fromEntries(Object.entries(dataSource.aggregations).map(([alias, rule]) => [
|
|
50
|
+
alias,
|
|
51
|
+
createAggregationRule(rule),
|
|
52
|
+
])), dataSource.groupBy ? createGroupByRule(dataSource.groupBy) : undefined);
|
|
53
|
+
const columns = Object.keys((_a = rows[0]) !== null && _a !== void 0 ? _a : {});
|
|
54
|
+
if (!dataSource.groupBy) {
|
|
55
|
+
const values = (_b = rows[0]) !== null && _b !== void 0 ? _b : {};
|
|
56
|
+
return {
|
|
57
|
+
kind: 'aggregate',
|
|
58
|
+
columns: Object.keys(values),
|
|
59
|
+
rows: Object.keys(values).length ? [values] : [],
|
|
60
|
+
values,
|
|
61
|
+
};
|
|
16
62
|
}
|
|
17
|
-
const query = widget.query;
|
|
18
|
-
const rows = yield adminforth.resource(query.resource).list([], query.limit, 0, query.order
|
|
19
|
-
? [query.order.direction === 'desc' ? Sorts.DESC(query.order.field) : Sorts.ASC(query.order.field)]
|
|
20
|
-
: undefined);
|
|
21
|
-
const columns = (_a = query.select) !== null && _a !== void 0 ? _a : Object.keys((_b = rows[0]) !== null && _b !== void 0 ? _b : {});
|
|
22
63
|
return {
|
|
64
|
+
kind: 'aggregate',
|
|
23
65
|
columns,
|
|
24
|
-
rows
|
|
66
|
+
rows,
|
|
25
67
|
};
|
|
26
68
|
});
|
|
27
69
|
}
|
|
70
|
+
function getWidgetDataSource(dataSource) {
|
|
71
|
+
if (isWidgetDataSource(dataSource)) {
|
|
72
|
+
return dataSource;
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
function isWidgetDataSource(value) {
|
|
77
|
+
return isRecord(value)
|
|
78
|
+
&& (value.type === 'resource' || value.type === 'aggregate')
|
|
79
|
+
&& typeof value.resourceId === 'string';
|
|
80
|
+
}
|
|
81
|
+
function normalizeFilters(filters) {
|
|
82
|
+
if (Array.isArray(filters)) {
|
|
83
|
+
return filters;
|
|
84
|
+
}
|
|
85
|
+
if (isRecord(filters)) {
|
|
86
|
+
return filters;
|
|
87
|
+
}
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
function normalizeSort(sort) {
|
|
91
|
+
if (Array.isArray(sort)) {
|
|
92
|
+
return sort;
|
|
93
|
+
}
|
|
94
|
+
if (!isRecord(sort) || typeof sort.field !== 'string') {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
if (sort.direction === 'asc') {
|
|
98
|
+
return [Sorts.ASC(sort.field)];
|
|
99
|
+
}
|
|
100
|
+
if (sort.direction === 'desc') {
|
|
101
|
+
return [Sorts.DESC(sort.field)];
|
|
102
|
+
}
|
|
103
|
+
return sort;
|
|
104
|
+
}
|
|
105
|
+
function createAggregationRule(rule) {
|
|
106
|
+
switch (rule.operation) {
|
|
107
|
+
case 'sum':
|
|
108
|
+
return Aggregates.sum(rule.field);
|
|
109
|
+
case 'count':
|
|
110
|
+
return Aggregates.count();
|
|
111
|
+
case 'avg':
|
|
112
|
+
return Aggregates.avg(rule.field);
|
|
113
|
+
case 'min':
|
|
114
|
+
return Aggregates.min(rule.field);
|
|
115
|
+
case 'max':
|
|
116
|
+
return Aggregates.max(rule.field);
|
|
117
|
+
case 'median':
|
|
118
|
+
return Aggregates.median(rule.field);
|
|
119
|
+
default:
|
|
120
|
+
throw new Error(`Unsupported aggregation operation: ${rule.operation}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function createGroupByRule(rule) {
|
|
124
|
+
if (rule.type === 'field') {
|
|
125
|
+
return GroupBy.Field(rule.field);
|
|
126
|
+
}
|
|
127
|
+
return GroupBy.DateTrunc(rule.field, rule.truncation, rule.timezone);
|
|
128
|
+
}
|
|
129
|
+
function isRecord(value) {
|
|
130
|
+
return typeof value === 'object' && value !== null;
|
|
131
|
+
}
|
|
28
132
|
export function createWidgetDataService(adminforth) {
|
|
29
133
|
return {
|
|
30
|
-
getWidgetData: (widget) => getWidgetData(adminforth, widget),
|
|
134
|
+
getWidgetData: (widget, options) => getWidgetData(adminforth, widget, options),
|
|
31
135
|
};
|
|
32
136
|
}
|
package/endpoint/widgets.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { AdminUser, IHttpServer } from 'adminforth';
|
|
2
2
|
import { randomUUID } from 'crypto';
|
|
3
|
+
import {
|
|
4
|
+
normalizeDashboardWidgetConfig,
|
|
5
|
+
} from '../custom/model/dashboard.types.js';
|
|
3
6
|
import type { DashboardConfig, DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
|
|
4
7
|
import {
|
|
5
8
|
DashboardApiResponseSchema,
|
|
@@ -7,8 +10,10 @@ import {
|
|
|
7
10
|
GroupIdRequestSchema,
|
|
8
11
|
MoveWidgetRequestSchema,
|
|
9
12
|
SetWidgetConfigRequestSchema,
|
|
13
|
+
WidgetDataRequestSchema,
|
|
10
14
|
WidgetIdRequestSchema,
|
|
11
15
|
} from '../schema/api.js';
|
|
16
|
+
import { StoredWidgetConfigSchema } from '../schema/widget.js';
|
|
12
17
|
import type { DashboardWidgetConfigValidationError } from '../schema/widget.js';
|
|
13
18
|
import type { DashboardRecord, PersistedDashboardResponse } from '../services/dashboardConfigService.js';
|
|
14
19
|
|
|
@@ -24,9 +29,42 @@ type WidgetEndpointsContext = {
|
|
|
24
29
|
validateDashboardWidgetApiConfig: (
|
|
25
30
|
widget: DashboardWidgetConfig,
|
|
26
31
|
) => DashboardWidgetConfigValidationError[];
|
|
27
|
-
getWidgetData: (
|
|
32
|
+
getWidgetData: (
|
|
33
|
+
widget: DashboardWidgetConfig,
|
|
34
|
+
options?: { pagination?: { page: number, pageSize: number } },
|
|
35
|
+
) => Promise<unknown>;
|
|
28
36
|
};
|
|
29
37
|
|
|
38
|
+
function formatWidgetConfigValidationErrors(error: { issues: { path: PropertyKey[], message: string }[] }) {
|
|
39
|
+
return error.issues.map((issue) => ({
|
|
40
|
+
field: issue.path.length ? formatWidgetConfigFieldPath(issue.path.map(String).join('.')) : 'config',
|
|
41
|
+
message: issue.message,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatWidgetConfigApiValidationErrors(errors: DashboardWidgetConfigValidationError[]) {
|
|
46
|
+
return errors.map((error) => ({
|
|
47
|
+
...error,
|
|
48
|
+
field: formatWidgetConfigFieldPath(error.field),
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function formatWidgetConfigFieldPath(field: string) {
|
|
53
|
+
const fieldAliases = new Map([
|
|
54
|
+
['minWidth', 'min_width'],
|
|
55
|
+
['maxWidth', 'max_width'],
|
|
56
|
+
['dataSource', 'data_source'],
|
|
57
|
+
['resourceId', 'resource_id'],
|
|
58
|
+
['groupBy', 'group_by'],
|
|
59
|
+
['pageSize', 'page_size'],
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
return field
|
|
63
|
+
.split('.')
|
|
64
|
+
.map((segment) => fieldAliases.get(segment) ?? segment)
|
|
65
|
+
.join('.');
|
|
66
|
+
}
|
|
67
|
+
|
|
30
68
|
export function registerWidgetEndpoints(
|
|
31
69
|
server: IHttpServer,
|
|
32
70
|
ctx: WidgetEndpointsContext,
|
|
@@ -197,14 +235,24 @@ export function registerWidgetEndpoints(
|
|
|
197
235
|
return { error: 'Dashboard widget not found' };
|
|
198
236
|
}
|
|
199
237
|
|
|
200
|
-
const
|
|
238
|
+
const parsedWidgetConfig = StoredWidgetConfigSchema.safeParse(normalizeDashboardWidgetConfig(body.config));
|
|
239
|
+
|
|
240
|
+
if (!parsedWidgetConfig.success) {
|
|
241
|
+
response.setStatus(422);
|
|
242
|
+
return {
|
|
243
|
+
error: 'Invalid widget config',
|
|
244
|
+
validationErrors: formatWidgetConfigValidationErrors(parsedWidgetConfig.error),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const typedWidgetConfig = parsedWidgetConfig.data as DashboardWidgetConfig;
|
|
201
249
|
const apiValidationErrors = ctx.validateDashboardWidgetApiConfig(typedWidgetConfig);
|
|
202
250
|
|
|
203
251
|
if (apiValidationErrors.length) {
|
|
204
252
|
response.setStatus(422);
|
|
205
253
|
return {
|
|
206
254
|
error: 'Invalid widget config',
|
|
207
|
-
validationErrors: apiValidationErrors,
|
|
255
|
+
validationErrors: formatWidgetConfigApiValidationErrors(apiValidationErrors),
|
|
208
256
|
};
|
|
209
257
|
}
|
|
210
258
|
|
|
@@ -225,8 +273,8 @@ export function registerWidgetEndpoints(
|
|
|
225
273
|
server.endpoint({
|
|
226
274
|
method: 'POST',
|
|
227
275
|
path: '/dashboard/get_dashboard_widget_data',
|
|
228
|
-
description: 'Loads
|
|
229
|
-
request_schema:
|
|
276
|
+
description: 'Loads widget data for one dashboard widget by dashboard slug and widget id.',
|
|
277
|
+
request_schema: WidgetDataRequestSchema,
|
|
230
278
|
response_schema: DashboardWidgetDataResponseSchema,
|
|
231
279
|
handler: async ({ body, response }) => {
|
|
232
280
|
const slug = String(body?.slug || 'default');
|
|
@@ -248,7 +296,9 @@ export function registerWidgetEndpoints(
|
|
|
248
296
|
|
|
249
297
|
return {
|
|
250
298
|
widget,
|
|
251
|
-
data: await ctx.getWidgetData(widget
|
|
299
|
+
data: await ctx.getWidgetData(widget, {
|
|
300
|
+
pagination: body?.pagination,
|
|
301
|
+
}),
|
|
252
302
|
};
|
|
253
303
|
},
|
|
254
304
|
});
|
package/package.json
CHANGED
package/schema/api.ts
CHANGED
|
@@ -72,6 +72,15 @@ export const WidgetIdRequestZodSchema = z.object({
|
|
|
72
72
|
widgetId: z.string(),
|
|
73
73
|
}).strict()
|
|
74
74
|
|
|
75
|
+
export const WidgetDataRequestZodSchema = z.object({
|
|
76
|
+
slug: z.string().optional(),
|
|
77
|
+
widgetId: z.string(),
|
|
78
|
+
pagination: z.object({
|
|
79
|
+
page: z.number().int().positive(),
|
|
80
|
+
pageSize: z.number().int().positive(),
|
|
81
|
+
}).optional(),
|
|
82
|
+
}).strict()
|
|
83
|
+
|
|
75
84
|
export const MoveWidgetRequestZodSchema = z.object({
|
|
76
85
|
slug: z.string().optional(),
|
|
77
86
|
widgetId: z.string(),
|
|
@@ -81,7 +90,7 @@ export const MoveWidgetRequestZodSchema = z.object({
|
|
|
81
90
|
export const SetWidgetConfigRequestZodSchema = z.object({
|
|
82
91
|
slug: z.string().optional(),
|
|
83
92
|
widgetId: z.string(),
|
|
84
|
-
config:
|
|
93
|
+
config: z.record(z.string(), z.unknown()),
|
|
85
94
|
}).strict()
|
|
86
95
|
|
|
87
96
|
export const DashboardErrorResponseSchema = toAdminForthJsonSchema(DashboardErrorResponseZodSchema)
|
|
@@ -95,5 +104,6 @@ export const GroupIdRequestSchema = toAdminForthJsonSchema(GroupIdRequestZodSche
|
|
|
95
104
|
export const MoveGroupRequestSchema = toAdminForthJsonSchema(MoveGroupRequestZodSchema)
|
|
96
105
|
export const SetGroupConfigRequestSchema = toAdminForthJsonSchema(SetGroupConfigRequestZodSchema)
|
|
97
106
|
export const WidgetIdRequestSchema = toAdminForthJsonSchema(WidgetIdRequestZodSchema)
|
|
107
|
+
export const WidgetDataRequestSchema = toAdminForthJsonSchema(WidgetDataRequestZodSchema)
|
|
98
108
|
export const MoveWidgetRequestSchema = toAdminForthJsonSchema(MoveWidgetRequestZodSchema)
|
|
99
109
|
export const SetWidgetConfigRequestSchema = toAdminForthJsonSchema(SetWidgetConfigRequestZodSchema)
|