@adminforth/dashboard 1.11.2 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -2
- package/custom/api/dashboardApi.ts +72 -10
- package/custom/model/dashboard.types.ts +6 -0
- package/custom/runtime/DashboardGroup.vue +15 -68
- package/custom/runtime/DashboardToolbarButton.vue +32 -0
- package/custom/runtime/DashboardToolbarIcon.vue +52 -0
- package/custom/runtime/WidgetShell.vue +15 -68
- package/custom/skills/adminforth-dashboard/SKILL.md +63 -13
- package/custom/widgets/chart/StackedBarChart.vue +64 -7
- package/dist/custom/api/dashboardApi.js +59 -10
- package/dist/custom/api/dashboardApi.ts +72 -10
- package/dist/custom/model/dashboard.types.d.ts +9 -0
- package/dist/custom/model/dashboard.types.ts +6 -0
- package/dist/custom/queries/useDashboardConfig.d.ts +80 -0
- package/dist/custom/queries/useWidgetData.d.ts +80 -0
- package/dist/custom/runtime/DashboardGroup.vue +15 -68
- package/dist/custom/runtime/DashboardToolbarButton.vue +32 -0
- package/dist/custom/runtime/DashboardToolbarIcon.vue +52 -0
- package/dist/custom/runtime/WidgetShell.vue +15 -68
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +63 -13
- package/dist/custom/widgets/chart/StackedBarChart.vue +64 -7
- package/dist/schema/api.d.ts +742 -802
- package/dist/schema/api.js +2 -2
- package/dist/schema/widget.d.ts +75 -81
- package/dist/schema/widget.js +1 -1
- package/dist/schema/widgets/charts.d.ts +84 -160
- package/dist/schema/widgets/charts.js +2 -2
- package/dist/schema/widgets/common.d.ts +115 -0
- package/dist/schema/widgets/common.js +17 -1
- package/dist/schema/widgets/gauge-card.d.ts +8 -0
- package/dist/schema/widgets/kpi-card.d.ts +8 -0
- package/dist/schema/widgets/pivot-table.d.ts +8 -0
- package/dist/schema/widgets/table.d.ts +8 -0
- package/dist/services/widgetDataService.js +42 -0
- package/package.json +2 -2
- package/schema/api.ts +2 -1
- package/schema/widget.ts +2 -0
- package/schema/widgets/charts.ts +2 -1
- package/schema/widgets/common.ts +19 -1
- package/services/widgetDataService.ts +68 -0
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ Each widget has common fields:
|
|
|
39
39
|
| Widget target | Config field | Main settings | Data usage |
|
|
40
40
|
| --- | --- | --- | --- |
|
|
41
41
|
| `table` | `table` | `pagination`, `page_size`, `columns` | Uses `query` to display raw or aggregate rows. |
|
|
42
|
-
| `chart` | `chart` | `type`, `x`, `y`, `label`, `value`, `series`, `buckets`, `color`, `colors` | Uses the same `query` shape for
|
|
42
|
+
| `chart` | `chart` | `type`, `x`, `y`, `label`, `value`, `series`, `buckets`, `color`, `colors` | Uses the same `query` shape for most chart types. Multi-resource charts use `query.source: steps`; add `query.bucket` for shared numeric buckets across resources. |
|
|
43
43
|
| `kpi_card` | `card` | `value`, `subtitle`, `comparison`, `sparkline` | Reads the first returned query row. |
|
|
44
44
|
| `gauge_card` | `card` | `value`, `target`, `progress`, `color` | Reads the first returned query row. |
|
|
45
45
|
| `pivot_table` | `pivot` | `rows`, `columns`, `values` | Uses query rows to build a pivot table. |
|
|
@@ -53,7 +53,7 @@ Chart widget types:
|
|
|
53
53
|
| `bar` | Uses `x` and `y`. |
|
|
54
54
|
| `stacked_bar` | Uses `x`, `y`, and `series`. |
|
|
55
55
|
| `funnel` | Uses `label`, `value`, and optional `colors`. Data comes from the same `query` shapes as every other chart. |
|
|
56
|
-
| `histogram` | Uses `x`, `y`, and optional `buckets`. |
|
|
56
|
+
| `histogram` | Uses `x`, `y`, and optional `buckets`. Current histogram runtime support is single-resource only: provide raw rows for the numeric field and let `chart.buckets` derive counts on the frontend. For multi-resource bucket distributions, use `stacked_bar` with `query.source: steps` and `query.bucket`. |
|
|
57
57
|
|
|
58
58
|
## Query Shape
|
|
59
59
|
|
|
@@ -89,6 +89,8 @@ type QueryConfig = {
|
|
|
89
89
|
formatting?: Record<string, JsonValue>
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
`source: 'steps'` returns one aggregate row per step by default. Each step supports aggregate `select` items plus optional `filters`; it does not support per-step `field` selects, `calc` selects, or `group_by`. Add `query.bucket` when multiple resources need the same numeric buckets, for example a stacked bar distribution by price range.
|
|
93
|
+
|
|
92
94
|
type DashboardFilter =
|
|
93
95
|
| { and: DashboardFilter[] }
|
|
94
96
|
| { or: DashboardFilter[] }
|
|
@@ -155,6 +157,45 @@ query:
|
|
|
155
157
|
as: value
|
|
156
158
|
```
|
|
157
159
|
|
|
160
|
+
Bucketed multi-resource queries use `query.bucket`. The dashboard runs each step once per bucket and returns rows with `label`, `name`, `resource`, and the selected aggregate aliases:
|
|
161
|
+
|
|
162
|
+
```yaml
|
|
163
|
+
target: chart
|
|
164
|
+
label: Cars by price range and database
|
|
165
|
+
chart:
|
|
166
|
+
type: stacked_bar
|
|
167
|
+
title: Cars by price range and database
|
|
168
|
+
x:
|
|
169
|
+
field: label
|
|
170
|
+
y:
|
|
171
|
+
field: count
|
|
172
|
+
series:
|
|
173
|
+
field: name
|
|
174
|
+
query:
|
|
175
|
+
source: steps
|
|
176
|
+
bucket:
|
|
177
|
+
field: price
|
|
178
|
+
buckets:
|
|
179
|
+
- label: Budget
|
|
180
|
+
max: 3500
|
|
181
|
+
- label: Mid-range
|
|
182
|
+
min: 3500
|
|
183
|
+
max: 7000
|
|
184
|
+
- label: Premium
|
|
185
|
+
min: 7000
|
|
186
|
+
steps:
|
|
187
|
+
- name: SQLite
|
|
188
|
+
resource: cars_sl
|
|
189
|
+
select:
|
|
190
|
+
- agg: count
|
|
191
|
+
as: count
|
|
192
|
+
- name: MySQL
|
|
193
|
+
resource: cars_mysql
|
|
194
|
+
select:
|
|
195
|
+
- agg: count
|
|
196
|
+
as: count
|
|
197
|
+
```
|
|
198
|
+
|
|
158
199
|
Cost calculation example:
|
|
159
200
|
|
|
160
201
|
```yaml
|
|
@@ -62,20 +62,82 @@ export class DashboardApiError extends Error {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
function normalizeValidationErrors(response: any): DashboardWidgetConfigValidationError[] {
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
const errors = Array.isArray(response?.validationErrors)
|
|
66
|
+
? response.validationErrors
|
|
67
|
+
: Array.isArray(response?.details)
|
|
68
|
+
? response.details.map((detail: any) => ({
|
|
69
|
+
field: getValidationErrorField(detail),
|
|
70
|
+
message: String(detail.message || 'Invalid value'),
|
|
71
|
+
}))
|
|
72
|
+
: []
|
|
73
|
+
|
|
74
|
+
return simplifyValidationErrors(errors)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getValidationErrorField(detail: any) {
|
|
78
|
+
return Array.isArray(detail.instancePath)
|
|
79
|
+
? detail.instancePath.join('.')
|
|
80
|
+
: String(detail.instancePath || detail.path || 'config').replace(/^\//, '').replaceAll('/', '.')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function simplifyValidationErrors(errors: DashboardWidgetConfigValidationError[]) {
|
|
84
|
+
const collapsed = new Map<string, DashboardWidgetConfigValidationError>()
|
|
85
|
+
|
|
86
|
+
for (const error of errors) {
|
|
87
|
+
const selectItemMatch = error.field.match(/^config\.query\.select\.(\d+)$/)
|
|
88
|
+
|
|
89
|
+
if (selectItemMatch) {
|
|
90
|
+
const field = error.field
|
|
91
|
+
collapsed.set(field, {
|
|
92
|
+
field,
|
|
93
|
+
message: 'must be a valid select item: field, aggregate, or calc',
|
|
94
|
+
})
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (isUnionBranchNoise(error)) {
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const key = `${error.field}:${error.message}`
|
|
103
|
+
collapsed.set(key, error)
|
|
67
104
|
}
|
|
68
105
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
106
|
+
const simplifiedErrors = Array.from(collapsed.values())
|
|
107
|
+
|
|
108
|
+
if (simplifiedErrors.length) {
|
|
109
|
+
return simplifiedErrors
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return dedupeValidationErrors(errors).filter((error) => error.message !== 'must match a schema in anyOf').slice(0, 5)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function dedupeValidationErrors(errors: DashboardWidgetConfigValidationError[]) {
|
|
116
|
+
const deduped = new Map<string, DashboardWidgetConfigValidationError>()
|
|
117
|
+
|
|
118
|
+
for (const error of errors) {
|
|
119
|
+
deduped.set(`${error.field}:${error.message}`, error)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return Array.from(deduped.values())
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isUnionBranchNoise(error: DashboardWidgetConfigValidationError) {
|
|
126
|
+
if (error.field !== 'config') {
|
|
127
|
+
return false
|
|
76
128
|
}
|
|
77
129
|
|
|
78
|
-
return
|
|
130
|
+
return error.message === 'must NOT have additional properties'
|
|
131
|
+
|| error.message === 'must match a schema in anyOf'
|
|
132
|
+
|| error.message === 'must match exactly one schema in oneOf'
|
|
133
|
+
|| error.message === 'must have required property \'chart\''
|
|
134
|
+
|| error.message === 'must have required property "chart"'
|
|
135
|
+
|| error.message === 'must have required property \'card\''
|
|
136
|
+
|| error.message === 'must have required property "card"'
|
|
137
|
+
|| error.message === 'must have required property \'table\''
|
|
138
|
+
|| error.message === 'must have required property "table"'
|
|
139
|
+
|| error.message === 'must have required property \'pivot\''
|
|
140
|
+
|| error.message === 'must have required property "pivot"'
|
|
79
141
|
}
|
|
80
142
|
|
|
81
143
|
async function parseDashboardResponse(rawResponse: Response) {
|
|
@@ -140,6 +140,11 @@ export type ResourceQueryConfig = {
|
|
|
140
140
|
formatting?: Record<string, JsonValue>
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
export type QueryBucketConfig = {
|
|
144
|
+
field: string
|
|
145
|
+
buckets: Array<{ label: string, min?: number, max?: number }>
|
|
146
|
+
}
|
|
147
|
+
|
|
143
148
|
export type StepsQueryStepConfig = {
|
|
144
149
|
name: string
|
|
145
150
|
resource: string
|
|
@@ -150,6 +155,7 @@ export type StepsQueryStepConfig = {
|
|
|
150
155
|
export type StepsQueryConfig = {
|
|
151
156
|
source: 'steps'
|
|
152
157
|
steps: StepsQueryStepConfig[]
|
|
158
|
+
bucket?: QueryBucketConfig
|
|
153
159
|
calcs?: QueryCalcSelectItem[]
|
|
154
160
|
order_by?: QueryOrderByItem[]
|
|
155
161
|
limit?: number
|
|
@@ -14,91 +14,36 @@
|
|
|
14
14
|
v-if="isAdmin"
|
|
15
15
|
class="absolute right-3 top-3 flex gap-1 opacity-0 transition-opacity group-hover/dashboard:opacity-100"
|
|
16
16
|
>
|
|
17
|
-
<
|
|
18
|
-
type="button"
|
|
19
|
-
class="flex h-8 w-8 items-center justify-center rounded-lg border border-lightListViewButtonBorder bg-lightListViewButtonBackground text-lightListViewButtonText shadow-sm hover:bg-lightListViewButtonBackgroundHover hover:text-lightListViewButtonTextHover dark:border-darkListViewButtonBorder dark:bg-darkListViewButtonBackground dark:text-darkListViewButtonText dark:hover:bg-darkListViewButtonBackgroundHover dark:hover:text-darkListViewButtonTextHover"
|
|
17
|
+
<DashboardToolbarButton
|
|
20
18
|
title="Edit JSON"
|
|
21
19
|
@click="emit('edit-group', group)"
|
|
22
20
|
>
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
viewBox="0 0 24 24"
|
|
26
|
-
fill="none"
|
|
27
|
-
stroke="currentColor"
|
|
28
|
-
stroke-width="1.8"
|
|
29
|
-
stroke-linecap="round"
|
|
30
|
-
stroke-linejoin="round"
|
|
31
|
-
aria-hidden="true"
|
|
32
|
-
>
|
|
33
|
-
<path d="M15.5 7.5a3 3 0 1 1 1 2.2l-6.8 6.8H7.5v2.2H5.3v2.2H2.8v-2.5l7.5-7.5a5.5 5.5 0 1 1 5.2 1.6" />
|
|
34
|
-
</svg>
|
|
35
|
-
</button>
|
|
21
|
+
<DashboardToolbarIcon name="edit" />
|
|
22
|
+
</DashboardToolbarButton>
|
|
36
23
|
|
|
37
|
-
<
|
|
38
|
-
type="button"
|
|
39
|
-
class="flex h-8 w-8 items-center justify-center rounded-lg border border-lightListViewButtonBorder bg-lightListViewButtonBackground text-lightListViewButtonText shadow-sm hover:bg-lightListViewButtonBackgroundHover hover:text-lightListViewButtonTextHover disabled:opacity-45 dark:border-darkListViewButtonBorder dark:bg-darkListViewButtonBackground dark:text-darkListViewButtonText dark:hover:bg-darkListViewButtonBackgroundHover dark:hover:text-darkListViewButtonTextHover"
|
|
24
|
+
<DashboardToolbarButton
|
|
40
25
|
title="Move up"
|
|
41
26
|
:disabled="!canMoveUp"
|
|
42
27
|
@click="emit('move-up')"
|
|
43
28
|
>
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
viewBox="0 0 24 24"
|
|
47
|
-
fill="none"
|
|
48
|
-
stroke="currentColor"
|
|
49
|
-
stroke-width="2"
|
|
50
|
-
stroke-linecap="round"
|
|
51
|
-
stroke-linejoin="round"
|
|
52
|
-
aria-hidden="true"
|
|
53
|
-
>
|
|
54
|
-
<path d="m18 15-6-6-6 6" />
|
|
55
|
-
</svg>
|
|
56
|
-
</button>
|
|
29
|
+
<DashboardToolbarIcon name="move-up" />
|
|
30
|
+
</DashboardToolbarButton>
|
|
57
31
|
|
|
58
|
-
<
|
|
59
|
-
type="button"
|
|
60
|
-
class="flex h-8 w-8 items-center justify-center rounded-lg border border-lightListViewButtonBorder bg-lightListViewButtonBackground text-lightListViewButtonText shadow-sm hover:bg-lightListViewButtonBackgroundHover hover:text-lightListViewButtonTextHover disabled:opacity-45 dark:border-darkListViewButtonBorder dark:bg-darkListViewButtonBackground dark:text-darkListViewButtonText dark:hover:bg-darkListViewButtonBackgroundHover dark:hover:text-darkListViewButtonTextHover"
|
|
32
|
+
<DashboardToolbarButton
|
|
61
33
|
title="Move down"
|
|
62
34
|
:disabled="!canMoveDown"
|
|
63
35
|
@click="emit('move-down')"
|
|
64
36
|
>
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
viewBox="0 0 24 24"
|
|
68
|
-
fill="none"
|
|
69
|
-
stroke="currentColor"
|
|
70
|
-
stroke-width="2"
|
|
71
|
-
stroke-linecap="round"
|
|
72
|
-
stroke-linejoin="round"
|
|
73
|
-
aria-hidden="true"
|
|
74
|
-
>
|
|
75
|
-
<path d="m6 9 6 6 6-6" />
|
|
76
|
-
</svg>
|
|
77
|
-
</button>
|
|
37
|
+
<DashboardToolbarIcon name="move-down" />
|
|
38
|
+
</DashboardToolbarButton>
|
|
78
39
|
|
|
79
|
-
<
|
|
80
|
-
type="button"
|
|
81
|
-
class="flex h-8 w-8 items-center justify-center rounded-lg border border-lightInputErrorColor/30 bg-lightSecondary text-lightInputErrorColor shadow-sm hover:bg-lightListViewButtonBackgroundHover dark:bg-darkSecondary dark:hover:bg-darkListViewButtonBackgroundHover"
|
|
40
|
+
<DashboardToolbarButton
|
|
82
41
|
title="Remove"
|
|
42
|
+
variant="danger"
|
|
83
43
|
@click="emit('remove-group')"
|
|
84
44
|
>
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
viewBox="0 0 24 24"
|
|
88
|
-
fill="none"
|
|
89
|
-
stroke="currentColor"
|
|
90
|
-
stroke-width="2"
|
|
91
|
-
stroke-linecap="round"
|
|
92
|
-
stroke-linejoin="round"
|
|
93
|
-
aria-hidden="true"
|
|
94
|
-
>
|
|
95
|
-
<path d="M3 6h18" />
|
|
96
|
-
<path d="M8 6V4h8v2" />
|
|
97
|
-
<path d="M19 6l-1 14H6L5 6" />
|
|
98
|
-
<path d="M10 11v5" />
|
|
99
|
-
<path d="M14 11v5" />
|
|
100
|
-
</svg>
|
|
101
|
-
</button>
|
|
45
|
+
<DashboardToolbarIcon name="remove" />
|
|
46
|
+
</DashboardToolbarButton>
|
|
102
47
|
</div>
|
|
103
48
|
</header>
|
|
104
49
|
|
|
@@ -158,6 +103,8 @@
|
|
|
158
103
|
|
|
159
104
|
<script setup lang="ts">
|
|
160
105
|
import { Button } from '@/afcl'
|
|
106
|
+
import DashboardToolbarButton from './DashboardToolbarButton.vue'
|
|
107
|
+
import DashboardToolbarIcon from './DashboardToolbarIcon.vue'
|
|
161
108
|
import WidgetRenderer from './WidgetRenderer.vue'
|
|
162
109
|
import WidgetShell from './WidgetShell.vue'
|
|
163
110
|
import type { DashboardGroupConfig, DashboardWidgetConfig } from '../model/dashboard.types.js'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<button
|
|
3
|
+
type="button"
|
|
4
|
+
class="flex h-8 w-8 items-center justify-center rounded-lg border shadow-sm"
|
|
5
|
+
:class="buttonClass"
|
|
6
|
+
:title="title"
|
|
7
|
+
:disabled="disabled"
|
|
8
|
+
>
|
|
9
|
+
<slot />
|
|
10
|
+
</button>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup lang="ts">
|
|
14
|
+
import { computed } from 'vue'
|
|
15
|
+
|
|
16
|
+
const props = withDefaults(defineProps<{
|
|
17
|
+
title: string
|
|
18
|
+
disabled?: boolean
|
|
19
|
+
variant?: 'default' | 'danger'
|
|
20
|
+
}>(), {
|
|
21
|
+
disabled: false,
|
|
22
|
+
variant: 'default',
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const buttonClass = computed(() => {
|
|
26
|
+
if (props.variant === 'danger') {
|
|
27
|
+
return 'border-lightInputErrorColor/30 bg-lightSecondary text-lightInputErrorColor hover:bg-lightListViewButtonBackgroundHover dark:bg-darkSecondary dark:hover:bg-darkListViewButtonBackgroundHover'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return 'border-lightListViewButtonBorder bg-lightListViewButtonBackground text-lightListViewButtonText hover:bg-lightListViewButtonBackgroundHover hover:text-lightListViewButtonTextHover disabled:opacity-45 dark:border-darkListViewButtonBorder dark:bg-darkListViewButtonBackground dark:text-darkListViewButtonText dark:hover:bg-darkListViewButtonBackgroundHover dark:hover:text-darkListViewButtonTextHover'
|
|
31
|
+
})
|
|
32
|
+
</script>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg
|
|
3
|
+
v-if="name === 'edit'"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="16"
|
|
6
|
+
height="16"
|
|
7
|
+
viewBox="0 0 90 90"
|
|
8
|
+
fill="none"
|
|
9
|
+
aria-hidden="true"
|
|
10
|
+
>
|
|
11
|
+
<g transform="translate(90 0) scale(-1 1)">
|
|
12
|
+
<path
|
|
13
|
+
fill="currentColor"
|
|
14
|
+
d="M69.243 90c-5.389 0-10.67-2.107-14.643-6.081-5.433-5.432-7.391-13.514-5.115-20.831L26.912 40.515c-7.313 2.276-15.397.32-20.832-5.114C.147 29.468-1.625 20.617 1.566 12.852l.846-2.059 12.493 12.493c2.311 2.31 6.07 2.311 8.381 0 2.31-2.311 2.31-6.071 0-8.381L10.794 2.413l2.059-.846c7.766-3.191 16.616-1.418 22.549 4.514 5.433 5.433 7.39 13.516 5.114 20.831l22.572 22.573c7.314-2.278 15.398-.32 20.832 5.114 5.934 5.933 7.704 14.784 4.514 22.549l-.847 2.059-12.492-12.493c-1.113-1.113-2.601-1.726-4.191-1.726s-3.077.613-4.191 1.726c-2.31 2.311-2.31 6.071 0 8.381l12.493 12.492-2.06.847C74.582 89.487 71.899 89.999 69.243 90zM27.692 37.1l25.206 25.207-.322.887c-2.345 6.469-.728 13.779 4.119 18.626 4.538 4.538 11.069 6.235 17.152 4.603l-9.232-9.232c-3.466-3.467-3.466-9.109 0-12.576 3.466-3.468 9.109-3.468 12.576 0l9.232 9.232c1.633-6.083-.064-12.615-4.602-17.153-4.847-4.847-12.161-6.464-18.627-4.118l-.887.322L37.101 27.692l.322-.887c2.345-6.469.729-13.78-4.118-18.627-4.539-4.538-11.07-6.235-17.152-4.603l9.232 9.232c3.467 3.467 3.467 9.109 0 12.576s-9.109 3.468-12.576 0l-9.233-9.232c-1.634 6.083.064 12.614 4.602 17.153 4.848 4.848 12.16 6.464 18.628 4.118l.886-.322z"
|
|
15
|
+
/>
|
|
16
|
+
</g>
|
|
17
|
+
</svg>
|
|
18
|
+
|
|
19
|
+
<svg
|
|
20
|
+
v-else
|
|
21
|
+
class="h-4 w-4"
|
|
22
|
+
viewBox="0 0 24 24"
|
|
23
|
+
fill="none"
|
|
24
|
+
stroke="currentColor"
|
|
25
|
+
stroke-width="2"
|
|
26
|
+
stroke-linecap="round"
|
|
27
|
+
stroke-linejoin="round"
|
|
28
|
+
aria-hidden="true"
|
|
29
|
+
>
|
|
30
|
+
<path
|
|
31
|
+
v-if="name === 'move-up'"
|
|
32
|
+
d="m18 15-6-6-6 6"
|
|
33
|
+
/>
|
|
34
|
+
<path
|
|
35
|
+
v-else-if="name === 'move-down'"
|
|
36
|
+
d="m6 9 6 6 6-6"
|
|
37
|
+
/>
|
|
38
|
+
<template v-else>
|
|
39
|
+
<path d="M3 6h18" />
|
|
40
|
+
<path d="M8 6V4h8v2" />
|
|
41
|
+
<path d="M19 6l-1 14H6L5 6" />
|
|
42
|
+
<path d="M10 11v5" />
|
|
43
|
+
<path d="M14 11v5" />
|
|
44
|
+
</template>
|
|
45
|
+
</svg>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<script setup lang="ts">
|
|
49
|
+
defineProps<{
|
|
50
|
+
name: 'edit' | 'move-up' | 'move-down' | 'remove'
|
|
51
|
+
}>()
|
|
52
|
+
</script>
|
|
@@ -10,91 +10,36 @@
|
|
|
10
10
|
v-if="isAdmin"
|
|
11
11
|
class="absolute right-2 top-2 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100"
|
|
12
12
|
>
|
|
13
|
-
<
|
|
14
|
-
type="button"
|
|
15
|
-
class="flex h-8 w-8 items-center justify-center rounded-lg border border-lightListViewButtonBorder bg-lightListViewButtonBackground text-lightListViewButtonText shadow-sm hover:bg-lightListViewButtonBackgroundHover hover:text-lightListViewButtonTextHover dark:border-darkListViewButtonBorder dark:bg-darkListViewButtonBackground dark:text-darkListViewButtonText dark:hover:bg-darkListViewButtonBackgroundHover dark:hover:text-darkListViewButtonTextHover"
|
|
13
|
+
<DashboardToolbarButton
|
|
16
14
|
title="Edit JSON"
|
|
17
15
|
@click="emit('edit')"
|
|
18
16
|
>
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
viewBox="0 0 24 24"
|
|
22
|
-
fill="none"
|
|
23
|
-
stroke="currentColor"
|
|
24
|
-
stroke-width="1.8"
|
|
25
|
-
stroke-linecap="round"
|
|
26
|
-
stroke-linejoin="round"
|
|
27
|
-
aria-hidden="true"
|
|
28
|
-
>
|
|
29
|
-
<path d="M15.5 7.5a3 3 0 1 1 1 2.2l-6.8 6.8H7.5v2.2H5.3v2.2H2.8v-2.5l7.5-7.5a5.5 5.5 0 1 1 5.2 1.6" />
|
|
30
|
-
</svg>
|
|
31
|
-
</button>
|
|
17
|
+
<DashboardToolbarIcon name="edit" />
|
|
18
|
+
</DashboardToolbarButton>
|
|
32
19
|
|
|
33
|
-
<
|
|
34
|
-
type="button"
|
|
35
|
-
class="flex h-8 w-8 items-center justify-center rounded-lg border border-lightListViewButtonBorder bg-lightListViewButtonBackground text-lightListViewButtonText shadow-sm hover:bg-lightListViewButtonBackgroundHover hover:text-lightListViewButtonTextHover disabled:opacity-45 dark:border-darkListViewButtonBorder dark:bg-darkListViewButtonBackground dark:text-darkListViewButtonText dark:hover:bg-darkListViewButtonBackgroundHover dark:hover:text-darkListViewButtonTextHover"
|
|
20
|
+
<DashboardToolbarButton
|
|
36
21
|
title="Move up"
|
|
37
22
|
:disabled="!canMoveUp"
|
|
38
23
|
@click="emit('move-up')"
|
|
39
24
|
>
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
viewBox="0 0 24 24"
|
|
43
|
-
fill="none"
|
|
44
|
-
stroke="currentColor"
|
|
45
|
-
stroke-width="2"
|
|
46
|
-
stroke-linecap="round"
|
|
47
|
-
stroke-linejoin="round"
|
|
48
|
-
aria-hidden="true"
|
|
49
|
-
>
|
|
50
|
-
<path d="m18 15-6-6-6 6" />
|
|
51
|
-
</svg>
|
|
52
|
-
</button>
|
|
25
|
+
<DashboardToolbarIcon name="move-up" />
|
|
26
|
+
</DashboardToolbarButton>
|
|
53
27
|
|
|
54
|
-
<
|
|
55
|
-
type="button"
|
|
56
|
-
class="flex h-8 w-8 items-center justify-center rounded-lg border border-lightListViewButtonBorder bg-lightListViewButtonBackground text-lightListViewButtonText shadow-sm hover:bg-lightListViewButtonBackgroundHover hover:text-lightListViewButtonTextHover disabled:opacity-45 dark:border-darkListViewButtonBorder dark:bg-darkListViewButtonBackground dark:text-darkListViewButtonText dark:hover:bg-darkListViewButtonBackgroundHover dark:hover:text-darkListViewButtonTextHover"
|
|
28
|
+
<DashboardToolbarButton
|
|
57
29
|
title="Move down"
|
|
58
30
|
:disabled="!canMoveDown"
|
|
59
31
|
@click="emit('move-down')"
|
|
60
32
|
>
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
viewBox="0 0 24 24"
|
|
64
|
-
fill="none"
|
|
65
|
-
stroke="currentColor"
|
|
66
|
-
stroke-width="2"
|
|
67
|
-
stroke-linecap="round"
|
|
68
|
-
stroke-linejoin="round"
|
|
69
|
-
aria-hidden="true"
|
|
70
|
-
>
|
|
71
|
-
<path d="m6 9 6 6 6-6" />
|
|
72
|
-
</svg>
|
|
73
|
-
</button>
|
|
33
|
+
<DashboardToolbarIcon name="move-down" />
|
|
34
|
+
</DashboardToolbarButton>
|
|
74
35
|
|
|
75
|
-
<
|
|
76
|
-
type="button"
|
|
77
|
-
class="flex h-8 w-8 items-center justify-center rounded-lg border border-lightInputErrorColor/30 bg-lightSecondary text-lightInputErrorColor shadow-sm hover:bg-lightListViewButtonBackgroundHover dark:bg-darkSecondary dark:hover:bg-darkListViewButtonBackgroundHover"
|
|
36
|
+
<DashboardToolbarButton
|
|
78
37
|
title="Remove"
|
|
38
|
+
variant="danger"
|
|
79
39
|
@click="emit('remove')"
|
|
80
40
|
>
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
viewBox="0 0 24 24"
|
|
84
|
-
fill="none"
|
|
85
|
-
stroke="currentColor"
|
|
86
|
-
stroke-width="2"
|
|
87
|
-
stroke-linecap="round"
|
|
88
|
-
stroke-linejoin="round"
|
|
89
|
-
aria-hidden="true"
|
|
90
|
-
>
|
|
91
|
-
<path d="M3 6h18" />
|
|
92
|
-
<path d="M8 6V4h8v2" />
|
|
93
|
-
<path d="M19 6l-1 14H6L5 6" />
|
|
94
|
-
<path d="M10 11v5" />
|
|
95
|
-
<path d="M14 11v5" />
|
|
96
|
-
</svg>
|
|
97
|
-
</button>
|
|
41
|
+
<DashboardToolbarIcon name="remove" />
|
|
42
|
+
</DashboardToolbarButton>
|
|
98
43
|
</div>
|
|
99
44
|
</div>
|
|
100
45
|
</template>
|
|
@@ -105,6 +50,8 @@
|
|
|
105
50
|
import { computed } from 'vue'
|
|
106
51
|
import type { CSSProperties } from 'vue'
|
|
107
52
|
import type { WidgetLayout } from '../model/dashboard.types.js'
|
|
53
|
+
import DashboardToolbarButton from './DashboardToolbarButton.vue'
|
|
54
|
+
import DashboardToolbarIcon from './DashboardToolbarIcon.vue'
|
|
108
55
|
|
|
109
56
|
const DEFAULT_WIDGET_HEIGHT = 500
|
|
110
57
|
|
|
@@ -253,6 +253,50 @@ query:
|
|
|
253
253
|
direction: asc
|
|
254
254
|
```
|
|
255
255
|
|
|
256
|
+
Note: use `stacked_bar` with a normal single-resource grouped query when you need a dynamic series dimension such as `series.field: purpose`. For the same numeric buckets across multiple resources, use `query.source: steps` with `query.bucket`; set `chart.x.field: label`, `chart.y.field` to the aggregate alias, and `chart.series.field: name`.
|
|
257
|
+
|
|
258
|
+
Example bucketed multi-resource stacked bar:
|
|
259
|
+
|
|
260
|
+
```yaml
|
|
261
|
+
target: chart
|
|
262
|
+
label: Cars by price range and database
|
|
263
|
+
size: wide
|
|
264
|
+
chart:
|
|
265
|
+
type: stacked_bar
|
|
266
|
+
x:
|
|
267
|
+
field: label
|
|
268
|
+
label: Price range
|
|
269
|
+
y:
|
|
270
|
+
field: count
|
|
271
|
+
label: Cars
|
|
272
|
+
series:
|
|
273
|
+
field: name
|
|
274
|
+
label: Database
|
|
275
|
+
query:
|
|
276
|
+
source: steps
|
|
277
|
+
bucket:
|
|
278
|
+
field: price
|
|
279
|
+
buckets:
|
|
280
|
+
- label: Budget
|
|
281
|
+
max: 3500
|
|
282
|
+
- label: Mid-range
|
|
283
|
+
min: 3500
|
|
284
|
+
max: 7000
|
|
285
|
+
- label: Premium
|
|
286
|
+
min: 7000
|
|
287
|
+
steps:
|
|
288
|
+
- name: SQLite
|
|
289
|
+
resource: cars_sl
|
|
290
|
+
select:
|
|
291
|
+
- agg: count
|
|
292
|
+
as: count
|
|
293
|
+
- name: MySQL
|
|
294
|
+
resource: cars_mysql
|
|
295
|
+
select:
|
|
296
|
+
- agg: count
|
|
297
|
+
as: count
|
|
298
|
+
```
|
|
299
|
+
|
|
256
300
|
Example `dashboard_configure_pie_chart_widget` config:
|
|
257
301
|
|
|
258
302
|
```yaml
|
|
@@ -291,26 +335,24 @@ chart:
|
|
|
291
335
|
field: total_tokens
|
|
292
336
|
label: Tokens
|
|
293
337
|
y:
|
|
294
|
-
field:
|
|
338
|
+
field: count
|
|
295
339
|
label: Requests
|
|
340
|
+
buckets:
|
|
341
|
+
- label: Small
|
|
342
|
+
max: 1000
|
|
343
|
+
- label: Medium
|
|
344
|
+
min: 1000
|
|
345
|
+
max: 10000
|
|
346
|
+
- label: Large
|
|
347
|
+
min: 10000
|
|
296
348
|
query:
|
|
297
349
|
resource: llm_usage
|
|
298
350
|
select:
|
|
299
351
|
- field: total_tokens
|
|
300
|
-
- agg: count
|
|
301
|
-
as: requests
|
|
302
|
-
bucket:
|
|
303
|
-
field: total_tokens
|
|
304
|
-
buckets:
|
|
305
|
-
- label: Small
|
|
306
|
-
max: 1000
|
|
307
|
-
- label: Medium
|
|
308
|
-
min: 1000
|
|
309
|
-
max: 10000
|
|
310
|
-
- label: Large
|
|
311
|
-
min: 10000
|
|
312
352
|
```
|
|
313
353
|
|
|
354
|
+
Note: for the current dashboard runtime, histogram buckets are computed on the frontend from raw rows using `chart.buckets`. Do not rely on `query.bucket` for histogram widgets, do not use `query.source: steps`, and do not aggregate the source rows first. Histogram widgets should use a single-resource plain query with raw numeric rows, for example `select: - field: total_tokens`; the histogram component will derive the per-bucket `count` values itself.
|
|
355
|
+
|
|
314
356
|
Example `dashboard_configure_funnel_chart_widget` config:
|
|
315
357
|
|
|
316
358
|
```yaml
|
|
@@ -476,6 +518,14 @@ query:
|
|
|
476
518
|
|
|
477
519
|
Do not use bare query.steps without source: steps.
|
|
478
520
|
Do not use metric. Use select even when a step has only one aggregate.
|
|
521
|
+
Each `steps[]` item supports only:
|
|
522
|
+
- name
|
|
523
|
+
- resource
|
|
524
|
+
- select with aggregate items only, for example `agg: count`, `agg: sum`, `agg: avg`
|
|
525
|
+
- optional filters
|
|
526
|
+
Do not put `field` selects, `calc` selects, `group_by`, `order_by`, `limit`, `offset`, or `bucket` inside a step.
|
|
527
|
+
Without `query.bucket`, `query.source: steps` produces one output row per step, with built-in `name` and `resource` fields plus the aggregate aliases from that step.
|
|
528
|
+
For per-bucket comparisons across multiple resources, put `bucket` at the query level, not inside a step. Use stacked_bar with `chart.x.field: label`, `chart.series.field: name`, and `chart.y.field` set to the aggregate alias such as `count`.
|
|
479
529
|
All filters, including aggregate select item filters, must use filter expression shape.
|
|
480
530
|
Use `filters: { field: model, eq: gpt-5.4 }`, not shorthand maps like `filters: { model: gpt-5.4 }`.
|
|
481
531
|
When grouping by a derived date alias, repeat the source field object in `group_by`.
|