From f19bd2a6f14ea9c856b55f076a43854671ddada1 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 4 Nov 2026 22:33:40 +0000 Subject: [PATCH] fix(ui): keep alert save disabled while a save is in flight Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/components/settings.test.tsx | 37 +++++++++++++++++++ .../src/components/settings.tsx | 6 ++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/components/settings.test.tsx b/ui/litellm-dashboard/src/components/settings.test.tsx index 47cdd5d91ed..752d7858ef9 100644 --- a/ui/litellm-dashboard/src/components/settings.test.tsx +++ b/ui/litellm-dashboard/src/components/settings.test.tsx @@ -613,6 +613,43 @@ describe("Settings", () => { expect(savedSettings).not.toHaveProperty("alert_types"); }); + it("should save the reverted alert types when a switch is reverted while a save is in flight", async () => { + mockGetCallbacksCall.mockResolvedValue(teamsOnlyCallbacksResponse); + const finishSaves: Array<() => void> = []; + const pendingSave = () => + new Promise((resolve) => { + finishSaves.push(resolve); + }); + vi.mocked(setCallbacksCall).mockImplementationOnce(pendingSave).mockImplementationOnce(pendingSave); + + 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"); + const saveButton = screen.getByRole("button", { name: "Save Changes" }); + await user.click(deprecationWarningsSwitch); + await user.click(saveButton); + await user.click(deprecationWarningsSwitch); + await user.click(saveButton); + await act(async () => { + finishSaves.forEach((finish) => finish()); + }); + await user.click(saveButton); + + await waitFor(() => { + expect(vi.mocked(setCallbacksCall)).toHaveBeenLastCalledWith( + "token", + expect.objectContaining({ + general_settings: expect.objectContaining({ + alert_types: ["llm_exceptions", "spend_reports"], + }), + }), + ); + }); + }); + it("should load alert types and webhook values from the Slack callback row", async () => { mockGetCallbacksCall.mockResolvedValue(slackCallbacksResponseWithoutTopLevelAlertTypes); diff --git a/ui/litellm-dashboard/src/components/settings.tsx b/ui/litellm-dashboard/src/components/settings.tsx index 93f0a8bde6b..5aea93c79ca 100644 --- a/ui/litellm-dashboard/src/components/settings.tsx +++ b/ui/litellm-dashboard/src/components/settings.tsx @@ -324,6 +324,7 @@ const Settings: React.FC = ({ accessToken, userRole, userID, const [alertToWebhooks, setAlertToWebhooks] = useState>({}); const [activeAlerts, setActiveAlerts] = useState([]); const [savedAlerts, setSavedAlerts] = useState([]); + const [isSavingAlerts, setIsSavingAlerts] = useState(false); const [showAddCallbacksModal, setShowAddCallbacksModal] = useState(false); const [callbackConfigs, setCallbackConfigs] = useState([]); @@ -558,11 +559,14 @@ const Settings: React.FC = ({ accessToken, userRole, userID, }, }; + setIsSavingAlerts(true); try { await setCallbacksCall(accessToken, payload); } catch (error) { toast.fromError(error); return; + } finally { + setIsSavingAlerts(false); } setSavedAlerts(activeAlerts); toast.success(t("loggingAndAlerts.alertTypes.toasts.saved")); @@ -708,7 +712,7 @@ const Settings: React.FC = ({ accessToken, userRole, userID, ))} -