django_dashboard — Admin Dashboard Tabs
django_dashboard adds a tabbed panel system to the Django Unfold admin overview page. Each tab is a standard Django view: a Python callback function returns a dict, and a Django template renders it. No Streamlit, no iframes, no special base classes.
How it works
DjangoConfig.dashboard: DashboardConfig
└── tabs: List[DashboardTab]
├── slug → URL segment /cfg/admin/dashboard/<slug>/
├── title → Tab label
├── icon → Material icon name
├── callback → dotted path fn(request) -> dict
├── template → Django template path
└── permission → dotted path fn(request) -> boolWhen a tab is requested, the view:
- Resolves the
callbackdotted path and calls the function withrequest. - Renders
templatewith the callback’s return dict as context (pluscurrent_tab,all_tabs,dashboard_config).
Quick start
1. Create a dashboard app
myproject/
└── dashboard/
├── __init__.py
├── apps.py
├── callbacks/
│ ├── __init__.py
│ ├── overview.py
│ └── system.py
└── templates/
└── dashboard/
├── overview.html
└── system.html2. Write a callback
# dashboard/callbacks/overview.py
from django.contrib.auth import get_user_model
def callback(request):
User = get_user_model()
return {
"total_users": User.objects.count(),
"recent_users": User.objects.order_by("-date_joined")[:5],
}The callback receives the real HttpRequest. You can do anything: ORM queries, aggregations, pagination (request.GET.get("page")), Redis lookups, gRPC calls — whatever you need.
3. Write a template
{# dashboard/templates/dashboard/overview.html #}
{% extends "django_dashboard/base.html" %}
{% block tab_content %}
<div class="p-4">
<p>Total users: {{ total_users }}</p>
{% for user in recent_users %}
<div>{{ user.email }} — {{ user.date_joined|date:"d M Y" }}</div>
{% endfor %}
</div>
{% endblock %}Templates are standard Django templates. Use {% include %}, template tags, filters — anything goes.
4. Register in config
# api/config.py
from django_cfg import DjangoConfig, DashboardConfig, DashboardTab
class MyConfig(DjangoConfig):
project_apps = [..., "dashboard"] # needed so Django finds your templates
dashboard = DashboardConfig(
tabs=[
DashboardTab(
slug="overview",
title="Overview",
icon="bar_chart",
callback="dashboard.callbacks.overview.callback",
template="dashboard/overview.html",
),
DashboardTab(
slug="system",
title="System",
icon="monitor_heart",
callback="dashboard.callbacks.system.callback",
template="dashboard/system.html",
permission="dashboard.permissions.is_superuser",
),
],
)No migrations, no URL changes, no admin registration — that’s all you need.
Configuration reference
DashboardTab fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
slug | str | Yes | — | URL segment. Must be unique across all tabs. |
title | str | Yes | — | Tab label shown in the UI. |
icon | str | No | "dashboard" | Material icon name. |
template | str | One of template/callback | — | Django template path. |
callback | str | One of template/callback | — | Dotted path to fn(request) -> dict. |
permission | str | No | None | Dotted path to fn(request) -> bool. Default: request.user.is_staff. |
superuser_only | bool | No | False | If True, only superusers can view this tab. Overrides permission. |
At least one of template or callback must be set — validated at startup by DashboardTab’s model validator.
DashboardConfig fields
| Field | Type | Default | Description |
|---|---|---|---|
tabs | List[DashboardTab] | [] | Ordered list of tabs. |
default_tab | str | None | None | Slug of the tab shown first. Defaults to the first tab in the list. |
tailwind_css | str | None | None | Static path to a compiled CSS file for custom tab styles. |
Template context
Every tab template receives:
| Variable | Type | Description |
|---|---|---|
current_tab | DashboardTab | The active tab object. |
all_tabs | List[DashboardTab] | All configured tabs (used by the tab bar). |
dashboard_config | DashboardConfig | The full dashboard config object. |
title | str | Same as current_tab.title. |
**callback_result | dict | Everything your callback returned, merged into the context. |
Built-in base templates
| Template | Purpose |
|---|---|
django_dashboard/base.html | Full page — extends Unfold admin/base.html, renders the tab bar and {% block tab_content %}. |
django_dashboard/tabs_bar.html | Tab bar snippet only (included by base.html). |
django_dashboard/tab_empty.html | Fallback rendered when no template is set (callback-only tab). |
Extend django_dashboard/base.html and fill {% block tab_content %} in your templates.
URL structure
| URL | Name | Description |
|---|---|---|
/cfg/admin/dashboard/ | django_cfg_dashboard_index | Redirects to the default tab. |
/cfg/admin/dashboard/<slug>/ | django_cfg_dashboard_tab | Renders a specific tab. |
Both require is_staff. Per-tab permission callbacks override the default.
Custom CSS (Tailwind)
If your templates use custom Tailwind classes, point tailwind_css to your compiled output file:
DashboardConfig(
tailwind_css="dashboard/css/dashboard.css", # relative to STATIC_ROOT
tabs=[...],
)The base template injects a <link rel="stylesheet"> automatically when this is set.
Template tags
{% load django_cfg_dashboard %}
{# Returns DashboardConfig or None for the current staff user #}
{% get_dashboard_config as dashboard %}
{% if dashboard %}
{% include "django_dashboard/tabs_bar.html" with
all_tabs=dashboard.tabs
current_tab=dashboard.tabs.0
dashboard_config=dashboard %}
{% endif %}Also available via {% load django_cfg %} as a backward-compatibility shim.
Permission control
Permission is checked in this order:
- If
superuser_only=True— onlyrequest.user.is_superuserpasses. - If
permissionis set — the resolved function is called withrequest, must return a truthy value. - Otherwise —
request.user.is_staffis the default.
# dashboard/permissions.py
def is_superuser(request) -> bool:
return bool(request.user.is_superuser)
def is_analytics_team(request) -> bool:
return request.user.groups.filter(name="analytics").exists()Adding a new tab (checklist)
- Create
dashboard/callbacks/mypage.pywithdef callback(request) -> dict. - Create
dashboard/templates/dashboard/mypage.htmlextendingdjango_dashboard/base.html. - Add
DashboardTab(slug="mypage", ...)toDashboardConfig.tabsinconfig.py. - Done — no migrations, no URL changes, no admin registration needed.