From f85abd4b88219d7b5497efbb7786c232e9a2c26a Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Thu, 30 Jul 2026 19:02:30 -0700 Subject: [PATCH] fix(ui): let the per-user Internal Users edit form save sub-cent budgets The same native step veto lived on the per-user edit form: its Max Budget field renders through NumericalInput with step={0.01} inside an antd form that never opted out of constraint validation, so typing 0.001 on a user and pressing Save Changes did nothing at all; no request, no error, and the detail view kept showing the previous budget. The field now uses step="any" and the form carries noValidate, so antd's own rules stay the only gate. The dead precision={2} prop went with it: it is an antd InputNumber prop and NumericalInput renders a Tremor NumberInput, so it only ever leaked to the DOM. --- .../users/_components/user_edit_view.test.tsx | 18 ++++++++++++++++++ .../users/_components/user_edit_view.tsx | 4 ++-- .../src/components/shared/numerical_input.tsx | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/user_edit_view.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/user_edit_view.test.tsx index 4d5ddaa1df6..4858178a76f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/user_edit_view.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/user_edit_view.test.tsx @@ -389,6 +389,24 @@ describe("UserEditView", () => { expect(callArgs.metadata).toEqual(MOCK_USER_DATA.user_info.metadata); }); + it("should submit a sub-cent max budget the browser would veto under a 0.01 step", async () => { + const onSubmitMock = vi.fn(); + renderWithProviders(); + + const budget: HTMLInputElement = await screen.findByRole("spinbutton"); + await userEvent.clear(budget); + await userEvent.type(budget, "0.001"); + + // jsdom never blocks the submit itself, so assert the constraint the real browser + // enforces before onFinish ever runs + expect(budget.checkValidity()).toBe(true); + + await userEvent.click(screen.getByRole("button", { name: /save changes/i })); + + await waitFor(() => expect(onSubmitMock).toHaveBeenCalled()); + expect(Number(onSubmitMock.mock.calls[0][0].max_budget)).toBe(0.001); + }); + it("should set max_budget to null when unlimited budget is checked on submit", async () => { const onSubmitMock = vi.fn(); const userDataWithNullBudget = { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/user_edit_view.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/user_edit_view.tsx index c83a8e48e43..4b29ad7ef5d 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/user_edit_view.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/user_edit_view.tsx @@ -96,7 +96,7 @@ export function UserEditView({ }; return ( -
+ {!isBulkEdit && ( @@ -191,7 +191,7 @@ export function UserEditView({ }, ]} > - + diff --git a/ui/litellm-dashboard/src/components/shared/numerical_input.tsx b/ui/litellm-dashboard/src/components/shared/numerical_input.tsx index 2682635dc27..86729237bb0 100644 --- a/ui/litellm-dashboard/src/components/shared/numerical_input.tsx +++ b/ui/litellm-dashboard/src/components/shared/numerical_input.tsx @@ -2,7 +2,7 @@ import React from "react"; import { NumberInput } from "@tremor/react"; interface NumericalInputProps { - step?: number; + step?: number | "any"; style?: React.CSSProperties; placeholder?: string; min?: number;