@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.
- package/README.md +43 -52
- package/custom/composables/useElementSize.ts +17 -2
- package/custom/model/dashboard.types.ts +385 -98
- package/custom/runtime/DashboardRuntime.vue +2 -1
- package/custom/runtime/WidgetRenderer.vue +2 -1
- package/custom/skills/adminforth-dashboard/SKILL.md +8 -4
- package/custom/widgets/chart/ChartWidget.vue +36 -35
- package/custom/widgets/chart/bar/BarChart.vue +20 -12
- package/custom/widgets/chart/chart.types.ts +42 -8
- 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 -43
- package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -10
- package/custom/widgets/pivot-table/PivotTableWidget.vue +10 -11
- package/custom/widgets/table/TableWidget.vue +9 -4
- package/dist/custom/composables/useElementSize.js +14 -2
- package/dist/custom/composables/useElementSize.ts +17 -2
- package/dist/custom/model/dashboard.types.d.ts +179 -38
- package/dist/custom/model/dashboard.types.js +108 -42
- package/dist/custom/model/dashboard.types.ts +385 -98
- package/dist/custom/queries/useDashboardConfig.d.ts +832 -68
- package/dist/custom/queries/useWidgetData.d.ts +828 -64
- package/dist/custom/runtime/DashboardRuntime.vue +2 -1
- package/dist/custom/runtime/WidgetRenderer.vue +2 -1
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +8 -4
- package/dist/custom/widgets/chart/ChartWidget.vue +36 -35
- package/dist/custom/widgets/chart/bar/BarChart.vue +20 -12
- package/dist/custom/widgets/chart/chart.types.d.ts +14 -8
- package/dist/custom/widgets/chart/chart.types.js +23 -0
- package/dist/custom/widgets/chart/chart.types.ts +42 -8
- 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 -43
- package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -10
- package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +10 -11
- package/dist/custom/widgets/table/TableWidget.vue +9 -4
- package/dist/endpoint/widgets.js +23 -3
- package/dist/schema/api.d.ts +2637 -933
- package/dist/schema/widget.d.ts +1562 -582
- package/dist/schema/widget.js +207 -127
- package/dist/services/widgetConfigValidator.js +16 -80
- package/dist/services/widgetDataService.d.ts +0 -9
- package/dist/services/widgetDataService.js +356 -97
- package/endpoint/dashboard.ts +1 -1
- package/endpoint/widgets.ts +29 -3
- package/package.json +1 -1
- package/schema/widget.ts +221 -121
- package/services/widgetConfigValidator.ts +29 -100
- 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
|
-
|
|
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,15 +260,54 @@ export const ChartConfigSchema = z.discriminatedUnion('type', [
|
|
|
154
260
|
FunnelChartSchema,
|
|
155
261
|
])
|
|
156
262
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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:
|
|
174
|
-
query:
|
|
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:
|
|
325
|
+
query: z.union([QueryConfigSchema, FunnelQueryConfigSchema]),
|
|
189
326
|
}).superRefine((widget, ctx) => {
|
|
190
|
-
|
|
191
|
-
|
|
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 (
|
|
330
|
+
if (isFunnelChart !== isFunnelQuery) {
|
|
207
331
|
ctx.addIssue({
|
|
208
332
|
code: z.ZodIssueCode.custom,
|
|
209
|
-
path: ['
|
|
210
|
-
message: '
|
|
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
|
-
|
|
218
|
-
query:
|
|
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
|
-
|
|
232
|
-
query:
|
|
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
|
-
|
|
246
|
-
query:
|
|
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 (
|
|
15
|
+
if (!('query' in widget)) {
|
|
17
16
|
return [];
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
|
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
|
-
}
|