Skip to content

Table

Table shows one row per kept event, with server-side pagination and a button per row for each of its actions.

import dachshund
from dachshund.widgets import Table

with dachshund.row():
    dachshund.add(Table("sessions", columns=["user", "ip"], key="user"))
dachshund.emit("sessions", user="ada", ip="10.0.0.1")

dachshund.widgets.table.table.Table dataclass

Table(name, *, source='', title=None, actions=list(), action_args=dict(), persist=False, columns, key=None, page_size=10, loader=None, search=True, sortable=None)

Bases: KeyedWidget

A searchable, sortable table, one row per kept event.

Example

with dachshund.row():
    dachshund.add(Table("sessions", columns=["user", "ip"], key="user"))
dachshund.emit("sessions", user="ada", ip="10.0.0.1")

Options

  • columns (list[str]) - The payload fields shown, left to right. Required.
  • key (str) - The field identifying a row across events. An event whose key matches a kept row replaces it. Defaults to None, every event is a new row.
  • page_size (int) - Rows per page. Defaults to 10.
  • search (bool) - Shows a search box. Defaults to True.
  • sortable (list[str]) - Columns sorted by clicking their header. Defaults to every shown column.
  • loader (Loader) - Fetches rows from data dachshund doesn't own, see Loader. Defaults to None.
  • persist (bool) - Keeps rows across restarts. Needs key and the dashboard's storage. Can't be combined with loader. Defaults to False.
  • actions (list[str]) - Actions shown as a button on every row. Defaults to [].

source, title and action_args are common to every widget, see Widget.

Row fields

A field starting with _ is hidden from the table and passed to the row's actions without the underscore:

@dachshund.action("ban", label="Ban", confirm=True)
def ban(ctx, user: str, team_id: str) -> None: ...


with dachshund.row():
    dachshund.add(Table("sessions", columns=["user"], key="user", actions=["ban"]))
dachshund.emit("sessions", user="ada", _team_id="42")

A truthy _delete field removes the row with that key.

Loader

A function of (ctx, query), sync or async, returning (rows, total). query is a TableQuery: apply its search, sort, offset and limit, and return that page. total is the number of rows matching the search, or None when counting them is too costly. It's called every time a page is shown.

def load_users(ctx, query):
    users = ctx["db"].users(search=query.search, order_by=query.sort_by)
    page = users[query.offset : query.offset + query.limit]

    return page, len(users)


with dachshund.row():
    dachshund.add(Table("users", columns=["name", "email"], loader=load_users))

Pagination

Pages move by offset, showing "11-20 of 412". Without a total, the next page is offered while a full page comes back. A live update refreshes the page shown, so rows can shift across pages as they change.

columns instance-attribute

columns

key class-attribute instance-attribute

key = None

page_size class-attribute instance-attribute

page_size = 10

loader class-attribute instance-attribute

loader = None

search class-attribute instance-attribute

search = True

sortable class-attribute instance-attribute

sortable = field(default=None)

type class-attribute

type = 'table'

op class-attribute

op = 'replace'

renders_own_actions class-attribute

renders_own_actions = True

paginated class-attribute

paginated = True

script class-attribute

script = Path(__file__).parent / 'table.js'

style class-attribute

style = Path(__file__).parent / 'table.css'

shown property

shown

The columns drawn, hidden _ ones left out.

sortable_columns property

sortable_columns

The columns whose header sorts the table.

render

render(events)

Rebuild every row from the events kept for this source.

An event carrying key replaces the row it matches; one that doesn't, or carries no value for it, is always its own row.

A persisting table reads its own kept rows instead, since those cover more than whatever events are still around to rebuild from.

Source code in dachshund/widgets/table/table.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
def render(self, events: Sequence[Event]) -> dict[str, Any]:
    """Rebuild every row from the events kept for this source.

    An event carrying `key` replaces the row it matches; one that
    doesn't, or carries no value for it, is always its own row.

    A persisting table reads its own kept rows instead, since those
    cover more than whatever events are still around to rebuild from.
    """
    if self.persist:
        rows = [{"fields": fields} for fields in self.state.entries.values()]
    else:
        rows = []
        by_key: dict[Any, int] = {}

        for event in events:
            fields = _fields(event.payload)

            if self.key is not None and self.key in fields:
                row_key = fields[self.key]

                if row_key in by_key:
                    rows[by_key[row_key]] = {"fields": fields}
                    continue

                by_key[row_key] = len(rows)

            rows.append({"fields": fields})

    return {"columns": self.shown, "rows": rows}

query

query(rows, query)

rows matching query's search, in its order. No page is cut.

Source code in dachshund/widgets/table/table.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
def query(
    self, rows: list[dict[str, Any]], query: TableQuery
) -> list[dict[str, Any]]:
    """`rows` matching `query`'s search, in its order. No page is cut."""
    if query.search:
        needle = query.search.lower()
        rows = [
            row
            for row in rows
            if any(
                needle in str(row["fields"].get(column, "")).lower()
                for column in self.shown
            )
        ]

    if query.sort_by in self.sortable_columns:
        column = query.sort_by
        present = [row for row in rows if row["fields"].get(column) is not None]
        missing = [row for row in rows if row["fields"].get(column) is None]
        present.sort(key=lambda row: _sort_key(row["fields"][column]))

        if query.descending:
            present.reverse()

        rows = present + missing

    return rows

page async

page(events, query, context, host_loop=None)

One page of this table's rows, as query asks for.

A loader answers directly, sync or async; see run().

Source code in dachshund/widgets/table/table.py
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
async def page(
    self,
    events: Sequence[Event],
    query: TableQuery,
    context: Context,
    host_loop: asyncio.AbstractEventLoop | None = None,
) -> dict[str, Any]:
    """One page of this table's rows, as `query` asks for.

    A `loader` answers directly, sync or async; see `run()`.
    """
    limit = query.limit or self.page_size
    query = replace(query, limit=limit)

    if self.loader is not None:
        loaded, total = await run(self.loader, context, query, host_loop=host_loop)
        rows = [{"fields": _fields(row)} for row in loaded]
    else:
        matching = self.query(self.render(events)["rows"], query)
        total = len(matching)
        rows = matching[query.offset : query.offset + limit]

    return {
        "columns": self.shown,
        "rows": rows,
        "total": total,
        "offset": query.offset,
        "limit": limit,
        "sortable": self.sortable_columns,
        "search": self.search,
        "sort_by": query.sort_by,
        "descending": query.descending,
    }

accumulate

accumulate(event)

Fold one more event into the kept rows.

A truthy _delete removes the row instead of updating it.

Source code in dachshund/widgets/table/table.py
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
def accumulate(self, event: Event) -> None:
    """Fold one more event into the kept rows.

    A truthy `_delete` removes the row instead of updating it.
    """
    if self.key is None:
        return

    if self.key not in event.payload:
        self.warn(self.key, None)
        return

    row_key = event.payload[self.key]

    if event.payload.get("_delete"):
        self.state.remove(row_key)
        return

    self.state.upsert(row_key, _fields(event.payload), event.at.timestamp())

dachshund.widgets.query.TableQuery dataclass

TableQuery(search=None, sort_by=None, descending=False, offset=0, limit=None, levels=frozenset())

What a table asks of its rows: a search, a sort and a page.

Options

  • search (str) - Text typed in the search box. Defaults to None.
  • sort_by (str) - The column sorted by. Defaults to None, unsorted.
  • descending (bool) - Sorts highest first. Defaults to False.
  • offset (int) - Rows skipped before the page. Defaults to 0.
  • limit (int) - Rows in the page. Defaults to None, the table's page_size.
  • levels (frozenset[int]) - The log levels a Logs widget shows. Defaults to empty, every level it captures.

search class-attribute instance-attribute

search = None

sort_by class-attribute instance-attribute

sort_by = None

descending class-attribute instance-attribute

descending = False

offset class-attribute instance-attribute

offset = 0

limit class-attribute instance-attribute

limit = None

levels class-attribute instance-attribute

levels = frozenset()