@adminforth/dashboard 1.2.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.
- package/README.md +47 -39
- package/custom/composables/useElementSize.ts +17 -2
- package/custom/model/dashboard.types.ts +327 -234
- package/custom/skills/adminforth-dashboard/SKILL.md +6 -2
- package/custom/widgets/chart/ChartWidget.vue +23 -55
- package/custom/widgets/chart/bar/BarChart.vue +20 -12
- package/custom/widgets/chart/chart.types.ts +17 -66
- package/custom/widgets/chart/chart.utils.ts +11 -0
- package/custom/widgets/chart/funnel/FunnelChart.vue +6 -4
- package/custom/widgets/chart/line/LineChart.vue +23 -15
- package/custom/widgets/chart/stacked-bar/StackedBarChart.vue +28 -43
- package/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -12
- package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
- package/custom/widgets/pivot-table/PivotTableWidget.vue +8 -7
- package/custom/widgets/table/TableWidget.vue +8 -3
- package/dist/custom/composables/useElementSize.js +14 -2
- package/dist/custom/composables/useElementSize.ts +17 -2
- package/dist/custom/model/dashboard.types.d.ts +178 -61
- package/dist/custom/model/dashboard.types.js +67 -92
- package/dist/custom/model/dashboard.types.ts +327 -234
- package/dist/custom/queries/useDashboardConfig.d.ts +832 -66
- package/dist/custom/queries/useWidgetData.d.ts +828 -62
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +6 -2
- package/dist/custom/widgets/chart/ChartWidget.vue +23 -55
- package/dist/custom/widgets/chart/bar/BarChart.vue +20 -12
- package/dist/custom/widgets/chart/chart.types.d.ts +13 -22
- package/dist/custom/widgets/chart/chart.types.js +2 -25
- package/dist/custom/widgets/chart/chart.types.ts +17 -66
- package/dist/custom/widgets/chart/chart.utils.d.ts +1 -0
- package/dist/custom/widgets/chart/chart.utils.js +7 -0
- package/dist/custom/widgets/chart/chart.utils.ts +11 -0
- package/dist/custom/widgets/chart/funnel/FunnelChart.vue +6 -4
- package/dist/custom/widgets/chart/line/LineChart.vue +23 -15
- package/dist/custom/widgets/chart/stacked-bar/StackedBarChart.vue +28 -43
- package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -12
- package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
- package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +8 -7
- package/dist/custom/widgets/table/TableWidget.vue +8 -3
- package/dist/endpoint/widgets.js +5 -2
- package/dist/schema/api.d.ts +2680 -736
- package/dist/schema/widget.d.ts +1588 -476
- package/dist/schema/widget.js +205 -139
- package/dist/services/widgetConfigValidator.js +16 -40
- package/dist/services/widgetDataService.js +359 -82
- package/endpoint/dashboard.ts +1 -1
- package/endpoint/widgets.ts +5 -2
- package/package.json +1 -1
- package/schema/widget.ts +222 -139
- package/services/widgetConfigValidator.ts +29 -53
- package/services/widgetDataService.ts +484 -100
package/schema/widget.ts
CHANGED
|
@@ -13,61 +13,164 @@ const DashboardWidgetSizeSchema = z.enum([
|
|
|
13
13
|
'full',
|
|
14
14
|
])
|
|
15
15
|
|
|
16
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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 ${
|
|
90
|
+
message: `field is required for ${item.agg}`,
|
|
34
91
|
})
|
|
35
92
|
}
|
|
36
93
|
})
|
|
37
94
|
|
|
38
|
-
|
|
39
|
-
z.
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
144
|
-
|
|
249
|
+
label: ChartFieldRefSchema.optional(),
|
|
250
|
+
value: ChartFieldRefSchema.optional(),
|
|
145
251
|
colors: z.array(z.string()).optional(),
|
|
146
252
|
})
|
|
147
253
|
|
|
@@ -154,121 +260,98 @@ export const ChartConfigSchema = z.discriminatedUnion('type', [
|
|
|
154
260
|
FunnelChartSchema,
|
|
155
261
|
])
|
|
156
262
|
|
|
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()
|
|
311
|
+
|
|
157
312
|
export const EmptyWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
158
313
|
target: z.literal('empty'),
|
|
159
314
|
})
|
|
160
315
|
|
|
161
316
|
const TableWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
162
317
|
target: z.literal('table'),
|
|
163
|
-
table:
|
|
164
|
-
|
|
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
|
-
}
|
|
318
|
+
table: TableViewConfigSchema.optional(),
|
|
319
|
+
query: QueryConfigSchema,
|
|
180
320
|
})
|
|
181
321
|
|
|
182
322
|
const ChartWidgetTargetConfigSchema = WidgetBaseSchema.extend({
|
|
183
323
|
target: z.literal('chart'),
|
|
184
324
|
chart: ChartConfigSchema,
|
|
325
|
+
query: z.union([QueryConfigSchema, FunnelQueryConfigSchema]),
|
|
185
326
|
}).superRefine((widget, ctx) => {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
code: z.ZodIssueCode.custom,
|
|
189
|
-
path: ['dataSource'],
|
|
190
|
-
message: 'Chart widget must have dataSource config',
|
|
191
|
-
})
|
|
192
|
-
}
|
|
327
|
+
const isFunnelChart = widget.chart.type === 'funnel'
|
|
328
|
+
const isFunnelQuery = 'steps' in widget.query
|
|
193
329
|
|
|
194
|
-
if (
|
|
330
|
+
if (isFunnelChart !== isFunnelQuery) {
|
|
195
331
|
ctx.addIssue({
|
|
196
332
|
code: z.ZodIssueCode.custom,
|
|
197
|
-
path: ['
|
|
198
|
-
message: '
|
|
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',
|
|
333
|
+
path: ['query'],
|
|
334
|
+
message: 'Funnel charts must use steps query, other charts must use resource query',
|
|
207
335
|
})
|
|
208
336
|
}
|
|
209
337
|
})
|
|
210
338
|
|
|
211
339
|
const KpiCardWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
212
340
|
target: z.literal('kpi_card'),
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
}
|
|
341
|
+
card: KpiCardViewConfigSchema,
|
|
342
|
+
query: QueryConfigSchema,
|
|
230
343
|
})
|
|
231
344
|
|
|
232
345
|
const GaugeCardWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
233
346
|
target: z.literal('gauge_card'),
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
}
|
|
347
|
+
card: GaugeCardViewConfigSchema,
|
|
348
|
+
query: QueryConfigSchema,
|
|
251
349
|
})
|
|
252
350
|
|
|
253
351
|
const PivotTableWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
254
352
|
target: z.literal('pivot_table'),
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
}
|
|
353
|
+
pivot: PivotTableViewConfigSchema,
|
|
354
|
+
query: QueryConfigSchema,
|
|
272
355
|
})
|
|
273
356
|
|
|
274
357
|
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 (
|
|
15
|
+
if (!('query' in widget)) {
|
|
17
16
|
return [];
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
45
|
+
if (resource) {
|
|
46
|
+
return [];
|
|
52
47
|
}
|
|
53
48
|
|
|
54
|
-
|
|
55
|
-
field
|
|
56
|
-
message:
|
|
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
|
-
}
|