Skip to content

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.