@adminforth/dashboard 1.2.0 → 1.4.0

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