feat: Add paginated user list with selectable limit

This commit is contained in:
ReturnFI
2025-10-25 18:55:55 +00:00
parent 9349037f06
commit 17660d6a54
3 changed files with 155 additions and 8 deletions

View File

@ -9,10 +9,32 @@ $(function () {
const RESET_USER_URL_TEMPLATE = contentSection.dataset.resetUserUrlTemplate; const RESET_USER_URL_TEMPLATE = contentSection.dataset.resetUserUrlTemplate;
const USER_URI_URL_TEMPLATE = contentSection.dataset.userUriUrlTemplate; const USER_URI_URL_TEMPLATE = contentSection.dataset.userUriUrlTemplate;
const BULK_URI_URL = contentSection.dataset.bulkUriUrl; const BULK_URI_URL = contentSection.dataset.bulkUriUrl;
const USERS_BASE_URL = contentSection.dataset.usersBaseUrl;
const usernameRegex = /^[a-zA-Z0-9_]+$/; const usernameRegex = /^[a-zA-Z0-9_]+$/;
let cachedUserData = []; let cachedUserData = [];
function setCookie(name, value, days) {
let expires = "";
if (days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + (value || "") + expires + "; path=/";
}
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
function checkIpLimitServiceStatus() { function checkIpLimitServiceStatus() {
$.getJSON(SERVICE_STATUS_URL) $.getJSON(SERVICE_STATUS_URL)
.done(data => { .done(data => {
@ -324,6 +346,18 @@ $(function () {
$("#searchButton").on("click", filterUsers); $("#searchButton").on("click", filterUsers);
$("#searchInput").on("keyup", filterUsers); $("#searchInput").on("keyup", filterUsers);
function initializeLimitSelector() {
const savedLimit = getCookie('limit') || '50';
$('#limit-select').val(savedLimit);
$('#limit-select').on('change', function() {
const newLimit = $(this).val();
setCookie('limit', newLimit, 365);
window.location.href = USERS_BASE_URL;
});
}
initializeLimitSelector();
checkIpLimitServiceStatus(); checkIpLimitServiceStatus();
}); });

View File

@ -1,5 +1,8 @@
from fastapi import APIRouter, HTTPException, Request, Depends from fastapi import APIRouter, HTTPException, Request, Depends, Query, Path, Cookie
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from fastapi.responses import RedirectResponse
from starlette.status import HTTP_302_FOUND
import math
from dependency import get_templates from dependency import get_templates
from .viewmodel import User from .viewmodel import User
@ -9,12 +12,57 @@ import cli_api
router = APIRouter() router = APIRouter()
@router.get('/') async def get_users_page(
async def users(request: Request, templates: Jinja2Templates = Depends(get_templates)): request: Request,
templates: Jinja2Templates,
page: int,
limit: int
):
try: try:
users_list = cli_api.list_users() or [] users_list = cli_api.list_users() or []
users: list[User] = [User.from_dict(user_data.get('username', ''), user_data) for user_data in users_list] total_users = len(users_list)
total_pages = math.ceil(total_users / limit) if limit > 0 else 1
return templates.TemplateResponse('users.html', {'users': users, 'request': request}) if page > total_pages and total_pages > 0:
return RedirectResponse(url=f"/users/{total_pages}", status_code=HTTP_302_FOUND)
if page < 1:
return RedirectResponse(url=f"/users/1", status_code=HTTP_302_FOUND)
start_index = (page - 1) * limit
end_index = start_index + limit
paginated_list = users_list[start_index:end_index]
users: list[User] = [User.from_dict(user_data.get('username', ''), user_data) for user_data in paginated_list]
return templates.TemplateResponse(
'users.html',
{
'users': users,
'request': request,
'current_page': page,
'total_pages': total_pages,
'limit': limit,
'total_users': total_users,
}
)
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=f'Error: {str(e)}') raise HTTPException(status_code=400, detail=f'Error: {str(e)}')
@router.get('/{page}', name="users_paginated")
async def users_paginated(
request: Request,
templates: Jinja2Templates = Depends(get_templates),
page: int = Path(..., ge=1),
limit: int = Cookie(default=50, ge=1)
):
return await get_users_page(request, templates, page, limit)
@router.get('/', name="users")
async def users_root(
request: Request,
templates: Jinja2Templates = Depends(get_templates),
limit: int = Cookie(default=50, ge=1)
):
return await get_users_page(request, templates, 1, limit)

View File

@ -44,11 +44,12 @@
data-edit-user-url-template="{{ url_for('edit_user_api', username='U') }}" data-edit-user-url-template="{{ url_for('edit_user_api', username='U') }}"
data-reset-user-url-template="{{ url_for('reset_user_api', username='U') }}" data-reset-user-url-template="{{ url_for('reset_user_api', username='U') }}"
data-user-uri-url-template="{{ url_for('show_user_uri_api', username='U') }}" data-user-uri-url-template="{{ url_for('show_user_uri_api', username='U') }}"
data-bulk-uri-url="{{ url_for('show_multiple_user_uris_api') }}"> data-bulk-uri-url="{{ url_for('show_multiple_user_uris_api') }}"
data-users-base-url="{{ url_for('users') }}">
<div class="container-fluid"> <div class="container-fluid">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h3 class="card-title">User List {% if users %}({{ users|length }}){% endif %}</h3> <h3 class="card-title">User List {% if total_users is defined %}({{ total_users }}){% endif %}</h3>
<div class="card-tools d-flex align-items-center flex-wrap"> <div class="card-tools d-flex align-items-center flex-wrap">
<!-- Mobile Filter Dropdown --> <!-- Mobile Filter Dropdown -->
@ -143,7 +144,7 @@
<td> <td>
<input type="checkbox" class="user-checkbox" value="{{ user.username }}"> <input type="checkbox" class="user-checkbox" value="{{ user.username }}">
</td> </td>
<td>{{ loop.index }}</td> <td>{{ loop.index + ((current_page - 1) * limit) }}</td>
<td data-username="{{ user.username }}">{{ user.username }}</td> <td data-username="{{ user.username }}">{{ user.username }}</td>
<td class="d-none d-md-table-cell"> <td class="d-none d-md-table-cell">
{% if user.status == "Online" %} {% if user.status == "Online" %}
@ -275,6 +276,70 @@
</table> </table>
{% endif %} {% endif %}
</div> </div>
<div class="card-footer clearfix">
<div class="row">
<div class="col-sm-12 col-md-5 d-flex align-items-center">
<div class="dataTables_info" role="status" aria-live="polite">
Showing {{ ((current_page - 1) * limit) + 1 }} to {{ [((current_page - 1) * limit) + limit, total_users] | min }} of {{ total_users }} users
</div>
</div>
<div class="col-sm-12 col-md-7">
<div class="d-flex justify-content-end align-items-center">
<div class="mr-3">
<form id="limit-form" method="get" class="form-inline">
<label for="limit-select" class="mr-2">Per Page:</label>
<select id="limit-select" class="form-control form-control-sm">
<option value="10">10</option>
<option value="25">25</option>
<option value="50" selected>50</option>
<option value="100">100</option>
</select>
</form>
</div>
{% if total_pages > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm m-0">
<li class="page-item {% if current_page == 1 %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('users_paginated', page=current_page - 1) }}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% set page_start = [1, current_page - 2] | max %}
{% set page_end = [total_pages, current_page + 2] | min %}
{% if page_start > 1 %}
<li class="page-item"><a class="page-link" href="{{ url_for('users_paginated', page=1) }}">1</a></li>
{% if page_start > 2 %}
<li class="page-item disabled"><span class="page-link">...</span></li>
{% endif %}
{% endif %}
{% for page_num in range(page_start, page_end + 1) %}
<li class="page-item {% if page_num == current_page %}active{% endif %}">
<a class="page-link" href="{{ url_for('users_paginated', page=page_num) }}">{{ page_num }}</a>
</li>
{% endfor %}
{% if page_end < total_pages %}
{% if page_end < total_pages - 1 %}
<li class="page-item disabled"><span class="page-link">...</span></li>
{% endif %}
<li class="page-item"><a class="page-link" href="{{ url_for('users_paginated', page=total_pages) }}">{{ total_pages }}</a></li>
{% endif %}
<li class="page-item {% if current_page == total_pages %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('users_paginated', page=current_page + 1) }}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
</ul>
</nav>
{% endif %}
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</section> </section>