Bigger Dashboard
As a dashboard grows, keeping every page in one file gets hard to follow. A Page is a plain object built at import, so each one can live in a module of its own.
Here's an example structure:
.
├─ myapp
│ ├─ __init__.py
│ ├─ main.py
│ └─ dashboard
│ ├─ __init__.py
│ ├─ dashboard.py
│ ├─ loaders.py
│ └─ pages
│ ├─ __init__.py
│ ├─ overview.py
│ └─ activity.py
dashboard.py is the dashboard's entry point: it includes every page and starts the dashboard. Each module in pages/ defines one page, with the actions its widgets use. This is one way to lay it out, pages fit whatever structure works for your project.
A page module
# myapp/dashboard/pages/overview.py
from dachshund import Page
from dachshund.widgets import Number, Table, Text, TimeSeriesChart
from myapp.dashboard.loaders import load_users
overview_page = Page("overview", title="Overview")
@overview_page.action("deactivate", label="Deactivate", confirm=True)
async def deactivate(ctx, id: str) -> None:
await ctx["db"].deactivate_user(int(id))
with overview_page.row():
overview_page.add(Text("summary"))
with overview_page.row():
with overview_page.column(span=4):
overview_page.add(Number("user_count"))
overview_page.add(TimeSeriesChart("latency"))
with overview_page.row():
overview_page.add(
Table(
"users",
columns=["id", "name", "email"],
loader=load_users,
actions=["deactivate"],
)
)
# myapp/dashboard/pages/activity.py
from dachshund import Page
from dachshund.widgets import BarChart, Number
activity_page = Page("activity", title="Activity")
with activity_page.row():
with activity_page.column(span=4):
activity_page.add(Number("requests_today", title="Requests today"))
activity_page.add(BarChart("requests_per_day", x="date", persist=True))
with activity_page.row():
activity_page.add(BarChart("errors_per_route", x="route", persist=True))
Code shared by several pages, like a table's loader, goes in a module of its own:
# myapp/dashboard/loaders.py
from dachshund.widgets import TableQuery
async def load_users(ctx, query: TableQuery):
users = await ctx["db"].users(
search=query.search,
order_by=query.sort_by,
descending=query.descending,
offset=query.offset,
limit=query.limit,
)
total = await ctx["db"].count_users(search=query.search)
return [{"id": u.id, "name": u.name, "email": u.email} for u in users], total
The entry point
# myapp/dashboard/dashboard.py
import dachshund
from dachshund.storage import SQLiteStorage
from myapp.dashboard.pages.activity import activity_page
from myapp.dashboard.pages.overview import overview_page
def start_dashboard(context: dict) -> None:
dachshund.init(
"My service",
storage=SQLiteStorage("widget_state.db"),
context=context,
)
for page in (overview_page, activity_page):
dachshund.include(page)
Your application calls it once at startup:
# myapp/main.py
from myapp.dashboard.dashboard import start_dashboard
start_dashboard(context)
context is passed as ctx to every action and loader, see Context.
Including a page twice does nothing, so start_dashboard() can run again after a restart of the dashboard.
Events are emitted from wherever they happen in myapp, not from the dashboard modules.
Pages from a library
A library can ship a page the same way, for applications to include:
# dachshund_jobs/pages/jobs.py
from dachshund import Page
from dachshund.widgets import Number, Table
jobs_page = Page("jobs", title="Jobs")
with jobs_page.row():
jobs_page.add(Number("queued_jobs"))
with jobs_page.row():
jobs_page.add(Table("jobs", columns=["id", "name", "status"], key="id"))
from dachshund_jobs.pages.jobs import jobs_page
dachshund.include(jobs_page)