Add page-size selector and jump-to-page to the audit log table.

Use the shared ListPager (10/20/50/100/200 per page, remembered in
localStorage, default 20 instead of a fixed 10), keep the previous rows while the
next page loads and clamp the page when the total shrinks.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
oliver 2026-10-12 07:34:30 +08:00
parent 45de8144a3
commit e3a15c3d7b

View file

@ -1,11 +1,27 @@
import { Button, Input } from "@heroui/react"; import { Button, Input } from "@heroui/react";
import { Fragment, useMemo, useState } from "react"; import { Fragment, useEffect, useMemo, useState } from "react";
import { useQuery } from "@tanstack/react-query"; import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { ListPager } from "../../components/ListPager";
import { useAuth } from "../auth/AuthContext"; import { useAuth } from "../auth/AuthContext";
import { useI18n } from "../../i18n"; import { useI18n } from "../../i18n";
import { apiGet } from "../../services/api"; import { apiGet } from "../../services/api";
import { pageCount } from "../../utils/display";
import { formatSystemTime } from "../../utils/time"; import { formatSystemTime } from "../../utils/time";
/** Backend caps audit-logs at 200 rows per page. */
const PAGE_SIZE_OPTIONS = [10, 20, 50, 100, 200];
const DEFAULT_PAGE_SIZE = 20;
const PAGE_SIZE_KEY = "netx.audit.pageSize";
function loadPageSize(): number {
try {
const n = Number(localStorage.getItem(PAGE_SIZE_KEY));
return PAGE_SIZE_OPTIONS.includes(n) ? n : DEFAULT_PAGE_SIZE;
} catch {
return DEFAULT_PAGE_SIZE;
}
}
type AuditItem = { type AuditItem = {
id: string; id: string;
ts: string | null; ts: string | null;
@ -119,6 +135,7 @@ export function AuditPage() {
const { t } = useI18n(); const { t } = useI18n();
const { ready, isAdmin } = useAuth(); const { ready, isAdmin } = useAuth();
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(loadPageSize);
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [action, setAction] = useState(""); const [action, setAction] = useState("");
const [quick, setQuick] = useState<QuickFilter>("business"); const [quick, setQuick] = useState<QuickFilter>("business");
@ -129,11 +146,11 @@ export function AuditPage() {
const excludeNoise = action.trim() ? false : derived.excludeNoise; const excludeNoise = action.trim() ? false : derived.excludeNoise;
const query = useQuery({ const query = useQuery({
queryKey: ["auditLogs", page, username, effectiveAction, excludeNoise], queryKey: ["auditLogs", page, pageSize, username, effectiveAction, excludeNoise],
queryFn: () => { queryFn: () => {
const p = new URLSearchParams(); const p = new URLSearchParams();
p.set("page", String(page)); p.set("page", String(page));
p.set("page_size", "10"); p.set("page_size", String(pageSize));
p.set("exclude_noise", excludeNoise ? "true" : "false"); p.set("exclude_noise", excludeNoise ? "true" : "false");
if (username.trim()) p.set("username", username.trim()); if (username.trim()) p.set("username", username.trim());
if (effectiveAction) p.set("action", effectiveAction); if (effectiveAction) p.set("action", effectiveAction);
@ -142,11 +159,29 @@ export function AuditPage() {
); );
}, },
enabled: ready, enabled: ready,
// Keep the current rows on screen while the next page loads (no flicker).
placeholderData: keepPreviousData,
}); });
const items = useMemo(() => query.data?.items || [], [query.data]); const items = useMemo(() => query.data?.items || [], [query.data]);
const total = query.data?.total || 0; const total = query.data?.total || 0;
const pages = Math.max(1, Math.ceil(total / 10)); const pages = pageCount(total, pageSize);
// Total shrank (filters / purge): never sit on a page past the end.
useEffect(() => {
if (!query.isPlaceholderData && page > pages) setPage(pages);
}, [page, pages, query.isPlaceholderData]);
const changePageSize = (size: number) => {
setPageSize(size);
setPage(1);
setExpanded(null);
try {
localStorage.setItem(PAGE_SIZE_KEY, String(size));
} catch {
/* ignore */
}
};
const hasFilters = Boolean(username.trim() || action.trim() || quick !== "business"); const hasFilters = Boolean(username.trim() || action.trim() || quick !== "business");
const setQuickFilter = (next: QuickFilter) => { const setQuickFilter = (next: QuickFilter) => {
@ -298,33 +333,19 @@ export function AuditPage() {
</div> </div>
)} )}
<div className="pager pt-list-pager"> <ListPager
<span className="muted"> page={page}
{t("common.pagerMeta", { pages={pages}
total: String(total), total={total}
page: String(page), pageSize={pageSize}
pages: String(pages), pageSizeOptions={PAGE_SIZE_OPTIONS}
})} onPageChange={(next) => {
</span> setExpanded(null);
<div className="btn-row"> setPage(next);
<Button }}
size="sm" onPageSizeChange={changePageSize}
variant="secondary" disabled={query.isLoading}
isDisabled={page <= 1} />
onPress={() => setPage((p) => Math.max(1, p - 1))}
>
{t("common.prevPage")}
</Button>
<Button
size="sm"
variant="secondary"
isDisabled={page >= pages}
onPress={() => setPage((p) => Math.min(pages, p + 1))}
>
{t("common.nextPage")}
</Button>
</div>
</div>
</div> </div>
</section> </section>
</div> </div>