diff --git a/ui/litellm-dashboard/src/components/view_logs/index.integration.test.tsx b/ui/litellm-dashboard/src/components/view_logs/index.integration.test.tsx
new file mode 100644
index 00000000000..b86ad015b91
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/view_logs/index.integration.test.tsx
@@ -0,0 +1,104 @@
+import { screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import SpendLogsTable from "./index";
+import { renderWithProviders, testQueryClient } from "../../../tests/test-utils";
+
+const { useAuthorizedMock } = vi.hoisted(() => ({ useAuthorizedMock: vi.fn() }));
+
+vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
+ default: useAuthorizedMock,
+}));
+
+vi.mock("./RequestLogsPanel", () => ({
+ default: function RequestLogsPanelMock() {
+ return
;
+ },
+}));
+
+const fetchMock = vi.fn();
+
+const jsonResponse = (body: unknown) => ({
+ ok: true,
+ status: 200,
+ statusText: "OK",
+ json: async () => body,
+});
+
+const requestedUrls = () => fetchMock.mock.calls.map(([url]) => String(url));
+
+const emptyAuditLogs = { audit_logs: [], total: 0, page: 1, page_size: 50, total_pages: 0 };
+
+const defaultProps = {
+ accessToken: "sk-test",
+ token: "jwt-test",
+ userRole: "Admin",
+ userID: "user-1",
+ premiumUser: true,
+};
+
+const renderAs = (sessionRole: string) => {
+ useAuthorizedMock.mockReturnValue({ accessToken: "sk-test", userRole: sessionRole, premiumUser: true });
+ return renderWithProviders();
+};
+
+describe("SpendLogsTable network access by role", () => {
+ beforeEach(() => {
+ testQueryClient.clear();
+ vi.clearAllMocks();
+ fetchMock.mockImplementation(async (url: string) => {
+ if (String(url).includes("/audit")) {
+ return jsonResponse(emptyAuditLogs);
+ }
+ if (String(url).includes("/v2/team/list")) {
+ return jsonResponse({ teams: [] });
+ }
+ return jsonResponse({ keys: [], total_count: 0 });
+ });
+ vi.stubGlobal("fetch", fetchMock);
+ });
+
+ it("fires neither the audit nor the deleted-teams request for an internal user", async () => {
+ const user = userEvent.setup();
+ renderAs("Internal User");
+
+ // Liveness gate: the sibling Deleted Keys panel does reach the network, so a
+ // silent absence below means the gate worked, not that nothing rendered.
+ await waitFor(() => expect(requestedUrls().some((url) => url.includes("/key/list"))).toBe(true));
+
+ await user.click(screen.getByRole("tab", { name: "Deleted Keys" }));
+ await user.click(screen.getByRole("tab", { name: "Request Logs" }));
+
+ expect(requestedUrls().filter((url) => url.includes("/audit"))).toEqual([]);
+ expect(requestedUrls().filter((url) => url.includes("/v2/team/list"))).toEqual([]);
+ });
+
+ it("fetches deleted teams and audit logs for an admin", async () => {
+ const user = userEvent.setup();
+ renderAs("Admin");
+
+ await waitFor(() =>
+ expect(requestedUrls().some((url) => url.includes("/v2/team/list") && url.includes("status=deleted"))).toBe(true),
+ );
+
+ expect(requestedUrls().filter((url) => url.includes("/audit"))).toEqual([]);
+
+ await user.click(screen.getByRole("tab", { name: "Audit Logs" }));
+
+ await waitFor(() => expect(requestedUrls().some((url) => url.includes("/audit"))).toBe(true));
+ });
+
+ it("leaves the audit request unsent when an admin selects a tab after Audit Logs", async () => {
+ const user = userEvent.setup();
+ renderAs("Admin");
+
+ await user.click(screen.getByRole("tab", { name: "Deleted Teams" }));
+
+ expect(screen.getByRole("tab", { name: "Deleted Teams" })).toHaveAttribute("aria-selected", "true");
+ expect(requestedUrls().filter((url) => url.includes("/audit"))).toEqual([]);
+
+ await user.click(screen.getByRole("tab", { name: "Audit Logs" }));
+
+ await waitFor(() => expect(requestedUrls().some((url) => url.includes("/audit"))).toBe(true));
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/view_logs/index.test.tsx b/ui/litellm-dashboard/src/components/view_logs/index.test.tsx
index b2e77ec7fd5..785fa0cc6f8 100644
--- a/ui/litellm-dashboard/src/components/view_logs/index.test.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/index.test.tsx
@@ -1,9 +1,15 @@
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
-import { describe, expect, it, vi } from "vitest";
+import { beforeEach, describe, expect, it, vi } from "vitest";
import SpendLogsTable from "./index";
import { renderWithProviders } from "../../../tests/test-utils";
+const { useAuthorizedMock } = vi.hoisted(() => ({ useAuthorizedMock: vi.fn() }));
+
+vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
+ default: useAuthorizedMock,
+}));
+
vi.mock("./RequestLogsPanel", () => ({
default: function RequestLogsPanelMock({ isActive }: { isActive: boolean }) {
return {isActive ? "active" : "inactive"}
;
@@ -36,9 +42,18 @@ const defaultProps = {
premiumUser: false,
};
+const renderAs = (sessionRole: string) => {
+ useAuthorizedMock.mockReturnValue({ userRole: sessionRole });
+ return renderWithProviders();
+};
+
describe("SpendLogsTable", () => {
+ beforeEach(() => {
+ useAuthorizedMock.mockReturnValue({ userRole: "Admin" });
+ });
+
it("renders the four log tabs", () => {
- renderWithProviders();
+ renderAs("Admin");
for (const label of ["Request Logs", "Audit Logs", "Deleted Keys", "Deleted Teams"]) {
expect(screen.getByRole("tab", { name: label })).toBeInTheDocument();
@@ -47,7 +62,7 @@ describe("SpendLogsTable", () => {
it("marks only the visible tab's panel active so background tabs do not query", async () => {
const user = userEvent.setup();
- renderWithProviders();
+ renderAs("Admin");
expect(screen.getByTestId("request-logs-panel")).toHaveTextContent("active");
@@ -57,8 +72,64 @@ describe("SpendLogsTable", () => {
expect(screen.getByTestId("request-logs-panel")).toHaveTextContent("inactive");
});
+ describe("admin-only tabs", () => {
+ it.each(["Internal User", "Internal Viewer"])("hides Audit Logs and Deleted Teams from %s", (role) => {
+ renderAs(role);
+
+ expect(screen.getByRole("tab", { name: "Request Logs" })).toBeInTheDocument();
+ expect(screen.getByRole("tab", { name: "Deleted Keys" })).toBeInTheDocument();
+ expect(screen.queryByRole("tab", { name: "Audit Logs" })).not.toBeInTheDocument();
+ expect(screen.queryByRole("tab", { name: "Deleted Teams" })).not.toBeInTheDocument();
+ });
+
+ it("never mounts the panels that call the admin-only endpoints for an internal user", () => {
+ renderAs("Internal User");
+
+ expect(screen.queryByTestId("audit-logs-panel")).not.toBeInTheDocument();
+ expect(screen.queryByTestId("deleted-teams-page")).not.toBeInTheDocument();
+ expect(screen.getByTestId("deleted-keys-page")).toBeInTheDocument();
+ });
+ });
+
+ describe("tab index mapping", () => {
+ it("activates the panel the admin selected, not the one at the old hardcoded index", async () => {
+ const user = userEvent.setup();
+ renderAs("Admin");
+
+ await user.click(screen.getByRole("tab", { name: "Deleted Keys" }));
+
+ expect(screen.getByTestId("audit-logs-panel")).toHaveTextContent("inactive");
+ expect(screen.getByTestId("request-logs-panel")).toHaveTextContent("inactive");
+ });
+
+ it("keeps the audit panel inert when an admin selects the last tab", async () => {
+ const user = userEvent.setup();
+ renderAs("Admin");
+
+ await user.click(screen.getByRole("tab", { name: "Deleted Teams" }));
+
+ expect(screen.getByTestId("audit-logs-panel")).toHaveTextContent("inactive");
+ expect(screen.getByTestId("deleted-teams-page")).toBeInTheDocument();
+ });
+
+ it("selects the last visible tab for an internal user and returns to Request Logs", async () => {
+ const user = userEvent.setup();
+ renderAs("Internal User");
+
+ await user.click(screen.getByRole("tab", { name: "Deleted Keys" }));
+
+ expect(screen.getByTestId("deleted-keys-page")).toBeInTheDocument();
+ expect(screen.getByTestId("request-logs-panel")).toHaveTextContent("inactive");
+
+ await user.click(screen.getByRole("tab", { name: "Request Logs" }));
+
+ expect(screen.getByTestId("request-logs-panel")).toHaveTextContent("active");
+ });
+ });
+
describe("auth-not-ready guard", () => {
it("shows a loading spinner when credentials are not yet resolved", () => {
+ useAuthorizedMock.mockReturnValue({ userRole: "Admin" });
renderWithProviders();
expect(document.querySelector(".ant-spin")).toBeInTheDocument();
@@ -66,7 +137,7 @@ describe("SpendLogsTable", () => {
});
it("renders the tabs (no spinner) once all credentials are present", () => {
- renderWithProviders();
+ renderAs("Admin");
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
index 8e7423e3fae..7269564dcec 100644
--- a/ui/litellm-dashboard/src/components/view_logs/index.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx
@@ -1,5 +1,6 @@
import { useState } from "react";
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react";
+import useCan from "@/app/(dashboard)/hooks/useCan";
import DeletedKeysPage from "../DeletedKeysPage/DeletedKeysPage";
import DeletedTeamsPage from "../DeletedTeamsPage/DeletedTeamsPage";
import AuditLogsPanel from "./AuditLogsPanel";
@@ -14,8 +15,22 @@ interface SpendLogsTableProps {
premiumUser: boolean;
}
+type LogsTabId = "request logs" | "audit logs" | "deleted keys" | "deleted teams";
+
+interface LogsTab {
+ id: LogsTabId;
+ label: string;
+}
+
+const REQUEST_LOGS_TAB: LogsTab = { id: "request logs", label: "Request Logs" };
+const AUDIT_LOGS_TAB: LogsTab = { id: "audit logs", label: "Audit Logs" };
+const DELETED_KEYS_TAB: LogsTab = { id: "deleted keys", label: "Deleted Keys" };
+const DELETED_TEAMS_TAB: LogsTab = { id: "deleted teams", label: "Deleted Teams" };
+
export default function SpendLogsTable({ accessToken, token, userRole, userID, premiumUser }: SpendLogsTableProps) {
- const [activeTab, setActiveTab] = useState("request logs");
+ const [activeTab, setActiveTab] = useState(REQUEST_LOGS_TAB.id);
+ const canViewAuditLogs = useCan("viewAuditLogs");
+ const canViewDeletedTeams = useCan("viewDeletedTeams");
if (!accessToken || !token || !userRole || !userID) {
return (
@@ -25,41 +40,55 @@ export default function SpendLogsTable({ accessToken, token, userRole, userID, p
);
}
+ const tabs: LogsTab[] = [
+ REQUEST_LOGS_TAB,
+ ...(canViewAuditLogs ? [AUDIT_LOGS_TAB] : []),
+ DELETED_KEYS_TAB,
+ ...(canViewDeletedTeams ? [DELETED_TEAMS_TAB] : []),
+ ];
+
+ const renderPanel = (tabId: LogsTabId) => {
+ switch (tabId) {
+ case "request logs":
+ return (
+
+ );
+ case "audit logs":
+ return (
+
+ );
+ case "deleted keys":
+ return ;
+ case "deleted teams":
+ return ;
+ }
+ };
+
return (
-
setActiveTab(index === 0 ? "request logs" : "audit logs")}>
+ setActiveTab(tabs[index].id)}>
- Request Logs
- Audit Logs
- Deleted Keys
- Deleted Teams
+ {tabs.map((tab) => (
+ {tab.label}
+ ))}
-
-
-
-
-
-
-
-
-
-
-
-
+ {tabs.map((tab) => (
+ {renderPanel(tab.id)}
+ ))}
diff --git a/ui/litellm-dashboard/src/utils/capabilities.test.ts b/ui/litellm-dashboard/src/utils/capabilities.test.ts
index f48609b0b9d..611c9626065 100644
--- a/ui/litellm-dashboard/src/utils/capabilities.test.ts
+++ b/ui/litellm-dashboard/src/utils/capabilities.test.ts
@@ -18,6 +18,19 @@ describe("hasCapability", () => {
);
});
+describe.each(["viewAuditLogs", "viewDeletedTeams"] as const)("hasCapability - %s", (capability) => {
+ it.each(["Admin", "Admin Viewer", "proxy_admin", "proxy_admin_viewer"])("should grant it to %s", (role) => {
+ expect(hasCapability(role, capability)).toBe(true);
+ });
+
+ it.each(["Internal User", "Internal Viewer", "App User", "Org Admin", "Unknown Role", "", null, undefined])(
+ "should deny it to %s",
+ (role) => {
+ expect(hasCapability(role, capability)).toBe(false);
+ },
+ );
+});
+
describe("rolesWithCapability", () => {
it("should return a copy so callers cannot mutate the capability map", () => {
const roles = rolesWithCapability("viewToolPolicies");
diff --git a/ui/litellm-dashboard/src/utils/capabilities.ts b/ui/litellm-dashboard/src/utils/capabilities.ts
index 77ead2568fb..f0847cc3400 100644
--- a/ui/litellm-dashboard/src/utils/capabilities.ts
+++ b/ui/litellm-dashboard/src/utils/capabilities.ts
@@ -2,6 +2,8 @@ import { all_admin_roles } from "./roles";
const CAPABILITY_ROLES = {
viewToolPolicies: all_admin_roles,
+ viewAuditLogs: all_admin_roles,
+ viewDeletedTeams: all_admin_roles,
} as const satisfies Record;
export type Capability = keyof typeof CAPABILITY_ROLES;