@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.
- package/custom/api/dashboardApi.ts +137 -1
- package/custom/model/dashboard.types.ts +32 -22
- package/custom/runtime/DashboardRuntime.vue +2 -3
- package/custom/skills/adminforth-dashboard/SKILL.md +66 -10
- package/custom/widgets/KpiCardWidget.vue +172 -9
- package/custom/widgets/chart/ChartWidget.vue +5 -5
- package/custom/widgets/registry.ts +4 -4
- package/dist/custom/api/dashboardApi.d.ts +46 -1
- package/dist/custom/api/dashboardApi.js +90 -0
- package/dist/custom/api/dashboardApi.ts +137 -1
- package/dist/custom/model/dashboard.types.d.ts +30 -14
- package/dist/custom/model/dashboard.types.js +2 -2
- package/dist/custom/model/dashboard.types.ts +32 -22
- package/dist/custom/queries/useDashboardConfig.d.ts +106 -104
- package/dist/custom/queries/useWidgetData.d.ts +106 -104
- package/dist/custom/runtime/DashboardRuntime.vue +2 -3
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +66 -10
- package/dist/custom/widgets/KpiCardWidget.vue +172 -9
- package/dist/custom/widgets/chart/ChartWidget.vue +5 -5
- package/dist/custom/widgets/registry.js +4 -4
- package/dist/custom/widgets/registry.ts +4 -4
- package/dist/endpoint/widgets.js +99 -14
- package/dist/schema/api.d.ts +11426 -1634
- package/dist/schema/api.js +118 -21
- package/dist/schema/widget.d.ts +425 -1980
- package/dist/schema/widget.js +13 -374
- package/dist/schema/widgets/charts.d.ts +1689 -0
- package/dist/schema/widgets/charts.js +92 -0
- package/dist/schema/widgets/common.d.ts +275 -0
- package/dist/schema/widgets/common.js +171 -0
- package/dist/schema/widgets/gauge-card.d.ts +172 -0
- package/dist/schema/widgets/gauge-card.js +28 -0
- package/dist/schema/widgets/kpi-card.d.ts +212 -0
- package/dist/schema/widgets/kpi-card.js +43 -0
- package/dist/schema/widgets/pivot-table.d.ts +196 -0
- package/dist/schema/widgets/pivot-table.js +17 -0
- package/dist/schema/widgets/table.d.ts +130 -0
- package/dist/schema/widgets/table.js +12 -0
- package/dist/services/widgetDataService.js +96 -2
- package/endpoint/widgets.ts +173 -26
- package/package.json +1 -1
- package/schema/api.ts +148 -22
- package/schema/widget.ts +43 -425
- package/schema/widgets/charts.ts +113 -0
- package/schema/widgets/common.ts +194 -0
- package/schema/widgets/gauge-card.ts +34 -0
- package/schema/widgets/kpi-card.ts +49 -0
- package/schema/widgets/pivot-table.ts +24 -0
- package/schema/widgets/table.ts +18 -0
- 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?:
|
|
182
|
-
|
|
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
|
-
...
|
|
312
|
+
...editableWidgetConfig
|
|
303
313
|
} = widget
|
|
304
314
|
|
|
305
|
-
return
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
->
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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-
|
|
19
|
+
class="grid gap-3"
|
|
20
20
|
>
|
|
21
|
-
<div class="
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
?
|
|
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 ?? ''}${
|
|
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 './
|
|
87
|
-
import FunnelChart from './
|
|
88
|
-
import LineChart from './
|
|
89
|
-
import PieChart from './
|
|
90
|
-
import StackedBarChart from './
|
|
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 './
|
|
5
|
-
import KpiCardWidget from './
|
|
6
|
-
import PivotTableWidget from './
|
|
7
|
-
import TableWidget from './
|
|
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
|
|