generated from john/python-template
jobs table
This commit is contained in:
@@ -1,55 +0,0 @@
|
|||||||
"""Reusable jobs table rendering helpers."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from collections.abc import Sequence
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from nicegui import ui
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class JobTableRow:
|
|
||||||
"""Read model consumed by the shared jobs table component."""
|
|
||||||
|
|
||||||
id: UUID
|
|
||||||
status: str
|
|
||||||
created_at: str
|
|
||||||
updated_at: str
|
|
||||||
|
|
||||||
|
|
||||||
def _serialize_rows(rows: Sequence[JobTableRow]) -> list[dict[str, str]]:
|
|
||||||
"""Convert typed rows into table-compatible dictionaries."""
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"id": str(row.id),
|
|
||||||
"status": row.status,
|
|
||||||
"created_at": row.created_at,
|
|
||||||
"updated_at": row.updated_at,
|
|
||||||
}
|
|
||||||
for row in rows
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def render_jobs_table(rows: Sequence[JobTableRow]) -> None:
|
|
||||||
"""Render jobs table and per-row detail links."""
|
|
||||||
if not rows:
|
|
||||||
ui.label("No jobs yet.")
|
|
||||||
return
|
|
||||||
|
|
||||||
serialized_rows = _serialize_rows(rows)
|
|
||||||
ui.table(
|
|
||||||
columns=[
|
|
||||||
{"name": "id", "label": "Job ID", "field": "id"},
|
|
||||||
{"name": "status", "label": "Status", "field": "status"},
|
|
||||||
{"name": "created_at", "label": "Created", "field": "created_at"},
|
|
||||||
{"name": "updated_at", "label": "Updated", "field": "updated_at"},
|
|
||||||
],
|
|
||||||
rows=serialized_rows,
|
|
||||||
row_key="id",
|
|
||||||
).classes("w-full")
|
|
||||||
|
|
||||||
with ui.column().classes("gap-1"):
|
|
||||||
for row in serialized_rows:
|
|
||||||
ui.link(f"Open {row['id']}", f"/jobs/{row['id']}")
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
from .jobs import JobTableRow
|
||||||
|
from .jobs import render_jobs_table
|
||||||
|
|
||||||
|
__all__ = ["JobTableRow", "render_jobs_table"]
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"""Common logic for generating table widgets."""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from nicegui import events
|
||||||
|
from nicegui import ui
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_row_id(args: Any) -> str | None:
|
||||||
|
if isinstance(args, dict):
|
||||||
|
if isinstance(args.get("row"), dict):
|
||||||
|
row_id = args["row"].get("id")
|
||||||
|
return str(row_id) if row_id is not None else None
|
||||||
|
row_id = args.get("id")
|
||||||
|
return str(row_id) if row_id is not None else None
|
||||||
|
|
||||||
|
if isinstance(args, list):
|
||||||
|
for value in args:
|
||||||
|
if isinstance(value, dict):
|
||||||
|
row_id = value.get("id")
|
||||||
|
if row_id is not None:
|
||||||
|
return str(row_id)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _bind_row_click_handler(
|
||||||
|
table: Any,
|
||||||
|
*,
|
||||||
|
on_row_click_id: Callable[[str], None],
|
||||||
|
) -> None:
|
||||||
|
def handle_row_click(event: events.GenericEventArguments) -> None:
|
||||||
|
row_id = _extract_row_id(event.args)
|
||||||
|
if row_id is None:
|
||||||
|
return
|
||||||
|
on_row_click_id(row_id)
|
||||||
|
|
||||||
|
table.on("rowClick", handle_row_click)
|
||||||
|
|
||||||
|
|
||||||
|
def build_table(
|
||||||
|
rows: list[dict[str, Any]],
|
||||||
|
columns: list[dict[str, Any]],
|
||||||
|
*,
|
||||||
|
default_sort_by: str | None = None,
|
||||||
|
default_descending: bool = False,
|
||||||
|
classes: str = "app-table",
|
||||||
|
on_row_click_id: Callable[[str], None] | None = None,
|
||||||
|
) -> Any:
|
||||||
|
pagination: dict[str, Any] = {"rowsPerPage": 25}
|
||||||
|
if default_sort_by is not None:
|
||||||
|
pagination["sortBy"] = default_sort_by
|
||||||
|
pagination["descending"] = default_descending
|
||||||
|
|
||||||
|
table = (
|
||||||
|
ui.table(
|
||||||
|
rows=rows,
|
||||||
|
columns=columns,
|
||||||
|
row_key="id",
|
||||||
|
pagination=pagination,
|
||||||
|
)
|
||||||
|
.classes(classes)
|
||||||
|
.props('table-style="table-layout: fixed; width: 100%;"')
|
||||||
|
)
|
||||||
|
if on_row_click_id is not None:
|
||||||
|
_bind_row_click_handler(table, on_row_click_id=on_row_click_id)
|
||||||
|
return table
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
"""Jobs table rendering helpers."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import UTC
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from nicegui import ui
|
||||||
|
|
||||||
|
from .common import build_table
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class JobTableRow:
|
||||||
|
"""Read model consumed by the jobs table component."""
|
||||||
|
|
||||||
|
id: UUID
|
||||||
|
status: str
|
||||||
|
filename: str
|
||||||
|
retry_count: int
|
||||||
|
created_at: str
|
||||||
|
updated_at: str
|
||||||
|
|
||||||
|
|
||||||
|
def _format_timestamp(value: str) -> str:
|
||||||
|
"""Return a friendly UTC timestamp for table display."""
|
||||||
|
try:
|
||||||
|
parsed = datetime.fromisoformat(value)
|
||||||
|
except ValueError:
|
||||||
|
return value
|
||||||
|
parsed = parsed.replace(tzinfo=UTC) if parsed.tzinfo is None else parsed.astimezone(UTC)
|
||||||
|
return parsed.astimezone().strftime("%b %d, %I:%M %p")
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_rows(rows: Sequence[JobTableRow]) -> list[dict[str, Any]]:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"id": str(row.id),
|
||||||
|
"status": row.status,
|
||||||
|
"filename": row.filename,
|
||||||
|
"retry_count": row.retry_count,
|
||||||
|
"created_at": _format_timestamp(row.created_at),
|
||||||
|
"updated_at": _format_timestamp(row.updated_at),
|
||||||
|
"created_sort": row.created_at,
|
||||||
|
"updated_sort": row.updated_at,
|
||||||
|
}
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def render_jobs_table(rows: Sequence[JobTableRow]) -> None:
|
||||||
|
"""Render jobs table and open a detail page when clicking a row."""
|
||||||
|
if not rows:
|
||||||
|
ui.label("No jobs yet.")
|
||||||
|
return
|
||||||
|
|
||||||
|
build_table(
|
||||||
|
rows=_serialize_rows(rows),
|
||||||
|
columns=[
|
||||||
|
{"name": "id", "label": "Job ID", "field": "id", "sortable": True},
|
||||||
|
{"name": "status", "label": "Status", "field": "status", "sortable": True},
|
||||||
|
{"name": "filename", "label": "Filename", "field": "filename", "sortable": True},
|
||||||
|
{"name": "retry_count", "label": "Retries", "field": "retry_count", "sortable": True},
|
||||||
|
{"name": "created_at", "label": "Created", "field": "created_at", "sortable": True},
|
||||||
|
{"name": "updated_at", "label": "Updated", "field": "updated_at", "sortable": True},
|
||||||
|
],
|
||||||
|
default_sort_by="created_sort",
|
||||||
|
default_descending=True,
|
||||||
|
classes="app-table w-full",
|
||||||
|
on_row_click_id=lambda job_id: ui.navigate.to(f"/jobs/{job_id}"),
|
||||||
|
)
|
||||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from nicegui import ui
|
from nicegui import ui
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
from sqlmodel import desc
|
from sqlmodel import desc
|
||||||
from sqlmodel import select
|
from sqlmodel import select
|
||||||
|
|
||||||
@@ -15,18 +16,26 @@ from transcription.models import Transcript
|
|||||||
from transcription.ui.components.error_presenter import show_error
|
from transcription.ui.components.error_presenter import show_error
|
||||||
from transcription.ui.components.error_presenter import summarize_error
|
from transcription.ui.components.error_presenter import summarize_error
|
||||||
from transcription.ui.components.job_detail import render_job_detail
|
from transcription.ui.components.job_detail import render_job_detail
|
||||||
from transcription.ui.components.job_table import JobTableRow
|
from transcription.ui.components.table.jobs import JobTableRow
|
||||||
from transcription.ui.components.job_table import render_jobs_table
|
from transcription.ui.components.table.jobs import render_jobs_table
|
||||||
|
|
||||||
|
|
||||||
async def fetch_jobs() -> list[JobTableRow]:
|
async def fetch_job_rows() -> list[JobTableRow]:
|
||||||
"""Return jobs for display in most-recent-first order."""
|
"""Return jobs for display in most-recent-first order."""
|
||||||
async with get_session() as session:
|
async with get_session() as session:
|
||||||
jobs = (await session.exec(select(Job).order_by(desc(Job.created_at)))).all()
|
jobs = (
|
||||||
|
await session.exec(
|
||||||
|
select(Job)
|
||||||
|
.options(selectinload(Job.document)) # pyright: ignore[reportArgumentType]
|
||||||
|
.order_by(desc(Job.created_at))
|
||||||
|
)
|
||||||
|
).all()
|
||||||
return [
|
return [
|
||||||
JobTableRow(
|
JobTableRow(
|
||||||
id=job.id,
|
id=job.id,
|
||||||
status=job.status.value,
|
status=job.status.value,
|
||||||
|
filename=job.filename,
|
||||||
|
retry_count=job.retry_count,
|
||||||
created_at=job.created_at.isoformat(),
|
created_at=job.created_at.isoformat(),
|
||||||
updated_at=job.updated_at.isoformat(),
|
updated_at=job.updated_at.isoformat(),
|
||||||
)
|
)
|
||||||
@@ -55,7 +64,7 @@ def register_page() -> None:
|
|||||||
|
|
||||||
@ui.refreshable
|
@ui.refreshable
|
||||||
async def render_table() -> None:
|
async def render_table() -> None:
|
||||||
jobs = await fetch_jobs()
|
jobs = await fetch_job_rows()
|
||||||
render_jobs_table(jobs)
|
render_jobs_table(jobs)
|
||||||
|
|
||||||
async def refresh() -> None:
|
async def refresh() -> None:
|
||||||
@@ -69,7 +78,7 @@ def register_page() -> None:
|
|||||||
|
|
||||||
ui.button("Refresh", on_click=refresh)
|
ui.button("Refresh", on_click=refresh)
|
||||||
await render_table()
|
await render_table()
|
||||||
ui.link("Back to upload", "/")
|
ui.link("Back to upload", "/upload")
|
||||||
|
|
||||||
@ui.page("/jobs/{job_id}")
|
@ui.page("/jobs/{job_id}")
|
||||||
async def job_detail_page(job_id: str) -> None:
|
async def job_detail_page(job_id: str) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user