mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): model_add/EditCredentialModal to shadcn
- antd Modal/Button/Typography/Tooltip + @tremor TextInput \u2192 shadcn Dialog/Button/Input/Tooltip. antd Form / AntdSelect Option tree preserved for provider logo rendering (same deferral as AddCredentialModal). Gates: TS \u2713 | Build \u2713. Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
5f39320211
commit
91b5776910
1 changed files with 131 additions and 97 deletions
|
|
@ -1,15 +1,28 @@
|
|||
import { TextInput } from "@tremor/react";
|
||||
import { Select as AntdSelect, Button, Form, Modal, Tooltip, Typography } from "antd";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Select as AntdSelect, Form } 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";
|
||||
import { Providers, providerLogoMap } from "../provider_info_helpers";
|
||||
const { Link } = Typography;
|
||||
|
||||
interface EditCredentialsModalProps {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onUpdateCredential: (values: any) => void;
|
||||
uploadProps: UploadProps;
|
||||
existingCredential: CredentialItem | null;
|
||||
|
|
@ -23,127 +36,148 @@ export default function EditCredentialsModal({
|
|||
existingCredential,
|
||||
}: EditCredentialsModalProps) {
|
||||
const [form] = Form.useForm();
|
||||
const [selectedProvider, setSelectedProvider] = useState<Providers>(Providers.Anthropic);
|
||||
const [selectedProvider, setSelectedProvider] = useState<Providers>(
|
||||
Providers.Anthropic,
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
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);
|
||||
const filteredValues = Object.entries(values).reduce(
|
||||
(acc, [key, value]) => {
|
||||
if (value !== "" && value !== undefined && value !== null) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
{} as any,
|
||||
);
|
||||
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(
|
||||
const credentialValues = Object.entries(
|
||||
existingCredential.credential_values || {},
|
||||
).reduce(
|
||||
(acc, [key, value]) => {
|
||||
acc[key] = value ?? null;
|
||||
return acc;
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
{} as Record<string, any>,
|
||||
);
|
||||
|
||||
form.setFieldsValue({
|
||||
credential_name: existingCredential.credential_name,
|
||||
custom_llm_provider: existingCredential.credential_info.custom_llm_provider,
|
||||
custom_llm_provider:
|
||||
existingCredential.credential_info.custom_llm_provider,
|
||||
...credentialValues,
|
||||
});
|
||||
setSelectedProvider(existingCredential.credential_info.custom_llm_provider as Providers);
|
||||
setSelectedProvider(
|
||||
existingCredential.credential_info.custom_llm_provider as Providers,
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [existingCredential]);
|
||||
|
||||
const handleCancel = () => {
|
||||
onCancel();
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Edit Credential"
|
||||
open={open}
|
||||
onCancel={() => {
|
||||
onCancel();
|
||||
form.resetFields();
|
||||
}}
|
||||
footer={null}
|
||||
width={600}
|
||||
destroyOnHidden={true}
|
||||
>
|
||||
<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" }]}
|
||||
initialValue={existingCredential?.credential_name}
|
||||
>
|
||||
<TextInput
|
||||
placeholder="Enter a friendly name for these credentials"
|
||||
disabled={existingCredential?.credential_name ? true : false}
|
||||
/>
|
||||
</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);
|
||||
}}
|
||||
<Dialog open={open} onOpenChange={(o) => (!o ? handleCancel() : undefined)}>
|
||||
<DialogContent className="max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Credential</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form form={form} onFinish={handleSubmit} layout="vertical">
|
||||
<Form.Item
|
||||
label="Credential Name:"
|
||||
name="credential_name"
|
||||
rules={[{ required: true, message: "Credential name is required" }]}
|
||||
initialValue={existingCredential?.credential_name}
|
||||
>
|
||||
{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>
|
||||
<Input
|
||||
placeholder="Enter a friendly name for these credentials"
|
||||
disabled={existingCredential?.credential_name ? true : false}
|
||||
/>
|
||||
</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();
|
||||
<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);
|
||||
}}
|
||||
style={{ marginRight: 10 }}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button htmlType="submit">{"Update Credential"}</Button>
|
||||
{Object.entries(Providers).map(
|
||||
([providerEnum, providerDisplayName]) => (
|
||||
<AntdSelect.Option key={providerEnum} value={providerEnum}>
|
||||
<div className="flex items-center space-x-2">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<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-muted 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}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<a
|
||||
href="https://github.com/BerriAI/litellm/issues"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Need Help?
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Get help on our github</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit">Update Credential</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue