From 7519a3e30b640683111e19693bb14896c1e67aff Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 3 Dec 2025 22:08:41 -0800 Subject: [PATCH] Change credentials to use react-query --- .../hooks/credentials/useCredentials.ts | 13 ++ .../ModelsAndEndpointsView.tsx | 23 +--- .../components/model_add/credentials.test.tsx | 111 ++++++++++++------ .../src/components/model_add/credentials.tsx | 31 ++--- 4 files changed, 101 insertions(+), 77 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/credentials/useCredentials.ts diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/credentials/useCredentials.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/credentials/useCredentials.ts new file mode 100644 index 00000000000..aa0a6c2c9fb --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/credentials/useCredentials.ts @@ -0,0 +1,13 @@ +import { credentialListCall, CredentialsResponse } from "@/components/networking"; +import { useQuery } from "@tanstack/react-query"; +import { createQueryKeys } from "../common/queryKeysFactory"; + +const credentialsKeys = createQueryKeys("credentials"); + +export const useCredentials = (accessToken: string | null) => { + return useQuery({ + queryKey: credentialsKeys.list({}), + queryFn: async () => await credentialListCall(accessToken!), + enabled: Boolean(accessToken), + }); +}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx index 4f33ba6585d..4cd1f549e69 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useRef } from "react"; import { Text, Grid, Col } from "@tremor/react"; import { useQueryClient } from "@tanstack/react-query"; -import { CredentialItem, credentialListCall, CredentialsResponse } from "@/components/networking"; +import { CredentialItem } from "@/components/networking"; import { handleAddModelSubmit } from "@/components/add_model/handle_add_model_submit"; @@ -23,6 +23,7 @@ import { allEndUsersCall, } from "@/components/networking"; import { useModelsInfo } from "@/app/(dashboard)/hooks/models/useModels"; +import { useCredentials } from "@/app/(dashboard)/hooks/credentials/useCredentials"; import { Form } from "antd"; import { Typography } from "antd"; import { RefreshIcon } from "@heroicons/react/outline"; @@ -134,8 +135,6 @@ const ModelsAndEndpointsView: React.FC = ({ const [allEndUsers, setAllEndUsers] = useState([]); - const [credentialsList, setCredentialsList] = useState([]); - // Model Group Alias state const [modelGroupAlias, setModelGroupAlias] = useState<{ [key: string]: string }>({}); @@ -160,21 +159,14 @@ const ModelsAndEndpointsView: React.FC = ({ isLoading: isLoadingModels, refetch: refetchModels, } = useModelsInfo(accessToken, userID, userRole); + const { data: credentialsResponse } = useCredentials(accessToken); + const credentialsList = credentialsResponse?.credentials || []; const setProviderModelsFn = (provider: Providers) => { const _providerModels = getProviderModels(provider, modelMap); setProviderModels(_providerModels); }; - const fetchCredentials = async (accessToken: string) => { - try { - const response: CredentialsResponse = await credentialListCall(accessToken); - setCredentialsList(response.credentials); - } catch (error) { - console.error("Error fetching credentials:", error); - } - }; - useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { @@ -686,12 +678,7 @@ const ModelsAndEndpointsView: React.FC = ({ /> - + ({ + default: () => mockUseAuthorized(), +})); + +vi.mock("@/app/(dashboard)/hooks/credentials/useCredentials", () => ({ + useCredentials: () => mockUseCredentials(), +})); + +const createQueryClient = () => + new QueryClient({ + defaultOptions: { + queries: { + retry: false, + gcTime: 0, + }, + }, + }); + describe("CredentialsPanel", () => { it("should render", () => { - const fetchCredentials = vi.fn(() => Promise.resolve()); + mockUseAuthorized.mockReturnValue({ accessToken: "test-token" }); + mockUseCredentials.mockReturnValue({ + data: { credentials: [] }, + refetch: vi.fn(), + }); render( - , + + + , ); expect(screen.getByRole("button", { name: /add credential/i })).toBeInTheDocument(); }); - it("should call fetchCredentials when accessToken exists", async () => { - const fetchCredentials = vi.fn(() => Promise.resolve()); - - render( - , - ); - - await waitFor(() => { - expect(fetchCredentials).toHaveBeenCalledWith("test-token"); - }); - }); - it("should display provided credentials", () => { - const fetchCredentials = vi.fn(() => Promise.resolve()); const credentials: CredentialItem[] = [ { credential_name: "openai-key", @@ -49,30 +54,58 @@ describe("CredentialsPanel", () => { }, ]; + mockUseAuthorized.mockReturnValue({ accessToken: "test-token" }); + mockUseCredentials.mockReturnValue({ + data: { credentials }, + refetch: vi.fn(), + }); + render( - , + + + , ); expect(screen.getByText("openai-key")).toBeInTheDocument(); }); it("should display empty state when no credentials are provided", () => { - const fetchCredentials = vi.fn(() => Promise.resolve()); + mockUseAuthorized.mockReturnValue({ accessToken: "test-token" }); + mockUseCredentials.mockReturnValue({ + data: { credentials: [] }, + refetch: vi.fn(), + }); render( - , + + + , ); expect(screen.getByText("No credentials configured")).toBeInTheDocument(); }); + + it("should open add modal when add button is clicked", async () => { + mockUseAuthorized.mockReturnValue({ accessToken: "test-token" }); + mockUseCredentials.mockReturnValue({ + data: { credentials: [] }, + refetch: vi.fn(), + }); + + render( + + + , + ); + + const addButton = screen.getByRole("button", { name: /add credential/i }); + + act(() => { + fireEvent.click(addButton); + }); + + await waitFor(() => { + expect(screen.getByText("Add New Credential")).toBeInTheDocument(); + }); + }); }); diff --git a/ui/litellm-dashboard/src/components/model_add/credentials.tsx b/ui/litellm-dashboard/src/components/model_add/credentials.tsx index 1bba14bef97..3887e340daa 100644 --- a/ui/litellm-dashboard/src/components/model_add/credentials.tsx +++ b/ui/litellm-dashboard/src/components/model_add/credentials.tsx @@ -19,24 +19,22 @@ import { } from "@tremor/react"; import { Form } from "antd"; import { UploadProps } from "antd/es/upload"; -import { useEffect, useState } from "react"; +import { useState } from "react"; 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 useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; interface CredentialsPanelProps { - accessToken: string | null; uploadProps: UploadProps; - credentialList: CredentialItem[]; - fetchCredentials: (accessToken: string) => Promise; } -const CredentialsPanel: React.FC = ({ - accessToken, - uploadProps, - credentialList, - fetchCredentials, -}) => { +const CredentialsPanel: React.FC = ({ uploadProps }) => { + const { accessToken } = useAuthorized(); + const { data: credentialsResponse, refetch: refetchCredentials } = useCredentials(accessToken); + const credentialList = credentialsResponse?.credentials || []; + const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isUpdateModalOpen, setIsUpdateModalOpen] = useState(false); const [selectedCredential, setSelectedCredential] = useState(null); @@ -66,7 +64,7 @@ const CredentialsPanel: React.FC = ({ await credentialUpdateCall(accessToken, values.credential_name, newCredential); NotificationsManager.success("Credential updated successfully"); setIsUpdateModalOpen(false); - await fetchCredentials(accessToken); + await refetchCredentials(); }; const handleAddCredential = async (values: any) => { @@ -90,16 +88,9 @@ const CredentialsPanel: React.FC = ({ await credentialCreateCall(accessToken, newCredential); NotificationsManager.success("Credential added successfully"); setIsAddModalOpen(false); - await fetchCredentials(accessToken); + await refetchCredentials(); }; - useEffect(() => { - if (!accessToken) { - return; - } - fetchCredentials(accessToken); - }, [accessToken]); - const renderProviderBadge = (provider: string) => { const providerColors: Record = { openai: "blue", @@ -124,7 +115,7 @@ const CredentialsPanel: React.FC = ({ try { await credentialDeleteCall(accessToken, credentialToDelete.credential_name); NotificationsManager.success("Credential deleted successfully"); - await fetchCredentials(accessToken); + await refetchCredentials(); } catch (error) { NotificationsManager.error("Failed to delete credential"); } finally {