@adminforth/dashboard 1.5.0 → 1.6.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 (50) hide show
  1. package/custom/api/dashboardApi.ts +137 -1
  2. package/custom/model/dashboard.types.ts +32 -22
  3. package/custom/runtime/DashboardRuntime.vue +2 -3
  4. package/custom/skills/adminforth-dashboard/SKILL.md +66 -10
  5. package/custom/widgets/KpiCardWidget.vue +172 -9
  6. package/custom/widgets/chart/ChartWidget.vue +5 -5
  7. package/custom/widgets/registry.ts +4 -4
  8. package/dist/custom/api/dashboardApi.d.ts +46 -1
  9. package/dist/custom/api/dashboardApi.js +90 -0
  10. package/dist/custom/api/dashboardApi.ts +137 -1
  11. package/dist/custom/model/dashboard.types.d.ts +30 -14
  12. package/dist/custom/model/dashboard.types.js +2 -2
  13. package/dist/custom/model/dashboard.types.ts +32 -22
  14. package/dist/custom/queries/useDashboardConfig.d.ts +106 -104
  15. package/dist/custom/queries/useWidgetData.d.ts +106 -104
  16. package/dist/custom/runtime/DashboardRuntime.vue +2 -3
  17. package/dist/custom/skills/adminforth-dashboard/SKILL.md +66 -10
  18. package/dist/custom/widgets/KpiCardWidget.vue +172 -9
  19. package/dist/custom/widgets/chart/ChartWidget.vue +5 -5
  20. package/dist/custom/widgets/registry.js +4 -4
  21. package/dist/custom/widgets/registry.ts +4 -4
  22. package/dist/endpoint/widgets.js +99 -14
  23. package/dist/schema/api.d.ts +11426 -1634
  24. package/dist/schema/api.js +118 -21
  25. package/dist/schema/widget.d.ts +425 -1980
  26. package/dist/schema/widget.js +13 -374
  27. package/dist/schema/widgets/charts.d.ts +1689 -0
  28. package/dist/schema/widgets/charts.js +92 -0
  29. package/dist/schema/widgets/common.d.ts +275 -0
  30. package/dist/schema/widgets/common.js +171 -0
  31. package/dist/schema/widgets/gauge-card.d.ts +172 -0
  32. package/dist/schema/widgets/gauge-card.js +28 -0
  33. package/dist/schema/widgets/kpi-card.d.ts +212 -0
  34. package/dist/schema/widgets/kpi-card.js +43 -0
  35. package/dist/schema/widgets/pivot-table.d.ts +196 -0
  36. package/dist/schema/widgets/pivot-table.js +17 -0
  37. package/dist/schema/widgets/table.d.ts +130 -0
  38. package/dist/schema/widgets/table.js +12 -0
  39. package/dist/services/widgetDataService.js +96 -2
  40. package/endpoint/widgets.ts +173 -26
  41. package/package.json +1 -1
  42. package/schema/api.ts +148 -22
  43. package/schema/widget.ts +43 -425
  44. package/schema/widgets/charts.ts +113 -0
  45. package/schema/widgets/common.ts +194 -0
  46. package/schema/widgets/gauge-card.ts +34 -0
  47. package/schema/widgets/kpi-card.ts +49 -0
  48. package/schema/widgets/pivot-table.ts +24 -0
  49. package/schema/widgets/table.ts +18 -0
  50. package/services/widgetDataService.ts +129 -3
@@ -1,11 +1,15 @@
1
1
  import type {
2
2
  DashboardConfig,
3
3
  EditableDashboardGroupConfig,
4
- EditableDashboardWidgetConfig,
5
4
  DashboardGroupMoveDirection,
5
+ ChartDashboardWidgetConfig,
6
6
  DashboardWidgetConfig,
7
7
  DashboardWidgetConfigValidationError,
8
8
  DashboardWidgetMoveDirection,
9
+ GaugeCardWidgetConfig,
10
+ KpiCardWidgetConfig,
11
+ PivotTableWidgetConfig,
12
+ TableWidgetConfig,
9
13
  } from '../model/dashboard.types.js'
10
14
 
11
15
  export type DashboardResponse = {
@@ -28,6 +32,18 @@ export type DashboardWidgetDataRequest = {
28
32
  }
29
33
  }
30
34
 
35
+ export type ConfigurableTableWidgetConfig = Omit<TableWidgetConfig, 'id' | 'group_id' | 'order'>
36
+ export type ConfigurableKpiCardWidgetConfig = Omit<KpiCardWidgetConfig, 'id' | 'group_id' | 'order'>
37
+ export type ConfigurableGaugeCardWidgetConfig = Omit<GaugeCardWidgetConfig, 'id' | 'group_id' | 'order'>
38
+ export type ConfigurableChartWidgetConfig = Omit<ChartDashboardWidgetConfig, 'id' | 'group_id' | 'order'>
39
+ export type ConfigurableLineChartWidgetConfig = ConfigurableChartWidgetConfig & { chart: { type: 'line' } }
40
+ export type ConfigurableBarChartWidgetConfig = ConfigurableChartWidgetConfig & { chart: { type: 'bar' } }
41
+ export type ConfigurableStackedBarChartWidgetConfig = ConfigurableChartWidgetConfig & { chart: { type: 'stacked_bar' } }
42
+ export type ConfigurablePieChartWidgetConfig = ConfigurableChartWidgetConfig & { chart: { type: 'pie' } }
43
+ export type ConfigurableHistogramChartWidgetConfig = ConfigurableChartWidgetConfig & { chart: { type: 'histogram' } }
44
+ export type ConfigurableFunnelChartWidgetConfig = ConfigurableChartWidgetConfig & { chart: { type: 'funnel' } }
45
+ export type ConfigurablePivotTableWidgetConfig = Omit<PivotTableWidgetConfig, 'id' | 'group_id' | 'order'>
46
+
31
47
  export class DashboardApiError extends Error {
32
48
  validationErrors: DashboardWidgetConfigValidationError[]
33
49
 
@@ -205,6 +221,126 @@ export const dashboardApi = {
205
221
  })
206
222
  },
207
223
 
224
+ async configureTableWidget(
225
+ slug: string,
226
+ widgetId: string,
227
+ config: ConfigurableTableWidgetConfig,
228
+ ): Promise<DashboardResponse> {
229
+ return callDashboardApi('/adminapi/v1/dashboard/configure_table_widget', {
230
+ slug,
231
+ widgetId,
232
+ config,
233
+ })
234
+ },
235
+
236
+ async configureKpiCardWidget(
237
+ slug: string,
238
+ widgetId: string,
239
+ config: ConfigurableKpiCardWidgetConfig,
240
+ ): Promise<DashboardResponse> {
241
+ return callDashboardApi('/adminapi/v1/dashboard/configure_kpi_card_widget', {
242
+ slug,
243
+ widgetId,
244
+ config,
245
+ })
246
+ },
247
+
248
+ async configureGaugeCardWidget(
249
+ slug: string,
250
+ widgetId: string,
251
+ config: ConfigurableGaugeCardWidgetConfig,
252
+ ): Promise<DashboardResponse> {
253
+ return callDashboardApi('/adminapi/v1/dashboard/configure_gauge_card_widget', {
254
+ slug,
255
+ widgetId,
256
+ config,
257
+ })
258
+ },
259
+
260
+ async configureLineChartWidget(
261
+ slug: string,
262
+ widgetId: string,
263
+ config: ConfigurableLineChartWidgetConfig,
264
+ ): Promise<DashboardResponse> {
265
+ return callDashboardApi('/adminapi/v1/dashboard/configure_line_chart_widget', {
266
+ slug,
267
+ widgetId,
268
+ config,
269
+ })
270
+ },
271
+
272
+ async configureBarChartWidget(
273
+ slug: string,
274
+ widgetId: string,
275
+ config: ConfigurableBarChartWidgetConfig,
276
+ ): Promise<DashboardResponse> {
277
+ return callDashboardApi('/adminapi/v1/dashboard/configure_bar_chart_widget', {
278
+ slug,
279
+ widgetId,
280
+ config,
281
+ })
282
+ },
283
+
284
+ async configureStackedBarChartWidget(
285
+ slug: string,
286
+ widgetId: string,
287
+ config: ConfigurableStackedBarChartWidgetConfig,
288
+ ): Promise<DashboardResponse> {
289
+ return callDashboardApi('/adminapi/v1/dashboard/configure_stacked_bar_chart_widget', {
290
+ slug,
291
+ widgetId,
292
+ config,
293
+ })
294
+ },
295
+
296
+ async configurePieChartWidget(
297
+ slug: string,
298
+ widgetId: string,
299
+ config: ConfigurablePieChartWidgetConfig,
300
+ ): Promise<DashboardResponse> {
301
+ return callDashboardApi('/adminapi/v1/dashboard/configure_pie_chart_widget', {
302
+ slug,
303
+ widgetId,
304
+ config,
305
+ })
306
+ },
307
+
308
+ async configureHistogramChartWidget(
309
+ slug: string,
310
+ widgetId: string,
311
+ config: ConfigurableHistogramChartWidgetConfig,
312
+ ): Promise<DashboardResponse> {
313
+ return callDashboardApi('/adminapi/v1/dashboard/configure_histogram_chart_widget', {
314
+ slug,
315
+ widgetId,
316
+ config,
317
+ })
318
+ },
319
+
320
+ async configureFunnelChartWidget(
321
+ slug: string,
322
+ widgetId: string,
323
+ config: ConfigurableFunnelChartWidgetConfig,
324
+ ): Promise<DashboardResponse> {
325
+ return callDashboardApi('/adminapi/v1/dashboard/configure_funnel_chart_widget', {
326
+ slug,
327
+ widgetId,
328
+ config,
329
+ })
330
+ },
331
+
332
+ async configurePivotTableWidget(
333
+ slug: string,
334
+ widgetId: string,
335
+ config: ConfigurablePivotTableWidgetConfig,
336
+ ): Promise<DashboardResponse> {
337
+ return callDashboardApi('/adminapi/v1/dashboard/configure_pivot_table_widget', {
338
+ slug,
339
+ widgetId,
340
+ config,
341
+ })
342
+ },
343
+
208
344
  async getDashboardWidgetData(
209
345
  slug: string,
210
346
  widgetId: string,
@@ -36,6 +36,7 @@ export type QueryAggregateOperation = 'sum' | 'count' | 'count_distinct' | 'avg'
36
36
  export type TimeGrain = 'day' | 'week' | 'month' | 'year'
37
37
  export type ValueFormat =
38
38
  | 'number'
39
+ | 'integer'
39
40
  | 'compact_number'
40
41
  | 'currency'
41
42
  | 'percent'
@@ -119,21 +120,17 @@ export type QueryOrderByItem = {
119
120
  export type QueryConfig = {
120
121
  resource: string
121
122
  select?: QuerySelectItem[]
123
+ sparkline?: {
124
+ field: string
125
+ grain: TimeGrain
126
+ as: string
127
+ fill_missing?: Record<string, JsonValue>
128
+ }
122
129
  filters?: FilterExpression
123
130
  group_by?: QueryGroupByItem[]
124
131
  order_by?: QueryOrderByItem[]
125
132
  limit?: number
126
133
  offset?: number
127
- time_series?: {
128
- field: string
129
- grain: TimeGrain
130
- timezone?: string
131
- }
132
- period?: {
133
- field: string
134
- gte?: JsonValue
135
- lt?: JsonValue
136
- }
137
134
  bucket?: {
138
135
  field: string
139
136
  buckets: Array<{ label: string, min?: number, max?: number }>
@@ -178,8 +175,29 @@ export type KpiCardViewConfig = {
178
175
  text?: string
179
176
  field?: string
180
177
  }
181
- comparison?: JsonValue
182
- sparkline?: JsonValue
178
+ comparison?: {
179
+ field: string
180
+ format?: ValueFormat
181
+ positive_is_good?: boolean
182
+ compact?: {
183
+ show?: boolean
184
+ template?: string
185
+ }
186
+ tooltip?: {
187
+ label?: string
188
+ template?: string
189
+ }
190
+ }
191
+ sparkline?: {
192
+ type?: 'line'
193
+ field: string
194
+ x: string
195
+ show_axes?: boolean
196
+ show_labels?: boolean
197
+ fill?: {
198
+ type?: 'gradient' | 'solid'
199
+ }
200
+ }
183
201
  }
184
202
 
185
203
  export type GaugeCardViewConfig = {
@@ -257,14 +275,6 @@ export type DashboardWidgetConfig =
257
275
  | GaugeCardWidgetConfig
258
276
  | PivotTableWidgetConfig
259
277
 
260
- export type EditableDashboardWidgetConfig =
261
- | Omit<EmptyWidgetConfig, 'id' | 'group_id' | 'order'>
262
- | Omit<TableWidgetConfig, 'id' | 'group_id' | 'order'>
263
- | Omit<ChartDashboardWidgetConfig, 'id' | 'group_id' | 'order'>
264
- | Omit<KpiCardWidgetConfig, 'id' | 'group_id' | 'order'>
265
- | Omit<GaugeCardWidgetConfig, 'id' | 'group_id' | 'order'>
266
- | Omit<PivotTableWidgetConfig, 'id' | 'group_id' | 'order'>
267
-
268
278
  export type DashboardWidgetTableData = {
269
279
  kind?: 'table'
270
280
  columns: string[]
@@ -299,10 +309,10 @@ export function serializeDashboardWidgetConfigForEditor(
299
309
  id: _id,
300
310
  group_id: _groupId,
301
311
  order: _order,
302
- ...editableWidget
312
+ ...editableWidgetConfig
303
313
  } = widget
304
314
 
305
- return editableWidget
315
+ return editableWidgetConfig
306
316
  }
307
317
 
308
318
  export function getFieldRefField(value: FieldRef | undefined) {
@@ -208,7 +208,6 @@ import type {
208
208
  DashboardConfig,
209
209
  DashboardGroupConfig,
210
210
  EditableDashboardGroupConfig,
211
- EditableDashboardWidgetConfig,
212
211
  DashboardGroupMoveDirection,
213
212
  DashboardWidgetConfig,
214
213
  DashboardWidgetMoveDirection,
@@ -414,13 +413,13 @@ async function saveWidgetConfig() {
414
413
  try {
415
414
  widgetConfigError.value = ''
416
415
  widgetConfigFieldErrors.value = []
417
- const widgetConfig = parseYaml(widgetConfigCode.value) as EditableDashboardWidgetConfig
416
+ const widgetConfig = parseYaml(widgetConfigCode.value) as DashboardWidgetConfig
418
417
 
419
418
  applyDashboardResponse(
420
419
  await dashboardApi.setWidgetConfig(
421
420
  props.dashboardSlug,
422
421
  editingWidgetId.value,
423
- widgetConfig,
422
+ serializeDashboardWidgetConfigForEditor(widgetConfig),
424
423
  ),
425
424
  )
426
425
  closeWidgetConfigEditor()
@@ -19,7 +19,6 @@ Dashboard root, groups, and widgets are different entities.
19
19
  - Widget tools use widgetId and change target/query/card/chart/table/pivot.
20
20
  - Never call dashboard_set_dashboard_group_config to configure a widget.
21
21
  - Never call dashboard_add_dashboard_group to configure a widget.
22
- - If widget target, label, query, chart, card, table, pivot, variables, formulas, filters, or display fields must change, use dashboard_set_widget_config.
23
22
 
24
23
  ## Tool routing
25
24
 
@@ -27,7 +26,16 @@ Dashboard root, groups, and widgets are different entities.
27
26
  - Add group: dashboard_add_dashboard_group
28
27
  - Rename group: dashboard_set_dashboard_group_config
29
28
  - Add widget slot: dashboard_add_dashboard_widget
30
- - Configure widget: dashboard_set_widget_config
29
+ - Configure table widget: dashboard_configure_table_widget
30
+ - Configure KPI card widget: dashboard_configure_kpi_card_widget
31
+ - Configure gauge card widget: dashboard_configure_gauge_card_widget
32
+ - Configure line chart widget: dashboard_configure_line_chart_widget
33
+ - Configure bar chart widget: dashboard_configure_bar_chart_widget
34
+ - Configure stacked bar chart widget: dashboard_configure_stacked_bar_chart_widget
35
+ - Configure pie chart widget: dashboard_configure_pie_chart_widget
36
+ - Configure histogram chart widget: dashboard_configure_histogram_chart_widget
37
+ - Configure funnel chart widget: dashboard_configure_funnel_chart_widget
38
+ - Configure pivot table widget: dashboard_configure_pivot_table_widget
31
39
  - Move/remove widget/group: matching move/remove tool
32
40
  - Load widget data: dashboard_get_dashboard_widget_data
33
41
 
@@ -42,7 +50,7 @@ Before creating a group, call dashboard_get_config and check existing groups.
42
50
  - If no matching group exists, call dashboard_add_dashboard_group at most once for that requested group.
43
51
  - After dashboard_add_dashboard_group succeeds, extract the new groupId from the returned dashboard response.
44
52
  - If the group needs a label, call dashboard_set_dashboard_group_config once with that groupId.
45
- - After that, the next mutation must be dashboard_add_dashboard_widget or dashboard_set_widget_config, not another dashboard_add_dashboard_group.
53
+ - After that, the next mutation must be dashboard_add_dashboard_widget or a widget configure tool, not another dashboard_add_dashboard_group.
46
54
 
47
55
  Never call dashboard_add_dashboard_group repeatedly for the same user request. If you are about to create a second group for the same label/section, stop and report:
48
56
 
@@ -57,7 +65,7 @@ For any request to create KPI/chart/table/pivot/gauge widgets:
57
65
  3. if needed, rename group once
58
66
  4. for each widget:
59
67
  - dashboard_add_dashboard_widget
60
- - immediately dashboard_set_widget_config for the returned widgetId
68
+ - immediately configure it with the matching dashboard_configure_*_widget tool
61
69
  - confirm target is not empty
62
70
  5. dashboard_get_config
63
71
  6. validate all requested widgets are configured
@@ -79,7 +87,7 @@ This is incomplete for KPI/chart/table/pivot/gauge/spend/revenue/usage widgets.
79
87
 
80
88
  If the same mutation tool is about to be called with the same payload twice, stop and reassess.
81
89
  If dashboard_add_dashboard_group repeats for the same requested group, stop and reuse the groupId from dashboard_get_config.
82
- If dashboard_set_dashboard_group_config repeats while new widgets are target: empty, use dashboard_set_widget_config instead.
90
+ If dashboard_set_dashboard_group_config repeats while new widgets are target: empty, use a widget configure tool instead.
83
91
  After 2 repeated no-op mutations, stop with an explicit error.
84
92
 
85
93
  ## State machine
@@ -90,22 +98,43 @@ dashboard_get_config
90
98
  -> maybe dashboard_add_dashboard_group
91
99
  -> maybe dashboard_set_dashboard_group_config
92
100
  -> dashboard_add_dashboard_widget
93
- -> dashboard_set_widget_config
101
+ -> matching dashboard_configure_*_widget tool
94
102
  -> repeat only the two widget steps for more widgets
95
103
  -> dashboard_get_config
96
104
 
97
105
  Allowed repeats:
98
106
  - dashboard_add_dashboard_widget may repeat once per requested widget.
99
- - dashboard_set_widget_config may repeat once per requested widget.
107
+ - widget configure tools may repeat once per requested widget.
100
108
 
101
109
  Forbidden repeats:
102
110
  - dashboard_add_dashboard_group for the same requested group.
103
111
  - dashboard_set_dashboard_group_config with the same label/groupId.
104
112
 
105
113
  Forbidden substitutions:
106
- - dashboard_set_dashboard_group_config instead of set widget config.
114
+ - dashboard_set_dashboard_group_config instead of a widget configure tool.
107
115
  - dashboard_add_dashboard_group instead of add widget.
108
116
 
117
+ ## Specialized widget tools
118
+
119
+ Available specialized tools:
120
+ - dashboard_configure_table_widget
121
+ - dashboard_configure_kpi_card_widget
122
+ - dashboard_configure_gauge_card_widget
123
+ - dashboard_configure_pivot_table_widget
124
+ - dashboard_configure_line_chart_widget
125
+ - dashboard_configure_bar_chart_widget
126
+ - dashboard_configure_stacked_bar_chart_widget
127
+ - dashboard_configure_pie_chart_widget
128
+ - dashboard_configure_histogram_chart_widget
129
+ - dashboard_configure_funnel_chart_widget
130
+
131
+ Each specialized tool accepts:
132
+ - slug
133
+ - widgetId
134
+ - config
135
+
136
+ The config is the normal widget config for that target without server-owned id, group_id, and order.
137
+
109
138
  ## Widget config keys
110
139
 
111
140
  Use current keys:
@@ -116,22 +145,49 @@ Use table for table.
116
145
  Use pivot for pivot_table.
117
146
 
118
147
  Use target, not type.
148
+ For charts, use target: chart and chart.type for the concrete chart kind.
119
149
  Use query, not dataSource.
120
150
  Use resource, not resourceId.
121
151
 
122
152
  ## Query shape rules
123
153
 
124
- Use query.steps only for funnel charts. Do not use query.steps for kpi_card, gauge_card, table, pivot_table, or normal bar/line/stacked/pie charts.
154
+ Use dashboard_configure_funnel_chart_widget for funnel charts and set query.steps.
155
+ Do not use query.steps for kpi_card, gauge_card, table, pivot_table, line, bar, stacked bar, pie, or histogram charts.
125
156
 
126
157
  For kpi_card and normal charts, use:
127
158
  - query.resource
128
159
  - query.select
129
160
  - optional query.filters
130
161
  - optional query.group_by
131
- - optional query.period
132
162
  - optional query.order_by
133
163
  - optional query.calcs
134
164
 
165
+ ## Date range rules
166
+
167
+ Use only query.filters for time ranges.
168
+ Never use fixed ISO dates for rolling dashboard periods.
169
+ Never use query.period, period.range, query.time_series, or time_series.range.
170
+
171
+ For rolling ranges, use this exact filter shape:
172
+
173
+ filters:
174
+ and:
175
+ - field: created_at
176
+ gte:
177
+ now_minus: 30d
178
+ - field: created_at
179
+ lt:
180
+ now: true
181
+
182
+ Supported relative duration suffixes:
183
+ - h for hours
184
+ - d for days
185
+ - w for weeks
186
+ - mo for months
187
+ - y for years
188
+
189
+ For today/yesterday/last 7 days comparisons, create separate aggregate select items with separate filters and aliases. Do not hard-code calendar dates.
190
+
135
191
  Calculations run after selected fields and aggregates are loaded into a row. Therefore:
136
192
  - aggregate real resource fields first
137
193
  - then calculate derived fields from those aggregate aliases
@@ -16,14 +16,68 @@
16
16
 
17
17
  <div
18
18
  v-else
19
- class="grid gap-1"
19
+ class="grid gap-3"
20
20
  >
21
- <div class="text-3xl font-bold text-lightNavbarText dark:text-darkNavbarText">
22
- {{ formattedValue }}
23
- </div>
24
- <div class="text-sm text-lightListTableText dark:text-darkListTableText">
25
- {{ label }}
21
+ <div class="grid gap-1">
22
+ <div class="text-3xl font-bold text-lightNavbarText dark:text-darkNavbarText">
23
+ {{ formattedValue }}
24
+ </div>
25
+ <div class="flex flex-wrap items-center gap-2 text-sm text-lightListTableText dark:text-darkListTableText">
26
+ <span>{{ label }}</span>
27
+ <span
28
+ v-if="comparison"
29
+ class="rounded px-1.5 py-0.5 text-xs font-medium"
30
+ :class="comparisonClass"
31
+ :title="comparison.tooltip"
32
+ >
33
+ {{ comparison.label }}
34
+ </span>
35
+ </div>
26
36
  </div>
37
+ <svg
38
+ v-if="sparklinePoints"
39
+ class="h-12 w-full overflow-visible"
40
+ viewBox="0 0 100 32"
41
+ preserveAspectRatio="none"
42
+ aria-hidden="true"
43
+ >
44
+ <defs v-if="usesSparklineGradient">
45
+ <linearGradient
46
+ :id="sparklineGradientId"
47
+ x1="0"
48
+ y1="0"
49
+ x2="0"
50
+ y2="1"
51
+ >
52
+ <stop
53
+ offset="0%"
54
+ stop-color="currentColor"
55
+ stop-opacity="0.24"
56
+ />
57
+ <stop
58
+ offset="100%"
59
+ stop-color="currentColor"
60
+ stop-opacity="0"
61
+ />
62
+ </linearGradient>
63
+ </defs>
64
+ <polygon
65
+ v-if="usesSparklineGradient"
66
+ class="text-lightPrimary dark:text-darkPrimary"
67
+ :points="sparklineFillPoints"
68
+ :fill="`url(#${sparklineGradientId})`"
69
+ />
70
+ <polyline
71
+ class="text-lightPrimary dark:text-darkPrimary"
72
+ :points="sparklinePoints"
73
+ fill="none"
74
+ stroke="currentColor"
75
+ stroke-width="2"
76
+ stroke-linecap="round"
77
+ stroke-linejoin="round"
78
+ vector-effect="non-scaling-stroke"
79
+ />
80
+ </svg>
27
81
  </div>
28
82
  </div>
29
83
  </template>
@@ -61,11 +115,120 @@ watch(
61
115
  const kpiConfig = computed(() => props.widget.target === 'kpi_card' ? props.widget.card : undefined)
62
116
  const widgetData = computed(() => data.value?.data as DashboardWidgetTableData | null)
63
117
  const columns = computed(() => widgetData.value?.columns ?? [])
64
- const firstRow = computed(() => widgetData.value?.rows[0] ?? {})
118
+ const firstRow = computed(() => widgetData.value?.values ?? widgetData.value?.rows[0] ?? {})
65
119
  const valueField = computed(() => kpiConfig.value?.value.field || columns.value[0])
66
120
  const value = computed(() => toFiniteNumber(firstRow.value[valueField.value]))
67
121
  const label = computed(() => kpiConfig.value?.subtitle?.field
68
- ? String(firstRow.value[kpiConfig.value.subtitle.field])
122
+ ? [kpiConfig.value.subtitle.text, formatValue(firstRow.value[kpiConfig.value.subtitle.field], kpiConfig.value.value.format)]
123
+ .filter(Boolean)
124
+ .join(': ')
69
125
  : kpiConfig.value?.subtitle?.text ?? kpiConfig.value?.title ?? props.widget.label)
70
- const formattedValue = computed(() => `${kpiConfig.value?.value.prefix ?? ''}${formatChartValue(value.value)}${kpiConfig.value?.value.suffix ?? ''}`)
126
+ const formattedValue = computed(() => `${kpiConfig.value?.value.prefix ?? ''}${formatValue(value.value, kpiConfig.value?.value.format)}${kpiConfig.value?.value.suffix ?? ''}`)
127
+ const comparisonValue = computed(() => toFiniteNumber(kpiConfig.value?.comparison?.field
128
+ ? firstRow.value[kpiConfig.value.comparison.field]
129
+ : undefined))
130
+ const comparison = computed(() => {
131
+ const config = kpiConfig.value?.comparison
132
+
133
+ if (!config) {
134
+ return null
135
+ }
136
+
137
+ const template = config.compact?.template ?? '{sign}{value}'
138
+ const tooltipTemplate = config.tooltip?.template
139
+ const valueText = formatValue(Math.abs(comparisonValue.value), config.format, { signed: false, compactTemplate: true })
140
+ const sign = comparisonValue.value > 0 ? '+' : comparisonValue.value < 0 ? '-' : ''
141
+
142
+ return {
143
+ value: comparisonValue.value,
144
+ label: config.compact?.show === false ? valueText : applyTemplate(template, sign, valueText),
145
+ tooltip: tooltipTemplate
146
+ ? applyTemplate(tooltipTemplate, sign, valueText)
147
+ : config.tooltip?.label,
148
+ positiveIsGood: config.positive_is_good ?? true,
149
+ }
150
+ })
151
+ const comparisonClass = computed(() => {
152
+ if (!comparison.value || comparison.value.value === 0) {
153
+ return 'bg-lightListBorder text-lightListTableText dark:bg-darkListBorder dark:text-darkListTableText'
154
+ }
155
+
156
+ const isGood = comparison.value.positiveIsGood
157
+ ? comparison.value.value > 0
158
+ : comparison.value.value < 0
159
+
160
+ return isGood
161
+ ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300'
162
+ : 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300'
163
+ })
164
+ const sparklineRows = computed(() => widgetData.value?.rows ?? [])
165
+ const sparklineConfig = computed(() => kpiConfig.value?.sparkline)
166
+ const sparklineGradientId = computed(() => `kpi-sparkline-${props.widget.id}`)
167
+ const usesSparklineGradient = computed(() => sparklineConfig.value?.fill?.type === 'gradient')
168
+ const sparklineCoordinates = computed(() => {
169
+ const field = sparklineConfig.value?.field
170
+
171
+ if (!field || sparklineRows.value.length < 2) {
172
+ return []
173
+ }
174
+
175
+ const values = sparklineRows.value.map((row) => toFiniteNumber(row[field]))
176
+ const min = Math.min(...values)
177
+ const max = Math.max(...values)
178
+ const range = max - min || 1
179
+
180
+ return values.map((item, index) => ({
181
+ x: (index / Math.max(values.length - 1, 1)) * 100,
182
+ y: 30 - ((item - min) / range) * 28,
183
+ }))
184
+ })
185
+ const sparklinePoints = computed(() => sparklineCoordinates.value.length
186
+ ? sparklineCoordinates.value.map((point) => `${point.x},${point.y}`).join(' ')
187
+ : '')
188
+ const sparklineFillPoints = computed(() => sparklineCoordinates.value.length
189
+ ? `0,32 ${sparklinePoints.value} 100,32`
190
+ : '')
191
+
192
+ function applyTemplate(template: string, sign: string, value: string) {
193
+ return template
194
+ .replaceAll('{sign}', sign)
195
+ .replaceAll('{value}', value)
196
+ }
197
+
198
+ function formatValue(
199
+ rawValue: unknown,
200
+ format = 'number',
201
+ options: { signed?: boolean, compactTemplate?: boolean } = {},
202
+ ) {
203
+ const numericValue = toFiniteNumber(rawValue)
204
+ const sign = options.signed && numericValue > 0 ? '+' : ''
205
+ const absoluteValue = options.signed ? Math.abs(numericValue) : numericValue
206
+
207
+ if (format === 'integer') {
208
+ return `${sign}${formatChartValue(absoluteValue, { maximumFractionDigits: 0 })}`
209
+ }
210
+
211
+ if (format === 'compact_number') {
212
+ return `${sign}${formatChartValue(absoluteValue, { notation: 'compact', maximumFractionDigits: 1 })}`
213
+ }
214
+
215
+ if (format === 'currency' || format === 'currency_delta') {
216
+ return `${sign}${formatChartValue(absoluteValue, {
217
+ style: 'currency',
218
+ currency: 'USD',
219
+ maximumFractionDigits: 2,
220
+ })}`
221
+ }
222
+
223
+ if (format === 'percent' || format === 'percent_delta') {
224
+ const value = formatChartValue(absoluteValue, { maximumFractionDigits: 1 })
225
+ return options.compactTemplate ? value : `${sign}${value}%`
226
+ }
227
+
228
+ if (format === 'number_delta') {
229
+ return `${sign}${formatChartValue(absoluteValue, { maximumFractionDigits: 2 })}`
230
+ }
231
+
232
+ return `${sign}${formatChartValue(absoluteValue, { maximumFractionDigits: 2 })}`
233
+ }
71
234
  </script>
@@ -83,11 +83,11 @@
83
83
  import { computed, watch } from 'vue'
84
84
  import { useWidgetData } from '../../queries/useWidgetData.js'
85
85
  import type { ChartDashboardWidgetConfig, DashboardWidgetTableData } from '../../model/dashboard.types.js'
86
- import BarChart from './bar/BarChart.vue'
87
- import FunnelChart from './funnel/FunnelChart.vue'
88
- import LineChart from './line/LineChart.vue'
89
- import PieChart from './pie/PieChart.vue'
90
- import StackedBarChart from './stacked-bar/StackedBarChart.vue'
86
+ import BarChart from './BarChart.vue'
87
+ import FunnelChart from './FunnelChart.vue'
88
+ import LineChart from './LineChart.vue'
89
+ import PieChart from './PieChart.vue'
90
+ import StackedBarChart from './StackedBarChart.vue'
91
91
  import { toFiniteNumber } from './chart.utils.js'
92
92
 
93
93
  const DEFAULT_WIDGET_HEIGHT = 500
@@ -1,10 +1,10 @@
1
1
  import type { Component } from 'vue'
2
2
  import type { DashboardWidgetTarget } from '../model/dashboard.types.js'
3
3
  import ChartWidget from './chart/ChartWidget.vue'
4
- import GaugeCardWidget from './gauge-card/GaugeCardWidget.vue'
5
- import KpiCardWidget from './kpi-card/KpiCardWidget.vue'
6
- import PivotTableWidget from './pivot-table/PivotTableWidget.vue'
7
- import TableWidget from './table/TableWidget.vue'
4
+ import GaugeCardWidget from './GaugeCardWidget.vue'
5
+ import KpiCardWidget from './KpiCardWidget.vue'
6
+ import PivotTableWidget from './PivotTableWidget.vue'
7
+ import TableWidget from './TableWidget.vue'
8
8
 
9
9
  export type DashboardWidgetType = DashboardWidgetTarget
10
10