From 7a718d19c263f45423098b1914c45a44302daadd Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Fri, 24 Jul 2026 23:49:16 -0700 Subject: [PATCH] fix(ui): keep team role and budget when the team ID is blank normalizeTeams guarded the object branch on the truthiness of team.team_id, so a row with an empty ID fell through to the reset branch that discards user_role and max_budget_in_team. Setting a role before typing an ID showed the row snapping back to "user", and the next edit to that row dropped whatever had been set before it. Guard on the shape of the entry instead. --- .../_components/DefaultUserSettings.test.tsx | 107 ++++++++++++++++++ .../users/_components/DefaultUserSettings.tsx | 2 +- 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/DefaultUserSettings.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/DefaultUserSettings.test.tsx index 06dafcfcffd..6020d0b3ba3 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/DefaultUserSettings.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/DefaultUserSettings.test.tsx @@ -150,4 +150,111 @@ describe("DefaultUserSettings", () => { expect(screen.getByText("Edit Settings")).toBeInTheDocument(); }); + + describe("editable fields", () => { + const teamsOnlySettings = { + values: { + teams: [], + }, + field_schema: { + description: "Default user settings", + properties: { + teams: { + type: "array", + description: "Teams", + }, + }, + }, + }; + + const enterEditMode = async () => { + mockGetInternalUserSettings.mockResolvedValue(teamsOnlySettings); + mockUpdateInternalUserSettings.mockResolvedValue({ settings: {} }); + + render(); + + await waitFor(() => { + expect(screen.getByText("Edit Settings")).toBeInTheDocument(); + }); + + act(() => { + fireEvent.click(screen.getByText("Edit Settings")); + }); + }; + + const savedPayload = () => mockUpdateInternalUserSettings.mock.calls[0][1] as Record; + + const selectUserRole = async (optionText: string) => { + act(() => { + fireEvent.mouseDown(document.querySelector(".ant-select-selector")!); + }); + + await waitFor(() => { + expect(document.querySelectorAll(".ant-select-item-option").length).toBeGreaterThan(0); + }); + + const option = Array.from(document.querySelectorAll(".ant-select-item-option")).find((el) => + el.textContent?.includes(optionText), + ); + expect(option).toBeTruthy(); + + act(() => { + fireEvent.click(option!); + }); + }; + + it("keeps the stored role and max budget of a team saved without an ID", async () => { + mockGetInternalUserSettings.mockResolvedValue({ + ...teamsOnlySettings, + values: { teams: [{ team_id: "", max_budget_in_team: 25, user_role: "admin" }] }, + }); + mockUpdateInternalUserSettings.mockResolvedValue({ settings: {} }); + + render(); + + await waitFor(() => { + expect(screen.getByText("Edit Settings")).toBeInTheDocument(); + }); + act(() => { + fireEvent.click(screen.getByText("Edit Settings")); + }); + + expect(screen.getByPlaceholderText("Enter team ID")).toHaveValue(""); + expect(screen.getByPlaceholderText("Optional")).toHaveValue("25.00"); + expect(document.querySelector(".ant-select-selection-item")).toHaveTextContent("Admin"); + }); + + it("keeps showing the selected role of a team whose ID has not been typed yet", async () => { + await enterEditMode(); + + act(() => { + fireEvent.click(screen.getByText("Add Team")); + }); + await selectUserRole("Admin"); + + expect(document.querySelector(".ant-select-selection-item")).toHaveTextContent("Admin"); + }); + + it("keeps the role and max budget of a team whose ID has not been typed yet", async () => { + await enterEditMode(); + + act(() => { + fireEvent.click(screen.getByText("Add Team")); + }); + + act(() => { + fireEvent.change(screen.getByPlaceholderText("Optional"), { target: { value: "25" } }); + }); + await selectUserRole("Admin"); + + act(() => { + fireEvent.click(screen.getByText("Save Changes")); + }); + + await waitFor(() => { + expect(mockUpdateInternalUserSettings).toHaveBeenCalled(); + }); + expect(savedPayload().teams).toEqual([{ team_id: "", max_budget_in_team: 25, user_role: "admin" }]); + }); + }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/DefaultUserSettings.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/DefaultUserSettings.tsx index 7fee2e14b27..b14dadefe80 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/DefaultUserSettings.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/DefaultUserSettings.tsx @@ -115,7 +115,7 @@ const DefaultUserSettings: React.FC = ({ team_id: team, user_role: "user" as const, }; - } else if (typeof team === "object" && team.team_id) { + } else if (typeof team === "object" && team !== null && "team_id" in team) { return { team_id: team.team_id, max_budget_in_team: team.max_budget_in_team,