feat(ui): TopModelView + ModelSettingsModal + AddCredentialModal

- TopModelView: antd Segmented + tremor BarChart \u2192 shadcn ToggleGroup
  + preserved BarChart (phase-1 chart whitelist). View-mode pills
  use the shared blue categorical palette.
- ModelSettingsModal: antd Modal/Button/Skeleton/Space/Typography \u2192
  shadcn Dialog/Button/Skeleton. Form.Item still drives the store-in-DB
  toggle using the original antd Switch (as AntdFormSwitch) since
  Form.Item still provides the value binding.
- AddCredentialModal: antd Modal/Button/Typography/Tooltip + @tremor
  TextInput \u2192 shadcn Dialog/Button/Input/Tooltip. antd Select is
  preserved inside Form.Item for now \u2014 full provider-logo rewrite
  is a larger section migration.

Gates: TS \u2713 | Build \u2713.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 12:22:11 +00:00
parent 6fa4d35d7d
commit 381cb8a614
No known key found for this signature in database
3 changed files with 274 additions and 148 deletions

View file

@ -1,6 +1,11 @@
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
import { BarChart } from "@tremor/react";
import { Segmented } from "antd";
import {
ToggleGroup,
ToggleGroupItem,
} from "@/components/ui/toggle-group";
import { useState } from "react";
import { cn } from "@/lib/utils";
import { formatNumberWithCommas } from "../../../../utils/dataUtils";
import { DataTable } from "../../../view_logs/table";
@ -18,18 +23,26 @@ interface TopModelViewProps {
setTopModelsLimit: (limit: number) => void;
}
export default function TopModelView({ topModels, topModelsLimit, setTopModelsLimit }: TopModelViewProps) {
const [modelViewMode, setModelViewMode] = useState<"chart" | "table">("table");
export default function TopModelView({
topModels,
topModelsLimit,
setTopModelsLimit,
}: TopModelViewProps) {
const [modelViewMode, setModelViewMode] = useState<"chart" | "table">(
"table",
);
const columns = [
{
header: "Model",
accessorKey: "key",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cell: (info: any) => info.getValue() || "-",
},
{
header: "Spend (USD)",
accessorKey: "spend",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cell: (info: any) => {
const value = info.getValue();
return `$${formatNumberWithCommas(value, 2)}`;
@ -38,16 +51,27 @@ export default function TopModelView({ topModels, topModelsLimit, setTopModelsLi
{
header: "Successful",
accessorKey: "successful_requests",
cell: (info: any) => <span className="text-green-600">{info.getValue()?.toLocaleString() || 0}</span>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cell: (info: any) => (
<span className="text-emerald-600 dark:text-emerald-400">
{info.getValue()?.toLocaleString() || 0}
</span>
),
},
{
header: "Failed",
accessorKey: "failed_requests",
cell: (info: any) => <span className="text-red-600">{info.getValue()?.toLocaleString() || 0}</span>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cell: (info: any) => (
<span className="text-destructive">
{info.getValue()?.toLocaleString() || 0}
</span>
),
},
{
header: "Tokens",
accessorKey: "tokens",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cell: (info: any) => info.getValue()?.toLocaleString() || 0,
},
];
@ -56,26 +80,42 @@ export default function TopModelView({ topModels, topModelsLimit, setTopModelsLi
return (
<>
<div className="mb-4 flex justify-between items-center">
<Segmented
options={[
{ label: "5", value: 5 },
{ label: "10", value: 10 },
{ label: "25", value: 25 },
{ label: "50", value: 50 },
]}
value={topModelsLimit}
onChange={(value) => setTopModelsLimit(value as number)}
/>
<ToggleGroup
type="single"
value={String(topModelsLimit)}
onValueChange={(v) => {
if (!v) return;
setTopModelsLimit(parseInt(v));
}}
>
{[5, 10, 25, 50].map((n) => (
<ToggleGroupItem key={n} value={String(n)}>
{n}
</ToggleGroupItem>
))}
</ToggleGroup>
<div className="flex space-x-2">
<button
type="button"
onClick={() => setModelViewMode("table")}
className={`px-3 py-1 text-sm rounded-md ${modelViewMode === "table" ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-700"}`}
className={cn(
"px-3 py-1 text-sm rounded-md",
modelViewMode === "table"
? "bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300"
: "bg-muted text-muted-foreground",
)}
>
Table View
</button>
<button
type="button"
onClick={() => setModelViewMode("chart")}
className={`px-3 py-1 text-sm rounded-md ${modelViewMode === "chart" ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-700"}`}
className={cn(
"px-3 py-1 text-sm rounded-md",
modelViewMode === "chart"
? "bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300"
: "bg-muted text-muted-foreground",
)}
>
Chart View
</button>
@ -85,7 +125,10 @@ export default function TopModelView({ topModels, topModelsLimit, setTopModelsLi
<div className="relative max-h-[600px] overflow-y-auto">
<BarChart
className="mt-4 cursor-pointer hover:opacity-90"
style={{ height: Math.min(processedTopModels.length, topModelsLimit) * 52 }}
style={{
height:
Math.min(processedTopModels.length, topModelsLimit) * 52,
}}
data={processedTopModels}
index="key"
categories={["spend"]}

View file

@ -1,117 +1,154 @@
import { TextInput } from "@tremor/react";
import { Select as AntdSelect, Button, Form, Modal, Tooltip, Typography } from "antd";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
Select as AntdSelect,
Form,
} from "antd";
import type { UploadProps } from "antd/es/upload";
import React, { useState } from "react";
import ProviderSpecificFields from "../add_model/provider_specific_fields";
import { Providers, providerLogoMap } from "../provider_info_helpers";
const { Link } = Typography;
interface AddCredentialsModalProps {
open: boolean;
onCancel: () => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onAddCredential: (values: any) => void;
uploadProps: UploadProps;
}
const AddCredentialsModal: React.FC<AddCredentialsModalProps> = ({ open, onCancel, onAddCredential, uploadProps }) => {
const AddCredentialsModal: React.FC<AddCredentialsModalProps> = ({
open,
onCancel,
onAddCredential,
uploadProps,
}) => {
const [form] = Form.useForm();
const [selectedProvider, setSelectedProvider] = useState<Providers>(Providers.OpenAI);
const [selectedProvider, setSelectedProvider] = useState<Providers>(
Providers.OpenAI,
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleSubmit = (values: any) => {
const filteredValues = Object.entries(values).reduce((acc, [key, value]) => {
if (value !== "" && value !== undefined && value !== null) {
acc[key] = value;
}
return acc;
}, {} as any);
const filteredValues = Object.entries(values).reduce(
(acc, [key, value]) => {
if (value !== "" && value !== undefined && value !== null) {
acc[key] = value;
}
return acc;
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{} as any,
);
onAddCredential(filteredValues);
form.resetFields();
};
const handleCancel = () => {
onCancel();
form.resetFields();
};
return (
<Modal
title="Add New Credential"
open={open}
onCancel={() => {
onCancel();
form.resetFields();
}}
footer={null}
width={600}
>
<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" }]}
>
<TextInput placeholder="Enter a friendly name for these credentials" />
</Form.Item>
{/* Provider Selection */}
<Form.Item
rules={[{ required: true, message: "Required" }]}
label="Provider:"
name="custom_llm_provider"
tooltip="Helper to auto-populate provider specific fields"
>
<AntdSelect
showSearch
onChange={(value) => {
setSelectedProvider(value as Providers);
form.setFieldValue("custom_llm_provider", value);
}}
<Dialog open={open} onOpenChange={(o) => (!o ? handleCancel() : undefined)}>
<DialogContent className="max-w-[600px]">
<DialogHeader>
<DialogTitle>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" }]}
>
{Object.entries(Providers).map(([providerEnum, providerDisplayName]) => (
<AntdSelect.Option key={providerEnum} value={providerEnum}>
<div className="flex items-center space-x-2">
<img
src={providerLogoMap[providerDisplayName]}
alt={`${providerEnum} logo`}
className="w-5 h-5"
onError={(e) => {
const target = e.target as HTMLImageElement;
const parent = target.parentElement;
if (parent) {
const fallbackDiv = document.createElement("div");
fallbackDiv.className =
"w-5 h-5 rounded-full bg-gray-200 flex items-center justify-center text-xs";
fallbackDiv.textContent = providerDisplayName.charAt(0);
parent.replaceChild(fallbackDiv, target);
}
}}
/>
<span>{providerDisplayName}</span>
</div>
</AntdSelect.Option>
))}
</AntdSelect>
</Form.Item>
<Input placeholder="Enter a friendly name for these credentials" />
</Form.Item>
<ProviderSpecificFields selectedProvider={selectedProvider} uploadProps={uploadProps} />
{/* Modal Footer */}
<div className="flex justify-between items-center">
<Tooltip title="Get help on our github">
<Link href="https://github.com/BerriAI/litellm/issues">Need Help?</Link>
</Tooltip>
<div>
<Button
onClick={() => {
onCancel();
form.resetFields();
<Form.Item
rules={[{ required: true, message: "Required" }]}
label="Provider:"
name="custom_llm_provider"
tooltip="Helper to auto-populate provider specific fields"
>
<AntdSelect
showSearch
onChange={(value) => {
setSelectedProvider(value as Providers);
form.setFieldValue("custom_llm_provider", value);
}}
style={{ marginRight: 10 }}
>
Cancel
</Button>
<Button htmlType="submit">{"Add Credential"}</Button>
{Object.entries(Providers).map(
([providerEnum, providerDisplayName]) => (
<AntdSelect.Option key={providerEnum} value={providerEnum}>
<div className="flex items-center space-x-2">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={providerLogoMap[providerDisplayName]}
alt={`${providerEnum} logo`}
className="w-5 h-5"
onError={(e) => {
const target = e.target as HTMLImageElement;
const parent = target.parentElement;
if (parent) {
const fallbackDiv = document.createElement("div");
fallbackDiv.className =
"w-5 h-5 rounded-full bg-muted flex items-center justify-center text-xs";
fallbackDiv.textContent =
providerDisplayName.charAt(0);
parent.replaceChild(fallbackDiv, target);
}
}}
/>
<span>{providerDisplayName}</span>
</div>
</AntdSelect.Option>
),
)}
</AntdSelect>
</Form.Item>
<ProviderSpecificFields
selectedProvider={selectedProvider}
uploadProps={uploadProps}
/>
<div className="flex justify-between items-center">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<a
href="https://github.com/BerriAI/litellm/issues"
className="text-primary hover:underline"
>
Need Help?
</a>
</TooltipTrigger>
<TooltipContent>Get help on our github</TooltipContent>
</Tooltip>
</TooltipProvider>
<div className="flex gap-2">
<Button variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button type="submit">Add Credential</Button>
</div>
</div>
</div>
</Form>
</Modal>
</Form>
</DialogContent>
</Dialog>
);
};

View file

@ -1,10 +1,25 @@
"use client";
import { ConfigType, useProxyConfig } from "@/app/(dashboard)/hooks/proxyConfig/useProxyConfig";
import { StoreModelInDBParams, useStoreModelInDB } from "@/app/(dashboard)/hooks/storeModelInDB/useStoreModelInDB";
import {
ConfigType,
useProxyConfig,
} from "@/app/(dashboard)/hooks/proxyConfig/useProxyConfig";
import {
StoreModelInDBParams,
useStoreModelInDB,
} from "@/app/(dashboard)/hooks/storeModelInDB/useStoreModelInDB";
import NotificationsManager from "@/components/molecules/notifications_manager";
import { parseErrorMessage } from "@/components/shared/errorUtils";
import { Button, Form, Modal, Skeleton, Space, Switch, Typography } from "antd";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Skeleton } from "@/components/ui/skeleton";
import { Form, Switch as AntdFormSwitch } from "antd";
import React, { useEffect, useMemo } from "react";
interface ModelSettingsModalProps {
@ -13,19 +28,25 @@ interface ModelSettingsModalProps {
onSuccess?: () => void;
}
const ModelSettingsModal: React.FC<ModelSettingsModalProps> = ({ isVisible, onCancel, onSuccess }) => {
const ModelSettingsModal: React.FC<ModelSettingsModalProps> = ({
isVisible,
onCancel,
onSuccess,
}) => {
const [form] = Form.useForm();
const { mutateAsync, isPending } = useStoreModelInDB();
const { data: proxyConfigData, isLoading: isLoadingConfig, refetch } = useProxyConfig(ConfigType.GENERAL_SETTINGS);
const {
data: proxyConfigData,
isLoading: isLoadingConfig,
refetch,
} = useProxyConfig(ConfigType.GENERAL_SETTINGS);
// Refetch config when modal opens to ensure we have the latest values
useEffect(() => {
if (isVisible) {
refetch();
}
}, [isVisible, refetch]);
// Compute initial values from fetched config data
const initialValues = useMemo(() => {
if (!proxyConfigData) {
return {
@ -33,7 +54,9 @@ const ModelSettingsModal: React.FC<ModelSettingsModalProps> = ({ isVisible, onCa
};
}
const storeModelField = proxyConfigData.find(field => field.field_name === 'store_model_in_db');
const storeModelField = proxyConfigData.find(
(field) => field.field_name === "store_model_in_db",
);
return {
store_model_in_db: storeModelField?.field_value ?? false,
@ -44,16 +67,22 @@ const ModelSettingsModal: React.FC<ModelSettingsModalProps> = ({ isVisible, onCa
try {
await mutateAsync(formValues, {
onSuccess: () => {
NotificationsManager.success("Model storage settings updated successfully");
NotificationsManager.success(
"Model storage settings updated successfully",
);
refetch();
onSuccess?.();
},
onError: (error) => {
NotificationsManager.fromBackend("Failed to save model storage settings: " + parseErrorMessage(error));
NotificationsManager.fromBackend(
"Failed to save model storage settings: " + parseErrorMessage(error),
);
},
});
} catch (error) {
NotificationsManager.fromBackend("Failed to save model storage settings: " + parseErrorMessage(error));
NotificationsManager.fromBackend(
"Failed to save model storage settings: " + parseErrorMessage(error),
);
}
};
@ -62,42 +91,59 @@ const ModelSettingsModal: React.FC<ModelSettingsModalProps> = ({ isVisible, onCa
onCancel();
};
const storeFieldDescription =
proxyConfigData?.find((f) => f.field_name === "store_model_in_db")
?.field_description ||
"If enabled, models and config are stored in and loaded from the database.";
return (
<Modal
title={<Typography.Title level={5}>Model Settings</Typography.Title>}
<Dialog
open={isVisible}
footer={
<Space>
<Button onClick={handleCancel} disabled={isPending || isLoadingConfig}>
onOpenChange={(o) => (!o ? handleCancel() : undefined)}
>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-base font-semibold">
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"
>
{isLoadingConfig ? (
<Skeleton className="h-6 w-full" />
) : (
<AntdFormSwitch />
)}
</Form.Item>
</Form>
<DialogFooter>
<Button
variant="outline"
onClick={handleCancel}
disabled={isPending || isLoadingConfig}
>
Cancel
</Button>
<Button type="primary" loading={isPending} disabled={isLoadingConfig} onClick={() => form.submit()}>
<Button
onClick={() => form.submit()}
disabled={isPending || isLoadingConfig}
>
{isPending ? "Saving..." : "Save Settings"}
</Button>
</Space>
}
onCancel={handleCancel}
>
<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={
proxyConfigData?.find(f => f.field_name === 'store_model_in_db')?.field_description ||
"If enabled, models and config are stored in and loaded from the database."
}
valuePropName="checked"
>
{isLoadingConfig ? <Skeleton.Input active block /> : <Switch />}
</Form.Item>
</Form>
</Modal>
</DialogFooter>
</DialogContent>
</Dialog>
);
};