@adminforth/dashboard 1.2.0 → 1.4.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 (60) hide show
  1. package/README.md +116 -39
  2. package/custom/api/dashboardApi.ts +4 -0
  3. package/custom/composables/useElementSize.ts +17 -2
  4. package/custom/model/dashboard.types.ts +337 -236
  5. package/custom/skills/adminforth-dashboard/SKILL.md +113 -2
  6. package/custom/widgets/chart/ChartWidget.vue +38 -53
  7. package/custom/widgets/chart/bar/BarChart.vue +20 -12
  8. package/custom/widgets/chart/chart.types.ts +17 -66
  9. package/custom/widgets/chart/chart.utils.ts +11 -0
  10. package/custom/widgets/chart/funnel/FunnelChart.vue +6 -4
  11. package/custom/widgets/chart/line/LineChart.vue +23 -15
  12. package/custom/widgets/chart/stacked-bar/StackedBarChart.vue +28 -43
  13. package/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -12
  14. package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
  15. package/custom/widgets/pivot-table/PivotTableWidget.vue +8 -7
  16. package/custom/widgets/table/TableWidget.vue +8 -3
  17. package/dist/custom/api/dashboardApi.d.ts +1 -0
  18. package/dist/custom/api/dashboardApi.js +5 -0
  19. package/dist/custom/api/dashboardApi.ts +4 -0
  20. package/dist/custom/composables/useElementSize.js +14 -2
  21. package/dist/custom/composables/useElementSize.ts +17 -2
  22. package/dist/custom/model/dashboard.types.d.ts +181 -61
  23. package/dist/custom/model/dashboard.types.js +82 -93
  24. package/dist/custom/model/dashboard.types.ts +337 -236
  25. package/dist/custom/queries/useDashboardConfig.d.ts +852 -66
  26. package/dist/custom/queries/useWidgetData.d.ts +848 -62
  27. package/dist/custom/skills/adminforth-dashboard/SKILL.md +113 -2
  28. package/dist/custom/widgets/chart/ChartWidget.vue +38 -53
  29. package/dist/custom/widgets/chart/bar/BarChart.vue +20 -12
  30. package/dist/custom/widgets/chart/chart.types.d.ts +13 -22
  31. package/dist/custom/widgets/chart/chart.types.js +2 -25
  32. package/dist/custom/widgets/chart/chart.types.ts +17 -66
  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 -12
  40. package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
  41. package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +8 -7
  42. package/dist/custom/widgets/table/TableWidget.vue +8 -3
  43. package/dist/endpoint/dashboard.d.ts +7 -2
  44. package/dist/endpoint/dashboard.js +45 -1
  45. package/dist/endpoint/widgets.d.ts +2 -1
  46. package/dist/endpoint/widgets.js +6 -2
  47. package/dist/schema/api.d.ts +2773 -736
  48. package/dist/schema/api.js +5 -0
  49. package/dist/schema/widget.d.ts +1648 -476
  50. package/dist/schema/widget.js +208 -139
  51. package/dist/services/widgetConfigValidator.js +16 -40
  52. package/dist/services/widgetDataService.d.ts +2 -1
  53. package/dist/services/widgetDataService.js +389 -82
  54. package/endpoint/dashboard.ts +77 -4
  55. package/endpoint/widgets.ts +11 -4
  56. package/package.json +1 -1
  57. package/schema/api.ts +6 -0
  58. package/schema/widget.ts +225 -139
  59. package/services/widgetConfigValidator.ts +29 -53
  60. package/services/widgetDataService.ts +522 -100
package/schema/api.ts CHANGED
@@ -50,6 +50,11 @@ export const SlugRequestZodSchema = z.object({
50
50
  slug: z.string().optional(),
51
51
  }).strict()
52
52
 
53
+ export const SetDashboardConfigRequestZodSchema = z.object({
54
+ slug: z.string().optional(),
55
+ config: z.record(z.string(), z.unknown()),
56
+ }).strict()
57
+
53
58
  export const GroupIdRequestZodSchema = z.object({
54
59
  slug: z.string().optional(),
55
60
  groupId: z.string(),
@@ -100,6 +105,7 @@ export const DashboardResponseSchema = toAdminForthJsonSchema(DashboardResponseZ
100
105
  export const DashboardApiResponseSchema = toAdminForthJsonSchema(DashboardApiResponseZodSchema)
101
106
  export const DashboardWidgetDataResponseSchema = toAdminForthJsonSchema(DashboardWidgetDataResponseZodSchema)
102
107
  export const SlugRequestSchema = toAdminForthJsonSchema(SlugRequestZodSchema)
108
+ export const SetDashboardConfigRequestSchema = toAdminForthJsonSchema(SetDashboardConfigRequestZodSchema)
103
109
  export const GroupIdRequestSchema = toAdminForthJsonSchema(GroupIdRequestZodSchema)
104
110
  export const MoveGroupRequestSchema = toAdminForthJsonSchema(MoveGroupRequestZodSchema)
105
111
  export const SetGroupConfigRequestSchema = toAdminForthJsonSchema(SetGroupConfigRequestZodSchema)
package/schema/widget.ts CHANGED
@@ -13,75 +13,186 @@ 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
+ const VariablesConfigSchema = z.record(z.string(), z.unknown())
149
+
150
+ export const QueryConfigSchema = z.object({
151
+ resource: z.string(),
152
+ select: z.array(QuerySelectItemSchema).optional(),
153
+ filters: FilterExpressionSchema.optional(),
154
+ groupBy: z.array(QueryGroupByItemSchema).optional(),
155
+ orderBy: z.array(QueryOrderByItemSchema).optional(),
156
+ limit: z.number().int().positive().optional(),
157
+ offset: z.number().int().nonnegative().optional(),
158
+ timeSeries: TimeSeriesConfigSchema.optional(),
159
+ period: PeriodConfigSchema.optional(),
160
+ bucket: BucketConfigSchema.optional(),
161
+ calcs: z.array(QueryCalcItemSchema).optional(),
162
+ formatting: FormattingConfigSchema.optional(),
163
+ }).strict()
164
+
165
+ const FunnelQueryStepSchema = z.object({
166
+ name: z.string(),
167
+ resource: z.string(),
168
+ metric: QueryAggregateSelectItemSchema,
169
+ filters: FilterExpressionSchema.optional(),
170
+ }).strict()
171
+
172
+ export const FunnelQueryConfigSchema = z.object({
173
+ steps: z.array(FunnelQueryStepSchema).min(1),
174
+ calcs: z.array(QueryCalcItemSchema).optional(),
175
+ }).strict()
71
176
 
72
177
  const WidgetBaseSchema = z.object({
73
178
  id: z.string().optional(),
74
179
  group_id: z.string().optional(),
75
180
  label: z.string().optional(),
181
+ variables: VariablesConfigSchema.optional(),
76
182
  size: DashboardWidgetSizeSchema.optional(),
77
183
  width: z.number().positive('Width must be greater than 0').optional(),
78
184
  height: z.number().positive('Height must be greater than 0').optional(),
79
185
  minWidth: z.number().nonnegative('Min width must be a non-negative number').optional(),
80
186
  maxWidth: z.number().nonnegative('Max width must be a non-negative number').nullable().optional(),
81
187
  order: z.number().optional(),
82
- dataSource: WidgetDataSourceZodSchema.optional(),
83
188
  })
84
189
 
190
+ const TableViewConfigSchema = z.object({
191
+ columns: z.array(FieldRefSchema).optional(),
192
+ pagination: z.boolean().optional(),
193
+ pageSize: z.number().int().positive().optional(),
194
+ }).strict()
195
+
85
196
  const ChartBaseSchema = z.object({
86
197
  title: z.string().optional(),
87
198
  })
@@ -90,58 +201,56 @@ const ChartBucketSchema = z.object({
90
201
  label: z.string().min(1, 'Bucket label is required'),
91
202
  min: z.number().optional(),
92
203
  max: z.number().optional(),
93
- })
204
+ }).strict()
94
205
 
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
- })
206
+ const ChartSeriesRefSchema = z.object({
207
+ field: z.string(),
208
+ label: z.string().optional(),
209
+ }).strict()
100
210
 
101
211
  const LineChartSchema = ChartBaseSchema.extend({
102
212
  type: z.literal('line'),
103
- x_field: z.string().optional(),
104
- y_field: z.string().optional(),
105
- series_name: z.string().optional(),
213
+ x: ChartFieldRefSchema,
214
+ y: z.array(ChartFieldRefSchema).min(1),
215
+ series: ChartSeriesRefSchema.optional(),
106
216
  color: z.string().optional(),
217
+ colors: z.array(z.string()).optional(),
107
218
  })
108
219
 
109
220
  const BarChartSchema = ChartBaseSchema.extend({
110
221
  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(),
222
+ x: ChartFieldRefSchema,
223
+ y: ChartFieldRefSchema,
115
224
  color: z.string().optional(),
116
225
  })
117
226
 
118
227
  const StackedBarChartSchema = ChartBaseSchema.extend({
119
228
  type: z.literal('stacked_bar'),
120
- x_field: z.string().optional(),
121
- series: z.array(ChartSeriesSchema).optional(),
229
+ x: ChartFieldRefSchema,
230
+ y: z.union([ChartFieldRefSchema, z.array(ChartFieldRefSchema).min(1)]),
231
+ series: ChartSeriesRefSchema.optional(),
122
232
  colors: z.array(z.string()).optional(),
123
233
  })
124
234
 
125
235
  const PieChartSchema = ChartBaseSchema.extend({
126
236
  type: z.literal('pie'),
127
- label_field: z.string().optional(),
128
- value_field: z.string().optional(),
237
+ label: ChartFieldRefSchema,
238
+ value: ChartFieldRefSchema,
129
239
  colors: z.array(z.string()).optional(),
130
240
  })
131
241
 
132
242
  const HistogramChartSchema = ChartBaseSchema.extend({
133
243
  type: z.literal('histogram'),
134
- label_field: z.string().optional(),
135
- value_field: z.string().optional(),
136
- bucket_field: z.string().optional(),
244
+ x: ChartFieldRefSchema,
245
+ y: ChartFieldRefSchema,
137
246
  buckets: z.array(ChartBucketSchema).optional(),
138
247
  color: z.string().optional(),
139
248
  })
140
249
 
141
250
  const FunnelChartSchema = ChartBaseSchema.extend({
142
251
  type: z.literal('funnel'),
143
- label_field: z.string().optional(),
144
- value_field: z.string().optional(),
252
+ label: ChartFieldRefSchema.optional(),
253
+ value: ChartFieldRefSchema.optional(),
145
254
  colors: z.array(z.string()).optional(),
146
255
  })
147
256
 
@@ -154,121 +263,98 @@ export const ChartConfigSchema = z.discriminatedUnion('type', [
154
263
  FunnelChartSchema,
155
264
  ])
156
265
 
266
+ const KpiCardViewConfigSchema = z.object({
267
+ title: z.string().optional(),
268
+ value: z.object({
269
+ field: z.string(),
270
+ format: ValueFormatSchema,
271
+ prefix: z.string().optional(),
272
+ suffix: z.string().optional(),
273
+ }).strict(),
274
+ subtitle: z.object({
275
+ text: z.string().optional(),
276
+ field: z.string().optional(),
277
+ }).strict().optional(),
278
+ comparison: z.unknown().optional(),
279
+ sparkline: z.unknown().optional(),
280
+ }).strict()
281
+
282
+ const GaugeCardViewConfigSchema = z.object({
283
+ title: z.string().optional(),
284
+ value: z.object({
285
+ field: z.string(),
286
+ format: ValueFormatSchema,
287
+ prefix: z.string().optional(),
288
+ suffix: z.string().optional(),
289
+ }).strict(),
290
+ target: z.object({
291
+ value: z.number().optional(),
292
+ field: z.string().optional(),
293
+ label: z.string().optional(),
294
+ }).strict().optional(),
295
+ progress: z.object({
296
+ valueField: z.string(),
297
+ targetValue: z.number().optional(),
298
+ targetField: z.string().optional(),
299
+ format: ValueFormatSchema,
300
+ }).strict().optional(),
301
+ color: z.string().optional(),
302
+ }).strict()
303
+
304
+ const PivotTableViewConfigSchema = z.object({
305
+ rows: z.array(FieldRefSchema).min(1),
306
+ columns: z.array(FieldRefSchema).optional(),
307
+ values: z.array(z.object({
308
+ field: z.string(),
309
+ label: z.string().optional(),
310
+ format: ValueFormatSchema,
311
+ aggregation: z.enum(['sum', 'count', 'avg', 'min', 'max']).optional(),
312
+ }).strict()).min(1),
313
+ }).strict()
314
+
157
315
  export const EmptyWidgetConfigSchema = WidgetBaseSchema.extend({
158
316
  target: z.literal('empty'),
159
317
  })
160
318
 
161
319
  const TableWidgetConfigSchema = WidgetBaseSchema.extend({
162
320
  target: z.literal('table'),
163
- table: z.unknown().optional(),
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
- }
321
+ table: TableViewConfigSchema.optional(),
322
+ query: QueryConfigSchema,
180
323
  })
181
324
 
182
325
  const ChartWidgetTargetConfigSchema = WidgetBaseSchema.extend({
183
326
  target: z.literal('chart'),
184
327
  chart: ChartConfigSchema,
328
+ query: z.union([QueryConfigSchema, FunnelQueryConfigSchema]),
185
329
  }).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
- }
330
+ const isFunnelChart = widget.chart.type === 'funnel'
331
+ const isFunnelQuery = 'steps' in widget.query
193
332
 
194
- if (widget.dataSource?.type === 'resource') {
333
+ if (isFunnelChart && !isFunnelQuery) {
195
334
  ctx.addIssue({
196
335
  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',
336
+ path: ['query'],
337
+ message: 'Funnel charts must use steps query',
207
338
  })
208
339
  }
209
340
  })
210
341
 
211
342
  const KpiCardWidgetConfigSchema = WidgetBaseSchema.extend({
212
343
  target: z.literal('kpi_card'),
213
- kpi_card: z.unknown().optional(),
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
- }
344
+ card: KpiCardViewConfigSchema,
345
+ query: QueryConfigSchema,
230
346
  })
231
347
 
232
348
  const GaugeCardWidgetConfigSchema = WidgetBaseSchema.extend({
233
349
  target: z.literal('gauge_card'),
234
- gauge_card: z.unknown().optional(),
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
- }
350
+ card: GaugeCardViewConfigSchema,
351
+ query: QueryConfigSchema,
251
352
  })
252
353
 
253
354
  const PivotTableWidgetConfigSchema = WidgetBaseSchema.extend({
254
355
  target: z.literal('pivot_table'),
255
- pivot_table: z.unknown().optional(),
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
- }
356
+ pivot: PivotTableViewConfigSchema,
357
+ query: QueryConfigSchema,
272
358
  })
273
359
 
274
360
  export const WidgetConfigSchema = z.discriminatedUnion('target', [
@@ -1,6 +1,5 @@
1
1
  import type { IAdminForth } from 'adminforth';
2
- import type { DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
3
- import { normalizeChartWidgetConfig } from '../custom/widgets/chart/chart.types.js';
2
+ import type { DashboardWidgetConfig, QueryConfig } from '../custom/model/dashboard.types.js';
4
3
  import type { DashboardWidgetConfigValidationError } from '../schema/widget.js';
5
4
 
6
5
  export type WidgetConfigValidatorService = {
@@ -13,50 +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
- const chart = normalizeChartWidgetConfig(widget.chart);
23
-
24
- if (!chart) {
25
- errors.push({
26
- field: 'chart',
27
- message: 'Chart widget must have chart config',
28
- });
29
- 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
+ ));
30
25
  }
31
26
 
32
- const aggregateDataSource = getAggregateDataSource(widget.dataSource);
33
-
34
- if (aggregateDataSource) {
35
- const resource = adminforth.config.resources.find((item) => item.resourceId === aggregateDataSource.resourceId);
27
+ return validateQueryConfig(adminforth, widget.query, 'query');
28
+ }
36
29
 
37
- if (!resource) {
38
- errors.push({
39
- field: 'data_source.resource_id',
40
- message: `Resource "${aggregateDataSource.resourceId}" is not registered`,
41
- });
42
- }
30
+ function validateQueryConfig(
31
+ adminforth: IAdminForth,
32
+ query: QueryConfig,
33
+ fieldPrefix: string,
34
+ ): DashboardWidgetConfigValidationError[] {
35
+ return validateResource(adminforth, query.resource, `${fieldPrefix}.resource`);
36
+ }
43
37
 
44
- if (!aggregateDataSource.groupBy) {
45
- errors.push({
46
- field: 'data_source.group_by',
47
- message: 'Chart aggregate dataSource must define groupBy',
48
- });
49
- }
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);
50
44
 
51
- return errors;
45
+ if (resource) {
46
+ return [];
52
47
  }
53
48
 
54
- errors.push({
55
- field: 'data_source',
56
- message: 'Chart widget must have aggregate dataSource config',
57
- });
58
-
59
- return errors;
49
+ return [{
50
+ field,
51
+ message: `Resource "${resourceId}" is not registered`,
52
+ }];
60
53
  }
61
54
 
62
55
  export function createWidgetConfigValidatorService(
@@ -66,20 +59,3 @@ export function createWidgetConfigValidatorService(
66
59
  validateDashboardWidgetApiConfig: (widget) => validateDashboardWidgetApiConfig(adminforth, widget),
67
60
  };
68
61
  }
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
- };
85
- }