@adminforth/dashboard 1.1.0 → 1.3.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.
Files changed (55) hide show
  1. package/README.md +43 -52
  2. package/custom/composables/useElementSize.ts +17 -2
  3. package/custom/model/dashboard.types.ts +385 -98
  4. package/custom/runtime/DashboardRuntime.vue +2 -1
  5. package/custom/runtime/WidgetRenderer.vue +2 -1
  6. package/custom/skills/adminforth-dashboard/SKILL.md +8 -4
  7. package/custom/widgets/chart/ChartWidget.vue +36 -35
  8. package/custom/widgets/chart/bar/BarChart.vue +20 -12
  9. package/custom/widgets/chart/chart.types.ts +42 -8
  10. package/custom/widgets/chart/chart.utils.ts +11 -0
  11. package/custom/widgets/chart/funnel/FunnelChart.vue +6 -4
  12. package/custom/widgets/chart/line/LineChart.vue +23 -15
  13. package/custom/widgets/chart/stacked-bar/StackedBarChart.vue +28 -43
  14. package/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -43
  15. package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -10
  16. package/custom/widgets/pivot-table/PivotTableWidget.vue +10 -11
  17. package/custom/widgets/table/TableWidget.vue +9 -4
  18. package/dist/custom/composables/useElementSize.js +14 -2
  19. package/dist/custom/composables/useElementSize.ts +17 -2
  20. package/dist/custom/model/dashboard.types.d.ts +179 -38
  21. package/dist/custom/model/dashboard.types.js +108 -42
  22. package/dist/custom/model/dashboard.types.ts +385 -98
  23. package/dist/custom/queries/useDashboardConfig.d.ts +832 -68
  24. package/dist/custom/queries/useWidgetData.d.ts +828 -64
  25. package/dist/custom/runtime/DashboardRuntime.vue +2 -1
  26. package/dist/custom/runtime/WidgetRenderer.vue +2 -1
  27. package/dist/custom/skills/adminforth-dashboard/SKILL.md +8 -4
  28. package/dist/custom/widgets/chart/ChartWidget.vue +36 -35
  29. package/dist/custom/widgets/chart/bar/BarChart.vue +20 -12
  30. package/dist/custom/widgets/chart/chart.types.d.ts +14 -8
  31. package/dist/custom/widgets/chart/chart.types.js +23 -0
  32. package/dist/custom/widgets/chart/chart.types.ts +42 -8
  33. package/dist/custom/widgets/chart/chart.utils.d.ts +1 -0
  34. package/dist/custom/widgets/chart/chart.utils.js +7 -0
  35. package/dist/custom/widgets/chart/chart.utils.ts +11 -0
  36. package/dist/custom/widgets/chart/funnel/FunnelChart.vue +6 -4
  37. package/dist/custom/widgets/chart/line/LineChart.vue +23 -15
  38. package/dist/custom/widgets/chart/stacked-bar/StackedBarChart.vue +28 -43
  39. package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -43
  40. package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -10
  41. package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +10 -11
  42. package/dist/custom/widgets/table/TableWidget.vue +9 -4
  43. package/dist/endpoint/widgets.js +23 -3
  44. package/dist/schema/api.d.ts +2637 -933
  45. package/dist/schema/widget.d.ts +1562 -582
  46. package/dist/schema/widget.js +207 -127
  47. package/dist/services/widgetConfigValidator.js +16 -80
  48. package/dist/services/widgetDataService.d.ts +0 -9
  49. package/dist/services/widgetDataService.js +356 -97
  50. package/endpoint/dashboard.ts +1 -1
  51. package/endpoint/widgets.ts +29 -3
  52. package/package.json +1 -1
  53. package/schema/widget.ts +221 -121
  54. package/services/widgetConfigValidator.ts +29 -100
  55. package/services/widgetDataService.ts +478 -129
package/schema/widget.ts CHANGED
@@ -13,61 +13,164 @@ const DashboardWidgetSizeSchema = z.enum([
13
13
  'full',
14
14
  ])
15
15
 
16
- export const AggregationOperationZodSchema = z.enum([
16
+ const ValueFormatSchema = z.enum([
17
+ 'number',
18
+ 'compact_number',
19
+ 'currency',
20
+ 'percent',
21
+ 'percent_delta',
22
+ 'number_delta',
23
+ 'currency_delta',
24
+ ]).optional()
25
+
26
+ const ChartFieldRefSchema = z.object({
27
+ field: z.string(),
28
+ label: z.string().optional(),
29
+ format: ValueFormatSchema,
30
+ }).strict()
31
+
32
+ const FieldRefSchema = z.union([
33
+ z.string(),
34
+ z.object({
35
+ field: z.string(),
36
+ label: z.string().optional(),
37
+ format: ValueFormatSchema,
38
+ }).strict(),
39
+ ])
40
+
41
+ const FilterExpressionSchema: z.ZodType = z.lazy(() => z.union([
42
+ z.array(FilterExpressionSchema),
43
+ z.object({
44
+ and: z.array(FilterExpressionSchema),
45
+ }).strict(),
46
+ z.object({
47
+ or: z.array(FilterExpressionSchema),
48
+ }).strict(),
49
+ z.object({
50
+ field: z.string(),
51
+ eq: z.unknown().optional(),
52
+ neq: z.unknown().optional(),
53
+ gt: z.unknown().optional(),
54
+ gte: z.unknown().optional(),
55
+ lt: z.unknown().optional(),
56
+ lte: z.unknown().optional(),
57
+ in: z.array(z.unknown()).optional(),
58
+ not_in: z.array(z.unknown()).optional(),
59
+ like: z.unknown().optional(),
60
+ ilike: z.unknown().optional(),
61
+ }).strict(),
62
+ ]))
63
+
64
+ const QueryAggregateOperationSchema = z.enum([
17
65
  'sum',
18
66
  'count',
67
+ 'count_distinct',
19
68
  'avg',
20
69
  'min',
21
70
  'max',
22
71
  'median',
23
72
  ])
24
73
 
25
- export const AggregationRuleZodSchema = z.object({
26
- operation: AggregationOperationZodSchema,
74
+ const QueryFieldSelectItemSchema = z.object({
75
+ field: z.string(),
76
+ as: z.string().optional(),
77
+ grain: z.enum(['hour', 'day', 'week', 'month', 'quarter', 'year']).optional(),
78
+ }).strict()
79
+
80
+ const QueryAggregateSelectItemSchema = z.object({
81
+ agg: QueryAggregateOperationSchema,
27
82
  field: z.string().optional(),
28
- }).superRefine((rule, ctx) => {
29
- if (rule.operation !== 'count' && !rule.field) {
83
+ as: z.string(),
84
+ filters: FilterExpressionSchema.optional(),
85
+ }).strict().superRefine((item, ctx) => {
86
+ if (!['count'].includes(item.agg) && !item.field) {
30
87
  ctx.addIssue({
31
88
  code: z.ZodIssueCode.custom,
32
89
  path: ['field'],
33
- message: `field is required for ${rule.operation}`,
90
+ message: `field is required for ${item.agg}`,
34
91
  })
35
92
  }
36
93
  })
37
94
 
38
- export const GroupByRuleZodSchema = z.discriminatedUnion('type', [
39
- z.object({
40
- type: z.literal('field'),
41
- field: z.string(),
42
- }),
95
+ const QueryCalcSelectItemSchema = z.object({
96
+ calc: z.string(),
97
+ as: z.string(),
98
+ }).strict()
99
+
100
+ const QuerySelectItemSchema = z.union([
101
+ QueryFieldSelectItemSchema,
102
+ QueryAggregateSelectItemSchema,
103
+ QueryCalcSelectItemSchema,
104
+ ])
105
+
106
+ const QueryGroupByItemSchema = z.union([
107
+ z.string(),
43
108
  z.object({
44
- type: z.literal('date_trunc'),
45
109
  field: z.string(),
46
- truncation: z.enum(['day', 'week', 'month', 'year']),
110
+ as: z.string().optional(),
111
+ grain: z.enum(['hour', 'day', 'week', 'month', 'quarter', 'year']).optional(),
47
112
  timezone: z.string().optional(),
48
- }),
113
+ }).strict(),
49
114
  ])
50
115
 
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(),
116
+ const QueryOrderByItemSchema = z.object({
117
+ field: z.string(),
118
+ direction: z.enum(['asc', 'desc']).optional(),
57
119
  }).strict()
58
120
 
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(),
121
+ const TimeSeriesConfigSchema = z.object({
122
+ field: z.string(),
123
+ grain: z.enum(['hour', 'day', 'week', 'month', 'quarter', 'year']),
124
+ timezone: z.string().optional(),
65
125
  }).strict()
66
126
 
67
- export const WidgetDataSourceZodSchema = z.discriminatedUnion('type', [
68
- ResourceDataSourceZodSchema,
69
- AggregateDataSourceZodSchema,
70
- ])
127
+ const PeriodConfigSchema = z.object({
128
+ field: z.string(),
129
+ gte: z.unknown().optional(),
130
+ lt: z.unknown().optional(),
131
+ }).strict()
132
+
133
+ const BucketConfigSchema = z.object({
134
+ field: z.string(),
135
+ buckets: z.array(z.object({
136
+ label: z.string(),
137
+ min: z.number().optional(),
138
+ max: z.number().optional(),
139
+ }).strict()),
140
+ }).strict()
141
+
142
+ const QueryCalcItemSchema = z.object({
143
+ calc: z.string(),
144
+ as: z.string(),
145
+ }).strict()
146
+
147
+ const FormattingConfigSchema = z.record(z.string(), z.unknown())
148
+
149
+ export const QueryConfigSchema = z.object({
150
+ resource: z.string(),
151
+ select: z.array(QuerySelectItemSchema).optional(),
152
+ filters: FilterExpressionSchema.optional(),
153
+ groupBy: z.array(QueryGroupByItemSchema).optional(),
154
+ orderBy: z.array(QueryOrderByItemSchema).optional(),
155
+ limit: z.number().int().positive().optional(),
156
+ offset: z.number().int().nonnegative().optional(),
157
+ timeSeries: TimeSeriesConfigSchema.optional(),
158
+ period: PeriodConfigSchema.optional(),
159
+ bucket: BucketConfigSchema.optional(),
160
+ calcs: z.array(QueryCalcItemSchema).optional(),
161
+ formatting: FormattingConfigSchema.optional(),
162
+ }).strict()
163
+
164
+ const FunnelQueryStepSchema = z.object({
165
+ name: z.string(),
166
+ resource: z.string(),
167
+ metric: QueryAggregateSelectItemSchema,
168
+ filters: FilterExpressionSchema.optional(),
169
+ }).strict()
170
+
171
+ export const FunnelQueryConfigSchema = z.object({
172
+ steps: z.array(FunnelQueryStepSchema).min(1),
173
+ }).strict()
71
174
 
72
175
  const WidgetBaseSchema = z.object({
73
176
  id: z.string().optional(),
@@ -79,9 +182,14 @@ const WidgetBaseSchema = z.object({
79
182
  minWidth: z.number().nonnegative('Min width must be a non-negative number').optional(),
80
183
  maxWidth: z.number().nonnegative('Max width must be a non-negative number').nullable().optional(),
81
184
  order: z.number().optional(),
82
- dataSource: WidgetDataSourceZodSchema.optional(),
83
185
  })
84
186
 
187
+ const TableViewConfigSchema = z.object({
188
+ columns: z.array(FieldRefSchema).optional(),
189
+ pagination: z.boolean().optional(),
190
+ pageSize: z.number().int().positive().optional(),
191
+ }).strict()
192
+
85
193
  const ChartBaseSchema = z.object({
86
194
  title: z.string().optional(),
87
195
  })
@@ -90,58 +198,56 @@ const ChartBucketSchema = z.object({
90
198
  label: z.string().min(1, 'Bucket label is required'),
91
199
  min: z.number().optional(),
92
200
  max: z.number().optional(),
93
- })
201
+ }).strict()
94
202
 
95
- const ChartSeriesSchema = z.object({
96
- name: z.string().min(1, 'Series name is required'),
97
- field: z.string().min(1, 'Series field is required'),
98
- color: z.string().optional(),
99
- })
203
+ const ChartSeriesRefSchema = z.object({
204
+ field: z.string(),
205
+ label: z.string().optional(),
206
+ }).strict()
100
207
 
101
208
  const LineChartSchema = ChartBaseSchema.extend({
102
209
  type: z.literal('line'),
103
- x_field: z.string().optional(),
104
- y_field: z.string().optional(),
105
- series_name: z.string().optional(),
210
+ x: ChartFieldRefSchema,
211
+ y: z.array(ChartFieldRefSchema).min(1),
212
+ series: ChartSeriesRefSchema.optional(),
106
213
  color: z.string().optional(),
214
+ colors: z.array(z.string()).optional(),
107
215
  })
108
216
 
109
217
  const BarChartSchema = ChartBaseSchema.extend({
110
218
  type: z.literal('bar'),
111
- label_field: z.string().optional(),
112
- value_field: z.string().optional(),
113
- bucket_field: z.string().optional(),
114
- buckets: z.array(ChartBucketSchema).optional(),
219
+ x: ChartFieldRefSchema,
220
+ y: ChartFieldRefSchema,
115
221
  color: z.string().optional(),
116
222
  })
117
223
 
118
224
  const StackedBarChartSchema = ChartBaseSchema.extend({
119
225
  type: z.literal('stacked_bar'),
120
- x_field: z.string().optional(),
121
- series: z.array(ChartSeriesSchema).optional(),
226
+ x: ChartFieldRefSchema,
227
+ y: ChartFieldRefSchema,
228
+ series: ChartSeriesRefSchema,
122
229
  colors: z.array(z.string()).optional(),
123
230
  })
124
231
 
125
232
  const PieChartSchema = ChartBaseSchema.extend({
126
233
  type: z.literal('pie'),
127
- label_field: z.string().optional(),
128
- value_field: z.string().optional(),
234
+ label: ChartFieldRefSchema,
235
+ value: ChartFieldRefSchema,
129
236
  colors: z.array(z.string()).optional(),
130
237
  })
131
238
 
132
239
  const HistogramChartSchema = ChartBaseSchema.extend({
133
240
  type: z.literal('histogram'),
134
- label_field: z.string().optional(),
135
- value_field: z.string().optional(),
136
- bucket_field: z.string().optional(),
241
+ x: ChartFieldRefSchema,
242
+ y: ChartFieldRefSchema,
137
243
  buckets: z.array(ChartBucketSchema).optional(),
138
244
  color: z.string().optional(),
139
245
  })
140
246
 
141
247
  const FunnelChartSchema = ChartBaseSchema.extend({
142
248
  type: z.literal('funnel'),
143
- label_field: z.string().optional(),
144
- value_field: z.string().optional(),
249
+ label: ChartFieldRefSchema.optional(),
250
+ value: ChartFieldRefSchema.optional(),
145
251
  colors: z.array(z.string()).optional(),
146
252
  })
147
253
 
@@ -154,15 +260,54 @@ export const ChartConfigSchema = z.discriminatedUnion('type', [
154
260
  FunnelChartSchema,
155
261
  ])
156
262
 
157
- export const DashboardWidgetQuerySchema = z.object({
158
- resource: z.string().min(1, 'Query resource must be a non-empty string'),
159
- select: z.array(z.string()).optional(),
160
- order: z.object({
161
- field: z.string().min(1, 'Order field is required'),
162
- direction: z.enum(['asc', 'desc']),
163
- }).optional(),
164
- limit: z.number().optional(),
165
- })
263
+ const KpiCardViewConfigSchema = z.object({
264
+ title: z.string().optional(),
265
+ value: z.object({
266
+ field: z.string(),
267
+ format: ValueFormatSchema,
268
+ prefix: z.string().optional(),
269
+ suffix: z.string().optional(),
270
+ }).strict(),
271
+ subtitle: z.object({
272
+ text: z.string().optional(),
273
+ field: z.string().optional(),
274
+ }).strict().optional(),
275
+ comparison: z.unknown().optional(),
276
+ sparkline: z.unknown().optional(),
277
+ }).strict()
278
+
279
+ const GaugeCardViewConfigSchema = z.object({
280
+ title: z.string().optional(),
281
+ value: z.object({
282
+ field: z.string(),
283
+ format: ValueFormatSchema,
284
+ prefix: z.string().optional(),
285
+ suffix: z.string().optional(),
286
+ }).strict(),
287
+ target: z.object({
288
+ value: z.number().optional(),
289
+ field: z.string().optional(),
290
+ label: z.string().optional(),
291
+ }).strict().optional(),
292
+ progress: z.object({
293
+ valueField: z.string(),
294
+ targetValue: z.number().optional(),
295
+ targetField: z.string().optional(),
296
+ format: ValueFormatSchema,
297
+ }).strict().optional(),
298
+ color: z.string().optional(),
299
+ }).strict()
300
+
301
+ const PivotTableViewConfigSchema = z.object({
302
+ rows: z.array(FieldRefSchema).min(1),
303
+ columns: z.array(FieldRefSchema).optional(),
304
+ values: z.array(z.object({
305
+ field: z.string(),
306
+ label: z.string().optional(),
307
+ format: ValueFormatSchema,
308
+ aggregation: z.enum(['sum', 'count', 'avg', 'min', 'max']).optional(),
309
+ }).strict()).min(1),
310
+ }).strict()
166
311
 
167
312
  export const EmptyWidgetConfigSchema = WidgetBaseSchema.extend({
168
313
  target: z.literal('empty'),
@@ -170,88 +315,43 @@ export const EmptyWidgetConfigSchema = WidgetBaseSchema.extend({
170
315
 
171
316
  const TableWidgetConfigSchema = WidgetBaseSchema.extend({
172
317
  target: z.literal('table'),
173
- table: z.unknown().optional(),
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
- }
318
+ table: TableViewConfigSchema.optional(),
319
+ query: QueryConfigSchema,
183
320
  })
184
321
 
185
322
  const ChartWidgetTargetConfigSchema = WidgetBaseSchema.extend({
186
323
  target: z.literal('chart'),
187
324
  chart: ChartConfigSchema,
188
- query: DashboardWidgetQuerySchema.optional(),
325
+ query: z.union([QueryConfigSchema, FunnelQueryConfigSchema]),
189
326
  }).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
- }
327
+ const isFunnelChart = widget.chart.type === 'funnel'
328
+ const isFunnelQuery = 'steps' in widget.query
205
329
 
206
- if (widget.dataSource?.type === 'aggregate' && !widget.dataSource.groupBy) {
330
+ if (isFunnelChart !== isFunnelQuery) {
207
331
  ctx.addIssue({
208
332
  code: z.ZodIssueCode.custom,
209
- path: ['dataSource', 'groupBy'],
210
- message: 'Chart widget aggregate dataSource must define groupBy',
333
+ path: ['query'],
334
+ message: 'Funnel charts must use steps query, other charts must use resource query',
211
335
  })
212
336
  }
213
337
  })
214
338
 
215
339
  const KpiCardWidgetConfigSchema = WidgetBaseSchema.extend({
216
340
  target: z.literal('kpi_card'),
217
- kpi_card: z.unknown().optional(),
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
- }
341
+ card: KpiCardViewConfigSchema,
342
+ query: QueryConfigSchema,
227
343
  })
228
344
 
229
345
  const GaugeCardWidgetConfigSchema = WidgetBaseSchema.extend({
230
346
  target: z.literal('gauge_card'),
231
- gauge_card: z.unknown().optional(),
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
- }
347
+ card: GaugeCardViewConfigSchema,
348
+ query: QueryConfigSchema,
241
349
  })
242
350
 
243
351
  const PivotTableWidgetConfigSchema = WidgetBaseSchema.extend({
244
352
  target: z.literal('pivot_table'),
245
- pivot_table: z.unknown().optional(),
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
- }
353
+ pivot: PivotTableViewConfigSchema,
354
+ query: QueryConfigSchema,
255
355
  })
256
356
 
257
357
  export const WidgetConfigSchema = z.discriminatedUnion('target', [
@@ -1,7 +1,6 @@
1
1
  import type { IAdminForth } from 'adminforth';
2
- import type { DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
2
+ import type { DashboardWidgetConfig, QueryConfig } from '../custom/model/dashboard.types.js';
3
3
  import type { DashboardWidgetConfigValidationError } from '../schema/widget.js';
4
- import type { DashboardWidgetQueryConfig } from './widgetDataService.js';
5
4
 
6
5
  export type WidgetConfigValidatorService = {
7
6
  validateDashboardWidgetApiConfig: (
@@ -13,97 +12,44 @@ export function validateDashboardWidgetApiConfig(
13
12
  adminforth: IAdminForth,
14
13
  widget: DashboardWidgetConfig,
15
14
  ): DashboardWidgetConfigValidationError[] {
16
- if (widget.target !== 'chart') {
15
+ if (!('query' in widget)) {
17
16
  return [];
18
17
  }
19
18
 
20
- const errors: DashboardWidgetConfigValidationError[] = [];
21
-
22
- if (!widget.chart) {
23
- errors.push({
24
- field: 'chart',
25
- message: 'Chart widget must have chart config',
26
- });
27
- return errors;
28
- }
29
-
30
- const aggregateDataSource = getAggregateDataSource(widget.dataSource);
31
-
32
- if (aggregateDataSource) {
33
- const resource = adminforth.config.resources.find((item) => item.resourceId === aggregateDataSource.resourceId);
34
-
35
- if (!resource) {
36
- errors.push({
37
- field: 'dataSource.resourceId',
38
- message: `Resource "${aggregateDataSource.resourceId}" is not registered`,
39
- });
40
- }
41
-
42
- if (!aggregateDataSource.groupBy) {
43
- errors.push({
44
- field: 'dataSource.groupBy',
45
- message: 'Chart aggregate dataSource must define groupBy',
46
- });
47
- }
48
-
49
- return errors;
19
+ if ('steps' in widget.query) {
20
+ return widget.query.steps.flatMap((step, index) => validateResource(
21
+ adminforth,
22
+ step.resource,
23
+ `query.steps.${index}.resource`,
24
+ ));
50
25
  }
51
26
 
52
- if (!widget.query) {
53
- errors.push({
54
- field: 'query',
55
- message: 'Chart widget must have query or aggregate dataSource config',
56
- });
57
- return errors;
58
- }
59
-
60
- const query = widget.query as DashboardWidgetQueryConfig;
61
- const chart = widget.chart;
62
-
63
- const resource = adminforth.config.resources.find((item) => item.resourceId === query.resource);
64
-
65
- if (!resource) {
66
- errors.push({
67
- field: 'query.resource',
68
- message: `Resource "${query.resource}" is not registered`,
69
- });
70
- return errors;
71
- }
72
-
73
- if (!query.select) {
74
- return errors;
75
- }
76
-
77
- const resourceFields = resource.columns.map((column) => column.name);
27
+ return validateQueryConfig(adminforth, widget.query, 'query');
28
+ }
78
29
 
79
- for (const field of query.select) {
80
- if (!resourceFields.includes(field)) {
81
- errors.push({
82
- field: 'query.select',
83
- message: `Field "${field}" is not in resource "${query.resource}"`,
84
- });
85
- }
86
- }
30
+ function validateQueryConfig(
31
+ adminforth: IAdminForth,
32
+ query: QueryConfig,
33
+ fieldPrefix: string,
34
+ ): DashboardWidgetConfigValidationError[] {
35
+ return validateResource(adminforth, query.resource, `${fieldPrefix}.resource`);
36
+ }
87
37
 
88
- const chartFields = [
89
- chart.x_field,
90
- chart.y_field,
91
- chart.label_field,
92
- chart.value_field,
93
- chart.bucket_field,
94
- ...(chart.series?.map((series: { field: string }) => series.field) ?? []),
95
- ].filter((field): field is string => typeof field === 'string');
38
+ function validateResource(
39
+ adminforth: IAdminForth,
40
+ resourceId: string,
41
+ field: string,
42
+ ): DashboardWidgetConfigValidationError[] {
43
+ const resource = adminforth.config.resources.find((item) => item.resourceId === resourceId);
96
44
 
97
- for (const field of chartFields) {
98
- if (!query.select.includes(field)) {
99
- errors.push({
100
- field: 'query.select',
101
- message: `Query select must include chart field "${field}"`,
102
- });
103
- }
45
+ if (resource) {
46
+ return [];
104
47
  }
105
48
 
106
- return errors;
49
+ return [{
50
+ field,
51
+ message: `Resource "${resourceId}" is not registered`,
52
+ }];
107
53
  }
108
54
 
109
55
  export function createWidgetConfigValidatorService(
@@ -113,20 +59,3 @@ export function createWidgetConfigValidatorService(
113
59
  validateDashboardWidgetApiConfig: (widget) => validateDashboardWidgetApiConfig(adminforth, widget),
114
60
  };
115
61
  }
116
-
117
- function getAggregateDataSource(dataSource: unknown) {
118
- if (
119
- typeof dataSource !== 'object'
120
- || dataSource === null
121
- || (dataSource as { type?: string }).type !== 'aggregate'
122
- || typeof (dataSource as { resourceId?: unknown }).resourceId !== 'string'
123
- ) {
124
- return null;
125
- }
126
-
127
- return dataSource as {
128
- type: 'aggregate';
129
- resourceId: string;
130
- groupBy?: unknown;
131
- };
132
- }