@adminforth/dashboard 1.1.0 → 1.2.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 +11 -28
- package/custom/model/dashboard.types.ts +236 -42
- package/custom/runtime/DashboardRuntime.vue +2 -1
- package/custom/runtime/WidgetRenderer.vue +2 -1
- package/custom/skills/adminforth-dashboard/SKILL.md +4 -4
- package/custom/widgets/chart/ChartWidget.vue +45 -12
- package/custom/widgets/chart/chart.types.ts +83 -0
- package/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -38
- package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
- package/custom/widgets/pivot-table/PivotTableWidget.vue +8 -10
- package/custom/widgets/table/TableWidget.vue +1 -1
- package/dist/custom/model/dashboard.types.d.ts +25 -1
- package/dist/custom/model/dashboard.types.js +133 -42
- package/dist/custom/model/dashboard.types.ts +236 -42
- package/dist/custom/queries/useDashboardConfig.d.ts +0 -2
- package/dist/custom/queries/useWidgetData.d.ts +0 -2
- package/dist/custom/runtime/DashboardRuntime.vue +2 -1
- package/dist/custom/runtime/WidgetRenderer.vue +2 -1
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +4 -4
- package/dist/custom/widgets/chart/ChartWidget.vue +45 -12
- package/dist/custom/widgets/chart/chart.types.d.ts +15 -0
- package/dist/custom/widgets/chart/chart.types.js +46 -0
- package/dist/custom/widgets/chart/chart.types.ts +83 -0
- package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -38
- package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
- package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +8 -10
- package/dist/custom/widgets/table/TableWidget.vue +1 -1
- package/dist/endpoint/widgets.js +20 -3
- package/dist/schema/api.d.ts +0 -240
- package/dist/schema/widget.d.ts +0 -132
- package/dist/schema/widget.js +30 -16
- package/dist/services/widgetConfigValidator.js +9 -49
- package/dist/services/widgetDataService.d.ts +0 -9
- package/dist/services/widgetDataService.js +12 -30
- package/endpoint/widgets.ts +26 -3
- package/package.json +1 -1
- package/schema/widget.ts +34 -17
- package/services/widgetConfigValidator.ts +10 -57
- package/services/widgetDataService.ts +10 -45
package/README.md
CHANGED
|
@@ -30,19 +30,18 @@ Each widget has common fields:
|
|
|
30
30
|
| `target` | Widget type: `table`, `chart`, `kpi_card`, `pivot_table`, or `gauge_card`. |
|
|
31
31
|
| `order` | Widget order inside its group. |
|
|
32
32
|
| `size` | Preset width: `small`, `medium`, `large`, `wide`, or `full`. |
|
|
33
|
-
| `width`, `height`, `
|
|
34
|
-
| `
|
|
35
|
-
| `query` | Optional AdminForth resource query used to load widget data. |
|
|
33
|
+
| `width`, `height`, `min_width`, `max_width` | Optional explicit layout constraints. |
|
|
34
|
+
| `data_source` | Resource or aggregate data source definition. |
|
|
36
35
|
|
|
37
36
|
## Widget Support Matrix
|
|
38
37
|
|
|
39
38
|
| Widget target | Config field | Main settings | Data usage |
|
|
40
39
|
| --- | --- | --- | --- |
|
|
41
|
-
| `table` | `table` | `
|
|
42
|
-
| `chart` | `chart` | `type`, `x_field`, `y_field`, `label_field`, `value_field`, `bucket_field`, `buckets`, `series`, `series_name`, `color`, `colors` |
|
|
43
|
-
| `kpi_card` | `kpi_card` | `value_field`, `label_field`, `prefix`, `suffix` | Reads
|
|
44
|
-
| `gauge_card` | `gauge_card` | `value_field`, `min`, `max`, `min_field`, `max_field`, `suffix`, `color` | Reads the first row
|
|
45
|
-
| `pivot_table` | `pivot_table` | `row_field`, `column_field`, `value_field`, `aggregation` |
|
|
40
|
+
| `table` | `table` | `pagination`, `page_size` | Uses `data_source.type = 'resource'` to display resource rows with backend pagination unless `pagination` is `false`. |
|
|
41
|
+
| `chart` | `chart` | `type`, `x_field`, `y_field`, `label_field`, `value_field`, `bucket_field`, `buckets`, `series`, `series_name`, `color`, `colors` | Uses `data_source.type = 'aggregate'` with `group_by`. |
|
|
42
|
+
| `kpi_card` | `kpi_card` | `value_field`, `label_field`, `prefix`, `suffix` | Reads aggregate values or the first returned row from `data_source`. |
|
|
43
|
+
| `gauge_card` | `gauge_card` | `value_field`, `min`, `max`, `min_field`, `max_field`, `suffix`, `color` | Reads aggregate values or the first returned row from `data_source` and renders progress between static or field-driven bounds. |
|
|
44
|
+
| `pivot_table` | `pivot_table` | `row_field`, `column_field`, `value_field`, `aggregation` | Uses grouped aggregate rows from `data_source.type = 'aggregate'`. `aggregation` supports `count` and `sum`. |
|
|
46
45
|
|
|
47
46
|
Chart widget types:
|
|
48
47
|
|
|
@@ -55,41 +54,25 @@ Chart widget types:
|
|
|
55
54
|
| `funnel` | Uses `label_field`, `value_field`, and optional `colors`. |
|
|
56
55
|
| `histogram` | Uses the same bucket settings as `bar`. |
|
|
57
56
|
|
|
58
|
-
## Query Shape
|
|
59
|
-
|
|
60
|
-
```ts
|
|
61
|
-
type DashboardWidgetQuery = {
|
|
62
|
-
resource: string
|
|
63
|
-
select?: string[]
|
|
64
|
-
order?: {
|
|
65
|
-
field: string
|
|
66
|
-
direction: 'asc' | 'desc'
|
|
67
|
-
}
|
|
68
|
-
limit?: number
|
|
69
|
-
}
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
`resource` is an AdminForth `resourceId`. The query is executed through AdminForth resources, so widgets use the same resource contracts as the rest of the application.
|
|
73
|
-
|
|
74
57
|
## Data Source Shape
|
|
75
58
|
|
|
76
59
|
```ts
|
|
77
60
|
type WidgetDataSource =
|
|
78
61
|
| {
|
|
79
62
|
type: 'resource'
|
|
80
|
-
|
|
63
|
+
resource_id: string
|
|
81
64
|
columns?: string[]
|
|
82
65
|
filters?: unknown
|
|
83
66
|
sort?: unknown
|
|
84
67
|
}
|
|
85
68
|
| {
|
|
86
69
|
type: 'aggregate'
|
|
87
|
-
|
|
70
|
+
resource_id: string
|
|
88
71
|
aggregations: Record<string, {
|
|
89
72
|
operation: 'sum' | 'count' | 'avg' | 'min' | 'max' | 'median'
|
|
90
73
|
field?: string
|
|
91
74
|
}>
|
|
92
|
-
|
|
75
|
+
group_by?:
|
|
93
76
|
| { type: 'field'; field: string }
|
|
94
77
|
| {
|
|
95
78
|
type: 'date_trunc'
|
|
@@ -101,7 +84,7 @@ type WidgetDataSource =
|
|
|
101
84
|
}
|
|
102
85
|
```
|
|
103
86
|
|
|
104
|
-
`
|
|
87
|
+
`resource_id` is an AdminForth `resourceId`. The data source is executed through AdminForth resources, so widgets use the same resource contracts as the rest of the application.
|
|
105
88
|
|
|
106
89
|
## Runtime Structure
|
|
107
90
|
|
|
@@ -88,7 +88,6 @@ export type DashboardWidgetConfig = {
|
|
|
88
88
|
kpi_card?: unknown
|
|
89
89
|
pivot_table?: unknown
|
|
90
90
|
gauge_card?: unknown
|
|
91
|
-
query?: unknown
|
|
92
91
|
}
|
|
93
92
|
|
|
94
93
|
export type DashboardWidgetTableData = {
|
|
@@ -112,6 +111,30 @@ export type DashboardWidgetAggregateData = {
|
|
|
112
111
|
|
|
113
112
|
export type DashboardWidgetData = DashboardWidgetTableData | DashboardWidgetAggregateData
|
|
114
113
|
|
|
114
|
+
export type NormalizedKpiCardWidgetConfig = {
|
|
115
|
+
valueField?: string
|
|
116
|
+
labelField?: string
|
|
117
|
+
prefix?: string
|
|
118
|
+
suffix?: string
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type NormalizedGaugeCardWidgetConfig = {
|
|
122
|
+
valueField?: string
|
|
123
|
+
min?: number | string
|
|
124
|
+
max?: number | string
|
|
125
|
+
minField?: string
|
|
126
|
+
maxField?: string
|
|
127
|
+
suffix?: string
|
|
128
|
+
color?: string
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export type NormalizedPivotTableWidgetConfig = {
|
|
132
|
+
rowField?: string
|
|
133
|
+
columnField?: string
|
|
134
|
+
valueField?: string
|
|
135
|
+
aggregation?: 'count' | 'sum'
|
|
136
|
+
}
|
|
137
|
+
|
|
115
138
|
export function normalizeDashboardConfig(config: unknown): DashboardConfig {
|
|
116
139
|
const value = isRecord(config) ? config : {}
|
|
117
140
|
|
|
@@ -130,29 +153,114 @@ export function normalizeDashboardWidgetConfig(config: unknown) {
|
|
|
130
153
|
}
|
|
131
154
|
|
|
132
155
|
const normalized: Record<string, unknown> = { ...config }
|
|
133
|
-
|
|
156
|
+
normalizeWidgetLayoutConfig(normalized)
|
|
157
|
+
|
|
158
|
+
if (normalized.table !== undefined) {
|
|
159
|
+
normalized.table = normalizeTableConfig(normalized.table)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (normalized.data_source !== undefined) {
|
|
163
|
+
normalized.dataSource = normalizeWidgetDataSource(normalized.data_source)
|
|
164
|
+
}
|
|
134
165
|
|
|
135
|
-
|
|
166
|
+
const target = normalizeDashboardWidgetTarget(normalized.target)
|
|
167
|
+
|
|
168
|
+
if (target !== undefined) {
|
|
136
169
|
normalized.target = target
|
|
137
170
|
}
|
|
138
171
|
|
|
139
|
-
|
|
140
|
-
|
|
172
|
+
return normalized
|
|
173
|
+
}
|
|
141
174
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
175
|
+
export function serializeDashboardWidgetConfigForEditor(widget: DashboardWidgetConfig) {
|
|
176
|
+
const serialized: Record<string, unknown> = { ...widget }
|
|
177
|
+
|
|
178
|
+
if (Object.prototype.hasOwnProperty.call(serialized, 'minWidth')) {
|
|
179
|
+
serialized.min_width = widget.minWidth
|
|
180
|
+
delete serialized.minWidth
|
|
145
181
|
}
|
|
146
182
|
|
|
147
|
-
if (
|
|
148
|
-
|
|
183
|
+
if (Object.prototype.hasOwnProperty.call(serialized, 'maxWidth')) {
|
|
184
|
+
serialized.max_width = widget.maxWidth
|
|
185
|
+
delete serialized.maxWidth
|
|
186
|
+
}
|
|
149
187
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
188
|
+
if (widget.table !== undefined) {
|
|
189
|
+
serialized.table = serializeTableConfigForEditor(widget.table)
|
|
153
190
|
}
|
|
154
191
|
|
|
155
|
-
|
|
192
|
+
if (widget.dataSource !== undefined) {
|
|
193
|
+
serialized.data_source = serializeWidgetDataSourceForEditor(widget.dataSource)
|
|
194
|
+
delete serialized.dataSource
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return serialized
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function normalizeKpiCardWidgetConfig(value: unknown): NormalizedKpiCardWidgetConfig | undefined {
|
|
201
|
+
const config = asWidgetConfigRecord(value)
|
|
202
|
+
|
|
203
|
+
if (!config) {
|
|
204
|
+
return undefined
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const valueField = getStringField(config, 'value_field')
|
|
208
|
+
const labelField = getStringField(config, 'label_field')
|
|
209
|
+
const prefix = getStringField(config, 'prefix')
|
|
210
|
+
const suffix = getStringField(config, 'suffix')
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
...(valueField !== undefined ? { valueField } : {}),
|
|
214
|
+
...(labelField !== undefined ? { labelField } : {}),
|
|
215
|
+
...(prefix !== undefined ? { prefix } : {}),
|
|
216
|
+
...(suffix !== undefined ? { suffix } : {}),
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function normalizeGaugeCardWidgetConfig(value: unknown): NormalizedGaugeCardWidgetConfig | undefined {
|
|
221
|
+
const config = asWidgetConfigRecord(value)
|
|
222
|
+
|
|
223
|
+
if (!config) {
|
|
224
|
+
return undefined
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const valueField = getStringField(config, 'value_field')
|
|
228
|
+
const minField = getStringField(config, 'min_field')
|
|
229
|
+
const maxField = getStringField(config, 'max_field')
|
|
230
|
+
const suffix = getStringField(config, 'suffix')
|
|
231
|
+
const color = getStringField(config, 'color')
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
...(valueField !== undefined ? { valueField } : {}),
|
|
235
|
+
...(config.min !== undefined ? { min: config.min as number | string } : {}),
|
|
236
|
+
...(config.max !== undefined ? { max: config.max as number | string } : {}),
|
|
237
|
+
...(minField !== undefined ? { minField } : {}),
|
|
238
|
+
...(maxField !== undefined ? { maxField } : {}),
|
|
239
|
+
...(suffix !== undefined ? { suffix } : {}),
|
|
240
|
+
...(color !== undefined ? { color } : {}),
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function normalizePivotTableWidgetConfig(value: unknown): NormalizedPivotTableWidgetConfig | undefined {
|
|
245
|
+
const config = asWidgetConfigRecord(value)
|
|
246
|
+
|
|
247
|
+
if (!config) {
|
|
248
|
+
return undefined
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const rowField = getStringField(config, 'row_field')
|
|
252
|
+
const columnField = getStringField(config, 'column_field')
|
|
253
|
+
const valueField = getStringField(config, 'value_field')
|
|
254
|
+
const aggregation = config.aggregation === 'count' || config.aggregation === 'sum'
|
|
255
|
+
? config.aggregation
|
|
256
|
+
: undefined
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
...(rowField !== undefined ? { rowField } : {}),
|
|
260
|
+
...(columnField !== undefined ? { columnField } : {}),
|
|
261
|
+
...(valueField !== undefined ? { valueField } : {}),
|
|
262
|
+
...(aggregation !== undefined ? { aggregation } : {}),
|
|
263
|
+
}
|
|
156
264
|
}
|
|
157
265
|
|
|
158
266
|
function normalizeDashboardWidgetTarget(value: unknown): DashboardWidgetTarget | undefined {
|
|
@@ -169,60 +277,146 @@ function normalizeDashboardWidgetTarget(value: unknown): DashboardWidgetTarget |
|
|
|
169
277
|
}
|
|
170
278
|
}
|
|
171
279
|
|
|
172
|
-
function
|
|
173
|
-
|
|
280
|
+
function normalizeWidgetLayoutConfig(value: Record<string, unknown>) {
|
|
281
|
+
if (value.min_width !== undefined) {
|
|
282
|
+
value.minWidth = value.min_width
|
|
283
|
+
}
|
|
174
284
|
|
|
175
|
-
if (
|
|
176
|
-
|
|
285
|
+
if (value.max_width !== undefined) {
|
|
286
|
+
value.maxWidth = value.max_width
|
|
177
287
|
}
|
|
288
|
+
}
|
|
178
289
|
|
|
179
|
-
|
|
180
|
-
|
|
290
|
+
function normalizeTableConfig(value: unknown) {
|
|
291
|
+
if (!isRecord(value)) {
|
|
292
|
+
return value
|
|
181
293
|
}
|
|
182
294
|
|
|
183
|
-
|
|
184
|
-
|
|
295
|
+
const normalized = { ...value }
|
|
296
|
+
|
|
297
|
+
if (normalized.page_size !== undefined) {
|
|
298
|
+
normalized.pageSize = normalized.page_size
|
|
185
299
|
}
|
|
186
300
|
|
|
187
|
-
|
|
188
|
-
|
|
301
|
+
return normalized
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function normalizeWidgetDataSource(value: unknown) {
|
|
305
|
+
if (!isRecord(value) || typeof value.type !== 'string') {
|
|
306
|
+
return value
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const resourceId = typeof value.resource_id === 'string'
|
|
310
|
+
? value.resource_id
|
|
311
|
+
: undefined
|
|
312
|
+
|
|
313
|
+
if (value.type === 'resource') {
|
|
314
|
+
return {
|
|
315
|
+
type: 'resource',
|
|
316
|
+
...(resourceId !== undefined ? { resourceId } : {}),
|
|
317
|
+
...(value.columns !== undefined ? { columns: value.columns } : {}),
|
|
318
|
+
...(value.filters !== undefined ? { filters: value.filters } : {}),
|
|
319
|
+
...(value.sort !== undefined ? { sort: value.sort } : {}),
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (value.type === 'aggregate') {
|
|
324
|
+
const groupBy = normalizeGroupByRule(value.group_by)
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
type: 'aggregate',
|
|
328
|
+
...(resourceId !== undefined ? { resourceId } : {}),
|
|
329
|
+
...(value.aggregations !== undefined ? { aggregations: value.aggregations } : {}),
|
|
330
|
+
...(groupBy !== undefined ? { groupBy } : {}),
|
|
331
|
+
...(value.filters !== undefined ? { filters: value.filters } : {}),
|
|
332
|
+
}
|
|
189
333
|
}
|
|
190
334
|
|
|
191
|
-
return
|
|
335
|
+
return value
|
|
192
336
|
}
|
|
193
337
|
|
|
194
|
-
function
|
|
195
|
-
|
|
338
|
+
function normalizeGroupByRule(value: unknown) {
|
|
339
|
+
if (!isRecord(value) || typeof value.type !== 'string') {
|
|
340
|
+
return value
|
|
341
|
+
}
|
|
196
342
|
|
|
197
|
-
if (
|
|
198
|
-
|
|
343
|
+
if (value.type === 'field') {
|
|
344
|
+
return {
|
|
345
|
+
type: 'field',
|
|
346
|
+
...(value.field !== undefined ? { field: value.field } : {}),
|
|
347
|
+
}
|
|
199
348
|
}
|
|
200
349
|
|
|
201
|
-
if (value.
|
|
202
|
-
|
|
350
|
+
if (value.type === 'date_trunc') {
|
|
351
|
+
return {
|
|
352
|
+
type: 'date_trunc',
|
|
353
|
+
...(value.field !== undefined ? { field: value.field } : {}),
|
|
354
|
+
...(value.truncation !== undefined ? { truncation: value.truncation } : {}),
|
|
355
|
+
...(value.timezone !== undefined ? { timezone: value.timezone } : {}),
|
|
356
|
+
}
|
|
203
357
|
}
|
|
204
358
|
|
|
205
|
-
|
|
206
|
-
|
|
359
|
+
return value
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function serializeTableConfigForEditor(value: unknown) {
|
|
363
|
+
if (!isRecord(value)) {
|
|
364
|
+
return value
|
|
207
365
|
}
|
|
208
366
|
|
|
209
|
-
|
|
210
|
-
|
|
367
|
+
const serialized = { ...value }
|
|
368
|
+
|
|
369
|
+
if (Object.prototype.hasOwnProperty.call(serialized, 'pageSize')) {
|
|
370
|
+
serialized.page_size = serialized.pageSize
|
|
371
|
+
delete serialized.pageSize
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return serialized
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function serializeWidgetDataSourceForEditor(value: WidgetDataSource) {
|
|
378
|
+
if (value.type === 'resource') {
|
|
379
|
+
return {
|
|
380
|
+
type: 'resource',
|
|
381
|
+
resource_id: value.resourceId,
|
|
382
|
+
...(value.columns !== undefined ? { columns: value.columns } : {}),
|
|
383
|
+
...(value.filters !== undefined ? { filters: value.filters } : {}),
|
|
384
|
+
...(value.sort !== undefined ? { sort: value.sort } : {}),
|
|
385
|
+
}
|
|
211
386
|
}
|
|
212
387
|
|
|
213
|
-
|
|
214
|
-
|
|
388
|
+
return {
|
|
389
|
+
type: 'aggregate',
|
|
390
|
+
resource_id: value.resourceId,
|
|
391
|
+
aggregations: value.aggregations,
|
|
392
|
+
...(value.groupBy !== undefined ? { group_by: serializeGroupByRuleForEditor(value.groupBy) } : {}),
|
|
393
|
+
...(value.filters !== undefined ? { filters: value.filters } : {}),
|
|
215
394
|
}
|
|
395
|
+
}
|
|
216
396
|
|
|
217
|
-
|
|
218
|
-
|
|
397
|
+
function serializeGroupByRuleForEditor(value: GroupByRule) {
|
|
398
|
+
if (value.type === 'field') {
|
|
399
|
+
return {
|
|
400
|
+
type: 'field',
|
|
401
|
+
field: value.field,
|
|
402
|
+
}
|
|
219
403
|
}
|
|
220
404
|
|
|
221
|
-
|
|
222
|
-
|
|
405
|
+
return {
|
|
406
|
+
type: 'date_trunc',
|
|
407
|
+
field: value.field,
|
|
408
|
+
truncation: value.truncation,
|
|
409
|
+
...(value.timezone !== undefined ? { timezone: value.timezone } : {}),
|
|
223
410
|
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function asWidgetConfigRecord(value: unknown): Record<string, unknown> | undefined {
|
|
414
|
+
return isRecord(value) ? value : undefined
|
|
415
|
+
}
|
|
224
416
|
|
|
225
|
-
|
|
417
|
+
function getStringField(record: Record<string, unknown>, key: string) {
|
|
418
|
+
const value = record[key]
|
|
419
|
+
return typeof value === 'string' ? value : undefined
|
|
226
420
|
}
|
|
227
421
|
|
|
228
422
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -211,6 +211,7 @@ import type {
|
|
|
211
211
|
DashboardWidgetConfig,
|
|
212
212
|
DashboardWidgetMoveDirection,
|
|
213
213
|
} from '../model/dashboard.types.js'
|
|
214
|
+
import { serializeDashboardWidgetConfigForEditor } from '../model/dashboard.types.js'
|
|
214
215
|
|
|
215
216
|
const props = defineProps<{
|
|
216
217
|
dashboardSlug: string
|
|
@@ -388,7 +389,7 @@ async function removeWidget(widgetId: string) {
|
|
|
388
389
|
|
|
389
390
|
function editWidget(widget: DashboardWidgetConfig) {
|
|
390
391
|
editingWidgetId.value = widget.id
|
|
391
|
-
widgetConfigCode.value = stringifyYaml(widget)
|
|
392
|
+
widgetConfigCode.value = stringifyYaml(serializeDashboardWidgetConfigForEditor(widget))
|
|
392
393
|
widgetConfigError.value = ''
|
|
393
394
|
widgetConfigFieldErrors.value = []
|
|
394
395
|
}
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
<script setup lang="ts">
|
|
27
27
|
import { computed } from 'vue'
|
|
28
28
|
import type { DashboardWidgetConfig } from '../model/dashboard.types.js'
|
|
29
|
+
import { normalizeChartWidgetConfig } from '../widgets/chart/chart.types.js'
|
|
29
30
|
import { getWidgetLabel, getWidgetRegistration } from '../widgets/registry.js'
|
|
30
31
|
|
|
31
32
|
const props = defineProps<{
|
|
@@ -52,7 +53,7 @@ const widgetTitle = computed(() => {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
if (props.widget.target === 'chart') {
|
|
55
|
-
return props.widget.chart?.title || 'Untitled chart'
|
|
56
|
+
return normalizeChartWidgetConfig(props.widget.chart)?.title || 'Untitled chart'
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
return getWidgetLabel(props.widget.target)
|
|
@@ -119,7 +119,7 @@ Use the current schema keys exactly:
|
|
|
119
119
|
|
|
120
120
|
- Use `target`, not `type`.
|
|
121
121
|
- Use `label`, not `title`.
|
|
122
|
-
- Use `
|
|
123
|
-
- Use `
|
|
124
|
-
- Use `
|
|
125
|
-
- Use `
|
|
122
|
+
- Use `data_source`, not `dataSource`.
|
|
123
|
+
- Use `resource_id`, not `resourceId`.
|
|
124
|
+
- Use `group_by`, not `groupBy`.
|
|
125
|
+
- Use `page_size`, not `pageSize`.
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
:rows="rows"
|
|
27
27
|
:x-field="xField"
|
|
28
28
|
:y-field="yField"
|
|
29
|
-
:series-name="chartConfig.
|
|
29
|
+
:series-name="chartConfig.seriesName"
|
|
30
30
|
:color="chartConfig.color"
|
|
31
31
|
:height="chartHeight"
|
|
32
32
|
/>
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
import { computed, watch } from 'vue'
|
|
92
92
|
import { useWidgetData } from '../../queries/useWidgetData.js'
|
|
93
93
|
import type { DashboardWidgetConfig, DashboardWidgetTableData } from '../../model/dashboard.types.js'
|
|
94
|
+
import { normalizeChartWidgetConfig } from './chart.types.js'
|
|
94
95
|
import BarChart from './bar/BarChart.vue'
|
|
95
96
|
import FunnelChart from './funnel/FunnelChart.vue'
|
|
96
97
|
import HistogramChart from './histogram/HistogramChart.vue'
|
|
@@ -126,13 +127,45 @@ watch(
|
|
|
126
127
|
const chartData = computed(() => data.value?.data as DashboardWidgetTableData | null)
|
|
127
128
|
const rows = computed(() => chartData.value?.rows ?? [])
|
|
128
129
|
const columns = computed(() => chartData.value?.columns ?? [])
|
|
129
|
-
const chartConfig = computed(() => props.widget.chart)
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
const chartConfig = computed(() => normalizeChartWidgetConfig(props.widget.chart))
|
|
131
|
+
const aggregateGroupField = computed(() => {
|
|
132
|
+
const dataSource = props.widget.dataSource
|
|
133
|
+
|
|
134
|
+
if (dataSource?.type !== 'aggregate' || !dataSource.groupBy) {
|
|
135
|
+
return undefined
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return dataSource.groupBy.field
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
function resolveChartDimensionField(field: string | undefined, fallbackField: string | undefined) {
|
|
142
|
+
const resolvedField = field ?? fallbackField
|
|
143
|
+
|
|
144
|
+
if (!resolvedField) {
|
|
145
|
+
return ''
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (columns.value.includes(resolvedField)) {
|
|
149
|
+
return resolvedField
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (
|
|
153
|
+
aggregateGroupField.value
|
|
154
|
+
&& resolvedField === aggregateGroupField.value
|
|
155
|
+
&& columns.value.includes('group')
|
|
156
|
+
) {
|
|
157
|
+
return 'group'
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return resolvedField
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const xField = computed(() => resolveChartDimensionField(chartConfig.value?.xField, columns.value[0]))
|
|
164
|
+
const yField = computed(() => chartConfig.value?.yField || columns.value[1])
|
|
165
|
+
const labelField = computed(() => resolveChartDimensionField(chartConfig.value?.labelField, columns.value[0]))
|
|
166
|
+
const valueField = computed(() => chartConfig.value?.valueField || columns.value[1])
|
|
134
167
|
const pieRows = computed(() => {
|
|
135
|
-
if (chartConfig.value?.
|
|
168
|
+
if (chartConfig.value?.valueField) {
|
|
136
169
|
return rows.value
|
|
137
170
|
}
|
|
138
171
|
|
|
@@ -147,10 +180,10 @@ const pieRows = computed(() => {
|
|
|
147
180
|
|
|
148
181
|
return Array.from(groupedRows.values())
|
|
149
182
|
})
|
|
150
|
-
const pieLabelField = computed(() => chartConfig.value?.
|
|
151
|
-
const pieValueField = computed(() => chartConfig.value?.
|
|
183
|
+
const pieLabelField = computed(() => chartConfig.value?.valueField ? labelField.value : 'label')
|
|
184
|
+
const pieValueField = computed(() => chartConfig.value?.valueField ? valueField.value : 'value')
|
|
152
185
|
const barRows = computed(() => {
|
|
153
|
-
const bucketField = chartConfig.value?.
|
|
186
|
+
const bucketField = chartConfig.value?.bucketField
|
|
154
187
|
|
|
155
188
|
if (!bucketField) {
|
|
156
189
|
return rows.value
|
|
@@ -167,8 +200,8 @@ const barRows = computed(() => {
|
|
|
167
200
|
}).length,
|
|
168
201
|
}))
|
|
169
202
|
})
|
|
170
|
-
const barLabelField = computed(() => chartConfig.value?.
|
|
171
|
-
const barValueField = computed(() => chartConfig.value?.
|
|
203
|
+
const barLabelField = computed(() => chartConfig.value?.bucketField ? 'label' : labelField.value)
|
|
204
|
+
const barValueField = computed(() => chartConfig.value?.bucketField ? 'count' : valueField.value)
|
|
172
205
|
const stackedBarSeries = computed(() => {
|
|
173
206
|
if (chartConfig.value?.series?.length) {
|
|
174
207
|
return chartConfig.value.series
|
|
@@ -32,3 +32,86 @@ export type ChartWidgetConfig = {
|
|
|
32
32
|
color?: string
|
|
33
33
|
colors?: string[]
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
export type NormalizedChartWidgetConfig = {
|
|
37
|
+
type: ChartWidgetType
|
|
38
|
+
title?: string
|
|
39
|
+
xField?: string
|
|
40
|
+
yField?: string
|
|
41
|
+
labelField?: string
|
|
42
|
+
valueField?: string
|
|
43
|
+
bucketField?: string
|
|
44
|
+
buckets?: ChartWidgetBucketConfig[]
|
|
45
|
+
series?: ChartWidgetSeriesConfig[]
|
|
46
|
+
seriesName?: string
|
|
47
|
+
color?: string
|
|
48
|
+
colors?: string[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function normalizeChartWidgetConfig(value: unknown): NormalizedChartWidgetConfig | undefined {
|
|
52
|
+
const config = asChartWidgetConfigRecord(value)
|
|
53
|
+
|
|
54
|
+
if (!config) {
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const type = normalizeChartWidgetType(config.type)
|
|
59
|
+
|
|
60
|
+
if (!type) {
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const xField = getStringField(config, 'x_field')
|
|
65
|
+
const yField = getStringField(config, 'y_field')
|
|
66
|
+
const labelField = getStringField(config, 'label_field')
|
|
67
|
+
const valueField = getStringField(config, 'value_field')
|
|
68
|
+
const bucketField = getStringField(config, 'bucket_field')
|
|
69
|
+
const seriesName = getStringField(config, 'series_name')
|
|
70
|
+
const title = getStringField(config, 'title')
|
|
71
|
+
const color = getStringField(config, 'color')
|
|
72
|
+
const colors = Array.isArray(config.colors) ? config.colors as string[] : undefined
|
|
73
|
+
const buckets = Array.isArray(config.buckets) ? config.buckets as ChartWidgetBucketConfig[] : undefined
|
|
74
|
+
const series = Array.isArray(config.series) ? config.series as ChartWidgetSeriesConfig[] : undefined
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
type,
|
|
78
|
+
...(title !== undefined ? { title } : {}),
|
|
79
|
+
...(xField !== undefined ? { xField } : {}),
|
|
80
|
+
...(yField !== undefined ? { yField } : {}),
|
|
81
|
+
...(labelField !== undefined ? { labelField } : {}),
|
|
82
|
+
...(valueField !== undefined ? { valueField } : {}),
|
|
83
|
+
...(bucketField !== undefined ? { bucketField } : {}),
|
|
84
|
+
...(buckets !== undefined ? { buckets } : {}),
|
|
85
|
+
...(series !== undefined ? { series } : {}),
|
|
86
|
+
...(seriesName !== undefined ? { seriesName } : {}),
|
|
87
|
+
...(color !== undefined ? { color } : {}),
|
|
88
|
+
...(colors !== undefined ? { colors } : {}),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeChartWidgetType(value: unknown): ChartWidgetType | undefined {
|
|
93
|
+
switch (value) {
|
|
94
|
+
case 'line':
|
|
95
|
+
case 'pie':
|
|
96
|
+
case 'bar':
|
|
97
|
+
case 'stacked_bar':
|
|
98
|
+
case 'funnel':
|
|
99
|
+
case 'histogram':
|
|
100
|
+
return value
|
|
101
|
+
default:
|
|
102
|
+
return undefined
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function asChartWidgetConfigRecord(value: unknown): Record<string, unknown> | undefined {
|
|
107
|
+
return isRecord(value) ? value : undefined
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getStringField(record: Record<string, unknown>, key: string) {
|
|
111
|
+
const value = record[key]
|
|
112
|
+
return typeof value === 'string' ? value : undefined
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
116
|
+
return typeof value === 'object' && value !== null
|
|
117
|
+
}
|