diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/credentials/useCredentials.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/credentials/useCredentials.ts index e3266de4fbc..bdbf4445514 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/credentials/useCredentials.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/credentials/useCredentials.ts @@ -3,7 +3,7 @@ import { useQuery } from "@tanstack/react-query"; import { createQueryKeys } from "../common/queryKeysFactory"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -const credentialsKeys = createQueryKeys("credentials"); +export const credentialsKeys = createQueryKeys("credentials"); export const useCredentials = () => { const { accessToken } = useAuthorized(); diff --git a/ui/litellm-dashboard/src/components/model_add/credentials.test.tsx b/ui/litellm-dashboard/src/components/model_add/credentials.test.tsx index 2504b2a9789..79bc8b95dad 100644 --- a/ui/litellm-dashboard/src/components/model_add/credentials.test.tsx +++ b/ui/litellm-dashboard/src/components/model_add/credentials.test.tsx @@ -1,4 +1,4 @@ -import { CredentialItem } from "@/components/networking"; +import { credentialCreateCall, CredentialItem } from "@/components/networking"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { UploadProps } from "antd/es/upload"; @@ -16,6 +16,29 @@ vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ vi.mock("@/app/(dashboard)/hooks/credentials/useCredentials", () => ({ useCredentials: () => mockUseCredentials(), + credentialsKeys: { all: ["credentials"] }, +})); + +vi.mock("@/components/networking", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + credentialCreateCall: vi.fn(), + }; +}); + +// Mock the modal to expose a direct submit trigger so tests don't need to +// interact with Ant Design form internals and multi-step validation. +vi.mock("./AddCredentialModal", () => ({ + default: ({ open, onAddCredential }: any) => + open ? ( + + ) : null, })); const createQueryClient = () => @@ -33,7 +56,6 @@ describe("CredentialsPanel", () => { mockUseAuthorized.mockReturnValue({ accessToken: "test-token" }); mockUseCredentials.mockReturnValue({ data: { credentials: [] }, - refetch: vi.fn(), }); render( @@ -57,7 +79,6 @@ describe("CredentialsPanel", () => { mockUseAuthorized.mockReturnValue({ accessToken: "test-token" }); mockUseCredentials.mockReturnValue({ data: { credentials }, - refetch: vi.fn(), }); render( @@ -73,7 +94,6 @@ describe("CredentialsPanel", () => { mockUseAuthorized.mockReturnValue({ accessToken: "test-token" }); mockUseCredentials.mockReturnValue({ data: { credentials: [] }, - refetch: vi.fn(), }); render( @@ -89,7 +109,6 @@ describe("CredentialsPanel", () => { mockUseAuthorized.mockReturnValue({ accessToken: "test-token" }); mockUseCredentials.mockReturnValue({ data: { credentials: [] }, - refetch: vi.fn(), }); render( @@ -105,7 +124,39 @@ describe("CredentialsPanel", () => { }); await waitFor(() => { - expect(screen.getByText("Add New Credential")).toBeInTheDocument(); + expect(screen.getByTestId("mock-add-credential-submit")).toBeInTheDocument(); + }); + }); + + it("should invalidate shared credentials cache after adding a credential", async () => { + const queryClient = createQueryClient(); + const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries").mockResolvedValue(); + + mockUseAuthorized.mockReturnValue({ accessToken: "test-token" }); + mockUseCredentials.mockReturnValue({ data: { credentials: [] } }); + (credentialCreateCall as ReturnType).mockResolvedValue({}); + + render( + + + , + ); + + // Open modal + act(() => { + fireEvent.click(screen.getByRole("button", { name: /add credential/i })); + }); + + // Trigger submission via the mocked modal's direct submit button + await waitFor(() => screen.getByTestId("mock-add-credential-submit")); + await act(async () => { + fireEvent.click(screen.getByTestId("mock-add-credential-submit")); + }); + + await waitFor(() => { + expect(invalidateSpy).toHaveBeenCalledWith( + expect.objectContaining({ queryKey: ["credentials"] }), + ); }); }); }); diff --git a/ui/litellm-dashboard/src/components/model_add/credentials.tsx b/ui/litellm-dashboard/src/components/model_add/credentials.tsx index af3c757955e..eb96e1f0ff1 100644 --- a/ui/litellm-dashboard/src/components/model_add/credentials.tsx +++ b/ui/litellm-dashboard/src/components/model_add/credentials.tsx @@ -24,7 +24,8 @@ import DeleteResourceModal from "../common_components/DeleteResourceModal"; import NotificationsManager from "../molecules/notifications_manager"; import AddCredentialsTab from "./AddCredentialModal"; import EditCredentialsModal from "./EditCredentialModal"; -import { useCredentials } from "@/app/(dashboard)/hooks/credentials/useCredentials"; +import { credentialsKeys, useCredentials } from "@/app/(dashboard)/hooks/credentials/useCredentials"; +import { useQueryClient } from "@tanstack/react-query"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; interface CredentialsPanelProps { uploadProps: UploadProps; @@ -32,7 +33,8 @@ interface CredentialsPanelProps { const CredentialsPanel: React.FC = ({ uploadProps }) => { const { accessToken } = useAuthorized(); - const { data: credentialsResponse, refetch: refetchCredentials } = useCredentials(); + const { data: credentialsResponse } = useCredentials(); + const queryClient = useQueryClient(); const credentialList = credentialsResponse?.credentials || []; const [isAddModalOpen, setIsAddModalOpen] = useState(false); @@ -64,7 +66,7 @@ const CredentialsPanel: React.FC = ({ uploadProps }) => { await credentialUpdateCall(accessToken, values.credential_name, newCredential); NotificationsManager.success("Credential updated successfully"); setIsUpdateModalOpen(false); - await refetchCredentials(); + await queryClient.invalidateQueries({ queryKey: credentialsKeys.all }); }; const handleAddCredential = async (values: any) => { @@ -88,7 +90,7 @@ const CredentialsPanel: React.FC = ({ uploadProps }) => { await credentialCreateCall(accessToken, newCredential); NotificationsManager.success("Credential added successfully"); setIsAddModalOpen(false); - await refetchCredentials(); + await queryClient.invalidateQueries({ queryKey: credentialsKeys.all }); }; const renderProviderBadge = (provider: string) => { @@ -115,7 +117,7 @@ const CredentialsPanel: React.FC = ({ uploadProps }) => { try { await credentialDeleteCall(accessToken, credentialToDelete.credential_name); NotificationsManager.success("Credential deleted successfully"); - await refetchCredentials(); + await queryClient.invalidateQueries({ queryKey: credentialsKeys.all }); } catch (error) { NotificationsManager.error("Failed to delete credential"); } finally {