mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
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:
parent
298489075a
commit
b34f9a118e
9 changed files with 573 additions and 349 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue