Skip to Content
FeaturesModulesDashboard

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) -> bool

When a tab is requested, the view:

  1. Resolves the callback dotted path and calls the function with request.
  2. Renders template with the callback’s return dict as context (plus current_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.html

2. 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

FieldTypeRequiredDefaultDescription
slugstrYesURL segment. Must be unique across all tabs.
titlestrYesTab label shown in the UI.
iconstrNo"dashboard"Material icon name.
templatestrOne of template/callbackDjango template path.
callbackstrOne of template/callbackDotted path to fn(request) -> dict.
permissionstrNoNoneDotted path to fn(request) -> bool. Default: request.user.is_staff.
superuser_onlyboolNoFalseIf 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

FieldTypeDefaultDescription
tabsList[DashboardTab][]Ordered list of tabs.
default_tabstr | NoneNoneSlug of the tab shown first. Defaults to the first tab in the list.
tailwind_cssstr | NoneNoneStatic path to a compiled CSS file for custom tab styles.

Template context

Every tab template receives:

VariableTypeDescription
current_tabDashboardTabThe active tab object.
all_tabsList[DashboardTab]All configured tabs (used by the tab bar).
dashboard_configDashboardConfigThe full dashboard config object.
titlestrSame as current_tab.title.
**callback_resultdictEverything your callback returned, merged into the context.

Built-in base templates

TemplatePurpose
django_dashboard/base.htmlFull page — extends Unfold admin/base.html, renders the tab bar and {% block tab_content %}.
django_dashboard/tabs_bar.htmlTab bar snippet only (included by base.html).
django_dashboard/tab_empty.htmlFallback rendered when no template is set (callback-only tab).

Extend django_dashboard/base.html and fill {% block tab_content %} in your templates.

URL structure

URLNameDescription
/cfg/admin/dashboard/django_cfg_dashboard_indexRedirects to the default tab.
/cfg/admin/dashboard/<slug>/django_cfg_dashboard_tabRenders 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:

  1. If superuser_only=True — only request.user.is_superuser passes.
  2. If permission is set — the resolved function is called with request, must return a truthy value.
  3. Otherwise — request.user.is_staff is 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.py with def callback(request) -> dict.
  • Create dashboard/templates/dashboard/mypage.html extending django_dashboard/base.html.
  • Add DashboardTab(slug="mypage", ...) to DashboardConfig.tabs in config.py.
  • Done — no migrations, no URL changes, no admin registration needed.
Last updated on