@adminforth/dashboard 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +43 -52
  2. package/custom/composables/useElementSize.ts +17 -2
  3. package/custom/model/dashboard.types.ts +385 -98
  4. package/custom/runtime/DashboardRuntime.vue +2 -1
  5. package/custom/runtime/WidgetRenderer.vue +2 -1
  6. package/custom/skills/adminforth-dashboard/SKILL.md +8 -4
  7. package/custom/widgets/chart/ChartWidget.vue +36 -35
  8. package/custom/widgets/chart/bar/BarChart.vue +20 -12
  9. package/custom/widgets/chart/chart.types.ts +42 -8
  10. package/custom/widgets/chart/chart.utils.ts +11 -0
  11. package/custom/widgets/chart/funnel/FunnelChart.vue +6 -4
  12. package/custom/widgets/chart/line/LineChart.vue +23 -15
  13. package/custom/widgets/chart/stacked-bar/StackedBarChart.vue +28 -43
  14. package/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -43
  15. package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -10
  16. package/custom/widgets/pivot-table/PivotTableWidget.vue +10 -11
  17. package/custom/widgets/table/TableWidget.vue +9 -4
  18. package/dist/custom/composables/useElementSize.js +14 -2
  19. package/dist/custom/composables/useElementSize.ts +17 -2
  20. package/dist/custom/model/dashboard.types.d.ts +179 -38
  21. package/dist/custom/model/dashboard.types.js +108 -42
  22. package/dist/custom/model/dashboard.types.ts +385 -98
  23. package/dist/custom/queries/useDashboardConfig.d.ts +832 -68
  24. package/dist/custom/queries/useWidgetData.d.ts +828 -64
  25. package/dist/custom/runtime/DashboardRuntime.vue +2 -1
  26. package/dist/custom/runtime/WidgetRenderer.vue +2 -1
  27. package/dist/custom/skills/adminforth-dashboard/SKILL.md +8 -4
  28. package/dist/custom/widgets/chart/ChartWidget.vue +36 -35
  29. package/dist/custom/widgets/chart/bar/BarChart.vue +20 -12
  30. package/dist/custom/widgets/chart/chart.types.d.ts +14 -8
  31. package/dist/custom/widgets/chart/chart.types.js +23 -0
  32. package/dist/custom/widgets/chart/chart.types.ts +42 -8
  33. package/dist/custom/widgets/chart/chart.utils.d.ts +1 -0
  34. package/dist/custom/widgets/chart/chart.utils.js +7 -0
  35. package/dist/custom/widgets/chart/chart.utils.ts +11 -0
  36. package/dist/custom/widgets/chart/funnel/FunnelChart.vue +6 -4
  37. package/dist/custom/widgets/chart/line/LineChart.vue +23 -15
  38. package/dist/custom/widgets/chart/stacked-bar/StackedBarChart.vue +28 -43
  39. package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -43
  40. package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -10
  41. package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +10 -11
  42. package/dist/custom/widgets/table/TableWidget.vue +9 -4
  43. package/dist/endpoint/widgets.js +23 -3
  44. package/dist/schema/api.d.ts +2637 -933
  45. package/dist/schema/widget.d.ts +1562 -582
  46. package/dist/schema/widget.js +207 -127
  47. package/dist/services/widgetConfigValidator.js +16 -80
  48. package/dist/services/widgetDataService.d.ts +0 -9
  49. package/dist/services/widgetDataService.js +356 -97
  50. package/endpoint/dashboard.ts +1 -1
  51. package/endpoint/widgets.ts +29 -3
  52. package/package.json +1 -1
  53. package/schema/widget.ts +221 -121
  54. package/services/widgetConfigValidator.ts +29 -100
  55. package/services/widgetDataService.ts +478 -129
@@ -1,42 +1,5 @@
1
1
  import type { ChartWidgetConfig } from '../widgets/chart/chart.types.js'
2
2
 
3
- export type AggregationOperation = 'sum' | 'count' | 'avg' | 'min' | 'max' | 'median'
4
-
5
- export type AggregationRule = {
6
- operation: AggregationOperation
7
- field?: string
8
- }
9
-
10
- export type GroupByRule =
11
- | {
12
- type: 'field'
13
- field: string
14
- }
15
- | {
16
- type: 'date_trunc'
17
- field: string
18
- truncation: 'day' | 'week' | 'month' | 'year'
19
- timezone?: string
20
- }
21
-
22
- export type ResourceWidgetDataSource = {
23
- type: 'resource'
24
- resourceId: string
25
- columns?: string[]
26
- sort?: unknown
27
- filters?: unknown
28
- }
29
-
30
- export type AggregateWidgetDataSource = {
31
- type: 'aggregate'
32
- resourceId: string
33
- aggregations: Record<string, AggregationRule>
34
- groupBy?: GroupByRule
35
- filters?: unknown
36
- }
37
-
38
- export type WidgetDataSource = ResourceWidgetDataSource | AggregateWidgetDataSource
39
-
40
3
  export type DashboardConfig = {
41
4
  version: number
42
5
  groups: DashboardGroupConfig[]
@@ -50,18 +13,19 @@ export type DashboardGroupConfig = {
50
13
  }
51
14
 
52
15
  export type DashboardGroupMoveDirection = 'up' | 'down'
53
-
54
16
  export type DashboardWidgetMoveDirection = 'up' | 'down'
55
-
56
- export type DashboardWidgetTarget =
57
- | 'empty'
58
- | 'table'
59
- | 'chart'
60
- | 'kpi_card'
61
- | 'pivot_table'
62
- | 'gauge_card'
63
-
17
+ export type DashboardWidgetTarget = 'empty' | 'table' | 'chart' | 'kpi_card' | 'pivot_table' | 'gauge_card'
64
18
  export type DashboardWidgetSize = 'small' | 'medium' | 'large' | 'wide' | 'full'
19
+ export type QueryAggregateOperation = 'sum' | 'count' | 'count_distinct' | 'avg' | 'min' | 'max' | 'median'
20
+ export type TimeGrain = 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'
21
+ export type ValueFormat =
22
+ | 'number'
23
+ | 'compact_number'
24
+ | 'currency'
25
+ | 'percent'
26
+ | 'percent_delta'
27
+ | 'number_delta'
28
+ | 'currency_delta'
65
29
 
66
30
  export type WidgetLayout = {
67
31
  size?: DashboardWidgetSize
@@ -71,7 +35,7 @@ export type WidgetLayout = {
71
35
  height?: number
72
36
  }
73
37
 
74
- export type DashboardWidgetConfig = {
38
+ export type WidgetBaseConfig = {
75
39
  id: string
76
40
  group_id: string
77
41
  label?: string
@@ -81,16 +45,200 @@ export type DashboardWidgetConfig = {
81
45
  minWidth?: number
82
46
  maxWidth?: number | null
83
47
  order: number
84
- target: DashboardWidgetTarget
85
- dataSource?: WidgetDataSource
86
- chart?: ChartWidgetConfig
87
- table?: unknown
88
- kpi_card?: unknown
89
- pivot_table?: unknown
90
- gauge_card?: unknown
91
- query?: unknown
92
48
  }
93
49
 
50
+ export type FilterExpression =
51
+ | { and: FilterExpression[] }
52
+ | { or: FilterExpression[] }
53
+ | Array<FilterExpression>
54
+ | {
55
+ field: string
56
+ eq?: unknown
57
+ neq?: unknown
58
+ gt?: unknown
59
+ gte?: unknown
60
+ lt?: unknown
61
+ lte?: unknown
62
+ in?: unknown[]
63
+ not_in?: unknown[]
64
+ like?: unknown
65
+ ilike?: unknown
66
+ }
67
+
68
+ export type QueryFieldSelectItem = {
69
+ field: string
70
+ as?: string
71
+ grain?: TimeGrain
72
+ }
73
+
74
+ export type QueryAggregateSelectItem = {
75
+ agg: QueryAggregateOperation
76
+ field?: string
77
+ as: string
78
+ filters?: FilterExpression
79
+ }
80
+
81
+ export type QueryCalcSelectItem = {
82
+ calc: string
83
+ as: string
84
+ }
85
+
86
+ export type QuerySelectItem = QueryFieldSelectItem | QueryAggregateSelectItem | QueryCalcSelectItem
87
+
88
+ export type QueryGroupByItem =
89
+ | string
90
+ | {
91
+ field: string
92
+ as?: string
93
+ grain?: TimeGrain
94
+ timezone?: string
95
+ }
96
+
97
+ export type QueryOrderByItem = {
98
+ field: string
99
+ direction?: 'asc' | 'desc'
100
+ }
101
+
102
+ export type QueryConfig = {
103
+ resource: string
104
+ select?: QuerySelectItem[]
105
+ filters?: FilterExpression
106
+ groupBy?: QueryGroupByItem[]
107
+ orderBy?: QueryOrderByItem[]
108
+ limit?: number
109
+ offset?: number
110
+ timeSeries?: {
111
+ field: string
112
+ grain: TimeGrain
113
+ timezone?: string
114
+ }
115
+ period?: {
116
+ field: string
117
+ gte?: unknown
118
+ lt?: unknown
119
+ }
120
+ bucket?: {
121
+ field: string
122
+ buckets: Array<{ label: string, min?: number, max?: number }>
123
+ }
124
+ calcs?: QueryCalcSelectItem[]
125
+ formatting?: Record<string, unknown>
126
+ }
127
+
128
+ export type FunnelQueryConfig = {
129
+ steps: FunnelQueryStep[]
130
+ }
131
+
132
+ export type FunnelQueryStep = {
133
+ name: string
134
+ resource: string
135
+ metric: QueryAggregateSelectItem
136
+ filters?: FilterExpression
137
+ }
138
+
139
+ export type FieldRef = string | {
140
+ field: string
141
+ label?: string
142
+ format?: ValueFormat
143
+ }
144
+
145
+ export type TableViewConfig = {
146
+ columns?: FieldRef[]
147
+ pagination?: boolean
148
+ pageSize?: number
149
+ }
150
+
151
+ export type KpiCardViewConfig = {
152
+ title?: string
153
+ value: {
154
+ field: string
155
+ format?: ValueFormat
156
+ prefix?: string
157
+ suffix?: string
158
+ }
159
+ subtitle?: {
160
+ text?: string
161
+ field?: string
162
+ }
163
+ comparison?: unknown
164
+ sparkline?: unknown
165
+ }
166
+
167
+ export type GaugeCardViewConfig = {
168
+ title?: string
169
+ value: {
170
+ field: string
171
+ format?: ValueFormat
172
+ prefix?: string
173
+ suffix?: string
174
+ }
175
+ target?: {
176
+ value?: number
177
+ field?: string
178
+ label?: string
179
+ }
180
+ progress?: {
181
+ valueField: string
182
+ targetValue?: number
183
+ targetField?: string
184
+ format?: ValueFormat
185
+ }
186
+ color?: string
187
+ }
188
+
189
+ export type PivotTableViewConfig = {
190
+ rows: FieldRef[]
191
+ columns?: FieldRef[]
192
+ values: Array<{
193
+ field: string
194
+ label?: string
195
+ format?: ValueFormat
196
+ aggregation?: 'sum' | 'count' | 'avg' | 'min' | 'max'
197
+ }>
198
+ }
199
+
200
+ export type EmptyWidgetConfig = WidgetBaseConfig & {
201
+ target: 'empty'
202
+ }
203
+
204
+ export type TableWidgetConfig = WidgetBaseConfig & {
205
+ target: 'table'
206
+ table?: TableViewConfig
207
+ query: QueryConfig
208
+ }
209
+
210
+ export type ChartDashboardWidgetConfig = WidgetBaseConfig & {
211
+ target: 'chart'
212
+ chart: ChartWidgetConfig
213
+ query: QueryConfig | FunnelQueryConfig
214
+ }
215
+
216
+ export type KpiCardWidgetConfig = WidgetBaseConfig & {
217
+ target: 'kpi_card'
218
+ card: KpiCardViewConfig
219
+ query: QueryConfig
220
+ }
221
+
222
+ export type GaugeCardWidgetConfig = WidgetBaseConfig & {
223
+ target: 'gauge_card'
224
+ card: GaugeCardViewConfig
225
+ query: QueryConfig
226
+ }
227
+
228
+ export type PivotTableWidgetConfig = WidgetBaseConfig & {
229
+ target: 'pivot_table'
230
+ pivot: PivotTableViewConfig
231
+ query: QueryConfig
232
+ }
233
+
234
+ export type DashboardWidgetConfig =
235
+ | EmptyWidgetConfig
236
+ | TableWidgetConfig
237
+ | ChartDashboardWidgetConfig
238
+ | KpiCardWidgetConfig
239
+ | GaugeCardWidgetConfig
240
+ | PivotTableWidgetConfig
241
+
94
242
  export type DashboardWidgetTableData = {
95
243
  kind?: 'table'
96
244
  columns: string[]
@@ -108,6 +256,12 @@ export type DashboardWidgetAggregateData = {
108
256
  columns: string[]
109
257
  rows: Record<string, unknown>[]
110
258
  values?: Record<string, unknown>
259
+ pagination?: {
260
+ page: number
261
+ pageSize: number
262
+ total: number
263
+ totalPages: number
264
+ }
111
265
  }
112
266
 
113
267
  export type DashboardWidgetData = DashboardWidgetTableData | DashboardWidgetAggregateData
@@ -130,31 +284,73 @@ export function normalizeDashboardWidgetConfig(config: unknown) {
130
284
  }
131
285
 
132
286
  const normalized: Record<string, unknown> = { ...config }
133
- const target = normalizeDashboardWidgetTarget(normalized.target ?? normalized.type)
287
+ normalizeWidgetLayoutConfig(normalized)
134
288
 
135
- if (target && normalized.target === undefined) {
136
- normalized.target = target
289
+ if (normalized.query !== undefined) {
290
+ normalized.query = normalizeQueryConfig(normalized.query)
137
291
  }
138
292
 
139
- if (target === 'kpi_card') {
140
- const kpiCardConfig = normalizeKpiCardConfig(normalized)
293
+ if (normalized.table !== undefined) {
294
+ normalized.table = normalizeTableConfig(normalized.table)
295
+ }
141
296
 
142
- if (kpiCardConfig !== undefined) {
143
- normalized.kpi_card = kpiCardConfig
144
- }
297
+ if (normalized.card !== undefined) {
298
+ normalized.card = normalizeCardConfig(normalized.card)
145
299
  }
146
300
 
147
- if (target === 'gauge_card') {
148
- const gaugeCardConfig = normalizeGaugeCardConfig(normalized)
301
+ if (normalized.pivot !== undefined) {
302
+ normalized.pivot = normalizePivotConfig(normalized.pivot)
303
+ }
149
304
 
150
- if (gaugeCardConfig !== undefined) {
151
- normalized.gauge_card = gaugeCardConfig
152
- }
305
+ const target = normalizeDashboardWidgetTarget(normalized.target)
306
+
307
+ if (target !== undefined) {
308
+ normalized.target = target
153
309
  }
154
310
 
155
311
  return normalized
156
312
  }
157
313
 
314
+ export function serializeDashboardWidgetConfigForEditor(widget: DashboardWidgetConfig) {
315
+ const serialized: Record<string, unknown> = { ...widget }
316
+
317
+ if (Object.prototype.hasOwnProperty.call(serialized, 'minWidth')) {
318
+ serialized.min_width = widget.minWidth
319
+ delete serialized.minWidth
320
+ }
321
+
322
+ if (Object.prototype.hasOwnProperty.call(serialized, 'maxWidth')) {
323
+ serialized.max_width = widget.maxWidth
324
+ delete serialized.maxWidth
325
+ }
326
+
327
+ if ('query' in widget) {
328
+ serialized.query = serializeQueryConfigForEditor(widget.query)
329
+ }
330
+
331
+ if ('table' in widget && widget.table !== undefined) {
332
+ serialized.table = serializeTableConfigForEditor(widget.table)
333
+ }
334
+
335
+ if ('card' in widget && widget.card !== undefined) {
336
+ serialized.card = serializeCardConfigForEditor(widget.card)
337
+ }
338
+
339
+ if ('pivot' in widget && widget.pivot !== undefined) {
340
+ serialized.pivot = serializePivotConfigForEditor(widget.pivot)
341
+ }
342
+
343
+ return serialized
344
+ }
345
+
346
+ export function getFieldRefField(value: FieldRef | undefined) {
347
+ return typeof value === 'string' ? value : value?.field
348
+ }
349
+
350
+ export function getFieldRefLabel(value: FieldRef | undefined) {
351
+ return typeof value === 'string' ? value : value?.label
352
+ }
353
+
158
354
  function normalizeDashboardWidgetTarget(value: unknown): DashboardWidgetTarget | undefined {
159
355
  switch (value) {
160
356
  case 'empty':
@@ -169,62 +365,153 @@ function normalizeDashboardWidgetTarget(value: unknown): DashboardWidgetTarget |
169
365
  }
170
366
  }
171
367
 
172
- function normalizeKpiCardConfig(value: Record<string, unknown>) {
173
- const config = isRecord(value.kpi_card) ? { ...value.kpi_card } : {}
368
+ function normalizeWidgetLayoutConfig(value: Record<string, unknown>) {
369
+ if (value.min_width !== undefined) {
370
+ value.minWidth = value.min_width
371
+ }
174
372
 
175
- if (typeof value.valueField === 'string' && config.value_field === undefined) {
176
- config.value_field = value.valueField
373
+ if (value.max_width !== undefined) {
374
+ value.maxWidth = value.max_width
177
375
  }
376
+ }
178
377
 
179
- if (typeof value.labelField === 'string' && config.label_field === undefined) {
180
- config.label_field = value.labelField
378
+ function normalizeQueryConfig(value: unknown): unknown {
379
+ if (!isRecord(value)) {
380
+ return value
181
381
  }
182
382
 
183
- if (typeof value.prefix === 'string' && config.prefix === undefined) {
184
- config.prefix = value.prefix
383
+ if (Array.isArray(value.steps)) {
384
+ return {
385
+ steps: value.steps.map((step) => normalizeFunnelQueryStep(step)),
386
+ }
185
387
  }
186
388
 
187
- if (typeof value.suffix === 'string' && config.suffix === undefined) {
188
- config.suffix = value.suffix
389
+ return {
390
+ ...value,
391
+ ...(Array.isArray(value.group_by) ? { groupBy: value.group_by } : {}),
392
+ ...(Array.isArray(value.order_by) ? { orderBy: value.order_by } : {}),
393
+ ...(value.time_series !== undefined ? { timeSeries: value.time_series } : {}),
394
+ }
395
+ }
396
+
397
+ function normalizeFunnelQueryStep(value: unknown) {
398
+ if (!isRecord(value)) {
399
+ return value
189
400
  }
190
401
 
191
- return Object.keys(config).length ? config : value.kpi_card
402
+ return {
403
+ ...value,
404
+ ...(typeof value.resource_id === 'string' ? { resource: value.resource_id } : {}),
405
+ }
192
406
  }
193
407
 
194
- function normalizeGaugeCardConfig(value: Record<string, unknown>) {
195
- const config = isRecord(value.gauge_card) ? { ...value.gauge_card } : {}
408
+ function normalizeTableConfig(value: unknown) {
409
+ if (!isRecord(value)) {
410
+ return value
411
+ }
196
412
 
197
- if (typeof value.valueField === 'string' && config.value_field === undefined) {
198
- config.value_field = value.valueField
413
+ return {
414
+ ...value,
415
+ ...(value.page_size !== undefined ? { pageSize: value.page_size } : {}),
199
416
  }
417
+ }
200
418
 
201
- if (value.min !== undefined && config.min === undefined) {
202
- config.min = value.min
419
+ function normalizeCardConfig(value: unknown): unknown {
420
+ if (!isRecord(value)) {
421
+ return value
203
422
  }
204
423
 
205
- if (value.max !== undefined && config.max === undefined) {
206
- config.max = value.max
424
+ const normalized = { ...value }
425
+
426
+ if (isRecord(normalized.progress)) {
427
+ normalized.progress = {
428
+ ...normalized.progress,
429
+ ...(normalized.progress.value_field !== undefined ? { valueField: normalized.progress.value_field } : {}),
430
+ ...(normalized.progress.target_value !== undefined ? { targetValue: normalized.progress.target_value } : {}),
431
+ ...(normalized.progress.target_field !== undefined ? { targetField: normalized.progress.target_field } : {}),
432
+ }
207
433
  }
208
434
 
209
- if (typeof value.minField === 'string' && config.min_field === undefined) {
210
- config.min_field = value.minField
435
+ if (isRecord(normalized.comparison)) {
436
+ normalized.comparison = {
437
+ ...normalized.comparison,
438
+ ...(normalized.comparison.positive_is_good !== undefined ? { positiveIsGood: normalized.comparison.positive_is_good } : {}),
439
+ }
211
440
  }
212
441
 
213
- if (typeof value.maxField === 'string' && config.max_field === undefined) {
214
- config.max_field = value.maxField
442
+ return normalized
443
+ }
444
+
445
+ function normalizePivotConfig(value: unknown): unknown {
446
+ return value
447
+ }
448
+
449
+ function serializeQueryConfigForEditor(value: QueryConfig | FunnelQueryConfig) {
450
+ if ('steps' in value) {
451
+ return {
452
+ steps: value.steps.map((step) => ({
453
+ ...step,
454
+ resource_id: step.resource,
455
+ resource: undefined,
456
+ })).map((step) => removeUndefinedFields(step)),
457
+ }
215
458
  }
216
459
 
217
- if (typeof value.suffix === 'string' && config.suffix === undefined) {
218
- config.suffix = value.suffix
460
+ return removeUndefinedFields({
461
+ ...value,
462
+ group_by: value.groupBy,
463
+ groupBy: undefined,
464
+ order_by: value.orderBy,
465
+ orderBy: undefined,
466
+ time_series: value.timeSeries,
467
+ timeSeries: undefined,
468
+ })
469
+ }
470
+
471
+ function serializeTableConfigForEditor(value: TableViewConfig) {
472
+ return removeUndefinedFields({
473
+ ...value,
474
+ page_size: value.pageSize,
475
+ pageSize: undefined,
476
+ })
477
+ }
478
+
479
+ function serializeCardConfigForEditor(value: KpiCardViewConfig | GaugeCardViewConfig) {
480
+ const serialized: Record<string, unknown> = { ...value }
481
+
482
+ if (isRecord(serialized.progress)) {
483
+ serialized.progress = removeUndefinedFields({
484
+ ...serialized.progress,
485
+ value_field: serialized.progress.valueField,
486
+ valueField: undefined,
487
+ target_value: serialized.progress.targetValue,
488
+ targetValue: undefined,
489
+ target_field: serialized.progress.targetField,
490
+ targetField: undefined,
491
+ })
219
492
  }
220
493
 
221
- if (typeof value.color === 'string' && config.color === undefined) {
222
- config.color = value.color
494
+ if (isRecord(serialized.comparison)) {
495
+ serialized.comparison = removeUndefinedFields({
496
+ ...serialized.comparison,
497
+ positive_is_good: serialized.comparison.positiveIsGood,
498
+ positiveIsGood: undefined,
499
+ })
223
500
  }
224
501
 
225
- return Object.keys(config).length ? config : value.gauge_card
502
+ return removeUndefinedFields(serialized)
503
+ }
504
+
505
+ function serializePivotConfigForEditor(value: PivotTableViewConfig) {
506
+ return value
507
+ }
508
+
509
+ function removeUndefinedFields<T extends Record<string, unknown>>(value: T) {
510
+ return Object.fromEntries(
511
+ Object.entries(value).filter(([, item]) => item !== undefined),
512
+ )
226
513
  }
227
514
 
228
- function isRecord(value: unknown): value is Record<string, unknown> {
515
+ function isRecord(value: unknown): value is Record<string, any> {
229
516
  return typeof value === 'object' && value !== null
230
517
  }
@@ -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,11 @@ 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 `query`, not `data_source`.
123
+ - Use `resource`, not `resource_id`.
124
+ - Use `group_by`, not `groupBy`.
125
+ - Use `order_by`, not `orderBy`.
126
+ - Use `page_size`, not `pageSize`.
127
+ - For funnel charts, use `query.steps` as an ordered array of `{ name, resource, metric, filters }` steps.
128
+ - Use `card` for KPI and gauge widget view config.
129
+ - Use `pivot` for pivot table view config.