diff --git a/ui/litellm-dashboard/src/components/Teams.test.tsx b/ui/litellm-dashboard/src/components/Teams.test.tsx index bee35ac5c5d..1a317c6aa91 100644 --- a/ui/litellm-dashboard/src/components/Teams.test.tsx +++ b/ui/litellm-dashboard/src/components/Teams.test.tsx @@ -1488,3 +1488,71 @@ describe("Teams - the exact bytes the create call sends", () => { expect(teamCreateCall).not.toHaveBeenCalled(); }); }); + +describe("Teams - create team modal keeps user selections", () => { + const orgs = () => [ + { organization_id: "org-1", organization_alias: "Org One", models: [], members: [] }, + { organization_id: "org-2", organization_alias: "Org Two", models: [], members: [] }, + ]; + + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(fetchAvailableModelsForTeamOrKey).mockResolvedValue(["gpt-4"]); + vi.mocked(fetchMCPAccessGroups).mockResolvedValue([]); + vi.mocked(getGuardrailsList).mockResolvedValue({ guardrails: [] }); + mockUseOrganizations.mockReturnValue({ data: orgs() }); + }); + + const openCreateModal = async () => { + const queryClient = createQueryClient(); + const wrap = (children: React.ReactElement) => ( + + {children} + + ); + const view = render(wrap()); + act(() => { + fireEvent.click(screen.getAllByRole("button", { name: /create team/i })[0]); + }); + await waitFor(() => { + expect(screen.getByLabelText(/team name/i)).toBeInTheDocument(); + }); + return { + rerenderTeams: () => view.rerender(wrap()), + }; + }; + + const selectOrganization = async (label: string) => { + const user = userEvent.setup(); + const orgCombobox = screen.getByRole("combobox", { name: /organization/i }); + await user.click(orgCombobox); + await user.click(await screen.findByText(label)); + return orgCombobox; + }; + + it("keeps the selected organization and models when the organizations list refetches", async () => { + const { rerenderTeams } = await openCreateModal(); + + const orgCombobox = await selectOrganization("Org One"); + fireEvent.change(screen.getByTestId("create-team-models-select"), { target: { value: "gpt-4" } }); + + mockUseOrganizations.mockReturnValue({ data: orgs() }); + act(() => { + rerenderTeams(); + }); + + expect(orgCombobox).toHaveValue("Org One"); + expect(screen.getByTestId("create-team-models-select")).toHaveValue("gpt-4"); + }); + + it("clears the selected models when the user picks a different organization", async () => { + await openCreateModal(); + + await selectOrganization("Org One"); + fireEvent.change(screen.getByTestId("create-team-models-select"), { target: { value: "gpt-4" } }); + expect(screen.getByTestId("create-team-models-select")).toHaveValue("gpt-4"); + + await selectOrganization("Org Two"); + expect(screen.getByTestId("create-team-models-select")).toHaveValue(""); + }); +}); diff --git a/ui/litellm-dashboard/src/components/Teams.tsx b/ui/litellm-dashboard/src/components/Teams.tsx index e2eda4adb23..acc55d2c371 100644 --- a/ui/litellm-dashboard/src/components/Teams.tsx +++ b/ui/litellm-dashboard/src/components/Teams.tsx @@ -17,7 +17,7 @@ import { useZodForm } from "@/lib/forms/useZodForm"; import { TagsInput } from "@/app/(dashboard)/guardrails/_components/content_filter/TagsInput"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ChevronDown, Plus, Users } from "lucide-react"; -import React, { useEffect, useMemo, useState } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import { z } from "zod/v4"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { PageHeader } from "@/components/shared/PageHeader"; @@ -264,13 +264,10 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser ? `Default: ${getBudgetDurationLabel(defaultBudgetDuration)} (${defaultBudgetDuration})` : "n/a"; - useEffect(() => { - form.setValue("models", []); - }, [currentOrgForCreateTeam, userModels]); - // Handle organization preselection when modal opens + const wasTeamModalVisible = useRef(false); useEffect(() => { - if (isTeamModalVisible) { + if (isTeamModalVisible && !wasTeamModalVisible.current) { const adminOrgs = getAdminOrganizations(userRole, userID, organizations); // Org admins must scope a team to an org, so with exactly one we preselect it. @@ -284,6 +281,7 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser setCurrentOrgForCreateTeam(currentOrg); } } + wasTeamModalVisible.current = isTeamModalVisible; }, [isTeamModalVisible, isOrgAdmin, userRole, userID, organizations, currentOrg]); // Add this useEffect to fetch guardrails @@ -686,6 +684,11 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser const adminOrgs = getAdminOrganizations(userRole, userID, organizations); const isSingleOrg = adminOrgs.length === 1; const hasNoOrgs = adminOrgs.length === 0; + const handleCreateTeamOrgChange = (next: string, onChange: (value: string | null) => void) => { + onChange(next === "" ? null : next); + setCurrentOrgForCreateTeam(adminOrgs.find((org) => org.organization_id === next) ?? null); + form.setValue("models", []); + }; return ( <> @@ -721,12 +724,7 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser hasNoOrgs ? "No organizations available" : "Search or select an Organization" } emptyText="No organizations available" - onValueChange={(next) => { - onChange(next === "" ? null : next); - setCurrentOrgForCreateTeam( - adminOrgs.find((org) => org.organization_id === next) ?? null, - ); - }} + onValueChange={(next) => handleCreateTeamOrgChange(next, onChange)} /> )}