@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.
Files changed (39) hide show
  1. package/README.md +11 -28
  2. package/custom/model/dashboard.types.ts +236 -42
  3. package/custom/runtime/DashboardRuntime.vue +2 -1
  4. package/custom/runtime/WidgetRenderer.vue +2 -1
  5. package/custom/skills/adminforth-dashboard/SKILL.md +4 -4
  6. package/custom/widgets/chart/ChartWidget.vue +45 -12
  7. package/custom/widgets/chart/chart.types.ts +83 -0
  8. package/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -38
  9. package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
  10. package/custom/widgets/pivot-table/PivotTableWidget.vue +8 -10
  11. package/custom/widgets/table/TableWidget.vue +1 -1
  12. package/dist/custom/model/dashboard.types.d.ts +25 -1
  13. package/dist/custom/model/dashboard.types.js +133 -42
  14. package/dist/custom/model/dashboard.types.ts +236 -42
  15. package/dist/custom/queries/useDashboardConfig.d.ts +0 -2
  16. package/dist/custom/queries/useWidgetData.d.ts +0 -2
  17. package/dist/custom/runtime/DashboardRuntime.vue +2 -1
  18. package/dist/custom/runtime/WidgetRenderer.vue +2 -1
  19. package/dist/custom/skills/adminforth-dashboard/SKILL.md +4 -4
  20. package/dist/custom/widgets/chart/ChartWidget.vue +45 -12
  21. package/dist/custom/widgets/chart/chart.types.d.ts +15 -0
  22. package/dist/custom/widgets/chart/chart.types.js +46 -0
  23. package/dist/custom/widgets/chart/chart.types.ts +83 -0
  24. package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -38
  25. package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
  26. package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +8 -10
  27. package/dist/custom/widgets/table/TableWidget.vue +1 -1
  28. package/dist/endpoint/widgets.js +20 -3
  29. package/dist/schema/api.d.ts +0 -240
  30. package/dist/schema/widget.d.ts +0 -132
  31. package/dist/schema/widget.js +30 -16
  32. package/dist/services/widgetConfigValidator.js +9 -49
  33. package/dist/services/widgetDataService.d.ts +0 -9
  34. package/dist/services/widgetDataService.js +12 -30
  35. package/endpoint/widgets.ts +26 -3
  36. package/package.json +1 -1
  37. package/schema/widget.ts +34 -17
  38. package/services/widgetConfigValidator.ts +10 -57
  39. 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`, `minWidth`, `maxWidth` | Optional explicit layout constraints. |
34
- | `dataSource` | Optional resource or aggregate data source definition. |
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` | `columns`, `pagination`, `pageSize` | Uses `dataSource.type = 'resource'` or legacy `query` to display resource rows with backend pagination unless `pagination` is `false`. |
42
- | `chart` | `chart` | `type`, `x_field`, `y_field`, `label_field`, `value_field`, `bucket_field`, `buckets`, `series`, `series_name`, `color`, `colors` | Supports legacy `query` or `dataSource.type = 'aggregate'` with `groupBy`. |
43
- | `kpi_card` | `kpi_card` | `value_field`, `label_field`, `prefix`, `suffix` | Reads the first row or aggregate values and formats one numeric value. |
44
- | `gauge_card` | `gauge_card` | `value_field`, `min`, `max`, `min_field`, `max_field`, `suffix`, `color` | Reads the first row or aggregate values and renders progress between static or field-driven bounds. |
45
- | `pivot_table` | `pivot_table` | `row_field`, `column_field`, `value_field`, `aggregation` | Supports legacy row-based queries and grouped aggregate rows. `aggregation` supports `count` and `sum`. |
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
- resourceId: string
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
- resourceId: string
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
- groupBy?:
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
- `query` remains supported for backwards compatibility. When both are present, widgets prefer `dataSource`.
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
- const target = normalizeDashboardWidgetTarget(normalized.target ?? normalized.type)
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
- if (target && normalized.target === undefined) {
166
+ const target = normalizeDashboardWidgetTarget(normalized.target)
167
+
168
+ if (target !== undefined) {
136
169
  normalized.target = target
137
170
  }
138
171
 
139
- if (target === 'kpi_card') {
140
- const kpiCardConfig = normalizeKpiCardConfig(normalized)
172
+ return normalized
173
+ }
141
174
 
142
- if (kpiCardConfig !== undefined) {
143
- normalized.kpi_card = kpiCardConfig
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 (target === 'gauge_card') {
148
- const gaugeCardConfig = normalizeGaugeCardConfig(normalized)
183
+ if (Object.prototype.hasOwnProperty.call(serialized, 'maxWidth')) {
184
+ serialized.max_width = widget.maxWidth
185
+ delete serialized.maxWidth
186
+ }
149
187
 
150
- if (gaugeCardConfig !== undefined) {
151
- normalized.gauge_card = gaugeCardConfig
152
- }
188
+ if (widget.table !== undefined) {
189
+ serialized.table = serializeTableConfigForEditor(widget.table)
153
190
  }
154
191
 
155
- return normalized
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 normalizeKpiCardConfig(value: Record<string, unknown>) {
173
- const config = isRecord(value.kpi_card) ? { ...value.kpi_card } : {}
280
+ function normalizeWidgetLayoutConfig(value: Record<string, unknown>) {
281
+ if (value.min_width !== undefined) {
282
+ value.minWidth = value.min_width
283
+ }
174
284
 
175
- if (typeof value.valueField === 'string' && config.value_field === undefined) {
176
- config.value_field = value.valueField
285
+ if (value.max_width !== undefined) {
286
+ value.maxWidth = value.max_width
177
287
  }
288
+ }
178
289
 
179
- if (typeof value.labelField === 'string' && config.label_field === undefined) {
180
- config.label_field = value.labelField
290
+ function normalizeTableConfig(value: unknown) {
291
+ if (!isRecord(value)) {
292
+ return value
181
293
  }
182
294
 
183
- if (typeof value.prefix === 'string' && config.prefix === undefined) {
184
- config.prefix = value.prefix
295
+ const normalized = { ...value }
296
+
297
+ if (normalized.page_size !== undefined) {
298
+ normalized.pageSize = normalized.page_size
185
299
  }
186
300
 
187
- if (typeof value.suffix === 'string' && config.suffix === undefined) {
188
- config.suffix = value.suffix
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 Object.keys(config).length ? config : value.kpi_card
335
+ return value
192
336
  }
193
337
 
194
- function normalizeGaugeCardConfig(value: Record<string, unknown>) {
195
- const config = isRecord(value.gauge_card) ? { ...value.gauge_card } : {}
338
+ function normalizeGroupByRule(value: unknown) {
339
+ if (!isRecord(value) || typeof value.type !== 'string') {
340
+ return value
341
+ }
196
342
 
197
- if (typeof value.valueField === 'string' && config.value_field === undefined) {
198
- config.value_field = value.valueField
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.min !== undefined && config.min === undefined) {
202
- config.min = value.min
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
- if (value.max !== undefined && config.max === undefined) {
206
- config.max = value.max
359
+ return value
360
+ }
361
+
362
+ function serializeTableConfigForEditor(value: unknown) {
363
+ if (!isRecord(value)) {
364
+ return value
207
365
  }
208
366
 
209
- if (typeof value.minField === 'string' && config.min_field === undefined) {
210
- config.min_field = value.minField
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
- if (typeof value.maxField === 'string' && config.max_field === undefined) {
214
- config.max_field = value.maxField
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
- if (typeof value.suffix === 'string' && config.suffix === undefined) {
218
- config.suffix = value.suffix
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
- if (typeof value.color === 'string' && config.color === undefined) {
222
- config.color = value.color
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
- return Object.keys(config).length ? config : value.gauge_card
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 `query.resource`, not `resourceId`.
123
- - Use `query.select`, not `columns`.
124
- - Use `query.order`, not `sort`.
125
- - Use `query.limit` for row count.
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.series_name"
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 xField = computed(() => chartConfig.value?.x_field || columns.value[0])
131
- const yField = computed(() => chartConfig.value?.y_field || columns.value[1])
132
- const labelField = computed(() => chartConfig.value?.label_field || columns.value[0])
133
- const valueField = computed(() => chartConfig.value?.value_field || columns.value[1])
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?.value_field) {
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?.value_field ? labelField.value : 'label')
151
- const pieValueField = computed(() => chartConfig.value?.value_field ? valueField.value : '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?.bucket_field
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?.bucket_field ? 'label' : labelField.value)
171
- const barValueField = computed(() => chartConfig.value?.bucket_field ? 'count' : valueField.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
+ }