You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by sb...@apache.org on 2026/09/04 08:43:21 UTC
(tooling-trusted-releases) branch sbp updated: Paginate the admin list of user sessions
This is an automated email from the ASF dual-hosted git repository.
sbp pushed a commit to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
The following commit(s) were added to refs/heads/sbp by this push:
new 45d062d6 Paginate the admin list of user sessions
45d062d6 is described below
commit 45d062d6aca74183f8d5df0fecfac783e2b982a2
Author: Sean B. Palmer <se...@miscoranda.com>
AuthorDate: Fri Sep 4 09:42:52 2026 +0100
Paginate the admin list of user sessions
---
atr/admin/__init__.py | 76 +++++++++++++++++++++++++++++++++++++--------------
atr/sessions.py | 4 ++-
2 files changed, 58 insertions(+), 22 deletions(-)
diff --git a/atr/admin/__init__.py b/atr/admin/__init__.py
index 6b84d908..76488a4f 100644
--- a/atr/admin/__init__.py
+++ b/atr/admin/__init__.py
@@ -67,6 +67,7 @@ import atr.noisy as noisy
import atr.paths as paths
import atr.pgp as pgp
import atr.principal as principal
+import atr.render as render
import atr.shared as shared
import atr.shared.catalogue_diff as catalogue_diff
import atr.shared.catalogue_import as catalogue_import
@@ -135,6 +136,11 @@ class ReleaseAgeRow(NamedTuple):
inactive_bold: bool
+@dataclasses.dataclass
+class SessionsQuery(web.PageQuery):
+ limit: int = 250
+
+
class RestoreBannerForm(form.Form):
banner_id: form.Int = form.label("Banner ID", widget=form.Widget.HIDDEN)
@@ -2298,8 +2304,12 @@ async def toggle_view_post(
@admin.typed
-async def users_get(session: web.Committer, _users: Literal["users"]) -> str:
- return await _users_page(session, quart.request.args.get("tab", "browse-as"))
+async def users_get(session: web.Committer, _users: Literal["users"], query_args: SessionsQuery) -> str:
+ try:
+ validation.pagination_args_validate(query_args)
+ except ValueError as e:
+ raise exceptions.BadRequest(str(e))
+ return await _users_page(session, quart.request.args.get("tab", "browse-as"), query_args)
@admin.typed
@@ -2310,7 +2320,7 @@ async def users_post(
case BrowseAsUserForm():
return await _users_browse_as(session, users_form)
case LdapLookupForm():
- return await _users_page(session, "ldap", ldap_form=users_form)
+ return await _users_page(session, "ldap", SessionsQuery(), ldap_form=users_form)
case RevokeUserTokensForm():
return await _users_revoke_tokens(session, users_form)
case RevokeAllTokensForm():
@@ -2839,11 +2849,13 @@ async def _users_ldap_tab(lookup_form: LdapLookupForm | None) -> htm.Element:
return htm.div[markupsafe.Markup(content)]
-async def _users_page(session: web.Committer, active_tab: str, ldap_form: LdapLookupForm | None = None) -> str:
+async def _users_page(
+ session: web.Committer, active_tab: str, query_args: SessionsQuery, ldap_form: LdapLookupForm | None = None
+) -> str:
tab_items = [
htm.Tab("browse-as", "Browse as user", _users_browse_as_tab),
htm.Tab("ldap", "LDAP lookup", lambda: _users_ldap_tab(ldap_form)),
- htm.Tab("sessions", "Sessions", _users_sessions_tab),
+ htm.Tab("sessions", "Sessions", lambda: _users_sessions_tab(query_args)),
htm.Tab("revoke-tokens", "Revoke tokens", _users_revoke_tokens_tab),
htm.Tab("revoke-all-tokens", "Revoke all tokens", _users_revoke_all_tokens_tab),
htm.Tab("revoke-ssh-keys", "Revoke SSH keys", _users_revoke_ssh_keys_tab),
@@ -3028,9 +3040,19 @@ async def _users_rotate_jwt_tab() -> htm.Element:
return block.collect()
-async def _users_sessions_tab() -> htm.Element:
+def _users_sessions_page_url(limit: int, offset: int | None) -> str | None:
+ if offset is None:
+ return None
+ return util.as_url(users_get, tab="sessions", limit=limit, offset=offset)
+
+
+async def _users_sessions_tab(query_args: SessionsQuery) -> htm.Element:
+ sessions = await asfquart.APP.sessions.current()
+ page_sessions = sessions[query_args.offset : (query_args.offset + query_args.limit)]
+ pagination = web.page_nav(query_args.offset, query_args.limit, len(sessions), len(page_sessions))
+ paged = (pagination.previous_offset is not None) or (pagination.next_offset is not None)
tbody = htm.Block(htm.tbody)
- for user_session in await asfquart.APP.sessions.current():
+ for user_session in page_sessions:
tbody.tr[
htm.td[htm.code[user_session.uid]],
htm.td[htm.code[user_session.admin_uid] if user_session.admin_uid else ""],
@@ -3041,20 +3063,32 @@ async def _users_sessions_tab() -> htm.Element:
block = htm.Block()
block.h2["Sessions"]
block.p["Every recorded session, most recently active first."]
- block.append(
- htm.table(".table.table-sm.table-striped.table-bordered")[
- htm.thead[
- htm.tr[
- htm.th["User ID"],
- htm.th["Admin"],
- htm.th["Created"],
- htm.th["Last active"],
- htm.th["IP address"],
- ]
- ],
- tbody.collect(),
- ]
- )
+ if paged and page_sessions:
+ block.p[f"Showing sessions {pagination.start} to {pagination.end} of {len(sessions)}."]
+ if sessions and (not page_sessions):
+ block.div(".alert.alert-info")[f"No sessions to show on this page ({len(sessions)} total)."]
+ else:
+ block.append(
+ htm.table(".table.table-sm.table-striped.table-bordered")[
+ htm.thead[
+ htm.tr[
+ htm.th["User ID"],
+ htm.th["Admin"],
+ htm.th["Created"],
+ htm.th["Last active"],
+ htm.th["IP address"],
+ ]
+ ],
+ tbody.collect(),
+ ]
+ )
+ if paged:
+ render.html_page_nav(
+ block,
+ aria_label="Session pages",
+ previous_url=_users_sessions_page_url(query_args.limit, pagination.previous_offset),
+ next_url=_users_sessions_page_url(query_args.limit, pagination.next_offset),
+ )
return block.collect()
diff --git a/atr/sessions.py b/atr/sessions.py
index 01d230c9..43cd516e 100644
--- a/atr/sessions.py
+++ b/atr/sessions.py
@@ -189,7 +189,9 @@ class Store:
if max_session_age > 0:
statement = statement.where(sql.UserSession.cts >= (now - max_session_age))
async with db.session() as data:
- result = await data.execute(statement.order_by(via(sql.UserSession.uts).desc()))
+ result = await data.execute(
+ statement.order_by(via(sql.UserSession.uts).desc(), via(sql.UserSession.sid_hash))
+ )
return list(result.scalars().all())
async def destroy(self, hsid: str) -> None:
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org