mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
fix(ui): let the internal user and org forms save sub-cent budgets
The Default User Settings form on Internal Users, the org settings form and
the org create dialog all rendered their money fields as
`<input type="number" step={0.01}>` inside a form that never opted out of
native constraint validation. Any value with more than two decimals, such as
a 0.001 max budget, failed the browser's step check, so Chrome vetoed the
submit before react-hook-form ran. No request went out, no field error was
shown, and the read view kept displaying the old value; it looked like the
budget silently refused to stick.
Money fields now use `step="any"`, and the three react-hook-form forms carry
`noValidate` so zod stays the only validator and a DOM-level constraint can
never swallow a submit again.
This commit is contained in:
parent
8ccbc3e735
commit
9dbf530d6e
6 changed files with 75 additions and 7 deletions
|
|
@ -149,6 +149,34 @@ describe("DefaultUserSettingsForm", () => {
|
|||
expect(updateSettings).toHaveBeenCalledWith({ ...SAVED_BODY, max_budget: 250 });
|
||||
});
|
||||
|
||||
it("saves a sub-cent budget the browser would veto under a 0.01 step", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { updateSettings } = renderForm();
|
||||
|
||||
await enterEditMode(user);
|
||||
const budget: HTMLInputElement = await screen.findByLabelText("Max Budget (USD)");
|
||||
await user.clear(budget);
|
||||
await user.type(budget, "0.001");
|
||||
|
||||
const teamBudget: HTMLInputElement = screen.getByLabelText("Max Budget in Team (USD)");
|
||||
await user.clear(teamBudget);
|
||||
await user.type(teamBudget, "0.002");
|
||||
|
||||
// jsdom never blocks the submit itself, so assert the constraint the real browser
|
||||
// enforces before handleSubmit ever runs
|
||||
expect(budget.checkValidity()).toBe(true);
|
||||
expect(teamBudget.checkValidity()).toBe(true);
|
||||
|
||||
await user.click(await saveButton());
|
||||
|
||||
await waitFor(() => expect(updateSettings).toHaveBeenCalledTimes(1));
|
||||
expect(updateSettings).toHaveBeenCalledWith({
|
||||
...SAVED_BODY,
|
||||
max_budget: 0.001,
|
||||
teams: [{ team_id: "team-alpha", max_budget_in_team: 0.002, user_role: "user" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("clears an emptied budget with null", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { updateSettings } = renderForm();
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ const TeamsField = ({ control }: { control: SettingsControl }) => {
|
|||
|
||||
<FormField control={control} name={`teams.${index}.max_budget_in_team`} label="Max Budget in Team (USD)">
|
||||
{({ ref, ...budgetField }) => (
|
||||
<Input {...budgetField} ref={ref} type="number" step={0.01} min={0} placeholder="Optional" />
|
||||
<Input {...budgetField} ref={ref} type="number" step="any" min={0} placeholder="Optional" />
|
||||
)}
|
||||
</FormField>
|
||||
|
||||
|
|
@ -249,7 +249,7 @@ const SettingsForm = ({ initialValues, roleOptions, updateSettings, onCancel, on
|
|||
const onSubmit = form.handleSubmit((values) => mutation.mutate(values));
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
<form onSubmit={onSubmit} noValidate>
|
||||
<FieldGroup>
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
|
@ -286,7 +286,7 @@ const SettingsForm = ({ initialValues, roleOptions, updateSettings, onCancel, on
|
|||
label="Max Budget (USD)"
|
||||
description="Default maximum budget for new users"
|
||||
>
|
||||
{({ ref, ...field }) => <Input {...field} ref={ref} type="number" step={0.01} min={0} />}
|
||||
{({ ref, ...field }) => <Input {...field} ref={ref} type="number" step="any" min={0} />}
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
|
|
|
|||
|
|
@ -84,6 +84,28 @@ describe("OrgCreateDialog", () => {
|
|||
await waitFor(() => expect(screen.queryByLabelText("Organization Name")).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it("creates with a sub-cent max budget the browser would veto under a 0.01 step", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { createOrganization } = renderDialog();
|
||||
|
||||
await user.type(screen.getByLabelText("Organization Name"), "new-org");
|
||||
const budget: HTMLInputElement = screen.getByLabelText("Max Budget (USD)");
|
||||
await user.type(budget, "0.001");
|
||||
|
||||
// jsdom never blocks the submit itself, so assert the constraint the real browser
|
||||
// enforces before handleSubmit ever runs
|
||||
expect(budget.checkValidity()).toBe(true);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Create Organization" }));
|
||||
|
||||
await waitFor(() => expect(createOrganization).toHaveBeenCalledTimes(1));
|
||||
expect(createOrganization.mock.calls[0][0]).toStrictEqual({
|
||||
organization_alias: "new-org",
|
||||
models: [],
|
||||
max_budget: 0.001,
|
||||
});
|
||||
});
|
||||
|
||||
it("maps selectors and limits into the create body", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { createOrganization } = renderDialog();
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export const OrgCreateDialog = ({
|
|||
<DialogTitle>Create Organization</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={onSubmit}>
|
||||
<form onSubmit={onSubmit} noValidate>
|
||||
<FieldGroup>
|
||||
<FormField control={form.control} name="organization_alias" label="Organization Name">
|
||||
{({ ref, ...field }) => <Input {...field} ref={ref} />}
|
||||
|
|
@ -97,7 +97,7 @@ export const OrgCreateDialog = ({
|
|||
</FormField>
|
||||
|
||||
<FormField control={form.control} name="max_budget" label="Max Budget (USD)">
|
||||
{({ ref, ...field }) => <Input {...field} ref={ref} type="number" step={0.01} min={0} />}
|
||||
{({ ref, ...field }) => <Input {...field} ref={ref} type="number" step="any" min={0} />}
|
||||
</FormField>
|
||||
|
||||
<FormField control={form.control} name="budget_duration" label="Reset Budget">
|
||||
|
|
|
|||
|
|
@ -113,6 +113,24 @@ describe("OrgSettingsForm", () => {
|
|||
expect(patchOrganization).toHaveBeenCalledWith("org-1", { organization_alias: "acme-2" });
|
||||
});
|
||||
|
||||
it("saves a sub-cent max budget the browser would veto under a 0.01 step", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { patchOrganization } = renderForm();
|
||||
|
||||
const budget: HTMLInputElement = screen.getByLabelText("Max Budget (USD)");
|
||||
await user.clear(budget);
|
||||
await user.type(budget, "0.001");
|
||||
|
||||
// jsdom never blocks the submit itself, so assert the constraint the real browser
|
||||
// enforces before handleSubmit ever runs
|
||||
expect(budget.checkValidity()).toBe(true);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Save Changes" }));
|
||||
|
||||
await waitFor(() => expect(patchOrganization).toHaveBeenCalledTimes(1));
|
||||
expect(patchOrganization).toHaveBeenCalledWith("org-1", { max_budget: 0.001 });
|
||||
});
|
||||
|
||||
it("sends null when a limit is cleared", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { patchOrganization } = renderForm();
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ export const OrgSettingsForm = ({
|
|||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
<form onSubmit={onSubmit} noValidate>
|
||||
<FieldGroup>
|
||||
<FormField control={form.control} name="organization_alias" label="Organization Name">
|
||||
{({ ref, ...field }) => <Input {...field} ref={ref} />}
|
||||
|
|
@ -96,7 +96,7 @@ export const OrgSettingsForm = ({
|
|||
</FormField>
|
||||
|
||||
<FormField control={form.control} name="max_budget" label="Max Budget (USD)">
|
||||
{({ ref, ...field }) => <Input {...field} ref={ref} type="number" step={0.01} min={0} />}
|
||||
{({ ref, ...field }) => <Input {...field} ref={ref} type="number" step="any" min={0} />}
|
||||
</FormField>
|
||||
|
||||
<FormField control={form.control} name="budget_duration" label="Reset Budget">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue