diff --git a/ui/litellm-dashboard/src/components/ModelSelect/ModelSelect.tsx b/ui/litellm-dashboard/src/components/ModelSelect/ModelSelect.tsx index d3ede5f4318..c3936cc456d 100644 --- a/ui/litellm-dashboard/src/components/ModelSelect/ModelSelect.tsx +++ b/ui/litellm-dashboard/src/components/ModelSelect/ModelSelect.tsx @@ -40,6 +40,7 @@ export const MODEL_SENTINEL_OPTIONS = [ const MAX_VISIBLE_MODEL_CHIPS = 5; export interface ModelSelectProps { + id?: string; teamID?: string; organizationID?: string; options?: { @@ -122,7 +123,7 @@ const filterModels = ( export const ModelSelect = (props: ModelSelectProps) => { const anchor = useComboboxAnchor(); - const { teamID, organizationID, options, context, dataTestId, value = [], onChange, style } = props; + const { id, teamID, organizationID, options, context, dataTestId, value = [], onChange, style } = props; const { showAllProxyModelsOverride, includeSpecialOptions } = options || {}; const { data: allProxyModels, isLoading: isLoadingAllProxyModels } = useAllProxyModels(); const { data: team, isLoading: isLoadingTeam } = useTeam(teamID); @@ -256,7 +257,7 @@ export const ModelSelect = (props: ModelSelectProps) => { )} - + No models found diff --git a/ui/litellm-dashboard/src/components/Teams.test.tsx b/ui/litellm-dashboard/src/components/Teams.test.tsx index 0ef4357e73f..d0d41837dce 100644 --- a/ui/litellm-dashboard/src/components/Teams.test.tsx +++ b/ui/litellm-dashboard/src/components/Teams.test.tsx @@ -1223,3 +1223,261 @@ describe("Teams - which fields reach the create payload depends on the open sect expect(payload.team_id).toBe("tid-kept"); }); }); + +describe("Teams - the exact bytes the create call sends", () => { + beforeEach(() => { + vi.clearAllMocks(); + can.mockReturnValue(true); + vi.mocked(fetchAvailableModelsForTeamOrKey).mockResolvedValue(["gpt-4"]); + vi.mocked(fetchMCPAccessGroups).mockResolvedValue([]); + vi.mocked(getGuardrailsList).mockResolvedValue({ guardrails: [] }); + vi.mocked(getPoliciesList).mockResolvedValue({ policies: [] }); + vi.mocked(getDefaultTeamSettings).mockResolvedValue({ values: {} }); + vi.mocked(teamCreateCall).mockResolvedValue({ team_id: "new-team-1" }); + vi.mocked(useTeamMetadataSchema).mockReturnValue({ data: [], isLoading: false } as any); + mockUseOrganizations.mockReturnValue({ data: null }); + }); + + const openCreateModal = async (options?: { premiumUser?: boolean }) => { + renderWithQueryClient( + , + ); + act(() => { + fireEvent.click(screen.getAllByRole("button", { name: /create team/i })[0]); + }); + await waitFor(() => { + expect(screen.getByLabelText(/team name/i)).toBeInTheDocument(); + }); + fireEvent.change(screen.getByLabelText(/team name/i), { target: { value: "Byte Contract Team" } }); + }; + + const submit = async () => { + const buttons = screen.getAllByRole("button", { name: /create team/i }); + fireEvent.click(buttons[buttons.length - 1]); + await waitFor(() => { + expect(teamCreateCall).toHaveBeenCalled(); + }); + return vi.mocked(teamCreateCall).mock.calls[0][1] as Record; + }; + + const wireBody = (payload: Record) => JSON.parse(JSON.stringify(payload)) as Record; + + const openSection = async (title: string, mountedProbe: RegExp | string) => { + fireEvent.click(screen.getByText(title)); + await waitFor(() => { + expect(screen.getAllByText(mountedProbe).length).toBeGreaterThan(0); + }); + }; + + it("sends three keys and nothing else when every section is left closed", async () => { + await openCreateModal(); + + const payload = await submit(); + + expect(payload).toStrictEqual({ + team_alias: "Byte Contract Team", + organization_id: null, + models: ["no-default-models"], + max_budget: undefined, + budget_duration: undefined, + tpm_limit: undefined, + rpm_limit: undefined, + metadata: undefined, + }); + expect(wireBody(payload)).toStrictEqual({ + team_alias: "Byte Contract Team", + organization_id: null, + models: ["no-default-models"], + }); + }); + + it("keeps every newly mounted but untouched field out of the request body", async () => { + await openCreateModal(); + + await openSection("Additional Settings", /Team Member Key Duration/); + await openSection("MCP Settings", /Allowed MCP Servers/); + await openSection("Agent Settings", /Allowed Agents/); + await openSection("Search Tool Settings", /Allowed Search Tools/); + + const payload = await submit(); + + expect(payload).toStrictEqual({ + team_alias: "Byte Contract Team", + organization_id: null, + models: ["no-default-models"], + max_budget: undefined, + budget_duration: undefined, + tpm_limit: undefined, + rpm_limit: undefined, + metadata: undefined, + team_id: undefined, + team_member_budget: undefined, + team_member_key_duration: undefined, + team_member_rpm_limit: undefined, + team_member_tpm_limit: undefined, + secret_manager_settings: undefined, + guardrails: undefined, + disable_global_guardrails: undefined, + policies: undefined, + access_group_ids: undefined, + allowed_vector_store_ids: undefined, + allowed_passthrough_routes: undefined, + allowed_mcp_servers_and_groups: undefined, + mcp_tool_permissions: {}, + allowed_agents_and_groups: undefined, + object_permission_search_tools: undefined, + }); + expect(wireBody(payload)).toStrictEqual({ + team_alias: "Byte Contract Team", + organization_id: null, + models: ["no-default-models"], + mcp_tool_permissions: {}, + }); + }); + + it.each([ + ["MCP Settings", /Allowed MCP Servers/, ["allowed_mcp_servers_and_groups", "mcp_tool_permissions"]], + ["Agent Settings", /Allowed Agents/, ["allowed_agents_and_groups"]], + ["Search Tool Settings", /Allowed Search Tools/, ["object_permission_search_tools"]], + ])("registers %s fields only while that one section is open", async (title, probe, keys) => { + await openCreateModal(); + + const closedPayload = await submit(); + for (const key of keys as string[]) { + expect(closedPayload).not.toHaveProperty(key); + } + }); + + it("carries every typed value to the payload at the type antd sends today", async () => { + await openCreateModal(); + + fireEvent.change(screen.getByLabelText("Max Budget (USD)"), { target: { value: "150.75" } }); + fireEvent.change(screen.getByLabelText("Tokens per minute Limit (TPM)"), { target: { value: "900" } }); + fireEvent.change(screen.getByLabelText("Requests per minute Limit (RPM)"), { target: { value: "800" } }); + + await openSection("Additional Settings", /Team Member Key Duration/); + + fireEvent.change(screen.getByLabelText("Team ID"), { target: { value: "tid-1" } }); + fireEvent.change(screen.getByLabelText("Team Member Budget (USD)"), { target: { value: "12.5" } }); + fireEvent.change(screen.getByLabelText(/Team Member Key Duration/), { target: { value: "30d" } }); + fireEvent.change(screen.getByLabelText("Team Member RPM Limit"), { target: { value: "7" } }); + fireEvent.change(screen.getByLabelText("Team Member TPM Limit"), { target: { value: "8" } }); + fireEvent.change(screen.getByLabelText("Secret Manager Settings"), { + target: { value: '{"namespace":"admin"}' }, + }); + + const payload = await submit(); + + expect(payload.max_budget).toBe("150.75"); + expect(payload.tpm_limit).toBe("900"); + expect(payload.rpm_limit).toBe("800"); + expect(payload.team_id).toBe("tid-1"); + expect(payload.team_member_budget).toBe(12.5); + expect(payload.team_member_key_duration).toBe("30d"); + expect(payload.team_member_rpm_limit).toBe("7"); + expect(payload.team_member_tpm_limit).toBe("8"); + expect(payload.secret_manager_settings).toStrictEqual({ namespace: "admin" }); + }); + + it("blocks the create on an invalid secret manager config, with the rule message suppressed by help", async () => { + await openCreateModal(); + await openSection("Additional Settings", /Team Member Key Duration/); + + fireEvent.change(screen.getByLabelText("Secret Manager Settings"), { target: { value: " " } }); + + const buttons = screen.getAllByRole("button", { name: /create team/i }); + fireEvent.click(buttons[buttons.length - 1]); + + await waitFor(() => { + expect(screen.getByLabelText("Secret Manager Settings")).toHaveAttribute("aria-invalid", "true"); + }); + expect(teamCreateCall).not.toHaveBeenCalled(); + expect(screen.queryByText("Please enter valid JSON")).not.toBeInTheDocument(); + }); + + it("turns the disable-global-guardrails switch into a boolean for a premium user", async () => { + await openCreateModal({ premiumUser: true }); + await openSection("Additional Settings", /Team Member Key Duration/); + + const switches = screen.getAllByRole("switch"); + fireEvent.click(switches[switches.length - 1]); + + const payload = await submit(); + + expect(payload.disable_global_guardrails).toBe(true); + }); + + it("leaves the disable-global-guardrails switch inert for a non-premium user", async () => { + await openCreateModal(); + await openSection("Additional Settings", /Team Member Key Duration/); + + const switches = screen.getAllByRole("switch"); + fireEvent.click(switches[switches.length - 1]); + + const payload = await submit(); + + expect(payload.disable_global_guardrails).toBeUndefined(); + }); + + it.each([ + ["MCP Settings", /Allowed MCP Servers/, ["allowed_mcp_servers_and_groups", "mcp_tool_permissions"]], + ["Agent Settings", /Allowed Agents/, ["allowed_agents_and_groups"]], + ["Search Tool Settings", /Allowed Search Tools/, ["object_permission_search_tools"]], + ])("adds the %s keys as soon as that one section is opened", async (title, probe, keys) => { + await openCreateModal(); + + await openSection(title as string, probe as RegExp); + const payload = await submit(); + + for (const key of keys as string[]) { + expect(payload).toHaveProperty(key); + } + }); + + it("leaves policies out of the request body for a caller without the viewPolicies capability", async () => { + can.mockReturnValue(false); + + await openCreateModal(); + await openSection("Additional Settings", /Team Member Key Duration/); + + const payload = await submit(); + + expect(payload).toStrictEqual({ + team_alias: "Byte Contract Team", + organization_id: null, + models: ["no-default-models"], + max_budget: undefined, + budget_duration: undefined, + tpm_limit: undefined, + rpm_limit: undefined, + metadata: undefined, + team_id: undefined, + team_member_budget: undefined, + team_member_key_duration: undefined, + team_member_rpm_limit: undefined, + team_member_tpm_limit: undefined, + secret_manager_settings: undefined, + guardrails: undefined, + disable_global_guardrails: undefined, + access_group_ids: undefined, + allowed_vector_store_ids: undefined, + allowed_passthrough_routes: undefined, + }); + }); + + it("blocks the create on an empty team name and names the rule", async () => { + renderWithQueryClient(); + act(() => { + fireEvent.click(screen.getAllByRole("button", { name: /create team/i })[0]); + }); + await waitFor(() => { + expect(screen.getByLabelText(/team name/i)).toBeInTheDocument(); + }); + + const buttons = screen.getAllByRole("button", { name: /create team/i }); + fireEvent.click(buttons[buttons.length - 1]); + + expect(await screen.findByText("Please input a team name")).toBeInTheDocument(); + expect(teamCreateCall).not.toHaveBeenCalled(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/Teams.tsx b/ui/litellm-dashboard/src/components/Teams.tsx index b1f96f51028..94e84c6e721 100644 --- a/ui/litellm-dashboard/src/components/Teams.tsx +++ b/ui/litellm-dashboard/src/components/Teams.tsx @@ -4,12 +4,21 @@ import AvailableTeamsPanel from "@/components/team/AvailableTeamsPanel"; import TeamInfoView from "@/components/team/TeamInfo"; import TeamSSOSettings from "@/components/TeamSSOSettings"; import { isProxyAdminRole } from "@/utils/roles"; -import { InfoCircleOutlined } from "@ant-design/icons"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Input as UIInput } from "@/components/ui/input"; -import { Button, Form, Input, Layout, Modal, Select, Switch, Tabs, theme, Tooltip, Typography } from "antd"; +import { Switch } from "@/components/ui/switch"; +import { Textarea } from "@/components/ui/textarea"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { Field, FieldDescription, FieldGroup, FieldLabel } from "@/components/shared/form/field"; +import { FormField } from "@/components/shared/form/FormField"; +import { SearchSelect } from "@/components/shared/SearchSelect"; +import { labelWithDocsHint, labelWithHint } from "@/components/shared/form/LabelWithHint"; +import { useZodForm } from "@/lib/forms/useZodForm"; +import { TagsInput } from "@/app/(dashboard)/guardrails/_components/content_filter/TagsInput"; +import { Layout, Modal, Tabs, theme } from "antd"; import { ChevronDown, Plus, Users } from "lucide-react"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; +import { z } from "zod/v4"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { PageHeader } from "@/components/shared/PageHeader"; import { Button as UIButton } from "@/components/ui/button"; @@ -17,7 +26,10 @@ import { teamsTableKeys } from "@/app/(dashboard)/hooks/teams/useTeams"; import { parseAsString, useQueryState } from "nuqs"; import { TeamsTable } from "./TeamsPage/TeamsTable"; import AccessGroupSelector from "./common_components/AccessGroupSelector"; -import MetadataKeyValueFields, { metadataPairsToObject } from "./common_components/MetadataKeyValueFields"; +import MetadataKeyValueFields, { + metadataPairsSchema, + metadataPairsToObject, +} from "./common_components/MetadataKeyValueFields"; import { useTeamMetadataSchema } from "@/app/(dashboard)/hooks/teams/useTeamMetadataSchema"; import PassThroughRoutesSelector from "./common_components/PassThroughRoutesSelector"; import AgentSelector from "./agent_management/AgentSelector"; @@ -51,6 +63,102 @@ import { teamCreateCall } from "./networking"; import { normalizeTeamModelSelection } from "./team/teamModelAccess"; import { ModelSelect } from "./ModelSelect/ModelSelect"; +const SUPPRESSED_BY_DESCRIPTION = ""; + +const numericInputSchema = z.union([z.string(), z.number()]).optional(); + +const teamCreateFieldsSchema = z.object({ + team_alias: z.string().min(1, "Please input a team name"), + organization_id: z.string().nullish(), + models: z.array(z.string()).optional(), + max_budget: numericInputSchema, + budget_duration: z.string().nullish(), + tpm_limit: numericInputSchema, + rpm_limit: numericInputSchema, + metadata: metadataPairsSchema.optional(), + team_id: z.string().optional(), + team_member_budget: z.number().optional(), + team_member_key_duration: z.string().optional(), + team_member_rpm_limit: numericInputSchema, + team_member_tpm_limit: numericInputSchema, + secret_manager_settings: z.string().optional(), + guardrails: z.array(z.string()).optional(), + disable_global_guardrails: z.boolean().optional(), + policies: z.array(z.string()).optional(), + access_group_ids: z.array(z.string()).optional(), + allowed_vector_store_ids: z.array(z.string()).optional(), + allowed_passthrough_routes: z.array(z.string()).optional(), + allowed_mcp_servers_and_groups: z + .object({ + servers: z.array(z.string()), + accessGroups: z.array(z.string()), + toolsets: z.array(z.string()).optional(), + }) + .optional(), + mcp_tool_permissions: z.record(z.string(), z.array(z.string())).optional(), + allowed_agents_and_groups: z.object({ agents: z.array(z.string()), accessGroups: z.array(z.string()) }).optional(), + object_permission_search_tools: z.array(z.string()).optional(), +}); + +type TeamCreateFormValues = z.infer; + +const EMPTY_TEAM_CREATE_VALUES: TeamCreateFormValues = { + team_alias: "", + organization_id: null, + models: [], + max_budget: undefined, + budget_duration: undefined, + tpm_limit: undefined, + rpm_limit: undefined, + metadata: [], + team_id: undefined, + team_member_budget: undefined, + team_member_key_duration: undefined, + team_member_rpm_limit: undefined, + team_member_tpm_limit: undefined, + secret_manager_settings: undefined, + guardrails: undefined, + disable_global_guardrails: undefined, + policies: undefined, + access_group_ids: undefined, + allowed_vector_store_ids: undefined, + allowed_passthrough_routes: undefined, + allowed_mcp_servers_and_groups: undefined, + mcp_tool_permissions: {}, + allowed_agents_and_groups: undefined, + object_permission_search_tools: undefined, +}; + +const ADDITIONAL_SETTINGS_FIELDS = [ + "team_id", + "team_member_budget", + "team_member_key_duration", + "team_member_rpm_limit", + "team_member_tpm_limit", + "secret_manager_settings", + "guardrails", + "disable_global_guardrails", + "policies", + "access_group_ids", + "allowed_vector_store_ids", + "allowed_passthrough_routes", +] as const; +const MCP_SETTINGS_FIELDS = ["allowed_mcp_servers_and_groups", "mcp_tool_permissions"] as const; +const AGENT_SETTINGS_FIELDS = ["allowed_agents_and_groups"] as const; +const SEARCH_TOOL_SETTINGS_FIELDS = ["object_permission_search_tools"] as const; + +const isParsableJson = (value: string | undefined): boolean => { + if (!value) { + return true; + } + try { + JSON.parse(value); + return true; + } catch { + return false; + } +}; + const canCreateOrManageTeams = ( userRole: string | null, userID: string | null, @@ -101,7 +209,29 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser const [currentOrg] = useState(null); const [currentOrgForCreateTeam, setCurrentOrgForCreateTeam] = useState(null); - const [form] = Form.useForm(); + const isOrgAdmin = userRole !== "Admin"; + const [additionalSettingsOpen, setAdditionalSettingsOpen] = useState(false); + const [mcpSettingsOpen, setMcpSettingsOpen] = useState(false); + const [agentSettingsOpen, setAgentSettingsOpen] = useState(false); + const [searchToolSettingsOpen, setSearchToolSettingsOpen] = useState(false); + + const teamCreateSchema = useMemo( + () => + teamCreateFieldsSchema.superRefine((values, ctx) => { + if (isOrgAdmin && !values.organization_id) { + ctx.addIssue({ code: "custom", message: SUPPRESSED_BY_DESCRIPTION, path: ["organization_id"] }); + } + if (additionalSettingsOpen && !isParsableJson(values.secret_manager_settings)) { + ctx.addIssue({ code: "custom", message: SUPPRESSED_BY_DESCRIPTION, path: ["secret_manager_settings"] }); + } + }), + [isOrgAdmin, additionalSettingsOpen], + ); + + const form = useZodForm(teamCreateSchema, { defaultValues: EMPTY_TEAM_CREATE_VALUES }); + const watchedOrganizationId = form.watch("organization_id"); + const watchedMcpSelection = form.watch("allowed_mcp_servers_and_groups"); + const watchedToolPermissions = form.watch("mcp_tool_permissions"); const [selectedTeam, setSelectedTeam] = useState(null); const [selectedTeamId, setSelectedTeamId] = useQueryState("team", parseAsString.withOptions({ history: "push" })); @@ -134,27 +264,26 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser : "n/a"; useEffect(() => { - form.setFieldValue("models", []); + form.setValue("models", []); }, [currentOrgForCreateTeam, userModels]); // Handle organization preselection when modal opens useEffect(() => { if (isTeamModalVisible) { const adminOrgs = getAdminOrganizations(userRole, userID, organizations); - const isOrgAdmin = userRole !== "Admin"; // Org admins must scope a team to an org, so with exactly one we preselect it. // Proxy admins can create org-less teams, so the field stays optional regardless of org count. if (isOrgAdmin && adminOrgs.length === 1) { const org = adminOrgs[0]; - form.setFieldValue("organization_id", org.organization_id); + form.setValue("organization_id", org.organization_id); setCurrentOrgForCreateTeam(org); } else { - form.setFieldValue("organization_id", currentOrg?.organization_id || null); + form.setValue("organization_id", currentOrg?.organization_id || null); setCurrentOrgForCreateTeam(currentOrg); } } - }, [isTeamModalVisible, userRole, userID, organizations, currentOrg]); + }, [isTeamModalVisible, isOrgAdmin, userRole, userID, organizations, currentOrg]); // Add this useEffect to fetch guardrails useEffect(() => { @@ -190,22 +319,26 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser if (canViewPolicies) fetchPolicies(); }, [accessToken, canViewPolicies]); - const handleOk = () => { - setIsTeamModalVisible(false); - form.resetFields(); + const resetCreateForm = () => { + form.reset(EMPTY_TEAM_CREATE_VALUES); + setAdditionalSettingsOpen(false); + setMcpSettingsOpen(false); + setAgentSettingsOpen(false); + setSearchToolSettingsOpen(false); setLoggingSettings([]); setModelAliases({}); setRouterSettings(null); setRouterSettingsKey((prev) => prev + 1); }; + const handleOk = () => { + setIsTeamModalVisible(false); + resetCreateForm(); + }; + const handleCancel = () => { setIsTeamModalVisible(false); - form.resetFields(); - setLoggingSettings([]); - setModelAliases({}); - setRouterSettings(null); - setRouterSettingsKey((prev) => prev + 1); + resetCreateForm(); }; const handleDelete = async (team: Team) => { @@ -378,11 +511,7 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser await teamCreateCall(accessToken, { ...formValues, models: normalizeTeamModelSelection(formValues.models) }); toast.success("Team created"); await refreshTeams(); - form.resetFields(); - setLoggingSettings([]); - setModelAliases({}); - setRouterSettings(null); - setRouterSettingsKey((prev) => prev + 1); + resetCreateForm(); setIsTeamModalVisible(false); } } catch (error) { @@ -391,6 +520,19 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser } }; + const mountedCreateValues = (values: TeamCreateFormValues): Record => { + const unmounted = new Set([ + ...(additionalSettingsOpen ? [] : ADDITIONAL_SETTINGS_FIELDS), + ...(additionalSettingsOpen && canViewPolicies ? [] : ["policies"]), + ...(mcpSettingsOpen ? [] : MCP_SETTINGS_FIELDS), + ...(agentSettingsOpen ? [] : AGENT_SETTINGS_FIELDS), + ...(searchToolSettingsOpen ? [] : SEARCH_TOOL_SETTINGS_FIELDS), + ]); + return Object.fromEntries(Object.entries(values).filter(([key]) => !unmounted.has(key))); + }; + + const onCreateSubmit = (values: TeamCreateFormValues) => handleCreate(mountedCreateValues(values)); + const is_team_admin = (team: any) => { if (team == null || team.members_with_roles == null) { return false; @@ -405,7 +547,6 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser }; const { token } = theme.useToken(); - const { Text } = Typography; const { Content } = Layout; const tabItems = [ @@ -531,556 +672,538 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser onCancel={handleCancel} destroyOnHidden > -
- <> - - - - {(() => { - const adminOrgs = getAdminOrganizations(userRole, userID, organizations); - const isOrgAdmin = userRole !== "Admin"; - const isSingleOrg = adminOrgs.length === 1; - const hasNoOrgs = adminOrgs.length === 0; - - 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" - rules={ - isOrgAdmin - ? [ - { - required: true, - message: "Please select an organization", - }, - ] - : [] - } - help={ - isOrgAdmin && isSingleOrg - ? "You can only create teams within this organization" - : isOrgAdmin - ? "required" - : "" - } - > - - - - {/* Show message when org admin needs to select organization */} - {isOrgAdmin && !isSingleOrg && adminOrgs.length > 1 && ( -
- - Please select an organization to create a team for. You can only create teams within - organizations where you are an admin. - -
- )} - - ); - })()} - - Models{" "} - - - - - } - name="models" - > - form.setFieldValue("models", values)} - organizationID={form.getFieldValue("organization_id")} - options={{ - includeSpecialOptions: true, - showAllProxyModelsOverride: !form.getFieldValue("organization_id"), - }} - context="team" - dataTestId="create-team-models-select" - /> - - - - - - - - - - - - - - - - - - - - - Additional Settings - - - - - { - e.target.value = e.target.value.trim(); - }} - /> - - (value ? Number(value) : undefined)} - tooltip="This is the individual budget for a user in the 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()} - > - - - - - } - name="guardrails" - className="mt-8" - help="Select existing guardrails or enter new ones" - > - ({ - value: name, - label: name, - }))} - /> - + + + + + {({ ref, value, ...field }) => ( + )} - - Access Groups{" "} - - - - - } - name="access_group_ids" - className="mt-8" - help="Select access groups to assign to this team" - > - - - - Allowed Vector Stores{" "} - - - - - } - name="allowed_vector_store_ids" - className="mt-8" - help="Select vector stores this team can access. Leave empty for access to all vector stores" - > - form.setFieldValue("allowed_vector_store_ids", values)} - value={form.getFieldValue("allowed_vector_store_ids")} - accessToken={accessToken || ""} - placeholder="Select vector stores (optional)" - /> - - - - - - + + {(() => { + const adminOrgs = getAdminOrganizations(userRole, userID, organizations); + const isSingleOrg = adminOrgs.length === 1; + const hasNoOrgs = adminOrgs.length === 0; - - - 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)" - allowAllProxyMcpServers={isProxyAdminRole(userRole || "")} + return ( + <> + + {({ id, value, onChange }) => ( + ({ + value: org.organization_id ?? "", + label: org.organization_alias ?? "", + sublabel: org.organization_id ?? "", + }))} + disabled={isOrgAdmin && isSingleOrg} + allowClear={!isOrgAdmin} + placeholder={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); + }} + /> + )} + + + {isOrgAdmin && !isSingleOrg && adminOrgs.length > 1 && ( +
+ + Please select an organization to create a team for. You can only create teams within + organizations where you are an admin. + +
+ )} + + ); + })()} + + {({ id, value, onChange }) => ( + -
+ )} + - {/* Hidden field to register mcp_tool_permissions with the form */} - + + {({ ref, value, ...field }) => ( + + )} + + + {({ id, value, onChange }) => ( + + )} + + + {({ ref, value, ...field }) => ( + + )} + + + {({ ref, value, ...field }) => ( + + )} + + + Metadata + + + Values are saved as text. Enter JSON for typed values, e.g. 3, true, or {'{"region": "us"}'}. + + - - prevValues.allowed_mcp_servers_and_groups !== currentValues.allowed_mcp_servers_and_groups || - prevValues.mcp_tool_permissions !== currentValues.mcp_tool_permissions - } - > - {() => ( -
- + + Additional Settings + + + + + + {({ ref, value, ...field }) => } + + + {({ ref, value, onChange, ...field }) => ( + ) => + onChange(event.target.value ? Number(event.target.value) : undefined) + } + step={0.01} + precision={2} + width={200} + /> + )} + + + {({ ref, value, ...field }) => ( + + )} + + + {({ ref, value, ...field }) => ( + + )} + + + {({ ref, value, ...field }) => ( + + )} + + + {({ ref, value, ...field }) => ( +