Merge pull request #17317 from BerriAI/litellm_ui_cred_refresh

[Feature] Delete Credential Enhancements
This commit is contained in:
yuneng-jiang
2025-12-03 12:20:55 -08:00
committed by GitHub
3 changed files with 93 additions and 47 deletions
@@ -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(
<CredentialsPanel
accessToken="test-token"
uploadProps={DEFAULT_UPLOAD_PROPS}
credentialList={[]}
fetchCredentials={fetchCredentials}
/>,
);
expect(screen.getByRole("button", { name: /add credential/i })).toBeInTheDocument();
});
it("should call fetchCredentials when accessToken exists", async () => {
const fetchCredentials = vi.fn(() => Promise.resolve());
render(
<CredentialsPanel
accessToken="test-token"
uploadProps={DEFAULT_UPLOAD_PROPS}
@@ -20,13 +35,11 @@ describe("CredentialsPanel", () => {
);
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(
<CredentialsPanel
accessToken="another-token"
uploadProps={DEFAULT_UPLOAD_PROPS}
@@ -45,6 +58,21 @@ describe("CredentialsPanel", () => {
/>,
);
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(
<CredentialsPanel
accessToken="test-token"
uploadProps={DEFAULT_UPLOAD_PROPS}
credentialList={[]}
fetchCredentials={fetchCredentials}
/>,
);
expect(screen.getByText("No credentials configured")).toBeInTheDocument();
});
});
@@ -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 "./AddCredentialModal";
import CredentialDeleteModal from "./CredentialDeleteModal";
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";
interface CredentialsPanelProps {
accessToken: string | null;
@@ -40,7 +40,9 @@ const CredentialsPanel: React.FC<CredentialsPanelProps> = ({
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [isUpdateModalOpen, setIsUpdateModalOpen] = useState(false);
const [selectedCredential, setSelectedCredential] = useState<CredentialItem | null>(null);
const [credentialToDelete, setCredentialToDelete] = useState<string | null>(null);
const [credentialToDelete, setCredentialToDelete] = useState<CredentialItem | null>(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isCredentialDeleting, setIsCredentialDeleting] = useState(false);
const [form] = Form.useForm();
const restrictedFields = ["credential_name", "custom_llm_provider"];
@@ -114,27 +116,38 @@ const CredentialsPanel: React.FC<CredentialsPanelProps> = ({
);
};
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 (
<div className="w-full mx-auto flex-auto overflow-y-auto m-8 p-2">
<div className="flex justify-between items-center mb-4">
<div className="w-full mx-auto flex-auto overflow-y-auto p-2">
<Button onClick={() => setIsAddModalOpen(true)}>Add Credential</Button>
<div className="flex justify-between items-center mt-4 mb-4">
<Text>Configured credentials for different AI providers. Add and manage your API credentials.</Text>
</div>
@@ -144,6 +157,7 @@ const CredentialsPanel: React.FC<CredentialsPanelProps> = ({
<TableRow>
<TableHeaderCell>Credential Name</TableHeaderCell>
<TableHeaderCell>Provider</TableHeaderCell>
<TableHeaderCell>Actions</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
@@ -174,7 +188,8 @@ const CredentialsPanel: React.FC<CredentialsPanelProps> = ({
icon={TrashIcon}
variant="light"
size="sm"
onClick={() => openDeleteModal(credential.credential_name)}
onClick={() => openDeleteModal(credential)}
className="ml-2"
/>
</TableCell>
</TableRow>
@@ -183,9 +198,6 @@ const CredentialsPanel: React.FC<CredentialsPanelProps> = ({
</TableBody>
</Table>
</Card>
<Button onClick={() => setIsAddModalOpen(true)} className="mt-4">
Add Credential
</Button>
{isAddModalOpen && (
<AddCredentialsTab
@@ -205,14 +217,20 @@ const CredentialsPanel: React.FC<CredentialsPanelProps> = ({
/>
)}
{credentialToDelete && (
<CredentialDeleteModal
isVisible={true}
onCancel={closeDeleteModal}
onConfirm={() => handleDeleteCredential(credentialToDelete)}
credentialName={credentialToDelete}
/>
)}
<DeleteResourceModal
isOpen={isDeleteModalOpen}
onCancel={closeDeleteModal}
onOk={handleDeleteCredential}
title="Delete Credential?"
message="Are you sure you want to delete this credential? This action cannot be undone and may break existing integrations."
resourceInformationTitle="Credential Information"
resourceInformation={[
{ label: "Credential Name", value: credentialToDelete?.credential_name },
{ label: "Provider", value: credentialToDelete?.credential_info?.custom_llm_provider || "-" },
]}
confirmLoading={isCredentialDeleting}
requiredConfirmation={credentialToDelete?.credential_name}
/>
</div>
);
};
@@ -105,7 +105,7 @@ describe("UserDataTable", () => {
"Spend (USD)",
"Budget (USD)",
"SSO ID",
"API Keys",
"Virtual Keys",
"Created At",
"Updated At",
"Actions",