@adminforth/dashboard 1.0.0 → 1.1.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 +116 -54
- package/custom/api/dashboardApi.ts +9 -0
- package/custom/model/dashboard.types.ts +158 -1
- package/custom/queries/useWidgetData.ts +8 -4
- package/custom/runtime/WidgetShell.vue +8 -4
- package/custom/widgets/chart/chart.utils.ts +2 -2
- package/custom/widgets/gauge-card/GaugeCardWidget.vue +94 -12
- package/custom/widgets/pivot-table/PivotTableWidget.vue +27 -5
- 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 +45 -0
- package/dist/custom/model/dashboard.types.js +82 -1
- package/dist/custom/model/dashboard.types.ts +158 -1
- package/dist/custom/queries/useDashboardConfig.d.ts +42 -0
- package/dist/custom/queries/useWidgetData.d.ts +44 -1
- package/dist/custom/queries/useWidgetData.js +3 -3
- package/dist/custom/queries/useWidgetData.ts +8 -4
- package/dist/custom/runtime/WidgetShell.vue +8 -4
- 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 +94 -12
- package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +27 -5
- package/dist/custom/widgets/table/TableWidget.vue +155 -30
- package/dist/endpoint/widgets.d.ts +6 -1
- package/dist/endpoint/widgets.js +22 -4
- package/dist/schema/api.d.ts +882 -212
- package/dist/schema/api.js +11 -2
- package/dist/schema/widget.d.ts +542 -4
- package/dist/schema/widget.js +111 -1
- package/dist/services/widgetConfigValidator.js +32 -6
- package/dist/services/widgetDataService.d.ts +8 -6
- package/dist/services/widgetDataService.js +133 -11
- package/endpoint/widgets.ts +31 -4
- package/package.json +1 -1
- package/schema/api.ts +11 -1
- package/schema/widget.ts +114 -1
- package/services/widgetConfigValidator.ts +45 -6
- package/services/widgetDataService.ts +201 -19
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(),
|
|
@@ -95,26 +146,85 @@ const TableWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
|
95
146
|
target: z.literal('table'),
|
|
96
147
|
table: z.unknown().optional(),
|
|
97
148
|
query: DashboardWidgetQuerySchema.optional(),
|
|
149
|
+
}).superRefine((widget, ctx) => {
|
|
150
|
+
var _a;
|
|
151
|
+
if (((_a = widget.dataSource) === null || _a === void 0 ? void 0 : _a.type) === 'aggregate') {
|
|
152
|
+
ctx.addIssue({
|
|
153
|
+
code: z.ZodIssueCode.custom,
|
|
154
|
+
path: ['dataSource'],
|
|
155
|
+
message: 'Table widget dataSource must use resource type',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
98
158
|
});
|
|
99
159
|
const ChartWidgetTargetConfigSchema = WidgetBaseSchema.extend({
|
|
100
160
|
target: z.literal('chart'),
|
|
101
161
|
chart: ChartConfigSchema,
|
|
102
|
-
query: DashboardWidgetQuerySchema,
|
|
162
|
+
query: DashboardWidgetQuerySchema.optional(),
|
|
163
|
+
}).superRefine((widget, ctx) => {
|
|
164
|
+
var _a, _b;
|
|
165
|
+
if (!widget.query && !widget.dataSource) {
|
|
166
|
+
ctx.addIssue({
|
|
167
|
+
code: z.ZodIssueCode.custom,
|
|
168
|
+
path: ['dataSource'],
|
|
169
|
+
message: 'Chart widget must have query or dataSource config',
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if (((_a = widget.dataSource) === null || _a === void 0 ? void 0 : _a.type) === 'resource') {
|
|
173
|
+
ctx.addIssue({
|
|
174
|
+
code: z.ZodIssueCode.custom,
|
|
175
|
+
path: ['dataSource'],
|
|
176
|
+
message: 'Chart widget dataSource must use aggregate type',
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (((_b = widget.dataSource) === null || _b === void 0 ? void 0 : _b.type) === 'aggregate' && !widget.dataSource.groupBy) {
|
|
180
|
+
ctx.addIssue({
|
|
181
|
+
code: z.ZodIssueCode.custom,
|
|
182
|
+
path: ['dataSource', 'groupBy'],
|
|
183
|
+
message: 'Chart widget aggregate dataSource must define groupBy',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
103
186
|
});
|
|
104
187
|
const KpiCardWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
105
188
|
target: z.literal('kpi_card'),
|
|
106
189
|
kpi_card: z.unknown().optional(),
|
|
107
190
|
query: DashboardWidgetQuerySchema.optional(),
|
|
191
|
+
}).superRefine((widget, ctx) => {
|
|
192
|
+
var _a;
|
|
193
|
+
if (((_a = widget.dataSource) === null || _a === void 0 ? void 0 : _a.type) === 'aggregate' && widget.dataSource.groupBy) {
|
|
194
|
+
ctx.addIssue({
|
|
195
|
+
code: z.ZodIssueCode.custom,
|
|
196
|
+
path: ['dataSource', 'groupBy'],
|
|
197
|
+
message: 'KPI card aggregate dataSource must not define groupBy',
|
|
198
|
+
});
|
|
199
|
+
}
|
|
108
200
|
});
|
|
109
201
|
const GaugeCardWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
110
202
|
target: z.literal('gauge_card'),
|
|
111
203
|
gauge_card: z.unknown().optional(),
|
|
112
204
|
query: DashboardWidgetQuerySchema.optional(),
|
|
205
|
+
}).superRefine((widget, ctx) => {
|
|
206
|
+
var _a;
|
|
207
|
+
if (((_a = widget.dataSource) === null || _a === void 0 ? void 0 : _a.type) === 'aggregate' && widget.dataSource.groupBy) {
|
|
208
|
+
ctx.addIssue({
|
|
209
|
+
code: z.ZodIssueCode.custom,
|
|
210
|
+
path: ['dataSource', 'groupBy'],
|
|
211
|
+
message: 'Gauge card aggregate dataSource must not define groupBy',
|
|
212
|
+
});
|
|
213
|
+
}
|
|
113
214
|
});
|
|
114
215
|
const PivotTableWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
115
216
|
target: z.literal('pivot_table'),
|
|
116
217
|
pivot_table: z.unknown().optional(),
|
|
117
218
|
query: DashboardWidgetQuerySchema.optional(),
|
|
219
|
+
}).superRefine((widget, ctx) => {
|
|
220
|
+
var _a;
|
|
221
|
+
if (((_a = widget.dataSource) === null || _a === void 0 ? void 0 : _a.type) === 'aggregate' && !widget.dataSource.groupBy) {
|
|
222
|
+
ctx.addIssue({
|
|
223
|
+
code: z.ZodIssueCode.custom,
|
|
224
|
+
path: ['dataSource', 'groupBy'],
|
|
225
|
+
message: 'Pivot table aggregate dataSource must define groupBy',
|
|
226
|
+
});
|
|
227
|
+
}
|
|
118
228
|
});
|
|
119
229
|
export const WidgetConfigSchema = z.discriminatedUnion('target', [
|
|
120
230
|
TableWidgetConfigSchema,
|
|
@@ -4,17 +4,34 @@ export function validateDashboardWidgetApiConfig(adminforth, widget) {
|
|
|
4
4
|
return [];
|
|
5
5
|
}
|
|
6
6
|
const errors = [];
|
|
7
|
-
if (!widget.
|
|
7
|
+
if (!widget.chart) {
|
|
8
8
|
errors.push({
|
|
9
|
-
field: '
|
|
10
|
-
message: 'Chart widget must have
|
|
9
|
+
field: 'chart',
|
|
10
|
+
message: 'Chart widget must have chart config',
|
|
11
11
|
});
|
|
12
12
|
return errors;
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
const aggregateDataSource = getAggregateDataSource(widget.dataSource);
|
|
15
|
+
if (aggregateDataSource) {
|
|
16
|
+
const resource = adminforth.config.resources.find((item) => item.resourceId === aggregateDataSource.resourceId);
|
|
17
|
+
if (!resource) {
|
|
18
|
+
errors.push({
|
|
19
|
+
field: 'dataSource.resourceId',
|
|
20
|
+
message: `Resource "${aggregateDataSource.resourceId}" is not registered`,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
if (!aggregateDataSource.groupBy) {
|
|
24
|
+
errors.push({
|
|
25
|
+
field: 'dataSource.groupBy',
|
|
26
|
+
message: 'Chart aggregate dataSource must define groupBy',
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return errors;
|
|
30
|
+
}
|
|
31
|
+
if (!widget.query) {
|
|
15
32
|
errors.push({
|
|
16
|
-
field: '
|
|
17
|
-
message: 'Chart widget must have
|
|
33
|
+
field: 'query',
|
|
34
|
+
message: 'Chart widget must have query or aggregate dataSource config',
|
|
18
35
|
});
|
|
19
36
|
return errors;
|
|
20
37
|
}
|
|
@@ -63,3 +80,12 @@ export function createWidgetConfigValidatorService(adminforth) {
|
|
|
63
80
|
validateDashboardWidgetApiConfig: (widget) => validateDashboardWidgetApiConfig(adminforth, widget),
|
|
64
81
|
};
|
|
65
82
|
}
|
|
83
|
+
function getAggregateDataSource(dataSource) {
|
|
84
|
+
if (typeof dataSource !== 'object'
|
|
85
|
+
|| dataSource === null
|
|
86
|
+
|| dataSource.type !== 'aggregate'
|
|
87
|
+
|| typeof dataSource.resourceId !== 'string') {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return dataSource;
|
|
91
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IAdminForth } from 'adminforth';
|
|
2
|
-
import type { DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
|
|
2
|
+
import type { DashboardWidgetConfig, DashboardWidgetData } from '../custom/model/dashboard.types.js';
|
|
3
3
|
export type DashboardWidgetQueryConfig = {
|
|
4
4
|
resource: string;
|
|
5
5
|
select?: string[];
|
|
@@ -9,12 +9,14 @@ export type DashboardWidgetQueryConfig = {
|
|
|
9
9
|
};
|
|
10
10
|
limit?: number;
|
|
11
11
|
};
|
|
12
|
-
export type
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
export type DashboardWidgetDataOptions = {
|
|
13
|
+
pagination?: {
|
|
14
|
+
page: number;
|
|
15
|
+
pageSize: number;
|
|
16
|
+
};
|
|
15
17
|
};
|
|
16
18
|
export type WidgetDataService = {
|
|
17
|
-
getWidgetData: (widget: DashboardWidgetConfig) => Promise<DashboardWidgetData | null>;
|
|
19
|
+
getWidgetData: (widget: DashboardWidgetConfig, options?: DashboardWidgetDataOptions) => Promise<DashboardWidgetData | null>;
|
|
18
20
|
};
|
|
19
|
-
export declare function getWidgetData(adminforth: IAdminForth, widget: DashboardWidgetConfig): Promise<DashboardWidgetData | null>;
|
|
21
|
+
export declare function getWidgetData(adminforth: IAdminForth, widget: DashboardWidgetConfig, options?: DashboardWidgetDataOptions): Promise<DashboardWidgetData | null>;
|
|
20
22
|
export declare function createWidgetDataService(adminforth: IAdminForth): WidgetDataService;
|
|
@@ -7,26 +7,148 @@ 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 legacyQuery = getLegacyQueryConfig(widget.query);
|
|
14
|
+
const dataSource = getWidgetDataSource(widget, legacyQuery);
|
|
15
|
+
if (!dataSource) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
if (dataSource.type === 'aggregate') {
|
|
19
|
+
return getAggregateWidgetData(adminforth, dataSource);
|
|
20
|
+
}
|
|
21
|
+
return getResourceWidgetData(adminforth, dataSource, legacyQuery, options);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function getResourceWidgetData(adminforth, dataSource, legacyQuery, options) {
|
|
25
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
26
|
+
var _a, _b, _c, _d;
|
|
27
|
+
const resource = adminforth.resource(dataSource.resourceId);
|
|
28
|
+
const filters = normalizeFilters(dataSource.filters);
|
|
29
|
+
const sort = normalizeSort((_a = dataSource.sort) !== null && _a !== void 0 ? _a : legacyQuery === null || legacyQuery === void 0 ? void 0 : legacyQuery.order);
|
|
30
|
+
const pagination = options.pagination;
|
|
31
|
+
const offset = pagination ? (pagination.page - 1) * pagination.pageSize : 0;
|
|
32
|
+
const queryLimit = legacyQuery === null || legacyQuery === void 0 ? void 0 : legacyQuery.limit;
|
|
33
|
+
const limit = pagination
|
|
34
|
+
? Math.max(Math.min(pagination.pageSize, (queryLimit !== null && queryLimit !== void 0 ? queryLimit : Infinity) - offset), 0)
|
|
35
|
+
: queryLimit;
|
|
36
|
+
const rows = yield resource.list(filters, limit !== null && limit !== void 0 ? limit : undefined, offset, sort);
|
|
37
|
+
const columns = (_c = (_b = dataSource.columns) !== null && _b !== void 0 ? _b : legacyQuery === null || legacyQuery === void 0 ? void 0 : legacyQuery.select) !== null && _c !== void 0 ? _c : Object.keys((_d = rows[0]) !== null && _d !== void 0 ? _d : {});
|
|
38
|
+
const total = pagination ? Math.min(yield resource.count(filters), queryLimit !== null && queryLimit !== void 0 ? queryLimit : Infinity) : 0;
|
|
39
|
+
return Object.assign({ columns, rows: rows.map((row) => (Object.fromEntries(columns.map((column) => [column, row[column]])))) }, (pagination ? {
|
|
40
|
+
pagination: {
|
|
41
|
+
page: pagination.page,
|
|
42
|
+
pageSize: pagination.pageSize,
|
|
43
|
+
total,
|
|
44
|
+
totalPages: Math.max(Math.ceil(total / pagination.pageSize), 1),
|
|
45
|
+
},
|
|
46
|
+
} : {}));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function getAggregateWidgetData(adminforth, dataSource) {
|
|
12
50
|
return __awaiter(this, void 0, void 0, function* () {
|
|
13
51
|
var _a, _b;
|
|
14
|
-
|
|
15
|
-
|
|
52
|
+
const resource = adminforth.resource(dataSource.resourceId);
|
|
53
|
+
const rows = yield resource.aggregate(normalizeFilters(dataSource.filters), Object.fromEntries(Object.entries(dataSource.aggregations).map(([alias, rule]) => [
|
|
54
|
+
alias,
|
|
55
|
+
createAggregationRule(rule),
|
|
56
|
+
])), dataSource.groupBy ? createGroupByRule(dataSource.groupBy) : undefined);
|
|
57
|
+
const columns = Object.keys((_a = rows[0]) !== null && _a !== void 0 ? _a : {});
|
|
58
|
+
if (!dataSource.groupBy) {
|
|
59
|
+
const values = (_b = rows[0]) !== null && _b !== void 0 ? _b : {};
|
|
60
|
+
return {
|
|
61
|
+
kind: 'aggregate',
|
|
62
|
+
columns: Object.keys(values),
|
|
63
|
+
rows: Object.keys(values).length ? [values] : [],
|
|
64
|
+
values,
|
|
65
|
+
};
|
|
16
66
|
}
|
|
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
67
|
return {
|
|
68
|
+
kind: 'aggregate',
|
|
23
69
|
columns,
|
|
24
|
-
rows
|
|
70
|
+
rows,
|
|
25
71
|
};
|
|
26
72
|
});
|
|
27
73
|
}
|
|
74
|
+
function getLegacyQueryConfig(query) {
|
|
75
|
+
if (!isRecord(query) || typeof query.resource !== 'string') {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
return query;
|
|
79
|
+
}
|
|
80
|
+
function getWidgetDataSource(widget, legacyQuery) {
|
|
81
|
+
if (isWidgetDataSource(widget.dataSource)) {
|
|
82
|
+
return widget.dataSource;
|
|
83
|
+
}
|
|
84
|
+
if (!legacyQuery) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
type: 'resource',
|
|
89
|
+
resourceId: legacyQuery.resource,
|
|
90
|
+
columns: legacyQuery.select,
|
|
91
|
+
sort: legacyQuery.order,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function isWidgetDataSource(value) {
|
|
95
|
+
return isRecord(value)
|
|
96
|
+
&& (value.type === 'resource' || value.type === 'aggregate')
|
|
97
|
+
&& typeof value.resourceId === 'string';
|
|
98
|
+
}
|
|
99
|
+
function normalizeFilters(filters) {
|
|
100
|
+
if (Array.isArray(filters)) {
|
|
101
|
+
return filters;
|
|
102
|
+
}
|
|
103
|
+
if (isRecord(filters)) {
|
|
104
|
+
return filters;
|
|
105
|
+
}
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
function normalizeSort(sort) {
|
|
109
|
+
if (Array.isArray(sort)) {
|
|
110
|
+
return sort;
|
|
111
|
+
}
|
|
112
|
+
if (!isRecord(sort) || typeof sort.field !== 'string') {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
if (sort.direction === 'asc') {
|
|
116
|
+
return [Sorts.ASC(sort.field)];
|
|
117
|
+
}
|
|
118
|
+
if (sort.direction === 'desc') {
|
|
119
|
+
return [Sorts.DESC(sort.field)];
|
|
120
|
+
}
|
|
121
|
+
return sort;
|
|
122
|
+
}
|
|
123
|
+
function createAggregationRule(rule) {
|
|
124
|
+
switch (rule.operation) {
|
|
125
|
+
case 'sum':
|
|
126
|
+
return Aggregates.sum(rule.field);
|
|
127
|
+
case 'count':
|
|
128
|
+
return Aggregates.count();
|
|
129
|
+
case 'avg':
|
|
130
|
+
return Aggregates.avg(rule.field);
|
|
131
|
+
case 'min':
|
|
132
|
+
return Aggregates.min(rule.field);
|
|
133
|
+
case 'max':
|
|
134
|
+
return Aggregates.max(rule.field);
|
|
135
|
+
case 'median':
|
|
136
|
+
return Aggregates.median(rule.field);
|
|
137
|
+
default:
|
|
138
|
+
throw new Error(`Unsupported aggregation operation: ${rule.operation}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function createGroupByRule(rule) {
|
|
142
|
+
if (rule.type === 'field') {
|
|
143
|
+
return GroupBy.Field(rule.field);
|
|
144
|
+
}
|
|
145
|
+
return GroupBy.DateTrunc(rule.field, rule.truncation, rule.timezone);
|
|
146
|
+
}
|
|
147
|
+
function isRecord(value) {
|
|
148
|
+
return typeof value === 'object' && value !== null;
|
|
149
|
+
}
|
|
28
150
|
export function createWidgetDataService(adminforth) {
|
|
29
151
|
return {
|
|
30
|
-
getWidgetData: (widget) => getWidgetData(adminforth, widget),
|
|
152
|
+
getWidgetData: (widget, options) => getWidgetData(adminforth, widget, options),
|
|
31
153
|
};
|
|
32
154
|
}
|
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,19 @@ 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 ? issue.path.map(String).join('.') : 'config',
|
|
41
|
+
message: issue.message,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
|
|
30
45
|
export function registerWidgetEndpoints(
|
|
31
46
|
server: IHttpServer,
|
|
32
47
|
ctx: WidgetEndpointsContext,
|
|
@@ -197,7 +212,17 @@ export function registerWidgetEndpoints(
|
|
|
197
212
|
return { error: 'Dashboard widget not found' };
|
|
198
213
|
}
|
|
199
214
|
|
|
200
|
-
const
|
|
215
|
+
const parsedWidgetConfig = StoredWidgetConfigSchema.safeParse(normalizeDashboardWidgetConfig(body.config));
|
|
216
|
+
|
|
217
|
+
if (!parsedWidgetConfig.success) {
|
|
218
|
+
response.setStatus(422);
|
|
219
|
+
return {
|
|
220
|
+
error: 'Invalid widget config',
|
|
221
|
+
validationErrors: formatWidgetConfigValidationErrors(parsedWidgetConfig.error),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const typedWidgetConfig = parsedWidgetConfig.data as DashboardWidgetConfig;
|
|
201
226
|
const apiValidationErrors = ctx.validateDashboardWidgetApiConfig(typedWidgetConfig);
|
|
202
227
|
|
|
203
228
|
if (apiValidationErrors.length) {
|
|
@@ -226,7 +251,7 @@ export function registerWidgetEndpoints(
|
|
|
226
251
|
method: 'POST',
|
|
227
252
|
path: '/dashboard/get_dashboard_widget_data',
|
|
228
253
|
description: 'Loads query result data for one dashboard widget by dashboard slug and widget id.',
|
|
229
|
-
request_schema:
|
|
254
|
+
request_schema: WidgetDataRequestSchema,
|
|
230
255
|
response_schema: DashboardWidgetDataResponseSchema,
|
|
231
256
|
handler: async ({ body, response }) => {
|
|
232
257
|
const slug = String(body?.slug || 'default');
|
|
@@ -248,7 +273,9 @@ export function registerWidgetEndpoints(
|
|
|
248
273
|
|
|
249
274
|
return {
|
|
250
275
|
widget,
|
|
251
|
-
data: await ctx.getWidgetData(widget
|
|
276
|
+
data: await ctx.getWidgetData(widget, {
|
|
277
|
+
pagination: body?.pagination,
|
|
278
|
+
}),
|
|
252
279
|
};
|
|
253
280
|
},
|
|
254
281
|
});
|
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)
|
package/schema/widget.ts
CHANGED
|
@@ -13,6 +13,62 @@ const DashboardWidgetSizeSchema = z.enum([
|
|
|
13
13
|
'full',
|
|
14
14
|
])
|
|
15
15
|
|
|
16
|
+
export const AggregationOperationZodSchema = z.enum([
|
|
17
|
+
'sum',
|
|
18
|
+
'count',
|
|
19
|
+
'avg',
|
|
20
|
+
'min',
|
|
21
|
+
'max',
|
|
22
|
+
'median',
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
export const AggregationRuleZodSchema = z.object({
|
|
26
|
+
operation: AggregationOperationZodSchema,
|
|
27
|
+
field: z.string().optional(),
|
|
28
|
+
}).superRefine((rule, ctx) => {
|
|
29
|
+
if (rule.operation !== 'count' && !rule.field) {
|
|
30
|
+
ctx.addIssue({
|
|
31
|
+
code: z.ZodIssueCode.custom,
|
|
32
|
+
path: ['field'],
|
|
33
|
+
message: `field is required for ${rule.operation}`,
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
export const GroupByRuleZodSchema = z.discriminatedUnion('type', [
|
|
39
|
+
z.object({
|
|
40
|
+
type: z.literal('field'),
|
|
41
|
+
field: z.string(),
|
|
42
|
+
}),
|
|
43
|
+
z.object({
|
|
44
|
+
type: z.literal('date_trunc'),
|
|
45
|
+
field: z.string(),
|
|
46
|
+
truncation: z.enum(['day', 'week', 'month', 'year']),
|
|
47
|
+
timezone: z.string().optional(),
|
|
48
|
+
}),
|
|
49
|
+
])
|
|
50
|
+
|
|
51
|
+
export const AggregateDataSourceZodSchema = z.object({
|
|
52
|
+
type: z.literal('aggregate'),
|
|
53
|
+
resourceId: z.string(),
|
|
54
|
+
aggregations: z.record(z.string(), AggregationRuleZodSchema),
|
|
55
|
+
groupBy: GroupByRuleZodSchema.optional(),
|
|
56
|
+
filters: z.unknown().optional(),
|
|
57
|
+
}).strict()
|
|
58
|
+
|
|
59
|
+
export const ResourceDataSourceZodSchema = z.object({
|
|
60
|
+
type: z.literal('resource'),
|
|
61
|
+
resourceId: z.string(),
|
|
62
|
+
columns: z.array(z.string()).optional(),
|
|
63
|
+
filters: z.unknown().optional(),
|
|
64
|
+
sort: z.unknown().optional(),
|
|
65
|
+
}).strict()
|
|
66
|
+
|
|
67
|
+
export const WidgetDataSourceZodSchema = z.discriminatedUnion('type', [
|
|
68
|
+
ResourceDataSourceZodSchema,
|
|
69
|
+
AggregateDataSourceZodSchema,
|
|
70
|
+
])
|
|
71
|
+
|
|
16
72
|
const WidgetBaseSchema = z.object({
|
|
17
73
|
id: z.string().optional(),
|
|
18
74
|
group_id: z.string().optional(),
|
|
@@ -23,6 +79,7 @@ const WidgetBaseSchema = z.object({
|
|
|
23
79
|
minWidth: z.number().nonnegative('Min width must be a non-negative number').optional(),
|
|
24
80
|
maxWidth: z.number().nonnegative('Max width must be a non-negative number').nullable().optional(),
|
|
25
81
|
order: z.number().optional(),
|
|
82
|
+
dataSource: WidgetDataSourceZodSchema.optional(),
|
|
26
83
|
})
|
|
27
84
|
|
|
28
85
|
const ChartBaseSchema = z.object({
|
|
@@ -115,30 +172,86 @@ const TableWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
|
115
172
|
target: z.literal('table'),
|
|
116
173
|
table: z.unknown().optional(),
|
|
117
174
|
query: DashboardWidgetQuerySchema.optional(),
|
|
175
|
+
}).superRefine((widget, ctx) => {
|
|
176
|
+
if (widget.dataSource?.type === 'aggregate') {
|
|
177
|
+
ctx.addIssue({
|
|
178
|
+
code: z.ZodIssueCode.custom,
|
|
179
|
+
path: ['dataSource'],
|
|
180
|
+
message: 'Table widget dataSource must use resource type',
|
|
181
|
+
})
|
|
182
|
+
}
|
|
118
183
|
})
|
|
119
184
|
|
|
120
185
|
const ChartWidgetTargetConfigSchema = WidgetBaseSchema.extend({
|
|
121
186
|
target: z.literal('chart'),
|
|
122
187
|
chart: ChartConfigSchema,
|
|
123
|
-
query: DashboardWidgetQuerySchema,
|
|
188
|
+
query: DashboardWidgetQuerySchema.optional(),
|
|
189
|
+
}).superRefine((widget, ctx) => {
|
|
190
|
+
if (!widget.query && !widget.dataSource) {
|
|
191
|
+
ctx.addIssue({
|
|
192
|
+
code: z.ZodIssueCode.custom,
|
|
193
|
+
path: ['dataSource'],
|
|
194
|
+
message: 'Chart widget must have query or dataSource config',
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (widget.dataSource?.type === 'resource') {
|
|
199
|
+
ctx.addIssue({
|
|
200
|
+
code: z.ZodIssueCode.custom,
|
|
201
|
+
path: ['dataSource'],
|
|
202
|
+
message: 'Chart widget dataSource must use aggregate type',
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (widget.dataSource?.type === 'aggregate' && !widget.dataSource.groupBy) {
|
|
207
|
+
ctx.addIssue({
|
|
208
|
+
code: z.ZodIssueCode.custom,
|
|
209
|
+
path: ['dataSource', 'groupBy'],
|
|
210
|
+
message: 'Chart widget aggregate dataSource must define groupBy',
|
|
211
|
+
})
|
|
212
|
+
}
|
|
124
213
|
})
|
|
125
214
|
|
|
126
215
|
const KpiCardWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
127
216
|
target: z.literal('kpi_card'),
|
|
128
217
|
kpi_card: z.unknown().optional(),
|
|
129
218
|
query: DashboardWidgetQuerySchema.optional(),
|
|
219
|
+
}).superRefine((widget, ctx) => {
|
|
220
|
+
if (widget.dataSource?.type === 'aggregate' && widget.dataSource.groupBy) {
|
|
221
|
+
ctx.addIssue({
|
|
222
|
+
code: z.ZodIssueCode.custom,
|
|
223
|
+
path: ['dataSource', 'groupBy'],
|
|
224
|
+
message: 'KPI card aggregate dataSource must not define groupBy',
|
|
225
|
+
})
|
|
226
|
+
}
|
|
130
227
|
})
|
|
131
228
|
|
|
132
229
|
const GaugeCardWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
133
230
|
target: z.literal('gauge_card'),
|
|
134
231
|
gauge_card: z.unknown().optional(),
|
|
135
232
|
query: DashboardWidgetQuerySchema.optional(),
|
|
233
|
+
}).superRefine((widget, ctx) => {
|
|
234
|
+
if (widget.dataSource?.type === 'aggregate' && widget.dataSource.groupBy) {
|
|
235
|
+
ctx.addIssue({
|
|
236
|
+
code: z.ZodIssueCode.custom,
|
|
237
|
+
path: ['dataSource', 'groupBy'],
|
|
238
|
+
message: 'Gauge card aggregate dataSource must not define groupBy',
|
|
239
|
+
})
|
|
240
|
+
}
|
|
136
241
|
})
|
|
137
242
|
|
|
138
243
|
const PivotTableWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
139
244
|
target: z.literal('pivot_table'),
|
|
140
245
|
pivot_table: z.unknown().optional(),
|
|
141
246
|
query: DashboardWidgetQuerySchema.optional(),
|
|
247
|
+
}).superRefine((widget, ctx) => {
|
|
248
|
+
if (widget.dataSource?.type === 'aggregate' && !widget.dataSource.groupBy) {
|
|
249
|
+
ctx.addIssue({
|
|
250
|
+
code: z.ZodIssueCode.custom,
|
|
251
|
+
path: ['dataSource', 'groupBy'],
|
|
252
|
+
message: 'Pivot table aggregate dataSource must define groupBy',
|
|
253
|
+
})
|
|
254
|
+
}
|
|
142
255
|
})
|
|
143
256
|
|
|
144
257
|
export const WidgetConfigSchema = z.discriminatedUnion('target', [
|