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]) => {