From 0803617e62935c27095b30f21252c51cbeb23a66 Mon Sep 17 00:00:00 2001 From: Abel Luck Date: Mon, 30 Mar 2026 15:28:56 +0200 Subject: [PATCH] add empty table placeholders --- repub/components.py | 16 +++++++++++++--- repub/pages/dashboard.py | 16 +++++++++++++--- repub/pages/runs.py | 3 +++ repub/pages/sources.py | 1 + tests/test_web.py | 30 ++++++++++++++++++++++++++++++ 5 files changed, 60 insertions(+), 6 deletions(-) diff --git a/repub/components.py b/repub/components.py index 4113244..6ecf837 100644 --- a/repub/components.py +++ b/repub/components.py @@ -190,6 +190,7 @@ def table_section( eyebrow: str | None = None, title: str, subtitle: str | None = None, + empty_message: str, headers: tuple[str, ...], rows: tuple[tuple[Node, ...], ...], actions: Node | None = None, @@ -208,6 +209,17 @@ def table_section( ), ] + body_rows: Node + if rows: + body_rows = (render_row(row) for row in rows) + else: + body_rows = h.tr[ + h.td( + colspan=str(len(headers)), + class_="px-4 py-8 text-center text-sm text-slate-500 sm:px-6", + )[empty_message] + ] + return h.section[ h.div(class_="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between")[ h.div[ @@ -238,9 +250,7 @@ def table_section( ) ] ], - h.tbody(class_="divide-y divide-slate-200 bg-white")[ - (render_row(row) for row in rows) - ], + h.tbody(class_="divide-y divide-slate-200 bg-white")[body_rows], ] ] ], diff --git a/repub/pages/dashboard.py b/repub/pages/dashboard.py index e0f841a..8f61b53 100644 --- a/repub/pages/dashboard.py +++ b/repub/pages/dashboard.py @@ -143,6 +143,17 @@ def running_executions_table( ), ] + body_rows: Node + if rows: + body_rows = (render_row(row) for row in rows) + else: + body_rows = h.tr[ + h.td( + colspan=str(len(headers)), + class_="px-4 py-8 text-center text-sm text-slate-500", + )["No job executions are running."] + ] + return h.section[ h.div(class_="mb-3 flex items-end justify-between gap-4")[ h.div[ @@ -173,9 +184,7 @@ def running_executions_table( ) ] ], - h.tbody(class_="divide-y divide-slate-200 bg-white")[ - (render_row(row) for row in rows) - ], + h.tbody(class_="divide-y divide-slate-200 bg-white")[body_rows], ] ] ], @@ -225,6 +234,7 @@ def published_feeds_table( return table_section( eyebrow="Published feeds", title="Published feeds", + empty_message="No feeds have been published yet.", headers=("Source", "Feed URL", "Status", "Last updated", "Disk usage"), rows=rows, actions=muted_action_link(href="/sources", label="Manage sources"), diff --git a/repub/pages/runs.py b/repub/pages/runs.py index 0b911f3..a42c751 100644 --- a/repub/pages/runs.py +++ b/repub/pages/runs.py @@ -211,6 +211,7 @@ def runs_page( table_section( eyebrow="Live work", title="Running job executions", + empty_message="No job executions are running.", headers=( "Source", "Execution", @@ -224,6 +225,7 @@ def runs_page( table_section( eyebrow="Queue", title="Upcoming jobs", + empty_message="No jobs are scheduled.", headers=( "Source", "Next run", @@ -237,6 +239,7 @@ def runs_page( table_section( eyebrow="History", title="Completed job executions", + empty_message="No job executions have completed yet.", headers=( "Source", "Execution", diff --git a/repub/pages/sources.py b/repub/pages/sources.py index dd67691..26e4a5e 100644 --- a/repub/pages/sources.py +++ b/repub/pages/sources.py @@ -92,6 +92,7 @@ def sources_table( return table_section( eyebrow="Inventory", title="Sources", + empty_message="No sources yet.", headers=("Source", "Type", "Upstream", "Schedule", "Job state", "Actions"), rows=rows, actions=header_action_link(href="/sources/create", label="Create source"), diff --git a/tests/test_web.py b/tests/test_web.py index 9035c59..e668543 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -215,6 +215,20 @@ def test_render_dashboard_shows_dashboard_information_architecture( asyncio.run(run()) +def test_render_dashboard_shows_empty_state_rows(monkeypatch, tmp_path: Path) -> None: + db_path = tmp_path / "dashboard-empty.db" + monkeypatch.setenv("REPUBLISHER_DB_PATH", str(db_path)) + + async def run() -> None: + app = create_app() + body = str(await render_dashboard(app)) + + assert "No job executions are running." in body + assert "No feeds have been published yet." in body + + asyncio.run(run()) + + def test_load_dashboard_view_measures_log_artifact_path( monkeypatch, tmp_path: Path ) -> None: @@ -390,6 +404,7 @@ def test_render_sources_shows_table_and_create_link() -> None: assert ">Sources<" in body assert 'href="/sources/create"' in body + assert "No sources yet." in body assert "guardian-feed" not in body assert "podcast-audio" not in body @@ -840,6 +855,21 @@ def test_render_runs_shows_running_upcoming_and_completed_tables( asyncio.run(run()) +def test_render_runs_shows_empty_state_rows(monkeypatch, tmp_path: Path) -> None: + db_path = tmp_path / "runs-empty.db" + monkeypatch.setenv("REPUBLISHER_DB_PATH", str(db_path)) + + async def run() -> None: + app = create_app() + body = str(await render_runs(app)) + + assert body.count("No job executions are running.") == 1 + assert "No jobs are scheduled." in body + assert "No job executions have completed yet." in body + + asyncio.run(run()) + + def test_render_execution_logs_uses_app_route(monkeypatch, tmp_path: Path) -> None: db_path = tmp_path / "logs-render.db" monkeypatch.setenv("REPUBLISHER_DB_PATH", str(db_path))