refactor(ui): give each Logs tab its own route

Split the Logs page's four tabs (Request Logs, Audit Logs, Deleted Keys,
Deleted Teams) into their own prerendered paths under /logs, mirroring
the earlier per-tab routing migrations. A shared layout gates on
credentials (the existing loading-spinner contract), renders the tab bar
and derives the active tab from the pathname; each tab is its own
page.tsx, so deep links and hard-loads to /logs/audit, /logs/deleted-keys
and /logs/deleted-teams resolve to real static HTML with no nginx change.

The former SpendLogsTable god-component (a Tremor TabGroup wrapping the
four panels) is gone; its tab bar is rebuilt on the shadcn Tabs
primitive. Because each tab now mounts only when its route is active, the
Request Logs and Audit Logs panels receive isActive directly instead of a
shared activeTab flag, which also drops the quirk where selecting Deleted
Keys/Teams marked the audit-logs panel active.
This commit is contained in:
ryan-crabbe-berri 2026-07-23 14:19:00 -07:00
parent a507394841
commit 1211259a97
11 changed files with 259 additions and 163 deletions

View file

@ -4393,14 +4393,6 @@
"count": 1
}
},
"src/components/view_logs/index.tsx": {
"local/filename-pascal-case": {
"count": 1
},
"no-restricted-imports": {
"count": 1
}
},
"src/components/view_logs/log_filter_logic.tsx": {
"local/filename-pascal-case": {
"count": 1
@ -4529,4 +4521,4 @@
"count": 1
}
}
}
}

View file

@ -0,0 +1,18 @@
"use client";
import AuditLogsPanel from "@/components/view_logs/AuditLogsPanel";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
export default function AuditLogsPage() {
const { accessToken, token, userRole, userId, premiumUser } = useAuthorized();
return (
<AuditLogsPanel
userID={userId}
userRole={userRole}
token={token}
accessToken={accessToken}
isActive
premiumUser={premiumUser}
/>
);
}

View file

@ -0,0 +1,7 @@
"use client";
import DeletedKeysPage from "@/components/DeletedKeysPage/DeletedKeysPage";
export default function DeletedKeysRoute() {
return <DeletedKeysPage />;
}

View file

@ -0,0 +1,7 @@
"use client";
import DeletedTeamsPage from "@/components/DeletedTeamsPage/DeletedTeamsPage";
export default function DeletedTeamsRoute() {
return <DeletedTeamsPage />;
}

View file

@ -0,0 +1,94 @@
/* @vitest-environment jsdom */
import { act, render } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import LogsLayout from "./layout";
const { mockPush, navState } = vi.hoisted(() => ({
mockPush: vi.fn(),
navState: { pathname: "/logs" },
}));
vi.mock("next/navigation", () => ({
usePathname: () => navState.pathname,
useRouter: () => ({ push: mockPush }),
}));
vi.mock("@/components/networking", () => ({ serverRootPath: "" }));
const mockUseAuthorized = vi.fn();
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ default: () => mockUseAuthorized() }));
const READY = { accessToken: "at", token: "tok", userRole: "Admin", userId: "u1", premiumUser: false };
const renderLayout = () =>
render(
<LogsLayout>
<div data-testid="tab-content">CHILD</div>
</LogsLayout>,
);
describe("LogsLayout", () => {
beforeEach(() => {
navState.pathname = "/logs";
mockPush.mockClear();
mockUseAuthorized.mockReturnValue(READY);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(global as any).ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
};
});
it("renders the four log tabs and the active tab's page content", () => {
const { getByRole, getByTestId } = renderLayout();
for (const name of ["Request Logs", "Audit Logs", "Deleted Keys", "Deleted Teams"]) {
expect(getByRole("tab", { name })).toBeInTheDocument();
}
expect(getByTestId("tab-content")).toHaveTextContent("CHILD");
});
it("marks the base route's Request Logs tab active", () => {
const { getByRole } = renderLayout();
expect(getByRole("tab", { name: "Request Logs" })).toHaveAttribute("aria-selected", "true");
expect(getByRole("tab", { name: "Audit Logs" })).toHaveAttribute("aria-selected", "false");
});
it("navigates to a tab's path when its tab is clicked", async () => {
const { getByRole } = renderLayout();
await act(async () => {
getByRole("tab", { name: "Audit Logs" }).click();
});
expect(mockPush).toHaveBeenCalledWith(expect.stringMatching(/\/logs\/audit\/$/));
});
it("routes the base tab back to the logs root (no slug)", async () => {
navState.pathname = "/logs/audit";
const { getByRole } = renderLayout();
await act(async () => {
getByRole("tab", { name: "Request Logs" }).click();
});
expect(mockPush).toHaveBeenCalledWith(expect.stringMatching(/\/logs\/$/));
});
it("redirects to the base logs path when the tab slug is unknown", async () => {
const replaceMock = vi.fn();
const originalLocation = window.location;
Object.defineProperty(window, "location", {
configurable: true,
value: { replace: replaceMock, assign: vi.fn(), href: "http://localhost/", pathname: "/", search: "" },
});
navState.pathname = "/logs/bogus";
await act(async () => {
renderLayout();
});
expect(replaceMock).toHaveBeenCalledWith(expect.stringMatching(/\/logs\/$/));
Object.defineProperty(window, "location", { configurable: true, value: originalLocation });
});
it("shows a loading spinner and no tabs until credentials resolve", () => {
mockUseAuthorized.mockReturnValue({ ...READY, accessToken: null });
const { container, queryByRole } = renderLayout();
expect(container.querySelector(".ant-spin")).toBeInTheDocument();
expect(queryByRole("tab", { name: "Request Logs" })).not.toBeInTheDocument();
});
});

View file

@ -0,0 +1,63 @@
"use client";
import type { ReactNode } from "react";
import { useEffect } from "react";
import { usePathname, useRouter } from "next/navigation";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { AntDLoadingSpinner } from "@/components/ui/AntDLoadingSpinner";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { logsTabHref, slugFromPathname, type LogsTabSlug } from "@/app/(dashboard)/logs/tabRoutes";
const BASE_TAB_KEY = "request-logs";
const ORDERED_KEYS: Array<"" | LogsTabSlug> = ["", "audit", "deleted-keys", "deleted-teams"];
const TAB_LABELS: Record<"" | LogsTabSlug, string> = {
"": "Request Logs",
audit: "Audit Logs",
"deleted-keys": "Deleted Keys",
"deleted-teams": "Deleted Teams",
};
export default function LogsLayout({ children }: { children: ReactNode }) {
const { accessToken, token, userRole, userId } = useAuthorized();
const pathname = usePathname();
const router = useRouter();
const activeSlug = slugFromPathname(pathname);
const isKnownSlug = ORDERED_KEYS.some((slug) => slug === activeSlug);
const activeKey = isKnownSlug ? activeSlug || BASE_TAB_KEY : BASE_TAB_KEY;
useEffect(() => {
if (activeSlug !== "" && !isKnownSlug) {
window.location.replace(logsTabHref(""));
}
}, [activeSlug, isKnownSlug]);
const hasCredentials = Boolean(accessToken && token);
const hasIdentity = Boolean(userRole && userId);
if (!hasCredentials || !hasIdentity) {
return (
<div className="flex items-center justify-center h-64">
<AntDLoadingSpinner size="large" />
</div>
);
}
return (
<div className="w-full p-6 overflow-x-hidden box-border">
<Tabs value={activeKey} onValueChange={(key) => router.push(logsTabHref(key === BASE_TAB_KEY ? "" : key))}>
<TabsList variant="line">
{ORDERED_KEYS.map((slug) => (
<TabsTrigger key={slug || BASE_TAB_KEY} value={slug || BASE_TAB_KEY}>
{TAB_LABELS[slug]}
</TabsTrigger>
))}
</TabsList>
</Tabs>
<div className="mt-4">{children}</div>
</div>
);
}

View file

@ -1,17 +1,15 @@
"use client";
import SpendLogsTable from "@/components/view_logs";
import RequestLogsPanel from "@/components/view_logs/RequestLogsPanel";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
export default function Logs() {
const { accessToken, userRole, userId, token, premiumUser } = useAuthorized();
return (
<SpendLogsTable
userID={userId}
userRole={userRole}
token={token}
accessToken={accessToken}
premiumUser={premiumUser}
/>
);
export default function RequestLogsPage() {
const { accessToken, token, userRole, userId } = useAuthorized();
if (!accessToken || !token) {
return null;
}
if (!userRole || !userId) {
return null;
}
return <RequestLogsPanel accessToken={accessToken} token={token} userRole={userRole} userID={userId} isActive />;
}

View file

@ -0,0 +1,38 @@
/* @vitest-environment jsdom */
import { describe, expect, it, vi } from "vitest";
vi.mock("@/components/networking", () => ({ serverRootPath: "" }));
import { LOGS_TAB_SLUGS, logsTabHref, slugFromPathname } from "./tabRoutes";
describe("slugFromPathname", () => {
it("returns empty string for the base path with or without a trailing slash", () => {
expect(slugFromPathname("/logs")).toBe("");
expect(slugFromPathname("/logs/")).toBe("");
});
it("extracts the tab slug from dev and proxy-mounted (/ui) paths", () => {
expect(slugFromPathname("/logs/audit")).toBe("audit");
expect(slugFromPathname("/ui/logs/deleted-teams/")).toBe("deleted-teams");
});
it("returns the raw segment for an unknown tab so the layout can redirect to base", () => {
expect(slugFromPathname("/ui/logs/bogus")).toBe("bogus");
});
it("returns empty string when the logs base segment is not in the path", () => {
expect(slugFromPathname("/teams")).toBe("");
});
});
describe("logsTabHref", () => {
it("builds the trailing-slash base href for the empty slug", () => {
expect(logsTabHref("")).toBe("/ui/logs/");
});
it("builds a trailing-slash href for every tab slug (required by static export)", () => {
for (const slug of LOGS_TAB_SLUGS) {
expect(logsTabHref(slug)).toBe(`/ui/logs/${slug}/`);
}
});
});

View file

@ -0,0 +1,21 @@
import { migratedHref } from "@/utils/migratedPages";
export const LOGS_BASE_SEGMENT = "logs";
export const LOGS_TAB_SLUGS = ["audit", "deleted-keys", "deleted-teams"] as const;
export type LogsTabSlug = (typeof LOGS_TAB_SLUGS)[number];
export function logsTabHref(slug: string): string {
const base = migratedHref(LOGS_BASE_SEGMENT);
return slug ? `${base}/${slug}/` : `${base}/`;
}
export function slugFromPathname(pathname: string): string {
const parts = pathname.split("/").filter(Boolean);
const idx = parts.indexOf(LOGS_BASE_SEGMENT);
if (idx === -1) {
return "";
}
return parts[idx + 1] ?? "";
}

View file

@ -1,75 +0,0 @@
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import SpendLogsTable from "./index";
import { renderWithProviders } from "../../../tests/test-utils";
vi.mock("./RequestLogsPanel", () => ({
default: function RequestLogsPanelMock({ isActive }: { isActive: boolean }) {
return <div data-testid="request-logs-panel">{isActive ? "active" : "inactive"}</div>;
},
}));
vi.mock("./AuditLogsPanel", () => ({
default: function AuditLogsPanelMock({ isActive }: { isActive: boolean }) {
return <div data-testid="audit-logs-panel">{isActive ? "active" : "inactive"}</div>;
},
}));
vi.mock("../DeletedKeysPage/DeletedKeysPage", () => ({
default: function DeletedKeysPageMock() {
return <div data-testid="deleted-keys-page" />;
},
}));
vi.mock("../DeletedTeamsPage/DeletedTeamsPage", () => ({
default: function DeletedTeamsPageMock() {
return <div data-testid="deleted-teams-page" />;
},
}));
const defaultProps = {
accessToken: "test-token",
token: "test-token",
userRole: "Admin",
userID: "user-1",
premiumUser: false,
};
describe("SpendLogsTable", () => {
it("renders the four log tabs", () => {
renderWithProviders(<SpendLogsTable {...defaultProps} />);
for (const label of ["Request Logs", "Audit Logs", "Deleted Keys", "Deleted Teams"]) {
expect(screen.getByRole("tab", { name: label })).toBeInTheDocument();
}
});
it("marks only the visible tab's panel active so background tabs do not query", async () => {
const user = userEvent.setup();
renderWithProviders(<SpendLogsTable {...defaultProps} />);
expect(screen.getByTestId("request-logs-panel")).toHaveTextContent("active");
await user.click(screen.getByRole("tab", { name: "Audit Logs" }));
expect(await screen.findByTestId("audit-logs-panel")).toHaveTextContent("active");
expect(screen.getByTestId("request-logs-panel")).toHaveTextContent("inactive");
});
describe("auth-not-ready guard", () => {
it("shows a loading spinner when credentials are not yet resolved", () => {
renderWithProviders(<SpendLogsTable {...defaultProps} accessToken={null} />);
expect(document.querySelector(".ant-spin")).toBeInTheDocument();
expect(screen.queryByRole("tab", { name: "Request Logs" })).not.toBeInTheDocument();
});
it("renders the tabs (no spinner) once all credentials are present", () => {
renderWithProviders(<SpendLogsTable {...defaultProps} />);
expect(document.querySelector(".ant-spin")).not.toBeInTheDocument();
expect(screen.getByRole("tab", { name: "Request Logs" })).toBeInTheDocument();
});
});
});

View file

@ -1,67 +0,0 @@
import { useState } from "react";
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react";
import DeletedKeysPage from "../DeletedKeysPage/DeletedKeysPage";
import DeletedTeamsPage from "../DeletedTeamsPage/DeletedTeamsPage";
import AuditLogsPanel from "./AuditLogsPanel";
import RequestLogsPanel from "./RequestLogsPanel";
import { AntDLoadingSpinner } from "../ui/AntDLoadingSpinner";
interface SpendLogsTableProps {
accessToken: string | null;
token: string | null;
userRole: string | null;
userID: string | null;
premiumUser: boolean;
}
export default function SpendLogsTable({ accessToken, token, userRole, userID, premiumUser }: SpendLogsTableProps) {
const [activeTab, setActiveTab] = useState("request logs");
if (!accessToken || !token || !userRole || !userID) {
return (
<div className="flex items-center justify-center h-64">
<AntDLoadingSpinner size="large" />
</div>
);
}
return (
<div className="w-full p-6 overflow-x-hidden box-border">
<TabGroup defaultIndex={0} onIndexChange={(index) => setActiveTab(index === 0 ? "request logs" : "audit logs")}>
<TabList>
<Tab>Request Logs</Tab>
<Tab>Audit Logs</Tab>
<Tab>Deleted Keys</Tab>
<Tab>Deleted Teams</Tab>
</TabList>
<TabPanels>
<TabPanel>
<RequestLogsPanel
accessToken={accessToken}
token={token}
userRole={userRole}
userID={userID}
isActive={activeTab === "request logs"}
/>
</TabPanel>
<TabPanel>
<AuditLogsPanel
userID={userID}
userRole={userRole}
token={token}
accessToken={accessToken}
isActive={activeTab === "audit logs"}
premiumUser={premiumUser}
/>
</TabPanel>
<TabPanel>
<DeletedKeysPage />
</TabPanel>
<TabPanel>
<DeletedTeamsPage />
</TabPanel>
</TabPanels>
</TabGroup>
</div>
);
}