@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/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({
|
|
@@ -97,16 +154,6 @@ export const ChartConfigSchema = z.discriminatedUnion('type', [
|
|
|
97
154
|
FunnelChartSchema,
|
|
98
155
|
])
|
|
99
156
|
|
|
100
|
-
export const DashboardWidgetQuerySchema = z.object({
|
|
101
|
-
resource: z.string().min(1, 'Query resource must be a non-empty string'),
|
|
102
|
-
select: z.array(z.string()).optional(),
|
|
103
|
-
order: z.object({
|
|
104
|
-
field: z.string().min(1, 'Order field is required'),
|
|
105
|
-
direction: z.enum(['asc', 'desc']),
|
|
106
|
-
}).optional(),
|
|
107
|
-
limit: z.number().optional(),
|
|
108
|
-
})
|
|
109
|
-
|
|
110
157
|
export const EmptyWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
111
158
|
target: z.literal('empty'),
|
|
112
159
|
})
|
|
@@ -114,31 +161,114 @@ export const EmptyWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
|
114
161
|
const TableWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
115
162
|
target: z.literal('table'),
|
|
116
163
|
table: z.unknown().optional(),
|
|
117
|
-
|
|
164
|
+
}).superRefine((widget, ctx) => {
|
|
165
|
+
if (!widget.dataSource) {
|
|
166
|
+
ctx.addIssue({
|
|
167
|
+
code: z.ZodIssueCode.custom,
|
|
168
|
+
path: ['dataSource'],
|
|
169
|
+
message: 'Table widget must have dataSource config',
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (widget.dataSource?.type === 'aggregate') {
|
|
174
|
+
ctx.addIssue({
|
|
175
|
+
code: z.ZodIssueCode.custom,
|
|
176
|
+
path: ['dataSource'],
|
|
177
|
+
message: 'Table widget dataSource must use resource type',
|
|
178
|
+
})
|
|
179
|
+
}
|
|
118
180
|
})
|
|
119
181
|
|
|
120
182
|
const ChartWidgetTargetConfigSchema = WidgetBaseSchema.extend({
|
|
121
183
|
target: z.literal('chart'),
|
|
122
184
|
chart: ChartConfigSchema,
|
|
123
|
-
|
|
185
|
+
}).superRefine((widget, ctx) => {
|
|
186
|
+
if (!widget.dataSource) {
|
|
187
|
+
ctx.addIssue({
|
|
188
|
+
code: z.ZodIssueCode.custom,
|
|
189
|
+
path: ['dataSource'],
|
|
190
|
+
message: 'Chart widget must have dataSource config',
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (widget.dataSource?.type === 'resource') {
|
|
195
|
+
ctx.addIssue({
|
|
196
|
+
code: z.ZodIssueCode.custom,
|
|
197
|
+
path: ['dataSource'],
|
|
198
|
+
message: 'Chart widget dataSource must use aggregate type',
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (widget.dataSource?.type === 'aggregate' && !widget.dataSource.groupBy) {
|
|
203
|
+
ctx.addIssue({
|
|
204
|
+
code: z.ZodIssueCode.custom,
|
|
205
|
+
path: ['dataSource', 'groupBy'],
|
|
206
|
+
message: 'Chart widget aggregate dataSource must define groupBy',
|
|
207
|
+
})
|
|
208
|
+
}
|
|
124
209
|
})
|
|
125
210
|
|
|
126
211
|
const KpiCardWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
127
212
|
target: z.literal('kpi_card'),
|
|
128
213
|
kpi_card: z.unknown().optional(),
|
|
129
|
-
|
|
214
|
+
}).superRefine((widget, ctx) => {
|
|
215
|
+
if (!widget.dataSource) {
|
|
216
|
+
ctx.addIssue({
|
|
217
|
+
code: z.ZodIssueCode.custom,
|
|
218
|
+
path: ['dataSource'],
|
|
219
|
+
message: 'KPI card widget must have dataSource config',
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (widget.dataSource?.type === 'aggregate' && widget.dataSource.groupBy) {
|
|
224
|
+
ctx.addIssue({
|
|
225
|
+
code: z.ZodIssueCode.custom,
|
|
226
|
+
path: ['dataSource', 'groupBy'],
|
|
227
|
+
message: 'KPI card aggregate dataSource must not define groupBy',
|
|
228
|
+
})
|
|
229
|
+
}
|
|
130
230
|
})
|
|
131
231
|
|
|
132
232
|
const GaugeCardWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
133
233
|
target: z.literal('gauge_card'),
|
|
134
234
|
gauge_card: z.unknown().optional(),
|
|
135
|
-
|
|
235
|
+
}).superRefine((widget, ctx) => {
|
|
236
|
+
if (!widget.dataSource) {
|
|
237
|
+
ctx.addIssue({
|
|
238
|
+
code: z.ZodIssueCode.custom,
|
|
239
|
+
path: ['dataSource'],
|
|
240
|
+
message: 'Gauge card widget must have dataSource config',
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (widget.dataSource?.type === 'aggregate' && widget.dataSource.groupBy) {
|
|
245
|
+
ctx.addIssue({
|
|
246
|
+
code: z.ZodIssueCode.custom,
|
|
247
|
+
path: ['dataSource', 'groupBy'],
|
|
248
|
+
message: 'Gauge card aggregate dataSource must not define groupBy',
|
|
249
|
+
})
|
|
250
|
+
}
|
|
136
251
|
})
|
|
137
252
|
|
|
138
253
|
const PivotTableWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
139
254
|
target: z.literal('pivot_table'),
|
|
140
255
|
pivot_table: z.unknown().optional(),
|
|
141
|
-
|
|
256
|
+
}).superRefine((widget, ctx) => {
|
|
257
|
+
if (!widget.dataSource) {
|
|
258
|
+
ctx.addIssue({
|
|
259
|
+
code: z.ZodIssueCode.custom,
|
|
260
|
+
path: ['dataSource'],
|
|
261
|
+
message: 'Pivot table widget must have dataSource config',
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (widget.dataSource?.type === 'aggregate' && !widget.dataSource.groupBy) {
|
|
266
|
+
ctx.addIssue({
|
|
267
|
+
code: z.ZodIssueCode.custom,
|
|
268
|
+
path: ['dataSource', 'groupBy'],
|
|
269
|
+
message: 'Pivot table aggregate dataSource must define groupBy',
|
|
270
|
+
})
|
|
271
|
+
}
|
|
142
272
|
})
|
|
143
273
|
|
|
144
274
|
export const WidgetConfigSchema = z.discriminatedUnion('target', [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { IAdminForth } from 'adminforth';
|
|
2
2
|
import type { DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
|
|
3
|
+
import { normalizeChartWidgetConfig } from '../custom/widgets/chart/chart.types.js';
|
|
3
4
|
import type { DashboardWidgetConfigValidationError } from '../schema/widget.js';
|
|
4
|
-
import type { DashboardWidgetQueryConfig } from './widgetDataService.js';
|
|
5
5
|
|
|
6
6
|
export type WidgetConfigValidatorService = {
|
|
7
7
|
validateDashboardWidgetApiConfig: (
|
|
@@ -19,15 +19,9 @@ export function validateDashboardWidgetApiConfig(
|
|
|
19
19
|
|
|
20
20
|
const errors: DashboardWidgetConfigValidationError[] = [];
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
errors.push({
|
|
24
|
-
field: 'query',
|
|
25
|
-
message: 'Chart widget must have query config',
|
|
26
|
-
});
|
|
27
|
-
return errors;
|
|
28
|
-
}
|
|
22
|
+
const chart = normalizeChartWidgetConfig(widget.chart);
|
|
29
23
|
|
|
30
|
-
if (!
|
|
24
|
+
if (!chart) {
|
|
31
25
|
errors.push({
|
|
32
26
|
field: 'chart',
|
|
33
27
|
message: 'Chart widget must have chart config',
|
|
@@ -35,52 +29,33 @@ export function validateDashboardWidgetApiConfig(
|
|
|
35
29
|
return errors;
|
|
36
30
|
}
|
|
37
31
|
|
|
38
|
-
const
|
|
39
|
-
const chart = widget.chart;
|
|
40
|
-
|
|
41
|
-
const resource = adminforth.config.resources.find((item) => item.resourceId === query.resource);
|
|
42
|
-
|
|
43
|
-
if (!resource) {
|
|
44
|
-
errors.push({
|
|
45
|
-
field: 'query.resource',
|
|
46
|
-
message: `Resource "${query.resource}" is not registered`,
|
|
47
|
-
});
|
|
48
|
-
return errors;
|
|
49
|
-
}
|
|
32
|
+
const aggregateDataSource = getAggregateDataSource(widget.dataSource);
|
|
50
33
|
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
}
|
|
34
|
+
if (aggregateDataSource) {
|
|
35
|
+
const resource = adminforth.config.resources.find((item) => item.resourceId === aggregateDataSource.resourceId);
|
|
54
36
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
for (const field of query.select) {
|
|
58
|
-
if (!resourceFields.includes(field)) {
|
|
37
|
+
if (!resource) {
|
|
59
38
|
errors.push({
|
|
60
|
-
field: '
|
|
61
|
-
message: `
|
|
39
|
+
field: 'data_source.resource_id',
|
|
40
|
+
message: `Resource "${aggregateDataSource.resourceId}" is not registered`,
|
|
62
41
|
});
|
|
63
42
|
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const chartFields = [
|
|
67
|
-
chart.x_field,
|
|
68
|
-
chart.y_field,
|
|
69
|
-
chart.label_field,
|
|
70
|
-
chart.value_field,
|
|
71
|
-
chart.bucket_field,
|
|
72
|
-
...(chart.series?.map((series: { field: string }) => series.field) ?? []),
|
|
73
|
-
].filter((field): field is string => typeof field === 'string');
|
|
74
43
|
|
|
75
|
-
|
|
76
|
-
if (!query.select.includes(field)) {
|
|
44
|
+
if (!aggregateDataSource.groupBy) {
|
|
77
45
|
errors.push({
|
|
78
|
-
field: '
|
|
79
|
-
message:
|
|
46
|
+
field: 'data_source.group_by',
|
|
47
|
+
message: 'Chart aggregate dataSource must define groupBy',
|
|
80
48
|
});
|
|
81
49
|
}
|
|
50
|
+
|
|
51
|
+
return errors;
|
|
82
52
|
}
|
|
83
53
|
|
|
54
|
+
errors.push({
|
|
55
|
+
field: 'data_source',
|
|
56
|
+
message: 'Chart widget must have aggregate dataSource config',
|
|
57
|
+
});
|
|
58
|
+
|
|
84
59
|
return errors;
|
|
85
60
|
}
|
|
86
61
|
|
|
@@ -90,4 +65,21 @@ export function createWidgetConfigValidatorService(
|
|
|
90
65
|
return {
|
|
91
66
|
validateDashboardWidgetApiConfig: (widget) => validateDashboardWidgetApiConfig(adminforth, widget),
|
|
92
67
|
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getAggregateDataSource(dataSource: unknown) {
|
|
71
|
+
if (
|
|
72
|
+
typeof dataSource !== 'object'
|
|
73
|
+
|| dataSource === null
|
|
74
|
+
|| (dataSource as { type?: string }).type !== 'aggregate'
|
|
75
|
+
|| typeof (dataSource as { resourceId?: unknown }).resourceId !== 'string'
|
|
76
|
+
) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return dataSource as {
|
|
81
|
+
type: 'aggregate';
|
|
82
|
+
resourceId: string;
|
|
83
|
+
groupBy?: unknown;
|
|
84
|
+
};
|
|
93
85
|
}
|
|
@@ -1,57 +1,204 @@
|
|
|
1
|
-
import { Sorts } from 'adminforth';
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import { Aggregates, GroupBy, Sorts } from 'adminforth';
|
|
2
|
+
import type {
|
|
3
|
+
IAdminForth,
|
|
4
|
+
IAdminForthAndOrFilter,
|
|
5
|
+
IAdminForthSingleFilter,
|
|
6
|
+
IAdminForthSort,
|
|
7
|
+
} from 'adminforth';
|
|
8
|
+
import type {
|
|
9
|
+
AggregationRule,
|
|
10
|
+
DashboardWidgetConfig,
|
|
11
|
+
DashboardWidgetData,
|
|
12
|
+
GroupByRule,
|
|
13
|
+
WidgetDataSource,
|
|
14
|
+
} from '../custom/model/dashboard.types.js';
|
|
15
|
+
|
|
16
|
+
export type DashboardWidgetDataOptions = {
|
|
17
|
+
pagination?: {
|
|
18
|
+
page: number;
|
|
19
|
+
pageSize: number;
|
|
11
20
|
};
|
|
12
|
-
limit?: number;
|
|
13
21
|
};
|
|
14
22
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
type DashboardWidgetFilters =
|
|
24
|
+
| IAdminForthSingleFilter
|
|
25
|
+
| IAdminForthAndOrFilter
|
|
26
|
+
| Array<IAdminForthSingleFilter | IAdminForthAndOrFilter>;
|
|
19
27
|
|
|
20
28
|
export type WidgetDataService = {
|
|
21
|
-
getWidgetData: (widget: DashboardWidgetConfig) => Promise<DashboardWidgetData | null>;
|
|
29
|
+
getWidgetData: (widget: DashboardWidgetConfig, options?: DashboardWidgetDataOptions) => Promise<DashboardWidgetData | null>;
|
|
22
30
|
};
|
|
23
31
|
|
|
24
32
|
export async function getWidgetData(
|
|
25
33
|
adminforth: IAdminForth,
|
|
26
34
|
widget: DashboardWidgetConfig,
|
|
35
|
+
options: DashboardWidgetDataOptions = {},
|
|
27
36
|
): Promise<DashboardWidgetData | null> {
|
|
28
|
-
|
|
37
|
+
const dataSource = getWidgetDataSource(widget.dataSource);
|
|
38
|
+
|
|
39
|
+
if (!dataSource) {
|
|
29
40
|
return null;
|
|
30
41
|
}
|
|
31
42
|
|
|
32
|
-
|
|
43
|
+
if (dataSource.type === 'aggregate') {
|
|
44
|
+
return getAggregateWidgetData(adminforth, dataSource);
|
|
45
|
+
}
|
|
33
46
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
return getResourceWidgetData(adminforth, dataSource, options);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function getResourceWidgetData(
|
|
51
|
+
adminforth: IAdminForth,
|
|
52
|
+
dataSource: Extract<WidgetDataSource, { type: 'resource' }>,
|
|
53
|
+
options: DashboardWidgetDataOptions,
|
|
54
|
+
): Promise<DashboardWidgetData> {
|
|
55
|
+
const resource = adminforth.resource(dataSource.resourceId);
|
|
56
|
+
const filters = normalizeFilters(dataSource.filters);
|
|
57
|
+
const sort = normalizeSort(dataSource.sort);
|
|
58
|
+
const pagination = options.pagination;
|
|
59
|
+
const offset = pagination ? (pagination.page - 1) * pagination.pageSize : 0;
|
|
60
|
+
const limit = pagination ? pagination.pageSize : undefined;
|
|
61
|
+
|
|
62
|
+
const rows = await resource.list(
|
|
63
|
+
filters,
|
|
64
|
+
limit ?? undefined,
|
|
65
|
+
offset,
|
|
66
|
+
sort,
|
|
41
67
|
);
|
|
42
68
|
|
|
43
|
-
const columns =
|
|
69
|
+
const columns = dataSource.columns ?? Object.keys(rows[0] ?? {});
|
|
70
|
+
const total = pagination ? await resource.count(filters) : 0;
|
|
44
71
|
|
|
45
72
|
return {
|
|
46
73
|
columns,
|
|
47
74
|
rows: rows.map((row) => (
|
|
48
75
|
Object.fromEntries(columns.map((column) => [column, row[column]]))
|
|
49
76
|
)),
|
|
77
|
+
...(pagination ? {
|
|
78
|
+
pagination: {
|
|
79
|
+
page: pagination.page,
|
|
80
|
+
pageSize: pagination.pageSize,
|
|
81
|
+
total,
|
|
82
|
+
totalPages: Math.max(Math.ceil(total / pagination.pageSize), 1),
|
|
83
|
+
},
|
|
84
|
+
} : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function getAggregateWidgetData(
|
|
89
|
+
adminforth: IAdminForth,
|
|
90
|
+
dataSource: Extract<WidgetDataSource, { type: 'aggregate' }>,
|
|
91
|
+
): Promise<DashboardWidgetData> {
|
|
92
|
+
const resource = adminforth.resource(dataSource.resourceId);
|
|
93
|
+
const rows = await resource.aggregate(
|
|
94
|
+
normalizeFilters(dataSource.filters),
|
|
95
|
+
Object.fromEntries(
|
|
96
|
+
Object.entries(dataSource.aggregations).map(([alias, rule]) => [
|
|
97
|
+
alias,
|
|
98
|
+
createAggregationRule(rule),
|
|
99
|
+
]),
|
|
100
|
+
),
|
|
101
|
+
dataSource.groupBy ? createGroupByRule(dataSource.groupBy) : undefined,
|
|
102
|
+
);
|
|
103
|
+
const columns = Object.keys(rows[0] ?? {});
|
|
104
|
+
|
|
105
|
+
if (!dataSource.groupBy) {
|
|
106
|
+
const values = rows[0] ?? {};
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
kind: 'aggregate',
|
|
110
|
+
columns: Object.keys(values),
|
|
111
|
+
rows: Object.keys(values).length ? [values] : [],
|
|
112
|
+
values,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
kind: 'aggregate',
|
|
118
|
+
columns,
|
|
119
|
+
rows,
|
|
50
120
|
};
|
|
51
121
|
}
|
|
52
122
|
|
|
123
|
+
function getWidgetDataSource(dataSource: unknown): WidgetDataSource | undefined {
|
|
124
|
+
if (isWidgetDataSource(dataSource)) {
|
|
125
|
+
return dataSource;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isWidgetDataSource(value: unknown): value is WidgetDataSource {
|
|
132
|
+
return isRecord(value)
|
|
133
|
+
&& (value.type === 'resource' || value.type === 'aggregate')
|
|
134
|
+
&& typeof value.resourceId === 'string';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function normalizeFilters(filters: unknown): DashboardWidgetFilters {
|
|
138
|
+
if (Array.isArray(filters)) {
|
|
139
|
+
return filters as DashboardWidgetFilters;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (isRecord(filters)) {
|
|
143
|
+
return filters as DashboardWidgetFilters;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function normalizeSort(sort: unknown): IAdminForthSort | IAdminForthSort[] | undefined {
|
|
150
|
+
if (Array.isArray(sort)) {
|
|
151
|
+
return sort as IAdminForthSort[];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!isRecord(sort) || typeof sort.field !== 'string') {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (sort.direction === 'asc') {
|
|
159
|
+
return [Sorts.ASC(sort.field)];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (sort.direction === 'desc') {
|
|
163
|
+
return [Sorts.DESC(sort.field)];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return sort as IAdminForthSort;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function createAggregationRule(rule: AggregationRule) {
|
|
170
|
+
switch (rule.operation) {
|
|
171
|
+
case 'sum':
|
|
172
|
+
return Aggregates.sum(rule.field!);
|
|
173
|
+
case 'count':
|
|
174
|
+
return Aggregates.count();
|
|
175
|
+
case 'avg':
|
|
176
|
+
return Aggregates.avg(rule.field!);
|
|
177
|
+
case 'min':
|
|
178
|
+
return Aggregates.min(rule.field!);
|
|
179
|
+
case 'max':
|
|
180
|
+
return Aggregates.max(rule.field!);
|
|
181
|
+
case 'median':
|
|
182
|
+
return Aggregates.median(rule.field!);
|
|
183
|
+
default:
|
|
184
|
+
throw new Error(`Unsupported aggregation operation: ${(rule as AggregationRule).operation}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function createGroupByRule(rule: GroupByRule) {
|
|
189
|
+
if (rule.type === 'field') {
|
|
190
|
+
return GroupBy.Field(rule.field);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return GroupBy.DateTrunc(rule.field, rule.truncation, rule.timezone);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function isRecord(value: unknown): value is Record<string, any> {
|
|
197
|
+
return typeof value === 'object' && value !== null;
|
|
198
|
+
}
|
|
199
|
+
|
|
53
200
|
export function createWidgetDataService(adminforth: IAdminForth): WidgetDataService {
|
|
54
201
|
return {
|
|
55
|
-
getWidgetData: (widget) => getWidgetData(adminforth, widget),
|
|
202
|
+
getWidgetData: (widget, options) => getWidgetData(adminforth, widget, options),
|
|
56
203
|
};
|
|
57
|
-
}
|
|
204
|
+
}
|