feat: add admin user list page with search and pagination

This commit is contained in:
Johan Lundberg 2026-02-19 11:35:25 +01:00
parent f2d669d705
commit 1a795914f9
No known key found for this signature in database
GPG key ID: A6C152738D03C7D1
4 changed files with 111 additions and 2 deletions

View file

@ -30,5 +30,31 @@ async def users_list(request: Request) -> Response:
if admin is None: if admin is None:
return HTMLResponse("Forbidden", status_code=403) return HTMLResponse("Forbidden", status_code=403)
# Placeholder — will be implemented in Task 4 per_page = 20
return HTMLResponse("Admin users list") q = request.query_params.get("q", "")
offset = int(request.query_params.get("offset", "0"))
user_repo = request.app.state.user_repo
if q:
users = await user_repo.search_users(q, offset, per_page)
total = await user_repo.count_users(query=q)
else:
users = await user_repo.list_users(offset, per_page)
total = await user_repo.count_users()
context = {
"users": users,
"query": q,
"offset": offset,
"per_page": per_page,
"total": total,
"active_page": "users",
}
# HTMX search requests get just the table rows partial
if request.headers.get("HX-Request") and request.headers.get("HX-Trigger-Name") == "q":
templates = request.app.state.templates
return templates.TemplateResponse(request, "admin/_user_rows.html", context)
templates = request.app.state.templates
return templates.TemplateResponse(request, "admin/users.html", context)

View file

@ -0,0 +1,13 @@
{% if total > 0 %}
<span>
Showing {{ offset + 1 }}{{ offset + users|length }} of {{ total }}
</span>
{% endif %}
<span>
{% if offset > 0 %}
<a href="/admin/users?offset={{ offset - per_page }}&q={{ query or '' }}">Previous</a>
{% endif %}
{% if offset + per_page < total %}
<a href="/admin/users?offset={{ offset + per_page }}&q={{ query or '' }}">Next</a>
{% endif %}
</span>

View file

@ -0,0 +1,21 @@
{% for user in users %}
<tr>
<td><a href="/admin/users/{{ user.userid }}">{{ user.username }}</a></td>
<td>{{ [user.given_name, user.family_name]|select|join(' ') }}</td>
<td>{{ user.email or '' }}</td>
<td>{% for g in user.groups %}<span class="group-tag">{{ g }}</span> {% endfor %}</td>
<td>
<span id="status-{{ user.userid }}">
{% if user.active %}
<span class="status-badge status-active">Active</span>
{% else %}
<span class="status-badge status-inactive">Inactive</span>
{% endif %}
</span>
</td>
<td>{{ user.created_at.strftime('%Y-%m-%d') }}</td>
</tr>
{% endfor %}
{% if not users %}
<tr><td colspan="6">No users found.</td></tr>
{% endif %}

View file

@ -0,0 +1,49 @@
{% extends "admin/base.html" %}
{% block title %}Users — Admin — Porchlight{% endblock %}
{% block admin_content %}
<h1>Users</h1>
<section>
<h2>Create invite</h2>
<form hx-post="/admin/invite" hx-target="#invite-status" hx-swap="innerHTML">
<div class="admin-search">
<input type="text" name="username" placeholder="Username for new invite" required>
<button type="submit">Create invite</button>
</div>
</form>
<div id="invite-status"></div>
</section>
<section>
<div class="admin-search">
<input type="search" name="q" placeholder="Search by username or email..."
hx-get="/admin/users" hx-target="#user-table-body" hx-swap="innerHTML"
hx-trigger="input changed delay:300ms, search"
hx-include="this" hx-push-url="false"
value="{{ query or '' }}">
</div>
<div id="user-table-container">
<table class="admin-table">
<thead>
<tr>
<th>Username</th>
<th>Name</th>
<th>Email</th>
<th>Groups</th>
<th>Status</th>
<th>Created</th>
</tr>
</thead>
<tbody id="user-table-body">
{% include "admin/_user_rows.html" %}
</tbody>
</table>
<div class="pagination" id="pagination">
{% include "admin/_pagination.html" %}
</div>
</div>
</section>
{% endblock %}