mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-05 10:24:03 +00:00
Merge pull request #17397 from BerriAI/litellm_ui_cred_fix
[Fix] Show all credential values on Edit Credential Modal
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { Providers } from "../provider_info_helpers";
|
||||
import AddCredentialModal from "./AddCredentialModal";
|
||||
|
||||
vi.mock("../networking", async () => {
|
||||
const actual = await vi.importActual("../networking");
|
||||
return {
|
||||
...actual,
|
||||
getProviderCreateMetadata: vi.fn().mockResolvedValue([
|
||||
{
|
||||
provider: "OpenAI",
|
||||
provider_display_name: Providers.OpenAI,
|
||||
litellm_provider: "openai",
|
||||
default_model_placeholder: "gpt-3.5-turbo",
|
||||
credential_fields: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "OpenAI API Key",
|
||||
field_type: "password",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "api_base",
|
||||
label: "API Base",
|
||||
field_type: "text",
|
||||
placeholder: "https://api.openai.com/v1",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
provider: "Anthropic",
|
||||
provider_display_name: Providers.Anthropic,
|
||||
litellm_provider: "anthropic",
|
||||
default_model_placeholder: "claude-3-opus-20240229",
|
||||
credential_fields: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "Anthropic API Key",
|
||||
field_type: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
};
|
||||
});
|
||||
|
||||
const createQueryClient = () =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
gcTime: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mockUploadProps = {
|
||||
beforeUpload: vi.fn(),
|
||||
onChange: vi.fn(),
|
||||
};
|
||||
|
||||
describe("AddCredentialModal", () => {
|
||||
it("should render", () => {
|
||||
const queryClient = createQueryClient();
|
||||
const onCancel = vi.fn();
|
||||
const onAddCredential = vi.fn();
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AddCredentialModal
|
||||
open={true}
|
||||
onCancel={onCancel}
|
||||
onAddCredential={onAddCredential}
|
||||
uploadProps={mockUploadProps}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Add New Credential")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Credential Name:")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Provider:")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show the correct provider fields", async () => {
|
||||
const queryClient = createQueryClient();
|
||||
const onCancel = vi.fn();
|
||||
const onAddCredential = vi.fn();
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AddCredentialModal
|
||||
open={true}
|
||||
onCancel={onCancel}
|
||||
onAddCredential={onAddCredential}
|
||||
uploadProps={mockUploadProps}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText("OpenAI API Key")).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText("https://api.openai.com/v1")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
import { TextInput } from "@tremor/react";
|
||||
import { Select as AntdSelect, Button, Form, Modal, Tooltip, Typography } from "antd";
|
||||
import type { UploadProps } from "antd/es/upload";
|
||||
import React, { useState } from "react";
|
||||
import ProviderSpecificFields from "../add_model/provider_specific_fields";
|
||||
import { Providers, providerLogoMap } from "../provider_info_helpers";
|
||||
const { Link } = Typography;
|
||||
|
||||
interface AddCredentialsModalProps {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
onAddCredential: (values: any) => void;
|
||||
uploadProps: UploadProps;
|
||||
}
|
||||
|
||||
const AddCredentialsModal: React.FC<AddCredentialsModalProps> = ({ open, onCancel, onAddCredential, uploadProps }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [selectedProvider, setSelectedProvider] = useState<Providers>(Providers.OpenAI);
|
||||
|
||||
const handleSubmit = (values: any) => {
|
||||
const filteredValues = Object.entries(values).reduce((acc, [key, value]) => {
|
||||
if (value !== "" && value !== undefined && value !== null) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {} as any);
|
||||
onAddCredential(filteredValues);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Add New Credential"
|
||||
open={open}
|
||||
onCancel={() => {
|
||||
onCancel();
|
||||
form.resetFields();
|
||||
}}
|
||||
footer={null}
|
||||
width={600}
|
||||
>
|
||||
<Form form={form} onFinish={handleSubmit} layout="vertical">
|
||||
{/* Credential Name */}
|
||||
<Form.Item
|
||||
label="Credential Name:"
|
||||
name="credential_name"
|
||||
rules={[{ required: true, message: "Credential name is required" }]}
|
||||
>
|
||||
<TextInput placeholder="Enter a friendly name for these credentials" />
|
||||
</Form.Item>
|
||||
|
||||
{/* Provider Selection */}
|
||||
<Form.Item
|
||||
rules={[{ required: true, message: "Required" }]}
|
||||
label="Provider:"
|
||||
name="custom_llm_provider"
|
||||
tooltip="Helper to auto-populate provider specific fields"
|
||||
>
|
||||
<AntdSelect
|
||||
showSearch
|
||||
onChange={(value) => {
|
||||
setSelectedProvider(value as Providers);
|
||||
form.setFieldValue("custom_llm_provider", value);
|
||||
}}
|
||||
>
|
||||
{Object.entries(Providers).map(([providerEnum, providerDisplayName]) => (
|
||||
<AntdSelect.Option key={providerEnum} value={providerEnum}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<img
|
||||
src={providerLogoMap[providerDisplayName]}
|
||||
alt={`${providerEnum} logo`}
|
||||
className="w-5 h-5"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
const parent = target.parentElement;
|
||||
if (parent) {
|
||||
const fallbackDiv = document.createElement("div");
|
||||
fallbackDiv.className =
|
||||
"w-5 h-5 rounded-full bg-gray-200 flex items-center justify-center text-xs";
|
||||
fallbackDiv.textContent = providerDisplayName.charAt(0);
|
||||
parent.replaceChild(fallbackDiv, target);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span>{providerDisplayName}</span>
|
||||
</div>
|
||||
</AntdSelect.Option>
|
||||
))}
|
||||
</AntdSelect>
|
||||
</Form.Item>
|
||||
|
||||
<ProviderSpecificFields selectedProvider={selectedProvider} uploadProps={uploadProps} />
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className="flex justify-between items-center">
|
||||
<Tooltip title="Get help on our github">
|
||||
<Link href="https://github.com/BerriAI/litellm/issues">Need Help?</Link>
|
||||
</Tooltip>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onCancel();
|
||||
form.resetFields();
|
||||
}}
|
||||
style={{ marginRight: 10 }}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button htmlType="submit">{"Add Credential"}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddCredentialsModal;
|
||||
@@ -0,0 +1,123 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { Providers } from "../provider_info_helpers";
|
||||
import { CredentialItem } from "../networking";
|
||||
import EditCredentialModal from "./EditCredentialModal";
|
||||
|
||||
vi.mock("../networking", async () => {
|
||||
const actual = await vi.importActual("../networking");
|
||||
return {
|
||||
...actual,
|
||||
getProviderCreateMetadata: vi.fn().mockResolvedValue([
|
||||
{
|
||||
provider: "OpenAI",
|
||||
provider_display_name: Providers.OpenAI,
|
||||
litellm_provider: "openai",
|
||||
default_model_placeholder: "gpt-3.5-turbo",
|
||||
credential_fields: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "OpenAI API Key",
|
||||
field_type: "password",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "api_base",
|
||||
label: "API Base",
|
||||
field_type: "text",
|
||||
placeholder: "https://api.openai.com/v1",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
provider: "Anthropic",
|
||||
provider_display_name: Providers.Anthropic,
|
||||
litellm_provider: "anthropic",
|
||||
default_model_placeholder: "claude-3-opus-20240229",
|
||||
credential_fields: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "Anthropic API Key",
|
||||
field_type: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
};
|
||||
});
|
||||
|
||||
const createQueryClient = () =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
gcTime: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mockUploadProps = {
|
||||
beforeUpload: vi.fn(),
|
||||
onChange: vi.fn(),
|
||||
};
|
||||
|
||||
const mockCredential: CredentialItem = {
|
||||
credential_name: "test-credential",
|
||||
credential_values: {
|
||||
api_key: "test-api-key",
|
||||
api_base: "https://api.test.com",
|
||||
},
|
||||
credential_info: {
|
||||
custom_llm_provider: Providers.OpenAI,
|
||||
},
|
||||
};
|
||||
|
||||
describe("EditCredentialModal", () => {
|
||||
it("should render", () => {
|
||||
const queryClient = createQueryClient();
|
||||
const onCancel = vi.fn();
|
||||
const onUpdateCredential = vi.fn();
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<EditCredentialModal
|
||||
open={true}
|
||||
onCancel={onCancel}
|
||||
onUpdateCredential={onUpdateCredential}
|
||||
uploadProps={mockUploadProps}
|
||||
existingCredential={mockCredential}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Edit Credential")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Credential Name:")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Provider:")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render initial values", async () => {
|
||||
const queryClient = createQueryClient();
|
||||
const onCancel = vi.fn();
|
||||
const onUpdateCredential = vi.fn();
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<EditCredentialModal
|
||||
open={true}
|
||||
onCancel={onCancel}
|
||||
onUpdateCredential={onUpdateCredential}
|
||||
uploadProps={mockUploadProps}
|
||||
existingCredential={mockCredential}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const credentialNameInput = screen.getByLabelText("Credential Name:") as HTMLInputElement;
|
||||
expect(credentialNameInput.value).toBe("test-credential");
|
||||
expect(credentialNameInput.disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
+28
-32
@@ -1,34 +1,29 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Form, Button, Tooltip, Typography, Select as AntdSelect, Modal } from "antd";
|
||||
import type { UploadProps } from "antd/es/upload";
|
||||
import { Providers, providerLogoMap } from "../provider_info_helpers";
|
||||
import ProviderSpecificFields from "../add_model/provider_specific_fields";
|
||||
import { TextInput } from "@tremor/react";
|
||||
import { Select as AntdSelect, Button, Form, Modal, Tooltip, Typography } from "antd";
|
||||
import type { UploadProps } from "antd/es/upload";
|
||||
import { useEffect, useState } from "react";
|
||||
import ProviderSpecificFields from "../add_model/provider_specific_fields";
|
||||
import { CredentialItem } from "../networking";
|
||||
const { Title, Link } = Typography;
|
||||
import { Providers, providerLogoMap } from "../provider_info_helpers";
|
||||
const { Link } = Typography;
|
||||
|
||||
interface AddCredentialsModalProps {
|
||||
isVisible: boolean;
|
||||
interface EditCredentialsModalProps {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
onAddCredential: (values: any) => void;
|
||||
onUpdateCredential: (values: any) => void;
|
||||
uploadProps: UploadProps;
|
||||
addOrEdit: "add" | "edit";
|
||||
existingCredential: CredentialItem | null;
|
||||
}
|
||||
|
||||
const AddCredentialsModal: React.FC<AddCredentialsModalProps> = ({
|
||||
isVisible,
|
||||
export default function EditCredentialsModal({
|
||||
open,
|
||||
onCancel,
|
||||
onAddCredential,
|
||||
onUpdateCredential,
|
||||
uploadProps,
|
||||
addOrEdit,
|
||||
existingCredential,
|
||||
}) => {
|
||||
}: EditCredentialsModalProps) {
|
||||
const [form] = Form.useForm();
|
||||
const [selectedProvider, setSelectedProvider] = useState<Providers>(Providers.OpenAI);
|
||||
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
||||
const [selectedProvider, setSelectedProvider] = useState<Providers>(Providers.Anthropic);
|
||||
|
||||
const handleSubmit = (values: any) => {
|
||||
const filteredValues = Object.entries(values).reduce((acc, [key, value]) => {
|
||||
@@ -37,23 +32,25 @@ const AddCredentialsModal: React.FC<AddCredentialsModalProps> = ({
|
||||
}
|
||||
return acc;
|
||||
}, {} as any);
|
||||
if (addOrEdit === "add") {
|
||||
onAddCredential(filteredValues);
|
||||
} else {
|
||||
onUpdateCredential(filteredValues);
|
||||
}
|
||||
onUpdateCredential(filteredValues);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (existingCredential) {
|
||||
// Spread all credential_values dynamically, converting undefined/null to null for form compatibility
|
||||
const credentialValues = Object.entries(existingCredential.credential_values || {}).reduce(
|
||||
(acc, [key, value]) => {
|
||||
acc[key] = value ?? null;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
);
|
||||
|
||||
form.setFieldsValue({
|
||||
credential_name: existingCredential.credential_name,
|
||||
custom_llm_provider: existingCredential.credential_info.custom_llm_provider,
|
||||
api_base: existingCredential.credential_values.api_base,
|
||||
api_version: existingCredential.credential_values.api_version,
|
||||
base_model: existingCredential.credential_values.base_model,
|
||||
api_key: existingCredential.credential_values.api_key,
|
||||
...credentialValues,
|
||||
});
|
||||
setSelectedProvider(existingCredential.credential_info.custom_llm_provider as Providers);
|
||||
}
|
||||
@@ -61,14 +58,15 @@ const AddCredentialsModal: React.FC<AddCredentialsModalProps> = ({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={addOrEdit === "add" ? "Add New Credential" : "Edit Credential"}
|
||||
visible={isVisible}
|
||||
title="Edit Credential"
|
||||
open={open}
|
||||
onCancel={() => {
|
||||
onCancel();
|
||||
form.resetFields();
|
||||
}}
|
||||
footer={null}
|
||||
width={600}
|
||||
destroyOnHidden={true}
|
||||
>
|
||||
<Form form={form} onFinish={handleSubmit} layout="vertical">
|
||||
{/* Credential Name */}
|
||||
@@ -142,12 +140,10 @@ const AddCredentialsModal: React.FC<AddCredentialsModalProps> = ({
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button htmlType="submit">{addOrEdit === "add" ? "Add Credential" : "Update Credential"}</Button>
|
||||
<Button htmlType="submit">{"Update Credential"}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddCredentialsModal;
|
||||
}
|
||||
@@ -19,10 +19,11 @@ import {
|
||||
credentialUpdateCall,
|
||||
CredentialItem,
|
||||
} from "@/components/networking"; // Assume this is your networking function
|
||||
import AddCredentialsTab from "./add_credentials_tab";
|
||||
import AddCredentialsTab from "./AddCredentialModal";
|
||||
import CredentialDeleteModal from "./CredentialDeleteModal";
|
||||
import { Form } from "antd";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import EditCredentialsModal from "./EditCredentialModal";
|
||||
interface CredentialsPanelProps {
|
||||
accessToken: string | null;
|
||||
uploadProps: UploadProps;
|
||||
@@ -60,10 +61,10 @@ const CredentialsPanel: React.FC<CredentialsPanelProps> = ({
|
||||
},
|
||||
};
|
||||
|
||||
const response = await credentialUpdateCall(accessToken, values.credential_name, newCredential);
|
||||
await credentialUpdateCall(accessToken, values.credential_name, newCredential);
|
||||
NotificationsManager.success("Credential updated successfully");
|
||||
setIsUpdateModalOpen(false);
|
||||
fetchCredentials(accessToken);
|
||||
await fetchCredentials(accessToken);
|
||||
};
|
||||
|
||||
const handleAddCredential = async (values: any) => {
|
||||
@@ -84,10 +85,10 @@ const CredentialsPanel: React.FC<CredentialsPanelProps> = ({
|
||||
};
|
||||
|
||||
// Add to list and close modal
|
||||
const response = await credentialCreateCall(accessToken, newCredential);
|
||||
await credentialCreateCall(accessToken, newCredential);
|
||||
NotificationsManager.success("Credential added successfully");
|
||||
setIsAddModalOpen(false);
|
||||
fetchCredentials(accessToken);
|
||||
await fetchCredentials(accessToken);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -189,23 +190,18 @@ const CredentialsPanel: React.FC<CredentialsPanelProps> = ({
|
||||
{isAddModalOpen && (
|
||||
<AddCredentialsTab
|
||||
onAddCredential={handleAddCredential}
|
||||
isVisible={isAddModalOpen}
|
||||
open={isAddModalOpen}
|
||||
onCancel={() => setIsAddModalOpen(false)}
|
||||
uploadProps={uploadProps}
|
||||
addOrEdit="add"
|
||||
onUpdateCredential={handleUpdateCredential}
|
||||
existingCredential={null}
|
||||
/>
|
||||
)}
|
||||
{isUpdateModalOpen && (
|
||||
<AddCredentialsTab
|
||||
onAddCredential={handleAddCredential}
|
||||
isVisible={isUpdateModalOpen}
|
||||
<EditCredentialsModal
|
||||
open={isUpdateModalOpen}
|
||||
existingCredential={selectedCredential}
|
||||
onUpdateCredential={handleUpdateCredential}
|
||||
uploadProps={uploadProps}
|
||||
onCancel={() => setIsUpdateModalOpen(false)}
|
||||
addOrEdit="edit"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user