diff --git a/ui/litellm-dashboard/src/components/team/team_info.test.tsx b/ui/litellm-dashboard/src/components/team/team_info.test.tsx index 6d5e55f734b..6c68188e37c 100644 --- a/ui/litellm-dashboard/src/components/team/team_info.test.tsx +++ b/ui/litellm-dashboard/src/components/team/team_info.test.tsx @@ -1,10 +1,10 @@ import * as networking from "@/components/networking"; -import { act, fireEvent, screen, waitFor } from "@testing-library/react"; -import { renderWithProviders } from "../../../tests/test-utils"; +import { screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { renderWithProviders } from "../../../tests/test-utils"; import TeamInfoView from "./team_info"; -// Mock the networking module vi.mock("@/components/networking", () => ({ teamInfoCall: vi.fn(), teamMemberDeleteCall: vi.fn(), @@ -12,12 +12,18 @@ vi.mock("@/components/networking", () => ({ teamMemberUpdateCall: vi.fn(), teamUpdateCall: vi.fn(), getGuardrailsList: vi.fn(), + getPoliciesList: vi.fn(), + getPolicyInfoWithGuardrails: vi.fn(), fetchMCPAccessGroups: vi.fn(), getTeamPermissionsCall: vi.fn(), organizationInfoCall: vi.fn(), })); -// Mock hooks used by ModelSelect +vi.mock("@/components/utils/dataUtils", () => ({ + copyToClipboard: vi.fn().mockResolvedValue(true), + formatNumberWithCommas: vi.fn((value: number) => value.toLocaleString()), +})); + vi.mock("@/app/(dashboard)/hooks/models/useModels", () => ({ useAllProxyModels: vi.fn(), })); @@ -34,6 +40,59 @@ vi.mock("@/app/(dashboard)/hooks/users/useCurrentUser", () => ({ useCurrentUser: vi.fn(), })); +vi.mock("@/components/team/team_member_view", () => ({ + default: vi.fn(({ setIsAddMemberModalVisible }) => ( +
+ +
+ )), +})); + +vi.mock("@/components/common_components/user_search_modal", () => ({ + default: vi.fn(({ isVisible, onCancel, onSubmit }) => + isVisible ? ( +
+ + +
+ ) : null + ), +})); + +vi.mock("@/components/team/EditMembership", () => ({ + default: vi.fn(({ visible, onCancel, onSubmit }) => + visible ? ( +
+ + +
+ ) : null + ), +})); + +vi.mock("@/components/common_components/DeleteResourceModal", () => ({ + default: vi.fn(({ isOpen, onCancel, onOk }) => + isOpen ? ( +
+ + +
+ ) : null + ), +})); + +vi.mock("@/components/team/member_permissions", () => ({ + default: vi.fn(() =>
Member Permissions
), +})); + +vi.mock("@/components/team/member_permissions", () => ({ + default: vi.fn(() =>
Member Permissions
), +})); + import { useAllProxyModels } from "@/app/(dashboard)/hooks/models/useModels"; import { useOrganization } from "@/app/(dashboard)/hooks/organizations/useOrganizations"; import { useTeam } from "@/app/(dashboard)/hooks/teams/useTeams"; @@ -44,9 +103,60 @@ const mockUseTeam = vi.mocked(useTeam); const mockUseOrganization = vi.mocked(useOrganization); const mockUseCurrentUser = vi.mocked(useCurrentUser); +const createMockTeamData = (overrides = {}) => ({ + team_id: "123", + team_info: { + team_alias: "Test Team", + team_id: "123", + organization_id: null, + admins: ["admin@test.com"], + members: ["user1@test.com"], + members_with_roles: [ + { + user_id: "user1@test.com", + user_email: "user1@test.com", + role: "member", + spend: 0, + budget_id: "budget1", + }, + ], + metadata: {}, + tpm_limit: null, + rpm_limit: null, + max_budget: null, + budget_duration: null, + models: [], + blocked: false, + spend: 0, + max_parallel_requests: null, + budget_reset_at: null, + model_id: null, + litellm_model_table: null, + created_at: "2024-01-01T00:00:00Z", + team_member_budget_table: null, + guardrails: [], + policies: [], + object_permission: null, + ...overrides, + }, + keys: [], + team_memberships: [], +}); + describe("TeamInfoView", () => { + const defaultProps = { + teamId: "123", + onUpdate: vi.fn(), + onClose: vi.fn(), + accessToken: "test-token", + is_team_admin: true, + is_proxy_admin: true, + userModels: ["gpt-4", "gpt-3.5-turbo"], + editTeam: false, + premiumUser: false, + }; + beforeEach(() => { - // Set up default mock implementations mockUseAllProxyModels.mockReturnValue({ data: { data: [] }, isLoading: false, @@ -63,6 +173,14 @@ describe("TeamInfoView", () => { data: { models: [] }, isLoading: false, } as any); + + vi.mocked(networking.getGuardrailsList).mockResolvedValue({ guardrails: [] }); + vi.mocked(networking.getPoliciesList).mockResolvedValue({ policies: [] }); + vi.mocked(networking.fetchMCPAccessGroups).mockResolvedValue([]); + vi.mocked(networking.getTeamPermissionsCall).mockResolvedValue({ + all_available_permissions: [], + team_member_permissions: [], + }); }); afterEach(() => { @@ -70,503 +188,389 @@ describe("TeamInfoView", () => { }); it("should render", async () => { - // Mock the team info response - vi.mocked(networking.teamInfoCall).mockResolvedValue({ - team_id: "123", - team_info: { - team_alias: "Test Team", - team_id: "123", - organization_id: null, - admins: ["admin@test.com"], - members: ["user1@test.com", "user2@test.com"], - members_with_roles: [ - { - user_id: "user1@test.com", - user_email: "user1@test.com", - role: "member", - spend: 0, - budget_id: "budget1", - }, - ], - metadata: {}, - tpm_limit: null, - rpm_limit: null, - max_budget: null, - budget_duration: null, - models: [], - blocked: false, - spend: 0, - max_parallel_requests: null, - budget_reset_at: null, - model_id: null, - litellm_model_table: null, - created_at: "2024-01-01T00:00:00Z", - team_member_budget_table: null, - }, - keys: [], - team_memberships: [], + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); }); - - vi.mocked(networking.getGuardrailsList).mockResolvedValue({ guardrails: [] }); - vi.mocked(networking.fetchMCPAccessGroups).mockResolvedValue([]); - - renderWithProviders( - {}} - onClose={() => {}} - accessToken="123" - is_team_admin={true} - is_proxy_admin={true} - userModels={[]} - editTeam={false} - premiumUser={false} - />, - ); - await waitFor( - () => { - expect(screen.queryByText("User ID")).not.toBeNull(); - }, - // This is a workaround to fix the flaky test issue. TODO: Remove this once we have a better solution. - { timeout: 10000 }, - ); }); - it("should not show all-proxy-models option when user has no access to it", async () => { + it("should display loading state while fetching team data", () => { + vi.mocked(networking.teamInfoCall).mockImplementation(() => new Promise(() => { })); + + renderWithProviders(); + + expect(screen.getByText("Loading...")).toBeInTheDocument(); + }); + + it("should display error message when team is not found", async () => { vi.mocked(networking.teamInfoCall).mockResolvedValue({ team_id: "123", - team_info: { - team_alias: "Test Team", - team_id: "123", - organization_id: null, - admins: ["admin@test.com"], - members: ["user1@test.com", "user2@test.com"], - members_with_roles: [ - { - user_id: "user1@test.com", - user_email: "user1@test.com", - role: "member", - spend: 0, - budget_id: "budget1", - }, - ], - metadata: {}, - tpm_limit: null, - rpm_limit: null, - max_budget: null, - budget_duration: null, - models: ["gpt-4"], - blocked: false, - spend: 0, - max_parallel_requests: null, - budget_reset_at: null, - model_id: null, - litellm_model_table: null, - created_at: "2024-01-01T00:00:00Z", - team_member_budget_table: null, - }, + team_info: null as any, keys: [], team_memberships: [], }); - vi.mocked(networking.getGuardrailsList).mockResolvedValue({ guardrails: [] }); - vi.mocked(networking.fetchMCPAccessGroups).mockResolvedValue([]); - - renderWithProviders( - {}} - onClose={() => {}} - accessToken="123" - is_team_admin={true} - is_proxy_admin={true} - userModels={["gpt-4", "gpt-3.5-turbo"]} - editTeam={false} - premiumUser={false} - />, - ); + renderWithProviders(); await waitFor(() => { - expect(screen.getAllByText("Test Team")).not.toBeNull(); + expect(screen.getByText("Team not found")).toBeInTheDocument(); + }); + }); + + it("should display budget information in overview", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue( + createMockTeamData({ + max_budget: 1000, + spend: 250.5, + budget_duration: "30d", + }) + ); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Budget Status")).toBeInTheDocument(); + }); + }); + + it("should display guardrails in overview when present", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue( + createMockTeamData({ + guardrails: ["guardrail1", "guardrail2"], + }) + ); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Guardrails")).toBeInTheDocument(); + }); + }); + + it("should display policies in overview when present", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue( + createMockTeamData({ + policies: ["policy1"], + }) + ); + vi.mocked(networking.getPolicyInfoWithGuardrails).mockResolvedValue({ + resolved_guardrails: ["guardrail1"], + }); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Policies")).toBeInTheDocument(); + }); + }); + + it("should show members tab when user can edit team", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByRole("tab", { name: "Members" })).toBeInTheDocument(); + }); + }); + + it("should not show members tab when user cannot edit team", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + expect(screen.queryByRole("tab", { name: "Members" })).not.toBeInTheDocument(); + }); + + it("should show settings tab when user can edit team", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByRole("tab", { name: "Settings" })).toBeInTheDocument(); + }); + }); + + it("should navigate to settings tab when clicked", async () => { + const user = userEvent.setup(); + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); }); const settingsTab = screen.getByRole("tab", { name: "Settings" }); - act(() => { - fireEvent.click(settingsTab); - }); + await user.click(settingsTab); await waitFor(() => { expect(screen.getByText("Team Settings")).toBeInTheDocument(); }); + }); - const editButton = screen.getByRole("button", { name: "Edit Settings" }); - act(() => { - fireEvent.click(editButton); - }); + it("should open edit mode when edit button is clicked", async () => { + const user = userEvent.setup(); + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + + renderWithProviders(); await waitFor(() => { - expect(screen.getByTestId("models-select")).toBeInTheDocument(); - }); - - const allProxyModelsOption = screen.queryByText("All Proxy Models"); - expect(allProxyModelsOption).not.toBeInTheDocument(); - }, 10000); // This is a workaround to fix the flaky test issue. TODO: Remove this once we have a better solution. - - it("should only show organization models in dropdown when team is in organization with limited models", async () => { - const organizationId = "org-123"; - const organizationModels = ["gpt-4", "claude-3-opus"]; - const userModels = ["gpt-4", "gpt-3.5-turbo", "claude-3-opus", "claude-2"]; - - // Mock all proxy models - should include all user models - const allProxyModels = userModels.map((id) => ({ - id, - object: "model", - created: 1234567890, - owned_by: "openai", - })); - - mockUseAllProxyModels.mockReturnValue({ - data: { data: allProxyModels }, - isLoading: false, - } as any); - - mockUseCurrentUser.mockReturnValue({ - data: { models: userModels }, - isLoading: false, - } as any); - - const organizationData = { - organization_id: organizationId, - organization_name: "Test Organization", - spend: 0, - max_budget: null, - models: organizationModels, - tpm_limit: null, - rpm_limit: null, - members: null, - }; - - mockUseOrganization.mockReturnValue({ - data: organizationData, - isLoading: false, - } as any); - - vi.mocked(networking.teamInfoCall).mockResolvedValue({ - team_id: "123", - team_info: { - team_alias: "Test Team", - team_id: "123", - organization_id: organizationId, - admins: ["admin@test.com"], - members: ["user1@test.com"], - members_with_roles: [ - { - user_id: "user1@test.com", - user_email: "user1@test.com", - role: "member", - spend: 0, - budget_id: "budget1", - }, - ], - metadata: {}, - tpm_limit: null, - rpm_limit: null, - max_budget: null, - budget_duration: null, - models: ["gpt-4"], - blocked: false, - spend: 0, - max_parallel_requests: null, - budget_reset_at: null, - model_id: null, - litellm_model_table: null, - created_at: "2024-01-01T00:00:00Z", - team_member_budget_table: null, - }, - keys: [], - team_memberships: [], - }); - - vi.mocked(networking.organizationInfoCall).mockResolvedValue(organizationData); - - vi.mocked(networking.getGuardrailsList).mockResolvedValue({ guardrails: [] }); - vi.mocked(networking.fetchMCPAccessGroups).mockResolvedValue([]); - - renderWithProviders( - {}} - onClose={() => {}} - accessToken="123" - is_team_admin={true} - is_proxy_admin={true} - userModels={userModels} - editTeam={false} - premiumUser={false} - />, - ); - - await waitFor(() => { - expect(screen.getAllByText("Test Team")).not.toBeNull(); + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); }); const settingsTab = screen.getByRole("tab", { name: "Settings" }); - act(() => { - fireEvent.click(settingsTab); - }); + await user.click(settingsTab); await waitFor(() => { - expect(screen.getByText("Team Settings")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Edit Settings" })).toBeInTheDocument(); }); const editButton = screen.getByRole("button", { name: "Edit Settings" }); - act(() => { - fireEvent.click(editButton); - }); + await user.click(editButton); await waitFor(() => { - expect(screen.getByTestId("models-select")).toBeInTheDocument(); + expect(screen.getByLabelText("Team Name")).toBeInTheDocument(); + }); + }); + + it("should close edit mode when cancel button is clicked", async () => { + const user = userEvent.setup(); + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); }); - // Find the Ant Design Select selector element to open the dropdown - // The data-testid is on the Select component, we need to find the selector inside it - const modelsSelectElement = screen.getByTestId("models-select"); - const selectSelector = modelsSelectElement.querySelector(".ant-select-selector"); - expect(selectSelector).toBeTruthy(); + const settingsTab = screen.getByRole("tab", { name: "Settings" }); + await user.click(settingsTab); - // Open the dropdown by clicking on the selector - act(() => { - fireEvent.mouseDown(selectSelector!); + await waitFor(() => { + expect(screen.getByRole("button", { name: "Edit Settings" })).toBeInTheDocument(); }); - // Wait for dropdown to open - Ant Design renders options in a portal - await waitFor( - () => { - const dropdownOptions = document.querySelectorAll(".ant-select-item-option"); - expect(dropdownOptions.length).toBeGreaterThan(0); - }, - { timeout: 5000 }, - ); + const editButton = screen.getByRole("button", { name: "Edit Settings" }); + await user.click(editButton); - const dropdownOptions = document.querySelectorAll(".ant-select-item-option"); - const optionTexts = Array.from(dropdownOptions).map((option) => option.textContent?.trim() || ""); - - organizationModels.forEach((model) => { - expect(optionTexts).toContain(model); + await waitFor(() => { + expect(screen.getByLabelText("Team Name")).toBeInTheDocument(); }); - const modelsNotInOrganization = userModels.filter((m) => !organizationModels.includes(m)); - modelsNotInOrganization.forEach((model) => { - expect(optionTexts).not.toContain(model); + const cancelButton = screen.getByRole("button", { name: "Cancel" }); + await user.click(cancelButton); + + await waitFor(() => { + expect(screen.queryByLabelText("Team Name")).not.toBeInTheDocument(); }); - }, 10000); + }); + + it("should call onClose when back button is clicked", async () => { + const user = userEvent.setup(); + const onClose = vi.fn(); + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + const backButton = screen.getByRole("button", { name: /back to teams/i }); + await user.click(backButton); + + expect(onClose).toHaveBeenCalled(); + }); + + it("should copy team ID to clipboard when copy button is clicked", async () => { + const user = userEvent.setup(); + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + const copyButtons = screen.getAllByRole("button"); + const copyButton = copyButtons.find((btn) => btn.querySelector("svg")); + expect(copyButton).toBeTruthy(); + + if (copyButton) { + await user.click(copyButton); + } + }); it("should disable secret manager settings for non-premium users", async () => { - const teamResponse = { - team_id: "123", - team_info: { - team_alias: "Test Team", - team_id: "123", - organization_id: null, - admins: ["admin@test.com"], - members: [], - members_with_roles: [], + const user = userEvent.setup(); + vi.mocked(networking.teamInfoCall).mockResolvedValue( + createMockTeamData({ metadata: { secret_manager_settings: { provider: "aws", secret_id: "abc" }, }, - tpm_limit: null, - rpm_limit: null, - max_budget: null, - budget_duration: null, - models: ["gpt-4"], - blocked: false, - spend: 0, - max_parallel_requests: null, - budget_reset_at: null, - model_id: null, - litellm_model_table: null, - created_at: "2024-01-01T00:00:00Z", - team_member_budget_table: null, - }, - keys: [], - team_memberships: [], - }; - - vi.mocked(networking.teamInfoCall).mockResolvedValue(teamResponse as any); - vi.mocked(networking.getGuardrailsList).mockResolvedValue({ guardrails: [] }); - vi.mocked(networking.fetchMCPAccessGroups).mockResolvedValue([]); - - renderWithProviders( - {}} - onClose={() => {}} - accessToken="123" - is_team_admin={true} - is_proxy_admin={true} - userModels={["gpt-4"]} - editTeam={false} - premiumUser={false} - />, + }) ); - const settingsTab = await screen.findByRole("tab", { name: "Settings" }); - act(() => fireEvent.click(settingsTab)); + renderWithProviders(); - const editButton = await screen.findByRole("button", { name: "Edit Settings" }); - act(() => fireEvent.click(editButton)); + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + const settingsTab = screen.getByRole("tab", { name: "Settings" }); + await user.click(settingsTab); + + await waitFor(() => { + expect(screen.getByRole("button", { name: "Edit Settings" })).toBeInTheDocument(); + }); + + const editButton = screen.getByRole("button", { name: "Edit Settings" }); + await user.click(editButton); const secretField = await screen.findByPlaceholderText( - '{"namespace": "admin", "mount": "secret", "path_prefix": "litellm"}', + '{"namespace": "admin", "mount": "secret", "path_prefix": "litellm"}' ); expect(secretField).toBeDisabled(); - expect(secretField).toHaveValue(JSON.stringify(teamResponse.team_info.metadata.secret_manager_settings, null, 2)); - }, 10000); + }); - it("should allow premium users to update secret manager settings", async () => { - const teamResponse = { - team_id: "123", - team_info: { - team_alias: "Test Team", - team_id: "123", - organization_id: null, - admins: ["admin@test.com"], - members: [], - members_with_roles: [], + it("should allow premium users to edit secret manager settings", async () => { + const user = userEvent.setup(); + vi.mocked(networking.teamInfoCall).mockResolvedValue( + createMockTeamData({ metadata: { secret_manager_settings: { provider: "aws", secret_id: "abc" }, }, - tpm_limit: null, - rpm_limit: null, - max_budget: null, - budget_duration: null, - models: ["gpt-4"], - blocked: false, - spend: 0, - max_parallel_requests: null, - budget_reset_at: null, - model_id: null, - litellm_model_table: null, - created_at: "2024-01-01T00:00:00Z", - team_member_budget_table: null, - }, - keys: [], - team_memberships: [], - }; - - vi.mocked(networking.teamInfoCall).mockResolvedValue(teamResponse as any); - vi.mocked(networking.getGuardrailsList).mockResolvedValue({ guardrails: [] }); - vi.mocked(networking.fetchMCPAccessGroups).mockResolvedValue([]); - vi.mocked(networking.teamUpdateCall).mockResolvedValue({ data: teamResponse.team_info, team_id: "123" } as any); - - renderWithProviders( - {}} - onClose={() => {}} - accessToken="123" - is_team_admin={true} - is_proxy_admin={true} - userModels={["gpt-4"]} - editTeam={false} - premiumUser={true} - />, + }) ); + vi.mocked(networking.teamUpdateCall).mockResolvedValue({ data: {}, team_id: "123" } as any); - const settingsTab = await screen.findByRole("tab", { name: "Settings" }); - act(() => fireEvent.click(settingsTab)); + renderWithProviders(); - const editButton = await screen.findByRole("button", { name: "Edit Settings" }); - act(() => fireEvent.click(editButton)); + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + const settingsTab = screen.getByRole("tab", { name: "Settings" }); + await user.click(settingsTab); + + await waitFor(() => { + expect(screen.getByRole("button", { name: "Edit Settings" })).toBeInTheDocument(); + }); + + const editButton = screen.getByRole("button", { name: "Edit Settings" }); + await user.click(editButton); const secretField = await screen.findByPlaceholderText( - '{"namespace": "admin", "mount": "secret", "path_prefix": "litellm"}', + '{"namespace": "admin", "mount": "secret", "path_prefix": "litellm"}' ); expect(secretField).not.toBeDisabled(); + }); - act(() => { - fireEvent.change(secretField, { target: { value: '{"provider":"azure","secret_id":"xyz"}' } }); - }); + it("should add team member when form is submitted", async () => { + const user = userEvent.setup(); + const onUpdate = vi.fn(); + const teamData = createMockTeamData(); + vi.mocked(networking.teamInfoCall).mockResolvedValue(teamData); + vi.mocked(networking.teamMemberAddCall).mockResolvedValue({} as any); - const saveButton = await screen.findByRole("button", { name: "Save Changes" }); - act(() => fireEvent.click(saveButton)); + renderWithProviders(); await waitFor(() => { - expect(networking.teamUpdateCall).toHaveBeenCalled(); + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); }); - const payload = vi.mocked(networking.teamUpdateCall).mock.calls[0][1]; - expect(payload.metadata.secret_manager_settings).toEqual({ provider: "azure", secret_id: "xyz" }); - }, 10000); + const membersTab = screen.getByRole("tab", { name: "Members" }); + await user.click(membersTab); - it("should include vector stores in object_permission when updating team", async () => { - const teamResponse = { - team_id: "123", - team_info: { - team_alias: "Test Team", - team_id: "123", - organization_id: null, - admins: ["admin@test.com"], - members: [], - members_with_roles: [], - metadata: {}, - tpm_limit: null, - rpm_limit: null, - max_budget: null, - budget_duration: null, - models: ["gpt-4"], - blocked: false, - spend: 0, - max_parallel_requests: null, - budget_reset_at: null, - model_id: null, - litellm_model_table: null, - created_at: "2024-01-01T00:00:00Z", - team_member_budget_table: null, - object_permission: { - vector_stores: ["store1", "store2"], + await waitFor(() => { + expect(screen.getByRole("button", { name: "Add Member" })).toBeInTheDocument(); + }); + + const addButton = screen.getByRole("button", { name: "Add Member" }); + await user.click(addButton); + + await waitFor(() => { + expect(screen.getByRole("button", { name: "Submit" })).toBeInTheDocument(); + }); + + const submitButton = screen.getByRole("button", { name: "Submit" }); + await user.click(submitButton); + + await waitFor(() => { + expect(networking.teamMemberAddCall).toHaveBeenCalled(); + }); + }); + + it("should display team member budget information when present", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue( + createMockTeamData({ + team_member_budget_table: { + max_budget: 500, + budget_duration: "30d", + tpm_limit: 5000, + rpm_limit: 50, }, - }, - keys: [], - team_memberships: [], - }; - - vi.mocked(networking.teamInfoCall).mockResolvedValue(teamResponse as any); - vi.mocked(networking.getGuardrailsList).mockResolvedValue({ guardrails: [] }); - vi.mocked(networking.fetchMCPAccessGroups).mockResolvedValue([]); - vi.mocked(networking.teamUpdateCall).mockResolvedValue({ data: teamResponse.team_info, team_id: "123" } as any); - - renderWithProviders( - {}} - onClose={() => {}} - accessToken="123" - is_team_admin={true} - is_proxy_admin={true} - userModels={["gpt-4"]} - editTeam={false} - premiumUser={true} - />, + }) ); - const settingsTab = await screen.findByRole("tab", { name: "Settings" }); - act(() => fireEvent.click(settingsTab)); - - const editButton = await screen.findByRole("button", { name: "Edit Settings" }); - act(() => fireEvent.click(editButton)); - - // Verify that Vector Stores field is present - expect(screen.getByLabelText("Vector Stores")).toBeInTheDocument(); - - const saveButton = await screen.findByRole("button", { name: "Save Changes" }); - act(() => fireEvent.click(saveButton)); + renderWithProviders(); await waitFor(() => { - expect(networking.teamUpdateCall).toHaveBeenCalled(); + expect(screen.getByText("Budget Status")).toBeInTheDocument(); + }); + }); + + it("should display virtual keys information", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue({ + ...createMockTeamData(), + keys: [ + { user_id: "user1", token: "key1" }, + { token: "key2" }, + ], }); - const payload = vi.mocked(networking.teamUpdateCall).mock.calls[0][1]; - expect(payload.object_permission.vector_stores).toEqual(["store1", "store2"]); - }, 10000); + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Virtual Keys")).toBeInTheDocument(); + }); + }); + + it("should display object permissions when present", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue( + createMockTeamData({ + object_permission: { + object_permission_id: "perm-1", + mcp_servers: ["server1"], + vector_stores: ["store1"], + }, + }) + ); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + }); }); diff --git a/ui/litellm-dashboard/src/components/team/team_info.tsx b/ui/litellm-dashboard/src/components/team/team_info.tsx index 193d056fdd4..34ff903c864 100644 --- a/ui/litellm-dashboard/src/components/team/team_info.tsx +++ b/ui/litellm-dashboard/src/components/team/team_info.tsx @@ -462,6 +462,7 @@ const TeamInfoView: React.FC = ({ ...parsedMetadata, guardrails: values.guardrails || [], logging: values.logging_settings || [], + disable_global_guardrails: values.disable_global_guardrails || false, ...(secretManagerSettings !== undefined ? { secret_manager_settings: secretManagerSettings } : {}), }, policies: values.policies || [], @@ -572,11 +573,10 @@ const TeamInfoView: React.FC = ({ size="small" icon={copiedStates["team-id"] ? : } onClick={() => copyToClipboard(info.team_id, "team-id")} - className={`left-2 z-10 transition-all duration-200 ${ - copiedStates["team-id"] - ? "text-green-600 bg-green-50 border-green-200" - : "text-gray-500 hover:text-gray-700 hover:bg-gray-100" - }`} + className={`left-2 z-10 transition-all duration-200 ${copiedStates["team-id"] + ? "text-green-600 bg-green-50 border-green-200" + : "text-gray-500 hover:text-gray-700 hover:bg-gray-100" + }`} /> @@ -588,10 +588,10 @@ const TeamInfoView: React.FC = ({ Overview, ...(canEditTeam ? [ - Members, - Member Permissions, - Settings, - ] + Members, + Member Permissions, + Settings, + ] : []), ]} @@ -764,10 +764,10 @@ const TeamInfoView: React.FC = ({ disable_global_guardrails: info.metadata?.disable_global_guardrails || false, metadata: info.metadata ? JSON.stringify( - (({ logging, secret_manager_settings, ...rest }) => rest)(info.metadata), - null, - 2, - ) + (({ logging, secret_manager_settings, ...rest }) => rest)(info.metadata), + null, + 2, + ) : "", logging_settings: info.metadata?.logging || [], secret_manager_settings: info.metadata?.secret_manager_settings @@ -905,7 +905,7 @@ const TeamInfoView: React.FC = ({ - Disable Global Guardrails{" "} + Disable Global Guardrails