Merge pull request #36478 from BerriAI/litellm_/vibrant-booth-d4258b

fix(ui): restore the Logs Deleted Teams tab for organization admins
This commit is contained in:
yuneng-jiang 2026-08-10 17:18:26 -07:00 committed by GitHub
commit 80f34cb6fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 298 additions and 30 deletions

View file

@ -3,10 +3,12 @@
import { hasCapability, type Capability } from "@/utils/capabilities";
import useAuthorized from "./useAuthorized";
import useIsOrgAdmin from "./useIsOrgAdmin";
const useCan = (capability: Capability): boolean => {
const { userRole } = useAuthorized();
return hasCapability(userRole, capability);
const isOrgAdmin = useIsOrgAdmin();
return hasCapability(userRole, capability, isOrgAdmin);
};
export default useCan;

View file

@ -0,0 +1,53 @@
/* @vitest-environment jsdom */
import { renderHook } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Organization } from "@/components/networking";
import useIsOrgAdmin from "./useIsOrgAdmin";
const { useAuthorizedMock, useOrganizationsMock } = vi.hoisted(() => ({
useAuthorizedMock: vi.fn(),
useOrganizationsMock: vi.fn(),
}));
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ default: useAuthorizedMock }));
vi.mock("./organizations/useOrganizations", () => ({ useOrganizations: useOrganizationsMock }));
const orgWithMembers = (members: { user_id: string; user_role: string }[]): Organization =>
({ organization_id: "org-1", members }) as unknown as Organization;
const renderAs = (userRole: string, organizations: Organization[] | undefined) => {
useAuthorizedMock.mockReturnValue({ userId: "user-1", userRole });
useOrganizationsMock.mockReturnValue({ data: organizations });
return renderHook(() => useIsOrgAdmin()).result;
};
describe("useIsOrgAdmin", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("is true for the session a real org admin carries: internal_user plus an org_admin membership", () => {
const result = renderAs("Internal User", [orgWithMembers([{ user_id: "user-1", user_role: "org_admin" }])]);
expect(result.current).toBe(true);
});
it("is false for an internal user with no org_admin membership", () => {
const result = renderAs("Internal User", [orgWithMembers([{ user_id: "user-1", user_role: "internal_user" }])]);
expect(result.current).toBe(false);
});
it("is false while the organization list is still loading", () => {
const result = renderAs("Internal User", undefined);
expect(result.current).toBe(false);
});
it("is true for a session role of org_admin even with no membership rows", () => {
expect(renderAs("org_admin", []).current).toBe(true);
expect(renderAs("Org Admin", []).current).toBe(true);
});
it("is false for a proxy admin, who is covered by role-based gates instead", () => {
const result = renderAs("Admin", []);
expect(result.current).toBe(false);
});
});

View file

@ -0,0 +1,14 @@
"use client";
import { isOrgAdminForAnyOrg, isOrgAdminSessionRole } from "@/utils/roles";
import { useOrganizations } from "./organizations/useOrganizations";
import useAuthorized from "./useAuthorized";
const useIsOrgAdmin = (): boolean => {
const { userId, userRole } = useAuthorized();
const { data: organizations } = useOrganizations();
return isOrgAdminSessionRole(userRole) || isOrgAdminForAnyOrg(organizations, userId);
};
export default useIsOrgAdmin;

View file

@ -1,4 +1,5 @@
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
import { renderWithProviders as render } from "@/../tests/test-utils";
import { beforeEach, describe, expect, it, vi } from "vitest";
import ChatUI from "./ChatUI";
import * as fetchModelsModule from "@/components/llm_calls/fetch_models";

View file

@ -3,8 +3,10 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../../tests/test-utils";
import Sidebar, { menuGroups, getBreadcrumb } from "./leftnav";
vi.mock("../utils/roles", () => {
vi.mock("../utils/roles", async (importOriginal) => {
const actual = await importOriginal<typeof import("../utils/roles")>();
return {
...actual,
all_admin_roles: ["admin", "admin_viewer"],
old_admin_roles: ["admin", "admin_viewer"],
internalUserRoles: ["internal"],
@ -92,6 +94,11 @@ describe("Sidebar (leftnav)", () => {
collapsed: false,
};
afterEach(() => {
mockUseAuthorized.mockReset();
mockUseOrganizations.mockReset();
});
it("should link the logo to the UI home route rather than the proxy origin", () => {
renderWithProviders(<Sidebar {...defaultProps} />);
@ -175,19 +182,19 @@ describe("Sidebar (leftnav)", () => {
};
it("hides Playground from Admin Viewer (cost-incurring action)", () => {
mockUseAuthorized.mockReturnValueOnce(adminViewerAuth);
mockUseAuthorized.mockReturnValue(adminViewerAuth);
renderWithProviders(<Sidebar {...defaultProps} />);
expect(screen.queryByText("Playground")).not.toBeInTheDocument();
});
it("shows Models + Endpoints to Admin Viewer (read-only)", () => {
mockUseAuthorized.mockReturnValueOnce(adminViewerAuth);
mockUseAuthorized.mockReturnValue(adminViewerAuth);
renderWithProviders(<Sidebar {...defaultProps} />);
expect(screen.getByText("Models + Endpoints")).toBeInTheDocument();
});
it("shows Agents (under Agentic) to Admin Viewer (read-only)", async () => {
mockUseAuthorized.mockReturnValueOnce(adminViewerAuth);
mockUseAuthorized.mockReturnValue(adminViewerAuth);
renderWithProviders(<Sidebar {...defaultProps} />);
// Agents is now nested under the "Agentic" submenu — expand parent
// first to render the children, then assert Agents is visible.
@ -200,7 +207,7 @@ describe("Sidebar (leftnav)", () => {
});
it("shows Logs to Admin Viewer", () => {
mockUseAuthorized.mockReturnValueOnce(adminViewerAuth);
mockUseAuthorized.mockReturnValue(adminViewerAuth);
renderWithProviders(<Sidebar {...defaultProps} />);
expect(screen.getByText("Logs")).toBeInTheDocument();
});
@ -384,10 +391,11 @@ describe("Sidebar (leftnav)", () => {
});
it("should show Organizations tab for organization admins", () => {
mockUseAuthorized.mockReturnValueOnce({
mockUseAuthorized.mockReturnValue({
userId: "org-admin-user-id",
accessToken: "test-access-token",
userRole: "viewer",
isViewOnly: false,
token: "test-token",
userEmail: "orgadmin@example.com",
premiumUser: false,
@ -395,7 +403,7 @@ describe("Sidebar (leftnav)", () => {
showSSOBanner: false,
});
mockUseOrganizations.mockReturnValueOnce({
mockUseOrganizations.mockReturnValue({
data: [
{
organization_id: "org-1",

View file

@ -1,6 +1,6 @@
import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import useIsOrgAdmin from "@/app/(dashboard)/hooks/useIsOrgAdmin";
import { useHealthReadinessDetails } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails";
import { useLogout } from "@/app/(dashboard)/hooks/useLogout";
import { getProxyBaseUrl } from "@/components/networking";
@ -75,7 +75,6 @@ import {
} from "../utils/roles";
import BetaBadge from "./BetaBadge";
import NewBadge from "./common_components/NewBadge";
import type { Organization } from "./networking";
import SidebarAccountMenu from "./SidebarAccountMenu/SidebarAccountMenu";
import SidebarUsageCard from "./SidebarUsageCard";
import { MIGRATED_PAGES, migratedHref, legacyPageHref } from "@/utils/migratedPages";
@ -432,7 +431,7 @@ const Sidebar_: React.FC<SidebarProps> = ({
allowVectorStoresForTeamAdmins,
}) => {
const { userId, accessToken, userRole, isViewOnly } = useAuthorized();
const { data: organizations } = useOrganizations();
const isOrgAdmin = useIsOrgAdmin();
const { data: teams } = useTeams();
const { logoUrl } = useTheme();
const { data: healthData } = useHealthReadinessDetails(accessToken);
@ -459,13 +458,6 @@ const Sidebar_: React.FC<SidebarProps> = ({
}
}
const isOrgAdmin = useMemo(() => {
if (!userId || !organizations) return false;
return organizations.some((org: Organization) =>
org.members?.some((member) => member.user_id === userId && member.user_role === "org_admin"),
);
}, [userId, organizations]);
const isTeamAdmin = useMemo(() => isUserTeamAdminForAnyTeam(teams ?? null, userId ?? ""), [teams, userId]);
const filterItemsByRole = (items: MenuItem[]): MenuItem[] => {

View file

@ -4,12 +4,19 @@ 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() }));
const { useAuthorizedMock, useOrganizationsMock } = vi.hoisted(() => ({
useAuthorizedMock: vi.fn(),
useOrganizationsMock: vi.fn(),
}));
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: useAuthorizedMock,
}));
vi.mock("@/app/(dashboard)/hooks/organizations/useOrganizations", () => ({
useOrganizations: useOrganizationsMock,
}));
vi.mock("./RequestLogsPanel", () => ({
default: function RequestLogsPanelMock() {
return <div data-testid="request-logs-panel" />;
@ -37,8 +44,16 @@ const defaultProps = {
premiumUser: true,
};
const renderAs = (sessionRole: string) => {
useAuthorizedMock.mockReturnValue({ accessToken: "sk-test", userRole: sessionRole, premiumUser: true });
const ORG_ADMIN_MEMBERSHIPS = [{ organization_id: "org-1", members: [{ user_id: "user-1", user_role: "org_admin" }] }];
const renderAs = (sessionRole: string, organizations: unknown[] = []) => {
useAuthorizedMock.mockReturnValue({
accessToken: "sk-test",
userId: "user-1",
userRole: sessionRole,
premiumUser: true,
});
useOrganizationsMock.mockReturnValue({ data: organizations });
return renderWithProviders(<SpendLogsTable {...defaultProps} userRole={sessionRole} />);
};
@ -46,6 +61,7 @@ describe("SpendLogsTable network access by role", () => {
beforeEach(() => {
testQueryClient.clear();
vi.clearAllMocks();
useOrganizationsMock.mockReturnValue({ data: [] });
fetchMock.mockImplementation(async (url: string) => {
if (String(url).includes("/audit")) {
return jsonResponse(emptyAuditLogs);
@ -73,6 +89,16 @@ describe("SpendLogsTable network access by role", () => {
expect(requestedUrls().filter((url) => url.includes("/v2/team/list"))).toEqual([]);
});
it("fetches the deleted teams an org admin is entitled to, and still no audit logs", async () => {
renderAs("Internal User", ORG_ADMIN_MEMBERSHIPS);
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([]);
});
it("fetches deleted teams and audit logs for an admin", async () => {
const user = userEvent.setup();
renderAs("Admin");

View file

@ -4,12 +4,19 @@ 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() }));
const { useAuthorizedMock, useOrganizationsMock } = vi.hoisted(() => ({
useAuthorizedMock: vi.fn(),
useOrganizationsMock: vi.fn(),
}));
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: useAuthorizedMock,
}));
vi.mock("@/app/(dashboard)/hooks/organizations/useOrganizations", () => ({
useOrganizations: useOrganizationsMock,
}));
vi.mock("./RequestLogsPanel", () => ({
default: function RequestLogsPanelMock({ isActive }: { isActive: boolean }) {
return <div data-testid="request-logs-panel">{isActive ? "active" : "inactive"}</div>;
@ -42,14 +49,20 @@ const defaultProps = {
premiumUser: false,
};
const renderAs = (sessionRole: string) => {
useAuthorizedMock.mockReturnValue({ userRole: sessionRole });
const ORG_ADMIN_MEMBERSHIPS = [{ organization_id: "org-1", members: [{ user_id: "user-1", user_role: "org_admin" }] }];
const renderAs = (sessionRole: string, organizations: unknown[] = []) => {
useAuthorizedMock.mockReturnValue({ userId: "user-1", userRole: sessionRole });
useOrganizationsMock.mockReturnValue({ data: organizations });
return renderWithProviders(<SpendLogsTable {...defaultProps} userRole={sessionRole} />);
};
const tabNames = () => screen.getAllByRole("tab").map((tab) => tab.textContent);
describe("SpendLogsTable", () => {
beforeEach(() => {
useAuthorizedMock.mockReturnValue({ userRole: "Admin" });
useAuthorizedMock.mockReturnValue({ userId: "user-1", userRole: "Admin" });
useOrganizationsMock.mockReturnValue({ data: [] });
});
it("renders the four log tabs", () => {
@ -91,6 +104,44 @@ describe("SpendLogsTable", () => {
});
});
describe("organization admins", () => {
it("shows Deleted Teams to an org admin, whose session role reads as a plain internal user", () => {
renderAs("Internal User", ORG_ADMIN_MEMBERSHIPS);
expect(screen.getByRole("tab", { name: "Deleted Teams" })).toBeInTheDocument();
expect(screen.getByTestId("deleted-teams-page")).toBeInTheDocument();
});
it("does not hand an org admin the Audit Logs tab, which the backend still refuses them", () => {
renderAs("Internal User", ORG_ADMIN_MEMBERSHIPS);
expect(tabNames()).toEqual(["Request Logs", "Deleted Keys", "Deleted Teams"]);
expect(screen.queryByTestId("audit-logs-panel")).not.toBeInTheDocument();
});
it("keeps an internal user in the same org without an org_admin membership at two tabs", () => {
renderAs("Internal User", [
{ organization_id: "org-1", members: [{ user_id: "user-1", user_role: "internal_user" }] },
]);
expect(tabNames()).toEqual(["Request Logs", "Deleted Keys"]);
});
it("activates the org admin's selected tab rather than the one at the four-tab index", async () => {
const user = userEvent.setup();
renderAs("Internal User", ORG_ADMIN_MEMBERSHIPS);
await user.click(screen.getByRole("tab", { name: "Deleted Teams" }));
expect(screen.getByRole("tab", { name: "Deleted Teams" })).toHaveAttribute("aria-selected", "true");
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("tab index mapping", () => {
it("activates the panel the admin selected, not the one at the old hardcoded index", async () => {
const user = userEvent.setup();

View file

@ -45,6 +45,43 @@ describe("hasCapability", () => {
});
});
const SESSION_ROLE_AN_ORG_ADMIN_ACTUALLY_CARRIES = "Internal User";
const ORG_ADMIN_BACKEND_ACCESS: ReadonlyArray<readonly [Capability, string, boolean]> = [
["viewDeletedTeams", "GET /v2/team/list?status=deleted -> 200 (scoped to their orgs)", true],
["viewToolPolicies", "GET /v1/tool/list -> 401", false],
["viewPolicies", "GET /policies/list -> 401", false],
["viewPrompts", "GET /prompts/list -> 401", false],
["viewAuditLogs", "GET /audit -> 401", false],
];
describe("hasCapability for organization admins", () => {
it.each(ORG_ADMIN_BACKEND_ACCESS)("%s matches the backend: %s", (capability, _endpoint, isEntitled) => {
expect(hasCapability(SESSION_ROLE_AN_ORG_ADMIN_ACTUALLY_CARRIES, capability, true)).toBe(isEntitled);
});
it.each(NON_ADMIN_ROLES)("grants viewDeletedTeams to an org admin whose session role is %s", (role) => {
expect(hasCapability(role, "viewDeletedTeams", true)).toBe(true);
});
it.each(ADMIN_ONLY_CAPABILITIES)("leaves %s denied when the caller is not an org admin", (capability) => {
expect(hasCapability(SESSION_ROLE_AN_ORG_ADMIN_ACTUALLY_CARRIES, capability, false)).toBe(false);
expect(hasCapability(SESSION_ROLE_AN_ORG_ADMIN_ACTUALLY_CARRIES, capability)).toBe(false);
});
it("keeps the org-admin allowance opt-in per capability", () => {
const orgAdminCapabilities = ADMIN_ONLY_CAPABILITIES.filter((capability) =>
hasCapability(SESSION_ROLE_AN_ORG_ADMIN_ACTUALLY_CARRIES, capability, true),
);
expect(orgAdminCapabilities).toEqual(["viewDeletedTeams"]);
});
it("does not let the org-admin allowance reopen the proxy-admin-only viewGlobalSpend gate", () => {
expect(hasCapability(SESSION_ROLE_AN_ORG_ADMIN_ACTUALLY_CARRIES, "viewGlobalSpend", true)).toBe(false);
expect(hasCapability("Org Admin", "viewGlobalSpend", true)).toBe(false);
});
});
describe("hasCapability - viewGlobalSpend", () => {
it.each(ADMIN_ROLES)("should grant it to %s", (role) => {
expect(hasCapability(role, "viewGlobalSpend")).toBe(true);

View file

@ -19,7 +19,14 @@ const CAPABILITY_ROLES = {
export type Capability = keyof typeof CAPABILITY_ROLES;
export const hasCapability = (userRole: string | null | undefined, capability: Capability): boolean =>
userRole != null && CAPABILITY_ROLES[capability].includes(userRole);
const ORG_ADMIN_CAPABILITIES: ReadonlySet<Capability> = new Set<Capability>(["viewDeletedTeams"]);
export const hasCapability = (
userRole: string | null | undefined,
capability: Capability,
isOrgAdmin: boolean = false,
): boolean =>
(isOrgAdmin && ORG_ADMIN_CAPABILITIES.has(capability)) ||
(userRole != null && CAPABILITY_ROLES[capability].includes(userRole));
export const rolesWithCapability = (capability: Capability): string[] => [...CAPABILITY_ROLES[capability]];

View file

@ -3,6 +3,8 @@ import {
all_admin_roles,
effectiveSessionRole,
isAdminRole,
isOrgAdminForAnyOrg,
isOrgAdminSessionRole,
isProxyAdminRole,
isUserTeamAdminForAnyTeam,
isUserTeamAdminForSingleTeam,
@ -11,7 +13,10 @@ import {
rolesWithWriteAccess,
teamListScopeUserId,
} from "./roles";
import { Team } from "@/components/networking";
import { Organization, Team } from "@/components/networking";
const orgWithMembers = (members: { user_id: string; user_role: string }[]): Organization =>
({ organization_id: "org-1", members }) as unknown as Organization;
describe("roles", () => {
describe("isAdminRole", () => {
@ -156,6 +161,55 @@ describe("roles", () => {
});
});
describe("isOrgAdminForAnyOrg", () => {
it("returns true when the user holds an org_admin membership in any organization", () => {
const organizations = [
orgWithMembers([{ user_id: "user-1", user_role: "internal_user" }]),
orgWithMembers([{ user_id: "user-1", user_role: "org_admin" }]),
];
expect(isOrgAdminForAnyOrg(organizations, "user-1")).toBe(true);
});
it("returns false when the user is only a plain member", () => {
const organizations = [orgWithMembers([{ user_id: "user-1", user_role: "internal_user" }])];
expect(isOrgAdminForAnyOrg(organizations, "user-1")).toBe(false);
});
it("does not credit one user with another user's org_admin membership", () => {
const organizations = [orgWithMembers([{ user_id: "user-2", user_role: "org_admin" }])];
expect(isOrgAdminForAnyOrg(organizations, "user-1")).toBe(false);
});
it("returns false for missing organizations, missing members, or a missing user id", () => {
expect(isOrgAdminForAnyOrg(null, "user-1")).toBe(false);
expect(isOrgAdminForAnyOrg(undefined, "user-1")).toBe(false);
expect(isOrgAdminForAnyOrg([], "user-1")).toBe(false);
expect(isOrgAdminForAnyOrg([{ organization_id: "org-1" } as unknown as Organization], "user-1")).toBe(false);
expect(isOrgAdminForAnyOrg([orgWithMembers([{ user_id: "user-1", user_role: "org_admin" }])], null)).toBe(false);
expect(isOrgAdminForAnyOrg([orgWithMembers([{ user_id: "user-1", user_role: "org_admin" }])], "")).toBe(false);
});
});
describe("isOrgAdminSessionRole", () => {
it("accepts both the raw and the formatted org admin role", () => {
expect(isOrgAdminSessionRole("org_admin")).toBe(true);
expect(isOrgAdminSessionRole(effectiveSessionRole("org_admin"))).toBe(true);
});
it("returns false for the role a membership-granted org admin actually carries", () => {
expect(isOrgAdminSessionRole("Internal User")).toBe(false);
expect(isOrgAdminSessionRole("internal_user")).toBe(false);
});
it("returns false for admin and missing roles", () => {
expect(isOrgAdminSessionRole("Admin")).toBe(false);
expect(isOrgAdminSessionRole("proxy_admin")).toBe(false);
expect(isOrgAdminSessionRole(null)).toBe(false);
expect(isOrgAdminSessionRole(undefined)).toBe(false);
expect(isOrgAdminSessionRole("")).toBe(false);
});
});
describe("rolesAllowedToViewWriteScopedPages", () => {
it("includes Admin Viewer (both display and stored forms)", () => {
// Admin Viewer follows the read-parity rule — they must be able to

View file

@ -1,4 +1,11 @@
import { Member, Team } from "@/components/networking";
import { Member, Organization, Team } from "@/components/networking";
const ORG_ADMIN_MEMBERSHIP_ROLE = "org_admin";
interface OrganizationMembership {
user_id?: string | null;
user_role?: string | null;
}
// Define admin roles and permissions
export const old_admin_roles = ["Admin", "Admin Viewer"];
@ -39,6 +46,19 @@ export const isUserTeamAdminForSingleTeam = (teamMemberWithRoles: Member[] | nul
return teamMemberWithRoles.some((member) => member.user_id === userID && member.role === "admin");
};
export const isOrgAdminForAnyOrg = (
organizations: Organization[] | null | undefined,
userID: string | null | undefined,
): boolean => {
if (organizations == null || !userID) {
return false;
}
return organizations.some((org) => {
const members: OrganizationMembership[] = org.members ?? [];
return members.some((member) => member.user_id === userID && member.user_role === ORG_ADMIN_MEMBERSHIP_ROLE);
});
};
export const formatUserRole = (userRole: string): string => {
if (!userRole) {
return "Undefined Role";
@ -66,6 +86,9 @@ export const formatUserRole = (userRole: string): string => {
}
};
export const isOrgAdminSessionRole = (userRole?: string | null): boolean =>
userRole === ORG_ADMIN_MEMBERSHIP_ROLE || userRole === formatUserRole(ORG_ADMIN_MEMBERSHIP_ROLE);
const viewOnlyRawRoles = ["proxy_admin_viewer", "internal_user_viewer", "internal_viewer"];
export const effectiveSessionRole = (rawUserRole?: string): string => {