From 095578fa57a27c1afbf876accfe1fd9928f6e2f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 14 May 2026 12:57:38 +0200 Subject: [PATCH] =?UTF-8?q?refactor(admin):=20drop=20redundant=20collapse?= =?UTF-8?q?=20on=20single-purpose=20Login-=C3=9Cbersicht=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `/admin/login-overview` is the only card on its dedicated route since v1.5 phase-4b. The Collapse/Expand toggle inherited from the v1.4 shared-admin page therefore hid the entire page contents on click, serving no purpose beyond clutter. Drop the `expanded` state, the header toggle button, and the `expanded && (…)` body wrapper; the section always renders. The two queries lose their `enabled: expanded` gate (a no-op now that they always run). The `settings.collapse` / `settings.expand` i18n keys stay so future surfaces can opt back in without re-introducing the strings. A regression test mirrors the `api-token-no-collapse` pattern: no "Collapse"/"Expand"/"Einklappen"/"Ausklappen" buttons, no `aria-expanded` on the section heading, and the filter pills paint unconditionally in SSR. --- .../login-overview-no-collapse.test.tsx | 94 +++ .../admin/login-overview-section.tsx | 559 +++++++++--------- 2 files changed, 364 insertions(+), 289 deletions(-) create mode 100644 src/components/admin/__tests__/login-overview-no-collapse.test.tsx diff --git a/src/components/admin/__tests__/login-overview-no-collapse.test.tsx b/src/components/admin/__tests__/login-overview-no-collapse.test.tsx new file mode 100644 index 000000000..ceb0488a3 --- /dev/null +++ b/src/components/admin/__tests__/login-overview-no-collapse.test.tsx @@ -0,0 +1,94 @@ +import { describe, expect, it, vi } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; + +/** + * v1.4.25 W8b — `/admin/login-overview` no longer renders a Collapse + * / Expand toggle. The toggle was a leftover from the v1.4 shared + * admin page where 13 sections lived on one route; collapsing the + * only card on the dedicated login-overview route hid the entire + * surface. Visiting the page is itself the opt-in. + */ + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ push: vi.fn(), replace: vi.fn() }), + usePathname: () => "/admin/login-overview", +})); + +vi.mock("@tanstack/react-query", () => ({ + useQuery: () => ({ + data: { + entries: [], + meta: { total: 0, limit: 50, offset: 0, page: 1, perPage: 50 }, + }, + isLoading: false, + isError: false, + refetch: vi.fn(), + }), + useQueryClient: () => ({ invalidateQueries: vi.fn() }), + useMutation: () => ({ + mutate: vi.fn(), + isPending: false, + isError: false, + error: null, + }), +})); + +vi.mock("@/hooks/use-auth", () => ({ + useAuth: () => ({ + user: { + id: "u1", + username: "marc", + email: "marc@example.com", + role: "ADMIN", + timezone: "Europe/Berlin", + }, + isAuthenticated: true, + isLoading: false, + refetch: vi.fn(), + }), +})); + +import { I18nProvider } from "@/lib/i18n/context"; +import { LoginOverviewSection } from "../login-overview-section"; + +function render(locale: "en" | "de" = "en") { + return renderToStaticMarkup( + + + , + ); +} + +describe("LoginOverviewSection — no collapse button", () => { + it("does not render a Collapse / Expand button (English)", () => { + const html = render("en"); + expect(html).not.toMatch(/\bCollapse\b/); + expect(html).not.toMatch(/\bExpand\b/); + }); + + it("does not render an Einklappen / Ausklappen button (German)", () => { + const html = render("de"); + expect(html).not.toMatch(/\bEinklappen\b/); + expect(html).not.toMatch(/\bAusklappen\b/); + }); + + it("never renders an `aria-expanded` button on the section header", () => { + const html = render(); + // The toggle previously surfaced as +
+ +
{t("admin.loginOverview")}
- {expanded && ( -
- {/* Quick-filter pills. Failed-only is the most common +
+ {/* Quick-filter pills. Failed-only is the most common admin "is something wrong" question — keep the one-tap shortcut. */} -
- - -
+
+ + +
- {/* Detailed filter row */} -
- { - setPage(1); - setActor(e.target.value); - }} - aria-label={t("admin.section.auditLog.filterActor")} - /> + {/* Detailed filter row */} +
+ { + setPage(1); + setActor(e.target.value); + }} + aria-label={t("admin.section.auditLog.filterActor")} + /> + + { + setPage(1); + setTarget(e.target.value); + }} + aria-label={t("admin.section.auditLog.filterTarget")} + /> + +
+ + {/* Toolbar row: per-page + export */} +
+
+ + {t("admin.section.auditLog.perPage")} + - { - setPage(1); - setTarget(e.target.value); - }} - aria-label={t("admin.section.auditLog.filterTarget")} - /> -
+ +
- {/* Toolbar row: per-page + export */} -
-
- - {t("admin.section.auditLog.perPage")} - - -
- + {isLoading ? ( +
+
- - {isLoading ? ( -
- -
- ) : !entries.length ? ( - } - title={ - filter === "failed" - ? t("admin.loginEmptyFailedTitle") - : t("admin.section.auditLog.empty") - } - description={ - filter === "failed" - ? t("admin.loginEmptyFailedDescription") - : t("admin.loginEmptyDescription") - } - action={ - filter === "failed" ? ( - - ) : undefined - } - /> - ) : ( -
- - - - - - - - - {/* + ) : !entries.length ? ( + } + title={ + filter === "failed" + ? t("admin.loginEmptyFailedTitle") + : t("admin.section.auditLog.empty") + } + description={ + filter === "failed" + ? t("admin.loginEmptyFailedDescription") + : t("admin.loginEmptyDescription") + } + action={ + filter === "failed" ? ( + + ) : undefined + } + /> + ) : ( +
+
- {t("admin.status")} - - {t("admin.users")} - - {t("admin.action")} - - {t("admin.provider")} - - {t("admin.ip")} -
+ + + + + + + + {/* v1.4.25 W8b — Standort column. Previously rendered `text-right`, which on wide audit tables let the `Berlin, DE` label drift to the table edge and @@ -408,103 +390,102 @@ export function LoginOverviewSection() { consistency) makes Standort a first-class column that admins actually notice. */} - - - - - - {entries.map((entry, i) => { - const isFailed = entry.action === "auth.login.failed"; - const provider = providerForAction(entry.action); - const ProviderIcon = iconForAuthProvider(provider); - return ( - + + + + + + + + {entries.map((entry, i) => { + const isFailed = entry.action === "auth.login.failed"; + const provider = providerForAction(entry.action); + const ProviderIcon = iconForAuthProvider(provider); + return ( + + + - - - - - - - - ); - })} - -
+ {t("admin.status")} + + {t("admin.users")} + + {t("admin.action")} + + {t("admin.provider")} + + {t("admin.ip")} + - - - - {t("admin.timestamp")} -
+ {t("admin.timestamp")} +
+ {isFailed ? ( + + ) : ( + + )} + - - {isFailed ? ( - - ) : ( - - )} - - {entry.user?.username ?? t("common.unknown")} - - {AUTH_ACTION_LABELS[entry.action] ?? entry.action} - - - - - {entry.ipAddress ?? "—"} - - {entry.location ?? "—"} - - {formatDateTime(entry.createdAt)} -
-
+ {entry.user?.username ?? t("common.unknown")} + + + {AUTH_ACTION_LABELS[entry.action] ?? entry.action} + + + + + + + {entry.ipAddress ?? "—"} + + + {entry.location ?? "—"} + + + {formatDateTime(entry.createdAt)} + + + ); + })} + + +
+ + {t("admin.showingEntries", { + count: entries.length, + total, + })} + +
+ - {t("admin.showingEntries", { - count: entries.length, - total, + {t("admin.section.auditLog.pageOf", { + page, + total: lastPage, })} -
- - - {t("admin.section.auditLog.pageOf", { - page, - total: lastPage, - })} - - -
+
- )} -
- )} +
+ )} +
); }