@adminforth/dashboard 1.0.0 → 1.2.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 +99 -54
- package/custom/api/dashboardApi.ts +9 -0
- package/custom/model/dashboard.types.ts +353 -2
- package/custom/queries/useWidgetData.ts +8 -4
- package/custom/runtime/DashboardRuntime.vue +2 -1
- package/custom/runtime/WidgetRenderer.vue +2 -1
- package/custom/runtime/WidgetShell.vue +8 -4
- package/custom/skills/adminforth-dashboard/SKILL.md +4 -4
- package/custom/widgets/chart/ChartWidget.vue +45 -12
- package/custom/widgets/chart/chart.types.ts +83 -0
- package/custom/widgets/chart/chart.utils.ts +2 -2
- package/custom/widgets/gauge-card/GaugeCardWidget.vue +63 -12
- package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
- package/custom/widgets/pivot-table/PivotTableWidget.vue +32 -12
- package/custom/widgets/table/TableWidget.vue +155 -30
- package/dist/custom/api/dashboardApi.d.ts +7 -1
- package/dist/custom/api/dashboardApi.js +4 -6
- package/dist/custom/api/dashboardApi.ts +9 -0
- package/dist/custom/model/dashboard.types.d.ts +70 -1
- package/dist/custom/model/dashboard.types.js +173 -1
- package/dist/custom/model/dashboard.types.ts +353 -2
- package/dist/custom/queries/useDashboardConfig.d.ts +42 -2
- package/dist/custom/queries/useWidgetData.d.ts +44 -3
- package/dist/custom/queries/useWidgetData.js +3 -3
- package/dist/custom/queries/useWidgetData.ts +8 -4
- package/dist/custom/runtime/DashboardRuntime.vue +2 -1
- package/dist/custom/runtime/WidgetRenderer.vue +2 -1
- package/dist/custom/runtime/WidgetShell.vue +8 -4
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +4 -4
- package/dist/custom/widgets/chart/ChartWidget.vue +45 -12
- package/dist/custom/widgets/chart/chart.types.d.ts +15 -0
- package/dist/custom/widgets/chart/chart.types.js +46 -0
- package/dist/custom/widgets/chart/chart.types.ts +83 -0
- package/dist/custom/widgets/chart/chart.utils.d.ts +1 -1
- package/dist/custom/widgets/chart/chart.utils.js +2 -2
- package/dist/custom/widgets/chart/chart.utils.ts +2 -2
- package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +63 -12
- package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
- package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +32 -12
- package/dist/custom/widgets/table/TableWidget.vue +155 -30
- package/dist/endpoint/widgets.d.ts +6 -1
- package/dist/endpoint/widgets.js +41 -6
- package/dist/schema/api.d.ts +874 -444
- package/dist/schema/api.js +11 -2
- package/dist/schema/widget.d.ts +538 -132
- package/dist/schema/widget.js +138 -14
- package/dist/services/widgetConfigValidator.js +26 -40
- package/dist/services/widgetDataService.d.ts +7 -14
- package/dist/services/widgetDataService.js +115 -11
- package/endpoint/widgets.ts +56 -6
- package/package.json +1 -1
- package/schema/api.ts +11 -1
- package/schema/widget.ts +145 -15
- package/services/widgetConfigValidator.ts +36 -44
- package/services/widgetDataService.ts +175 -28
package/README.md
CHANGED
|
@@ -2,58 +2,103 @@
|
|
|
2
2
|
|
|
3
3
|
Dashboard plugin for AdminForth.
|
|
4
4
|
|
|
5
|
+
It adds configurable dashboard pages backed by an AdminForth resource. Dashboard records define groups and widgets, the plugin renders them under `/dashboard/:slug`, contributes a **Dashboards** sidebar group, and exposes endpoints for editing groups and widgets from the AdminForth UI.
|
|
6
|
+
|
|
7
|
+
Full setup guide: https://adminforth.dev/docs/tutorial/Plugins/dashboard/
|
|
8
|
+
|
|
9
|
+
## Dashboard Config Shape
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
type DashboardConfig = {
|
|
13
|
+
version: number
|
|
14
|
+
groups: {
|
|
15
|
+
id: string
|
|
16
|
+
label: string
|
|
17
|
+
order: number
|
|
18
|
+
}[]
|
|
19
|
+
widgets: DashboardWidgetConfig[]
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Each widget has common fields:
|
|
24
|
+
|
|
25
|
+
| Field | Description |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `id` | Persisted widget id. |
|
|
28
|
+
| `group_id` | Group where the widget is rendered. |
|
|
29
|
+
| `label` | Optional widget title. |
|
|
30
|
+
| `target` | Widget type: `table`, `chart`, `kpi_card`, `pivot_table`, or `gauge_card`. |
|
|
31
|
+
| `order` | Widget order inside its group. |
|
|
32
|
+
| `size` | Preset width: `small`, `medium`, `large`, `wide`, or `full`. |
|
|
33
|
+
| `width`, `height`, `min_width`, `max_width` | Optional explicit layout constraints. |
|
|
34
|
+
| `data_source` | Resource or aggregate data source definition. |
|
|
35
|
+
|
|
36
|
+
## Widget Support Matrix
|
|
37
|
+
|
|
38
|
+
| Widget target | Config field | Main settings | Data usage |
|
|
39
|
+
| --- | --- | --- | --- |
|
|
40
|
+
| `table` | `table` | `pagination`, `page_size` | Uses `data_source.type = 'resource'` to display resource rows with backend pagination unless `pagination` is `false`. |
|
|
41
|
+
| `chart` | `chart` | `type`, `x_field`, `y_field`, `label_field`, `value_field`, `bucket_field`, `buckets`, `series`, `series_name`, `color`, `colors` | Uses `data_source.type = 'aggregate'` with `group_by`. |
|
|
42
|
+
| `kpi_card` | `kpi_card` | `value_field`, `label_field`, `prefix`, `suffix` | Reads aggregate values or the first returned row from `data_source`. |
|
|
43
|
+
| `gauge_card` | `gauge_card` | `value_field`, `min`, `max`, `min_field`, `max_field`, `suffix`, `color` | Reads aggregate values or the first returned row from `data_source` and renders progress between static or field-driven bounds. |
|
|
44
|
+
| `pivot_table` | `pivot_table` | `row_field`, `column_field`, `value_field`, `aggregation` | Uses grouped aggregate rows from `data_source.type = 'aggregate'`. `aggregation` supports `count` and `sum`. |
|
|
45
|
+
|
|
46
|
+
Chart widget types:
|
|
47
|
+
|
|
48
|
+
| Chart type | Notes |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| `line` | Uses `x_field` and `y_field`; optional `series_name` and `color`. |
|
|
51
|
+
| `pie` | Uses `label_field` and optional `value_field`; without `value_field`, rows are counted by label. |
|
|
52
|
+
| `bar` | Uses `label_field` and `value_field`, or `bucket_field` with `buckets`. |
|
|
53
|
+
| `stacked_bar` | Uses `x_field` and `series`; if `series` is omitted, non-x columns become series. |
|
|
54
|
+
| `funnel` | Uses `label_field`, `value_field`, and optional `colors`. |
|
|
55
|
+
| `histogram` | Uses the same bucket settings as `bar`. |
|
|
56
|
+
|
|
57
|
+
## Data Source Shape
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
type WidgetDataSource =
|
|
61
|
+
| {
|
|
62
|
+
type: 'resource'
|
|
63
|
+
resource_id: string
|
|
64
|
+
columns?: string[]
|
|
65
|
+
filters?: unknown
|
|
66
|
+
sort?: unknown
|
|
67
|
+
}
|
|
68
|
+
| {
|
|
69
|
+
type: 'aggregate'
|
|
70
|
+
resource_id: string
|
|
71
|
+
aggregations: Record<string, {
|
|
72
|
+
operation: 'sum' | 'count' | 'avg' | 'min' | 'max' | 'median'
|
|
73
|
+
field?: string
|
|
74
|
+
}>
|
|
75
|
+
group_by?:
|
|
76
|
+
| { type: 'field'; field: string }
|
|
77
|
+
| {
|
|
78
|
+
type: 'date_trunc'
|
|
79
|
+
field: string
|
|
80
|
+
truncation: 'day' | 'week' | 'month' | 'year'
|
|
81
|
+
timezone?: string
|
|
82
|
+
}
|
|
83
|
+
filters?: unknown
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`resource_id` is an AdminForth `resourceId`. The data source is executed through AdminForth resources, so widgets use the same resource contracts as the rest of the application.
|
|
88
|
+
|
|
89
|
+
## Runtime Structure
|
|
90
|
+
|
|
91
|
+
```text
|
|
5
92
|
DashboardPage.vue
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
runtime/
|
|
20
|
-
DashboardPage.vue
|
|
21
|
-
DashboardRuntime.vue
|
|
22
|
-
DashboardGroup.vue
|
|
23
|
-
WidgetShell.vue
|
|
24
|
-
WidgetRenderer.vue
|
|
25
|
-
|
|
26
|
-
widgets/
|
|
27
|
-
registry.ts
|
|
28
|
-
|
|
29
|
-
table/
|
|
30
|
-
TableWidget.vue
|
|
31
|
-
TableWidgetEditor.vue
|
|
32
|
-
table.adapter.ts
|
|
33
|
-
|
|
34
|
-
chart/
|
|
35
|
-
ChartWidget.vue
|
|
36
|
-
ChartWidgetEditor.vue
|
|
37
|
-
chart.adapter.ts
|
|
38
|
-
charts/
|
|
39
|
-
PieChart.vue
|
|
40
|
-
LineChart.vue
|
|
41
|
-
BarChart.vue
|
|
42
|
-
StackedBarChart.vue
|
|
43
|
-
FunnelChart.vue
|
|
44
|
-
HistogramChart.vue
|
|
45
|
-
|
|
46
|
-
kpi-card/
|
|
47
|
-
KpiCardWidget.vue
|
|
48
|
-
KpiCardWidgetEditor.vue
|
|
49
|
-
kpi.adapter.ts
|
|
50
|
-
|
|
51
|
-
pivot-table/
|
|
52
|
-
PivotTableWidget.vue
|
|
53
|
-
PivotTableWidgetEditor.vue
|
|
54
|
-
pivot.adapter.ts
|
|
55
|
-
|
|
56
|
-
gauge-card/
|
|
57
|
-
GaugeCardWidget.vue
|
|
58
|
-
GaugeCardWidgetEditor.vue
|
|
59
|
-
gauge.adapter.ts
|
|
93
|
+
└── DashboardRuntime.vue
|
|
94
|
+
└── DashboardGroup.vue
|
|
95
|
+
└── WidgetShell.vue
|
|
96
|
+
└── WidgetRenderer.vue
|
|
97
|
+
├── TableWidget.vue
|
|
98
|
+
├── ChartWidget.vue
|
|
99
|
+
├── KpiCardWidget.vue
|
|
100
|
+
├── PivotTableWidget.vue
|
|
101
|
+
└── GaugeCardWidget.vue
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`DashboardPage.vue` loads a dashboard by slug, `DashboardRuntime.vue` renders ordered groups, `WidgetShell.vue` provides the widget frame and editor actions, and `WidgetRenderer.vue` selects the widget component by `target`.
|
|
@@ -24,6 +24,13 @@ export type DashboardWidgetDataResponse = {
|
|
|
24
24
|
data: unknown
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
export type DashboardWidgetDataRequest = {
|
|
28
|
+
pagination?: {
|
|
29
|
+
page: number
|
|
30
|
+
pageSize: number
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
27
34
|
export class DashboardApiError extends Error {
|
|
28
35
|
validationErrors: DashboardWidgetConfigValidationError[]
|
|
29
36
|
|
|
@@ -204,10 +211,12 @@ export const dashboardApi = {
|
|
|
204
211
|
async getDashboardWidgetData(
|
|
205
212
|
slug: string,
|
|
206
213
|
widgetId: string,
|
|
214
|
+
request: DashboardWidgetDataRequest = {},
|
|
207
215
|
): Promise<DashboardWidgetDataResponse> {
|
|
208
216
|
return callDashboardWidgetDataApi('/adminapi/v1/dashboard/get_dashboard_widget_data', {
|
|
209
217
|
slug,
|
|
210
218
|
widgetId,
|
|
219
|
+
...request,
|
|
211
220
|
})
|
|
212
221
|
},
|
|
213
222
|
}
|
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
import type { ChartWidgetConfig } from '../widgets/chart/chart.types.js'
|
|
2
2
|
|
|
3
|
+
export type AggregationOperation = 'sum' | 'count' | 'avg' | 'min' | 'max' | 'median'
|
|
4
|
+
|
|
5
|
+
export type AggregationRule = {
|
|
6
|
+
operation: AggregationOperation
|
|
7
|
+
field?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type GroupByRule =
|
|
11
|
+
| {
|
|
12
|
+
type: 'field'
|
|
13
|
+
field: string
|
|
14
|
+
}
|
|
15
|
+
| {
|
|
16
|
+
type: 'date_trunc'
|
|
17
|
+
field: string
|
|
18
|
+
truncation: 'day' | 'week' | 'month' | 'year'
|
|
19
|
+
timezone?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ResourceWidgetDataSource = {
|
|
23
|
+
type: 'resource'
|
|
24
|
+
resourceId: string
|
|
25
|
+
columns?: string[]
|
|
26
|
+
sort?: unknown
|
|
27
|
+
filters?: unknown
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type AggregateWidgetDataSource = {
|
|
31
|
+
type: 'aggregate'
|
|
32
|
+
resourceId: string
|
|
33
|
+
aggregations: Record<string, AggregationRule>
|
|
34
|
+
groupBy?: GroupByRule
|
|
35
|
+
filters?: unknown
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type WidgetDataSource = ResourceWidgetDataSource | AggregateWidgetDataSource
|
|
39
|
+
|
|
3
40
|
export type DashboardConfig = {
|
|
4
41
|
version: number
|
|
5
42
|
groups: DashboardGroupConfig[]
|
|
@@ -45,17 +82,57 @@ export type DashboardWidgetConfig = {
|
|
|
45
82
|
maxWidth?: number | null
|
|
46
83
|
order: number
|
|
47
84
|
target: DashboardWidgetTarget
|
|
85
|
+
dataSource?: WidgetDataSource
|
|
48
86
|
chart?: ChartWidgetConfig
|
|
49
87
|
table?: unknown
|
|
50
88
|
kpi_card?: unknown
|
|
51
89
|
pivot_table?: unknown
|
|
52
90
|
gauge_card?: unknown
|
|
53
|
-
query?: unknown
|
|
54
91
|
}
|
|
55
92
|
|
|
56
93
|
export type DashboardWidgetTableData = {
|
|
94
|
+
kind?: 'table'
|
|
95
|
+
columns: string[]
|
|
96
|
+
rows: Record<string, unknown>[]
|
|
97
|
+
pagination?: {
|
|
98
|
+
page: number
|
|
99
|
+
pageSize: number
|
|
100
|
+
total: number
|
|
101
|
+
totalPages: number
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type DashboardWidgetAggregateData = {
|
|
106
|
+
kind: 'aggregate'
|
|
57
107
|
columns: string[]
|
|
58
108
|
rows: Record<string, unknown>[]
|
|
109
|
+
values?: Record<string, unknown>
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type DashboardWidgetData = DashboardWidgetTableData | DashboardWidgetAggregateData
|
|
113
|
+
|
|
114
|
+
export type NormalizedKpiCardWidgetConfig = {
|
|
115
|
+
valueField?: string
|
|
116
|
+
labelField?: string
|
|
117
|
+
prefix?: string
|
|
118
|
+
suffix?: string
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type NormalizedGaugeCardWidgetConfig = {
|
|
122
|
+
valueField?: string
|
|
123
|
+
min?: number | string
|
|
124
|
+
max?: number | string
|
|
125
|
+
minField?: string
|
|
126
|
+
maxField?: string
|
|
127
|
+
suffix?: string
|
|
128
|
+
color?: string
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export type NormalizedPivotTableWidgetConfig = {
|
|
132
|
+
rowField?: string
|
|
133
|
+
columnField?: string
|
|
134
|
+
valueField?: string
|
|
135
|
+
aggregation?: 'count' | 'sum'
|
|
59
136
|
}
|
|
60
137
|
|
|
61
138
|
export function normalizeDashboardConfig(config: unknown): DashboardConfig {
|
|
@@ -64,8 +141,282 @@ export function normalizeDashboardConfig(config: unknown): DashboardConfig {
|
|
|
64
141
|
return {
|
|
65
142
|
version: typeof value.version === 'number' ? value.version : 1,
|
|
66
143
|
groups: Array.isArray(value.groups) ? (value.groups as DashboardGroupConfig[]) : [],
|
|
67
|
-
widgets: Array.isArray(value.widgets)
|
|
144
|
+
widgets: Array.isArray(value.widgets)
|
|
145
|
+
? value.widgets.map((widget) => normalizeDashboardWidgetConfig(widget) as DashboardWidgetConfig)
|
|
146
|
+
: [],
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function normalizeDashboardWidgetConfig(config: unknown) {
|
|
151
|
+
if (!isRecord(config)) {
|
|
152
|
+
return config
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const normalized: Record<string, unknown> = { ...config }
|
|
156
|
+
normalizeWidgetLayoutConfig(normalized)
|
|
157
|
+
|
|
158
|
+
if (normalized.table !== undefined) {
|
|
159
|
+
normalized.table = normalizeTableConfig(normalized.table)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (normalized.data_source !== undefined) {
|
|
163
|
+
normalized.dataSource = normalizeWidgetDataSource(normalized.data_source)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const target = normalizeDashboardWidgetTarget(normalized.target)
|
|
167
|
+
|
|
168
|
+
if (target !== undefined) {
|
|
169
|
+
normalized.target = target
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return normalized
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function serializeDashboardWidgetConfigForEditor(widget: DashboardWidgetConfig) {
|
|
176
|
+
const serialized: Record<string, unknown> = { ...widget }
|
|
177
|
+
|
|
178
|
+
if (Object.prototype.hasOwnProperty.call(serialized, 'minWidth')) {
|
|
179
|
+
serialized.min_width = widget.minWidth
|
|
180
|
+
delete serialized.minWidth
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (Object.prototype.hasOwnProperty.call(serialized, 'maxWidth')) {
|
|
184
|
+
serialized.max_width = widget.maxWidth
|
|
185
|
+
delete serialized.maxWidth
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (widget.table !== undefined) {
|
|
189
|
+
serialized.table = serializeTableConfigForEditor(widget.table)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (widget.dataSource !== undefined) {
|
|
193
|
+
serialized.data_source = serializeWidgetDataSourceForEditor(widget.dataSource)
|
|
194
|
+
delete serialized.dataSource
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return serialized
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function normalizeKpiCardWidgetConfig(value: unknown): NormalizedKpiCardWidgetConfig | undefined {
|
|
201
|
+
const config = asWidgetConfigRecord(value)
|
|
202
|
+
|
|
203
|
+
if (!config) {
|
|
204
|
+
return undefined
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const valueField = getStringField(config, 'value_field')
|
|
208
|
+
const labelField = getStringField(config, 'label_field')
|
|
209
|
+
const prefix = getStringField(config, 'prefix')
|
|
210
|
+
const suffix = getStringField(config, 'suffix')
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
...(valueField !== undefined ? { valueField } : {}),
|
|
214
|
+
...(labelField !== undefined ? { labelField } : {}),
|
|
215
|
+
...(prefix !== undefined ? { prefix } : {}),
|
|
216
|
+
...(suffix !== undefined ? { suffix } : {}),
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function normalizeGaugeCardWidgetConfig(value: unknown): NormalizedGaugeCardWidgetConfig | undefined {
|
|
221
|
+
const config = asWidgetConfigRecord(value)
|
|
222
|
+
|
|
223
|
+
if (!config) {
|
|
224
|
+
return undefined
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const valueField = getStringField(config, 'value_field')
|
|
228
|
+
const minField = getStringField(config, 'min_field')
|
|
229
|
+
const maxField = getStringField(config, 'max_field')
|
|
230
|
+
const suffix = getStringField(config, 'suffix')
|
|
231
|
+
const color = getStringField(config, 'color')
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
...(valueField !== undefined ? { valueField } : {}),
|
|
235
|
+
...(config.min !== undefined ? { min: config.min as number | string } : {}),
|
|
236
|
+
...(config.max !== undefined ? { max: config.max as number | string } : {}),
|
|
237
|
+
...(minField !== undefined ? { minField } : {}),
|
|
238
|
+
...(maxField !== undefined ? { maxField } : {}),
|
|
239
|
+
...(suffix !== undefined ? { suffix } : {}),
|
|
240
|
+
...(color !== undefined ? { color } : {}),
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function normalizePivotTableWidgetConfig(value: unknown): NormalizedPivotTableWidgetConfig | undefined {
|
|
245
|
+
const config = asWidgetConfigRecord(value)
|
|
246
|
+
|
|
247
|
+
if (!config) {
|
|
248
|
+
return undefined
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const rowField = getStringField(config, 'row_field')
|
|
252
|
+
const columnField = getStringField(config, 'column_field')
|
|
253
|
+
const valueField = getStringField(config, 'value_field')
|
|
254
|
+
const aggregation = config.aggregation === 'count' || config.aggregation === 'sum'
|
|
255
|
+
? config.aggregation
|
|
256
|
+
: undefined
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
...(rowField !== undefined ? { rowField } : {}),
|
|
260
|
+
...(columnField !== undefined ? { columnField } : {}),
|
|
261
|
+
...(valueField !== undefined ? { valueField } : {}),
|
|
262
|
+
...(aggregation !== undefined ? { aggregation } : {}),
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function normalizeDashboardWidgetTarget(value: unknown): DashboardWidgetTarget | undefined {
|
|
267
|
+
switch (value) {
|
|
268
|
+
case 'empty':
|
|
269
|
+
case 'table':
|
|
270
|
+
case 'chart':
|
|
271
|
+
case 'kpi_card':
|
|
272
|
+
case 'pivot_table':
|
|
273
|
+
case 'gauge_card':
|
|
274
|
+
return value
|
|
275
|
+
default:
|
|
276
|
+
return undefined
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function normalizeWidgetLayoutConfig(value: Record<string, unknown>) {
|
|
281
|
+
if (value.min_width !== undefined) {
|
|
282
|
+
value.minWidth = value.min_width
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (value.max_width !== undefined) {
|
|
286
|
+
value.maxWidth = value.max_width
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function normalizeTableConfig(value: unknown) {
|
|
291
|
+
if (!isRecord(value)) {
|
|
292
|
+
return value
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const normalized = { ...value }
|
|
296
|
+
|
|
297
|
+
if (normalized.page_size !== undefined) {
|
|
298
|
+
normalized.pageSize = normalized.page_size
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return normalized
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function normalizeWidgetDataSource(value: unknown) {
|
|
305
|
+
if (!isRecord(value) || typeof value.type !== 'string') {
|
|
306
|
+
return value
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const resourceId = typeof value.resource_id === 'string'
|
|
310
|
+
? value.resource_id
|
|
311
|
+
: undefined
|
|
312
|
+
|
|
313
|
+
if (value.type === 'resource') {
|
|
314
|
+
return {
|
|
315
|
+
type: 'resource',
|
|
316
|
+
...(resourceId !== undefined ? { resourceId } : {}),
|
|
317
|
+
...(value.columns !== undefined ? { columns: value.columns } : {}),
|
|
318
|
+
...(value.filters !== undefined ? { filters: value.filters } : {}),
|
|
319
|
+
...(value.sort !== undefined ? { sort: value.sort } : {}),
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (value.type === 'aggregate') {
|
|
324
|
+
const groupBy = normalizeGroupByRule(value.group_by)
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
type: 'aggregate',
|
|
328
|
+
...(resourceId !== undefined ? { resourceId } : {}),
|
|
329
|
+
...(value.aggregations !== undefined ? { aggregations: value.aggregations } : {}),
|
|
330
|
+
...(groupBy !== undefined ? { groupBy } : {}),
|
|
331
|
+
...(value.filters !== undefined ? { filters: value.filters } : {}),
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return value
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function normalizeGroupByRule(value: unknown) {
|
|
339
|
+
if (!isRecord(value) || typeof value.type !== 'string') {
|
|
340
|
+
return value
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (value.type === 'field') {
|
|
344
|
+
return {
|
|
345
|
+
type: 'field',
|
|
346
|
+
...(value.field !== undefined ? { field: value.field } : {}),
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (value.type === 'date_trunc') {
|
|
351
|
+
return {
|
|
352
|
+
type: 'date_trunc',
|
|
353
|
+
...(value.field !== undefined ? { field: value.field } : {}),
|
|
354
|
+
...(value.truncation !== undefined ? { truncation: value.truncation } : {}),
|
|
355
|
+
...(value.timezone !== undefined ? { timezone: value.timezone } : {}),
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return value
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function serializeTableConfigForEditor(value: unknown) {
|
|
363
|
+
if (!isRecord(value)) {
|
|
364
|
+
return value
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const serialized = { ...value }
|
|
368
|
+
|
|
369
|
+
if (Object.prototype.hasOwnProperty.call(serialized, 'pageSize')) {
|
|
370
|
+
serialized.page_size = serialized.pageSize
|
|
371
|
+
delete serialized.pageSize
|
|
68
372
|
}
|
|
373
|
+
|
|
374
|
+
return serialized
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function serializeWidgetDataSourceForEditor(value: WidgetDataSource) {
|
|
378
|
+
if (value.type === 'resource') {
|
|
379
|
+
return {
|
|
380
|
+
type: 'resource',
|
|
381
|
+
resource_id: value.resourceId,
|
|
382
|
+
...(value.columns !== undefined ? { columns: value.columns } : {}),
|
|
383
|
+
...(value.filters !== undefined ? { filters: value.filters } : {}),
|
|
384
|
+
...(value.sort !== undefined ? { sort: value.sort } : {}),
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
type: 'aggregate',
|
|
390
|
+
resource_id: value.resourceId,
|
|
391
|
+
aggregations: value.aggregations,
|
|
392
|
+
...(value.groupBy !== undefined ? { group_by: serializeGroupByRuleForEditor(value.groupBy) } : {}),
|
|
393
|
+
...(value.filters !== undefined ? { filters: value.filters } : {}),
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function serializeGroupByRuleForEditor(value: GroupByRule) {
|
|
398
|
+
if (value.type === 'field') {
|
|
399
|
+
return {
|
|
400
|
+
type: 'field',
|
|
401
|
+
field: value.field,
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
type: 'date_trunc',
|
|
407
|
+
field: value.field,
|
|
408
|
+
truncation: value.truncation,
|
|
409
|
+
...(value.timezone !== undefined ? { timezone: value.timezone } : {}),
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function asWidgetConfigRecord(value: unknown): Record<string, unknown> | undefined {
|
|
414
|
+
return isRecord(value) ? value : undefined
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function getStringField(record: Record<string, unknown>, key: string) {
|
|
418
|
+
const value = record[key]
|
|
419
|
+
return typeof value === 'string' ? value : undefined
|
|
69
420
|
}
|
|
70
421
|
|
|
71
422
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { ref, watch, type Ref } from 'vue'
|
|
2
|
-
import { dashboardApi } from '../api/dashboardApi.js'
|
|
2
|
+
import { dashboardApi, type DashboardWidgetDataRequest } from '../api/dashboardApi.js'
|
|
3
3
|
|
|
4
|
-
export function useWidgetData(
|
|
4
|
+
export function useWidgetData(
|
|
5
|
+
slug: Ref<string>,
|
|
6
|
+
widgetId: Ref<string>,
|
|
7
|
+
request?: Ref<DashboardWidgetDataRequest>,
|
|
8
|
+
) {
|
|
5
9
|
const data = ref<Awaited<ReturnType<typeof dashboardApi.getDashboardWidgetData>> | null>(null)
|
|
6
10
|
const isLoading = ref(false)
|
|
7
11
|
const isFetching = ref(false)
|
|
@@ -20,7 +24,7 @@ export function useWidgetData(slug: Ref<string>, widgetId: Ref<string>) {
|
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
try {
|
|
23
|
-
const response = await dashboardApi.getDashboardWidgetData(slug.value, widgetId.value)
|
|
27
|
+
const response = await dashboardApi.getDashboardWidgetData(slug.value, widgetId.value, request?.value)
|
|
24
28
|
data.value = response
|
|
25
29
|
error.value = null
|
|
26
30
|
return response
|
|
@@ -34,7 +38,7 @@ export function useWidgetData(slug: Ref<string>, widgetId: Ref<string>) {
|
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
watch(
|
|
37
|
-
[slug, widgetId],
|
|
41
|
+
request ? [slug, widgetId, request] : [slug, widgetId],
|
|
38
42
|
() => {
|
|
39
43
|
void refetch()
|
|
40
44
|
},
|
|
@@ -211,6 +211,7 @@ import type {
|
|
|
211
211
|
DashboardWidgetConfig,
|
|
212
212
|
DashboardWidgetMoveDirection,
|
|
213
213
|
} from '../model/dashboard.types.js'
|
|
214
|
+
import { serializeDashboardWidgetConfigForEditor } from '../model/dashboard.types.js'
|
|
214
215
|
|
|
215
216
|
const props = defineProps<{
|
|
216
217
|
dashboardSlug: string
|
|
@@ -388,7 +389,7 @@ async function removeWidget(widgetId: string) {
|
|
|
388
389
|
|
|
389
390
|
function editWidget(widget: DashboardWidgetConfig) {
|
|
390
391
|
editingWidgetId.value = widget.id
|
|
391
|
-
widgetConfigCode.value = stringifyYaml(widget)
|
|
392
|
+
widgetConfigCode.value = stringifyYaml(serializeDashboardWidgetConfigForEditor(widget))
|
|
392
393
|
widgetConfigError.value = ''
|
|
393
394
|
widgetConfigFieldErrors.value = []
|
|
394
395
|
}
|
|
@@ -26,6 +26,7 @@
|
|
|
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'
|
|
29
30
|
import { getWidgetLabel, getWidgetRegistration } from '../widgets/registry.js'
|
|
30
31
|
|
|
31
32
|
const props = defineProps<{
|
|
@@ -52,7 +53,7 @@ const widgetTitle = computed(() => {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
if (props.widget.target === 'chart') {
|
|
55
|
-
return props.widget.chart?.title || 'Untitled chart'
|
|
56
|
+
return normalizeChartWidgetConfig(props.widget.chart)?.title || 'Untitled chart'
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
return getWidgetLabel(props.widget.target)
|
|
@@ -135,11 +135,11 @@ const widgetLayoutVars = computed<CSSProperties>(() => {
|
|
|
135
135
|
const fixedWidth = formatWidth(props.layout?.width)
|
|
136
136
|
|
|
137
137
|
return {
|
|
138
|
-
'--widget-basis': fixedWidth ?? basis,
|
|
139
|
-
'--widget-min-width': fixedWidth ?? formatWidth(props.layout?.minWidth) ?? basis,
|
|
138
|
+
'--widget-basis': clampToContainerWidth(fixedWidth ?? basis),
|
|
139
|
+
'--widget-min-width': clampToContainerWidth(fixedWidth ?? formatWidth(props.layout?.minWidth) ?? basis),
|
|
140
140
|
'--widget-max-width': props.layout?.maxWidth === null
|
|
141
|
-
? '
|
|
142
|
-
: fixedWidth ?? formatWidth(props.layout?.maxWidth) ?? '
|
|
141
|
+
? '100%'
|
|
142
|
+
: clampToContainerWidth(fixedWidth ?? formatWidth(props.layout?.maxWidth) ?? '100%'),
|
|
143
143
|
height: formatWidth(props.layout?.height ?? DEFAULT_WIDGET_HEIGHT),
|
|
144
144
|
}
|
|
145
145
|
})
|
|
@@ -149,4 +149,8 @@ function formatWidth(value: number | undefined) {
|
|
|
149
149
|
return `${value}px`
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
|
+
|
|
153
|
+
function clampToContainerWidth(value: string) {
|
|
154
|
+
return `min(${value}, 100%)`
|
|
155
|
+
}
|
|
152
156
|
</script>
|
|
@@ -119,7 +119,7 @@ Use the current schema keys exactly:
|
|
|
119
119
|
|
|
120
120
|
- Use `target`, not `type`.
|
|
121
121
|
- Use `label`, not `title`.
|
|
122
|
-
- Use `
|
|
123
|
-
- Use `
|
|
124
|
-
- Use `
|
|
125
|
-
- Use `
|
|
122
|
+
- Use `data_source`, not `dataSource`.
|
|
123
|
+
- Use `resource_id`, not `resourceId`.
|
|
124
|
+
- Use `group_by`, not `groupBy`.
|
|
125
|
+
- Use `page_size`, not `pageSize`.
|