feat(web): row-count selector on the Executions card

Adds a "Show" selector (10 / 25 / 50 / 100) to the card header and drops the
default row count from 15 to 10. The chosen value rides on the card's own
polling URL (`/executions?limit=N`), so it survives the 5s htmx refresh with no
client-side state; a config default outside those four is rendered as an extra
option instead of disappearing. The query is parsed permissively — a stale or
hand-edited polling URL clamps or falls back to the config default rather than
422-ing a card that would otherwise render.

Ordering is now explicit rather than inherited from whatever Gitea returns:
newest first, a not-yet-started (queued) run above everything, ties broken on
Gitea's run id so matrix runs sharing a timestamp keep a stable order.
This commit is contained in:
2026-07-25 15:36:21 +02:00
parent 30fd3120a6
commit 7d1f4e2784
9 changed files with 280 additions and 22 deletions
+116
View File
@@ -352,6 +352,122 @@ def test_end_to_end_against_a_stubbed_gitea(
assert "no run" not in resp.text
# ── row-count selector ──────────────────────────────────────────────────────
def _stub_limits(monkeypatch: pytest.MonkeyPatch) -> list[int | None]:
"""Replace the operations call; return the log of limits it was given."""
limits: list[int | None] = []
def fake_list_executions(
_config: Config,
_vms: list[LiveVm],
*,
token: str | None,
limit: int | None = None,
**_kw: Any,
) -> ExecutionsView:
limits.append(limit)
return ExecutionsView(runs=[], unmatched_vms=[], source="admin", error=None)
monkeypatch.setattr(app_module, "list_executions", fake_list_executions)
return limits
def test_default_limit_is_ten(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
limits = _stub_limits(monkeypatch)
_install_fake_keyring(monkeypatch)
app = create_app(_make_config(tmp_path, token_cred=_PAT_CRED_NAME))
app.dependency_overrides[get_backend] = lambda: _FakeBackend([])
client = TestClient(app)
resp = client.get("/executions")
assert limits == [10]
assert '<option value="10" selected>10</option>' in resp.text
assert "newest first" in resp.text
def test_selector_offers_the_choices_and_marks_the_current_one(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
limits = _stub_limits(monkeypatch)
_install_fake_keyring(monkeypatch)
app = create_app(_make_config(tmp_path, token_cred=_PAT_CRED_NAME))
app.dependency_overrides[get_backend] = lambda: _FakeBackend([])
client = TestClient(app)
resp = client.get("/executions?limit=50")
assert limits == [50]
for choice in (10, 25, 50, 100):
assert f'value="{choice}"' in resp.text
assert '<option value="50" selected>50</option>' in resp.text
# The re-rendered card polls with the chosen limit, so it survives refreshes.
assert 'hx-get="/executions?limit=50"' in resp.text
def test_config_default_outside_the_choices_still_appears(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
_stub_limits(monkeypatch)
_install_fake_keyring(monkeypatch)
config = _make_config(tmp_path, token_cred=_PAT_CRED_NAME)
object.__setattr__(config.web, "executions_limit", 7)
app = create_app(config)
app.dependency_overrides[get_backend] = lambda: _FakeBackend([])
client = TestClient(app)
resp = client.get("/executions")
assert '<option value="7" selected>7</option>' in resp.text
@pytest.mark.parametrize(
("query", "expected"),
[
("?limit=25", 25),
("?limit=7", 7), # not offered, but honoured
("?limit=0", 1), # clamped up
("?limit=9999", 100), # clamped down
("?limit=-5", 1),
("?limit=abc", 10), # unparseable -> config default
("?limit=", 10),
("", 10),
],
)
def test_limit_query_is_parsed_permissively(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, query: str, expected: int
) -> None:
"""A stale or hand-edited polling URL must never fail the card."""
limits = _stub_limits(monkeypatch)
_install_fake_keyring(monkeypatch)
app = create_app(_make_config(tmp_path, token_cred=_PAT_CRED_NAME))
app.dependency_overrides[get_backend] = lambda: _FakeBackend([])
client = TestClient(app)
resp = client.get(f"/executions{query}")
assert resp.status_code == 200
assert limits == [expected]
def test_dashboard_renders_the_selector_at_the_config_default(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
_stub_limits(monkeypatch)
_install_fake_keyring(monkeypatch)
_quiet_host(monkeypatch)
app = create_app(_make_config(tmp_path, token_cred=_PAT_CRED_NAME))
app.dependency_overrides[get_backend] = lambda: _FakeBackend([])
client = TestClient(app)
resp = client.get("/")
assert resp.status_code == 200
assert 'hx-get="/executions?limit=10"' in resp.text
# ── 2. degraded states ──────────────────────────────────────────────────────