diff --git a/ui/litellm-dashboard/src/components/email_settings.test.tsx b/ui/litellm-dashboard/src/components/email_settings.test.tsx index 1f826857b48..87ab05cfb3e 100644 --- a/ui/litellm-dashboard/src/components/email_settings.test.tsx +++ b/ui/litellm-dashboard/src/components/email_settings.test.tsx @@ -34,7 +34,8 @@ const alerts = [ { name: "slack", variables: { SLACK_WEBHOOK_URL: "https://hooks.example.com" } }, ]; -const inputNamed = (name: string) => document.querySelector(`input[name="${name}"]`)!; +const fieldNamed = (name: string) => + document.querySelector(`[name="${name}"]`)!; describe("EmailSettings", () => { beforeEach(() => { @@ -56,17 +57,19 @@ describe("EmailSettings", () => { it("renders one named input per email variable and none for other alert types", () => { renderWithProviders(); - expect(inputNamed("SMTP_HOST")).toHaveValue("smtp.example.com"); - expect(inputNamed("SMTP_PORT")).toHaveValue("587"); - expect(inputNamed("SMTP_PORT")).toHaveAttribute("type", "number"); - expect(inputNamed("SMTP_HOST")).toHaveAttribute("type", "text"); - expect(inputNamed("SMTP_TLS")).toHaveAttribute("type", "text"); - expect(inputNamed("SMTP_USE_SSL")).toHaveAttribute("type", "text"); - expect(inputNamed("SMTP_USERNAME")).toHaveAttribute("type", "text"); - expect(inputNamed("SMTP_PASSWORD")).toHaveValue("********"); - expect(inputNamed("SMTP_SENDER_EMAIL")).toHaveAttribute("type", "text"); - expect(inputNamed("TEST_EMAIL_ADDRESS")).toHaveAttribute("type", "text"); - expect(inputNamed("EMAIL_LOGO_URL")).toHaveAttribute("type", "text"); + expect(fieldNamed("SMTP_HOST")).toHaveValue("smtp.example.com"); + expect(fieldNamed("SMTP_PORT")).toHaveValue(587); + expect(fieldNamed("SMTP_PORT")).toHaveAttribute("type", "number"); + expect(fieldNamed("SMTP_HOST")).toHaveAttribute("type", "text"); + expect(fieldNamed("SMTP_TLS")).toHaveValue("True"); + expect(fieldNamed("SMTP_TLS").tagName).toBe("SELECT"); + expect(fieldNamed("SMTP_USE_SSL")).toHaveValue("False"); + expect(fieldNamed("SMTP_USE_SSL").tagName).toBe("SELECT"); + expect(fieldNamed("SMTP_USERNAME")).toHaveAttribute("type", "text"); + expect(fieldNamed("SMTP_PASSWORD")).toHaveValue("********"); + expect(fieldNamed("SMTP_SENDER_EMAIL")).toHaveAttribute("type", "text"); + expect(fieldNamed("TEST_EMAIL_ADDRESS")).toHaveAttribute("type", "text"); + expect(fieldNamed("EMAIL_LOGO_URL")).toHaveAttribute("type", "text"); expect(document.querySelector('input[name="SLACK_WEBHOOK_URL"]')).toBeNull(); }); @@ -83,22 +86,22 @@ describe("EmailSettings", () => { it("only requires the SMTP host and sender email", () => { renderWithProviders(); - expect(inputNamed("SMTP_HOST")).toBeRequired(); - expect(inputNamed("SMTP_SENDER_EMAIL")).toBeRequired(); - expect(inputNamed("SMTP_PORT")).not.toBeRequired(); - expect(inputNamed("SMTP_TLS")).not.toBeRequired(); - expect(inputNamed("SMTP_USE_SSL")).not.toBeRequired(); - expect(inputNamed("SMTP_USERNAME")).not.toBeRequired(); - expect(inputNamed("SMTP_PASSWORD")).not.toBeRequired(); - expect(inputNamed("TEST_EMAIL_ADDRESS")).not.toBeRequired(); + expect(fieldNamed("SMTP_HOST")).toBeRequired(); + expect(fieldNamed("SMTP_SENDER_EMAIL")).toBeRequired(); + expect(fieldNamed("SMTP_PORT")).not.toBeRequired(); + expect(fieldNamed("SMTP_TLS")).not.toBeRequired(); + expect(fieldNamed("SMTP_USE_SSL")).not.toBeRequired(); + expect(fieldNamed("SMTP_USERNAME")).not.toBeRequired(); + expect(fieldNamed("SMTP_PASSWORD")).not.toBeRequired(); + expect(fieldNamed("TEST_EMAIL_ADDRESS")).not.toBeRequired(); }); it("submits only the fields the admin actually edited", async () => { const user = userEvent.setup(); renderWithProviders(); - await user.clear(inputNamed("SMTP_HOST")); - await user.type(inputNamed("SMTP_HOST"), "smtp.changed.com"); + await user.clear(fieldNamed("SMTP_HOST")); + await user.type(fieldNamed("SMTP_HOST"), "smtp.changed.com"); await user.click(screen.getByRole("button", { name: "Save Changes" })); await waitFor(() => { @@ -123,17 +126,33 @@ describe("EmailSettings", () => { }); }); + it("submits empty values when optional credentials are cleared", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await user.clear(fieldNamed("SMTP_USERNAME")); + await user.clear(fieldNamed("SMTP_PASSWORD")); + await user.click(screen.getByRole("button", { name: "Save Changes" })); + + await waitFor(() => { + expect(setCallbacksCall).toHaveBeenCalledWith("sk-test", { + general_settings: { alerting: ["email"] }, + environment_variables: { SMTP_USERNAME: "", SMTP_PASSWORD: "" }, + }); + }); + }); + it("disables the premium-only fields for non-premium users", () => { renderWithProviders(); - expect(inputNamed("EMAIL_LOGO_URL")).toBeDisabled(); - expect(inputNamed("SMTP_HOST")).not.toBeDisabled(); + expect(fieldNamed("EMAIL_LOGO_URL")).toBeDisabled(); + expect(fieldNamed("SMTP_HOST")).not.toBeDisabled(); }); it("leaves the premium-only fields editable for premium users", () => { renderWithProviders(); - expect(inputNamed("EMAIL_LOGO_URL")).not.toBeDisabled(); + expect(fieldNamed("EMAIL_LOGO_URL")).not.toBeDisabled(); }); it("triggers a live email health check", async () => { diff --git a/ui/litellm-dashboard/src/components/email_settings.tsx b/ui/litellm-dashboard/src/components/email_settings.tsx index 0cfac8107ad..3b5280c3544 100644 --- a/ui/litellm-dashboard/src/components/email_settings.tsx +++ b/ui/litellm-dashboard/src/components/email_settings.tsx @@ -21,6 +21,8 @@ const FIELD_TYPES: Record = { SMTP_PASSWORD: "password", }; +const BOOLEAN_FIELDS = new Set(["SMTP_TLS", "SMTP_USE_SSL"]); + const FIELD_HELP: Record = { SMTP_HOST: <>Enter the SMTP host address, e.g. `smtp.resend.com`{REQUIRED_MARKER}, SMTP_PORT: <>Enter the SMTP port number, e.g. `587`, @@ -50,18 +52,18 @@ const EmailSettings: React.FC = ({ accessToken, premiumUser, .filter((alert) => alert.name === "email") .forEach((alert) => { Object.entries(alert.variables ?? {}).forEach(([key, value]) => { - const inputElement = document.querySelector(`input[name="${key}"]`) as HTMLInputElement; - if (!inputElement || !inputElement.value) { + const fieldElement = document.querySelector(`[name="${key}"]`) as HTMLInputElement | HTMLSelectElement; + if (!fieldElement) { return; } // Only send fields the admin actually edited. Values rendered from the // server are masked (SMTP_PASSWORD) or sourced from the process // environment, so re-submitting an untouched field would persist a mask // or copy env-managed config into the database. - if (inputElement.value === (value == null ? "" : String(value))) { + if (fieldElement.value === (value == null ? "" : String(value))) { return; } - updatedVariables[key] = inputElement.value; + updatedVariables[key] = fieldElement.value; }); }); @@ -122,14 +124,27 @@ const EmailSettings: React.FC = ({ accessToken, premiumUser, ) : (

{key}

)} - + {BOOLEAN_FIELDS.has(key) ? ( + + ) : ( + + )}
{FIELD_HELP[key]}
);