@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.
Files changed (80) hide show
  1. package/README.md +103 -15
  2. package/custom/api/dashboardApi.ts +9 -8
  3. package/custom/model/dashboard.types.ts +63 -270
  4. package/custom/model/dashboardTopics.ts +5 -0
  5. package/custom/runtime/DashboardGroup.vue +2 -2
  6. package/custom/runtime/DashboardPage.vue +17 -7
  7. package/custom/runtime/DashboardRuntime.vue +20 -8
  8. package/custom/runtime/WidgetRenderer.vue +1 -2
  9. package/custom/runtime/WidgetShell.vue +3 -3
  10. package/custom/skills/adminforth-dashboard/SKILL.md +110 -3
  11. package/custom/widgets/{gauge-card/GaugeCardWidget.vue → GaugeCardWidget.vue} +63 -61
  12. package/custom/widgets/{kpi-card/KpiCardWidget.vue → KpiCardWidget.vue} +35 -33
  13. package/custom/widgets/{pivot-table/PivotTableWidget.vue → PivotTableWidget.vue} +71 -68
  14. package/custom/widgets/{table/TableWidget.vue → TableWidget.vue} +5 -5
  15. package/custom/widgets/chart/{bar/BarChart.vue → BarChart.vue} +2 -2
  16. package/custom/widgets/chart/ChartWidget.vue +24 -18
  17. package/{dist/custom/widgets/chart/funnel → custom/widgets/chart}/FunnelChart.vue +80 -78
  18. package/{dist/custom/widgets/chart/line → custom/widgets/chart}/LineChart.vue +2 -2
  19. package/custom/widgets/chart/{pie/PieChart.vue → PieChart.vue} +2 -2
  20. package/{dist/custom/widgets/chart/stacked-bar → custom/widgets/chart}/StackedBarChart.vue +97 -95
  21. package/custom/widgets/chart/chart.types.ts +0 -28
  22. package/dist/custom/api/dashboardApi.d.ts +4 -7
  23. package/dist/custom/api/dashboardApi.js +5 -0
  24. package/dist/custom/api/dashboardApi.ts +9 -8
  25. package/dist/custom/model/dashboard.types.d.ts +40 -31
  26. package/dist/custom/model/dashboard.types.js +13 -152
  27. package/dist/custom/model/dashboard.types.ts +63 -270
  28. package/dist/custom/model/dashboardTopics.d.ts +2 -0
  29. package/dist/custom/model/dashboardTopics.js +8 -0
  30. package/dist/custom/model/dashboardTopics.ts +5 -0
  31. package/dist/custom/queries/useDashboardConfig.d.ts +116 -96
  32. package/dist/custom/queries/useWidgetData.d.ts +116 -96
  33. package/dist/custom/runtime/DashboardGroup.vue +2 -2
  34. package/dist/custom/runtime/DashboardPage.vue +17 -7
  35. package/dist/custom/runtime/DashboardRuntime.vue +20 -8
  36. package/dist/custom/runtime/WidgetRenderer.vue +1 -2
  37. package/dist/custom/runtime/WidgetShell.vue +3 -3
  38. package/dist/custom/skills/adminforth-dashboard/SKILL.md +110 -3
  39. package/dist/custom/widgets/{gauge-card/GaugeCardWidget.vue → GaugeCardWidget.vue} +63 -61
  40. package/dist/custom/widgets/{kpi-card/KpiCardWidget.vue → KpiCardWidget.vue} +35 -33
  41. package/dist/custom/widgets/{pivot-table/PivotTableWidget.vue → PivotTableWidget.vue} +71 -68
  42. package/dist/custom/widgets/{table/TableWidget.vue → TableWidget.vue} +5 -5
  43. package/dist/custom/widgets/chart/{bar/BarChart.vue → BarChart.vue} +2 -2
  44. package/dist/custom/widgets/chart/ChartWidget.vue +24 -18
  45. package/{custom/widgets/chart/funnel → dist/custom/widgets/chart}/FunnelChart.vue +80 -78
  46. package/{custom/widgets/chart/line → dist/custom/widgets/chart}/LineChart.vue +2 -2
  47. package/dist/custom/widgets/chart/{pie/PieChart.vue → PieChart.vue} +2 -2
  48. package/{custom/widgets/chart/stacked-bar → dist/custom/widgets/chart}/StackedBarChart.vue +97 -95
  49. package/dist/custom/widgets/chart/chart.types.d.ts +0 -2
  50. package/dist/custom/widgets/chart/chart.types.js +0 -23
  51. package/dist/custom/widgets/chart/chart.types.ts +0 -28
  52. package/dist/endpoint/dashboard.d.ts +6 -2
  53. package/dist/endpoint/dashboard.js +29 -5
  54. package/dist/endpoint/groups.d.ts +2 -21
  55. package/dist/endpoint/groups.js +18 -16
  56. package/dist/endpoint/widgets.d.ts +2 -4
  57. package/dist/endpoint/widgets.js +28 -74
  58. package/dist/index.js +1 -3
  59. package/dist/schema/api.d.ts +2172 -500
  60. package/dist/schema/api.js +21 -13
  61. package/dist/schema/widget.d.ts +1076 -263
  62. package/dist/schema/widget.js +108 -49
  63. package/dist/services/dashboardConfigService.d.ts +0 -10
  64. package/dist/services/dashboardConfigService.js +6 -21
  65. package/dist/services/widgetDataService.d.ts +2 -1
  66. package/dist/services/widgetDataService.js +266 -206
  67. package/endpoint/dashboard.ts +47 -7
  68. package/endpoint/groups.ts +25 -42
  69. package/endpoint/widgets.ts +41 -96
  70. package/index.ts +0 -3
  71. package/package.json +3 -3
  72. package/schema/api.ts +23 -13
  73. package/schema/widget.ts +119 -55
  74. package/services/dashboardConfigService.ts +6 -25
  75. package/services/widgetDataService.ts +350 -237
  76. package/custom/widgets/chart/histogram/HistogramChart.vue +0 -21
  77. package/dist/custom/widgets/chart/histogram/HistogramChart.vue +0 -21
  78. package/dist/services/widgetConfigValidator.d.ts +0 -8
  79. package/dist/services/widgetConfigValidator.js +0 -27
  80. 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
- const dashboardSlug = computed(() => {
60
- const slug = route.params.slug
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 (Array.isArray(slug)) {
63
- return slug[0] || 'default'
69
+ if (!value) {
70
+ throw new Error('Dashboard slug route param is required')
64
71
  }
65
72
 
66
- return (slug as string) || 'default'
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 `${DASHBOARD_CONFIG_UPDATED_TOPIC_PREFIX}/${dashboardSlug.value}`
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
- const widgetsByGroupId = computed<Map<string, DashboardWidgetConfig[]>>(() => {
257
+ function groupWidgetsByGroupId(widgets: DashboardWidgetConfig[]) {
256
258
  const result = new Map<string, DashboardWidgetConfig[]>()
257
259
 
258
- for (const widget of draftConfig.value.widgets) {
259
- const widgets = result.get(widget.group_id) ?? []
260
- widgets.push(widget)
261
- result.set(widget.group_id, widgets)
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(group)
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 DashboardGroupConfig
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 DashboardWidgetConfig
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 normalizeChartWidgetConfig(props.widget.chart)?.title || 'Untitled chart'
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?.minWidth) ?? basis),
140
- '--widget-max-width': props.layout?.maxWidth === null
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?.maxWidth) ?? '100%'),
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 `data_source`.
123
- - Use `resource`, not `resource_id`.
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 funnel charts, use `query.steps` as an ordered array of `{ name, resource, metric, filters }` steps.
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 '../../queries/useWidgetData.js'
4
- import type { DashboardWidgetConfig, DashboardWidgetTableData } from '../../model/dashboard.types.js'
5
- import { CHART_COLORS, formatChartValue, toFiniteNumber } from '../chart/chart.utils.js'
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?.targetField)
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?.targetValue) ?? 100
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 '../../queries/useWidgetData.js'
4
- import type { DashboardWidgetConfig, DashboardWidgetTableData } from '../../model/dashboard.types.js'
5
- import { formatChartValue, toFiniteNumber } from '../chart/chart.utils.js'
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 '../../queries/useWidgetData.js'
70
+ import { useWidgetData } from '../queries/useWidgetData.js'
4
71
  import {
5
72
  getFieldRefField,
6
- } from '../../model/dashboard.types.js'
7
- import type { DashboardWidgetConfig, DashboardWidgetData } from '../../model/dashboard.types.js'
8
- import { formatChartLabel, formatChartValue, toFiniteNumber } from '../chart/chart.utils.js'
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>