Defining and Seeding Dashboards
Developer guide for the Enezaframework dashboard system. Part of the Dashboards series.
Table of Contents
- Overview
- Where Definitions Live
- Definition Skeleton
- Dashboard Keys
- Tab Keys
- Widget Keys and Defaults
- Widget Types at a Glance
- Layout Rules
- Parameters, Filters, and Data Settings
- Seeding
- Publication Behavior
- Ownership and Versioning
- Repository Workflow
- Related Guides
Overview
A declarative definition is a Python dictionary that describes a dashboard, its tabs, and its widgets. Seeding turns that dictionary into rows:
flowchart LR
DEF[AppConfig.dashboards<br/>or settings or registry] --> LOAD[load_definitions]
LOAD --> VALIDATE[validate_definition]
VALIDATE --> SEED[seed_definitions]
SEED --> DASH[Dashboard]
SEED --> VER[Draft DashboardVersion]
SEED --> TAB[DashboardTab]
SEED --> WIDGET[DashboardWidget]
SEED --> PUB[Publish when requested]Definitions are declarative, not imperative: you describe the desired end
state, and re-running the seed is safe. Identity is stable, defaults are filled
in, and omitted tabs/widgets are pruned unless you opt out
(enezadashboard/services/seeding.py:94-118,
enezadashboard/services/seeding.py:210-325).
You will build: a DASHBOARDS list for your app, seed it, and publish a
dashboard.
Where Definitions Live
There are three supported sources, all loaded by
enezadashboard/definitions.py:22-55:
| Source | Best for | How |
|---|---|---|
AppConfig.dashboards |
Normal app-owned dashboards. | Set dashboards = DASHBOARDS on the app config. |
settings.ENEZA_DASHBOARD_DEFINITIONS |
Project-wide or deployment-specific dashboards. | A list of dicts, import paths, or callables. |
register_dashboard_definition() |
Dynamic registration from code. | from enezadashboard.registry import register_dashboard_definition. |
frameworkapp uses the first form:
# frameworkapp/apps.py
from django.apps import AppConfig
from .dashboards import DASHBOARDS
class FrameworkappConfig(AppConfig):
name = "frameworkapp"
dashboards = DASHBOARDS
When a definition comes from an AppConfig, the loader fills in module from
the app label if it is missing
(enezadashboard/definitions.py:31-37).
Precedence and conflicts:
- All sources are loaded.
- Duplicate
(module, code)identities raiseValueErrorduring loading (enezadashboard/definitions.py:43-54). - A definition is not merged with another definition; one identity must come from one source.
Definition Skeleton
DASHBOARDS = [
{
"module": "myapp",
"code": "sales_overview",
"name": "Sales overview",
"description": "Sales volume, mix, and item detail.",
"is_active": True,
"is_default": True,
"sort_order": 0,
"managed_by": "seed",
"layout_template": "",
"default_report_ref": "myapp.salesbyitem",
"access_code": "",
"seed_revision": "2026-10-24",
"publish": True,
"tabs": [
{
"code": "overview",
"name": "Overview",
"description": "",
"icon": "squares-2x2",
"sort_order": 0,
"layout": {"columns": 12},
"filter_config": {
"date_range": {"enabled": True}
},
"widgets": [
{
"code": "total_value",
"title": "Total value",
"type": "kpi",
"report_ref": "myapp.salesbyitem",
"display_config": {
"measure": "sum__value",
"aggregation": "sum",
"format": "currency",
"decimals": 2,
},
"layout": {"x": 0, "y": 0, "w": 3, "h": 2},
"sort_order": 0,
"is_active": True,
},
],
},
],
},
]
The live reference is
frameworkapp/dashboards.py:1-117, walked
through in Worked Example: frameworkapp.
Dashboard Keys
| Key | Required | Default | Meaning |
|---|---|---|---|
module |
Yes | App label when loaded from an AppConfig |
Installed Django app that owns the dashboard. |
code |
Yes | — | Slug; unique with module. |
name |
Yes | — | Display name. |
description |
No | "" |
Long description. |
is_active |
No | True |
Inactive dashboards are not visible. |
is_default |
No | False |
The module default for users without a preference. Only one active default per module. |
sort_order |
No | 0 |
Ordering in lists. |
managed_by |
No | seed |
seed or admin. Controls whether future seeds may update it. |
layout_template |
No | "" |
Wireframe code. Must exist when set. Recorded for provenance only; seeded tabs/widgets are authoritative. |
default_report_ref |
No | "" |
Stable report code. Must resolve. |
access_code |
No | "" |
Optional RBAC permission required to view the dashboard. |
seed_revision |
No | "" |
Free-form revision marker stored on the dashboard. |
publish |
No | True for new dashboards |
Publish after seeding. See Publication Behavior. |
tabs |
No | [] |
Tab definitions. |
Validation rules (enezadashboard/services/seeding.py:121-207):
modulemust be an installed app.codemust be a valid slug.nameis required.managed_bymust beseedoradmin.layout_template, when set, must be a known wireframe.default_report_ref, when set, must resolve to an eligible data source.
Data settings are not part of the seed schema.
Dashboard.data_settingsholdsreference_date_field,limit, anddefault_period. The seeding code does not map them from a definition (enezadashboard/services/seeding.py:230-241). Set them in the designer's dashboard form, or extend the seeding service if they must be code-controlled. Do not assume adata_settingskey in a definition will take effect.
Tab Keys
| Key | Required | Default | Meaning |
|---|---|---|---|
code |
Yes | — | Stable tab code; unique within the version. |
name |
Yes | — | Display name. |
description |
No | "" |
Tooltip/description. |
icon |
No | "" |
One of the tab icon keys. Seed validation rejects unknown values. |
sort_order |
No | 0 |
Tab order. |
layout |
No | {} |
Tab layout metadata; the designer stores {"columns": 12} for wireframes. |
filter_config |
No | {} |
Date range defaults and shared parameter/data settings. |
widgets |
No | [] |
Widget definitions. |
Tab icons must come from the fixed list in
enezadashboard/icons.py:1-18:
"", squares-2x2, chart-bar, chart-pie, arrow-trending-up,
arrow-trending-down, cube, truck, banknotes, Users,
building-office, shopping-cart, document-text,
clipboard-document-list, cog-6-tooth, shield-check.
A tab with no active widget cannot be published; publication validates at least
one active widget per tab (enezadashboard/services/validation.py:109-112).
Widget Keys and Defaults
| Key | Required | Default | Meaning |
|---|---|---|---|
code |
Yes | — | Stable widget code; unique within the tab. |
title |
Yes | — | Display title. |
type or widget_type |
Yes | — | kpi, chart, table, or report_preview. |
report_ref |
Yes | — | Stable data-source code. |
description |
No | "" |
Subtitle/description. |
display_config |
No | {} |
Type-specific settings. See the reference. |
layout |
No | By type: KPI 3×2, chart 6×4, preview 6×4, table 12×4 |
Grid position. |
parameter_config |
No | {} |
Report parameter values. Keys must be allowed parameter names. |
filter_config |
No | {} |
Query-builder filter rules for this widget. |
sort_order |
No | 0 |
Ordering inside the tab. |
is_active |
No | True |
Inactive widgets are skipped and not published. |
type and widget_type are aliases; _widget_type() accepts either
(enezadashboard/services/seeding.py:44-45).
Every active widget must reference a report and pass that widget type's
validation. Unknown widget types fail validation
(enezadashboard/services/validation.py:114-138).
Widget Types at a Glance
| Type | Renders | Required display_config |
Full reference |
|---|---|---|---|
kpi |
A single aggregate with optional sparkline and comparison. | measure (unless aggregation is count). |
KPI |
chart |
Server-rendered SVG chart. | category_field and value_field; gauge and other types add fields. |
Chart |
table |
Paginated, sortable data grid. | None; columns default to the first six report columns. | Table |
report_preview |
A short, non-paginated table. | None; columns default to the first six. | Report preview |
A minimal set:
"widgets": [
{
"code": "total_value",
"title": "Total value",
"type": "kpi",
"report_ref": "myapp.salesbyitem",
"display_config": {
"measure": "sum__value",
"aggregation": "sum",
},
"layout": {"x": 0, "y": 0, "w": 3, "h": 2},
},
{
"code": "value_by_item",
"title": "Value by item",
"type": "chart",
"report_ref": "myapp.salesbyitem",
"display_config": {
"chart_type": "bar",
"category_field": "item_code",
"value_field": "sum__value",
"limit": 10,
"sort": "desc",
},
"layout": {"x": 3, "y": 0, "w": 9, "h": 4},
},
{
"code": "detail",
"title": "Item detail",
"type": "table",
"report_ref": "myapp.salesbyitem",
"display_config": {
"columns": ["item_code", "item_description", "sum__quantity", "sum__value"],
"page_size": 10,
},
"layout": {"x": 0, "y": 4, "w": 12, "h": 5},
},
]
Layout Rules
The layout is a 12-column grid:
| Property | Rule | Source |
|---|---|---|
x |
Non-negative; must satisfy x + w <= 12. |
enezadashboard/services/validation.py:28-49 |
y |
Non-negative. The canvas endpoint accepts 0–200. | enezadashboard/views/designer.py:645-654 |
w |
2–12. | enezadashboard/services/validation.py:44-45 |
h |
2–8. | enezadashboard/services/validation.py:46-47 |
Example positions:
{"x": 0, "y": 0, "w": 3, "h": 2} # KPI
{"x": 3, "y": 0, "w": 9, "h": 4} # chart
{"x": 0, "y": 4, "w": 12, "h": 5} # full-width table
Publication rejects invalid layouts with a path such as
tabs[0].widgets[2].
Parameters, Filters, and Data Settings
Widget parameters
parameter_config supplies report form values:
"parameter_config": {
"start_date": "2026-01-01",
"end_date": "2026-03-31",
}
Allowed keys come from the report's serialized parameters:
DateRangeParam→start_date,end_date;- other parameters → their
nameandform_fields.
Unknown keys fail validation (enezadashboard/services/validation.py:52-62,
enezadashboard/services/seeding.py:202-206).
Widget filters
filter_config holds query-builder rules:
"filter_config": {
"condition": "AND",
"groups": [
{
"condition": "AND",
"rules": [
{"id": "item_code", "operator": "equal", "value": "A-100"}
],
}
],
}
Every rule id must appear in the report's metadata filter_fields, and the
operator must be supported by the workflow engine
(enezareporting/dashboard.py:433-466).
Tab date range
"filter_config": {
"date_range": {
"enabled": True,
"default_period": "last_30_days",
}
}
For a custom range:
"date_range": {
"enabled": True,
"default_period": "custom",
"default_start_date": "2026-01-01",
"default_end_date": "2026-03-31",
}
Tab defaults override the dashboard default; the renderer falls back to the
dashboard's data_settings.default_period and then the site default
(enezadashboard/services/renderer.py:218-254).
Tab shared parameter and data settings
The tab filter_config may also carry:
parameter_defaults: values applied to every widget before widget-levelparameter_config;data:reference_date_fieldandlimit, which override the dashboard-leveldata_settingsfor this tab.
Data settings merge in this order: dashboard data_settings, then tab
filter_config.data. Parameters merge in this order: tab
filter_config.parameter_defaults, then widget parameter_config, then
non-date request query values, then the resolved period dates
(enezadashboard/services/renderer.py:222-231,
enezadashboard/services/renderer.py:688-731).
Dashboard data settings (designer only)
| Setting | Stored key | Effect |
|---|---|---|
| Reference date field | data_settings.reference_date_field |
Overrides the report's date field when applying periods. |
| Data load limit | data_settings.limit |
Caps rows loaded by table, chart, and preview widgets. KPI/gauge aggregates are not capped. |
| Default period | data_settings.default_period |
Default date range when a tab does not define one. |
These are edited in the designer dashboard form and are not part of the seed
schema (enezadashboard/forms.py:274-292).
Seeding
Run the management command:
python manage.py seed_dashboards --dry-run --json
python manage.py seed_dashboards
python manage.py seed_dashboards --publish
Options (enezadashboard/management/commands/seed_dashboards.py:15-24):
| Flag | Effect |
|---|---|
--module <label> |
Restrict to one module. |
--dashboard <code> |
Restrict to one dashboard code. |
--dry-run |
Validate and report; write nothing. |
--json |
Machine-readable result. |
--force |
Update admin-owned dashboards. |
--publish |
Publish updated definitions whose draft changed. |
--no-prune |
Keep tabs/widgets omitted from the definition. |
--deactivate-missing |
Deactivate seed-owned dashboards not present in the definitions. Ignored when --dashboard is set. |
--continue-on-error |
Keep processing after an invalid definition. |
Dry-run output reports either Would seed ... or Would skip ... (administrator-owned)
and exits with an error if validation fails
(enezadashboard/management/commands/seed_dashboards.py:42-70).
A successful run reports counts:
Dashboards: 1 created, 0 updated, 0 skipped, 1 published, 0 pruned.
Publication Behavior
- A new seeded dashboard is published by default unless the definition sets
"publish": false(enezadashboard/services/seeding.py:312-324). - An existing dashboard is published only when
publishis truthy in the definition or--publishis passed. - Publishing is skipped when the published and draft signatures are identical, so re-seeding an unchanged definition does not create versions.
- Publication validates the draft first. Invalid definitions fail with
invalid_definitionfrom the seed command, and invalid drafts fail withpublish_invalidfrom publication (enezadashboard/services/publication.py:68-101).
Ownership and Versioning
managed_by |
Who owns it | Seed behavior |
|---|---|---|
seed |
The definition. | Updated on every run. |
admin |
An administrator. | Skipped unless --force. |
Designer-created dashboards default to admin (Dashboard.managed_by default is
admin in enezadashboard/models/dashboard.py:55-60). The designer's Take
ownership action switches a seeded dashboard to admin, protecting it from
future seed updates (enezadashboard/services/publication.py:154-157).
Stable identity:
- dashboard:
(module, code); - tab:
(version, code); - widget:
(tab, code).
Database primary keys are never external identity. Re-running the same definition creates no duplicates.
Pruning is on by default: tabs and widgets omitted from the definition are
deleted from the draft. Use --no-prune when a definition is intentionally
partial.
Repository Workflow
- Add or edit the report in
myapp/views/reports.py. - Add or edit
myapp/dashboards.py. - Wire
dashboards = DASHBOARDSintomyapp/apps.py. - Run
python manage.py seed_dashboards --dry-run --json. - Fix every validation error.
- Run
python manage.py seed_dashboards. - Run
python manage.py rbac_syncwhen report permissions changed. - Open
/dashboards/<module>/and verify the published dashboard. - Commit the report, definitions, and
AppConfigchange together.
Do not edit a seed-owned dashboard in the designer and then re-seed without
taking ownership; the next seed overwrites the edits.