@adminforth/dashboard 1.3.0 → 1.4.1
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/README.md +103 -15
- package/custom/api/dashboardApi.ts +9 -8
- package/custom/model/dashboard.types.ts +63 -270
- package/custom/model/dashboardTopics.ts +5 -0
- package/custom/runtime/DashboardGroup.vue +2 -2
- package/custom/runtime/DashboardPage.vue +17 -7
- package/custom/runtime/DashboardRuntime.vue +20 -8
- package/custom/runtime/WidgetRenderer.vue +1 -2
- package/custom/runtime/WidgetShell.vue +3 -3
- package/custom/skills/adminforth-dashboard/SKILL.md +110 -3
- package/custom/widgets/{gauge-card/GaugeCardWidget.vue → GaugeCardWidget.vue} +63 -61
- package/custom/widgets/{kpi-card/KpiCardWidget.vue → KpiCardWidget.vue} +35 -33
- package/custom/widgets/{pivot-table/PivotTableWidget.vue → PivotTableWidget.vue} +71 -68
- package/custom/widgets/{table/TableWidget.vue → TableWidget.vue} +5 -5
- package/custom/widgets/chart/{bar/BarChart.vue → BarChart.vue} +2 -2
- package/custom/widgets/chart/ChartWidget.vue +24 -18
- package/{dist/custom/widgets/chart/funnel → custom/widgets/chart}/FunnelChart.vue +80 -78
- package/{dist/custom/widgets/chart/line → custom/widgets/chart}/LineChart.vue +2 -2
- package/custom/widgets/chart/{pie/PieChart.vue → PieChart.vue} +2 -2
- package/{dist/custom/widgets/chart/stacked-bar → custom/widgets/chart}/StackedBarChart.vue +97 -95
- package/custom/widgets/chart/chart.types.ts +0 -28
- package/dist/custom/api/dashboardApi.d.ts +4 -7
- package/dist/custom/api/dashboardApi.js +5 -0
- package/dist/custom/api/dashboardApi.ts +9 -8
- package/dist/custom/model/dashboard.types.d.ts +40 -31
- package/dist/custom/model/dashboard.types.js +13 -152
- package/dist/custom/model/dashboard.types.ts +63 -270
- package/dist/custom/model/dashboardTopics.d.ts +2 -0
- package/dist/custom/model/dashboardTopics.js +8 -0
- package/dist/custom/model/dashboardTopics.ts +5 -0
- package/dist/custom/queries/useDashboardConfig.d.ts +116 -96
- package/dist/custom/queries/useWidgetData.d.ts +116 -96
- package/dist/custom/runtime/DashboardGroup.vue +2 -2
- package/dist/custom/runtime/DashboardPage.vue +17 -7
- package/dist/custom/runtime/DashboardRuntime.vue +20 -8
- package/dist/custom/runtime/WidgetRenderer.vue +1 -2
- package/dist/custom/runtime/WidgetShell.vue +3 -3
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +110 -3
- package/dist/custom/widgets/{gauge-card/GaugeCardWidget.vue → GaugeCardWidget.vue} +63 -61
- package/dist/custom/widgets/{kpi-card/KpiCardWidget.vue → KpiCardWidget.vue} +35 -33
- package/dist/custom/widgets/{pivot-table/PivotTableWidget.vue → PivotTableWidget.vue} +71 -68
- package/dist/custom/widgets/{table/TableWidget.vue → TableWidget.vue} +5 -5
- package/dist/custom/widgets/chart/{bar/BarChart.vue → BarChart.vue} +2 -2
- package/dist/custom/widgets/chart/ChartWidget.vue +24 -18
- package/{custom/widgets/chart/funnel → dist/custom/widgets/chart}/FunnelChart.vue +80 -78
- package/{custom/widgets/chart/line → dist/custom/widgets/chart}/LineChart.vue +2 -2
- package/dist/custom/widgets/chart/{pie/PieChart.vue → PieChart.vue} +2 -2
- package/{custom/widgets/chart/stacked-bar → dist/custom/widgets/chart}/StackedBarChart.vue +97 -95
- package/dist/custom/widgets/chart/chart.types.d.ts +0 -2
- package/dist/custom/widgets/chart/chart.types.js +0 -23
- package/dist/custom/widgets/chart/chart.types.ts +0 -28
- package/dist/endpoint/dashboard.d.ts +6 -2
- package/dist/endpoint/dashboard.js +29 -5
- package/dist/endpoint/groups.d.ts +2 -21
- package/dist/endpoint/groups.js +18 -16
- package/dist/endpoint/widgets.d.ts +2 -4
- package/dist/endpoint/widgets.js +28 -74
- package/dist/index.js +1 -3
- package/dist/schema/api.d.ts +2172 -500
- package/dist/schema/api.js +21 -13
- package/dist/schema/widget.d.ts +1076 -263
- package/dist/schema/widget.js +108 -49
- package/dist/services/dashboardConfigService.d.ts +0 -10
- package/dist/services/dashboardConfigService.js +6 -21
- package/dist/services/widgetDataService.d.ts +2 -1
- package/dist/services/widgetDataService.js +266 -206
- package/endpoint/dashboard.ts +47 -7
- package/endpoint/groups.ts +25 -42
- package/endpoint/widgets.ts +41 -96
- package/index.ts +0 -3
- package/package.json +3 -3
- package/schema/api.ts +23 -13
- package/schema/widget.ts +119 -55
- package/services/dashboardConfigService.ts +6 -25
- package/services/widgetDataService.ts +350 -237
- package/custom/widgets/chart/histogram/HistogramChart.vue +0 -21
- package/dist/custom/widgets/chart/histogram/HistogramChart.vue +0 -21
- package/dist/services/widgetConfigValidator.d.ts +0 -8
- package/dist/services/widgetConfigValidator.js +0 -27
- package/services/widgetConfigValidator.ts +0 -61
|
@@ -50,20 +50,31 @@ import { useRoute } from 'vue-router'
|
|
|
50
50
|
import { Button } from '@/afcl'
|
|
51
51
|
import { useCoreStore } from '@/stores/core'
|
|
52
52
|
import websocket from '@/websocket'
|
|
53
|
+
import { getDashboardConfigUpdatedTopic } from '../model/dashboardTopics.js'
|
|
53
54
|
import DashboardRuntime from './DashboardRuntime.vue'
|
|
54
55
|
import { useDashboardConfig } from '../queries/useDashboardConfig.js'
|
|
55
56
|
|
|
56
57
|
const route = useRoute()
|
|
57
58
|
const coreStore = useCoreStore()
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
function getDashboardSlugFromRouteParam(value: string | string[] | undefined) {
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
if (!value[0]) {
|
|
63
|
+
throw new Error('Dashboard slug route param is required')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return value[0]
|
|
67
|
+
}
|
|
61
68
|
|
|
62
|
-
if (
|
|
63
|
-
|
|
69
|
+
if (!value) {
|
|
70
|
+
throw new Error('Dashboard slug route param is required')
|
|
64
71
|
}
|
|
65
72
|
|
|
66
|
-
return
|
|
73
|
+
return value
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const dashboardSlug = computed(() => {
|
|
77
|
+
return getDashboardSlugFromRouteParam(route.params.slug as string | string[] | undefined)
|
|
67
78
|
})
|
|
68
79
|
|
|
69
80
|
const {
|
|
@@ -78,11 +89,10 @@ const isAdmin = computed(() => {
|
|
|
78
89
|
return coreStore.adminUser?.dbUser.role === 'superadmin'
|
|
79
90
|
})
|
|
80
91
|
|
|
81
|
-
const DASHBOARD_CONFIG_UPDATED_TOPIC_PREFIX = '/opentopic/dashboard-config-updated'
|
|
82
92
|
const subscribedTopic = ref<string | null>(null)
|
|
83
93
|
|
|
84
94
|
const dashboardConfigUpdatedTopic = computed(() => {
|
|
85
|
-
return
|
|
95
|
+
return getDashboardConfigUpdatedTopic(dashboardSlug.value)
|
|
86
96
|
})
|
|
87
97
|
|
|
88
98
|
function handleDashboardConfigUpdated(data: { slug?: string; revision?: number }) {
|
|
@@ -207,6 +207,8 @@ import { DashboardApiError, dashboardApi, type DashboardResponse } from '../api/
|
|
|
207
207
|
import type {
|
|
208
208
|
DashboardConfig,
|
|
209
209
|
DashboardGroupConfig,
|
|
210
|
+
EditableDashboardGroupConfig,
|
|
211
|
+
EditableDashboardWidgetConfig,
|
|
210
212
|
DashboardGroupMoveDirection,
|
|
211
213
|
DashboardWidgetConfig,
|
|
212
214
|
DashboardWidgetMoveDirection,
|
|
@@ -252,13 +254,15 @@ const sortedGroups = computed(() => {
|
|
|
252
254
|
return [...draftConfig.value.groups].sort((a, b) => a.order - b.order)
|
|
253
255
|
})
|
|
254
256
|
|
|
255
|
-
|
|
257
|
+
function groupWidgetsByGroupId(widgets: DashboardWidgetConfig[]) {
|
|
256
258
|
const result = new Map<string, DashboardWidgetConfig[]>()
|
|
257
259
|
|
|
258
|
-
for (const widget of
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
260
|
+
for (const widget of widgets) {
|
|
261
|
+
const nextWidgets = result.get(widget.group_id)
|
|
262
|
+
? [...result.get(widget.group_id)!, widget]
|
|
263
|
+
: [widget]
|
|
264
|
+
|
|
265
|
+
result.set(widget.group_id, nextWidgets)
|
|
262
266
|
}
|
|
263
267
|
|
|
264
268
|
for (const [groupId, widgets] of result.entries()) {
|
|
@@ -266,6 +270,10 @@ const widgetsByGroupId = computed<Map<string, DashboardWidgetConfig[]>>(() => {
|
|
|
266
270
|
}
|
|
267
271
|
|
|
268
272
|
return result
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const widgetsByGroupId = computed<Map<string, DashboardWidgetConfig[]>>(() => {
|
|
276
|
+
return groupWidgetsByGroupId(draftConfig.value.widgets as DashboardWidgetConfig[])
|
|
269
277
|
})
|
|
270
278
|
|
|
271
279
|
const visibleGroups = computed(() => {
|
|
@@ -329,8 +337,12 @@ async function removeGroup(groupId: string) {
|
|
|
329
337
|
}
|
|
330
338
|
|
|
331
339
|
function editGroup(group: DashboardGroupConfig) {
|
|
340
|
+
const editableGroupConfig: EditableDashboardGroupConfig = {
|
|
341
|
+
label: group.label,
|
|
342
|
+
}
|
|
343
|
+
|
|
332
344
|
editingGroupId.value = group.id
|
|
333
|
-
groupConfigCode.value = stringifyYaml(
|
|
345
|
+
groupConfigCode.value = stringifyYaml(editableGroupConfig)
|
|
334
346
|
groupConfigError.value = ''
|
|
335
347
|
}
|
|
336
348
|
|
|
@@ -340,7 +352,7 @@ async function saveGroupConfig() {
|
|
|
340
352
|
}
|
|
341
353
|
|
|
342
354
|
try {
|
|
343
|
-
const groupConfig = parseYaml(groupConfigCode.value) as
|
|
355
|
+
const groupConfig = parseYaml(groupConfigCode.value) as EditableDashboardGroupConfig
|
|
344
356
|
|
|
345
357
|
applyDashboardResponse(
|
|
346
358
|
await dashboardApi.setDashboardGroupConfig(
|
|
@@ -402,7 +414,7 @@ async function saveWidgetConfig() {
|
|
|
402
414
|
try {
|
|
403
415
|
widgetConfigError.value = ''
|
|
404
416
|
widgetConfigFieldErrors.value = []
|
|
405
|
-
const widgetConfig = parseYaml(widgetConfigCode.value) as
|
|
417
|
+
const widgetConfig = parseYaml(widgetConfigCode.value) as EditableDashboardWidgetConfig
|
|
406
418
|
|
|
407
419
|
applyDashboardResponse(
|
|
408
420
|
await dashboardApi.setWidgetConfig(
|
|
@@ -26,7 +26,6 @@
|
|
|
26
26
|
<script setup lang="ts">
|
|
27
27
|
import { computed } from 'vue'
|
|
28
28
|
import type { DashboardWidgetConfig } from '../model/dashboard.types.js'
|
|
29
|
-
import { normalizeChartWidgetConfig } from '../widgets/chart/chart.types.js'
|
|
30
29
|
import { getWidgetLabel, getWidgetRegistration } from '../widgets/registry.js'
|
|
31
30
|
|
|
32
31
|
const props = defineProps<{
|
|
@@ -53,7 +52,7 @@ const widgetTitle = computed(() => {
|
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
if (props.widget.target === 'chart') {
|
|
56
|
-
return
|
|
55
|
+
return props.widget.chart.title || 'Untitled chart'
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
return getWidgetLabel(props.widget.target)
|
|
@@ -136,10 +136,10 @@ const widgetLayoutVars = computed<CSSProperties>(() => {
|
|
|
136
136
|
|
|
137
137
|
return {
|
|
138
138
|
'--widget-basis': clampToContainerWidth(fixedWidth ?? basis),
|
|
139
|
-
'--widget-min-width': clampToContainerWidth(fixedWidth ?? formatWidth(props.layout?.
|
|
140
|
-
'--widget-max-width': props.layout?.
|
|
139
|
+
'--widget-min-width': clampToContainerWidth(fixedWidth ?? formatWidth(props.layout?.min_width) ?? basis),
|
|
140
|
+
'--widget-max-width': props.layout?.max_width === null
|
|
141
141
|
? '100%'
|
|
142
|
-
: clampToContainerWidth(fixedWidth ?? formatWidth(props.layout?.
|
|
142
|
+
: clampToContainerWidth(fixedWidth ?? formatWidth(props.layout?.max_width) ?? '100%'),
|
|
143
143
|
height: formatWidth(props.layout?.height ?? DEFAULT_WIDGET_HEIGHT),
|
|
144
144
|
}
|
|
145
145
|
})
|
|
@@ -41,6 +41,7 @@ If the user asks how the schema works, how to implement the API, or how to chang
|
|
|
41
41
|
Use these tools whenever available:
|
|
42
42
|
|
|
43
43
|
- `dashboard_get_config`
|
|
44
|
+
- `dashboard_set_dashboard_config`
|
|
44
45
|
- `dashboard_add_dashboard_group`
|
|
45
46
|
- `dashboard_set_dashboard_group_config`
|
|
46
47
|
- `dashboard_move_dashboard_group`
|
|
@@ -58,6 +59,7 @@ If a dashboard tool is known by name but its argument schema is not loaded, call
|
|
|
58
59
|
Do not pass fields between dashboard tools by analogy. Use each tool's schema.
|
|
59
60
|
|
|
60
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.
|
|
61
63
|
- `dashboard_add_dashboard_widget` creates a widget inside an existing group. Use it when you already have a `groupId`.
|
|
62
64
|
- `dashboard_set_dashboard_group_config`, `dashboard_move_dashboard_group`, and `dashboard_remove_dashboard_group` operate on an existing group and need `groupId`.
|
|
63
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`.
|
|
@@ -113,17 +115,122 @@ For group requests:
|
|
|
113
115
|
|
|
114
116
|
If slug is missing, use `default`.
|
|
115
117
|
|
|
118
|
+
## Dashboard Config Workflow
|
|
119
|
+
|
|
120
|
+
Use `dashboard_set_dashboard_config` only when the user explicitly asks to edit the whole dashboard root config.
|
|
121
|
+
|
|
122
|
+
For requests like:
|
|
123
|
+
|
|
124
|
+
- "update root dashboard config"
|
|
125
|
+
- "replace the whole dashboard config"
|
|
126
|
+
|
|
127
|
+
do this:
|
|
128
|
+
|
|
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.
|
|
133
|
+
|
|
134
|
+
Do not use `dashboard_set_dashboard_config` to store reusable widget variables.
|
|
135
|
+
|
|
116
136
|
## Widget Config Rules
|
|
117
137
|
|
|
118
138
|
Use the current schema keys exactly:
|
|
119
139
|
|
|
120
140
|
- Use `target`, not `type`.
|
|
121
141
|
- Use `label`, not `title`.
|
|
122
|
-
- Use `query`, not `
|
|
123
|
-
- Use `resource`, not `
|
|
142
|
+
- Use `query`, not `dataSource`.
|
|
143
|
+
- Use `resource`, not `resourceId`.
|
|
124
144
|
- Use `group_by`, not `groupBy`.
|
|
125
145
|
- Use `order_by`, not `orderBy`.
|
|
126
146
|
- Use `page_size`, not `pageSize`.
|
|
127
|
-
- For
|
|
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.
|
|
128
148
|
- Use `card` for KPI and gauge widget view config.
|
|
129
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.
|
|
152
|
+
|
|
153
|
+
## Variables And Lookup Calcs
|
|
154
|
+
|
|
155
|
+
Widget config can define variables:
|
|
156
|
+
|
|
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
|
+
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
|
|
213
|
+
|
|
214
|
+
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
|
+
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
|
+
```
|
|
@@ -1,8 +1,66 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="mt-3 rounded-lg border border-lightListBorder bg-lightTableBackground p-4 dark:border-darkListBorder dark:bg-darkTableBackground">
|
|
3
|
+
<div
|
|
4
|
+
v-if="isLoading"
|
|
5
|
+
class="text-sm text-lightListTableText dark:text-darkListTableText"
|
|
6
|
+
>
|
|
7
|
+
Loading...
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div
|
|
11
|
+
v-else-if="error"
|
|
12
|
+
class="text-sm text-lightInputErrorColor"
|
|
13
|
+
>
|
|
14
|
+
Failed to load gauge data
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div
|
|
18
|
+
v-else
|
|
19
|
+
class="flex flex-col items-center gap-2"
|
|
20
|
+
>
|
|
21
|
+
<svg
|
|
22
|
+
width="180"
|
|
23
|
+
height="104"
|
|
24
|
+
viewBox="0 0 180 104"
|
|
25
|
+
role="img"
|
|
26
|
+
:aria-label="valueField"
|
|
27
|
+
>
|
|
28
|
+
<path
|
|
29
|
+
d="M18 90a72 72 0 0 1 144 0"
|
|
30
|
+
class="text-lightListBorder dark:text-darkListBorder"
|
|
31
|
+
fill="none"
|
|
32
|
+
stroke="currentColor"
|
|
33
|
+
stroke-linecap="round"
|
|
34
|
+
stroke-width="18"
|
|
35
|
+
/>
|
|
36
|
+
<path
|
|
37
|
+
d="M18 90a72 72 0 0 1 144 0"
|
|
38
|
+
fill="none"
|
|
39
|
+
:stroke="gaugeColor"
|
|
40
|
+
stroke-linecap="round"
|
|
41
|
+
stroke-width="18"
|
|
42
|
+
:stroke-dasharray="circumference"
|
|
43
|
+
:stroke-dashoffset="strokeDashoffset"
|
|
44
|
+
/>
|
|
45
|
+
</svg>
|
|
46
|
+
|
|
47
|
+
<div class="text-3xl font-bold text-lightNavbarText dark:text-darkNavbarText">
|
|
48
|
+
{{ gaugeConfig?.value.prefix ?? '' }}{{ formattedValue }}{{ gaugeConfig?.value.suffix ?? '' }}
|
|
49
|
+
</div>
|
|
50
|
+
<div class="text-sm text-lightListTableText dark:text-darkListTableText">
|
|
51
|
+
{{ formattedMinValue }} - {{ formattedMaxValue }}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</template>
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
1
59
|
<script setup lang="ts">
|
|
2
60
|
import { computed, watch } from 'vue'
|
|
3
|
-
import { useWidgetData } from '
|
|
4
|
-
import type { DashboardWidgetConfig, DashboardWidgetTableData } from '
|
|
5
|
-
import { CHART_COLORS, formatChartValue, toFiniteNumber } from '
|
|
61
|
+
import { useWidgetData } from '../queries/useWidgetData.js'
|
|
62
|
+
import type { DashboardWidgetConfig, DashboardWidgetTableData } from '../model/dashboard.types.js'
|
|
63
|
+
import { CHART_COLORS, formatChartValue, toFiniteNumber } from './chart/chart.utils.js'
|
|
6
64
|
|
|
7
65
|
const props = defineProps<{
|
|
8
66
|
dashboardSlug: string
|
|
@@ -57,13 +115,13 @@ const widgetData = computed(() => data.value?.data as DashboardWidgetTableData |
|
|
|
57
115
|
const columns = computed(() => widgetData.value?.columns ?? [])
|
|
58
116
|
const firstRow = computed(() => widgetData.value?.rows[0] ?? {})
|
|
59
117
|
const valueField = computed(() => gaugeConfig.value?.value.field || columns.value[0])
|
|
60
|
-
const targetField = computed(() => gaugeConfig.value?.target?.field ?? gaugeConfig.value?.progress?.
|
|
118
|
+
const targetField = computed(() => gaugeConfig.value?.target?.field ?? gaugeConfig.value?.progress?.target_field)
|
|
61
119
|
const minValue = computed(() => {
|
|
62
120
|
return 0
|
|
63
121
|
})
|
|
64
122
|
const maxValue = computed(() => {
|
|
65
123
|
const dynamicMax = targetField.value ? parseOptionalNumber(firstRow.value[targetField.value]) : undefined
|
|
66
|
-
return dynamicMax ?? parseOptionalNumber(gaugeConfig.value?.target?.value ?? gaugeConfig.value?.progress?.
|
|
124
|
+
return dynamicMax ?? parseOptionalNumber(gaugeConfig.value?.target?.value ?? gaugeConfig.value?.progress?.target_value) ?? 100
|
|
67
125
|
})
|
|
68
126
|
const value = computed(() => toFiniteNumber(firstRow.value[valueField.value]))
|
|
69
127
|
const fractionDigits = computed(() => Math.min([
|
|
@@ -95,59 +153,3 @@ const circumference = Math.PI * radius
|
|
|
95
153
|
const strokeDashoffset = computed(() => circumference * (1 - progress.value))
|
|
96
154
|
const gaugeColor = computed(() => gaugeConfig.value?.color || CHART_COLORS[0])
|
|
97
155
|
</script>
|
|
98
|
-
|
|
99
|
-
<template>
|
|
100
|
-
<div class="mt-3 rounded-lg border border-lightListBorder bg-lightTableBackground p-4 dark:border-darkListBorder dark:bg-darkTableBackground">
|
|
101
|
-
<div
|
|
102
|
-
v-if="isLoading"
|
|
103
|
-
class="text-sm text-lightListTableText dark:text-darkListTableText"
|
|
104
|
-
>
|
|
105
|
-
Loading...
|
|
106
|
-
</div>
|
|
107
|
-
|
|
108
|
-
<div
|
|
109
|
-
v-else-if="error"
|
|
110
|
-
class="text-sm text-lightInputErrorColor"
|
|
111
|
-
>
|
|
112
|
-
Failed to load gauge data
|
|
113
|
-
</div>
|
|
114
|
-
|
|
115
|
-
<div
|
|
116
|
-
v-else
|
|
117
|
-
class="flex flex-col items-center gap-2"
|
|
118
|
-
>
|
|
119
|
-
<svg
|
|
120
|
-
width="180"
|
|
121
|
-
height="104"
|
|
122
|
-
viewBox="0 0 180 104"
|
|
123
|
-
role="img"
|
|
124
|
-
:aria-label="valueField"
|
|
125
|
-
>
|
|
126
|
-
<path
|
|
127
|
-
d="M18 90a72 72 0 0 1 144 0"
|
|
128
|
-
class="text-lightListBorder dark:text-darkListBorder"
|
|
129
|
-
fill="none"
|
|
130
|
-
stroke="currentColor"
|
|
131
|
-
stroke-linecap="round"
|
|
132
|
-
stroke-width="18"
|
|
133
|
-
/>
|
|
134
|
-
<path
|
|
135
|
-
d="M18 90a72 72 0 0 1 144 0"
|
|
136
|
-
fill="none"
|
|
137
|
-
:stroke="gaugeColor"
|
|
138
|
-
stroke-linecap="round"
|
|
139
|
-
stroke-width="18"
|
|
140
|
-
:stroke-dasharray="circumference"
|
|
141
|
-
:stroke-dashoffset="strokeDashoffset"
|
|
142
|
-
/>
|
|
143
|
-
</svg>
|
|
144
|
-
|
|
145
|
-
<div class="text-3xl font-bold text-lightNavbarText dark:text-darkNavbarText">
|
|
146
|
-
{{ gaugeConfig?.value.prefix ?? '' }}{{ formattedValue }}{{ gaugeConfig?.value.suffix ?? '' }}
|
|
147
|
-
</div>
|
|
148
|
-
<div class="text-sm text-lightListTableText dark:text-darkListTableText">
|
|
149
|
-
{{ formattedMinValue }} - {{ formattedMaxValue }}
|
|
150
|
-
</div>
|
|
151
|
-
</div>
|
|
152
|
-
</div>
|
|
153
|
-
</template>
|
|
@@ -1,8 +1,40 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="mt-3 rounded-lg border border-lightListBorder bg-lightTableBackground p-4 dark:border-darkListBorder dark:bg-darkTableBackground">
|
|
3
|
+
<div
|
|
4
|
+
v-if="isLoading"
|
|
5
|
+
class="text-sm text-lightListTableText dark:text-darkListTableText"
|
|
6
|
+
>
|
|
7
|
+
Loading...
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div
|
|
11
|
+
v-else-if="error"
|
|
12
|
+
class="text-sm text-lightInputErrorColor"
|
|
13
|
+
>
|
|
14
|
+
Failed to load KPI data
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div
|
|
18
|
+
v-else
|
|
19
|
+
class="grid gap-1"
|
|
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 }}
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
1
33
|
<script setup lang="ts">
|
|
2
34
|
import { computed, watch } from 'vue'
|
|
3
|
-
import { useWidgetData } from '
|
|
4
|
-
import type { DashboardWidgetConfig, DashboardWidgetTableData } from '
|
|
5
|
-
import { formatChartValue, toFiniteNumber } from '
|
|
35
|
+
import { useWidgetData } from '../queries/useWidgetData.js'
|
|
36
|
+
import type { DashboardWidgetConfig, DashboardWidgetTableData } from '../model/dashboard.types.js'
|
|
37
|
+
import { formatChartValue, toFiniteNumber } from './chart/chart.utils.js'
|
|
6
38
|
|
|
7
39
|
const props = defineProps<{
|
|
8
40
|
dashboardSlug: string
|
|
@@ -37,33 +69,3 @@ const label = computed(() => kpiConfig.value?.subtitle?.field
|
|
|
37
69
|
: kpiConfig.value?.subtitle?.text ?? kpiConfig.value?.title ?? props.widget.label)
|
|
38
70
|
const formattedValue = computed(() => `${kpiConfig.value?.value.prefix ?? ''}${formatChartValue(value.value)}${kpiConfig.value?.value.suffix ?? ''}`)
|
|
39
71
|
</script>
|
|
40
|
-
|
|
41
|
-
<template>
|
|
42
|
-
<div class="mt-3 rounded-lg border border-lightListBorder bg-lightTableBackground p-4 dark:border-darkListBorder dark:bg-darkTableBackground">
|
|
43
|
-
<div
|
|
44
|
-
v-if="isLoading"
|
|
45
|
-
class="text-sm text-lightListTableText dark:text-darkListTableText"
|
|
46
|
-
>
|
|
47
|
-
Loading...
|
|
48
|
-
</div>
|
|
49
|
-
|
|
50
|
-
<div
|
|
51
|
-
v-else-if="error"
|
|
52
|
-
class="text-sm text-lightInputErrorColor"
|
|
53
|
-
>
|
|
54
|
-
Failed to load KPI data
|
|
55
|
-
</div>
|
|
56
|
-
|
|
57
|
-
<div
|
|
58
|
-
v-else
|
|
59
|
-
class="grid gap-1"
|
|
60
|
-
>
|
|
61
|
-
<div class="text-3xl font-bold text-lightNavbarText dark:text-darkNavbarText">
|
|
62
|
-
{{ formattedValue }}
|
|
63
|
-
</div>
|
|
64
|
-
<div class="text-sm text-lightListTableText dark:text-darkListTableText">
|
|
65
|
-
{{ label }}
|
|
66
|
-
</div>
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
</template>
|
|
@@ -1,11 +1,78 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="mt-3 flex h-full min-h-0 flex-col overflow-hidden rounded-lg border border-lightListBorder bg-lightTableBackground dark:border-darkListBorder dark:bg-darkTableBackground">
|
|
3
|
+
<div
|
|
4
|
+
v-if="isLoading"
|
|
5
|
+
class="p-4 text-sm text-lightListTableText dark:text-darkListTableText"
|
|
6
|
+
>
|
|
7
|
+
Loading...
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div
|
|
11
|
+
v-else-if="error"
|
|
12
|
+
class="p-4 text-sm text-lightInputErrorColor"
|
|
13
|
+
>
|
|
14
|
+
Failed to load pivot data
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div
|
|
18
|
+
v-else-if="!pivotRows.length"
|
|
19
|
+
class="p-4 text-sm text-lightListTableText dark:text-darkListTableText"
|
|
20
|
+
>
|
|
21
|
+
No data available
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div
|
|
25
|
+
v-else
|
|
26
|
+
class="min-h-0 flex-1 overflow-auto"
|
|
27
|
+
>
|
|
28
|
+
<table class="min-w-max w-full border-collapse text-left text-sm">
|
|
29
|
+
<thead class="bg-lightTableHeadingBackground text-xs uppercase text-lightTableHeadingText dark:bg-darkTableHeadingBackground dark:text-darkTableHeadingText">
|
|
30
|
+
<tr>
|
|
31
|
+
<th class="px-3 py-2 font-semibold">
|
|
32
|
+
{{ rowField }}
|
|
33
|
+
</th>
|
|
34
|
+
<th
|
|
35
|
+
v-for="column in pivotColumnLabels"
|
|
36
|
+
:key="column"
|
|
37
|
+
class="px-3 py-2 text-right font-semibold"
|
|
38
|
+
>
|
|
39
|
+
{{ column }}
|
|
40
|
+
</th>
|
|
41
|
+
</tr>
|
|
42
|
+
</thead>
|
|
43
|
+
<tbody>
|
|
44
|
+
<tr
|
|
45
|
+
v-for="row in pivotRows"
|
|
46
|
+
:key="String(row.label)"
|
|
47
|
+
class="border-t border-lightListBorder odd:bg-lightTableOddBackground even:bg-lightTableEvenBackground dark:border-darkListBorder odd:dark:bg-darkTableOddBackground even:dark:bg-darkTableEvenBackground"
|
|
48
|
+
>
|
|
49
|
+
<td class="px-3 py-2 font-medium text-lightNavbarText dark:text-darkNavbarText">
|
|
50
|
+
{{ row.label }}
|
|
51
|
+
</td>
|
|
52
|
+
<td
|
|
53
|
+
v-for="column in pivotColumnLabels"
|
|
54
|
+
:key="column"
|
|
55
|
+
class="px-3 py-2 text-right text-lightListTableText dark:text-darkListTableText"
|
|
56
|
+
>
|
|
57
|
+
{{ formatChartValue(typeof row[column] === 'number' ? row[column] : 0) }}
|
|
58
|
+
</td>
|
|
59
|
+
</tr>
|
|
60
|
+
</tbody>
|
|
61
|
+
</table>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</template>
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
1
68
|
<script setup lang="ts">
|
|
2
69
|
import { computed, watch } from 'vue'
|
|
3
|
-
import { useWidgetData } from '
|
|
70
|
+
import { useWidgetData } from '../queries/useWidgetData.js'
|
|
4
71
|
import {
|
|
5
72
|
getFieldRefField,
|
|
6
|
-
} from '
|
|
7
|
-
import type { DashboardWidgetConfig, DashboardWidgetData } from '
|
|
8
|
-
import { formatChartLabel, formatChartValue, toFiniteNumber } from '
|
|
73
|
+
} from '../model/dashboard.types.js'
|
|
74
|
+
import type { DashboardWidgetConfig, DashboardWidgetData } from '../model/dashboard.types.js'
|
|
75
|
+
import { formatChartLabel, formatChartValue, toFiniteNumber } from './chart/chart.utils.js'
|
|
9
76
|
|
|
10
77
|
const props = defineProps<{
|
|
11
78
|
dashboardSlug: string
|
|
@@ -77,67 +144,3 @@ const pivotRows = computed(() => {
|
|
|
77
144
|
})
|
|
78
145
|
</script>
|
|
79
146
|
|
|
80
|
-
<template>
|
|
81
|
-
<div class="mt-3 flex h-full min-h-0 flex-col overflow-hidden rounded-lg border border-lightListBorder bg-lightTableBackground dark:border-darkListBorder dark:bg-darkTableBackground">
|
|
82
|
-
<div
|
|
83
|
-
v-if="isLoading"
|
|
84
|
-
class="p-4 text-sm text-lightListTableText dark:text-darkListTableText"
|
|
85
|
-
>
|
|
86
|
-
Loading...
|
|
87
|
-
</div>
|
|
88
|
-
|
|
89
|
-
<div
|
|
90
|
-
v-else-if="error"
|
|
91
|
-
class="p-4 text-sm text-lightInputErrorColor"
|
|
92
|
-
>
|
|
93
|
-
Failed to load pivot data
|
|
94
|
-
</div>
|
|
95
|
-
|
|
96
|
-
<div
|
|
97
|
-
v-else-if="!pivotRows.length"
|
|
98
|
-
class="p-4 text-sm text-lightListTableText dark:text-darkListTableText"
|
|
99
|
-
>
|
|
100
|
-
No data available
|
|
101
|
-
</div>
|
|
102
|
-
|
|
103
|
-
<div
|
|
104
|
-
v-else
|
|
105
|
-
class="min-h-0 flex-1 overflow-auto"
|
|
106
|
-
>
|
|
107
|
-
<table class="min-w-max w-full border-collapse text-left text-sm">
|
|
108
|
-
<thead class="bg-lightTableHeadingBackground text-xs uppercase text-lightTableHeadingText dark:bg-darkTableHeadingBackground dark:text-darkTableHeadingText">
|
|
109
|
-
<tr>
|
|
110
|
-
<th class="px-3 py-2 font-semibold">
|
|
111
|
-
{{ rowField }}
|
|
112
|
-
</th>
|
|
113
|
-
<th
|
|
114
|
-
v-for="column in pivotColumnLabels"
|
|
115
|
-
:key="column"
|
|
116
|
-
class="px-3 py-2 text-right font-semibold"
|
|
117
|
-
>
|
|
118
|
-
{{ column }}
|
|
119
|
-
</th>
|
|
120
|
-
</tr>
|
|
121
|
-
</thead>
|
|
122
|
-
<tbody>
|
|
123
|
-
<tr
|
|
124
|
-
v-for="row in pivotRows"
|
|
125
|
-
:key="String(row.label)"
|
|
126
|
-
class="border-t border-lightListBorder odd:bg-lightTableOddBackground even:bg-lightTableEvenBackground dark:border-darkListBorder odd:dark:bg-darkTableOddBackground even:dark:bg-darkTableEvenBackground"
|
|
127
|
-
>
|
|
128
|
-
<td class="px-3 py-2 font-medium text-lightNavbarText dark:text-darkNavbarText">
|
|
129
|
-
{{ row.label }}
|
|
130
|
-
</td>
|
|
131
|
-
<td
|
|
132
|
-
v-for="column in pivotColumnLabels"
|
|
133
|
-
:key="column"
|
|
134
|
-
class="px-3 py-2 text-right text-lightListTableText dark:text-darkListTableText"
|
|
135
|
-
>
|
|
136
|
-
{{ formatChartValue(typeof row[column] === 'number' ? row[column] : 0) }}
|
|
137
|
-
</td>
|
|
138
|
-
</tr>
|
|
139
|
-
</tbody>
|
|
140
|
-
</table>
|
|
141
|
-
</div>
|
|
142
|
-
</div>
|
|
143
|
-
</template>
|