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:
@@ -36,7 +36,7 @@ def _make_config(
|
||||
tmp_path: Path,
|
||||
*,
|
||||
gitea_url: str | None = "https://gitea.example",
|
||||
limit: int = 15,
|
||||
limit: int = 10,
|
||||
) -> Config:
|
||||
return Config(
|
||||
paths=Paths(
|
||||
@@ -190,7 +190,7 @@ def test_admin_payload_parsed_into_executions(
|
||||
# Bearer auth against the admin endpoint, with the configured limit.
|
||||
assert calls == [
|
||||
(
|
||||
"https://gitea.example/api/v1/admin/actions/runs?limit=15",
|
||||
"https://gitea.example/api/v1/admin/actions/runs?limit=10",
|
||||
"Bearer tok",
|
||||
)
|
||||
]
|
||||
@@ -398,6 +398,57 @@ def test_non_list_runs_payload_is_an_error(
|
||||
assert view.source == "none"
|
||||
|
||||
|
||||
# ── ordering ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_runs_are_sorted_newest_first(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Gitea's order is not contracted -- the card sorts by recency itself."""
|
||||
payload = [
|
||||
_run(run_id=1, number=1, started="2026-07-25T09:00:00Z"),
|
||||
_run(run_id=3, number=3, started="2026-07-25T11:00:00Z"),
|
||||
_run(run_id=2, number=2, started="2026-07-25T10:00:00Z"),
|
||||
]
|
||||
_install_urlopen(monkeypatch, _admin_ok(payload))
|
||||
|
||||
view = list_executions(_make_config(tmp_path), [], token="tok", now=_NOW)
|
||||
|
||||
assert [r.number for r in view.runs] == [3, 2, 1]
|
||||
|
||||
|
||||
def test_queued_run_sorts_above_started_runs(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""A run with no start time was just created: it is the newest of all."""
|
||||
payload = [
|
||||
_run(run_id=1, number=1, started="2026-07-25T11:00:00Z"),
|
||||
_run(run_id=2, number=2, status="waiting", conclusion="", started=None, completed=None),
|
||||
]
|
||||
_install_urlopen(monkeypatch, _admin_ok(payload))
|
||||
|
||||
view = list_executions(_make_config(tmp_path), [], token="tok", now=_NOW)
|
||||
|
||||
assert [r.status for r in view.runs] == ["queued", "success"]
|
||||
|
||||
|
||||
def test_same_timestamp_falls_back_to_run_id(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Matrix runs share a timestamp; ids keep the order stable, not arbitrary."""
|
||||
stamp = "2026-07-25T11:00:00Z"
|
||||
payload = [
|
||||
_run(run_id=10, number=10, started=stamp),
|
||||
_run(run_id=12, number=12, started=stamp),
|
||||
_run(run_id=11, number=11, started=stamp),
|
||||
]
|
||||
_install_urlopen(monkeypatch, _admin_ok(payload))
|
||||
|
||||
view = list_executions(_make_config(tmp_path), [], token="tok", now=_NOW)
|
||||
|
||||
assert [r.number for r in view.runs] == [12, 11, 10]
|
||||
|
||||
|
||||
# ── endpoint fallback ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -418,8 +469,8 @@ def test_admin_denied_falls_back_to_user_endpoint(
|
||||
assert view.error is None
|
||||
assert len(view.runs) == 1
|
||||
assert [url for url, _auth in calls] == [
|
||||
"https://gitea.example/api/v1/admin/actions/runs?limit=15",
|
||||
"https://gitea.example/api/v1/user/actions/runs?limit=15",
|
||||
"https://gitea.example/api/v1/admin/actions/runs?limit=10",
|
||||
"https://gitea.example/api/v1/user/actions/runs?limit=10",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user