mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-05 07:37:50 +00:00
fix(ui): send a team admin's member key edits on the first save (#50007)
The key edit form read react-hook-form's dirtyFields only inside the submit handler. formState is a proxy that only tracks what the component reads while rendering, so on the first save of an edit session that read returned the initial empty object. A team admin editing a member's key then sent only the key id to /key/update, got a 200 and a "Key updated successfully" toast, and nothing changed: a max_budget edit was silently dropped, and a rename went through as an empty update instead of the "team admins can only change budget fields" error. Read dirtyFields during render so the form subscribes to it and the submit handler sees the fields the user actually changed.
This commit is contained in:
parent
906fa7ed28
commit
5e91bc8e86
2 changed files with 57 additions and 1 deletions
|
|
@ -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<string, unknown>, dirtyFields: readonly string[]) => Promise<void>,
|
||||
) =>
|
||||
renderWithProviders(
|
||||
<KeyEditView
|
||||
keyData={{ ...MOCK_KEY_DATA, team_id: "team-1", user_id: "member-user" }}
|
||||
teams={[
|
||||
{
|
||||
team_id: "team-1",
|
||||
team_alias: "Team One",
|
||||
models: [],
|
||||
members_with_roles: [{ user_id: "team-admin-user", role: "admin" }],
|
||||
},
|
||||
]}
|
||||
onCancel={() => {}}
|
||||
onSubmit={onSubmit}
|
||||
accessToken="test-token"
|
||||
userID="team-admin-user"
|
||||
userRole="Internal User"
|
||||
/>,
|
||||
);
|
||||
|
||||
const firstSavePayload = async (onSubmit: ReturnType<typeof vi.fn>) => {
|
||||
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" },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ function KeyEditSession({
|
|||
values: toKeyEditFormValues(keyData),
|
||||
resetOptions: { keepDirtyValues: true },
|
||||
});
|
||||
const { dirtyFields } = form.formState;
|
||||
const [promptsList, setPromptsList] = useState<string[]>([]);
|
||||
const [tagsList, setTagsList] = useState<Record<string, Tag>>({});
|
||||
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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue