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"));
};