mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-22 00:31:44 +00:00
fix(ui): add credential on LLM Credentials tab leaves Add Model dropdown stale
After a credential was added/updated/deleted in the "LLM Credentials" tab,
ModelsAndEndpointsView's credentialsList (passed to the "Add Model" dropdown)
was never refreshed because CredentialsPanel called refetch() on its own
observer only.
Replace with queryClient.invalidateQueries({ queryKey: credentialsKeys.all })
— the established cross-component invalidation pattern used throughout the
codebase (accessGroupKeys, projectKeys, cloudZeroSettingsKeys, etc.) — which
marks the shared React Query cache entry as stale and triggers a refetch for
all active subscribers, including ModelsAndEndpointsView.
Co-Authored-By: Claude Sonnet 4 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b8f7d61400
commit
9c1c86bd48
3 changed files with 65 additions and 12 deletions
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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<typeof import("@/components/networking")>();
|
||||
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 ? (
|
||||
<button
|
||||
data-testid="mock-add-credential-submit"
|
||||
onClick={() => onAddCredential({ credential_name: "test-cred", custom_llm_provider: "openai" })}
|
||||
>
|
||||
Submit Credential
|
||||
</button>
|
||||
) : 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<typeof vi.fn>).mockResolvedValue({});
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CredentialsPanel uploadProps={DEFAULT_UPLOAD_PROPS} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
// 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"] }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<CredentialsPanelProps> = ({ 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<CredentialsPanelProps> = ({ 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<CredentialsPanelProps> = ({ 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<CredentialsPanelProps> = ({ 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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue