@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.
Files changed (60) hide show
  1. package/custom/api/dashboardApi.ts +137 -5
  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 +165 -179
  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 -2
  9. package/dist/custom/api/dashboardApi.js +90 -5
  10. package/dist/custom/api/dashboardApi.ts +137 -5
  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 +165 -179
  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/dashboard.d.ts +2 -4
  23. package/dist/endpoint/dashboard.js +1 -21
  24. package/dist/endpoint/groups.d.ts +1 -0
  25. package/dist/endpoint/groups.js +61 -48
  26. package/dist/endpoint/widgets.d.ts +1 -0
  27. package/dist/endpoint/widgets.js +167 -64
  28. package/dist/schema/api.d.ts +11710 -2785
  29. package/dist/schema/api.js +118 -26
  30. package/dist/schema/widget.d.ts +425 -1980
  31. package/dist/schema/widget.js +13 -374
  32. package/dist/schema/widgets/charts.d.ts +1689 -0
  33. package/dist/schema/widgets/charts.js +92 -0
  34. package/dist/schema/widgets/common.d.ts +275 -0
  35. package/dist/schema/widgets/common.js +171 -0
  36. package/dist/schema/widgets/gauge-card.d.ts +172 -0
  37. package/dist/schema/widgets/gauge-card.js +28 -0
  38. package/dist/schema/widgets/kpi-card.d.ts +212 -0
  39. package/dist/schema/widgets/kpi-card.js +43 -0
  40. package/dist/schema/widgets/pivot-table.d.ts +196 -0
  41. package/dist/schema/widgets/pivot-table.js +17 -0
  42. package/dist/schema/widgets/table.d.ts +130 -0
  43. package/dist/schema/widgets/table.js +12 -0
  44. package/dist/services/dashboardConfigService.d.ts +4 -0
  45. package/dist/services/dashboardConfigService.js +46 -0
  46. package/dist/services/widgetDataService.js +96 -2
  47. package/endpoint/dashboard.ts +2 -33
  48. package/endpoint/groups.ts +91 -72
  49. package/endpoint/widgets.ts +260 -87
  50. package/package.json +1 -1
  51. package/schema/api.ts +148 -28
  52. package/schema/widget.ts +43 -425
  53. package/schema/widgets/charts.ts +113 -0
  54. package/schema/widgets/common.ts +194 -0
  55. package/schema/widgets/gauge-card.ts +34 -0
  56. package/schema/widgets/kpi-card.ts +49 -0
  57. package/schema/widgets/pivot-table.ts +24 -0
  58. package/schema/widgets/table.ts +18 -0
  59. package/services/dashboardConfigService.ts +73 -0
  60. 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
- This skill is action-oriented. When the user asks to create, add, edit, update, move, remove, or configure dashboard entities, complete the request by calling the dashboard tools. Do not satisfy dashboard mutation requests by only showing JSON, YAML, JavaScript, TypeScript, Zod schemas, or example config snippets.
8
+ Use this skill for dashboard group/widget mutations and dashboard data loading.
9
9
 
10
- ## Primary Rule
10
+ ## Core rule
11
11
 
12
- If callable dashboard tools are available, use them.
12
+ If dashboard tools are callable, use tools. Do not answer mutation requests by only printing config.
13
13
 
14
- A response that only shows a config object, schema, or code snippet is incomplete unless the user explicitly asked for a schema, code example, or explanation.
14
+ ## Entity boundaries
15
15
 
16
- For dashboard mutation requests, the expected flow is:
16
+ Dashboard root, groups, and widgets are different entities.
17
17
 
18
- 1. Load dashboard state when needed.
19
- 2. Choose or create the target group/widget.
20
- 3. Call the appropriate dashboard mutation tool. (WITHOUT USER CONFIRMATION)
21
- 4. Return a short result summary.
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
- Do not print the widget config as the main answer instead of calling tools.
23
+ ## Tool routing
24
24
 
25
- ## User Intent Mapping
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
- Use dashboard tools for these intents:
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
- - "add/create/write/make a widget" → create and configure a widget.
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
- If the user asks how the schema works, how to implement the API, or how to change backend code, then answer as a developer/code task instead of mutating the dashboard.
47
+ Before creating a group, call dashboard_get_config and check existing groups.
38
48
 
39
- ## Callable Dashboard Tools
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
- Use these tools whenever available:
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
- - `dashboard_get_config`
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
- If a dashboard tool is known by name but its argument schema is not loaded, call `fetch_tool_schema` for that tool first. After the schema is loaded, call the dashboard tool. Do not guess arguments if `fetch_tool_schema` is available.
59
+ ## Create-and-configure workflow
56
60
 
57
- ## Tool Argument Rules
61
+ For any request to create KPI/chart/table/pivot/gauge widgets:
58
62
 
59
- Do not pass fields between dashboard tools by analogy. Use each tool's schema.
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
- - `dashboard_add_dashboard_group` creates a new group. It accepts the dashboard slug only. Never pass `groupId` to this tool.
62
- - `dashboard_set_dashboard_config` replaces the full dashboard config. Use it when the user explicitly asks to edit the whole dashboard config.
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
- ## Widget Creation Workflow
77
+ ## Empty widget rule
69
78
 
70
- For requests like:
79
+ dashboard_add_dashboard_widget creates only:
71
80
 
72
- - "add a table widget"
73
- - "create a chart"
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
- do this:
84
+ This is incomplete for KPI/chart/table/pivot/gauge/spend/revenue/usage widgets.
78
85
 
79
- 1. Call `dashboard_get_config` with the requested slug, or `default` if slug is not specified.
80
- 2. Select the requested group. If the group is not specified, use the first existing group.
81
- 3. If there are no groups, call `dashboard_add_dashboard_group`.
82
- 4. Call `dashboard_add_dashboard_widget` with the selected group id.
83
- 5. Call `dashboard_set_widget_config` with the returned widget id and schema-valid config.
84
- 6. Return a short summary with dashboard slug, group id, widget id, target, label, resource, selected fields, order, and limit.
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
- Do not stop after generating config text.
93
+ ## State machine
87
94
 
88
- ## Widget Update Workflow
95
+ For create group + widgets tasks, follow this exact state order:
89
96
 
90
- For requests like:
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
- - "change this widget"
93
- - "make the chart use another field"
94
- - "update the widget config"
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
- do this:
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
- 1. Call `dashboard_get_config`.
100
- 2. Find the widget by id, label, or clear context.
101
- 3. Build the new config while preserving server-owned fields handled by the API.
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
- If the widget cannot be identified, ask only for the missing widget id or label.
117
+ ## Specialized widget tools
106
118
 
107
- ## Group Workflow
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
- For group requests:
131
+ Each specialized tool accepts:
132
+ - slug
133
+ - widgetId
134
+ - config
110
135
 
111
- - Add group `dashboard_add_dashboard_group`
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
- If slug is missing, use `default`.
138
+ ## Widget config keys
117
139
 
118
- ## Dashboard Config Workflow
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 `dashboard_set_dashboard_config` only when the user explicitly asks to edit the whole dashboard root config.
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
- For requests like:
152
+ ## Query shape rules
123
153
 
124
- - "update root dashboard config"
125
- - "replace the whole dashboard config"
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
- do this:
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
- 1. Call `dashboard_get_config`.
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
- Do not use `dashboard_set_dashboard_config` to store reusable widget variables.
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
- ## Widget Config Rules
171
+ For rolling ranges, use this exact filter shape:
137
172
 
138
- Use the current schema keys exactly:
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
- - Use `target`, not `type`.
141
- - Use `label`, not `title`.
142
- - Use `query`, not `dataSource`.
143
- - Use `resource`, not `resourceId`.
144
- - Use `group_by`, not `groupBy`.
145
- - Use `order_by`, not `orderBy`.
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
- ## Variables And Lookup Calcs
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
- Widget config can define variables:
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
- token_prices_per_1m:
184
- input:
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: input_tokens / 1000000 * lookup($variables.token_prices_per_1m.input, model, 0)
231
- as: input_cost
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-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,8 +1,8 @@
1
1
  import ChartWidget from './chart/ChartWidget.vue';
2
- import GaugeCardWidget from './gauge-card/GaugeCardWidget.vue';
3
- import KpiCardWidget from './kpi-card/KpiCardWidget.vue';
4
- import PivotTableWidget from './pivot-table/PivotTableWidget.vue';
5
- import TableWidget from './table/TableWidget.vue';
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 './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
 
@@ -1,11 +1,9 @@
1
- import type { AdminUser, IHttpServer } from 'adminforth';
1
+ import type { IHttpServer } from 'adminforth';
2
2
  import type { DashboardConfig } from '../custom/model/dashboard.types.js';
3
- import type { DashboardRecord, PersistedDashboardResponse } from '../services/dashboardConfigService.js';
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 {};