diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/_components/default-team-settings/DefaultTeamSettingsForm.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/_components/default-team-settings/DefaultTeamSettingsForm.test.tsx index 7f9e9b4c55b..f86a760ea04 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/teams/_components/default-team-settings/DefaultTeamSettingsForm.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/_components/default-team-settings/DefaultTeamSettingsForm.test.tsx @@ -1,5 +1,5 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { render, screen, waitFor } from "@testing-library/react"; +import { act, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import React from "react"; import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -56,7 +56,7 @@ const renderForm = (overrides?: { , ); - return { fetchSettings, updateSettings }; + return { fetchSettings, updateSettings, queryClient }; }; const saveButton = async () => await screen.findByRole("button", { name: "Save Changes" }); @@ -222,6 +222,40 @@ describe("DefaultTeamSettingsForm", () => { expect(NotificationsManager.success).toHaveBeenCalledWith("Default team settings updated successfully"); }); + it("never lets a pre-save in-flight refetch restore the old values after saving", async () => { + const user = userEvent.setup(); + const staleRefetch: { resolve: (value: DefaultTeamSettings) => void } = { resolve: () => {} }; + const { updateSettings, queryClient } = renderForm({ + fetchSettings: vi + .fn() + .mockResolvedValueOnce(SETTINGS) + .mockImplementationOnce( + () => + new Promise((resolve) => { + staleRefetch.resolve = resolve; + }), + ) + .mockImplementation(() => new Promise(() => {})), + }); + + await enterEditMode(user); + await user.clear(await screen.findByLabelText("Max Budget (USD)")); + await user.type(screen.getByLabelText("Max Budget (USD)"), "250"); + void queryClient.refetchQueries({ queryKey: ["defaultTeamSettings"] }); + await user.click(await saveButton()); + + await waitFor(() => expect(updateSettings).toHaveBeenCalledTimes(1)); + expect(await screen.findByText("250")).toBeInTheDocument(); + + await act(async () => { + staleRefetch.resolve(SETTINGS); + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + + expect(screen.getByText("250")).toBeInTheDocument(); + expect(screen.queryByText("100")).not.toBeInTheDocument(); + }); + it("keeps the edit and surfaces the backend error when the save fails", async () => { const user = userEvent.setup(); const { updateSettings } = renderForm({ diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/_components/default-team-settings/DefaultTeamSettingsForm.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/_components/default-team-settings/DefaultTeamSettingsForm.tsx index 43ec838dc59..9f7d4749496 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/teams/_components/default-team-settings/DefaultTeamSettingsForm.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/_components/default-team-settings/DefaultTeamSettingsForm.tsx @@ -148,8 +148,9 @@ const SettingsForm = ({ initialValues, updateSettings, onCancel, onSaved }: Sett const mutation = useMutation({ mutationFn: (values: DefaultTeamSettingsFormValues) => updateSettings(buildBody(values)), - onSuccess: (_result, values) => { + onSuccess: async (_result, values) => { NotificationsManager.success("Default team settings updated successfully"); + await queryClient.cancelQueries({ queryKey: SETTINGS_QUERY_KEY }); queryClient.setQueryData(SETTINGS_QUERY_KEY, (existing) => ({ field_schema: existing?.field_schema ?? {}, values: buildBody(values), diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/default-user-settings/DefaultUserSettingsForm.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/default-user-settings/DefaultUserSettingsForm.test.tsx index d6563b940c3..7fd294f7586 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/default-user-settings/DefaultUserSettingsForm.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/default-user-settings/DefaultUserSettingsForm.test.tsx @@ -1,5 +1,5 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { render, screen, waitFor } from "@testing-library/react"; +import { act, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import React from "react"; import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -83,7 +83,7 @@ const renderForm = (overrides?: { , ); - return { fetchSettings, updateSettings }; + return { fetchSettings, updateSettings, queryClient }; }; const saveButton = async () => await screen.findByRole("button", { name: "Save Changes" }); @@ -273,6 +273,40 @@ describe("DefaultUserSettingsForm", () => { expect(await saveButton()).toBeDisabled(); }); + it("never lets a pre-save in-flight refetch restore the old values after saving", async () => { + const user = userEvent.setup(); + const staleRefetch: { resolve: (value: InternalUserSettings) => void } = { resolve: () => {} }; + const { updateSettings, queryClient } = renderForm({ + fetchSettings: vi + .fn() + .mockResolvedValueOnce(SETTINGS) + .mockImplementationOnce( + () => + new Promise((resolve) => { + staleRefetch.resolve = resolve; + }), + ) + .mockImplementation(() => new Promise(() => {})), + }); + + await enterEditMode(user); + await user.clear(await screen.findByLabelText("Max Budget (USD)")); + await user.type(screen.getByLabelText("Max Budget (USD)"), "250"); + void queryClient.refetchQueries({ queryKey: ["internalUserSettings"] }); + await user.click(await saveButton()); + + await waitFor(() => expect(updateSettings).toHaveBeenCalledTimes(1)); + expect(await screen.findByText("250")).toBeInTheDocument(); + + await act(async () => { + staleRefetch.resolve(SETTINGS); + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + + expect(screen.getByText("250")).toBeInTheDocument(); + expect(screen.queryByText("100")).not.toBeInTheDocument(); + }); + it("keeps the edit and surfaces the backend error when the save fails", async () => { const user = userEvent.setup(); const { updateSettings } = renderForm({ diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/default-user-settings/DefaultUserSettingsForm.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/default-user-settings/DefaultUserSettingsForm.tsx index c2fbcdb1112..dd2c85b8a18 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/default-user-settings/DefaultUserSettingsForm.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/default-user-settings/DefaultUserSettingsForm.tsx @@ -225,8 +225,9 @@ const SettingsForm = ({ initialValues, roleOptions, updateSettings, onCancel, on const mutation = useMutation({ mutationFn: (values: DefaultUserSettingsFormValues) => updateSettings(buildBody(values)), - onSuccess: (_result, values) => { + onSuccess: async (_result, values) => { NotificationsManager.success("Default user settings updated successfully"); + await queryClient.cancelQueries({ queryKey: SETTINGS_QUERY_KEY }); queryClient.setQueryData(SETTINGS_QUERY_KEY, (existing) => ({ field_schema: existing?.field_schema ?? {}, values: buildBody(values),