From 7519a3e30b640683111e19693bb14896c1e67aff Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 3 Dec 2025 22:08:41 -0800 Subject: [PATCH 1/2] 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 0000000000..aa0a6c2c9f --- /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 4f33ba6585..4cd1f549e6 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 1bba14bef9..3887e340da 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 { From b8190c6b6dff54a499c8326e4c67976968af08a8 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 3 Dec 2025 22:15:19 -0800 Subject: [PATCH 2/2] resolving build issues --- .../ModelsAndEndpointsView.tsx | 59 +++++++++---------- .../components/templates/model_dashboard.tsx | 7 +-- 2 files changed, 29 insertions(+), 37 deletions(-) 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 4cd1f549e6..8135580e29 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,48 +1,45 @@ -import React, { useState, useEffect, useRef } from "react"; -import { Text, Grid, Col } from "@tremor/react"; import { useQueryClient } from "@tanstack/react-query"; -import { CredentialItem } from "@/components/networking"; +import { Col, Grid, Text } from "@tremor/react"; +import React, { useEffect, useRef, useState } from "react"; import { handleAddModelSubmit } from "@/components/add_model/handle_add_model_submit"; +import { useCredentials } from "@/app/(dashboard)/hooks/credentials/useCredentials"; +import { useModelsInfo } from "@/app/(dashboard)/hooks/models/useModels"; +import { Team } from "@/components/key_team_helpers/key_list"; import CredentialsPanel from "@/components/model_add/credentials"; -import { getDisplayModelName } from "@/components/view_model/model_name_display"; -import { TabPanel, TabPanels, TabGroup, TabList, Tab, Icon } from "@tremor/react"; -import { DateRangePickerValue } from "@tremor/react"; import { - modelCostMap, - modelMetricsCall, - streamingModelMetricsCall, - modelExceptionsCall, - modelMetricsSlowResponsesCall, - getCallbacksCall, - setCallbacksCall, - modelSettingsCall, adminGlobalActivityExceptions, adminGlobalActivityExceptionsPerDeployment, allEndUsersCall, + getCallbacksCall, + modelCostMap, + modelExceptionsCall, + modelMetricsCall, + modelMetricsSlowResponsesCall, + modelSettingsCall, + setCallbacksCall, + streamingModelMetricsCall, } 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"; -import type { UploadProps } from "antd"; -import { Team } from "@/components/key_team_helpers/key_list"; -import TeamInfoView from "../../../components/team/team_info"; import { Providers, getPlaceholder, getProviderModels } from "@/components/provider_info_helpers"; -import ModelInfoView from "../../../components/model_info_view"; +import { getDisplayModelName } from "@/components/view_model/model_name_display"; +import { RefreshIcon } from "@heroicons/react/outline"; +import { DateRangePickerValue, Icon, Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react"; +import type { UploadProps } from "antd"; +import { Form, Typography } from "antd"; import AddModelTab from "../../../components/add_model/add_model_tab"; +import ModelInfoView from "../../../components/model_info_view"; +import TeamInfoView from "../../../components/team/team_info"; -import HealthCheckComponent from "../../../components/model_dashboard/HealthCheckComponent"; -import PassThroughSettings from "../../../components/pass_through_settings"; -import ModelGroupAliasSettings from "../../../components/model_group_alias_settings"; -import { all_admin_roles } from "@/utils/roles"; -import NotificationsManager from "../../../components/molecules/notifications_manager"; import AllModelsTab from "@/app/(dashboard)/models-and-endpoints/components/AllModelsTab"; -import PriceDataManagementTab from "@/app/(dashboard)/models-and-endpoints/components/PriceDataManagementTab"; -import ModelRetrySettingsTab from "@/app/(dashboard)/models-and-endpoints/components/ModelRetrySettingsTab"; import ModelAnalyticsTab from "@/app/(dashboard)/models-and-endpoints/components/ModelAnalyticsTab/ModelAnalyticsTab"; +import ModelRetrySettingsTab from "@/app/(dashboard)/models-and-endpoints/components/ModelRetrySettingsTab"; +import PriceDataManagementTab from "@/app/(dashboard)/models-and-endpoints/components/PriceDataManagementTab"; +import { all_admin_roles } from "@/utils/roles"; +import HealthCheckComponent from "../../../components/model_dashboard/HealthCheckComponent"; +import ModelGroupAliasSettings from "../../../components/model_group_alias_settings"; +import NotificationsManager from "../../../components/molecules/notifications_manager"; +import PassThroughSettings from "../../../components/pass_through_settings"; interface ModelDashboardProps { accessToken: string | null; @@ -678,7 +675,7 @@ const ModelsAndEndpointsView: React.FC = ({ /> - + = ({ /> - +