Dashboard plugin for AdminForth.
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.
Full setup guide: https://adminforth.dev/docs/tutorial/Plugins/dashboard/
type DashboardConfig = {
version: number
groups: {
id: string
label: string
order: number
}[]
widgets: DashboardWidgetConfig[]
}Each widget has common fields:
| Field | Description |
|---|---|
id |
Persisted widget id. |
group_id |
Group where the widget is rendered. |
label |
Optional widget title. |
target |
Widget type: table, chart, kpi_card, pivot_table, or gauge_card. |
order |
Widget order inside its group. |
variables |
Optional widget variables passed to widget data loading. Variables are not available inside query.calcs. |
size |
Preset width: small, medium, large, wide, or full. |
width, height, min_width, max_width |
Optional explicit layout constraints. |
query |
Data query definition. |
| Widget target | Config field | Main settings | Data usage |
|---|---|---|---|
table |
table |
pagination, page_size, columns |
Uses query to display raw or aggregate rows. |
chart |
chart |
type, x, y, label, value, series, buckets, color, colors |
Uses the same query shape for every chart type. Multi-resource charts use query.source: steps. |
kpi_card |
card |
value, subtitle, comparison, sparkline |
Reads the first returned query row. |
gauge_card |
card |
value, target, progress, color |
Reads the first returned query row. |
pivot_table |
pivot |
rows, columns, values |
Uses query rows to build a pivot table. |
Chart widget types:
| Chart type | Notes |
|---|---|
line |
Uses x and y; y may contain multiple fields in config. |
pie |
Uses label and value. |
bar |
Uses x and y. |
stacked_bar |
Uses x, y, and series. |
funnel |
Uses label, value, and optional colors. Data comes from the same query shapes as every other chart. |
histogram |
Uses x, y, and optional buckets. |
type QueryConfig = {
source?: 'resource'
resource: string
select?: Array<
| { field: string; as?: string; grain?: 'day' | 'week' | 'month' | 'year' }
| { agg: 'sum' | 'count' | 'count_distinct' | 'avg' | 'min' | 'max' | 'median'; field?: string; as: string; filters?: DashboardFilter | DashboardFilter[] }
| { calc: string; as: string }
>
filters?: DashboardFilter | DashboardFilter[]
group_by?: Array<string | { field: string; as?: string; grain?: 'day' | 'week' | 'month' | 'year'; timezone?: string }>
order_by?: Array<{ field: string; direction?: 'asc' | 'desc' }>
limit?: number
offset?: number
bucket?: { field: string; buckets: Array<{ label: string; min?: number; max?: number }> }
calcs?: Array<{ calc: string; as: string }>
formatting?: Record<string, JsonValue>
} | {
source: 'steps'
steps: Array<{
name: string
resource: string
select: Array<{ agg: 'sum' | 'count' | 'count_distinct' | 'avg' | 'min' | 'max' | 'median'; field?: string; as: string; filters?: DashboardFilter | DashboardFilter[] }>
filters?: DashboardFilter | DashboardFilter[]
}>
calcs?: Array<{ calc: string; as: string }>
order_by?: Array<{ field: string; direction?: 'asc' | 'desc' }>
limit?: number
offset?: number
formatting?: Record<string, JsonValue>
}
type DashboardFilter =
| { and: DashboardFilter[] }
| { or: DashboardFilter[] }
| {
field: string
eq?: FilterValue
neq?: FilterValue
gt?: FilterValue
gte?: FilterValue
lt?: FilterValue
lte?: FilterValue
in?: FilterValue[]
not_in?: FilterValue[]
like?: FilterValue
ilike?: FilterValue
}
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }
type RelativeDateValue = { now: true } | { now_minus: `${number}${'h' | 'd' | 'w' | 'mo' | 'y'}` }
type FilterValue = JsonValue | RelativeDateValueUse filters for rolling date ranges. Do not hard-code dates for dashboards that should move with time:
query:
resource: orders
filters:
and:
- field: created_at
gte:
now_minus: 30d
- field: created_at
lt:
now: trueMulti-resource queries use source: steps. Each step uses select, even if it has only one aggregate:
target: chart
label: Average price by database
chart:
type: bar
title: Average price by database
x:
field: name
y:
field: value
query:
source: steps
steps:
- name: SQLite
resource: cars_sl
select:
- agg: avg
field: price
as: value
- name: MySQL
resource: cars_mysql
select:
- agg: avg
field: price
as: valueCost calculation example:
target: chart
label: Model costs
chart:
type: stacked_bar
title: GPT-5.4 costs by day
x:
field: day
y:
- field: input_cost
- field: output_cost
- field: cached_cost
query:
resource: model_usage
filters:
and:
- field: model
eq: gpt-5.4
- field: used_at
gte:
now_minus: 7d
- field: used_at
lt:
now: true
select:
- field: used_at
as: day
grain: day
- agg: sum
field: input_tokens
as: input_tokens
- agg: sum
field: output_tokens
as: output_tokens
- agg: sum
field: cached_tokens
as: cached_tokens
group_by:
- field: used_at
as: day
grain: day
calcs:
- calc: input_tokens / 1000000 * 2.5
as: input_cost
- calc: output_tokens / 1000000 * 15
as: output_cost
- calc: cached_tokens / 1000000 * 0.25
as: cached_costDashboardPage.vue
└── DashboardRuntime.vue
└── DashboardGroup.vue
└── WidgetShell.vue
└── WidgetRenderer.vue
├── TableWidget.vue
├── ChartWidget.vue
├── KpiCardWidget.vue
├── PivotTableWidget.vue
└── GaugeCardWidget.vue
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.