mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
refactor(ui): consolidate Add/Edit credential modals into one CredentialModal (#32572)
* refactor(ui): consolidate Add/Edit credential modals into one CredentialModal AddCredentialModal and EditCredentialModal were ~90% identical: the same provider select, ProviderSpecificFields, and submit/filter logic, differing only in title, button text, edit-mode prefill, and the disabled credential name. Replace both with a single CredentialModal driven by a mode: 'add' | 'edit' prop, and point the two call sites in credentials.tsx at it. Removes ~120 lines of duplication and drops the no-explicit-any and no-restricted-imports baselines. The two per-file tests merge into one CredentialModal.test.tsx covering both modes (add: editable empty name; edit: prefilled, disabled name; provider fields render). * refactor(ui): derive credential name disabled state from mode, not data The disabled flag on the credential name field was tied to whether existingCredential?.credential_name is truthy, an artifact of the old EditCredentialModal. Drive it from the isEdit flag like the rest of the component so mode='add' with a stray existingCredential can't disable the field and mode='edit' with an empty name can't leave it editable. Behavior is unchanged for real call sites; adds a regression test for the edit-with- empty-name case. * refactor(ui): prefill credential form declaratively instead of via useEffect The edit-mode form was seeded with an imperative form.setFieldsValue inside a useEffect that also set React state (setSelectedProvider), an antd anti- pattern carried over from the old EditCredentialModal. Both call sites mount the modal fresh with existingCredential already present (conditional && plus destroyOnHidden), so there is no 'prop arrives after mount' case to handle. Replace it with antd's declarative initialValues on the Form and a lazy useState initializer for the provider. Removes the effect, its react-hooks/set-state-in-effect suppression and exhaustive-deps warning, and one any cast; behavior is unchanged (edit now shows the real provider on first paint instead of flashing the default). Existing tests cover prefill and the disabled name field.
This commit is contained in:
parent
703327a544
commit
6f4f4f69df
7 changed files with 191 additions and 423 deletions
|
|
@ -1885,19 +1885,11 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/model_add/AddCredentialModal.tsx": {
|
||||
"src/components/model_add/CredentialModal.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/model_add/EditCredentialModal.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/model_add/credentials.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
|
|
|
|||
|
|
@ -1,108 +0,0 @@
|
|||
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,140 @@
|
|||
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 CredentialModal from "./CredentialModal";
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
const renderModal = (props: Partial<React.ComponentProps<typeof CredentialModal>> = {}) =>
|
||||
render(
|
||||
<QueryClientProvider client={createQueryClient()}>
|
||||
<CredentialModal
|
||||
open={true}
|
||||
mode="add"
|
||||
onCancel={vi.fn()}
|
||||
onSubmit={vi.fn()}
|
||||
uploadProps={mockUploadProps}
|
||||
{...props}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
describe("CredentialModal", () => {
|
||||
describe("add mode", () => {
|
||||
it("renders the add title and an editable credential name", () => {
|
||||
renderModal({ mode: "add" });
|
||||
|
||||
expect(screen.getByText("Add New Credential")).toBeInTheDocument();
|
||||
expect(screen.getByText("Add Credential")).toBeInTheDocument();
|
||||
const nameInput = screen.getByLabelText("Credential Name:") as HTMLInputElement;
|
||||
expect(nameInput.value).toBe("");
|
||||
expect(nameInput.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it("shows provider-specific fields for the selected provider", async () => {
|
||||
renderModal({ mode: "add" });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText("OpenAI API Key")).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText("https://api.openai.com/v1")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("edit mode", () => {
|
||||
it("renders the edit title and update button", () => {
|
||||
renderModal({ mode: "edit", existingCredential: mockCredential });
|
||||
|
||||
expect(screen.getByText("Edit Credential")).toBeInTheDocument();
|
||||
expect(screen.getByText("Update Credential")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("prefills the credential name and disables it", async () => {
|
||||
renderModal({ mode: "edit", existingCredential: mockCredential });
|
||||
|
||||
await waitFor(() => {
|
||||
const nameInput = screen.getByLabelText("Credential Name:") as HTMLInputElement;
|
||||
expect(nameInput.value).toBe("test-credential");
|
||||
expect(nameInput.disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("disables the name from the mode, not the credential's name value", () => {
|
||||
renderModal({
|
||||
mode: "edit",
|
||||
existingCredential: { ...mockCredential, credential_name: "" },
|
||||
});
|
||||
|
||||
expect((screen.getByLabelText("Credential Name:") as HTMLInputElement).disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,23 +1,47 @@
|
|||
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 { useState } from "react";
|
||||
import ProviderSpecificFields from "../add_model/provider_specific_fields";
|
||||
import { CredentialItem } from "../networking";
|
||||
import { Providers, providerLogoMap } from "../provider_info_helpers";
|
||||
import { resolveLogoSrc } from "@/lib/assetPaths";
|
||||
import { resetCredentialFormOnProviderChange } from "./credential_form_helpers";
|
||||
|
||||
const { Link } = Typography;
|
||||
|
||||
interface AddCredentialsModalProps {
|
||||
interface CredentialModalProps {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
onAddCredential: (values: any) => void;
|
||||
onSubmit: (values: any) => void;
|
||||
uploadProps: UploadProps;
|
||||
mode: "add" | "edit";
|
||||
existingCredential?: CredentialItem | null;
|
||||
}
|
||||
|
||||
const AddCredentialsModal: React.FC<AddCredentialsModalProps> = ({ open, onCancel, onAddCredential, uploadProps }) => {
|
||||
export default function CredentialModal({
|
||||
open,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
uploadProps,
|
||||
mode,
|
||||
existingCredential = null,
|
||||
}: CredentialModalProps) {
|
||||
const isEdit = mode === "edit";
|
||||
const [form] = Form.useForm();
|
||||
const [selectedProvider, setSelectedProvider] = useState<Providers>(Providers.OpenAI);
|
||||
const [selectedProvider, setSelectedProvider] = useState<Providers>(
|
||||
(existingCredential?.credential_info.custom_llm_provider as Providers) ?? Providers.OpenAI,
|
||||
);
|
||||
|
||||
const initialValues = existingCredential
|
||||
? {
|
||||
credential_name: existingCredential.credential_name,
|
||||
custom_llm_provider: existingCredential.credential_info.custom_llm_provider,
|
||||
...Object.fromEntries(
|
||||
Object.entries(existingCredential.credential_values || {}).map(([key, value]) => [key, value ?? null]),
|
||||
),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const handleSubmit = (values: any) => {
|
||||
const filteredValues = Object.entries(values).reduce((acc, [key, value]) => {
|
||||
|
|
@ -26,32 +50,33 @@ const AddCredentialsModal: React.FC<AddCredentialsModalProps> = ({ open, onCance
|
|||
}
|
||||
return acc;
|
||||
}, {} as any);
|
||||
onAddCredential(filteredValues);
|
||||
onSubmit(filteredValues);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
const closeAndReset = () => {
|
||||
onCancel();
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Add New Credential"
|
||||
title={isEdit ? "Edit Credential" : "Add New Credential"}
|
||||
open={open}
|
||||
onCancel={() => {
|
||||
onCancel();
|
||||
form.resetFields();
|
||||
}}
|
||||
onCancel={closeAndReset}
|
||||
footer={null}
|
||||
width={600}
|
||||
destroyOnHidden={isEdit}
|
||||
>
|
||||
<Form form={form} onFinish={handleSubmit} layout="vertical">
|
||||
{/* Credential Name */}
|
||||
<Form form={form} onFinish={handleSubmit} layout="vertical" initialValues={initialValues}>
|
||||
<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" />
|
||||
<TextInput placeholder="Enter a friendly name for these credentials" disabled={isEdit} />
|
||||
</Form.Item>
|
||||
|
||||
{/* Provider Selection */}
|
||||
<Form.Item
|
||||
rules={[{ required: true, message: "Required" }]}
|
||||
label="Provider:"
|
||||
|
|
@ -92,28 +117,19 @@ const AddCredentialsModal: React.FC<AddCredentialsModalProps> = ({ open, onCance
|
|||
|
||||
<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 }}
|
||||
>
|
||||
<Button onClick={closeAndReset} style={{ marginRight: 10 }}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button htmlType="submit">{"Add Credential"}</Button>
|
||||
<Button htmlType="submit">{isEdit ? "Update Credential" : "Add Credential"}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddCredentialsModal;
|
||||
}
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
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";
|
||||
import { Providers, providerLogoMap } from "../provider_info_helpers";
|
||||
import { resolveLogoSrc } from "@/lib/assetPaths";
|
||||
import { resetCredentialFormOnProviderChange } from "./credential_form_helpers";
|
||||
const { Link } = Typography;
|
||||
|
||||
interface EditCredentialsModalProps {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
onUpdateCredential: (values: any) => void;
|
||||
uploadProps: UploadProps;
|
||||
existingCredential: CredentialItem | null;
|
||||
}
|
||||
|
||||
export default function EditCredentialsModal({
|
||||
open,
|
||||
onCancel,
|
||||
onUpdateCredential,
|
||||
uploadProps,
|
||||
existingCredential,
|
||||
}: EditCredentialsModalProps) {
|
||||
const [form] = Form.useForm();
|
||||
const [selectedProvider, setSelectedProvider] = useState<Providers>(Providers.Anthropic);
|
||||
|
||||
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);
|
||||
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,
|
||||
...credentialValues,
|
||||
});
|
||||
setSelectedProvider(existingCredential.credential_info.custom_llm_provider as Providers);
|
||||
}
|
||||
}, [existingCredential]);
|
||||
|
||||
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) => {
|
||||
resetCredentialFormOnProviderChange(form, value as Providers, setSelectedProvider);
|
||||
}}
|
||||
>
|
||||
{Object.entries(Providers).map(([providerEnum, providerDisplayName]) => (
|
||||
<AntdSelect.Option key={providerEnum} value={providerEnum}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<img
|
||||
src={resolveLogoSrc(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">{"Update Credential"}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -22,8 +22,7 @@ import { UploadProps } from "antd/es/upload";
|
|||
import { useState } from "react";
|
||||
import DeleteResourceModal from "../common_components/DeleteResourceModal";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import AddCredentialsTab from "./AddCredentialModal";
|
||||
import EditCredentialsModal from "./EditCredentialModal";
|
||||
import CredentialModal from "./CredentialModal";
|
||||
import { useCredentials } from "@/app/(dashboard)/hooks/credentials/useCredentials";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { isProxyAdminRole } from "@/utils/roles";
|
||||
|
|
@ -201,18 +200,20 @@ const CredentialsPanel: React.FC<CredentialsPanelProps> = ({ uploadProps }) => {
|
|||
</Card>
|
||||
|
||||
{isAddModalOpen && (
|
||||
<AddCredentialsTab
|
||||
onAddCredential={handleAddCredential}
|
||||
<CredentialModal
|
||||
mode="add"
|
||||
onSubmit={handleAddCredential}
|
||||
open={isAddModalOpen}
|
||||
onCancel={() => setIsAddModalOpen(false)}
|
||||
uploadProps={uploadProps}
|
||||
/>
|
||||
)}
|
||||
{isUpdateModalOpen && (
|
||||
<EditCredentialsModal
|
||||
<CredentialModal
|
||||
mode="edit"
|
||||
open={isUpdateModalOpen}
|
||||
existingCredential={selectedCredential}
|
||||
onUpdateCredential={handleUpdateCredential}
|
||||
onSubmit={handleUpdateCredential}
|
||||
uploadProps={uploadProps}
|
||||
onCancel={() => setIsUpdateModalOpen(false)}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue