@adminforth/dashboard 1.0.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 (55) hide show
  1. package/README.md +99 -54
  2. package/custom/api/dashboardApi.ts +9 -0
  3. package/custom/model/dashboard.types.ts +353 -2
  4. package/custom/queries/useWidgetData.ts +8 -4
  5. package/custom/runtime/DashboardRuntime.vue +2 -1
  6. package/custom/runtime/WidgetRenderer.vue +2 -1
  7. package/custom/runtime/WidgetShell.vue +8 -4
  8. package/custom/skills/adminforth-dashboard/SKILL.md +4 -4
  9. package/custom/widgets/chart/ChartWidget.vue +45 -12
  10. package/custom/widgets/chart/chart.types.ts +83 -0
  11. package/custom/widgets/chart/chart.utils.ts +2 -2
  12. package/custom/widgets/gauge-card/GaugeCardWidget.vue +63 -12
  13. package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
  14. package/custom/widgets/pivot-table/PivotTableWidget.vue +32 -12
  15. package/custom/widgets/table/TableWidget.vue +155 -30
  16. package/dist/custom/api/dashboardApi.d.ts +7 -1
  17. package/dist/custom/api/dashboardApi.js +4 -6
  18. package/dist/custom/api/dashboardApi.ts +9 -0
  19. package/dist/custom/model/dashboard.types.d.ts +70 -1
  20. package/dist/custom/model/dashboard.types.js +173 -1
  21. package/dist/custom/model/dashboard.types.ts +353 -2
  22. package/dist/custom/queries/useDashboardConfig.d.ts +42 -2
  23. package/dist/custom/queries/useWidgetData.d.ts +44 -3
  24. package/dist/custom/queries/useWidgetData.js +3 -3
  25. package/dist/custom/queries/useWidgetData.ts +8 -4
  26. package/dist/custom/runtime/DashboardRuntime.vue +2 -1
  27. package/dist/custom/runtime/WidgetRenderer.vue +2 -1
  28. package/dist/custom/runtime/WidgetShell.vue +8 -4
  29. package/dist/custom/skills/adminforth-dashboard/SKILL.md +4 -4
  30. package/dist/custom/widgets/chart/ChartWidget.vue +45 -12
  31. package/dist/custom/widgets/chart/chart.types.d.ts +15 -0
  32. package/dist/custom/widgets/chart/chart.types.js +46 -0
  33. package/dist/custom/widgets/chart/chart.types.ts +83 -0
  34. package/dist/custom/widgets/chart/chart.utils.d.ts +1 -1
  35. package/dist/custom/widgets/chart/chart.utils.js +2 -2
  36. package/dist/custom/widgets/chart/chart.utils.ts +2 -2
  37. package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +63 -12
  38. package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
  39. package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +32 -12
  40. package/dist/custom/widgets/table/TableWidget.vue +155 -30
  41. package/dist/endpoint/widgets.d.ts +6 -1
  42. package/dist/endpoint/widgets.js +41 -6
  43. package/dist/schema/api.d.ts +874 -444
  44. package/dist/schema/api.js +11 -2
  45. package/dist/schema/widget.d.ts +538 -132
  46. package/dist/schema/widget.js +138 -14
  47. package/dist/services/widgetConfigValidator.js +26 -40
  48. package/dist/services/widgetDataService.d.ts +7 -14
  49. package/dist/services/widgetDataService.js +115 -11
  50. package/endpoint/widgets.ts +56 -6
  51. package/package.json +1 -1
  52. package/schema/api.ts +11 -1
  53. package/schema/widget.ts +145 -15
  54. package/services/widgetConfigValidator.ts +36 -44
  55. package/services/widgetDataService.ts +175 -28
package/README.md CHANGED
@@ -2,58 +2,103 @@
2
2
 
3
3
  Dashboard plugin for AdminForth.
4
4
 
5
+ It adds configurable dashboard pages backed by an AdminForth resource. Dashboard records define groups and widgets, the plugin renders them under `/dashboard/:slug`, contributes a **Dashboards** sidebar group, and exposes endpoints for editing groups and widgets from the AdminForth UI.
6
+
7
+ Full setup guide: https://adminforth.dev/docs/tutorial/Plugins/dashboard/
8
+
9
+ ## Dashboard Config Shape
10
+
11
+ ```ts
12
+ type DashboardConfig = {
13
+ version: number
14
+ groups: {
15
+ id: string
16
+ label: string
17
+ order: number
18
+ }[]
19
+ widgets: DashboardWidgetConfig[]
20
+ }
21
+ ```
22
+
23
+ Each widget has common fields:
24
+
25
+ | Field | Description |
26
+ | --- | --- |
27
+ | `id` | Persisted widget id. |
28
+ | `group_id` | Group where the widget is rendered. |
29
+ | `label` | Optional widget title. |
30
+ | `target` | Widget type: `table`, `chart`, `kpi_card`, `pivot_table`, or `gauge_card`. |
31
+ | `order` | Widget order inside its group. |
32
+ | `size` | Preset width: `small`, `medium`, `large`, `wide`, or `full`. |
33
+ | `width`, `height`, `min_width`, `max_width` | Optional explicit layout constraints. |
34
+ | `data_source` | Resource or aggregate data source definition. |
35
+
36
+ ## Widget Support Matrix
37
+
38
+ | Widget target | Config field | Main settings | Data usage |
39
+ | --- | --- | --- | --- |
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`. |
45
+
46
+ Chart widget types:
47
+
48
+ | Chart type | Notes |
49
+ | --- | --- |
50
+ | `line` | Uses `x_field` and `y_field`; optional `series_name` and `color`. |
51
+ | `pie` | Uses `label_field` and optional `value_field`; without `value_field`, rows are counted by label. |
52
+ | `bar` | Uses `label_field` and `value_field`, or `bucket_field` with `buckets`. |
53
+ | `stacked_bar` | Uses `x_field` and `series`; if `series` is omitted, non-x columns become series. |
54
+ | `funnel` | Uses `label_field`, `value_field`, and optional `colors`. |
55
+ | `histogram` | Uses the same bucket settings as `bar`. |
56
+
57
+ ## Data Source Shape
58
+
59
+ ```ts
60
+ type WidgetDataSource =
61
+ | {
62
+ type: 'resource'
63
+ resource_id: string
64
+ columns?: string[]
65
+ filters?: unknown
66
+ sort?: unknown
67
+ }
68
+ | {
69
+ type: 'aggregate'
70
+ resource_id: string
71
+ aggregations: Record<string, {
72
+ operation: 'sum' | 'count' | 'avg' | 'min' | 'max' | 'median'
73
+ field?: string
74
+ }>
75
+ group_by?:
76
+ | { type: 'field'; field: string }
77
+ | {
78
+ type: 'date_trunc'
79
+ field: string
80
+ truncation: 'day' | 'week' | 'month' | 'year'
81
+ timezone?: string
82
+ }
83
+ filters?: unknown
84
+ }
85
+ ```
86
+
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.
88
+
89
+ ## Runtime Structure
90
+
91
+ ```text
5
92
  DashboardPage.vue
6
- └── DashboardRuntime.vue
7
- ├── DashboardGroup.vue
8
- └── WidgetShell.vue
9
- └── WidgetRenderer.vue
10
- ├── TableWidget.vue
11
- ├── ChartWidget.vue
12
- ├── KpiCardWidget.vue
13
- ├── PivotTableWidget.vue
14
- └── GaugeCardWidget.vue
15
- └── DashboardEditorPanel.vue
16
-
17
- src/features/dashboards/
18
-
19
- runtime/
20
- DashboardPage.vue
21
- DashboardRuntime.vue
22
- DashboardGroup.vue
23
- WidgetShell.vue
24
- WidgetRenderer.vue
25
-
26
- widgets/
27
- registry.ts
28
-
29
- table/
30
- TableWidget.vue
31
- TableWidgetEditor.vue
32
- table.adapter.ts
33
-
34
- chart/
35
- ChartWidget.vue
36
- ChartWidgetEditor.vue
37
- chart.adapter.ts
38
- charts/
39
- PieChart.vue
40
- LineChart.vue
41
- BarChart.vue
42
- StackedBarChart.vue
43
- FunnelChart.vue
44
- HistogramChart.vue
45
-
46
- kpi-card/
47
- KpiCardWidget.vue
48
- KpiCardWidgetEditor.vue
49
- kpi.adapter.ts
50
-
51
- pivot-table/
52
- PivotTableWidget.vue
53
- PivotTableWidgetEditor.vue
54
- pivot.adapter.ts
55
-
56
- gauge-card/
57
- GaugeCardWidget.vue
58
- GaugeCardWidgetEditor.vue
59
- gauge.adapter.ts
93
+ └── DashboardRuntime.vue
94
+ └── DashboardGroup.vue
95
+ └── WidgetShell.vue
96
+ └── WidgetRenderer.vue
97
+ ├── TableWidget.vue
98
+ ├── ChartWidget.vue
99
+ ├── KpiCardWidget.vue
100
+ ├── PivotTableWidget.vue
101
+ └── GaugeCardWidget.vue
102
+ ```
103
+
104
+ `DashboardPage.vue` loads a dashboard by slug, `DashboardRuntime.vue` renders ordered groups, `WidgetShell.vue` provides the widget frame and editor actions, and `WidgetRenderer.vue` selects the widget component by `target`.
@@ -24,6 +24,13 @@ export type DashboardWidgetDataResponse = {
24
24
  data: unknown
25
25
  }
26
26
 
27
+ export type DashboardWidgetDataRequest = {
28
+ pagination?: {
29
+ page: number
30
+ pageSize: number
31
+ }
32
+ }
33
+
27
34
  export class DashboardApiError extends Error {
28
35
  validationErrors: DashboardWidgetConfigValidationError[]
29
36
 
@@ -204,10 +211,12 @@ export const dashboardApi = {
204
211
  async getDashboardWidgetData(
205
212
  slug: string,
206
213
  widgetId: string,
214
+ request: DashboardWidgetDataRequest = {},
207
215
  ): Promise<DashboardWidgetDataResponse> {
208
216
  return callDashboardWidgetDataApi('/adminapi/v1/dashboard/get_dashboard_widget_data', {
209
217
  slug,
210
218
  widgetId,
219
+ ...request,
211
220
  })
212
221
  },
213
222
  }
@@ -1,5 +1,42 @@
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
+
3
40
  export type DashboardConfig = {
4
41
  version: number
5
42
  groups: DashboardGroupConfig[]
@@ -45,17 +82,57 @@ export type DashboardWidgetConfig = {
45
82
  maxWidth?: number | null
46
83
  order: number
47
84
  target: DashboardWidgetTarget
85
+ dataSource?: WidgetDataSource
48
86
  chart?: ChartWidgetConfig
49
87
  table?: unknown
50
88
  kpi_card?: unknown
51
89
  pivot_table?: unknown
52
90
  gauge_card?: unknown
53
- query?: unknown
54
91
  }
55
92
 
56
93
  export type DashboardWidgetTableData = {
94
+ kind?: 'table'
95
+ columns: string[]
96
+ rows: Record<string, unknown>[]
97
+ pagination?: {
98
+ page: number
99
+ pageSize: number
100
+ total: number
101
+ totalPages: number
102
+ }
103
+ }
104
+
105
+ export type DashboardWidgetAggregateData = {
106
+ kind: 'aggregate'
57
107
  columns: string[]
58
108
  rows: Record<string, unknown>[]
109
+ values?: Record<string, unknown>
110
+ }
111
+
112
+ export type DashboardWidgetData = DashboardWidgetTableData | DashboardWidgetAggregateData
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'
59
136
  }
60
137
 
61
138
  export function normalizeDashboardConfig(config: unknown): DashboardConfig {
@@ -64,8 +141,282 @@ export function normalizeDashboardConfig(config: unknown): DashboardConfig {
64
141
  return {
65
142
  version: typeof value.version === 'number' ? value.version : 1,
66
143
  groups: Array.isArray(value.groups) ? (value.groups as DashboardGroupConfig[]) : [],
67
- widgets: Array.isArray(value.widgets) ? (value.widgets as DashboardWidgetConfig[]) : [],
144
+ widgets: Array.isArray(value.widgets)
145
+ ? value.widgets.map((widget) => normalizeDashboardWidgetConfig(widget) as DashboardWidgetConfig)
146
+ : [],
147
+ }
148
+ }
149
+
150
+ export function normalizeDashboardWidgetConfig(config: unknown) {
151
+ if (!isRecord(config)) {
152
+ return config
153
+ }
154
+
155
+ const normalized: Record<string, unknown> = { ...config }
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
+ }
165
+
166
+ const target = normalizeDashboardWidgetTarget(normalized.target)
167
+
168
+ if (target !== undefined) {
169
+ normalized.target = target
170
+ }
171
+
172
+ return normalized
173
+ }
174
+
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
181
+ }
182
+
183
+ if (Object.prototype.hasOwnProperty.call(serialized, 'maxWidth')) {
184
+ serialized.max_width = widget.maxWidth
185
+ delete serialized.maxWidth
186
+ }
187
+
188
+ if (widget.table !== undefined) {
189
+ serialized.table = serializeTableConfigForEditor(widget.table)
190
+ }
191
+
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
+ }
264
+ }
265
+
266
+ function normalizeDashboardWidgetTarget(value: unknown): DashboardWidgetTarget | undefined {
267
+ switch (value) {
268
+ case 'empty':
269
+ case 'table':
270
+ case 'chart':
271
+ case 'kpi_card':
272
+ case 'pivot_table':
273
+ case 'gauge_card':
274
+ return value
275
+ default:
276
+ return undefined
277
+ }
278
+ }
279
+
280
+ function normalizeWidgetLayoutConfig(value: Record<string, unknown>) {
281
+ if (value.min_width !== undefined) {
282
+ value.minWidth = value.min_width
283
+ }
284
+
285
+ if (value.max_width !== undefined) {
286
+ value.maxWidth = value.max_width
287
+ }
288
+ }
289
+
290
+ function normalizeTableConfig(value: unknown) {
291
+ if (!isRecord(value)) {
292
+ return value
293
+ }
294
+
295
+ const normalized = { ...value }
296
+
297
+ if (normalized.page_size !== undefined) {
298
+ normalized.pageSize = normalized.page_size
299
+ }
300
+
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
+ }
333
+ }
334
+
335
+ return value
336
+ }
337
+
338
+ function normalizeGroupByRule(value: unknown) {
339
+ if (!isRecord(value) || typeof value.type !== 'string') {
340
+ return value
341
+ }
342
+
343
+ if (value.type === 'field') {
344
+ return {
345
+ type: 'field',
346
+ ...(value.field !== undefined ? { field: value.field } : {}),
347
+ }
348
+ }
349
+
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
+ }
357
+ }
358
+
359
+ return value
360
+ }
361
+
362
+ function serializeTableConfigForEditor(value: unknown) {
363
+ if (!isRecord(value)) {
364
+ return value
365
+ }
366
+
367
+ const serialized = { ...value }
368
+
369
+ if (Object.prototype.hasOwnProperty.call(serialized, 'pageSize')) {
370
+ serialized.page_size = serialized.pageSize
371
+ delete serialized.pageSize
68
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
+ }
386
+ }
387
+
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 } : {}),
394
+ }
395
+ }
396
+
397
+ function serializeGroupByRuleForEditor(value: GroupByRule) {
398
+ if (value.type === 'field') {
399
+ return {
400
+ type: 'field',
401
+ field: value.field,
402
+ }
403
+ }
404
+
405
+ return {
406
+ type: 'date_trunc',
407
+ field: value.field,
408
+ truncation: value.truncation,
409
+ ...(value.timezone !== undefined ? { timezone: value.timezone } : {}),
410
+ }
411
+ }
412
+
413
+ function asWidgetConfigRecord(value: unknown): Record<string, unknown> | undefined {
414
+ return isRecord(value) ? value : undefined
415
+ }
416
+
417
+ function getStringField(record: Record<string, unknown>, key: string) {
418
+ const value = record[key]
419
+ return typeof value === 'string' ? value : undefined
69
420
  }
70
421
 
71
422
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -1,7 +1,11 @@
1
1
  import { ref, watch, type Ref } from 'vue'
2
- import { dashboardApi } from '../api/dashboardApi.js'
2
+ import { dashboardApi, type DashboardWidgetDataRequest } from '../api/dashboardApi.js'
3
3
 
4
- export function useWidgetData(slug: Ref<string>, widgetId: Ref<string>) {
4
+ export function useWidgetData(
5
+ slug: Ref<string>,
6
+ widgetId: Ref<string>,
7
+ request?: Ref<DashboardWidgetDataRequest>,
8
+ ) {
5
9
  const data = ref<Awaited<ReturnType<typeof dashboardApi.getDashboardWidgetData>> | null>(null)
6
10
  const isLoading = ref(false)
7
11
  const isFetching = ref(false)
@@ -20,7 +24,7 @@ export function useWidgetData(slug: Ref<string>, widgetId: Ref<string>) {
20
24
  }
21
25
 
22
26
  try {
23
- const response = await dashboardApi.getDashboardWidgetData(slug.value, widgetId.value)
27
+ const response = await dashboardApi.getDashboardWidgetData(slug.value, widgetId.value, request?.value)
24
28
  data.value = response
25
29
  error.value = null
26
30
  return response
@@ -34,7 +38,7 @@ export function useWidgetData(slug: Ref<string>, widgetId: Ref<string>) {
34
38
  }
35
39
 
36
40
  watch(
37
- [slug, widgetId],
41
+ request ? [slug, widgetId, request] : [slug, widgetId],
38
42
  () => {
39
43
  void refetch()
40
44
  },
@@ -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)
@@ -135,11 +135,11 @@ const widgetLayoutVars = computed<CSSProperties>(() => {
135
135
  const fixedWidth = formatWidth(props.layout?.width)
136
136
 
137
137
  return {
138
- '--widget-basis': fixedWidth ?? basis,
139
- '--widget-min-width': fixedWidth ?? formatWidth(props.layout?.minWidth) ?? basis,
138
+ '--widget-basis': clampToContainerWidth(fixedWidth ?? basis),
139
+ '--widget-min-width': clampToContainerWidth(fixedWidth ?? formatWidth(props.layout?.minWidth) ?? basis),
140
140
  '--widget-max-width': props.layout?.maxWidth === null
141
- ? 'none'
142
- : fixedWidth ?? formatWidth(props.layout?.maxWidth) ?? 'none',
141
+ ? '100%'
142
+ : clampToContainerWidth(fixedWidth ?? formatWidth(props.layout?.maxWidth) ?? '100%'),
143
143
  height: formatWidth(props.layout?.height ?? DEFAULT_WIDGET_HEIGHT),
144
144
  }
145
145
  })
@@ -149,4 +149,8 @@ function formatWidth(value: number | undefined) {
149
149
  return `${value}px`
150
150
  }
151
151
  }
152
+
153
+ function clampToContainerWidth(value: string) {
154
+ return `min(${value}, 100%)`
155
+ }
152
156
  </script>
@@ -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`.