Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions ui/litellm-dashboard/eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,57 +1,82 @@
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;

Check warning on line 16 in ui/litellm-dashboard/src/components/model_add/CredentialModal.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
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) => {

Check warning on line 46 in ui/litellm-dashboard/src/components/model_add/CredentialModal.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
const filteredValues = Object.entries(values).reduce((acc, [key, value]) => {
if (value !== "" && value !== undefined && value !== null) {
acc[key] = value;
}
return acc;
}, {} as any);

Check warning on line 52 in ui/litellm-dashboard/src/components/model_add/CredentialModal.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
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>
Comment thread
ryan-crabbe-berri marked this conversation as resolved.

{/* Provider Selection */}
<Form.Item
rules={[{ required: true, message: "Required" }]}
label="Provider:"
Expand All @@ -67,7 +92,7 @@
{Object.entries(Providers).map(([providerEnum, providerDisplayName]) => (
<AntdSelect.Option key={providerEnum} value={providerEnum}>
<div className="flex items-center space-x-2">
<img

Check warning on line 95 in ui/litellm-dashboard/src/components/model_add/CredentialModal.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
src={resolveLogoSrc(providerLogoMap[providerDisplayName])}
alt={`${providerEnum} logo`}
className="w-5 h-5"
Expand All @@ -92,28 +117,19 @@

<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;
}
Loading
Loading