diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx
index c522d4ce1e5..8b934e10779 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx
@@ -1,4 +1,3 @@
-import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import Sidebar from "@/components/leftnav";
interface SidebarProviderProps {
@@ -8,17 +7,7 @@ interface SidebarProviderProps {
}
const SidebarProvider = ({ setPage, defaultSelectedKey, sidebarCollapsed }: SidebarProviderProps) => {
- const { accessToken, userRole } = useAuthorized();
-
- return (
-
- );
+ return ;
};
export default SidebarProvider;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/organizations/useOrganizations.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/organizations/useOrganizations.ts
new file mode 100644
index 00000000000..ed194203d9a
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/organizations/useOrganizations.ts
@@ -0,0 +1,13 @@
+import { useQuery, UseQueryResult } from "@tanstack/react-query";
+import { createQueryKeys } from "../common/queryKeysFactory";
+import { organizationListCall, Organization } from "@/components/networking";
+
+const organizationKeys = createQueryKeys("organizations");
+
+export const useOrganizations = (accessToken: string | null): UseQueryResult => {
+ return useQuery({
+ queryKey: organizationKeys.list({}),
+ queryFn: async () => await organizationListCall(accessToken!),
+ enabled: Boolean(accessToken),
+ });
+};
diff --git a/ui/litellm-dashboard/src/components/leftnav.test.tsx b/ui/litellm-dashboard/src/components/leftnav.test.tsx
index 1512c8b9350..09109300dce 100644
--- a/ui/litellm-dashboard/src/components/leftnav.test.tsx
+++ b/ui/litellm-dashboard/src/components/leftnav.test.tsx
@@ -1,15 +1,8 @@
-import { act, fireEvent, render, waitFor } from "@testing-library/react";
+import { act, fireEvent, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
+import { renderWithProviders } from "../../tests/test-utils";
import Sidebar from "./leftnav";
-// Stub ResizeObserver used by antd in jsdom
-class ResizeObserver {
- observe() {}
- unobserve() {}
- disconnect() {}
-}
-(global as any).ResizeObserver = ResizeObserver;
-
vi.mock("../utils/roles", () => {
return {
all_admin_roles: ["admin"],
@@ -19,17 +12,53 @@ vi.mock("../utils/roles", () => {
};
});
+const { mockUseAuthorized, mockUseOrganizations } = vi.hoisted(() => {
+ const mockUseAuthorized = vi.fn(() => ({
+ userId: "test-user-id",
+ accessToken: "test-access-token",
+ userRole: "admin",
+ token: "test-token",
+ userEmail: "test@example.com",
+ premiumUser: false,
+ disabledPersonalKeyCreation: false,
+ showSSOBanner: false,
+ }));
+
+ const mockUseOrganizations = vi.fn(() => ({
+ data: [],
+ isLoading: false,
+ error: null,
+ }));
+
+ return { mockUseAuthorized, mockUseOrganizations };
+});
+
+vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
+ default: mockUseAuthorized,
+}));
+
+vi.mock("@/app/(dashboard)/hooks/organizations/useOrganizations", () => ({
+ useOrganizations: mockUseOrganizations,
+}));
+
+vi.mock("@/app/(dashboard)/hooks/uiConfig/useUIConfig", () => {
+ return {
+ useUIConfig: () => ({
+ data: { admin_ui_disabled: false },
+ isLoading: false,
+ }),
+ };
+});
+
describe("Sidebar (leftnav)", () => {
const defaultProps = {
- accessToken: null as string | null,
setPage: vi.fn(),
- userRole: "admin",
defaultSelectedKey: "api-keys",
collapsed: false,
};
it("renders all top-level (non-nested) tabs for admin", () => {
- const { getByText } = render();
+ renderWithProviders();
const topLevelLabels = [
"Virtual Keys",
@@ -51,19 +80,19 @@ describe("Sidebar (leftnav)", () => {
];
topLevelLabels.forEach((label) => {
- expect(getByText(label)).toBeInTheDocument();
+ expect(screen.getByText(label)).toBeInTheDocument();
});
});
it("expands a nested tab to reveal its children (Tools > Search Tools)", async () => {
- const { getByText, queryByText } = render();
+ renderWithProviders();
- expect(queryByText("Search Tools")).not.toBeInTheDocument();
+ expect(screen.queryByText("Search Tools")).not.toBeInTheDocument();
act(() => {
- fireEvent.click(getByText("Tools"));
+ fireEvent.click(screen.getByText("Tools"));
});
await waitFor(() => {
- expect(getByText("Search Tools")).toBeInTheDocument();
+ expect(screen.getByText("Search Tools")).toBeInTheDocument();
});
});
it("has no duplicate keys among all menu items and their children", () => {
@@ -82,7 +111,7 @@ describe("Sidebar (leftnav)", () => {
return allKeys;
}
- const { container } = render();
+ const { container } = renderWithProviders();
const allRenderedKeys = getAllKeysFromMenu(container);
const keySet = new Set();
@@ -95,4 +124,43 @@ describe("Sidebar (leftnav)", () => {
}
expect(duplicates).toHaveLength(0);
});
+
+ it("should show Organizations tab for organization admins", () => {
+ mockUseAuthorized.mockReturnValueOnce({
+ userId: "org-admin-user-id",
+ accessToken: "test-access-token",
+ userRole: "viewer",
+ token: "test-token",
+ userEmail: "orgadmin@example.com",
+ premiumUser: false,
+ disabledPersonalKeyCreation: false,
+ showSSOBanner: false,
+ });
+
+ mockUseOrganizations.mockReturnValueOnce({
+ data: [
+ {
+ organization_id: "org-1",
+ organization_name: "Test Organization",
+ spend: 0,
+ max_budget: null,
+ models: [],
+ tpm_limit: null,
+ rpm_limit: null,
+ members: [
+ {
+ user_id: "org-admin-user-id",
+ user_role: "org_admin",
+ },
+ ],
+ },
+ ],
+ isLoading: false,
+ error: null,
+ } as any);
+
+ renderWithProviders();
+
+ expect(screen.getByText("Organizations")).toBeInTheDocument();
+ });
});
diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx
index ec000f7582e..6f716ccd66c 100644
--- a/ui/litellm-dashboard/src/components/leftnav.tsx
+++ b/ui/litellm-dashboard/src/components/leftnav.tsx
@@ -1,3 +1,5 @@
+import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
+import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import {
ApiOutlined,
AppstoreOutlined,
@@ -21,17 +23,17 @@ import {
ToolOutlined,
UserOutlined,
} from "@ant-design/icons";
-import { Badge, ConfigProvider, Layout, Menu } from "antd";
import type { MenuProps } from "antd";
+import { Badge, ConfigProvider, Layout, Menu } from "antd";
+import { useMemo } from "react";
import { all_admin_roles, internalUserRoles, isAdminRole, rolesWithWriteAccess } from "../utils/roles";
+import type { Organization } from "./networking";
import UsageIndicator from "./usage_indicator";
const { Sider } = Layout;
// Define the props type
interface SidebarProps {
- accessToken: string | null;
setPage: (page: string) => void;
- userRole: string;
defaultSelectedKey: string;
collapsed?: boolean;
}
@@ -53,7 +55,18 @@ interface MenuGroup {
roles?: string[];
}
-const Sidebar: React.FC = ({ accessToken, setPage, userRole, defaultSelectedKey, collapsed = false }) => {
+const Sidebar: React.FC = ({ setPage, defaultSelectedKey, collapsed = false }) => {
+ const { userId, accessToken, userRole } = useAuthorized();
+ const { data: organizations } = useOrganizations(accessToken);
+
+ // Check if user is an org_admin
+ 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]);
+
// Navigate to page helper
const navigateToPage = (page: string) => {
const newSearchParams = new URLSearchParams(window.location.search);
@@ -146,7 +159,7 @@ const Sidebar: React.FC = ({ accessToken, setPage, userRole, defau
Usage
- ),
+ ),
},
{
key: "logs",
@@ -302,7 +315,13 @@ const Sidebar: React.FC = ({ accessToken, setPage, userRole, defau
// Filter items based on user role
const filterItemsByRole = (items: MenuItem[]): MenuItem[] => {
return items
- .filter((item) => !item.roles || item.roles.includes(userRole))
+ .filter((item) => {
+ // Special handling for organizations menu item - allow org_admins
+ if (item.key === "organizations") {
+ return !item.roles || item.roles.includes(userRole) || isOrgAdmin;
+ }
+ return !item.roles || item.roles.includes(userRole);
+ })
.map((item) => ({
...item,
children: item.children ? filterItemsByRole(item.children) : undefined,