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", value="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, and charts and lists two.
The examples on this page use the example models.
Where the numbers come from¶
A card reads a SQLAlchemy select, or an async function that gets the session and returns the
value:
from adminsite.database import SessionAdapter
async def active_customers(session: SessionAdapter) -> int:
return await session.scalar(select(func.count()).where(Customer.is_active))
Stat("Active customers", active_customers)
A function can reach anything else too, such as another database, an API or a cache.
The cards¶
Stat¶
A single number, written in its format, such as "€{:,.2f}". Give it previous, a second
source for the period before, and it shows the change, such as "+12% on the period before". link
makes the card open a page, so "orders?status=PAID" opens the filtered list. See
Stat.
Chart¶
Bars or a line over rows of a label and a value, with kind set to "bar" or "line". The chart
is drawn on the server, so no chart library is loaded.
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. sort sets the
order, value a path shown beside each record, such as "total", and limit how many records
show. See RecentRecords.
ModelCounts¶
The record counts of every view the user may open. 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 starlette.requests import Request
from adminsite import Admin, Widget
class Weather(Widget):
title = "Weather at the warehouse"
template = "cards/weather.html"
width = 2
async def load(self, admin: Admin, request: Request) -> dict[str, float]:
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>
Show a card to some users¶
Every card has allows(admin, request). Override it to show a card only to some users:
class Margin(Stat):
async def allows(self, admin: Admin, request: Request) -> bool:
return request.state.user.is_manager
A card that fails to load says so, and the error goes to the adminsite logger. The rest of the
overview still loads.