From 24e49d71d7673748ade9e4fe8e51b9d71da57427 Mon Sep 17 00:00:00 2001 From: yucheng Date: Fri, 25 Sep 2026 01:00:39 +0000 Subject: [PATCH] feat(ui): add text input for String general settings so retention periods can be set from the Admin UI Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../general_settings.integration.test.tsx | 57 +++++++++++++++++++ .../_components/general_settings.tsx | 11 +++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.integration.test.tsx index cf5cad564ea..f6c940a2a71 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.integration.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.integration.test.tsx @@ -184,6 +184,63 @@ it("persists a List setting typed as comma-separated text as a trimmed string ar ]); }); +it("persists a String setting typed into its text input", async () => { + vi.mocked(getGeneralSettingsCall).mockResolvedValue([ + { + field_name: "maximum_daily_tag_spend_retention_period", + field_type: "String", + field_value: null, + field_description: "retention", + stored_in_db: null, + }, + ]); + vi.mocked(updateConfigFieldSetting).mockClear(); + const user = userEvent.setup(); + renderWithProviders(); + await user.click(screen.getByRole("tab", { name: "General" })); + const input = await screen.findByRole("textbox", { name: "maximum_daily_tag_spend_retention_period" }); + expect(input).toHaveValue(""); + fireEvent.change(input, { target: { value: "30d" } }); + await user.click( + within(screen.getByRole("row", { name: /maximum_daily_tag_spend_retention_period/ })).getByRole("button", { + name: "Update", + }), + ); + expect(vi.mocked(updateConfigFieldSetting).mock.calls).toEqual([ + ["token", "maximum_daily_tag_spend_retention_period", "30d"], + ]); + expect(screen.getByText("In DB")).toBeInTheDocument(); +}); + +it("clears a stored String setting when Update is clicked on an emptied input", async () => { + vi.mocked(getGeneralSettingsCall).mockResolvedValue([ + { + field_name: "maximum_daily_tag_spend_retention_period", + field_type: "String", + field_value: "30d", + field_description: "retention", + stored_in_db: true, + }, + ]); + vi.mocked(updateConfigFieldSetting).mockClear(); + vi.mocked(deleteConfigFieldSetting).mockClear(); + const user = userEvent.setup(); + renderWithProviders(); + await user.click(screen.getByRole("tab", { name: "General" })); + const input = await screen.findByRole("textbox", { name: "maximum_daily_tag_spend_retention_period" }); + expect(input).toHaveValue("30d"); + fireEvent.change(input, { target: { value: "" } }); + await user.click( + within(screen.getByRole("row", { name: /maximum_daily_tag_spend_retention_period/ })).getByRole("button", { + name: "Update", + }), + ); + expect(vi.mocked(deleteConfigFieldSetting).mock.calls).toEqual([ + ["token", "maximum_daily_tag_spend_retention_period"], + ]); + expect(updateConfigFieldSetting).not.toHaveBeenCalled(); +}); + it("clears a stored List setting when Update is clicked on an emptied input", async () => { vi.mocked(getGeneralSettingsCall).mockResolvedValue([ { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.tsx b/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.tsx index 3d0a2e438ac..ad006f9dd6d 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.tsx @@ -114,6 +114,15 @@ const SettingValueEditor: React.FC<{ /> ); } + if (setting.field_type === "String") { + return ( + onChange(setting.field_name, event.target.value === "" ? null : event.target.value)} + /> + ); + } if (setting.field_type === "Select") { return (