diff --git a/ui/litellm-dashboard/src/components/DefaultUserSettings.test.tsx b/ui/litellm-dashboard/src/components/DefaultUserSettings.test.tsx index a078d63d951..6fc83d79d98 100644 --- a/ui/litellm-dashboard/src/components/DefaultUserSettings.test.tsx +++ b/ui/litellm-dashboard/src/components/DefaultUserSettings.test.tsx @@ -310,6 +310,86 @@ describe("DefaultUserSettings", () => { // Should still be in edit mode (Save Changes button visible) expect(screen.getByText("Save Changes")).toBeInTheDocument(); }); + + it("should keep modal open when API save fails", async () => { + const settingsWithTeams = { + ...mockSettings, + values: { + ...mockSettings.values, + teams: [ + { team_id: "team-alpha", max_budget_in_team: 50, user_role: "user" }, + ], + }, + }; + mockGetInternalUserSettings.mockResolvedValue(settingsWithTeams); + mockUpdateInternalUserSettings.mockRejectedValue(new Error("Server error")); + + render(); + + await waitFor(() => { + expect(screen.getByText("Edit Settings")).toBeInTheDocument(); + }); + + // Enter edit mode, remove team, click Save → modal opens + act(() => { + fireEvent.click(screen.getByText("Edit Settings")); + }); + act(() => { + fireEvent.click(screen.getAllByText("Remove")[0]); + }); + act(() => { + fireEvent.click(screen.getByText("Save Changes")); + }); + + await waitFor(() => { + expect(screen.getByText("Review Changes")).toBeInTheDocument(); + }); + + // Click Confirm — API will reject + await act(async () => { + fireEvent.click(screen.getByText("Confirm Changes")); + }); + + // Modal should still be open (not cleared on failure) + await waitFor(() => { + expect(screen.getByText("Review Changes")).toBeInTheDocument(); + expect(screen.getByText("Confirm Changes")).toBeInTheDocument(); + }); + }); + + it("should reflect API response values after successful save", async () => { + mockGetInternalUserSettings.mockResolvedValue(mockSettings); + // API returns different max_budget than what was sent + mockUpdateInternalUserSettings.mockResolvedValue({ + settings: { + ...mockSettings.values, + max_budget: 2000, + }, + }); + + render(); + + await waitFor(() => { + expect(screen.getByText("Edit Settings")).toBeInTheDocument(); + }); + + act(() => { + fireEvent.click(screen.getByText("Edit Settings")); + }); + + const saveButton = screen.getByText("Save Changes"); + act(() => { + fireEvent.click(saveButton); + }); + + await waitFor(() => { + expect(mockUpdateInternalUserSettings).toHaveBeenCalled(); + }); + + // Should exit edit mode and show the API response value + expect(screen.getByText("Edit Settings")).toBeInTheDocument(); + expect(screen.getByText("2000")).toBeInTheDocument(); + }); }); // --------------------------------------------------------------------------- diff --git a/ui/litellm-dashboard/src/components/DefaultUserSettings.tsx b/ui/litellm-dashboard/src/components/DefaultUserSettings.tsx index c0dbc3ecc8f..0e07eae174a 100644 --- a/ui/litellm-dashboard/src/components/DefaultUserSettings.tsx +++ b/ui/litellm-dashboard/src/components/DefaultUserSettings.tsx @@ -167,18 +167,20 @@ const DefaultUserSettings: React.FC = ({ fetchSSOSettings(); }, [accessToken]); - /** Perform the actual API save with the given processed values. */ - const executeSave = async (processedValues: DefaultUserSettingsValues) => { - if (!accessToken) return; + /** Perform the actual API save. Returns true on success, false on failure. */ + const executeSave = async (processedValues: DefaultUserSettingsValues): Promise => { + if (!accessToken) return false; setSaving(true); try { const updatedSettings = await updateInternalUserSettings(accessToken, processedValues); setSettings({ ...settings, values: updatedSettings.settings }); setIsEditing(false); + return true; } catch (error) { console.error("Error updating SSO settings:", error); NotificationManager.fromBackend("Failed to update settings: " + error); + return false; } finally { setSaving(false); } @@ -209,13 +211,15 @@ const DefaultUserSettings: React.FC = ({ await executeSave(processedValues); }; - /** Called when user confirms changes in the modal. */ + /** Called when user confirms changes in the modal. Only clears modal on success. */ const handleConfirmSave = async () => { if (!pendingProcessedValues) return; - await executeSave(pendingProcessedValues); - setShowConfirmModal(false); - setPendingChanges([]); - setPendingProcessedValues(null); + const success = await executeSave(pendingProcessedValues); + if (success) { + setShowConfirmModal(false); + setPendingChanges([]); + setPendingProcessedValues(null); + } }; const handleTextInputChange = (key: keyof DefaultUserSettingsValues, value: DefaultUserSettingsValues[typeof key]) => {