From 381cb8a61454e0fdcac5ea95e837cfec09a9b871 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 12:22:11 +0000 Subject: [PATCH] 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 --- .../components/EntityUsage/TopModelView.tsx | 79 +++++-- .../model_add/AddCredentialModal.tsx | 215 ++++++++++-------- .../ModelSettingsModal/ModelSettingsModal.tsx | 128 +++++++---- 3 files changed, 274 insertions(+), 148 deletions(-) diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopModelView.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopModelView.tsx index c69ba42f182..14979936c29 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopModelView.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopModelView.tsx @@ -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) => {info.getValue()?.toLocaleString() || 0}, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + cell: (info: any) => ( + + {info.getValue()?.toLocaleString() || 0} + + ), }, { header: "Failed", accessorKey: "failed_requests", - cell: (info: any) => {info.getValue()?.toLocaleString() || 0}, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + cell: (info: any) => ( + + {info.getValue()?.toLocaleString() || 0} + + ), }, { 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 ( <>
- setTopModelsLimit(value as number)} - /> + { + if (!v) return; + setTopModelsLimit(parseInt(v)); + }} + > + {[5, 10, 25, 50].map((n) => ( + + {n} + + ))} +
@@ -85,7 +125,10 @@ export default function TopModelView({ topModels, topModelsLimit, setTopModelsLi
void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any onAddCredential: (values: any) => void; uploadProps: UploadProps; } -const AddCredentialsModal: React.FC = ({ open, onCancel, onAddCredential, uploadProps }) => { +const AddCredentialsModal: React.FC = ({ + open, + onCancel, + onAddCredential, + uploadProps, +}) => { const [form] = Form.useForm(); - const [selectedProvider, setSelectedProvider] = useState(Providers.OpenAI); + const [selectedProvider, setSelectedProvider] = useState( + 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 ( - { - onCancel(); - form.resetFields(); - }} - footer={null} - width={600} - > -
- {/* Credential Name */} - - - - - {/* Provider Selection */} - - { - setSelectedProvider(value as Providers); - form.setFieldValue("custom_llm_provider", value); - }} + (!o ? handleCancel() : undefined)}> + + + Add New Credential + + + - {Object.entries(Providers).map(([providerEnum, providerDisplayName]) => ( - -
- {`${providerEnum} { - 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); - } - }} - /> - {providerDisplayName} -
-
- ))} -
-
+ + - - - {/* Modal Footer */} -
- - Need Help? - - -
- - + {Object.entries(Providers).map( + ([providerEnum, providerDisplayName]) => ( + +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {`${providerEnum} { + 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); + } + }} + /> + {providerDisplayName} +
+
+ ), + )} + + + + + +
+ + + + + Need Help? + + + Get help on our github + + + +
+ + +
-
- - + + + ); }; diff --git a/ui/litellm-dashboard/src/components/model_dashboard/ModelSettingsModal/ModelSettingsModal.tsx b/ui/litellm-dashboard/src/components/model_dashboard/ModelSettingsModal/ModelSettingsModal.tsx index 106f82585dd..1aa11a00175 100644 --- a/ui/litellm-dashboard/src/components/model_dashboard/ModelSettingsModal/ModelSettingsModal.tsx +++ b/ui/litellm-dashboard/src/components/model_dashboard/ModelSettingsModal/ModelSettingsModal.tsx @@ -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 = ({ isVisible, onCancel, onSuccess }) => { +const ModelSettingsModal: React.FC = ({ + 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 = ({ 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 = ({ 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 = ({ 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 ( - Model Settings} + - - - - } - onCancel={handleCancel} - > -
- 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 ? : } - -
-
+ + + ); };