feat(admin): search recent audit metadata
This commit is contained in:
@@ -33,6 +33,29 @@ beforeEach(() => {
|
||||
const row = { id: 7, type: "Deleted", at: "2026-10-20T12:00:00Z", jobApplicationId: 42, note: "Owner authored text" };
|
||||
function deferred<T>() { let resolve!: (value: T) => void; const promise = new Promise<T>(done => { resolve = done; }); return { promise, resolve }; }
|
||||
|
||||
test("searches loaded audit metadata without querying the API or matching notes", async () => {
|
||||
mockedApi.get.mockResolvedValue({ data: [
|
||||
{ ...row, jobTitle: "Teacher", ownerEmail: "alice@example.test", ownerUserName: "al", ownerUserId: "user-a" },
|
||||
{ ...row, id: 8, jobApplicationId: 420, type: "StatusChanged", jobTitle: "Nurse", companyName: "Clinic", note: "Sensitive phrase" },
|
||||
] } as any);
|
||||
renderPage();
|
||||
await screen.findByText("Teacher");
|
||||
const search = screen.getByRole("searchbox", { name: "Search recent audit events" });
|
||||
expect(search).toHaveAccessibleDescription("Searches up to 200 loaded events by user, event type, application ID, title or company.");
|
||||
|
||||
for (const query of [" ALICE ", "al", "user-a", "Deleted", "42", "teacher"]) {
|
||||
fireEvent.change(search, { target: { value: query } });
|
||||
expect(screen.getByText("Teacher")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Clinic - Nurse")).not.toBeInTheDocument();
|
||||
}
|
||||
fireEvent.change(search, { target: { value: "sensitive phrase" } });
|
||||
expect(screen.getByText("No loaded events match your search.")).toBeInTheDocument();
|
||||
fireEvent.change(search, { target: { value: "" } });
|
||||
expect(screen.getByText("Clinic - Nurse")).toBeInTheDocument();
|
||||
expect(mockedApi.get).toHaveBeenCalledTimes(1);
|
||||
expect(mockedApi.post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("[P7-10 audit] retains shared heading hierarchy and a named keyboard scroll region", async () => {
|
||||
mockedApi.get.mockResolvedValue({ data: [row] } as any);
|
||||
renderPage();
|
||||
|
||||
@@ -2622,6 +2622,9 @@ export const translations = {
|
||||
adminAuditDetails: "Details",
|
||||
adminAuditActions: "Actions",
|
||||
adminAuditNoEvents: "No events.",
|
||||
adminAuditSearch: "Search recent audit events",
|
||||
adminAuditSearchScope: "Searches up to 200 loaded events by user, event type, application ID, title or company.",
|
||||
adminAuditNoMatches: "No loaded events match your search.",
|
||||
adminSystemEnvironment: "Environment",
|
||||
adminSystemStatusTab: "Status",
|
||||
adminSystemSettingsTab: "Settings",
|
||||
@@ -5852,6 +5855,9 @@ export const translations = {
|
||||
adminAuditDetails: "Detaljer",
|
||||
adminAuditActions: "Handlinger",
|
||||
adminAuditNoEvents: "Ingen hendelser.",
|
||||
adminAuditSearch: "Søk i nylige revisjonshendelser",
|
||||
adminAuditSearchScope: "Søker i opptil 200 innlastede hendelser etter bruker, hendelsestype, søknads-ID, stillingstittel eller virksomhet.",
|
||||
adminAuditNoMatches: "Ingen innlastede hendelser samsvarer med søket.",
|
||||
adminSystemEnvironment: "Miljø",
|
||||
adminSystemStatusTab: "Status",
|
||||
adminSystemSettingsTab: "Innstillinger",
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
|
||||
@@ -44,6 +45,7 @@ export default function AdminAuditPage() {
|
||||
const { toast } = useToast();
|
||||
const { language, t } = useI18n();
|
||||
const [busyId, setBusyId] = useState<number | null>(null);
|
||||
const [search, setSearch] = useState("");
|
||||
const body=useRef<HTMLDivElement>(null);
|
||||
const owner=useRef({ mounted: true, generation: 0, busy: false });
|
||||
const currentTranslation=useRef(t);
|
||||
@@ -81,12 +83,28 @@ export default function AdminAuditPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const rows = auditResource.data;
|
||||
const query = search.trim().toLowerCase();
|
||||
const rows = query ? auditResource.data.filter(e =>
|
||||
String(e.jobApplicationId) === query ||
|
||||
[e.type, e.jobTitle, e.companyName, e.ownerUserId, e.ownerEmail, e.ownerUserName]
|
||||
.some(value => value?.toLowerCase().includes(query))
|
||||
) : auditResource.data;
|
||||
const error=auditResource.error?.message===fallback.current ? { ...auditResource.error, message: t("adminAuditLoadFailed") } : auditResource.error;
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "grid", gap: "var(--app-section-gap)", minWidth: 0 }}>
|
||||
<PageSection title={t("adminAuditEvents")}>
|
||||
{!auditResource.error && auditResource.data.length > 0 && (
|
||||
<TextField
|
||||
type="search"
|
||||
size="small"
|
||||
label={t("adminAuditSearch")}
|
||||
helperText={t("adminAuditSearchScope")}
|
||||
value={search}
|
||||
onChange={event => setSearch(event.target.value)}
|
||||
sx={{ width: "100%", maxWidth: 480, mb: 2 }}
|
||||
/>
|
||||
)}
|
||||
<Box ref={body} role="region" aria-label={t("adminAuditTitle")} tabIndex={0} aria-busy={busyId!==null || auditResource.loading || auditResource.refreshing} sx={{ minWidth: 0, overflow: "auto" }}>
|
||||
|
||||
<ViewStateNotice
|
||||
@@ -97,7 +115,9 @@ export default function AdminAuditPage() {
|
||||
onRetry={async () => { body.current?.focus(); await auditResource.reload(); }}
|
||||
/>
|
||||
|
||||
{!auditResource.error ? <TableContainer sx={{ overflow: "visible" }}>
|
||||
{!auditResource.error && query && auditResource.data.length > 0 && rows.length === 0 ? (
|
||||
<Typography role="status" sx={{ color: "text.secondary" }}>{t("adminAuditNoMatches")}</Typography>
|
||||
) : !auditResource.error ? <TableContainer sx={{ overflow: "visible" }}>
|
||||
<Table size="small" aria-label={t("adminAuditTitle")} sx={{ minWidth: 1200, "& th, & td": { overflowWrap: "anywhere" }, "& button": { whiteSpace: "normal", "@media (pointer: coarse)": { minHeight: 44 } } }}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
|
||||
Reference in New Issue
Block a user