From 625b2fd54930a46d4647b906157ea94566fe5d0e Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Mon, 1 Dec 2025 10:18:38 -0800 Subject: [PATCH 1/3] Await cred delete refresh + migrate to reusable delete modal --- .../components/model_add/credentials.test.tsx | 46 ++++++++-- .../src/components/model_add/credentials.tsx | 92 +++++++++++-------- 2 files changed, 92 insertions(+), 46 deletions(-) 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 8e356baa12..fab74a39d2 100644 --- a/ui/litellm-dashboard/src/components/model_add/credentials.test.tsx +++ b/ui/litellm-dashboard/src/components/model_add/credentials.test.tsx @@ -1,5 +1,5 @@ import { CredentialItem } from "@/components/networking"; -import { render, waitFor } from "@testing-library/react"; +import { render, screen, waitFor } from "@testing-library/react"; import { UploadProps } from "antd/es/upload"; import { describe, expect, it, vi } from "vitest"; import CredentialsPanel from "./credentials"; @@ -7,10 +7,25 @@ import CredentialsPanel from "./credentials"; const DEFAULT_UPLOAD_PROPS = {} as UploadProps; describe("CredentialsPanel", () => { - it("renders without crashing and fetches credentials when token exists", async () => { + it("should render", () => { const fetchCredentials = vi.fn(() => Promise.resolve()); - const { getByRole, getByText } = render( + 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(getByRole("button", { name: /add credential/i })).toBeInTheDocument(); - expect(getByText("Credential Name")).toBeInTheDocument(); - expect(getByText("Provider")).toBeInTheDocument(); + expect(fetchCredentials).toHaveBeenCalledWith("test-token"); }); }); - it("displays provided credentials and still calls the fetch helper", async () => { + it("should display provided credentials", () => { const fetchCredentials = vi.fn(() => Promise.resolve()); const credentials: CredentialItem[] = [ { @@ -36,7 +49,7 @@ describe("CredentialsPanel", () => { }, ]; - const { getByText } = render( + render( { />, ); - await waitFor(() => expect(getByText("openai-key")).toBeInTheDocument()); + expect(screen.getByText("openai-key")).toBeInTheDocument(); + }); + + it("should display empty state when no credentials are provided", () => { + const fetchCredentials = vi.fn(() => Promise.resolve()); + + render( + , + ); + + expect(screen.getByText("No credentials configured")).toBeInTheDocument(); }); }); diff --git a/ui/litellm-dashboard/src/components/model_add/credentials.tsx b/ui/litellm-dashboard/src/components/model_add/credentials.tsx index e36a759294..cf6a5cb9ee 100644 --- a/ui/litellm-dashboard/src/components/model_add/credentials.tsx +++ b/ui/litellm-dashboard/src/components/model_add/credentials.tsx @@ -1,28 +1,28 @@ -import React, { useState, useEffect } from "react"; import { + credentialCreateCall, + credentialDeleteCall, + CredentialItem, + credentialUpdateCall, +} from "@/components/networking"; // Assume this is your networking function +import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline"; +import { + Badge, + Button, + Card, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, - Card, Text, - Badge, - Button, } from "@tremor/react"; -import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline"; -import { UploadProps } from "antd/es/upload"; -import { - credentialCreateCall, - credentialDeleteCall, - credentialUpdateCall, - CredentialItem, -} from "@/components/networking"; // Assume this is your networking function -import AddCredentialsTab from "./add_credentials_tab"; -import CredentialDeleteModal from "./CredentialDeleteModal"; import { Form } from "antd"; +import { UploadProps } from "antd/es/upload"; +import React, { useEffect, useState } from "react"; +import DeleteResourceModal from "../common_components/DeleteResourceModal"; import NotificationsManager from "../molecules/notifications_manager"; +import AddCredentialsTab from "./add_credentials_tab"; interface CredentialsPanelProps { accessToken: string | null; uploadProps: UploadProps; @@ -39,7 +39,9 @@ const CredentialsPanel: React.FC = ({ const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isUpdateModalOpen, setIsUpdateModalOpen] = useState(false); const [selectedCredential, setSelectedCredential] = useState(null); - const [credentialToDelete, setCredentialToDelete] = useState(null); + const [credentialToDelete, setCredentialToDelete] = useState(null); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isCredentialDeleting, setIsCredentialDeleting] = useState(false); const [form] = Form.useForm(); const restrictedFields = ["credential_name", "custom_llm_provider"]; @@ -113,27 +115,38 @@ const CredentialsPanel: React.FC = ({ ); }; - const handleDeleteCredential = async (credentialName: string) => { - if (!accessToken) { + const handleDeleteCredential = async () => { + if (!accessToken || !credentialToDelete) { return; } - const response = await credentialDeleteCall(accessToken, credentialName); - NotificationsManager.success("Credential deleted successfully"); - setCredentialToDelete(null); - fetchCredentials(accessToken); + setIsCredentialDeleting(true); + try { + await credentialDeleteCall(accessToken, credentialToDelete.credential_name); + NotificationsManager.success("Credential deleted successfully"); + await fetchCredentials(accessToken); + } catch (error) { + NotificationsManager.error("Failed to delete credential"); + } finally { + setCredentialToDelete(null); + setIsDeleteModalOpen(false); + setIsCredentialDeleting(false); + } }; - const openDeleteModal = (credentialName: string) => { - setCredentialToDelete(credentialName); + const openDeleteModal = (credential: CredentialItem) => { + setCredentialToDelete(credential); + setIsDeleteModalOpen(true); }; const closeDeleteModal = () => { setCredentialToDelete(null); + setIsDeleteModalOpen(false); }; return ( -
-
+
+ +
Configured credentials for different AI providers. Add and manage your API credentials.
@@ -143,6 +156,7 @@ const CredentialsPanel: React.FC = ({ Credential Name Provider + Actions @@ -173,7 +187,8 @@ const CredentialsPanel: React.FC = ({ icon={TrashIcon} variant="light" size="sm" - onClick={() => openDeleteModal(credential.credential_name)} + onClick={() => openDeleteModal(credential)} + className="ml-2" /> @@ -182,9 +197,6 @@ const CredentialsPanel: React.FC = ({ - {isAddModalOpen && ( = ({ /> )} - {credentialToDelete && ( - handleDeleteCredential(credentialToDelete)} - credentialName={credentialToDelete} - /> - )} +
); }; From 8f565a811c2b74b54106c015febf5f2aa77b5656 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 2 Dec 2025 17:31:03 -0800 Subject: [PATCH 2/3] Merge with main --- ui/litellm-dashboard/src/components/model_add/credentials.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/litellm-dashboard/src/components/model_add/credentials.tsx b/ui/litellm-dashboard/src/components/model_add/credentials.tsx index 73e5ce8058..1bba14bef9 100644 --- a/ui/litellm-dashboard/src/components/model_add/credentials.tsx +++ b/ui/litellm-dashboard/src/components/model_add/credentials.tsx @@ -19,6 +19,8 @@ import { } from "@tremor/react"; import { Form } from "antd"; import { UploadProps } from "antd/es/upload"; +import { useEffect, useState } from "react"; +import DeleteResourceModal from "../common_components/DeleteResourceModal"; import NotificationsManager from "../molecules/notifications_manager"; import AddCredentialsTab from "./AddCredentialModal"; import EditCredentialsModal from "./EditCredentialModal"; From afedc9aac33fc41c7b54af5c3420384774472007 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 2 Dec 2025 17:34:53 -0800 Subject: [PATCH 3/3] Fixing test --- ui/litellm-dashboard/src/components/view_users/table.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/components/view_users/table.test.tsx b/ui/litellm-dashboard/src/components/view_users/table.test.tsx index c688d5749d..82f49d9618 100644 --- a/ui/litellm-dashboard/src/components/view_users/table.test.tsx +++ b/ui/litellm-dashboard/src/components/view_users/table.test.tsx @@ -105,7 +105,7 @@ describe("UserDataTable", () => { "Spend (USD)", "Budget (USD)", "SSO ID", - "API Keys", + "Virtual Keys", "Created At", "Updated At", "Actions",