From c12bdfa81dfbd9d6b5dcf23fead72f09f8cfff27 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Thu, 3 Sep 2026 08:24:47 -0700 Subject: [PATCH] fix(ui): prepopulate team ID during team creation --- .../src/components/Teams.test.tsx | 20 +++++++++++++++++-- ui/litellm-dashboard/src/components/Teams.tsx | 9 +++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/ui/litellm-dashboard/src/components/Teams.test.tsx b/ui/litellm-dashboard/src/components/Teams.test.tsx index 2bceb00aae1..a80a19e252c 100644 --- a/ui/litellm-dashboard/src/components/Teams.test.tsx +++ b/ui/litellm-dashboard/src/components/Teams.test.tsx @@ -18,6 +18,7 @@ import Teams from "./Teams"; import { chooseSelectOption } from "../../tests/test-utils"; const can = vi.fn(); +const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; vi.mock("@/app/(dashboard)/hooks/useCan", () => ({ default: (...args: unknown[]) => can(...args), })); @@ -1188,6 +1189,20 @@ describe("Teams - which fields reach the create payload depends on the open sect ); }); + it("prepopulates an editable UUID when the create form opens", async () => { + await openCreateModal(); + fireEvent.change(screen.getByLabelText(/team name/i), { target: { value: "Generated ID Team" } }); + toggleAdditionalSettings(); + const teamIdInput = await screen.findByLabelText("Team ID"); + + const generatedTeamId = (teamIdInput as HTMLInputElement).value; + expect(generatedTeamId).toMatch(UUID_V4_PATTERN); + + const payload = await submit(); + + expect(payload.team_id).toBe(generatedTeamId); + }); + it("drops a value typed in Additional Settings when that section is closed again before saving", async () => { await openCreateModal(); fireEvent.change(screen.getByLabelText(/team name/i), { target: { value: "Reclosed Team" } }); @@ -1318,7 +1333,7 @@ describe("Teams - the exact bytes the create call sends", () => { tpm_limit: undefined, rpm_limit: undefined, metadata: undefined, - team_id: undefined, + team_id: expect.stringMatching(UUID_V4_PATTERN), team_member_budget: undefined, team_member_key_duration: undefined, team_member_rpm_limit: undefined, @@ -1339,6 +1354,7 @@ describe("Teams - the exact bytes the create call sends", () => { team_alias: "Byte Contract Team", organization_id: null, models: ["no-default-models"], + team_id: expect.stringMatching(UUID_V4_PATTERN), mcp_tool_permissions: {}, }); }); @@ -1459,7 +1475,7 @@ describe("Teams - the exact bytes the create call sends", () => { tpm_limit: undefined, rpm_limit: undefined, metadata: undefined, - team_id: undefined, + team_id: expect.stringMatching(UUID_V4_PATTERN), team_member_budget: undefined, team_member_key_duration: undefined, team_member_rpm_limit: undefined, diff --git a/ui/litellm-dashboard/src/components/Teams.tsx b/ui/litellm-dashboard/src/components/Teams.tsx index ef58237a6aa..e853f528efa 100644 --- a/ui/litellm-dashboard/src/components/Teams.tsx +++ b/ui/litellm-dashboard/src/components/Teams.tsx @@ -19,6 +19,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ChevronDown, Plus, Users } from "lucide-react"; import React, { useEffect, useMemo, useState } from "react"; import { z } from "zod/v4"; +import { v4 as uuidv4 } from "uuid"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { PageHeader } from "@/components/shared/PageHeader"; import { Button as UIButton } from "@/components/ui/button"; @@ -77,7 +78,10 @@ const teamCreateFieldsSchema = z.object({ tpm_limit: numericInputSchema, rpm_limit: numericInputSchema, metadata: metadataPairsSchema.optional(), - team_id: z.string().optional(), + team_id: z + .string() + .optional() + .transform((value) => (value?.trim() ? value : undefined)), team_member_budget: z.number().optional(), team_member_key_duration: z.string().optional(), team_member_rpm_limit: numericInputSchema, @@ -314,6 +318,7 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser }, [accessToken, canViewPolicies]); const openCreateTeamModal = () => { + form.setValue("team_id", uuidv4()); // 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) { @@ -829,7 +834,7 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser control={form.control} name="team_id" label="Team ID" - description="ID of the team you want to create. If not provided, it will be generated automatically." + description="Leave blank to generate an ID when the team is created, or enter a custom ID." > {({ ref, value, ...field }) => }