@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.
- package/README.md +116 -39
- package/custom/api/dashboardApi.ts +4 -0
- package/custom/composables/useElementSize.ts +17 -2
- package/custom/model/dashboard.types.ts +337 -236
- package/custom/skills/adminforth-dashboard/SKILL.md +113 -2
- package/custom/widgets/chart/ChartWidget.vue +38 -53
- 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/api/dashboardApi.d.ts +1 -0
- package/dist/custom/api/dashboardApi.js +5 -0
- package/dist/custom/api/dashboardApi.ts +4 -0
- package/dist/custom/composables/useElementSize.js +14 -2
- package/dist/custom/composables/useElementSize.ts +17 -2
- package/dist/custom/model/dashboard.types.d.ts +181 -61
- package/dist/custom/model/dashboard.types.js +82 -93
- package/dist/custom/model/dashboard.types.ts +337 -236
- package/dist/custom/queries/useDashboardConfig.d.ts +852 -66
- package/dist/custom/queries/useWidgetData.d.ts +848 -62
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +113 -2
- package/dist/custom/widgets/chart/ChartWidget.vue +38 -53
- 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/dashboard.d.ts +7 -2
- package/dist/endpoint/dashboard.js +45 -1
- package/dist/endpoint/widgets.d.ts +2 -1
- package/dist/endpoint/widgets.js +6 -2
- package/dist/schema/api.d.ts +2773 -736
- package/dist/schema/api.js +5 -0
- package/dist/schema/widget.d.ts +1648 -476
- package/dist/schema/widget.js +208 -139
- package/dist/services/widgetConfigValidator.js +16 -40
- package/dist/services/widgetDataService.d.ts +2 -1
- package/dist/services/widgetDataService.js +389 -82
- package/endpoint/dashboard.ts +77 -4
- package/endpoint/widgets.ts +11 -4
- package/package.json +1 -1
- package/schema/api.ts +6 -0
- package/schema/widget.ts +225 -139
- package/services/widgetConfigValidator.ts +29 -53
- 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
|
-
|
|
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
|
+
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
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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:
|
|
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
|
-
}
|
|
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
|
-
|
|
187
|
-
|
|
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 (
|
|
333
|
+
if (isFunnelChart && !isFunnelQuery) {
|
|
195
334
|
ctx.addIssue({
|
|
196
335
|
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',
|
|
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
|
-
|
|
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
|
-
}
|
|
344
|
+
card: KpiCardViewConfigSchema,
|
|
345
|
+
query: QueryConfigSchema,
|
|
230
346
|
})
|
|
231
347
|
|
|
232
348
|
const GaugeCardWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
233
349
|
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
|
-
}
|
|
350
|
+
card: GaugeCardViewConfigSchema,
|
|
351
|
+
query: QueryConfigSchema,
|
|
251
352
|
})
|
|
252
353
|
|
|
253
354
|
const PivotTableWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
254
355
|
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
|
-
}
|
|
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 (
|
|
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
|
-
}
|