#5211 "Lists load as you scroll: audit, alert history, Ansible runs" (milestone 477), per preference 172. - core/paging.py: keyset paging over (time, id), newest first, with an opaque "<iso>|<id>" cursor; a page reads one row past itself to know whether another follows. No OFFSET, so rows added while someone scrolls never shift the pages after them. - _macros.html scroll_more / scroll_end and static/scroll-more.js, written once: an IntersectionObserver with a 600px margin fetches the next page before the reader reaches the end; "Loading more…" while in flight, "End of the list." at the end, and a failed page says so with a Retry button instead of re-firing on every scroll. - Audit log: no "Load more" link and no 500-row cap. - Alert history: no 200-row stop with "pick a shorter range"; alert_svc.history takes the cursor, so the MCP tool and the page share one query. - Ansible runs: no silent cap of 50. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
35 lines
1.7 KiB
Python
35 lines
1.7 KiB
Python
"""Keyset paging for lists that load as you scroll (#5211): a cursor round-trips,
|
|
a mangled one starts over, and a page says whether another follows."""
|
|
from datetime import datetime, timezone
|
|
from types import SimpleNamespace as NS
|
|
|
|
from sqlalchemy import Column, DateTime, MetaData, String, Table, select
|
|
from sqlalchemy.dialects import postgresql
|
|
|
|
from steward.core.paging import cursor, keyset, next_page, parse_cursor
|
|
|
|
AT = datetime(2026, 10, 7, 3, 0, 5, 123456, tzinfo=timezone.utc)
|
|
|
|
|
|
def test_a_cursor_round_trips_and_a_mangled_one_starts_over():
|
|
assert parse_cursor(cursor(AT, "0190ab|cd")) == (AT, "0190ab|cd") # an id may hold the separator
|
|
assert parse_cursor(None) is None and parse_cursor("") is None
|
|
assert parse_cursor("not a cursor") is None and parse_cursor("yesterday|x") is None
|
|
|
|
|
|
def test_a_page_says_whether_another_follows():
|
|
rows = [NS(at=AT, id=str(i)) for i in range(5)]
|
|
page, after = next_page(rows, lambda r: (r.at, r.id), size=3)
|
|
assert [r.id for r in page] == ["0", "1", "2"] and after == cursor(AT, "2")
|
|
page, after = next_page(rows[:3], lambda r: (r.at, r.id), size=3)
|
|
assert len(page) == 3 and after is None # exactly a page: the end
|
|
|
|
|
|
def test_the_query_reads_one_row_past_the_page_older_than_the_cursor():
|
|
t = Table("t", MetaData(), Column("at", DateTime(timezone=True)), Column("id", String))
|
|
first = str(keyset(select(t), t.c.at, t.c.id, None, size=10).compile(dialect=postgresql.dialect()))
|
|
assert "WHERE" not in first and "ORDER BY t.at DESC, t.id DESC" in first and "LIMIT" in first
|
|
later = str(keyset(select(t), t.c.at, t.c.id, cursor(AT, "x"), size=10)
|
|
.compile(dialect=postgresql.dialect()))
|
|
assert "WHERE (t.at, t.id) < (" in later
|