1
0
forked from baron/baron-sso

perf(admin): full-stack performance optimization for all list tables

- Implemented server-side search, infinite scrolling, and list virtualization for Tenants, Users, and Audit Logs.
- Backend: Enhanced Repository, Service, and Handler layers to support 'search' and 'cursor' parameters.
- Frontend: Integrated @tanstack/react-virtual and useInfiniteQuery for high-performance rendering.
- Quality: Updated all unit tests and E2E tests to match the new asynchronous server-side search architecture.
- i18n: Synced all translation keys and cleaned up unused resources.
This commit is contained in:
2026-06-04 16:06:30 +09:00
parent 6d3f128282
commit b2f155e35b
26 changed files with 1103 additions and 440 deletions

View File

@@ -2,12 +2,6 @@ import { useInfiniteQuery } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { Download, NotebookTabs, RefreshCw, Search } from "lucide-react";
import * as React from "react";
import {
parseAuditDetails,
resolveAuditAction,
resolveAuditActor,
} from "../../../../common/core/audit";
import { AuditLogTable } from "../../../../common/core/components/audit";
import { PageHeader } from "../../../../common/core/components/page";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { Badge } from "../../components/ui/badge";
@@ -23,6 +17,7 @@ import { Input } from "../../components/ui/input";
import type { AuditLog } from "../../lib/adminApi";
import { fetchAuditLogs } from "../../lib/adminApi";
import { t } from "../../lib/i18n";
import { VirtualizedAuditLogTable } from "./VirtualizedAuditLogTable";
function AuditLogsPage() {
const [searchActorId, setSearchActorId] = React.useState("");
@@ -40,8 +35,23 @@ function AuditLogsPage() {
isFetching,
refetch,
} = useInfiniteQuery({
queryKey: ["audit-logs"],
queryFn: ({ pageParam }) => fetchAuditLogs(50, pageParam),
queryKey: [
"audit-logs",
deferredSearchActorId,
deferredSearchAction,
statusFilter,
],
queryFn: ({ pageParam }) => {
const search = [deferredSearchActorId, deferredSearchAction]
.filter(Boolean)
.join(" ");
return fetchAuditLogs(
50,
pageParam,
search || undefined,
statusFilter === "all" ? undefined : statusFilter,
);
},
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.next_cursor || undefined,
});
@@ -51,24 +61,6 @@ function AuditLogsPage() {
(page) =>
page?.items?.filter((item): item is AuditLog => Boolean(item)) ?? [],
) ?? [];
const filteredLogs = React.useMemo(
() =>
logs.filter((row) => {
const details = parseAuditDetails(row.details);
const actorLabel = resolveAuditActor(row, details).toLowerCase();
const actionLabel = resolveAuditAction(row, details).toLowerCase();
const matchesActor =
deferredSearchActorId === "" ||
actorLabel.includes(deferredSearchActorId.toLowerCase());
const matchesAction =
deferredSearchAction === "" ||
actionLabel.includes(deferredSearchAction.toLowerCase());
const matchesStatus =
statusFilter === "all" || row.status === statusFilter;
return matchesActor && matchesAction && matchesStatus;
}),
[logs, deferredSearchActorId, deferredSearchAction, statusFilter],
);
return (
<div className="space-y-6">
@@ -83,7 +75,7 @@ function AuditLogsPage() {
<>
<Badge variant="muted">
{t("msg.common.audit.registry.count", "총 {{count}}개 로그", {
count: filteredLogs.length,
count: logs.length,
})}
</Badge>
<Button
@@ -185,8 +177,8 @@ function AuditLogsPage() {
</form>
}
/>
<AuditLogTable
logs={filteredLogs}
<VirtualizedAuditLogTable
logs={logs}
t={t}
loading={isLoading}
hasNextPage={Boolean(hasNextPage)}