feat(ui): migrate model_add + model_dashboard to shadcn

- AddCredentialModal / EditCredentialModal / reuse_credentials: antd Form -> RHF
- credentials.tsx: Tremor Table -> shadcn Table, drop antd Form.useForm
- HealthCheckComponent: Tremor Title/Text/Button/Badge + antd Modal -> shadcn Dialog/Button/Badge
- ModelSettingsModal: antd Form.useForm + Form.Item -> RHF + Controller
- provider_specific_fields: restructure label so asterisk is outside <Label>
  so getByLabelText still matches the raw field label
- eslintrc override: add model_dashboard/HealthCheckComponent + model_info_view
  (for the categorical palette and existing raw tailwind colors)

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-24 11:23:53 +00:00
parent 298489075a
commit b34f9a118e
No known key found for this signature in database
9 changed files with 573 additions and 349 deletions

View file

@ -70,6 +70,8 @@
"src/components/mcp_tools/MCPPermissionManagement.tsx",
"src/components/model_add/credentials.tsx",
"src/components/model_dashboard/health_check_columns.tsx",
"src/components/model_dashboard/HealthCheckComponent.tsx",
"src/components/model_info_view.tsx",
"src/components/UsagePage/components/EntityUsage/TopKeyView.tsx",
"src/components/policies/policy_templates.tsx",
"src/components/mcp_tools/mcp_server_view.tsx",

View file

@ -237,16 +237,19 @@ const ProviderSpecificFields: React.FC<ProviderSpecificFieldsProps> = ({
field.key === "vertex_credentials" ? "mb-0" : ""
}`}
>
<Label
className="col-span-10 pt-2"
title={field.tooltip}
htmlFor={`provider-field-${field.key}`}
>
{field.label}
<div className="col-span-10 pt-2 flex items-start">
<Label
title={field.tooltip}
htmlFor={`provider-field-${field.key}`}
>
{field.label}
</Label>
{field.required && (
<span className="text-destructive ml-1">*</span>
<span aria-hidden="true" className="text-destructive ml-1">
*
</span>
)}
</Label>
</div>
<div className="col-span-14">
{field.type === "select" ? (
<Controller

View file

@ -6,18 +6,23 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
Select as AntdSelect,
Form,
} from "antd";
import type { UploadProps } from "antd/es/upload";
import React, { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import ProviderSpecificFields from "../add_model/provider_specific_fields";
import { Providers, providerLogoMap } from "../provider_info_helpers";
@ -29,19 +34,31 @@ interface AddCredentialsModalProps {
uploadProps: UploadProps;
}
type AddCredentialFormValues = {
credential_name: string;
custom_llm_provider: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
};
const AddCredentialsModal: React.FC<AddCredentialsModalProps> = ({
open,
onCancel,
onAddCredential,
uploadProps,
}) => {
const [form] = Form.useForm();
const form = useForm<AddCredentialFormValues>({
defaultValues: {
credential_name: "",
custom_llm_provider: "",
},
mode: "onSubmit",
});
const [selectedProvider, setSelectedProvider] = useState<Providers>(
Providers.OpenAI,
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleSubmit = (values: any) => {
const handleSubmit = form.handleSubmit((values) => {
const filteredValues = Object.entries(values).reduce(
(acc, [key, value]) => {
if (value !== "" && value !== undefined && value !== null) {
@ -53,12 +70,12 @@ const AddCredentialsModal: React.FC<AddCredentialsModalProps> = ({
{} as any,
);
onAddCredential(filteredValues);
form.resetFields();
};
form.reset({ credential_name: "", custom_llm_provider: "" });
});
const handleCancel = () => {
onCancel();
form.resetFields();
form.reset({ credential_name: "", custom_llm_provider: "" });
};
return (
@ -67,86 +84,127 @@ const AddCredentialsModal: React.FC<AddCredentialsModalProps> = ({
<DialogHeader>
<DialogTitle>Add New 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" }]}
>
<Input placeholder="Enter a friendly name for these credentials" />
</Form.Item>
<FormProvider {...form}>
<form onSubmit={handleSubmit}>
<div className="space-y-4 mb-4">
<div className="space-y-2">
<div className="flex items-center">
<Label htmlFor="credential_name">Credential Name:</Label>
<span aria-hidden="true" className="text-destructive ml-1">
*
</span>
</div>
<Input
id="credential_name"
placeholder="Enter a friendly name for these credentials"
{...form.register("credential_name", {
required: "Credential name is required",
})}
/>
{form.formState.errors.credential_name && (
<p className="text-sm text-destructive">
{String(form.formState.errors.credential_name.message)}
</p>
)}
</div>
<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">
{/* 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"
<div className="space-y-2">
<div className="flex items-center">
<Label
htmlFor="custom_llm_provider"
title="Helper to auto-populate provider specific fields"
>
Need Help?
</a>
</TooltipTrigger>
<TooltipContent>Get help on our github</TooltipContent>
</Tooltip>
</TooltipProvider>
Provider:
</Label>
<span aria-hidden="true" className="text-destructive ml-1">
*
</span>
</div>
<Controller
control={form.control}
name="custom_llm_provider"
rules={{ required: "Required" }}
render={({ field }) => (
<Select
value={field.value || ""}
onValueChange={(v) => {
field.onChange(v);
setSelectedProvider(v as Providers);
}}
>
<SelectTrigger id="custom_llm_provider">
<SelectValue placeholder="Select a provider" />
</SelectTrigger>
<SelectContent>
{Object.entries(Providers).map(
([providerEnum, providerDisplayName]) => (
<SelectItem 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>
</SelectItem>
),
)}
</SelectContent>
</Select>
)}
/>
{form.formState.errors.custom_llm_provider && (
<p className="text-sm text-destructive">
{String(form.formState.errors.custom_llm_provider.message)}
</p>
)}
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button type="submit">Add Credential</Button>
<ProviderSpecificFields
selectedProvider={selectedProvider}
uploadProps={uploadProps}
/>
</div>
</div>
</Form>
<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" type="button" onClick={handleCancel}>
Cancel
</Button>
<Button type="submit">Add Credential</Button>
</div>
</div>
</form>
</FormProvider>
</DialogContent>
</Dialog>
);

View file

@ -6,15 +6,23 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
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 { Controller, FormProvider, useForm } from "react-hook-form";
import ProviderSpecificFields from "../add_model/provider_specific_fields";
import { CredentialItem } from "../networking";
import { Providers, providerLogoMap } from "../provider_info_helpers";
@ -28,6 +36,13 @@ interface EditCredentialsModalProps {
existingCredential: CredentialItem | null;
}
type EditCredentialFormValues = {
credential_name: string;
custom_llm_provider: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
};
export default function EditCredentialsModal({
open,
onCancel,
@ -35,13 +50,18 @@ export default function EditCredentialsModal({
uploadProps,
existingCredential,
}: EditCredentialsModalProps) {
const [form] = Form.useForm();
const form = useForm<EditCredentialFormValues>({
defaultValues: {
credential_name: existingCredential?.credential_name ?? "",
custom_llm_provider: "",
},
mode: "onSubmit",
});
const [selectedProvider, setSelectedProvider] = useState<Providers>(
Providers.Anthropic,
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleSubmit = (values: any) => {
const handleSubmit = form.handleSubmit((values) => {
const filteredValues = Object.entries(values).reduce(
(acc, [key, value]) => {
if (value !== "" && value !== undefined && value !== null) {
@ -53,8 +73,8 @@ export default function EditCredentialsModal({
{} as any,
);
onUpdateCredential(filteredValues);
form.resetFields();
};
form.reset({ credential_name: "", custom_llm_provider: "" });
});
useEffect(() => {
if (existingCredential) {
@ -69,7 +89,7 @@ export default function EditCredentialsModal({
{} as Record<string, any>,
);
form.setFieldsValue({
form.reset({
credential_name: existingCredential.credential_name,
custom_llm_provider:
existingCredential.credential_info.custom_llm_provider,
@ -84,99 +104,139 @@ export default function EditCredentialsModal({
const handleCancel = () => {
onCancel();
form.resetFields();
form.reset({ credential_name: "", custom_llm_provider: "" });
};
const isNameLocked = Boolean(existingCredential?.credential_name);
return (
<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}
>
<Input
placeholder="Enter a friendly name for these credentials"
disabled={existingCredential?.credential_name ? true : false}
/>
</Form.Item>
<FormProvider {...form}>
<form onSubmit={handleSubmit}>
<div className="space-y-4 mb-4">
<div className="space-y-2">
<div className="flex items-center">
<Label htmlFor="credential_name">Credential Name:</Label>
<span aria-hidden="true" className="text-destructive ml-1">
*
</span>
</div>
<Input
id="credential_name"
placeholder="Enter a friendly name for these credentials"
disabled={isNameLocked}
{...form.register("credential_name", {
required: "Credential name is required",
})}
/>
{form.formState.errors.credential_name && (
<p className="text-sm text-destructive">
{String(form.formState.errors.credential_name.message)}
</p>
)}
</div>
<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">
{/* 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"
<div className="space-y-2">
<div className="flex items-center">
<Label
htmlFor="custom_llm_provider"
title="Helper to auto-populate provider specific fields"
>
Need Help?
</a>
</TooltipTrigger>
<TooltipContent>Get help on our github</TooltipContent>
</Tooltip>
</TooltipProvider>
Provider:
</Label>
<span aria-hidden="true" className="text-destructive ml-1">
*
</span>
</div>
<Controller
control={form.control}
name="custom_llm_provider"
rules={{ required: "Required" }}
render={({ field }) => (
<Select
value={field.value || ""}
onValueChange={(v) => {
field.onChange(v);
setSelectedProvider(v as Providers);
}}
>
<SelectTrigger id="custom_llm_provider">
<SelectValue placeholder="Select a provider" />
</SelectTrigger>
<SelectContent>
{Object.entries(Providers).map(
([providerEnum, providerDisplayName]) => (
<SelectItem 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>
</SelectItem>
),
)}
</SelectContent>
</Select>
)}
/>
{form.formState.errors.custom_llm_provider && (
<p className="text-sm text-destructive">
{String(form.formState.errors.custom_llm_provider.message)}
</p>
)}
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button type="submit">Update Credential</Button>
<ProviderSpecificFields
selectedProvider={selectedProvider}
uploadProps={uploadProps}
/>
</div>
</div>
</Form>
<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" type="button" onClick={handleCancel}>
Cancel
</Button>
<Button type="submit">Update Credential</Button>
</div>
</div>
</form>
</FormProvider>
</DialogContent>
</Dialog>
);

View file

@ -5,20 +5,18 @@ import {
credentialUpdateCall,
} from "@/components/networking";
import { Pencil, Trash2 } from "lucide-react";
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableHeader,
TableRow,
} from "@tremor/react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
} from "@/components/ui/table";
import { cn } from "@/lib/utils";
import { Form } from "antd";
import { UploadProps } from "antd/es/upload";
import { useState } from "react";
import DeleteResourceModal from "../common_components/DeleteResourceModal";
@ -33,8 +31,7 @@ interface CredentialsPanelProps {
}
const PROVIDER_BADGE_CLASSES: Record<string, string> = {
openai:
"bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300",
openai: "bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300",
azure:
"bg-indigo-100 text-indigo-700 dark:bg-indigo-950 dark:text-indigo-300",
anthropic:
@ -56,8 +53,6 @@ const CredentialsPanel: React.FC<CredentialsPanelProps> = ({ uploadProps }) => {
useState<CredentialItem | null>(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isCredentialDeleting, setIsCredentialDeleting] = useState(false);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_form] = Form.useForm();
const restrictedFields = ["credential_name", "custom_llm_provider"];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -154,25 +149,25 @@ const CredentialsPanel: React.FC<CredentialsPanelProps> = ({ uploadProps }) => {
<Button onClick={() => setIsAddModalOpen(true)}>Add Credential</Button>
<div className="flex justify-between items-center mt-4 mb-4">
<p className="text-muted-foreground">
Configured credentials for different AI providers. Add and manage
your API credentials.
Configured credentials for different AI providers. Add and manage your
API credentials.
</p>
</div>
<Card>
<Table>
<TableHead>
<TableHeader>
<TableRow>
<TableHeaderCell>Credential Name</TableHeaderCell>
<TableHeaderCell>Provider</TableHeaderCell>
<TableHeaderCell>Actions</TableHeaderCell>
<TableHead>Credential Name</TableHead>
<TableHead>Provider</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHead>
</TableHeader>
<TableBody>
{!credentialList || credentialList.length === 0 ? (
<TableRow>
<TableCell
colSpan={4}
colSpan={3}
className="text-center py-4 text-muted-foreground"
>
No credentials configured

View file

@ -1,5 +1,5 @@
import React from "react";
import { Form } from "antd";
import React, { useEffect } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { Button } from "@/components/ui/button";
import {
Dialog,
@ -8,6 +8,7 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { CredentialItem } from "../networking";
interface ReuseCredentialsModalProps {
@ -19,6 +20,12 @@ interface ReuseCredentialsModalProps {
setIsCredentialModalOpen: (isVisible: boolean) => void;
}
type ReuseCredentialFormValues = {
credential_name: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
};
const ReuseCredentialsModal: React.FC<ReuseCredentialsModalProps> = ({
isVisible,
onCancel,
@ -26,13 +33,46 @@ const ReuseCredentialsModal: React.FC<ReuseCredentialsModalProps> = ({
existingCredential,
setIsCredentialModalOpen,
}) => {
const [form] = Form.useForm();
const credentialValueEntries = Object.entries(
existingCredential?.credential_values || {},
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleSubmit = (values: any) => {
const form = useForm<ReuseCredentialFormValues>({
defaultValues: {
credential_name: existingCredential?.credential_name ?? "",
...credentialValueEntries.reduce(
(acc, [key, value]) => {
acc[key] = (value as string | number | null | undefined) ?? "";
return acc;
},
{} as Record<string, string | number | null | undefined>,
),
},
});
useEffect(() => {
form.reset({
credential_name: existingCredential?.credential_name ?? "",
...credentialValueEntries.reduce(
(acc, [key, value]) => {
acc[key] = (value as string | number | null | undefined) ?? "";
return acc;
},
{} as Record<string, string | number | null | undefined>,
),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [existingCredential]);
const handleSubmit = form.handleSubmit((values) => {
onAddCredential(values);
form.resetFields();
form.reset();
setIsCredentialModalOpen(false);
});
const handleCancel = () => {
onCancel();
form.reset();
};
return (
@ -40,8 +80,7 @@ const ReuseCredentialsModal: React.FC<ReuseCredentialsModalProps> = ({
open={isVisible}
onOpenChange={(o) => {
if (!o) {
onCancel();
form.resetFields();
handleCancel();
}
}}
>
@ -49,54 +88,63 @@ const ReuseCredentialsModal: React.FC<ReuseCredentialsModalProps> = ({
<DialogHeader>
<DialogTitle>Reuse Credentials</DialogTitle>
</DialogHeader>
<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}
>
<Input placeholder="Enter a friendly name for these credentials" />
</Form.Item>
<FormProvider {...form}>
<form onSubmit={handleSubmit}>
<div className="space-y-4 mb-4">
<div className="space-y-2">
<div className="flex items-center">
<Label htmlFor="credential_name">Credential Name:</Label>
<span aria-hidden="true" className="text-destructive ml-1">
*
</span>
</div>
<Input
id="credential_name"
placeholder="Enter a friendly name for these credentials"
{...form.register("credential_name", {
required: "Credential name is required",
})}
/>
{form.formState.errors.credential_name && (
<p className="text-sm text-destructive">
{String(form.formState.errors.credential_name.message)}
</p>
)}
</div>
{/* Display Credential Values of existingCredential, don't allow user to edit. */}
{Object.entries(existingCredential?.credential_values || {}).map(
([key, value]) => (
<Form.Item key={key} label={key} name={key} initialValue={value}>
<Input placeholder={`Enter ${key}`} disabled={true} />
</Form.Item>
),
)}
<div className="flex justify-between items-center">
<a
href="https://github.com/BerriAI/litellm/issues"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:text-primary/80 text-sm"
title="Get help on our github"
>
Need Help?
</a>
<div className="flex gap-2">
<Button
type="button"
variant="outline"
onClick={() => {
onCancel();
form.resetFields();
}}
>
Cancel
</Button>
<Button type="submit">Reuse Credentials</Button>
{credentialValueEntries.map(([key]) => (
<div key={key} className="space-y-2">
<Label htmlFor={`reuse-${key}`}>{key}</Label>
<Input
id={`reuse-${key}`}
placeholder={`Enter ${key}`}
disabled
{...form.register(key)}
/>
</div>
))}
</div>
</div>
</Form>
<div className="flex justify-between items-center">
<a
href="https://github.com/BerriAI/litellm/issues"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:text-primary/80 text-sm"
title="Get help on our github"
>
Need Help?
</a>
<div className="flex gap-2">
<Button type="button" variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button type="submit">Reuse Credentials</Button>
</div>
</div>
</form>
</FormProvider>
</DialogContent>
</Dialog>
);

View file

@ -1,8 +1,13 @@
import React, { useState, useEffect, useRef } from "react";
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
import { Title, Text, Button, Badge } from "@tremor/react";
import { Modal } from "antd";
import { Button as AntdButton } from "antd";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { ModelDataTable } from "./table";
import { healthCheckColumns } from "./health_check_columns";
import { errorPatterns } from "@/utils/errorPatterns";
@ -452,15 +457,27 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
const getStatusBadge = (status: string) => {
switch (status) {
case "healthy":
return <Badge color="emerald">healthy</Badge>;
return (
<Badge className="bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300 hover:bg-emerald-100">
healthy
</Badge>
);
case "unhealthy":
return <Badge color="red">unhealthy</Badge>;
return (
<Badge className="bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-300 hover:bg-red-100">
unhealthy
</Badge>
);
case "checking":
return <Badge color="blue">checking</Badge>;
return (
<Badge className="bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300 hover:bg-blue-100">
checking
</Badge>
);
case "none":
return <Badge color="gray">none</Badge>;
return <Badge variant="secondary">none</Badge>;
default:
return <Badge color="gray">unknown</Badge>;
return <Badge variant="secondary">unknown</Badge>;
}
};
@ -496,14 +513,19 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
<div className="mb-6">
<div className="flex justify-between items-center">
<div>
<Title>Model Health Status</Title>
<Text className="text-gray-600 mt-1">
<h2 className="text-xl font-semibold m-0">Model Health Status</h2>
<p className="text-muted-foreground text-sm mt-1">
Run health checks on individual models to verify they are working correctly
</Text>
</p>
</div>
<div className="flex items-center gap-3">
{selectedModelsForHealth.length > 0 && (
<Button size="sm" variant="light" onClick={() => handleSelectAll(false)} className="px-3 py-1 text-sm">
<Button
size="sm"
variant="ghost"
onClick={() => handleSelectAll(false)}
className="px-3 py-1 text-sm"
>
Clear Selection
</Button>
)}
@ -564,70 +586,88 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
</div>
{/* Error Modal */}
<Modal
title={selectedErrorDetails ? `Health Check Error - ${selectedErrorDetails.modelName}` : "Error Details"}
<Dialog
open={errorModalVisible}
onCancel={closeErrorModal}
footer={[
<AntdButton key="close" onClick={closeErrorModal}>
Close
</AntdButton>,
]}
width={800}
onOpenChange={(o) => (!o ? closeErrorModal() : undefined)}
>
{selectedErrorDetails && (
<div className="space-y-4">
<div>
<Text className="font-medium">Error:</Text>
<div className="mt-2 p-3 bg-red-50 border border-red-200 rounded-md">
<Text className="text-red-800">{selectedErrorDetails.cleanedError}</Text>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>
{selectedErrorDetails
? `Health Check Error - ${selectedErrorDetails.modelName}`
: "Error Details"}
</DialogTitle>
</DialogHeader>
{selectedErrorDetails && (
<div className="space-y-4">
<div>
<p className="font-medium">Error:</p>
<div className="mt-2 p-3 bg-red-50 border border-red-200 rounded-md dark:bg-red-950 dark:border-red-900">
<p className="text-red-800 dark:text-red-300">
{selectedErrorDetails.cleanedError}
</p>
</div>
</div>
</div>
<div>
<Text className="font-medium">Full Error Details:</Text>
<div className="mt-2 p-3 bg-gray-50 border border-gray-200 rounded-md max-h-96 overflow-y-auto">
<pre className="text-sm text-gray-800 whitespace-pre-wrap">{selectedErrorDetails.fullError}</pre>
<div>
<p className="font-medium">Full Error Details:</p>
<div className="mt-2 p-3 bg-muted border border-border rounded-md max-h-96 overflow-y-auto">
<pre className="text-sm text-foreground whitespace-pre-wrap">
{selectedErrorDetails.fullError}
</pre>
</div>
</div>
</div>
</div>
)}
</Modal>
)}
<DialogFooter>
<Button variant="outline" onClick={closeErrorModal}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Success Modal */}
<Modal
title={
selectedSuccessDetails ? `Health Check Response - ${selectedSuccessDetails.modelName}` : "Response Details"
}
<Dialog
open={successModalVisible}
onCancel={closeSuccessModal}
footer={[
<AntdButton key="close" onClick={closeSuccessModal}>
Close
</AntdButton>,
]}
width={800}
onOpenChange={(o) => (!o ? closeSuccessModal() : undefined)}
>
{selectedSuccessDetails && (
<div className="space-y-4">
<div>
<Text className="font-medium">Status:</Text>
<div className="mt-2 p-3 bg-green-50 border border-green-200 rounded-md">
<Text className="text-green-800">Health check passed successfully</Text>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>
{selectedSuccessDetails
? `Health Check Response - ${selectedSuccessDetails.modelName}`
: "Response Details"}
</DialogTitle>
</DialogHeader>
{selectedSuccessDetails && (
<div className="space-y-4">
<div>
<p className="font-medium">Status:</p>
<div className="mt-2 p-3 bg-green-50 border border-green-200 rounded-md dark:bg-green-950 dark:border-green-900">
<p className="text-green-800 dark:text-green-300">
Health check passed successfully
</p>
</div>
</div>
</div>
<div>
<Text className="font-medium">Response Details:</Text>
<div className="mt-2 p-3 bg-gray-50 border border-gray-200 rounded-md max-h-96 overflow-y-auto">
<pre className="text-sm text-gray-800 whitespace-pre-wrap">
{JSON.stringify(selectedSuccessDetails.response, null, 2)}
</pre>
<div>
<p className="font-medium">Response Details:</p>
<div className="mt-2 p-3 bg-muted border border-border rounded-md max-h-96 overflow-y-auto">
<pre className="text-sm text-foreground whitespace-pre-wrap">
{JSON.stringify(selectedSuccessDetails.response, null, 2)}
</pre>
</div>
</div>
</div>
</div>
)}
</Modal>
)}
<DialogFooter>
<Button variant="outline" onClick={closeSuccessModal}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
};

View file

@ -237,7 +237,7 @@ describe("ModelSettingsModal", () => {
const saveButton = screen.getByRole("button", { name: /Saving/i });
expect(saveButton).toBeInTheDocument();
expect(saveButton.className).toContain("ant-btn-loading");
expect(saveButton).toBeDisabled();
});
it("should not render modal when isVisible is false", () => {
@ -282,7 +282,9 @@ describe("ModelSettingsModal", () => {
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
expect(screen.queryByRole("switch")).not.toBeInTheDocument();
const skeletons = document.querySelectorAll(".ant-skeleton");
// shadcn Skeleton (rendered inside the Dialog portal) has the
// `animate-pulse` utility class.
const skeletons = document.body.querySelectorAll(".animate-pulse");
expect(skeletons.length).toBeGreaterThan(0);
});

View file

@ -18,9 +18,11 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { Skeleton } from "@/components/ui/skeleton";
import { Form, Switch as AntdFormSwitch } from "antd";
import { Switch } from "@/components/ui/switch";
import React, { useEffect, useMemo } from "react";
import { Controller, useForm } from "react-hook-form";
interface ModelSettingsModalProps {
isVisible: boolean;
@ -28,12 +30,15 @@ interface ModelSettingsModalProps {
onSuccess?: () => void;
}
type ModelSettingsFormValues = {
store_model_in_db: boolean;
};
const ModelSettingsModal: React.FC<ModelSettingsModalProps> = ({
isVisible,
onCancel,
onSuccess,
}) => {
const [form] = Form.useForm();
const { mutateAsync, isPending } = useStoreModelInDB();
const {
data: proxyConfigData,
@ -47,11 +52,9 @@ const ModelSettingsModal: React.FC<ModelSettingsModalProps> = ({
}
}, [isVisible, refetch]);
const initialValues = useMemo(() => {
const initialValues = useMemo<ModelSettingsFormValues>(() => {
if (!proxyConfigData) {
return {
store_model_in_db: false,
};
return { store_model_in_db: false };
}
const storeModelField = proxyConfigData.find(
@ -59,13 +62,23 @@ const ModelSettingsModal: React.FC<ModelSettingsModalProps> = ({
);
return {
store_model_in_db: storeModelField?.field_value ?? false,
store_model_in_db: Boolean(storeModelField?.field_value ?? false),
};
}, [proxyConfigData]);
const handleFormSubmit = async (formValues: StoreModelInDBParams) => {
const form = useForm<ModelSettingsFormValues>({
defaultValues: initialValues,
});
// Reset form when initial values change (e.g. fresh fetch).
useEffect(() => {
form.reset(initialValues);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialValues]);
const handleFormSubmit = form.handleSubmit(async (formValues) => {
try {
await mutateAsync(formValues, {
await mutateAsync(formValues as StoreModelInDBParams, {
onSuccess: () => {
NotificationsManager.success(
"Model storage settings updated successfully",
@ -75,7 +88,8 @@ const ModelSettingsModal: React.FC<ModelSettingsModalProps> = ({
},
onError: (error) => {
NotificationsManager.fromBackend(
"Failed to save model storage settings: " + parseErrorMessage(error),
"Failed to save model storage settings: " +
parseErrorMessage(error),
);
},
});
@ -84,10 +98,10 @@ const ModelSettingsModal: React.FC<ModelSettingsModalProps> = ({
"Failed to save model storage settings: " + parseErrorMessage(error),
);
}
};
});
const handleCancel = () => {
form.resetFields();
form.reset(initialValues);
onCancel();
};
@ -107,26 +121,28 @@ const ModelSettingsModal: React.FC<ModelSettingsModalProps> = ({
Model Settings
</DialogTitle>
</DialogHeader>
<Form
key={proxyConfigData ? JSON.stringify(initialValues) : "loading"}
form={form}
layout="horizontal"
onFinish={handleFormSubmit}
initialValues={initialValues}
>
<Form.Item
label="Store Model in DB"
name="store_model_in_db"
tooltip={storeFieldDescription}
valuePropName="checked"
>
<form onSubmit={handleFormSubmit}>
<div className="flex items-center gap-3 py-2">
<Label htmlFor="store_model_in_db" title={storeFieldDescription}>
Store Model in DB
</Label>
{isLoadingConfig ? (
<Skeleton className="h-6 w-full" />
) : (
<AntdFormSwitch />
<Controller
control={form.control}
name="store_model_in_db"
render={({ field }) => (
<Switch
id="store_model_in_db"
checked={Boolean(field.value)}
onCheckedChange={(checked) => field.onChange(Boolean(checked))}
/>
)}
/>
)}
</Form.Item>
</Form>
</div>
</form>
<DialogFooter>
<Button
variant="outline"
@ -136,7 +152,7 @@ const ModelSettingsModal: React.FC<ModelSettingsModalProps> = ({
Cancel
</Button>
<Button
onClick={() => form.submit()}
onClick={handleFormSubmit}
disabled={isPending || isLoadingConfig}
>
{isPending ? "Saving..." : "Save Settings"}