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
| Widget | What it draws | What it costs |
|---|---|---|
Metric(model) | A number, a signed delta, and a sparkline | One 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 average | One query per day |
Breakdown(model, on="status") | One meter per value of a column | One query per value |
Recent(model) | The newest rows, linked | One query |
Counts() | Every registered model’s row count, grouped by application | One 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.