Skip to content

Commit f2d5699

Browse files
committed
feat: refactor widget configuration and data handling
- Updated PivotTableWidget to use new field reference handling and improved computed properties for row, column, and value fields. - Enhanced TableWidget to support field references in column definitions. - Modified dashboard endpoint to ensure proper response formatting. - Improved widget schema to include new field reference and query configurations. - Refactored widget configuration validation to accommodate new query structures. - Updated widget data service to handle funnel queries and improved data aggregation logic. - Added support for new filter expressions and aggregation operations in the widget data service.
1 parent 80e6553 commit f2d5699

20 files changed

Lines changed: 1295 additions & 797 deletions

File tree

README.md

Lines changed: 47 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -31,60 +31,68 @@ Each widget has common fields:
3131
| `order` | Widget order inside its group. |
3232
| `size` | Preset width: `small`, `medium`, `large`, `wide`, or `full`. |
3333
| `width`, `height`, `min_width`, `max_width` | Optional explicit layout constraints. |
34-
| `data_source` | Resource or aggregate data source definition. |
34+
| `query` | Data query definition. |
3535
3636
## Widget Support Matrix
3737
3838
| Widget target | Config field | Main settings | Data usage |
3939
| --- | --- | --- | --- |
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`. |
40+
| `table` | `table` | `pagination`, `page_size`, `columns` | Uses `query` to display raw or aggregate rows. |
41+
| `chart` | `chart` | `type`, `x`, `y`, `label`, `value`, `series`, `buckets`, `color`, `colors` | Uses `query`; funnel charts use `query.steps`. |
42+
| `kpi_card` | `card` | `value`, `subtitle`, `comparison`, `sparkline` | Reads the first returned query row. |
43+
| `gauge_card` | `card` | `value`, `target`, `progress`, `color` | Reads the first returned query row. |
44+
| `pivot_table` | `pivot` | `rows`, `columns`, `values` | Uses query rows to build a pivot table. |
4545
4646
Chart widget types:
4747
4848
| Chart type | Notes |
4949
| --- | --- |
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`. |
50+
| `line` | Uses `x` and `y`; `y` may contain multiple fields in config. |
51+
| `pie` | Uses `label` and `value`. |
52+
| `bar` | Uses `x` and `y`. |
53+
| `stacked_bar` | Uses `x`, `y`, and `series`. |
54+
| `funnel` | Uses `query.steps` and optional `label`, `value`, `colors`. |
55+
| `histogram` | Uses `x`, `y`, and optional `buckets`. |
5656
57-
## Data Source Shape
57+
## Query Shape
5858
5959
```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-
}
60+
type QueryConfig = {
61+
resource: string
62+
select?: Array<
63+
| { field: string; as?: string; grain?: 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year' }
64+
| { agg: 'sum' | 'count' | 'count_distinct' | 'avg' | 'min' | 'max' | 'median'; field?: string; as: string; filters?: unknown }
65+
| { calc: string; as: string }
66+
>
67+
filters?: unknown
68+
group_by?: Array<string | { field: string; as?: string; grain?: 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'; timezone?: string }>
69+
order_by?: Array<{ field: string; direction?: 'asc' | 'desc' }>
70+
limit?: number
71+
offset?: number
72+
}
8573
```
8674
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.
75+
Funnel charts use a steps query:
76+
77+
```yaml
78+
target: chart
79+
chart:
80+
type: funnel
81+
title: Sales funnel
82+
query:
83+
steps:
84+
- name: Leads
85+
resource: leads
86+
metric:
87+
agg: count
88+
as: value
89+
- name: Customers
90+
resource: orders
91+
metric:
92+
agg: count_distinct
93+
field: customer_id
94+
as: value
95+
```
8896

8997
## Runtime Structure
9098

custom/composables/useElementSize.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,26 @@ export function useElementSize<T extends HTMLElement>(): ElementSizeState<T> {
1313
const height = ref(0)
1414

1515
let observer: ResizeObserver | undefined
16+
let frameId: number | undefined
1617

1718
onMounted(() => {
1819
observer = new ResizeObserver(([entry]) => {
1920
if (!entry) {
2021
return
2122
}
2223

23-
width.value = Math.floor(entry.contentRect.width)
24-
height.value = Math.floor(entry.contentRect.height)
24+
const nextWidth = Math.floor(entry.contentRect.width)
25+
const nextHeight = Math.floor(entry.contentRect.height)
26+
27+
if (frameId !== undefined) {
28+
cancelAnimationFrame(frameId)
29+
}
30+
31+
frameId = requestAnimationFrame(() => {
32+
frameId = undefined
33+
width.value = nextWidth
34+
height.value = nextHeight
35+
})
2536
})
2637

2738
if (el.value) {
@@ -30,6 +41,10 @@ export function useElementSize<T extends HTMLElement>(): ElementSizeState<T> {
3041
})
3142

3243
onBeforeUnmount(() => {
44+
if (frameId !== undefined) {
45+
cancelAnimationFrame(frameId)
46+
}
47+
3348
observer?.disconnect()
3449
})
3550

0 commit comments

Comments
 (0)