mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-02 10:21:52 +00:00
Merge pull request #17317 from BerriAI/litellm_ui_cred_refresh
[Feature] Delete Credential Enhancements
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user