Dashboard¶
The overview page is a grid of cards. Without any setup it shows how many records each view holds.
Pass dashboard to choose the cards:
from sqlalchemy import func, select
from adminsite import Admin, Chart, RecentRecords, Stat
order_day = func.date(Order.created_at)
admin = Admin(
engine,
views=[OrderView, CustomerView],
dashboard=[
Stat("Revenue", select(func.sum(Order.total)), format="€{:,.2f}"),
Stat(
"Waiting to ship",
select(func.count()).where(Order.status == "paid"),
link="orders?status=PAID",
),
Chart(
"Revenue per day",
select(order_day, func.sum(Order.total))
.group_by(order_day)
.order_by(order_day),
format="€{:,.2f}",
),
RecentRecords("Latest orders", "orders", sort="-created_at", detail="total"),
],
)
Cards sit four to a row on a wide screen and one to a row on a phone. Each has a width from 1 to
4; stats take one column, charts and lists two.
Where the numbers come from¶
A card reads either a SQLAlchemy select, or an async function that receives the session and
returns the value:
async def active_customers(session):
return await session.scalar(select(func.count()).where(Customer.is_active))
Stat("Active customers", active_customers)
The function is the way to reach anything else: another database, an API, a cache.
The cards¶
Stat¶
A single number.
| Option | What it does |
|---|---|
format |
How the value is written, such as "{:,}" or "€{:,.2f}". |
previous |
A second source for the period before. The card then shows the change, such as "+12% on the period before", in green or red. |
hint |
A line of text under the number, when there is no change to show. |
link |
Where clicking the card goes. A relative link starts at the admin, so "orders?status=PAID" opens the filtered list. |
Chart¶
Bars or a line over rows of a label and a value. kind is "bar" or "line". The chart is drawn
as plain SVG on the server, so no chart library is loaded. Hovering a bar shows its value, and
screen readers get the same numbers as a table.
Dates as labels are written short, such as "Sep 14".
RecentRecords¶
The latest records of a view, each linking to its page. It reads through the view, so the view's
scope_query applies and the card is hidden from users who may not open the view.
| Option | What it does |
|---|---|
sort |
The order, such as "-created_at". The view's own ordering if left out. |
detail |
A path shown at the right of each record, such as "total". |
limit |
How many records. Five unless you say. |
ModelCounts¶
The record counts of every view the user may open, within its scope. It is the whole overview when you set no dashboard, and you can put it among your own cards.
Cards of your own¶
Subclass Widget, give it a template from your template_dirs, and return what it needs from
load. The template gets widget and data:
from adminsite import Widget
class Weather(Widget):
title = "Weather at the warehouse"
template = "cards/weather.html"
width = 2
async def load(self, admin, request):
return await fetch_weather("Rotterdam")
<div class="card h-full border border-base-300">
<div class="card-body p-4">
<h2 class="text-sm font-medium text-base-content/70">{{ widget.title }}</h2>
<p class="text-2xl">{{ data.temperature }}°</p>
</div>
</div>
Who sees what¶
Every card has allows(admin, request). Override it to show a card only to some users:
class Margin(Stat):
async def allows(self, admin, request):
return request.state.user.is_manager
A card whose query fails shows "This card could not be loaded" and the error goes to the
adminsite logger. The rest of the overview still loads.