The dashboard

The front page is a list of widgets you arrange. Charts drawn by the server — no charting library, no canvas, and every one of them states what it costs in queries.

Available since v0.4.0

What you get without configuring anything

Every model’s row count, grouped the way the sidebar groups them, and a chart of new accounts over the last thirty days. That is two widgets — Signups() and Counts() — and it is what the dashboard has always been.

Nothing below is required. Reach for it when the front page should answer a question about your domain rather than about the schema.

Arranging it

from fastfort.admin import Breakdown, Counts, Metric, Recent, Trend

fort.set_dashboard(
    Metric(Shipment, title="Shipments booked", days=14, icon="truck"),
    Metric(Invoice, title="Invoices raised", days=14, icon="credit-card"),
    Trend(Shipment, title="Shipments per day", days=30),
    Breakdown(Shipment, on="status"),
    Recent(Shipment, limit=6),
    Counts(),
)

Widgets render in the order given. fort.set_dashboard() with no arguments is a dashboard with nothing on it, which is a reasonable thing to want and a confusing thing to reach by accident — so that is the only way to say it.

The five that ship

WidgetWhat it drawsWhat it costs
Metric(model)A number, a signed delta, and a sparklineOne query per day
Trend(model)The same series drawn large, as an area chart or as bars, with the window’s total, its busiest day and its daily averageOne query per day
Breakdown(model, on="status")One meter per value of a columnOne query per value
Recent(model)The newest rows, linkedOne query
Counts()Every registered model’s row count, grouped by applicationOne query per model

Every widget takes title=, span= (THIRD, HALF or FULL) and — where it plots time — days= and on=, the column recording when a row arrived. Leave on out and the usual names are detected: created_at, date_joined, registered_at and the rest.

The cost column is not decoration. A dashboard is the page that gets opened most in any admin, and the easy mistake is a card that quietly runs a hundred queries. Trend(model, days=90) is ninety indexed counts on every page load; days=14 is fourteen. Every widget on the page shares the request’s one unit of work, so the count is queries, not connections.

A widget that cannot say anything renders nothing

A column that does not exist, a model with no date column, a breakdown of free text, dashboard_days=0 — each of those leaves the card off rather than raising. A typo in a configuration file should cost a card, never the page everybody opens first.

That is also why Breakdown refuses a column without a fixed set of values: counting the distinct values of a free-text column is a scan of the table.

The charts are drawn by the server

An SVG path, a few <line>s, and boxes with a height. No charting library, no <canvas>, no second request, and not one byte of JavaScript beyond what the admin already ships.

Which means: the chart is in the first paint, it prints, it takes the theme’s own colours in light and dark, and it survives script being switched off. Script adds the tooltips and nothing else. Beside every chart is the same data as a table, clipped from view but not from a screen reader.

Every chart uses one colour, because the whole palette derives from one hue. A trend is a single series, and a breakdown compares bars by length — the one visual channel people read accurately — with the count and the share in text beside each bar.

Writing your own

from fastfort.admin.dashboard import Card, Widget

class OpenTickets(Widget):
    span = 1

    async def resolve(self, context):
        adapter = context.adapter_for(Ticket)
        count = await adapter.count(ListQuery(filters=(Filter("state", FilterOperator.EXACT, "open"),)))
        if not count:
            return None
        return Card(
            template="dashboard/open_tickets.html",
            context={"count": count},
            span=self.span,
        )

fort.set_dashboard(OpenTickets(), Counts())

Card.template is resolved through the renderer, and a project’s own template directory is searched before the package’s — so dashboard/open_tickets.html in your own templates directory is all that is needed. Nothing in the page is special-cased for the built-in widgets; yours renders exactly as they do.

context gives you the request’s unit of work (adapter_for, spec_for), the registry (key_for, url_for, title_for) and the configured window (days). Reaching past it into an ORM is the thing to avoid: a widget written against the adapter protocol keeps working when the project changes backends.