diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.test.tsx index b165b71be7..428f52dd98 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.test.tsx @@ -1,7 +1,7 @@ /* @vitest-environment jsdom */ -import { render } from "@testing-library/react"; -import { describe, it, expect, vi } from "vitest"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { render } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import ModelsAndEndpointsView from "./ModelsAndEndpointsView"; // Minimal stubs to avoid Next.js router and network usage during render @@ -57,21 +57,40 @@ vi.mock("@/app/(dashboard)/hooks/useTeams", () => ({ }), })); +const mockUseModelsInfo = vi.fn(); +vi.mock("@/app/(dashboard)/hooks/models/useModels", () => ({ + useModelsInfo: () => mockUseModelsInfo(), +})); + +const mockUseUISettings = vi.fn(); +vi.mock("@/app/(dashboard)/hooks/uiSettings/useUISettings", () => ({ + useUISettings: () => mockUseUISettings(), +})); + const createQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } }, }); describe("ModelsAndEndpointsView", () => { - it("should render the models and endpoints view", async () => { - // JSDOM polyfill for libraries expecting ResizeObserver (e.g., recharts) - // Note: ResizeObserver is now globally mocked in setupTests.ts, but keeping this for backwards compatibility + beforeEach(() => { + mockUseModelsInfo.mockReturnValue({ + data: { data: [] }, + isLoading: false, + refetch: vi.fn(), + }); + mockUseUISettings.mockReturnValue({ + data: { values: {} }, + }); // eslint-disable-next-line @typescript-eslint/no-explicit-any (global as any).ResizeObserver = class { observe() {} unobserve() {} disconnect() {} }; + }); + + it("should render the models and endpoints view", async () => { const queryClient = createQueryClient(); const { findByText } = render( diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx index 7b6199bc88..ffb92d1897 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx @@ -36,7 +36,7 @@ import ModelAnalyticsTab from "@/app/(dashboard)/models-and-endpoints/components import ModelRetrySettingsTab from "@/app/(dashboard)/models-and-endpoints/components/ModelRetrySettingsTab"; import PriceDataManagementTab from "@/app/(dashboard)/models-and-endpoints/components/PriceDataManagementTab"; import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings"; -import { all_admin_roles, internalUserRoles } from "@/utils/roles"; +import { all_admin_roles, internalUserRoles, isProxyAdminRole, isUserTeamAdminForAnyTeam } from "@/utils/roles"; import HealthCheckComponent from "../../../components/model_dashboard/HealthCheckComponent"; import ModelGroupAliasSettings from "../../../components/model_group_alias_settings"; import NotificationsManager from "../../../components/molecules/notifications_manager"; @@ -161,8 +161,13 @@ const ModelsAndEndpointsView: React.FC = ({ const credentialsList = credentialsResponse?.credentials || []; const { data: uiSettings } = useUISettings(accessToken || ""); + const isProxyAdmin = userRole && isProxyAdminRole(userRole); const isInternalUser = userRole && internalUserRoles.includes(userRole); - const shouldHideAddModelTab = isInternalUser && uiSettings?.values?.disable_model_add_for_internal_users === true; + const isUserTeamAdmin = userID && isUserTeamAdminForAnyTeam(teams, userID); + const addModelDisabledForInternalUsers = + isInternalUser && uiSettings?.values?.disable_model_add_for_internal_users === true; + // Hide tab if user is NOT a proxy admin AND (internal user with setting enabled OR not a team admin) + const shouldHideAddModelTab = !isProxyAdmin && (addModelDisabledForInternalUsers || !isUserTeamAdmin); const setProviderModelsFn = (provider: Providers) => { const _providerModels = getProviderModels(provider, modelMap); diff --git a/ui/litellm-dashboard/src/utils/roles.test.ts b/ui/litellm-dashboard/src/utils/roles.test.ts new file mode 100644 index 0000000000..f2d1812714 --- /dev/null +++ b/ui/litellm-dashboard/src/utils/roles.test.ts @@ -0,0 +1,196 @@ +import { describe, it, expect } from "vitest"; +import { isAdminRole, isProxyAdminRole, isUserTeamAdminForAnyTeam, isUserTeamAdminForSingleTeam } from "./roles"; +import { Team } from "@/components/networking"; + +describe("roles", () => { + describe("isAdminRole", () => { + it("should return true for all admin roles", () => { + expect(isAdminRole("Admin")).toBe(true); + expect(isAdminRole("Admin Viewer")).toBe(true); + expect(isAdminRole("proxy_admin")).toBe(true); + expect(isAdminRole("proxy_admin_viewer")).toBe(true); + expect(isAdminRole("org_admin")).toBe(true); + }); + + it("should return false for non-admin roles", () => { + expect(isAdminRole("Internal User")).toBe(false); + expect(isAdminRole("Internal Viewer")).toBe(false); + expect(isAdminRole("regular_user")).toBe(false); + expect(isAdminRole("")).toBe(false); + }); + }); + + describe("isProxyAdminRole", () => { + it("should return true for proxy_admin and Admin roles", () => { + expect(isProxyAdminRole("proxy_admin")).toBe(true); + expect(isProxyAdminRole("Admin")).toBe(true); + }); + + it("should return false for other admin roles", () => { + expect(isProxyAdminRole("Admin Viewer")).toBe(false); + expect(isProxyAdminRole("proxy_admin_viewer")).toBe(false); + expect(isProxyAdminRole("org_admin")).toBe(false); + }); + + it("should return false for non-admin roles", () => { + expect(isProxyAdminRole("Internal User")).toBe(false); + expect(isProxyAdminRole("Internal Viewer")).toBe(false); + expect(isProxyAdminRole("regular_user")).toBe(false); + expect(isProxyAdminRole("")).toBe(false); + }); + }); + + describe("isUserTeamAdminForSingleTeam", () => { + it("should return true when user is team admin", () => { + const team: Team = { + team_id: "team-1", + team_alias: "Test Team", + models: [], + max_budget: null, + budget_duration: null, + tpm_limit: null, + rpm_limit: null, + organization_id: "org-1", + created_at: "2024-01-01", + keys: [], + members_with_roles: [ + { user_id: "user-1", user_email: "user1@test.com", role: "admin" }, + { user_id: "user-2", user_email: "user2@test.com", role: "user" }, + ], + }; + expect(isUserTeamAdminForSingleTeam(team, "user-1")).toBe(true); + }); + + it("should return false when user is not team admin", () => { + const team: Team = { + team_id: "team-1", + team_alias: "Test Team", + models: [], + max_budget: null, + budget_duration: null, + tpm_limit: null, + rpm_limit: null, + organization_id: "org-1", + created_at: "2024-01-01", + keys: [], + members_with_roles: [ + { user_id: "user-1", user_email: "user1@test.com", role: "user" }, + { user_id: "user-2", user_email: "user2@test.com", role: "user" }, + ], + }; + expect(isUserTeamAdminForSingleTeam(team, "user-1")).toBe(false); + }); + + it("should return false when user is not in team", () => { + const team: Team = { + team_id: "team-1", + team_alias: "Test Team", + models: [], + max_budget: null, + budget_duration: null, + tpm_limit: null, + rpm_limit: null, + organization_id: "org-1", + created_at: "2024-01-01", + keys: [], + members_with_roles: [{ user_id: "user-2", user_email: "user2@test.com", role: "admin" }], + }; + expect(isUserTeamAdminForSingleTeam(team, "user-1")).toBe(false); + }); + + it("should return false when team is null", () => { + expect(isUserTeamAdminForSingleTeam(null, "user-1")).toBe(false); + }); + + it("should return false when members_with_roles is null", () => { + const team = { + team_id: "team-1", + team_alias: "Test Team", + models: [], + max_budget: null, + budget_duration: null, + tpm_limit: null, + rpm_limit: null, + organization_id: "org-1", + created_at: "2024-01-01", + keys: [], + members_with_roles: [], + } as Team; + expect(isUserTeamAdminForSingleTeam(team, "user-1")).toBe(false); + }); + }); + + describe("isUserTeamAdminForAnyTeam", () => { + it("should return true when user is admin of at least one team", () => { + const teams: Team[] = [ + { + team_id: "team-1", + team_alias: "Test Team 1", + models: [], + max_budget: null, + budget_duration: null, + tpm_limit: null, + rpm_limit: null, + organization_id: "org-1", + created_at: "2024-01-01", + keys: [], + members_with_roles: [{ user_id: "user-1", user_email: "user1@test.com", role: "user" }], + }, + { + team_id: "team-2", + team_alias: "Test Team 2", + models: [], + max_budget: null, + budget_duration: null, + tpm_limit: null, + rpm_limit: null, + organization_id: "org-1", + created_at: "2024-01-01", + keys: [], + members_with_roles: [{ user_id: "user-1", user_email: "user1@test.com", role: "admin" }], + }, + ]; + expect(isUserTeamAdminForAnyTeam(teams, "user-1")).toBe(true); + }); + + it("should return false when user is not admin of any team", () => { + const teams: Team[] = [ + { + team_id: "team-1", + team_alias: "Test Team 1", + models: [], + max_budget: null, + budget_duration: null, + tpm_limit: null, + rpm_limit: null, + organization_id: "org-1", + created_at: "2024-01-01", + keys: [], + members_with_roles: [{ user_id: "user-1", user_email: "user1@test.com", role: "user" }], + }, + { + team_id: "team-2", + team_alias: "Test Team 2", + models: [], + max_budget: null, + budget_duration: null, + tpm_limit: null, + rpm_limit: null, + organization_id: "org-1", + created_at: "2024-01-01", + keys: [], + members_with_roles: [{ user_id: "user-2", user_email: "user2@test.com", role: "admin" }], + }, + ]; + expect(isUserTeamAdminForAnyTeam(teams, "user-1")).toBe(false); + }); + + it("should return false when teams is null", () => { + expect(isUserTeamAdminForAnyTeam(null, "user-1")).toBe(false); + }); + + it("should return false when teams is empty array", () => { + expect(isUserTeamAdminForAnyTeam([], "user-1")).toBe(false); + }); + }); +}); diff --git a/ui/litellm-dashboard/src/utils/roles.test.tsx b/ui/litellm-dashboard/src/utils/roles.test.tsx deleted file mode 100644 index 871bbaba07..0000000000 --- a/ui/litellm-dashboard/src/utils/roles.test.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { isAdminRole, isProxyAdminRole } from "./roles"; - -describe("roles", () => { - describe("isAdminRole", () => { - it("should return true for all admin roles", () => { - expect(isAdminRole("Admin")).toBe(true); - expect(isAdminRole("Admin Viewer")).toBe(true); - expect(isAdminRole("proxy_admin")).toBe(true); - expect(isAdminRole("proxy_admin_viewer")).toBe(true); - expect(isAdminRole("org_admin")).toBe(true); - }); - - it("should return false for non-admin roles", () => { - expect(isAdminRole("Internal User")).toBe(false); - expect(isAdminRole("Internal Viewer")).toBe(false); - expect(isAdminRole("regular_user")).toBe(false); - expect(isAdminRole("")).toBe(false); - }); - }); - - describe("isProxyAdminRole", () => { - it("should return true for proxy_admin and Admin roles", () => { - expect(isProxyAdminRole("proxy_admin")).toBe(true); - expect(isProxyAdminRole("Admin")).toBe(true); - }); - - it("should return false for other admin roles", () => { - expect(isProxyAdminRole("Admin Viewer")).toBe(false); - expect(isProxyAdminRole("proxy_admin_viewer")).toBe(false); - expect(isProxyAdminRole("org_admin")).toBe(false); - }); - - it("should return false for non-admin roles", () => { - expect(isProxyAdminRole("Internal User")).toBe(false); - expect(isProxyAdminRole("Internal Viewer")).toBe(false); - expect(isProxyAdminRole("regular_user")).toBe(false); - expect(isProxyAdminRole("")).toBe(false); - }); - }); -}); diff --git a/ui/litellm-dashboard/src/utils/roles.ts b/ui/litellm-dashboard/src/utils/roles.ts index da9b00082e..7667a5b207 100644 --- a/ui/litellm-dashboard/src/utils/roles.ts +++ b/ui/litellm-dashboard/src/utils/roles.ts @@ -1,3 +1,5 @@ +import { Team } from "@/components/networking"; + // Define admin roles and permissions export const old_admin_roles = ["Admin", "Admin Viewer"]; export const v2_admin_role_names = ["proxy_admin", "proxy_admin_viewer", "org_admin"]; @@ -15,3 +17,17 @@ export const isAdminRole = (role: string): boolean => { export const isProxyAdminRole = (role: string): boolean => { return role === "proxy_admin" || role === "Admin"; }; + +export const isUserTeamAdminForAnyTeam = (teams: Team[] | null, userID: string): boolean => { + if (teams == null) { + return false; + } + return teams.some((team) => isUserTeamAdminForSingleTeam(team, userID)); +}; + +export const isUserTeamAdminForSingleTeam = (team: Team | null, userID: string): boolean => { + if (team == null || team.members_with_roles == null) { + return false; + } + return team.members_with_roles.some((member) => member.user_id === userID && member.role === "admin"); +};