diff --git a/ui/litellm-dashboard/src/components/settings.test.tsx b/ui/litellm-dashboard/src/components/settings.test.tsx index 280bffd8fe9..027db2e2aca 100644 --- a/ui/litellm-dashboard/src/components/settings.test.tsx +++ b/ui/litellm-dashboard/src/components/settings.test.tsx @@ -2,6 +2,7 @@ import { act, fireEvent, render, screen, waitFor, within } from "@testing-librar import userEvent from "@testing-library/user-event"; import { FormProvider, useForm } from "react-hook-form"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { toast } from "@/lib/toast"; import { alertingSettingsCall, getCallbackConfigsCall, getCallbacksCall, setCallbacksCall } from "./networking"; import Settings, { backendCallbackLogoSrc, CallbackSelector } from "./settings"; @@ -14,6 +15,10 @@ vi.mock("./networking", () => ({ alertingSettingsCall: vi.fn().mockResolvedValue([]), })); +vi.mock("@/lib/toast", () => ({ + toast: { success: vi.fn(), fromError: vi.fn() }, +})); + vi.mock("./alerting/alerting_settings", () => ({ __esModule: true, default: () =>
Mock Alerting Settings
, @@ -513,6 +518,79 @@ describe("Settings", () => { }); }); + it("should allow alert types to be enabled when Slack alerting is not configured", async () => { + mockGetCallbacksCall.mockResolvedValue({ + callbacks: [], + available_callbacks: [], + alerts: [ + { name: "email", variables: { EMAIL_ADDRESS: null } }, + { name: "ms_teams", variables: { MS_TEAMS_WEBHOOK_URL: null } }, + ], + }); + + const user = userEvent.setup(); + render(); + await user.click(await screen.findByRole("tab", { name: "Alerting Types" })); + + const deprecationWarningsRow = screen.getByRole("row", { name: "Model Deprecation Warnings" }); + const deprecationWarningsSwitch = within(deprecationWarningsRow).getByRole("switch"); + await user.click(deprecationWarningsSwitch); + + expect(deprecationWarningsSwitch).toBeChecked(); + await user.click(screen.getByRole("button", { name: "Save Changes" })); + + await waitFor(() => { + expect(vi.mocked(setCallbacksCall)).toHaveBeenCalledWith( + "token", + expect.objectContaining({ + general_settings: expect.objectContaining({ + alert_types: ["model_deprecation_warnings"], + }), + }), + ); + }); + }); + + it("should load alert types and webhook values from the Slack callback row", async () => { + mockGetCallbacksCall.mockResolvedValue({ + callbacks: [], + available_callbacks: [], + alerts: [ + { name: "email", variables: { EMAIL_ADDRESS: null } }, + { + name: "slack", + active_alerts: ["llm_exceptions"], + variables: { SLACK_WEBHOOK_URL: null }, + alerts_to_webhook: { model_deprecation_warnings: "https://hooks.example.com/dep" }, + }, + ], + }); + + const user = userEvent.setup(); + render(); + await user.click(await screen.findByRole("tab", { name: "Alerting Types" })); + + const llmExceptionsRow = screen.getByRole("row", { name: "LLM Exceptions" }); + const deprecationWarningsRow = screen.getByRole("row", { name: "Model Deprecation Warnings" }); + expect(within(llmExceptionsRow).getByRole("switch")).toBeChecked(); + expect(within(deprecationWarningsRow).getByRole("switch")).not.toBeChecked(); + expect(screen.getByDisplayValue("https://hooks.example.com/dep")).toBeInTheDocument(); + }); + + it("should show an error toast and no success toast when saving alert types fails", async () => { + vi.mocked(setCallbacksCall).mockRejectedValueOnce(new Error("save failed")); + + const user = userEvent.setup(); + render(); + await user.click(await screen.findByRole("tab", { name: "Alerting Types" })); + await user.click(screen.getByRole("button", { name: "Save Changes" })); + + await waitFor(() => { + expect(vi.mocked(toast.fromError)).toHaveBeenCalledWith(expect.any(Error)); + }); + expect(vi.mocked(toast.success)).not.toHaveBeenCalled(); + }); + it("should hold the callbacks table in loading state until the fetch settles", async () => { let resolveCallbacks: (value: { callbacks: never[]; diff --git a/ui/litellm-dashboard/src/components/settings.tsx b/ui/litellm-dashboard/src/components/settings.tsx index 4153de19499..db6cf918b94 100644 --- a/ui/litellm-dashboard/src/components/settings.tsx +++ b/ui/litellm-dashboard/src/components/settings.tsx @@ -421,16 +421,18 @@ const Settings: React.FC = ({ accessToken, userRole, userID, setAllCallbacks(data.available_callbacks); let alerts_data = data.alerts; - if (alerts_data) { - if (alerts_data.length > 0) { - let _alert_info = alerts_data[0]; - let catch_all_webhook = _alert_info.variables.SLACK_WEBHOOK_URL; - - let active_alerts = _alert_info.active_alerts; - setActiveAlerts(active_alerts); - setCatchAllWebhookURL(catch_all_webhook); - setAlertToWebhooks(_alert_info.alerts_to_webhook); - } + const slack_alert_info = alerts_data?.find( + (alert: { + name: string; + active_alerts?: string[] | null; + variables?: { SLACK_WEBHOOK_URL?: string | null } | null; + alerts_to_webhook?: Record | null; + }) => alert.name === "slack", + ); + if (slack_alert_info) { + setActiveAlerts(slack_alert_info.active_alerts ?? []); + setCatchAllWebhookURL(slack_alert_info.variables?.SLACK_WEBHOOK_URL ?? ""); + setAlertToWebhooks(slack_alert_info.alerts_to_webhook ?? {}); } setAlerts(alerts_data); @@ -555,6 +557,7 @@ const Settings: React.FC = ({ accessToken, userRole, userID, await setCallbacksCall(accessToken, payload); } catch (error) { toast.fromError(error); + return; } toast.success(t("loggingAndAlerts.alertTypes.toasts.saved")); };