From 7dae8f73aab7eda18f326343d897376ac142973c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Apr 2026 12:56:33 +0000 Subject: [PATCH] Migrate teams route chrome to shadcn (Phase 1) - TeamsView.tsx: drop antd Form.useForm placeholders (never passed down), drop Tremor Card/Button/Col/Text/Grid -> Tailwind/shadcn. Keep Tremor TabPanel (still-required container for TeamsHeaderTabs which wraps Tremor TabGroup). - CreateTeamModal.tsx: full RHF + Dialog + shadcn rewrite. Form.useForm -> useForm/FormProvider; Modal -> Dialog; Tremor Accordion -> shadcn Accordion; antd Select (single / multiple / tags) -> shadcn Select + custom MultiSelect/TagMultiSelect; antd Switch -> shadcn Switch; antd Input.TextArea -> shadcn Textarea; antd Tooltip -> shadcn Tooltip; Select2.Option trees -> SelectItem. Child selectors (VectorStoreSelector, MCPServerSelector, AgentSelector, MCPToolPermissions) drive Controllers instead of antd form.setFieldValue. - TeamsTable.test.tsx / TeamsHeaderTabs.test.tsx: repair sibling tests. Drop obsolete antd and @heroicons mocks; rewrite TeamsTable admin/non-admin button assertions to semantic getByRole queries against aria-labeled shadcn buttons. Notes: Tremor TabGroup/TabList/Tab/TabPanels still used inside TeamsHeaderTabs pending its own section (chrome-only imports kept behind eslint-disable directive already in the file). ModelsCell.test.tsx failures are pre-existing and out of scope. Co-authored-by: yuneng-jiang --- .../src/app/(dashboard)/teams/TeamsView.tsx | 49 +- .../teams/components/TeamsHeaderTabs.test.tsx | 4 - .../components/TeamsTable/TeamsTable.test.tsx | 36 +- .../components/modals/CreateTeamModal.tsx | 1548 +++++++++++------ 4 files changed, 1051 insertions(+), 586 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/TeamsView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/TeamsView.tsx index c4151d6604c..26e8f13ef43 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/teams/TeamsView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/TeamsView.tsx @@ -3,12 +3,13 @@ import { useQueryClient } from "@tanstack/react-query"; import { organizationKeys } from "@/app/(dashboard)/hooks/organizations/useOrganizations"; import { teamDeleteCall, Organization } from "@/components/networking"; import { fetchTeams } from "@/components/common_components/fetch_teams"; -import { Form } from "antd"; import TeamInfoView from "@/components/team/TeamInfo"; import TeamSSOSettings from "@/components/TeamSSOSettings"; -// eslint-disable-next-line litellm-ui/no-banned-ui-imports import { isAdminRole } from "@/utils/roles"; -import { Card, Button, Col, Text, Grid, TabPanel } from "@tremor/react"; +// eslint-disable-next-line litellm-ui/no-banned-ui-imports +import { TabPanel } from "@tremor/react"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; import AvailableTeamsPanel from "@/components/team/available_teams"; import type { KeyResponse, Team } from "@/components/key_team_helpers/key_list"; @@ -68,15 +69,10 @@ const TeamsView: React.FC = ({ sort_order: "desc", }); - const [form] = Form.useForm(); - const [memberForm] = Form.useForm(); - const [selectedTeamId, setSelectedTeamId] = useState(null); const [editTeam, setEditTeam] = useState(false); const [isTeamModalVisible, setIsTeamModalVisible] = useState(false); - const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false); - const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false); const [userModels, setUserModels] = useState([]); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [teamToDelete, setTeamToDelete] = useState(null); @@ -111,26 +107,17 @@ const TeamsView: React.FC = ({ const handleOk = () => { setIsTeamModalVisible(false); - form.resetFields(); setLoggingSettings([]); setModelAliases({}); }; - const handleMemberOk = () => { - setIsAddMemberModalVisible(false); - setIsEditMemberModalVisible(false); - memberForm.resetFields(); - }; - const handleCancel = () => { setIsTeamModalVisible(false); - form.resetFields(); setLoggingSettings([]); setModelAliases({}); }; const handleDelete = async (team_id: string) => { - // Set the team to delete and open the confirmation modal setTeamToDelete(team_id); setIsDeleteModalOpen(true); }; @@ -143,20 +130,16 @@ const TeamsView: React.FC = ({ try { await teamDeleteCall(accessToken, teamToDelete); queryClient.invalidateQueries({ queryKey: organizationKeys.all }); - // Successfully completed the deletion. Update the state to trigger a rerender. fetchTeams(accessToken, userID, userRole, currentOrg, setTeams); } catch (error) { console.error("Error deleting the team:", error); - // Handle any error situations, such as displaying an error message to the user. } - // Close the confirmation modal and reset the teamToDelete setIsDeleteModalOpen(false); setTeamToDelete(null); }; const cancelDelete = () => { - // Close the confirmation modal and reset the teamToDelete setIsDeleteModalOpen(false); setTeamToDelete(null); }; @@ -177,7 +160,6 @@ const TeamsView: React.FC = ({ const handleFilterChange = (key: keyof FilterState, value: string) => { const newFilters = { ...filters, [key]: value }; setFilters(newFilters); - // Call teamListCall with the new filters if (accessToken) { v2TeamListCall( accessToken, @@ -204,7 +186,6 @@ const TeamsView: React.FC = ({ sort_order: sortOrder, }; setFilters(newFilters); - // Call teamListCall with the new sort parameters if (accessToken) { v2TeamListCall( accessToken, @@ -232,7 +213,6 @@ const TeamsView: React.FC = ({ sort_by: "created_at", sort_order: "desc", }); - // Reset teams list if (accessToken) { v2TeamListCall(accessToken, null, userID || null, null, null) .then((response) => { @@ -248,8 +228,8 @@ const TeamsView: React.FC = ({ return (
- - +
+
{(userRole == "Admin" || userRole == "Org Admin") && (
); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsHeaderTabs.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsHeaderTabs.test.tsx index 50a7f10f047..c7937678ffa 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsHeaderTabs.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsHeaderTabs.test.tsx @@ -12,10 +12,6 @@ vi.mock("@tremor/react", () => ({ Icon: ({ onClick, ...props }: any) => + + ))} +
+ )} +
+ ); +} + +function ModelMultiSelect({ + value, + onChange, + options, + placeholder, + includeAllProxyModels, + getDisplay = (v) => v, +}: { + value: string[]; + onChange: (next: string[]) => void; + options: string[]; + placeholder: string; + includeAllProxyModels?: boolean; + getDisplay?: (v: string) => string; +}) { + const selected = useMemo(() => value ?? [], [value]); + const fullOptions = useMemo(() => { + const opts = options.map((v) => ({ value: v, label: getDisplay(v) })); + if (includeAllProxyModels) { + return [{ value: "all-proxy-models", label: "All Proxy Models" }, ...opts]; + } + return opts; + }, [options, includeAllProxyModels, getDisplay]); + + const remaining = useMemo( + () => fullOptions.filter((o) => !selected.includes(o.value)), + [fullOptions, selected], + ); + + return ( +
+ + {selected.length > 0 && ( +
+ {selected.map((v) => { + const opt = fullOptions.find((o) => o.value === v); + return ( + + {opt?.label ?? v} + + + ); + })} +
+ )} +
+ ); +} + const CreateTeamModal = ({ isTeamModalVisible, - handleOk, handleCancel, currentOrg, organizations, @@ -75,22 +319,63 @@ const CreateTeamModal = ({ }: CreateTeamModalProps) => { const { userId: userID, userRole, accessToken, premiumUser } = useAuthorized(); const queryClient = useQueryClient(); - const [form] = Form.useForm(); const [userModels, setUserModels] = useState([]); - const [currentOrgForCreateTeam, setCurrentOrgForCreateTeam] = useState(null); + const [currentOrgForCreateTeam, setCurrentOrgForCreateTeam] = + useState(null); const [modelsToPick, setModelsToPick] = useState([]); const [guardrailsList, setGuardrailsList] = useState([]); const [policiesList, setPoliciesList] = useState([]); - const [mcpAccessGroups, setMcpAccessGroups] = useState([]); const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false); + const form = useForm({ + defaultValues: { + team_alias: "", + organization_id: currentOrg ? currentOrg.organization_id : null, + models: [], + default_team_member_models: [], + team_member_budget: null, + team_member_key_duration: "", + team_member_rpm_limit: null, + team_member_tpm_limit: null, + max_budget: null, + budget_duration: undefined, + tpm_limit: null, + rpm_limit: null, + team_id: "", + metadata: "", + secret_manager_settings: "", + guardrails: [], + disable_global_guardrails: false, + policies: [], + allowed_vector_store_ids: [], + allowed_mcp_servers_and_groups: undefined, + mcp_tool_permissions: {}, + allowed_agents_and_groups: undefined, + }, + }); + const { + register, + handleSubmit, + control, + watch, + reset, + setValue, + formState: { errors }, + } = form; + + const watchedModels = watch("models"); + useEffect(() => { const fetchUserModels = async () => { try { if (userID === null || userRole === null || accessToken === null) { return; } - const models = await fetchAvailableModelsForTeamOrKey(userID, userRole, accessToken); + const models = await fetchAvailableModelsForTeamOrKey( + userID, + userRole, + accessToken, + ); if (models) { setUserModels(models); } @@ -103,28 +388,25 @@ const CreateTeamModal = ({ }, [accessToken, userID, userRole, teams]); useEffect(() => { - console.log(`currentOrgForCreateTeam: ${currentOrgForCreateTeam}`); const models = getOrganizationModels(currentOrgForCreateTeam, userModels); - console.log(`models: ${models}`); setModelsToPick(models); - form.setFieldValue("models", []); - }, [currentOrgForCreateTeam, userModels, form]); + setValue("models", []); + }, [currentOrgForCreateTeam, userModels, setValue]); - const fetchMcpAccessGroups = async () => { + const fetchMcpAccessGroupsFn = async () => { try { if (accessToken == null) { return; } - const groups = await fetchMCPAccessGroups(accessToken); - setMcpAccessGroups(groups); + await fetchMCPAccessGroups(accessToken); } catch (error) { console.error("Failed to fetch MCP access groups:", error); } }; useEffect(() => { - fetchMcpAccessGroups(); - }, [accessToken, fetchMcpAccessGroups]); + fetchMcpAccessGroupsFn(); + }, [accessToken]); useEffect(() => { const fetchGuardrails = async () => { @@ -134,7 +416,9 @@ const CreateTeamModal = ({ } const response = await getGuardrailsList(accessToken); - const guardrailNames = response.guardrails.map((g: { guardrail_name: string }) => g.guardrail_name); + const guardrailNames = response.guardrails.map( + (g: { guardrail_name: string }) => g.guardrail_name, + ); setGuardrailsList(guardrailNames); } catch (error) { console.error("Failed to fetch guardrails:", error); @@ -148,7 +432,9 @@ const CreateTeamModal = ({ } const response = await getPoliciesList(accessToken); - const policyNames = response.policies.map((p: { policy_name: string }) => p.policy_name); + const policyNames = response.policies.map( + (p: { policy_name: string }) => p.policy_name, + ); setPoliciesList(policyNames); } catch (error) { console.error("Failed to fetch policies:", error); @@ -159,41 +445,43 @@ const CreateTeamModal = ({ fetchPolicies(); }, [accessToken]); - const handleCreate = async (formValues: Record) => { + const handleCreate = async (rawValues: CreateTeamFormValues) => { + const formValues: Record = { ...rawValues }; try { - console.log(`formValues: ${JSON.stringify(formValues)}`); if (accessToken != null) { const newTeamAlias = formValues?.team_alias; const existingTeamAliases = teams?.map((t) => t.team_alias) ?? []; - let organizationId = formValues?.organization_id || currentOrg?.organization_id; + let organizationId = + formValues?.organization_id || currentOrg?.organization_id; if (organizationId === "" || typeof organizationId !== "string") { formValues.organization_id = null; } else { formValues.organization_id = organizationId.trim(); } - // Remove guardrails from top level since it's now in metadata if (existingTeamAliases.includes(newTeamAlias)) { - throw new Error(`Team alias ${newTeamAlias} already exists, please pick another alias`); + throw new Error( + `Team alias ${newTeamAlias} already exists, please pick another alias`, + ); } NotificationsManager.info("Creating Team"); - // Handle logging settings in metadata if (loggingSettings.length > 0) { - let metadata = {}; + let metadata: Record = {}; if (formValues.metadata) { try { metadata = JSON.parse(formValues.metadata); } catch (e) { - console.warn("Invalid JSON in metadata field, starting with empty object"); + console.warn( + "Invalid JSON in metadata field, starting with empty object", + ); } } - // Add logging settings to metadata metadata = { ...metadata, - logging: loggingSettings.filter((config) => config.callback_name), // Only include configs with callback_name + logging: loggingSettings.filter((config) => config.callback_name), }; formValues.metadata = JSON.stringify(metadata); @@ -205,29 +493,39 @@ const CreateTeamModal = ({ delete formValues.secret_manager_settings; } else { try { - formValues.secret_manager_settings = JSON.parse(formValues.secret_manager_settings); + formValues.secret_manager_settings = JSON.parse( + formValues.secret_manager_settings, + ); } catch (e) { - throw new Error("Failed to parse secret manager settings: " + e); + throw new Error( + "Failed to parse secret manager settings: " + e, + ); } } } } - // Transform allowed_vector_store_ids and allowed_mcp_servers_and_groups into object_permission if ( - (formValues.allowed_vector_store_ids && formValues.allowed_vector_store_ids.length > 0) || + (formValues.allowed_vector_store_ids && + formValues.allowed_vector_store_ids.length > 0) || (formValues.allowed_mcp_servers_and_groups && (formValues.allowed_mcp_servers_and_groups.servers?.length > 0 || - formValues.allowed_mcp_servers_and_groups.accessGroups?.length > 0 || + formValues.allowed_mcp_servers_and_groups.accessGroups?.length > + 0 || formValues.allowed_mcp_servers_and_groups.toolPermissions)) ) { formValues.object_permission = {}; - if (formValues.allowed_vector_store_ids && formValues.allowed_vector_store_ids.length > 0) { - formValues.object_permission.vector_stores = formValues.allowed_vector_store_ids; + if ( + formValues.allowed_vector_store_ids && + formValues.allowed_vector_store_ids.length > 0 + ) { + formValues.object_permission.vector_stores = + formValues.allowed_vector_store_ids; delete formValues.allowed_vector_store_ids; } if (formValues.allowed_mcp_servers_and_groups) { - const { servers, accessGroups } = formValues.allowed_mcp_servers_and_groups; + const { servers, accessGroups } = + formValues.allowed_mcp_servers_and_groups; if (servers && servers.length > 0) { formValues.object_permission.mcp_servers = servers; } @@ -237,18 +535,21 @@ const CreateTeamModal = ({ delete formValues.allowed_mcp_servers_and_groups; } - // Add tool permissions separately - if (formValues.mcp_tool_permissions && Object.keys(formValues.mcp_tool_permissions).length > 0) { + if ( + formValues.mcp_tool_permissions && + Object.keys(formValues.mcp_tool_permissions).length > 0 + ) { if (!formValues.object_permission) { formValues.object_permission = {}; } - formValues.object_permission.mcp_tool_permissions = formValues.mcp_tool_permissions; + formValues.object_permission.mcp_tool_permissions = + formValues.mcp_tool_permissions; delete formValues.mcp_tool_permissions; } - // Handle agent permissions if (formValues.allowed_agents_and_groups) { - const { agents, accessGroups } = formValues.allowed_agents_and_groups; + const { agents, accessGroups } = + formValues.allowed_agents_and_groups; if (!formValues.object_permission) { formValues.object_permission = {}; } @@ -262,16 +563,6 @@ const CreateTeamModal = ({ } } - // Transform allowed_mcp_access_groups into object_permission - if (formValues.allowed_mcp_access_groups && formValues.allowed_mcp_access_groups.length > 0) { - if (!formValues.object_permission) { - formValues.object_permission = {}; - } - formValues.object_permission.mcp_access_groups = formValues.allowed_mcp_access_groups; - delete formValues.allowed_mcp_access_groups; - } - - // Add model_aliases if any are defined if (Object.keys(modelAliases).length > 0) { formValues.model_aliases = modelAliases; } @@ -283,9 +574,8 @@ const CreateTeamModal = ({ } else { setTeams([response]); } - console.log(`response for team create call: ${response}`); NotificationsManager.success("Team created"); - form.resetFields(); + reset(); setLoggingSettings([]); setModelAliases({}); setIsTeamModalVisible(false); @@ -296,480 +586,686 @@ const CreateTeamModal = ({ } }; + const onOpenChange = (open: boolean) => { + if (!open) { + handleCancel(); + } + }; + + const defaultMemberModelOpts = + watchedModels && watchedModels.length > 0 ? watchedModels : modelsToPick; + return ( - -
- <> - - - - - Organization{" "} - - Organizations can have multiple teams. Learn more about{" "} - e.stopPropagation()} - > - user management hierarchy - - - } - > - - - - } - name="organization_id" - initialValue={currentOrg ? currentOrg.organization_id : null} - className="mt-8" - > - { - form.setFieldValue("organization_id", value); - setCurrentOrgForCreateTeam(organizations?.find((org) => org.organization_id === value) || null); - }} - filterOption={(input, option) => { - if (!option) return false; - const optionValue = option.children?.toString() || ""; - return optionValue.toLowerCase().includes(input.toLowerCase()); - }} - optionFilterProp="children" - > - {organizations?.map((org) => ( - - {org.organization_alias}{" "} - ({org.organization_id}) - - ))} - - - - Models{" "} - - - - - } - name="models" - > - - - All Proxy Models - - {modelsToPick.map((model) => ( - - {getModelDisplayName(model)} - - ))} - - - - - - Team Member Settings - - - - Optional defaults applied when members join this team. All fields can be overridden per member. - - prev.models !== cur.models} - > - {({ getFieldValue }) => { - const teamModels: string[] = getFieldValue("models") || []; - const opts = teamModels.length > 0 ? teamModels : modelsToPick; - return ( - - Default Model Access{" "} - - - - - } - name="default_team_member_models" - > - - {opts.map((m) => ( - - {getModelDisplayName(m)} - - ))} - - - ); - }} - - (value ? Number(value) : undefined)} - tooltip="Default spend budget for each member in this team." - > - - - - - - - - - - - - - - - - - - - - daily - weekly - monthly - - - - - - - - - - { - if (!mcpAccessGroupsLoaded) { - fetchMcpAccessGroups(); - setMcpAccessGroupsLoaded(true); - } - }} - > - - Additional Settings - - - - { - e.target.value = e.target.value.trim(); - }} + + e.preventDefault()} + > + + Create Team + + + +
+ +
+ - - - - - { - if (!value) { - return Promise.resolve(); - } - try { - JSON.parse(value); - return Promise.resolve(); - } catch (error) { - return Promise.reject(new Error("Please enter valid JSON")); - } - }, - }, - ]} - > - - - - Guardrails{" "} - - e.stopPropagation()} - > - - + {errors.team_alias && ( +

+ {errors.team_alias.message as string} +

+ )} +
+ + + ( + + )} + /> - - - MCP Settings - - - - Allowed MCP Servers{" "} - - - - - } - name="allowed_mcp_servers_and_groups" - className="mt-4" - help="Select MCP servers or access groups this team can access" - > - form.setFieldValue("allowed_mcp_servers_and_groups", val)} - value={form.getFieldValue("allowed_mcp_servers_and_groups")} - accessToken={accessToken || ""} - placeholder="Select MCP servers or access groups (optional)" - /> - - - {/* Hidden field to register mcp_tool_permissions with the form */} - - - - prevValues.allowed_mcp_servers_and_groups !== currentValues.allowed_mcp_servers_and_groups || - prevValues.mcp_tool_permissions !== currentValues.mcp_tool_permissions - } - > - {() => ( -
- form.setFieldsValue({ mcp_tool_permissions: toolPerms })} + + ( +
+
)} - - - + /> +
- - - Agent Settings - - - - Allowed Agents{" "} - - - - + + + + Team Member Settings + + +

+ Optional defaults applied when members join this team. All + fields can be overridden per member. +

+
+ + ( + + )} + /> + + + ( + + field.onChange(v !== null ? Number(v) : undefined) + } + /> + )} + /> + + + + + + ( + + )} + /> + + + ( + + )} + /> +
+
+
+
+ +
+ + ( + + )} + /> + + + ( + + )} + /> + + + ( + + )} + /> + + + ( + + )} + /> +
+ + { + if (!mcpAccessGroupsLoaded) { + fetchMcpAccessGroupsFn(); + setMcpAccessGroupsLoaded(true); } - name="allowed_agents_and_groups" - className="mt-4" - help="Select agents or access groups this team can access" - > - form.setFieldValue("allowed_agents_and_groups", val)} - value={form.getFieldValue("allowed_agents_and_groups")} - accessToken={accessToken || ""} - placeholder="Select agents or access groups (optional)" - /> -
-
-
+ }} + > + + + Additional Settings + + +
+ +
+ { + e.target.value = e.target.value.trim(); + }, + })} + /> +

+ ID of the team you want to create. If not provided, it + will be generated automatically. +

+
- - - Logging Settings - - -
- -
-
-
+ +
+