mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-20 00:11:50 +00:00
fix(ui): send only a changed TPM limit from the team admin settings form
Save stays disabled until the value differs from the team's, so an unchanged form never reaches /team/update
This commit is contained in:
parent
ab92a6637d
commit
3ee8d43fdd
4 changed files with 40 additions and 8 deletions
|
|
@ -53,6 +53,18 @@ describe("TeamAdminSettingsForm", () => {
|
|||
await waitFor(() => expect(onSave).toHaveBeenCalledWith({ tpm_limit: null }));
|
||||
});
|
||||
|
||||
it("keeps Save disabled until the TPM limit differs from the team's", () => {
|
||||
renderForm(new Set(["tpm_limit"]));
|
||||
const tpmInput = screen.getByLabelText("Tokens per minute Limit (TPM)");
|
||||
const save = screen.getByRole("button", { name: /save changes/i });
|
||||
|
||||
expect(save).toBeDisabled();
|
||||
fireEvent.change(tpmInput, { target: { value: "5000" } });
|
||||
expect(save).toBeEnabled();
|
||||
fireEvent.change(tpmInput, { target: { value: "1000" } });
|
||||
expect(save).toBeDisabled();
|
||||
});
|
||||
|
||||
it("closes without saving on cancel", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { onSave, onCancel } = renderForm(new Set(["tpm_limit"]));
|
||||
|
|
@ -65,6 +77,7 @@ describe("TeamAdminSettingsForm", () => {
|
|||
|
||||
it("locks both buttons while a save is in flight", () => {
|
||||
renderForm(new Set(["tpm_limit"]), { isSaving: true });
|
||||
fireEvent.change(screen.getByLabelText("Tokens per minute Limit (TPM)"), { target: { value: "5000" } });
|
||||
|
||||
expect(screen.getByRole("button", { name: "Cancel" })).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: /save changes/i })).toBeDisabled();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { Save } from "lucide-react";
|
||||
import { useWatch } from "react-hook-form";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { FormField } from "@/components/shared/form/FormField";
|
||||
|
|
@ -37,7 +38,9 @@ export default function TeamAdminSettingsForm({
|
|||
onSave,
|
||||
}: TeamAdminSettingsFormProps) {
|
||||
const form = useZodForm(teamAdminSettingsSchema, { defaultValues: initialValues });
|
||||
const submit = form.handleSubmit((values) => onSave(teamAdminSettingsChanges(values, editableFields)));
|
||||
const draft = useWatch({ control: form.control });
|
||||
const hasChanges = Object.keys(teamAdminSettingsChanges(draft, initialValues, editableFields)).length > 0;
|
||||
const submit = form.handleSubmit((values) => onSave(teamAdminSettingsChanges(values, initialValues, editableFields)));
|
||||
|
||||
return (
|
||||
<form onSubmit={(event) => void submit(event)}>
|
||||
|
|
@ -56,7 +59,7 @@ export default function TeamAdminSettingsForm({
|
|||
<Button type="button" variant="outline" onClick={onCancel} disabled={isSaving}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSaving}>
|
||||
<Button type="submit" disabled={isSaving || !hasChanges}>
|
||||
{isSaving ? <UiLoadingSpinner className="size-4" /> : <Save className="size-4" />}
|
||||
Save Changes
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -20,21 +20,31 @@ describe("teamAdminFieldLabel", () => {
|
|||
|
||||
describe("teamAdminSettingsChanges", () => {
|
||||
const tpmEnabled = new Set(["tpm_limit"]);
|
||||
const stored = { tpm_limit: 1000 };
|
||||
|
||||
it.each([
|
||||
["a typed number string", "5000", 5000],
|
||||
["a stored number", 1200, 1200],
|
||||
["a number", 1200, 1200],
|
||||
["zero", "0", 0],
|
||||
["an emptied input", "", null],
|
||||
["whitespace", " ", null],
|
||||
["no stored limit", null, null],
|
||||
["no limit", null, null],
|
||||
["an unset value", undefined, null],
|
||||
])("sends tpm_limit for %s", (_label, tpm_limit, expected) => {
|
||||
expect(teamAdminSettingsChanges({ tpm_limit }, tpmEnabled)).toStrictEqual({ tpm_limit: expected });
|
||||
])("sends tpm_limit changed to %s", (_label, tpm_limit, expected) => {
|
||||
expect(teamAdminSettingsChanges({ tpm_limit }, stored, tpmEnabled)).toStrictEqual({ tpm_limit: expected });
|
||||
});
|
||||
|
||||
it.each([
|
||||
["the stored number", 1000, { tpm_limit: 1000 }],
|
||||
["the stored number typed back in", "1000", { tpm_limit: 1000 }],
|
||||
["an emptied input over no stored limit", "", { tpm_limit: null }],
|
||||
["an unset value over no stored limit", undefined, { tpm_limit: null }],
|
||||
])("sends nothing for %s", (_label, tpm_limit, initialValues) => {
|
||||
expect(teamAdminSettingsChanges({ tpm_limit }, initialValues, tpmEnabled)).toStrictEqual({});
|
||||
});
|
||||
|
||||
it("leaves tpm_limit out when the proxy did not enable it for team admins", () => {
|
||||
expect(teamAdminSettingsChanges({ tpm_limit: "5000" }, new Set(["max_budget"]))).toStrictEqual({});
|
||||
expect(teamAdminSettingsChanges({ tpm_limit: "5000" }, stored, new Set(["max_budget"]))).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -59,8 +59,14 @@ const numberOrNull = (value: string | number | null | undefined): number | null
|
|||
|
||||
export const teamAdminSettingsChanges = (
|
||||
values: TeamAdminSettingsValues,
|
||||
initialValues: TeamAdminSettingsValues,
|
||||
editableFields: ReadonlySet<string>,
|
||||
): TeamAdminSettingsChanges => (editableFields.has("tpm_limit") ? { tpm_limit: numberOrNull(values.tpm_limit) } : {});
|
||||
): TeamAdminSettingsChanges => {
|
||||
const tpmLimit = numberOrNull(values.tpm_limit);
|
||||
return editableFields.has("tpm_limit") && tpmLimit !== numberOrNull(initialValues.tpm_limit)
|
||||
? { tpm_limit: tpmLimit }
|
||||
: {};
|
||||
};
|
||||
|
||||
export const parseTeamEditAccess = (callerEditAccess: unknown): TeamEditAccess => {
|
||||
const parsed = callerEditAccessSchema.safeParse(callerEditAccess);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue