@adminforth/dashboard 1.5.0 → 1.6.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/custom/api/dashboardApi.ts +137 -1
- package/custom/model/dashboard.types.ts +32 -22
- package/custom/runtime/DashboardRuntime.vue +2 -3
- package/custom/skills/adminforth-dashboard/SKILL.md +66 -10
- package/custom/widgets/KpiCardWidget.vue +172 -9
- package/custom/widgets/chart/ChartWidget.vue +5 -5
- package/custom/widgets/registry.ts +4 -4
- package/dist/custom/api/dashboardApi.d.ts +46 -1
- package/dist/custom/api/dashboardApi.js +90 -0
- package/dist/custom/api/dashboardApi.ts +137 -1
- package/dist/custom/model/dashboard.types.d.ts +30 -14
- package/dist/custom/model/dashboard.types.js +2 -2
- package/dist/custom/model/dashboard.types.ts +32 -22
- package/dist/custom/queries/useDashboardConfig.d.ts +106 -104
- package/dist/custom/queries/useWidgetData.d.ts +106 -104
- package/dist/custom/runtime/DashboardRuntime.vue +2 -3
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +66 -10
- package/dist/custom/widgets/KpiCardWidget.vue +172 -9
- package/dist/custom/widgets/chart/ChartWidget.vue +5 -5
- package/dist/custom/widgets/registry.js +4 -4
- package/dist/custom/widgets/registry.ts +4 -4
- package/dist/endpoint/widgets.js +99 -14
- package/dist/schema/api.d.ts +11426 -1634
- package/dist/schema/api.js +118 -21
- package/dist/schema/widget.d.ts +425 -1980
- package/dist/schema/widget.js +13 -374
- package/dist/schema/widgets/charts.d.ts +1689 -0
- package/dist/schema/widgets/charts.js +92 -0
- package/dist/schema/widgets/common.d.ts +275 -0
- package/dist/schema/widgets/common.js +171 -0
- package/dist/schema/widgets/gauge-card.d.ts +172 -0
- package/dist/schema/widgets/gauge-card.js +28 -0
- package/dist/schema/widgets/kpi-card.d.ts +212 -0
- package/dist/schema/widgets/kpi-card.js +43 -0
- package/dist/schema/widgets/pivot-table.d.ts +196 -0
- package/dist/schema/widgets/pivot-table.js +17 -0
- package/dist/schema/widgets/table.d.ts +130 -0
- package/dist/schema/widgets/table.js +12 -0
- package/dist/services/widgetDataService.js +96 -2
- package/endpoint/widgets.ts +173 -26
- package/package.json +1 -1
- package/schema/api.ts +148 -22
- package/schema/widget.ts +43 -425
- package/schema/widgets/charts.ts +113 -0
- package/schema/widgets/common.ts +194 -0
- package/schema/widgets/gauge-card.ts +34 -0
- package/schema/widgets/kpi-card.ts +49 -0
- package/schema/widgets/pivot-table.ts +24 -0
- package/schema/widgets/table.ts +18 -0
- package/services/widgetDataService.ts +129 -3
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import {
|
|
3
|
+
ChartFieldRefSchema,
|
|
4
|
+
FunnelQueryConfigSchema,
|
|
5
|
+
QueryConfigSchema,
|
|
6
|
+
WidgetBaseSchema,
|
|
7
|
+
} from './common.js'
|
|
8
|
+
|
|
9
|
+
const ChartBaseSchema = z.object({
|
|
10
|
+
title: z.string().optional(),
|
|
11
|
+
}).strict()
|
|
12
|
+
|
|
13
|
+
const ChartBucketSchema = z.object({
|
|
14
|
+
label: z.string().min(1, 'Bucket label is required'),
|
|
15
|
+
min: z.number().optional(),
|
|
16
|
+
max: z.number().optional(),
|
|
17
|
+
}).strict()
|
|
18
|
+
|
|
19
|
+
const ChartSeriesRefSchema = z.object({
|
|
20
|
+
field: z.string(),
|
|
21
|
+
label: z.string().optional(),
|
|
22
|
+
}).strict()
|
|
23
|
+
|
|
24
|
+
export const LineChartSchema = ChartBaseSchema.extend({
|
|
25
|
+
type: z.literal('line'),
|
|
26
|
+
x: ChartFieldRefSchema,
|
|
27
|
+
y: z.array(ChartFieldRefSchema).min(1),
|
|
28
|
+
series: ChartSeriesRefSchema.optional(),
|
|
29
|
+
color: z.string().optional(),
|
|
30
|
+
colors: z.array(z.string()).optional(),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export const BarChartSchema = ChartBaseSchema.extend({
|
|
34
|
+
type: z.literal('bar'),
|
|
35
|
+
x: ChartFieldRefSchema,
|
|
36
|
+
y: ChartFieldRefSchema,
|
|
37
|
+
color: z.string().optional(),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
export const StackedBarChartSchema = ChartBaseSchema.extend({
|
|
41
|
+
type: z.literal('stacked_bar'),
|
|
42
|
+
x: ChartFieldRefSchema,
|
|
43
|
+
y: z.union([ChartFieldRefSchema, z.array(ChartFieldRefSchema).min(1)]),
|
|
44
|
+
series: ChartSeriesRefSchema.optional(),
|
|
45
|
+
colors: z.array(z.string()).optional(),
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
export const PieChartSchema = ChartBaseSchema.extend({
|
|
49
|
+
type: z.literal('pie'),
|
|
50
|
+
label: ChartFieldRefSchema,
|
|
51
|
+
value: ChartFieldRefSchema,
|
|
52
|
+
colors: z.array(z.string()).optional(),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
export const HistogramChartSchema = ChartBaseSchema.extend({
|
|
56
|
+
type: z.literal('histogram'),
|
|
57
|
+
x: ChartFieldRefSchema,
|
|
58
|
+
y: ChartFieldRefSchema,
|
|
59
|
+
buckets: z.array(ChartBucketSchema).optional(),
|
|
60
|
+
color: z.string().optional(),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
export const FunnelChartSchema = ChartBaseSchema.extend({
|
|
64
|
+
type: z.literal('funnel'),
|
|
65
|
+
label: ChartFieldRefSchema.optional(),
|
|
66
|
+
value: ChartFieldRefSchema.optional(),
|
|
67
|
+
colors: z.array(z.string()).optional(),
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
export const LineChartWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
71
|
+
target: z.literal('chart'),
|
|
72
|
+
chart: LineChartSchema,
|
|
73
|
+
query: QueryConfigSchema,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
export const BarChartWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
77
|
+
target: z.literal('chart'),
|
|
78
|
+
chart: BarChartSchema,
|
|
79
|
+
query: QueryConfigSchema,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
export const StackedBarChartWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
83
|
+
target: z.literal('chart'),
|
|
84
|
+
chart: StackedBarChartSchema,
|
|
85
|
+
query: QueryConfigSchema,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
export const PieChartWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
89
|
+
target: z.literal('chart'),
|
|
90
|
+
chart: PieChartSchema,
|
|
91
|
+
query: QueryConfigSchema,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
export const HistogramChartWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
95
|
+
target: z.literal('chart'),
|
|
96
|
+
chart: HistogramChartSchema,
|
|
97
|
+
query: QueryConfigSchema,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
export const FunnelChartWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
101
|
+
target: z.literal('chart'),
|
|
102
|
+
chart: FunnelChartSchema,
|
|
103
|
+
query: FunnelQueryConfigSchema,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
export const ChartWidgetTargetConfigSchema = z.union([
|
|
107
|
+
LineChartWidgetConfigSchema,
|
|
108
|
+
BarChartWidgetConfigSchema,
|
|
109
|
+
StackedBarChartWidgetConfigSchema,
|
|
110
|
+
PieChartWidgetConfigSchema,
|
|
111
|
+
HistogramChartWidgetConfigSchema,
|
|
112
|
+
FunnelChartWidgetConfigSchema,
|
|
113
|
+
])
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export const DashboardWidgetSizeSchema = z.enum([
|
|
4
|
+
'small',
|
|
5
|
+
'medium',
|
|
6
|
+
'large',
|
|
7
|
+
'wide',
|
|
8
|
+
'full',
|
|
9
|
+
])
|
|
10
|
+
|
|
11
|
+
export const ValueFormatSchema = z.enum([
|
|
12
|
+
'number',
|
|
13
|
+
'integer',
|
|
14
|
+
'compact_number',
|
|
15
|
+
'currency',
|
|
16
|
+
'percent',
|
|
17
|
+
'percent_delta',
|
|
18
|
+
'number_delta',
|
|
19
|
+
'currency_delta',
|
|
20
|
+
]).optional()
|
|
21
|
+
|
|
22
|
+
export const VariablesConfigSchema = z.record(z.string(), z.unknown())
|
|
23
|
+
|
|
24
|
+
export const ChartFieldRefSchema = z.object({
|
|
25
|
+
field: z.string(),
|
|
26
|
+
label: z.string().optional(),
|
|
27
|
+
format: ValueFormatSchema,
|
|
28
|
+
}).strict()
|
|
29
|
+
|
|
30
|
+
const RelativeDateValueSchema = z.union([
|
|
31
|
+
z.object({
|
|
32
|
+
now: z.literal(true),
|
|
33
|
+
}).strict(),
|
|
34
|
+
z.object({
|
|
35
|
+
now_minus: z.string().regex(/^\d+(h|d|w|mo|y)$/),
|
|
36
|
+
}).strict(),
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
const FilterValueSchema: z.ZodType = z.lazy(() => z.union([
|
|
40
|
+
RelativeDateValueSchema,
|
|
41
|
+
z.string(),
|
|
42
|
+
z.number(),
|
|
43
|
+
z.boolean(),
|
|
44
|
+
z.null(),
|
|
45
|
+
z.array(FilterValueSchema),
|
|
46
|
+
z.record(z.string(), FilterValueSchema),
|
|
47
|
+
]))
|
|
48
|
+
|
|
49
|
+
export const FilterExpressionSchema: z.ZodType = z.lazy(() => z.union([
|
|
50
|
+
z.array(FilterExpressionSchema),
|
|
51
|
+
z.object({
|
|
52
|
+
and: z.array(FilterExpressionSchema),
|
|
53
|
+
}).strict(),
|
|
54
|
+
z.object({
|
|
55
|
+
or: z.array(FilterExpressionSchema),
|
|
56
|
+
}).strict(),
|
|
57
|
+
z.object({
|
|
58
|
+
field: z.string(),
|
|
59
|
+
eq: FilterValueSchema.optional(),
|
|
60
|
+
neq: FilterValueSchema.optional(),
|
|
61
|
+
gt: FilterValueSchema.optional(),
|
|
62
|
+
gte: FilterValueSchema.optional(),
|
|
63
|
+
lt: FilterValueSchema.optional(),
|
|
64
|
+
lte: FilterValueSchema.optional(),
|
|
65
|
+
in: z.array(FilterValueSchema).optional(),
|
|
66
|
+
not_in: z.array(FilterValueSchema).optional(),
|
|
67
|
+
like: FilterValueSchema.optional(),
|
|
68
|
+
ilike: FilterValueSchema.optional(),
|
|
69
|
+
}).strict(),
|
|
70
|
+
]))
|
|
71
|
+
|
|
72
|
+
export const QueryAggregateOperationSchema = z.enum([
|
|
73
|
+
'sum',
|
|
74
|
+
'count',
|
|
75
|
+
'count_distinct',
|
|
76
|
+
'avg',
|
|
77
|
+
'min',
|
|
78
|
+
'max',
|
|
79
|
+
'median',
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
export const QueryFieldSelectItemSchema = z.object({
|
|
83
|
+
field: z.string(),
|
|
84
|
+
as: z.string().optional(),
|
|
85
|
+
grain: z.enum(['day', 'week', 'month', 'year']).optional(),
|
|
86
|
+
}).strict()
|
|
87
|
+
|
|
88
|
+
export const QueryAggregateSelectItemSchema = z.object({
|
|
89
|
+
agg: QueryAggregateOperationSchema,
|
|
90
|
+
field: z.string().optional(),
|
|
91
|
+
as: z.string(),
|
|
92
|
+
filters: FilterExpressionSchema.optional(),
|
|
93
|
+
}).strict().superRefine((item, ctx) => {
|
|
94
|
+
if (!['count'].includes(item.agg) && !item.field) {
|
|
95
|
+
ctx.addIssue({
|
|
96
|
+
code: 'custom',
|
|
97
|
+
path: ['field'],
|
|
98
|
+
message: `field is required for ${item.agg}`,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
export const QueryCalcSelectItemSchema = z.object({
|
|
104
|
+
calc: z.string(),
|
|
105
|
+
as: z.string(),
|
|
106
|
+
}).strict()
|
|
107
|
+
|
|
108
|
+
export const QuerySelectItemSchema = z.union([
|
|
109
|
+
QueryFieldSelectItemSchema,
|
|
110
|
+
QueryAggregateSelectItemSchema,
|
|
111
|
+
QueryCalcSelectItemSchema,
|
|
112
|
+
])
|
|
113
|
+
|
|
114
|
+
export const QueryGroupByItemSchema = z.union([
|
|
115
|
+
z.string(),
|
|
116
|
+
z.object({
|
|
117
|
+
field: z.string(),
|
|
118
|
+
as: z.string().optional(),
|
|
119
|
+
grain: z.enum(['day', 'week', 'month', 'year']).optional(),
|
|
120
|
+
timezone: z.string().optional(),
|
|
121
|
+
}).strict(),
|
|
122
|
+
])
|
|
123
|
+
|
|
124
|
+
export const QueryOrderByItemSchema = z.object({
|
|
125
|
+
field: z.string(),
|
|
126
|
+
direction: z.enum(['asc', 'desc']).optional(),
|
|
127
|
+
}).strict()
|
|
128
|
+
|
|
129
|
+
const BucketConfigSchema = z.object({
|
|
130
|
+
field: z.string(),
|
|
131
|
+
buckets: z.array(z.object({
|
|
132
|
+
label: z.string(),
|
|
133
|
+
min: z.number().optional(),
|
|
134
|
+
max: z.number().optional(),
|
|
135
|
+
}).strict()),
|
|
136
|
+
}).strict()
|
|
137
|
+
|
|
138
|
+
export const QueryCalcItemSchema = z.object({
|
|
139
|
+
calc: z.string(),
|
|
140
|
+
as: z.string(),
|
|
141
|
+
}).strict()
|
|
142
|
+
|
|
143
|
+
export const QueryConfigSchema = z.object({
|
|
144
|
+
resource: z.string(),
|
|
145
|
+
select: z.array(QuerySelectItemSchema).optional(),
|
|
146
|
+
sparkline: z.object({
|
|
147
|
+
field: z.string(),
|
|
148
|
+
grain: z.enum(['day', 'week', 'month', 'year']),
|
|
149
|
+
as: z.string(),
|
|
150
|
+
fill_missing: z.record(z.string(), z.unknown()).optional(),
|
|
151
|
+
}).strict().optional(),
|
|
152
|
+
filters: FilterExpressionSchema.optional(),
|
|
153
|
+
group_by: z.array(QueryGroupByItemSchema).optional(),
|
|
154
|
+
order_by: z.array(QueryOrderByItemSchema).optional(),
|
|
155
|
+
limit: z.number().int().positive().optional(),
|
|
156
|
+
offset: z.number().int().nonnegative().optional(),
|
|
157
|
+
bucket: BucketConfigSchema.optional(),
|
|
158
|
+
calcs: z.array(QueryCalcItemSchema).optional(),
|
|
159
|
+
formatting: z.record(z.string(), z.unknown()).optional(),
|
|
160
|
+
}).strict()
|
|
161
|
+
|
|
162
|
+
const FunnelQueryStepSchema = z.object({
|
|
163
|
+
name: z.string(),
|
|
164
|
+
resource: z.string(),
|
|
165
|
+
metric: QueryAggregateSelectItemSchema,
|
|
166
|
+
filters: FilterExpressionSchema.optional(),
|
|
167
|
+
}).strict()
|
|
168
|
+
|
|
169
|
+
export const FunnelQueryConfigSchema = z.object({
|
|
170
|
+
steps: z.array(FunnelQueryStepSchema).min(1),
|
|
171
|
+
calcs: z.array(QueryCalcItemSchema).optional(),
|
|
172
|
+
}).strict()
|
|
173
|
+
|
|
174
|
+
export const WidgetPersistedFieldsSchema = z.object({
|
|
175
|
+
id: z.string(),
|
|
176
|
+
group_id: z.string(),
|
|
177
|
+
order: z.number(),
|
|
178
|
+
}).strict()
|
|
179
|
+
|
|
180
|
+
export const WidgetEditableBaseSchema = z.object({
|
|
181
|
+
label: z.string().optional(),
|
|
182
|
+
variables: VariablesConfigSchema.optional(),
|
|
183
|
+
size: DashboardWidgetSizeSchema.optional(),
|
|
184
|
+
width: z.number().positive('Width must be greater than 0').optional(),
|
|
185
|
+
height: z.number().positive('Height must be greater than 0').optional(),
|
|
186
|
+
min_width: z.number().nonnegative('Min width must be a non-negative number').optional(),
|
|
187
|
+
max_width: z.number().nonnegative('Max width must be a non-negative number').nullable().optional(),
|
|
188
|
+
}).strict()
|
|
189
|
+
|
|
190
|
+
export const WidgetBaseSchema = WidgetPersistedFieldsSchema.merge(WidgetEditableBaseSchema)
|
|
191
|
+
|
|
192
|
+
export const EmptyWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
193
|
+
target: z.literal('empty'),
|
|
194
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import {
|
|
3
|
+
QueryConfigSchema,
|
|
4
|
+
ValueFormatSchema,
|
|
5
|
+
WidgetBaseSchema,
|
|
6
|
+
} from './common.js'
|
|
7
|
+
|
|
8
|
+
export const GaugeCardViewConfigSchema = z.object({
|
|
9
|
+
title: z.string().optional(),
|
|
10
|
+
value: z.object({
|
|
11
|
+
field: z.string(),
|
|
12
|
+
format: ValueFormatSchema,
|
|
13
|
+
prefix: z.string().optional(),
|
|
14
|
+
suffix: z.string().optional(),
|
|
15
|
+
}).strict(),
|
|
16
|
+
target: z.object({
|
|
17
|
+
value: z.number().optional(),
|
|
18
|
+
field: z.string().optional(),
|
|
19
|
+
label: z.string().optional(),
|
|
20
|
+
}).strict().optional(),
|
|
21
|
+
progress: z.object({
|
|
22
|
+
value_field: z.string(),
|
|
23
|
+
target_value: z.number().optional(),
|
|
24
|
+
target_field: z.string().optional(),
|
|
25
|
+
format: ValueFormatSchema,
|
|
26
|
+
}).strict().optional(),
|
|
27
|
+
color: z.string().optional(),
|
|
28
|
+
}).strict()
|
|
29
|
+
|
|
30
|
+
export const GaugeCardWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
31
|
+
target: z.literal('gauge_card'),
|
|
32
|
+
card: GaugeCardViewConfigSchema,
|
|
33
|
+
query: QueryConfigSchema,
|
|
34
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import {
|
|
3
|
+
QueryConfigSchema,
|
|
4
|
+
ValueFormatSchema,
|
|
5
|
+
WidgetBaseSchema,
|
|
6
|
+
} from './common.js'
|
|
7
|
+
|
|
8
|
+
export const KpiCardViewConfigSchema = z.object({
|
|
9
|
+
title: z.string().optional(),
|
|
10
|
+
value: z.object({
|
|
11
|
+
field: z.string(),
|
|
12
|
+
format: ValueFormatSchema,
|
|
13
|
+
prefix: z.string().optional(),
|
|
14
|
+
suffix: z.string().optional(),
|
|
15
|
+
}).strict(),
|
|
16
|
+
subtitle: z.object({
|
|
17
|
+
text: z.string().optional(),
|
|
18
|
+
field: z.string().optional(),
|
|
19
|
+
}).strict().optional(),
|
|
20
|
+
comparison: z.object({
|
|
21
|
+
field: z.string(),
|
|
22
|
+
format: ValueFormatSchema,
|
|
23
|
+
positive_is_good: z.boolean().optional(),
|
|
24
|
+
compact: z.object({
|
|
25
|
+
show: z.boolean().optional(),
|
|
26
|
+
template: z.string().optional(),
|
|
27
|
+
}).strict().optional(),
|
|
28
|
+
tooltip: z.object({
|
|
29
|
+
label: z.string().optional(),
|
|
30
|
+
template: z.string().optional(),
|
|
31
|
+
}).strict().optional(),
|
|
32
|
+
}).strict().optional(),
|
|
33
|
+
sparkline: z.object({
|
|
34
|
+
type: z.enum(['line']).optional(),
|
|
35
|
+
field: z.string(),
|
|
36
|
+
x: z.string(),
|
|
37
|
+
show_axes: z.boolean().optional(),
|
|
38
|
+
show_labels: z.boolean().optional(),
|
|
39
|
+
fill: z.object({
|
|
40
|
+
type: z.enum(['gradient', 'solid']).optional(),
|
|
41
|
+
}).strict().optional(),
|
|
42
|
+
}).strict().optional(),
|
|
43
|
+
}).strict()
|
|
44
|
+
|
|
45
|
+
export const KpiCardWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
46
|
+
target: z.literal('kpi_card'),
|
|
47
|
+
card: KpiCardViewConfigSchema,
|
|
48
|
+
query: QueryConfigSchema,
|
|
49
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import {
|
|
3
|
+
ChartFieldRefSchema,
|
|
4
|
+
QueryConfigSchema,
|
|
5
|
+
ValueFormatSchema,
|
|
6
|
+
WidgetBaseSchema,
|
|
7
|
+
} from './common.js'
|
|
8
|
+
|
|
9
|
+
export const PivotTableViewConfigSchema = z.object({
|
|
10
|
+
rows: z.array(ChartFieldRefSchema).min(1),
|
|
11
|
+
columns: z.array(ChartFieldRefSchema).optional(),
|
|
12
|
+
values: z.array(z.object({
|
|
13
|
+
field: z.string(),
|
|
14
|
+
label: z.string().optional(),
|
|
15
|
+
format: ValueFormatSchema,
|
|
16
|
+
aggregation: z.enum(['sum', 'count', 'avg', 'min', 'max']).optional(),
|
|
17
|
+
}).strict()).min(1),
|
|
18
|
+
}).strict()
|
|
19
|
+
|
|
20
|
+
export const PivotTableWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
21
|
+
target: z.literal('pivot_table'),
|
|
22
|
+
pivot: PivotTableViewConfigSchema,
|
|
23
|
+
query: QueryConfigSchema,
|
|
24
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import {
|
|
3
|
+
ChartFieldRefSchema,
|
|
4
|
+
QueryConfigSchema,
|
|
5
|
+
WidgetBaseSchema,
|
|
6
|
+
} from './common.js'
|
|
7
|
+
|
|
8
|
+
export const TableViewConfigSchema = z.object({
|
|
9
|
+
columns: z.array(ChartFieldRefSchema).optional(),
|
|
10
|
+
pagination: z.boolean().optional(),
|
|
11
|
+
page_size: z.number().int().positive().optional(),
|
|
12
|
+
}).strict()
|
|
13
|
+
|
|
14
|
+
export const TableWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
15
|
+
target: z.literal('table'),
|
|
16
|
+
table: TableViewConfigSchema.optional(),
|
|
17
|
+
query: QueryConfigSchema,
|
|
18
|
+
})
|
|
@@ -70,7 +70,8 @@ type EffectiveGroupByItem = {
|
|
|
70
70
|
const CALC_IDENTIFIER_RE = /\b[a-zA-Z_][a-zA-Z0-9_]*\b/g;
|
|
71
71
|
const LOOKUP_CALL_RE = /lookup\(\s*(\$variables(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\s*,\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)/g;
|
|
72
72
|
const VARIABLE_PATH_PREFIX_RE = /^\$variables\.?/;
|
|
73
|
-
const SAFE_CALC_EXPRESSION_RE = /^[\d+\-*/().\s]+$/;
|
|
73
|
+
const SAFE_CALC_EXPRESSION_RE = /^[\d+\-*/().\s?:<>=!]+$/;
|
|
74
|
+
const RELATIVE_DURATION_RE = /^(\d+)(h|d|w|mo|y)$/;
|
|
74
75
|
const FILTER_OPERATORS = {
|
|
75
76
|
eq: Filters.EQ,
|
|
76
77
|
neq: Filters.NEQ,
|
|
@@ -165,6 +166,12 @@ async function getQueryWidgetData(
|
|
|
165
166
|
query: QueryConfig,
|
|
166
167
|
variables: DashboardVariables,
|
|
167
168
|
): Promise<DashboardWidgetData> {
|
|
169
|
+
const metricSelect = getSingleAggregateMetricSelect(query);
|
|
170
|
+
|
|
171
|
+
if (metricSelect) {
|
|
172
|
+
return getMetricWidgetData(adminforth, query, metricSelect);
|
|
173
|
+
}
|
|
174
|
+
|
|
168
175
|
const selectedRows = isAggregateQuery(query)
|
|
169
176
|
? await buildAggregateQueryRows(adminforth, query, variables)
|
|
170
177
|
: buildPlainQueryRows(
|
|
@@ -196,6 +203,64 @@ async function getQueryWidgetData(
|
|
|
196
203
|
};
|
|
197
204
|
}
|
|
198
205
|
|
|
206
|
+
async function getMetricWidgetData(
|
|
207
|
+
adminforth: IAdminForth,
|
|
208
|
+
query: QueryConfig,
|
|
209
|
+
metric: QueryAggregateSelectItem,
|
|
210
|
+
): Promise<DashboardWidgetData> {
|
|
211
|
+
const [currentValues = {}] = await getAggregateRows(
|
|
212
|
+
adminforth,
|
|
213
|
+
query.resource,
|
|
214
|
+
query.filters,
|
|
215
|
+
[metric],
|
|
216
|
+
[],
|
|
217
|
+
);
|
|
218
|
+
const values: Record<string, unknown> = {
|
|
219
|
+
[metric.as]: currentValues[metric.as] ?? 0,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const rows = query.sparkline
|
|
223
|
+
? await getMetricSparklineRows(adminforth, query, metric, getAdminForthFilters(query.filters))
|
|
224
|
+
: [values];
|
|
225
|
+
const columns = Array.from(new Set([
|
|
226
|
+
metric.as,
|
|
227
|
+
...(query.sparkline ? [query.sparkline.as] : []),
|
|
228
|
+
]));
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
kind: 'aggregate',
|
|
232
|
+
columns,
|
|
233
|
+
rows,
|
|
234
|
+
values,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function getMetricSparklineRows(
|
|
239
|
+
adminforth: IAdminForth,
|
|
240
|
+
query: QueryConfig,
|
|
241
|
+
metric: QueryAggregateSelectItem,
|
|
242
|
+
filters: DashboardWidgetFilters,
|
|
243
|
+
) {
|
|
244
|
+
const sparkline = query.sparkline!;
|
|
245
|
+
const groupBy = [{
|
|
246
|
+
field: sparkline.field,
|
|
247
|
+
as: sparkline.as,
|
|
248
|
+
grain: sparkline.grain,
|
|
249
|
+
}];
|
|
250
|
+
const rows = await getAggregateRows(
|
|
251
|
+
adminforth,
|
|
252
|
+
query.resource,
|
|
253
|
+
filters,
|
|
254
|
+
[metric],
|
|
255
|
+
groupBy,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
return rows.map((row) => ({
|
|
259
|
+
...query.sparkline?.fill_missing,
|
|
260
|
+
...row,
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
|
|
199
264
|
async function getResourceRows(
|
|
200
265
|
adminforth: IAdminForth,
|
|
201
266
|
resourceId: string,
|
|
@@ -237,7 +302,7 @@ async function buildAggregateQueryRows(
|
|
|
237
302
|
async function getAggregateRows(
|
|
238
303
|
adminforth: IAdminForth,
|
|
239
304
|
resourceId: string,
|
|
240
|
-
baseFilters: FilterExpression | undefined,
|
|
305
|
+
baseFilters: FilterExpression | DashboardWidgetFilters | undefined,
|
|
241
306
|
select: QueryAggregateSelectItem[],
|
|
242
307
|
groupBy: EffectiveGroupByItem[],
|
|
243
308
|
) {
|
|
@@ -405,6 +470,21 @@ function isAggregateQuery(query: QueryConfig) {
|
|
|
405
470
|
);
|
|
406
471
|
}
|
|
407
472
|
|
|
473
|
+
function getSingleAggregateMetricSelect(query: QueryConfig) {
|
|
474
|
+
if (query.group_by?.length) {
|
|
475
|
+
return undefined;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const select = query.select ?? [];
|
|
479
|
+
const aggregateItems = select.filter(isAggregateSelectItem);
|
|
480
|
+
|
|
481
|
+
if (aggregateItems.length !== 1 || aggregateItems.length !== select.length) {
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return aggregateItems[0];
|
|
486
|
+
}
|
|
487
|
+
|
|
408
488
|
function isFieldSelectItem(item: QuerySelectItem): item is QueryFieldSelectItem {
|
|
409
489
|
return 'field' in item && !('agg' in item);
|
|
410
490
|
}
|
|
@@ -678,13 +758,59 @@ function toAdminForthFilter(filter: FilterExpression): IAdminForthSingleFilter |
|
|
|
678
758
|
|
|
679
759
|
for (const [operator, createFilter] of Object.entries(FILTER_OPERATORS)) {
|
|
680
760
|
if (Object.prototype.hasOwnProperty.call(filter, operator)) {
|
|
681
|
-
return createFilter(filter.field, filter[operator as keyof typeof FILTER_OPERATORS]);
|
|
761
|
+
return createFilter(filter.field, resolveFilterValue(filter[operator as keyof typeof FILTER_OPERATORS]));
|
|
682
762
|
}
|
|
683
763
|
}
|
|
684
764
|
|
|
685
765
|
return Filters.AND([]);
|
|
686
766
|
}
|
|
687
767
|
|
|
768
|
+
function resolveFilterValue(value: unknown): unknown {
|
|
769
|
+
if (Array.isArray(value)) {
|
|
770
|
+
return value.map((item) => resolveFilterValue(item));
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (!isRecord(value)) {
|
|
774
|
+
return value;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (value.now === true) {
|
|
778
|
+
return new Date().toISOString();
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (typeof value.now_minus === 'string') {
|
|
782
|
+
return subtractDuration(new Date(), value.now_minus).toISOString();
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return value;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function subtractDuration(now: Date, duration: string) {
|
|
789
|
+
const match = duration.match(RELATIVE_DURATION_RE);
|
|
790
|
+
|
|
791
|
+
if (!match) {
|
|
792
|
+
throw new Error(`Unsupported relative date duration: ${duration}`);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const amount = Number(match[1]);
|
|
796
|
+
const unit = match[2];
|
|
797
|
+
const date = new Date(now);
|
|
798
|
+
|
|
799
|
+
if (unit === 'h') {
|
|
800
|
+
date.setUTCHours(date.getUTCHours() - amount);
|
|
801
|
+
} else if (unit === 'd') {
|
|
802
|
+
date.setUTCDate(date.getUTCDate() - amount);
|
|
803
|
+
} else if (unit === 'w') {
|
|
804
|
+
date.setUTCDate(date.getUTCDate() - amount * 7);
|
|
805
|
+
} else if (unit === 'mo') {
|
|
806
|
+
date.setUTCMonth(date.getUTCMonth() - amount);
|
|
807
|
+
} else if (unit === 'y') {
|
|
808
|
+
date.setUTCFullYear(date.getUTCFullYear() - amount);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return date;
|
|
812
|
+
}
|
|
813
|
+
|
|
688
814
|
function toFiniteNumber(value: unknown) {
|
|
689
815
|
const numberValue = typeof value === 'number' ? value : Number(value);
|
|
690
816
|
return Number.isFinite(numberValue) ? numberValue : 0;
|