diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json
index ec1e3ac05ba..71ee26e2326 100644
--- a/ui/litellm-dashboard/eslint-suppressions.json
+++ b/ui/litellm-dashboard/eslint-suppressions.json
@@ -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
}
}
-}
\ No newline at end of file
+}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/logs/audit/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/logs/audit/page.tsx
new file mode 100644
index 00000000000..1081ed83cf5
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/logs/audit/page.tsx
@@ -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 (
+
+ );
+}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/logs/deleted-keys/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/logs/deleted-keys/page.tsx
new file mode 100644
index 00000000000..a4accd8f2f9
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/logs/deleted-keys/page.tsx
@@ -0,0 +1,7 @@
+"use client";
+
+import DeletedKeysPage from "@/components/DeletedKeysPage/DeletedKeysPage";
+
+export default function DeletedKeysRoute() {
+ return ;
+}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/logs/deleted-teams/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/logs/deleted-teams/page.tsx
new file mode 100644
index 00000000000..4f7c597c4ae
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/logs/deleted-teams/page.tsx
@@ -0,0 +1,7 @@
+"use client";
+
+import DeletedTeamsPage from "@/components/DeletedTeamsPage/DeletedTeamsPage";
+
+export default function DeletedTeamsRoute() {
+ return ;
+}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/logs/layout.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/logs/layout.test.tsx
new file mode 100644
index 00000000000..30e71487609
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/logs/layout.test.tsx
@@ -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(
+
+ CHILD
+ ,
+ );
+
+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();
+ });
+});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/logs/layout.tsx b/ui/litellm-dashboard/src/app/(dashboard)/logs/layout.tsx
new file mode 100644
index 00000000000..a31368d6623
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/logs/layout.tsx
@@ -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 (
+
+ );
+ }
+
+ return (
+
+
router.push(logsTabHref(key === BASE_TAB_KEY ? "" : key))}>
+
+ {ORDERED_KEYS.map((slug) => (
+
+ {TAB_LABELS[slug]}
+
+ ))}
+
+
+
+
{children}
+
+ );
+}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx
index 88909e3b87f..e17059c1765 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx
@@ -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 (
-
- );
+export default function RequestLogsPage() {
+ const { accessToken, token, userRole, userId } = useAuthorized();
+ if (!accessToken || !token) {
+ return null;
+ }
+ if (!userRole || !userId) {
+ return null;
+ }
+ return ;
}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/logs/tabRoutes.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/logs/tabRoutes.test.ts
new file mode 100644
index 00000000000..2f5f14c22f8
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/logs/tabRoutes.test.ts
@@ -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}/`);
+ }
+ });
+});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/logs/tabRoutes.ts b/ui/litellm-dashboard/src/app/(dashboard)/logs/tabRoutes.ts
new file mode 100644
index 00000000000..ab672daaeda
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/logs/tabRoutes.ts
@@ -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] ?? "";
+}
diff --git a/ui/litellm-dashboard/src/components/view_logs/index.test.tsx b/ui/litellm-dashboard/src/components/view_logs/index.test.tsx
deleted file mode 100644
index b2e77ec7fd5..00000000000
--- a/ui/litellm-dashboard/src/components/view_logs/index.test.tsx
+++ /dev/null
@@ -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 {isActive ? "active" : "inactive"}
;
- },
-}));
-
-vi.mock("./AuditLogsPanel", () => ({
- default: function AuditLogsPanelMock({ isActive }: { isActive: boolean }) {
- return {isActive ? "active" : "inactive"}
;
- },
-}));
-
-vi.mock("../DeletedKeysPage/DeletedKeysPage", () => ({
- default: function DeletedKeysPageMock() {
- return ;
- },
-}));
-
-vi.mock("../DeletedTeamsPage/DeletedTeamsPage", () => ({
- default: function DeletedTeamsPageMock() {
- return ;
- },
-}));
-
-const defaultProps = {
- accessToken: "test-token",
- token: "test-token",
- userRole: "Admin",
- userID: "user-1",
- premiumUser: false,
-};
-
-describe("SpendLogsTable", () => {
- it("renders the four log tabs", () => {
- renderWithProviders();
-
- 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();
-
- 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();
-
- 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();
-
- expect(document.querySelector(".ant-spin")).not.toBeInTheDocument();
- expect(screen.getByRole("tab", { name: "Request Logs" })).toBeInTheDocument();
- });
- });
-});
diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx
deleted file mode 100644
index 8e7423e3fae..00000000000
--- a/ui/litellm-dashboard/src/components/view_logs/index.tsx
+++ /dev/null
@@ -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 (
-
- );
- }
-
- return (
-
-
setActiveTab(index === 0 ? "request logs" : "audit logs")}>
-
- Request Logs
- Audit Logs
- Deleted Keys
- Deleted Teams
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}