@adminforth/dashboard 1.4.2 → 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 -5
- package/custom/model/dashboard.types.ts +32 -22
- package/custom/runtime/DashboardRuntime.vue +2 -3
- package/custom/skills/adminforth-dashboard/SKILL.md +165 -179
- 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 -2
- package/dist/custom/api/dashboardApi.js +90 -5
- package/dist/custom/api/dashboardApi.ts +137 -5
- 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 +165 -179
- 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/dashboard.d.ts +2 -4
- package/dist/endpoint/dashboard.js +1 -21
- package/dist/endpoint/groups.d.ts +1 -0
- package/dist/endpoint/groups.js +61 -48
- package/dist/endpoint/widgets.d.ts +1 -0
- package/dist/endpoint/widgets.js +167 -64
- package/dist/schema/api.d.ts +11710 -2785
- package/dist/schema/api.js +118 -26
- 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/dashboardConfigService.d.ts +4 -0
- package/dist/services/dashboardConfigService.js +46 -0
- package/dist/services/widgetDataService.js +96 -2
- package/endpoint/dashboard.ts +2 -33
- package/endpoint/groups.ts +91 -72
- package/endpoint/widgets.ts +260 -87
- package/package.json +1 -1
- package/schema/api.ts +148 -28
- 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/dashboardConfigService.ts +73 -0
- package/services/widgetDataService.ts +129 -3
|
@@ -5,232 +5,218 @@ description: Use when the user wants to view, create, update, move, remove, vali
|
|
|
5
5
|
|
|
6
6
|
# AdminForth Dashboard Plugin
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Use this skill for dashboard group/widget mutations and dashboard data loading.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Core rule
|
|
11
11
|
|
|
12
|
-
If
|
|
12
|
+
If dashboard tools are callable, use tools. Do not answer mutation requests by only printing config.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
## Entity boundaries
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Dashboard root, groups, and widgets are different entities.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
- Group tools use groupId and only change group config.
|
|
19
|
+
- Widget tools use widgetId and change target/query/card/chart/table/pivot.
|
|
20
|
+
- Never call dashboard_set_dashboard_group_config to configure a widget.
|
|
21
|
+
- Never call dashboard_add_dashboard_group to configure a widget.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
## Tool routing
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
- Read dashboard: dashboard_get_config
|
|
26
|
+
- Add group: dashboard_add_dashboard_group
|
|
27
|
+
- Rename group: dashboard_set_dashboard_group_config
|
|
28
|
+
- Add widget slot: dashboard_add_dashboard_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
|
|
39
|
+
- Move/remove widget/group: matching move/remove tool
|
|
40
|
+
- Load widget data: dashboard_get_dashboard_widget_data
|
|
26
41
|
|
|
27
|
-
|
|
42
|
+
If a known dashboard tool schema is missing, call fetch_tool_schema for that exact tool.
|
|
43
|
+
If fetch_tool_schema returns but the intended tool is still not callable, stop and report a tool-routing error. Do not substitute another mutation tool.
|
|
28
44
|
|
|
29
|
-
|
|
30
|
-
- "change/update/edit this widget" → update widget config.
|
|
31
|
-
- "move this widget/group" → call the move tool.
|
|
32
|
-
- "remove/delete this widget/group" → call the remove tool.
|
|
33
|
-
- "show/load dashboard" → call the dashboard config tool.
|
|
34
|
-
- "load/check widget data" → call the widget data tool.
|
|
35
|
-
- "validate this widget config" → call validation logic/tool if available.
|
|
45
|
+
## Group creation guard
|
|
36
46
|
|
|
37
|
-
|
|
47
|
+
Before creating a group, call dashboard_get_config and check existing groups.
|
|
38
48
|
|
|
39
|
-
|
|
49
|
+
- If a requested group label already exists, reuse that groupId.
|
|
50
|
+
- If no matching group exists, call dashboard_add_dashboard_group at most once for that requested group.
|
|
51
|
+
- After dashboard_add_dashboard_group succeeds, extract the new groupId from the returned dashboard response.
|
|
52
|
+
- If the group needs a label, call dashboard_set_dashboard_group_config once with that groupId.
|
|
53
|
+
- After that, the next mutation must be dashboard_add_dashboard_widget or a widget configure tool, not another dashboard_add_dashboard_group.
|
|
40
54
|
|
|
41
|
-
|
|
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:
|
|
42
56
|
|
|
43
|
-
|
|
44
|
-
- `dashboard_set_dashboard_config`
|
|
45
|
-
- `dashboard_add_dashboard_group`
|
|
46
|
-
- `dashboard_set_dashboard_group_config`
|
|
47
|
-
- `dashboard_move_dashboard_group`
|
|
48
|
-
- `dashboard_remove_dashboard_group`
|
|
49
|
-
- `dashboard_add_dashboard_widget`
|
|
50
|
-
- `dashboard_move_dashboard_widget`
|
|
51
|
-
- `dashboard_remove_dashboard_widget`
|
|
52
|
-
- `dashboard_set_widget_config`
|
|
53
|
-
- `dashboard_get_dashboard_widget_data`
|
|
57
|
+
Repeated dashboard_add_dashboard_group; expected using the existing/new groupId.
|
|
54
58
|
|
|
55
|
-
|
|
59
|
+
## Create-and-configure workflow
|
|
56
60
|
|
|
57
|
-
|
|
61
|
+
For any request to create KPI/chart/table/pivot/gauge widgets:
|
|
58
62
|
|
|
59
|
-
|
|
63
|
+
1. dashboard_get_config
|
|
64
|
+
2. select existing group by label, or create one group once
|
|
65
|
+
3. if needed, rename group once
|
|
66
|
+
4. for each widget:
|
|
67
|
+
- dashboard_add_dashboard_widget
|
|
68
|
+
- immediately configure it with the matching dashboard_configure_*_widget tool
|
|
69
|
+
- confirm target is not empty
|
|
70
|
+
5. dashboard_get_config
|
|
71
|
+
6. validate all requested widgets are configured
|
|
72
|
+
7. return short summary
|
|
60
73
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
- `dashboard_add_dashboard_widget` creates a widget inside an existing group. Use it when you already have a `groupId`.
|
|
64
|
-
- `dashboard_set_dashboard_group_config`, `dashboard_move_dashboard_group`, and `dashboard_remove_dashboard_group` operate on an existing group and need `groupId`.
|
|
65
|
-
- `dashboard_set_widget_config`, `dashboard_move_dashboard_widget`, `dashboard_remove_dashboard_widget`, and `dashboard_get_dashboard_widget_data` operate on an existing widget and need `widgetId`.
|
|
66
|
-
- If a tool call fails with "input did not match expected schema", call `fetch_tool_schema` for that exact tool, remove unsupported arguments, and retry the correct tool.
|
|
74
|
+
Do not batch-create empty widgets and postpone configuration.
|
|
75
|
+
Do not build a full dashboard JSON object for this workflow.
|
|
67
76
|
|
|
68
|
-
##
|
|
77
|
+
## Empty widget rule
|
|
69
78
|
|
|
70
|
-
|
|
79
|
+
dashboard_add_dashboard_widget creates only:
|
|
71
80
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
- "write a widget showing top 5 orders"
|
|
75
|
-
- "make a widget for revenue by product"
|
|
81
|
+
label: New widget
|
|
82
|
+
target: empty
|
|
76
83
|
|
|
77
|
-
|
|
84
|
+
This is incomplete for KPI/chart/table/pivot/gauge/spend/revenue/usage widgets.
|
|
78
85
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
## No-op loop guard
|
|
87
|
+
|
|
88
|
+
If the same mutation tool is about to be called with the same payload twice, stop and reassess.
|
|
89
|
+
If dashboard_add_dashboard_group repeats for the same requested group, stop and reuse the groupId from dashboard_get_config.
|
|
90
|
+
If dashboard_set_dashboard_group_config repeats while new widgets are target: empty, use a widget configure tool instead.
|
|
91
|
+
After 2 repeated no-op mutations, stop with an explicit error.
|
|
85
92
|
|
|
86
|
-
|
|
93
|
+
## State machine
|
|
87
94
|
|
|
88
|
-
|
|
95
|
+
For create group + widgets tasks, follow this exact state order:
|
|
89
96
|
|
|
90
|
-
|
|
97
|
+
dashboard_get_config
|
|
98
|
+
-> maybe dashboard_add_dashboard_group
|
|
99
|
+
-> maybe dashboard_set_dashboard_group_config
|
|
100
|
+
-> dashboard_add_dashboard_widget
|
|
101
|
+
-> matching dashboard_configure_*_widget tool
|
|
102
|
+
-> repeat only the two widget steps for more widgets
|
|
103
|
+
-> dashboard_get_config
|
|
91
104
|
|
|
92
|
-
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
- "turn this widget into a table"
|
|
105
|
+
Allowed repeats:
|
|
106
|
+
- dashboard_add_dashboard_widget may repeat once per requested widget.
|
|
107
|
+
- widget configure tools may repeat once per requested widget.
|
|
96
108
|
|
|
97
|
-
|
|
109
|
+
Forbidden repeats:
|
|
110
|
+
- dashboard_add_dashboard_group for the same requested group.
|
|
111
|
+
- dashboard_set_dashboard_group_config with the same label/groupId.
|
|
98
112
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
4. Call `dashboard_set_widget_config`.
|
|
103
|
-
5. Return a short summary of what changed.
|
|
113
|
+
Forbidden substitutions:
|
|
114
|
+
- dashboard_set_dashboard_group_config instead of a widget configure tool.
|
|
115
|
+
- dashboard_add_dashboard_group instead of add widget.
|
|
104
116
|
|
|
105
|
-
|
|
117
|
+
## Specialized widget tools
|
|
106
118
|
|
|
107
|
-
|
|
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
|
|
108
130
|
|
|
109
|
-
|
|
131
|
+
Each specialized tool accepts:
|
|
132
|
+
- slug
|
|
133
|
+
- widgetId
|
|
134
|
+
- config
|
|
110
135
|
|
|
111
|
-
-
|
|
112
|
-
- Rename/change group config → `dashboard_set_dashboard_group_config`
|
|
113
|
-
- Move group → `dashboard_move_dashboard_group`
|
|
114
|
-
- Remove group → `dashboard_remove_dashboard_group`
|
|
136
|
+
The config is the normal widget config for that target without server-owned id, group_id, and order.
|
|
115
137
|
|
|
116
|
-
|
|
138
|
+
## Widget config keys
|
|
117
139
|
|
|
118
|
-
|
|
140
|
+
Use current keys:
|
|
141
|
+
target, label, query, resource, group_by, order_by, page_size, variables.
|
|
142
|
+
Use card for kpi_card/gauge_card.
|
|
143
|
+
Use chart for chart.
|
|
144
|
+
Use table for table.
|
|
145
|
+
Use pivot for pivot_table.
|
|
119
146
|
|
|
120
|
-
Use
|
|
147
|
+
Use target, not type.
|
|
148
|
+
For charts, use target: chart and chart.type for the concrete chart kind.
|
|
149
|
+
Use query, not dataSource.
|
|
150
|
+
Use resource, not resourceId.
|
|
121
151
|
|
|
122
|
-
|
|
152
|
+
## Query shape rules
|
|
123
153
|
|
|
124
|
-
|
|
125
|
-
|
|
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.
|
|
126
156
|
|
|
127
|
-
|
|
157
|
+
For kpi_card and normal charts, use:
|
|
158
|
+
- query.resource
|
|
159
|
+
- query.select
|
|
160
|
+
- optional query.filters
|
|
161
|
+
- optional query.group_by
|
|
162
|
+
- optional query.order_by
|
|
163
|
+
- optional query.calcs
|
|
128
164
|
|
|
129
|
-
|
|
130
|
-
2. Modify the returned root config, preserving existing `version`, `groups`, and `widgets` unless the user asked to change them.
|
|
131
|
-
3. Call `dashboard_set_dashboard_config` with the full updated config.
|
|
132
|
-
4. Return a short summary of the root-level fields changed.
|
|
165
|
+
## Date range rules
|
|
133
166
|
|
|
134
|
-
|
|
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.
|
|
135
170
|
|
|
136
|
-
|
|
171
|
+
For rolling ranges, use this exact filter shape:
|
|
137
172
|
|
|
138
|
-
|
|
173
|
+
filters:
|
|
174
|
+
and:
|
|
175
|
+
- field: created_at
|
|
176
|
+
gte:
|
|
177
|
+
now_minus: 30d
|
|
178
|
+
- field: created_at
|
|
179
|
+
lt:
|
|
180
|
+
now: true
|
|
139
181
|
|
|
140
|
-
|
|
141
|
-
-
|
|
142
|
-
-
|
|
143
|
-
-
|
|
144
|
-
-
|
|
145
|
-
-
|
|
146
|
-
- Use `page_size`, not `pageSize`.
|
|
147
|
-
- For step-based chart queries, use `query.steps` as an ordered array of `{ name, resource, metric, filters }` steps and add `query.calcs` when derived fields are needed.
|
|
148
|
-
- Use `card` for KPI and gauge widget view config.
|
|
149
|
-
- Use `pivot` for pivot table view config.
|
|
150
|
-
- Use `variables` for reusable static maps or constants at widget level.
|
|
151
|
-
- In `query.calcs`, use `lookup($variables.some.map, row_field, default_number)` to read a numeric value from a variable map by the current row/group field.
|
|
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
|
|
152
188
|
|
|
153
|
-
|
|
189
|
+
For today/yesterday/last 7 days comparisons, create separate aggregate select items with separate filters and aliases. Do not hard-code calendar dates.
|
|
154
190
|
|
|
155
|
-
|
|
191
|
+
Calculations run after selected fields and aggregates are loaded into a row. Therefore:
|
|
192
|
+
- aggregate real resource fields first
|
|
193
|
+
- then calculate derived fields from those aggregate aliases
|
|
194
|
+
- do not aggregate a calc alias such as cost unless cost is an actual resource field
|
|
195
|
+
|
|
196
|
+
For spend/cost widgets, prefer this pattern:
|
|
197
|
+
|
|
198
|
+
select raw token totals:
|
|
199
|
+
- sum uncached_input_tokens as uncached_input_tokens
|
|
200
|
+
- sum cached_input_tokens as cached_input_tokens
|
|
201
|
+
- sum output_tokens as output_tokens
|
|
202
|
+
|
|
203
|
+
then query.calcs:
|
|
204
|
+
- calculate total_spend from those aliases and lookup variables
|
|
205
|
+
|
|
206
|
+
For today vs yesterday KPI, use multiple aggregate select items with filters and distinct aliases, then calcs. Do not use query.steps.
|
|
207
|
+
|
|
208
|
+
## Calc variables
|
|
209
|
+
|
|
210
|
+
Use variables for static maps/rates.
|
|
211
|
+
Use lookup($variables.some.map, row_field, default_number) in query.calcs.
|
|
212
|
+
|
|
213
|
+
Minimal example:
|
|
156
214
|
|
|
157
|
-
```yaml
|
|
158
|
-
variables:
|
|
159
|
-
token_prices_per_1m:
|
|
160
|
-
input:
|
|
161
|
-
gpt-4.1: 2.00
|
|
162
|
-
gpt-4.1-mini: 0.40
|
|
163
|
-
gpt-4o-mini: 0.15
|
|
164
|
-
output:
|
|
165
|
-
gpt-4.1: 8.00
|
|
166
|
-
gpt-4.1-mini: 1.60
|
|
167
|
-
gpt-4o-mini: 0.60
|
|
168
|
-
cached:
|
|
169
|
-
gpt-4.1: 0.50
|
|
170
|
-
gpt-4.1-mini: 0.10
|
|
171
|
-
gpt-4o-mini: 0.075
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
Use variables when a calculation needs a static rate table, threshold table, coefficient map, or other reusable constants. In calcs, `lookup($variables.path.to.map, field_name, 0)` returns the value from the map using `field_name` from the current row/group. The third argument is the numeric fallback when the key is missing.
|
|
175
|
-
|
|
176
|
-
Example widget:
|
|
177
|
-
|
|
178
|
-
```yaml
|
|
179
|
-
target: chart
|
|
180
|
-
label: Model costs
|
|
181
|
-
size: large
|
|
182
215
|
variables:
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
gpt-4.1: 2.00
|
|
186
|
-
gpt-4.1-mini: 0.40
|
|
187
|
-
gpt-4o-mini: 0.15
|
|
188
|
-
output:
|
|
189
|
-
gpt-4.1: 8.00
|
|
190
|
-
gpt-4.1-mini: 1.60
|
|
191
|
-
gpt-4o-mini: 0.60
|
|
192
|
-
cached:
|
|
193
|
-
gpt-4.1: 0.50
|
|
194
|
-
gpt-4.1-mini: 0.10
|
|
195
|
-
gpt-4o-mini: 0.075
|
|
196
|
-
|
|
197
|
-
chart:
|
|
198
|
-
type: stacked_bar
|
|
199
|
-
title: LLM costs by model
|
|
200
|
-
x:
|
|
201
|
-
field: model
|
|
202
|
-
label: Model
|
|
203
|
-
y:
|
|
204
|
-
- field: input_cost
|
|
205
|
-
label: Input
|
|
206
|
-
format: currency
|
|
207
|
-
- field: output_cost
|
|
208
|
-
label: Output
|
|
209
|
-
format: currency
|
|
210
|
-
- field: cached_cost
|
|
211
|
-
label: Cached
|
|
212
|
-
format: currency
|
|
216
|
+
prices:
|
|
217
|
+
gpt-5.4: 2.5
|
|
213
218
|
|
|
214
219
|
query:
|
|
215
|
-
resource: model_usage
|
|
216
|
-
select:
|
|
217
|
-
- field: model
|
|
218
|
-
- agg: sum
|
|
219
|
-
field: input_tokens
|
|
220
|
-
as: input_tokens
|
|
221
|
-
- agg: sum
|
|
222
|
-
field: output_tokens
|
|
223
|
-
as: output_tokens
|
|
224
|
-
- agg: sum
|
|
225
|
-
field: cached_tokens
|
|
226
|
-
as: cached_tokens
|
|
227
|
-
group_by:
|
|
228
|
-
- model
|
|
229
220
|
calcs:
|
|
230
|
-
- calc:
|
|
231
|
-
as:
|
|
232
|
-
- calc: output_tokens / 1000000 * lookup($variables.token_prices_per_1m.output, model, 0)
|
|
233
|
-
as: output_cost
|
|
234
|
-
- calc: cached_tokens / 1000000 * lookup($variables.token_prices_per_1m.cached, model, 0)
|
|
235
|
-
as: cached_cost
|
|
236
|
-
```
|
|
221
|
+
- calc: tokens / 1000000 * lookup($variables.prices, model, 0)
|
|
222
|
+
as: cost
|
|
@@ -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,8 +1,8 @@
|
|
|
1
1
|
import ChartWidget from './chart/ChartWidget.vue';
|
|
2
|
-
import GaugeCardWidget from './
|
|
3
|
-
import KpiCardWidget from './
|
|
4
|
-
import PivotTableWidget from './
|
|
5
|
-
import TableWidget from './
|
|
2
|
+
import GaugeCardWidget from './GaugeCardWidget.vue';
|
|
3
|
+
import KpiCardWidget from './KpiCardWidget.vue';
|
|
4
|
+
import PivotTableWidget from './PivotTableWidget.vue';
|
|
5
|
+
import TableWidget from './TableWidget.vue';
|
|
6
6
|
export const widgetRegistry = [
|
|
7
7
|
{
|
|
8
8
|
type: 'table',
|
|
@@ -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
|
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { IHttpServer } from 'adminforth';
|
|
2
2
|
import type { DashboardConfig } from '../custom/model/dashboard.types.js';
|
|
3
|
-
import type { DashboardRecord
|
|
3
|
+
import type { DashboardRecord } from '../services/dashboardConfigService.js';
|
|
4
4
|
type DashboardEndpointsContext = {
|
|
5
|
-
canEditDashboard: (adminUser: AdminUser) => boolean;
|
|
6
5
|
getDashboardRecord: (slug: string) => Promise<DashboardRecord | null>;
|
|
7
6
|
parseStoredDashboardConfig: (config: unknown) => DashboardConfig;
|
|
8
|
-
persistDashboardConfig: (dashboard: DashboardRecord, config: DashboardConfig) => Promise<PersistedDashboardResponse>;
|
|
9
7
|
};
|
|
10
8
|
export declare function registerDashboardEndpoints(server: IHttpServer, ctx: DashboardEndpointsContext): void;
|
|
11
9
|
export {};
|