diff --git a/ui/litellm-dashboard/src/components/templates/key_edit_view.integration.test.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.integration.test.tsx index dce14adc2e5..d590d7beef9 100644 --- a/ui/litellm-dashboard/src/components/templates/key_edit_view.integration.test.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.integration.test.tsx @@ -15,6 +15,7 @@ import { vectorStoreListCall, } from "../networking"; import { KeyEditView } from "./key_edit_view"; +import { teamAdminMemberKeyPayload } from "./teamAdminMemberKeyPayload"; const can = vi.fn(); vi.mock("@/app/(dashboard)/hooks/useCan", () => ({ @@ -3234,4 +3235,58 @@ describe("KeyEditView", () => { expect(await screen.findByRole("switch", { name: /disable global guardrails/i })).toBeInTheDocument(); }); }); + + describe("team admin editing a member's key", () => { + const renderAsTeamAdmin = ( + onSubmit: (values: Record, dirtyFields: readonly string[]) => Promise, + ) => + renderWithProviders( + {}} + onSubmit={onSubmit} + accessToken="test-token" + userID="team-admin-user" + userRole="Internal User" + />, + ); + + const firstSavePayload = async (onSubmit: ReturnType) => { + await userEvent.click(screen.getByRole("button", { name: /save changes/i })); + await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); + const [values, dirtyFields] = onSubmit.mock.calls[0]; + return teamAdminMemberKeyPayload({ ...values, key: values.token }, dirtyFields); + }; + + it("should block a rename on the first save instead of sending an empty update", async () => { + const onSubmit = vi.fn().mockResolvedValue(undefined); + renderAsTeamAdmin(onSubmit); + + fireEvent.change(await screen.findByRole("textbox", { name: "Key Alias" }), { target: { value: "renamed" } }); + + expect(await firstSavePayload(onSubmit)).toEqual({ kind: "blocked", fields: ["key_alias"] }); + }); + + it("should send a budget change on the first save", async () => { + const onSubmit = vi.fn().mockResolvedValue(undefined); + renderAsTeamAdmin(onSubmit); + + fireEvent.change(await screen.findByRole("spinbutton", { name: "Max Budget (USD)" }), { + target: { value: "25" }, + }); + + expect(await firstSavePayload(onSubmit)).toEqual({ + kind: "ok", + payload: { key: MOCK_KEY_DATA.token, max_budget: "25" }, + }); + }); + }); }); diff --git a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx index 81464d0d195..940108f9721 100644 --- a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx @@ -116,6 +116,7 @@ function KeyEditSession({ values: toKeyEditFormValues(keyData), resetOptions: { keepDirtyValues: true }, }); + const { dirtyFields } = form.formState; const [promptsList, setPromptsList] = useState([]); const [tagsList, setTagsList] = useState>({}); const team = teams?.find((team) => team.team_id === keyData.team_id); @@ -321,7 +322,7 @@ function KeyEditSession({ ...values, ...(detachProject && enableProjectsUI && canDetachProject ? { project_id: null } : {}), }), - [...Object.keys(form.formState.dirtyFields), ...(budgetLimitsUnchanged ? [] : ["budget_limits"])], + [...Object.keys(dirtyFields), ...(budgetLimitsUnchanged ? [] : ["budget_limits"])], ); } finally { setIsKeySaving(false);