# Dashboards and data apps

> Data-heavy Python desktop apps made with Pyzed: an admin dashboard with KPIs and charts, a data table with filters and paging, a kanban board, a team chat, a music player, an IDE layout and a contacts app over SQLite.

Source: https://albakore.github.io/pyzed/showcase/dashboards/

Screens full of numbers, tables and lists: the kind of internal tool, admin panel or media app that many desktop
programs are. The first six are [Blocks](https://albakore.github.io/pyzed/blocks/), complete screens written with the
[components library](https://albakore.github.io/pyzed/library/) that you can copy and adapt; each block's page has its full code and notes on
what to change. Every capture follows this site's theme: switch between light and dark at the top of the page to
see them in both.

## Dashboard

An admin dashboard: sidebar navigation, KPI cards with trends, a revenue chart, traffic sources and a table of recent
orders.

- KPI cards are `ui.stat` with a `delta` and a small `trend` line. The charts are ready-made components too:
  `ui.bar_chart` for revenue (its range is a `ui.segmented` control) and `ui.donut` for traffic sources, with a
  `ui.progress` bar for the monthly goal.
- The sidebar switches pages by changing one attribute of the state: see [A multi-view
  app](https://albakore.github.io/pyzed/tutorials/intermediate/navigation/).

The code: [Dashboard block](https://albakore.github.io/pyzed/blocks/dashboard/).

## Data table

A team members table with search, status filters, sortable columns, row selection, paging and an export action.

The code: [Data table block](https://albakore.github.io/pyzed/blocks/data-table/). To learn the parts one by one (search, sort, select, show
the details), follow [Lists and tables with data](https://albakore.github.io/pyzed/tutorials/intermediate/lists-tables/), which builds this
small book library:

## Kanban board

A project board with columns, labelled cards, assignees, progress, and buttons to move cards along.

The code: [Kanban block](https://albakore.github.io/pyzed/blocks/kanban/).

## Team chat

A messenger with channels, direct messages, a message thread and a composer.

The code: [Chat block](https://albakore.github.io/pyzed/blocks/chat/). For a chat that streams answers from an AI model, see the
[AI chat](https://albakore.github.io/pyzed/showcase/apps/#ai-chat).

## Music player

A library sidebar, an album page with its track list, and a player bar whose progress moves while a song plays.

The code: [Music player block](https://albakore.github.io/pyzed/blocks/music-player/).

## IDE layout

The layout of a code editor: a project tree, editor tabs with breadcrumbs, a terminal, an outline panel and a
status bar.

The code: [File explorer block](https://albakore.github.io/pyzed/blocks/file-explorer/). A working editor on the same idea is the
[Workspace editor](https://albakore.github.io/pyzed/showcase/apps/#workspace-editor).

## Contacts over SQLite

A contacts app on [SQLAlchemy](https://www.sqlalchemy.org) 2.0 with **20,000 rows** in a virtual table. Searching and sorting run as SQL queries in a worker thread (`Command.perform`), so the window never waits
for the database.

**uv:**

```bash title="Windows, macOS and Linux"
cd examples/integrations/sqlalchemy_contacts
uv run main.py
```

**pip:**

```bash title="macOS / Linux"
cd examples/integrations/sqlalchemy_contacts
python3 -m venv .venv
source .venv/bin/activate
python -m pip install pyzed-ui "sqlalchemy>=2.0"
python main.py
```

```powershell title="Windows (PowerShell)"
cd examples\integrations\sqlalchemy_contacts
py -m venv .venv
.venv\Scripts\Activate.ps1
python -m pip install pyzed-ui "sqlalchemy>=2.0"
python main.py
```

**python:**

```bash title="With pyzed-ui and SQLAlchemy already installed"
cd examples/integrations/sqlalchemy_contacts
python main.py
```

<details>
<summary>Full code: <code>db.py</code> and <code>main.py</code></summary>
`examples/integrations/sqlalchemy_contacts/db.py`:

```py
"""The database: SQLAlchemy 2.0 models on SQLite, plus the queries the window runs."""

from __future__ import annotations

import random
from datetime import datetime
from pathlib import Path

from sqlalchemy import String, create_engine, func, or_, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column

DATABASE = Path(__file__).with_name("contacts.db")
engine = create_engine(f"sqlite:///{DATABASE}")

class Base(DeclarativeBase):
    pass

class Contact(Base):
    __tablename__ = "contacts"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(80), index=True)
    email: Mapped[str] = mapped_column(String(120), unique=True)
    company: Mapped[str] = mapped_column(String(80), default="")
    city: Mapped[str] = mapped_column(String(60), default="")
    created: Mapped[datetime] = mapped_column(default=datetime.now)

# Sorting by a column title of the table is an ORDER BY on this column.
COLUMNS = {"Name": Contact.name, "Email": Contact.email, "Company": Contact.company, "City": Contact.city}

def setup(rows: int = 20_000) -> None:
    """Creates the table and fills it with made-up contacts the first time."""
    Base.metadata.create_all(engine)
    with Session(engine) as session:
        if session.scalar(select(func.count()).select_from(Contact)):
            return
        rng = random.Random(7)
        first = ["Ada", "Alan", "Grace", "Linus", "Guido", "Margaret", "Ken", "Barbara", "Dennis", "Radia",
                 "Tim", "Frances", "Bjarne", "Anita", "John", "Hedy", "Yukihiro", "Sophie", "Brendan", "Katherine"]
        last = ["Lovelace", "Turing", "Hopper", "Torvalds", "van Rossum", "Hamilton", "Thompson", "Liskov",
                "Ritchie", "Perlman", "Berners-Lee", "Allen", "Stroustrup", "Borg", "McCarthy", "Lamarr",
                "Matsumoto", "Wilson", "Eich", "Johnson"]
        companies = ["Acme", "Globex", "Initech", "Umbrella", "Hooli", "Stark", "Wayne", "Tyrell", "Cyberdyne", "Aperture"]
        cities = ["Buenos Aires", "Madrid", "Mexico City", "Bogotá", "Lima", "Santiago", "Montevideo", "Barcelona",
                  "Berlin", "Tokyo", "Toronto", "Lisbon"]
        weights = [12, 10, 9, 7, 6, 5, 4, 4, 3, 3, 2, 2]
        session.add_all(
            Contact(name=f"{rng.choice(first)} {rng.choice(last)}", email=f"user{i}@example.com",
                    company=rng.choice(companies), city=rng.choices(cities, weights)[0])
            for i in range(rows)
        )
        session.commit()

def search(text: str, sort: str, descending: bool) -> list[Contact]:
    """Contacts whose name, email or company contains ``text``, sorted in SQL."""
    column = COLUMNS[sort]
    query = select(Contact).order_by(column.desc() if descending else column, Contact.id)
    if text:
        pattern = f"%{text}%"
        query = query.where(or_(Contact.name.ilike(pattern), Contact.email.ilike(pattern),
                                Contact.company.ilike(pattern)))
    with Session(engine) as session:
        return list(session.scalars(query))  # plain objects keep their loaded values after the session closes

def stats() -> dict:
    """Totals and the five cities with more contacts, for the header."""
    with Session(engine) as session:
        total = session.scalar(select(func.count()).select_from(Contact))
        companies = session.scalar(select(func.count(func.distinct(Contact.company))))
        cities = session.execute(select(Contact.city, func.count()).group_by(Contact.city)
                                 .order_by(func.count().desc()).limit(5)).all()
    return {"total": total, "companies": companies, "cities": [(city, count) for city, count in cities]}

def save(contact_id: int | None, values: dict) -> Contact | dict:
    """Inserts or updates a contact. Returns it, or ``{field: message}`` when it can't be saved."""
    errors = {}
    if not values["name"].strip():
        errors["name"] = "A contact needs a name"
    if "@" not in values["email"]:
        errors["email"] = "Not an email address"
    if errors:
        return errors
    with Session(engine, expire_on_commit=False) as session:
        contact = session.get(Contact, contact_id) if contact_id else Contact()
        for key, value in values.items():
            setattr(contact, key, value.strip())
        session.add(contact)
        try:
            session.commit()
        except IntegrityError:
            return {"email": "Another contact already uses this email"}
        return contact

def delete(contact_id: int) -> None:
    with Session(engine) as session:
        contact = session.get(Contact, contact_id)
        if contact is not None:
            session.delete(contact)
            session.commit()
```
`examples/integrations/sqlalchemy_contacts/main.py`:

```py
"""A contacts manager on SQLAlchemy 2.0: 20,000 rows in a virtual table, searched and sorted in SQL.

    uv run main.py

The database (contacts.db) is created next to this file on the first run.
Walkthrough: https://albakore.github.io/pyzed/integrations/sqlalchemy/
"""

from __future__ import annotations

import pyzed as pz
from pyzed import Command, ui

import db

FIELDS = [("name", "Name", "Ada Lovelace"), ("email", "Email", "ada@example.com"),
          ("company", "Company", "Analytical Engines"), ("city", "City", "London")]

class Contacts(pz.App):
    title = "Contacts · SQLAlchemy"
    size = (1180, 720)

    def __init__(self):
        self.rows: list[db.Contact] = []
        self.stats = {"total": 0, "companies": 0, "cities": []}
        self.query = ""
        self.sort, self.descending = "Name", False
        self.loading = True
        self.editing = None            # a Contact, "new" or None
        self.draft: dict[str, str] = {}
        self.errors: dict[str, str] = {}

    # -- database work runs in a thread with Command.perform ------------------------------
    def on_start(self):
        return Command.perform(db.setup, lambda _: self.reload())

    def reload(self):
        self.loading = True
        query, sort, descending = self.query, self.sort, self.descending
        return [
            Command.perform(lambda: db.search(query, sort, descending), lambda rows: self.loaded(query, rows)),
            Command.perform(db.stats, lambda stats: setattr(self, "stats", stats)),
        ]

    def loaded(self, query: str, rows: list[db.Contact]):
        if query == self.query:  # a newer search is on its way: keep waiting for it
            self.rows, self.loading = rows, False

    def search(self, text: str):
        self.query = text
        return self.reload()

    def sort_by(self, title: str):
        self.descending = not self.descending if title == self.sort else False
        self.sort = title
        return self.reload()

    # -- the edit panel ------------------------------------------------------------------
    def open(self, contact):
        self.editing, self.errors = contact, {}
        self.draft = {key: "" if contact == "new" else getattr(contact, key) for key, _, _ in FIELDS}

    def save(self):
        contact_id = None if self.editing == "new" else self.editing.id
        values = dict(self.draft)
        return Command.perform(lambda: db.save(contact_id, values), self.saved)

    def saved(self, result):
        if isinstance(result, dict):
            self.errors = result
            return None
        self.editing = None
        return self.reload()

    def delete(self):
        contact_id = self.editing.id
        self.editing = None
        return Command.perform(lambda: db.delete(contact_id), lambda _: self.reload())

    def on_key(self, combo):
        if combo == "escape":
            self.editing = None
        elif combo == "ctrl-n":
            self.open("new")

    # -- view ----------------------------------------------------------------------------
    def header(self):
        cities = self.stats["cities"]
        return pz.row([
            ui.stat("Contacts", f"{self.stats['total']:,}"),
            ui.stat("Companies", self.stats["companies"]),
            ui.card(pz.column([ui.muted("Top cities", size="sm"),
                               ui.bar_chart([count for _, count in cities], labels=[c.split()[0] for c, _ in cities],
                                            width=360, height=90)], spacing=6)),
        ], spacing=12)

    def panel(self, t):
        if self.editing is None:
            return None
        rows = [ui.field(title, ui.input(self.draft[key], lambda v, k=key: self.draft.__setitem__(k, v),
                                         placeholder=hint, width=220),
                         error=self.errors.get(key))
                for key, title, hint in FIELDS]
        new = self.editing == "new"
        return pz.container(
            pz.column([
                pz.row([ui.heading("New contact" if new else "Edit contact", level=3), pz.hspace(),
                        ui.icon_button("x", lambda: setattr(self, "editing", None), tooltip="Close (Esc)")],
                       align="center"),
                *rows,
                pz.row([None if new else ui.button("Delete", self.delete, icon="trash-2", variant="ghost", tint="error"),
                        pz.hspace(), ui.button("Save", self.save, icon="check", variant="solid")], spacing=8),
            ], spacing=14),
            padding=16, width=420, height="fill", bg=t.panel,
        )

    def view(self):
        t = self.resolved_theme()
        columns = [
            ui.Column("Name", "name", width="fill:3"),
            ui.Column("Email", "email", width="fill:3"),
            ui.Column("Company", "company", width="fill:2"),
            ui.Column("City", "city", width="fill:2"),
        ]
        toolbar = pz.row([
            ui.search_input(self.query, self.search, placeholder="Search by name, email or company…", width=360),
            ui.spinner() if self.loading else ui.muted(f"{len(self.rows):,} found", size="sm"),
            pz.hspace(),
            ui.button("New contact", lambda: self.open("new"), icon="user-plus", keys="ctrl-n"),
        ], spacing=10, align="center")
        # Rows come sorted from SQL; sort_by only draws the arrow (re-sorting a sorted list is cheap).
        table = ui.table(columns, self.rows, sort_by=self.sort, descending=self.descending, on_sort=self.sort_by,
                         on_row_press=self.open, selected=self.editing, striped=True, height="fill")
        main = pz.column([self.header(), toolbar, table], spacing=14, padding=16)
        return pz.container(pz.row([main, self.panel(t)]), width="fill", height="fill", bg=t.editor)

if __name__ == "__main__":
    pz.run(Contacts)
```
</details>

The walkthrough is in [SQLAlchemy](https://albakore.github.io/pyzed/integrations/sqlalchemy/). Without any library at all, the
[Connecting a backend](https://albakore.github.io/pyzed/tutorials/advanced/backend/) tutorial builds a smaller contacts app over the
`sqlite3` module that comes with Python, with a repository class, a spinner and friendly errors:

## More

- [Live data](https://albakore.github.io/pyzed/showcase/apps/#live-data): a price ticker fed by an async generator.
- [Glass control center](https://albakore.github.io/pyzed/showcase/glass-dashboard/): a dashboard with frosted-glass cards over a moving shader.
- [Integrations](https://albakore.github.io/pyzed/integrations/overview/): FastAPI, Django, SQLModel, httpx and more, each with a desktop app.

**About this page:**

The blocks are regenerated from `examples/blocks` and drawn in both themes by the docs tooling, so the captures
match the current code.
