From fdafcf28b2dd54c68413daafd4800fdfeca9b4b6 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Fri, 24 Jul 2026 22:49:07 -0700 Subject: [PATCH] refactor(ui): migrate cost-tracking and users off antd and Tremor Replaces antd and Tremor markup in the four route-exclusive components with installed shadcn primitives from @/components/ui plus token utilities. Markup only; no data flow, request or props contract changes. The route tests were not touched by this commit and stay green, which is what makes them evidence: they were written against the antd/Tremor markup. --- ui/litellm-dashboard/eslint-suppressions.json | 14 - .../_components/how_it_works.tsx | 88 ++--- .../multi_export_dropdown.tsx | 17 +- .../users/_components/DefaultUserSettings.tsx | 365 ++++++++++-------- .../users/_components/view_users.tsx | 69 ++-- 5 files changed, 298 insertions(+), 255 deletions(-) diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 09d0032e6c9..b8a18714361 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -260,9 +260,6 @@ "src/app/(dashboard)/cost-tracking/_components/how_it_works.tsx": { "local/filename-pascal-case": { "count": 1 - }, - "no-restricted-imports": { - "count": 1 } }, "src/app/(dashboard)/cost-tracking/_components/pricing_calculator/index.tsx": { @@ -294,9 +291,6 @@ "src/app/(dashboard)/cost-tracking/_components/pricing_calculator/multi_export_dropdown.tsx": { "local/filename-pascal-case": { "count": 1 - }, - "no-restricted-imports": { - "count": 1 } }, "src/app/(dashboard)/cost-tracking/_components/pricing_calculator/use_multi_cost_estimate.ts": { @@ -1860,11 +1854,6 @@ "count": 1 } }, - "src/app/(dashboard)/users/_components/DefaultUserSettings.tsx": { - "no-restricted-imports": { - "count": 2 - } - }, "src/app/(dashboard)/users/_components/edit_user.tsx": { "local/filename-pascal-case": { "count": 1 @@ -1901,9 +1890,6 @@ "local/filename-pascal-case": { "count": 1 }, - "no-restricted-imports": { - "count": 3 - }, "prefer-const": { "count": 1 }, diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/how_it_works.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/how_it_works.tsx index 5fa27551d16..5c79d0a1cad 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/how_it_works.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/how_it_works.tsx @@ -1,6 +1,6 @@ import React, { useState, useMemo } from "react"; -import { Text, TextInput } from "@tremor/react"; import CodeBlock from "@/components/CodeBlock"; +import { Input } from "@/components/ui/input"; const HowItWorks: React.FC = () => { const [responseCost, setResponseCost] = useState(""); @@ -28,30 +28,30 @@ const HowItWorks: React.FC = () => { return (
- Cost Calculation - +

Cost Calculation

+

Discounts are applied to provider costs:{" "} - + final_cost = base_cost × (1 - discount%/100) - +

- Example - +

Example

+

A 5% discount on a $10.00 request results in: $10.00 × (1 - 0.05) = $9.50 - +

- Valid Range - Discount percentages must be between 0% and 100% +

Valid Range

+

Discount percentages must be between 0% and 100%

-
- Validating Discounts - +
+

Validating Discounts

+

Make a test request and check the response headers to verify discounts are applied: - +

{ "messages": [{"role": "user", "content": "Hello"}] }'`} /> - Look for these headers in the response: +

Look for these headers in the response:

- + x-litellm-response-cost - Final cost after discount +

Final cost after discount

- + x-litellm-response-cost-original - Original cost before discount +

Original cost before discount

- + x-litellm-response-cost-discount-amount - Amount discounted +

Amount discounted

-
- Discount Calculator - +
+

Discount Calculator

+

Enter values from your response headers to verify the discount: - -

+

+
-
-
{calculatedDiscount && ( -
- Calculated Results +
+

Calculated Results

- Original Cost: - ${calculatedDiscount.originalCost} +

Original Cost:

+ ${calculatedDiscount.originalCost}
- Final Cost: - ${calculatedDiscount.finalCost} +

Final Cost:

+ ${calculatedDiscount.finalCost}
- Discount Amount: - ${calculatedDiscount.discountAmount} +

Discount Amount:

+ ${calculatedDiscount.discountAmount}
-
- Discount Applied: - {calculatedDiscount.discountPercentage}% +
+

Discount Applied:

+

{calculatedDiscount.discountPercentage}%

diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/pricing_calculator/multi_export_dropdown.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/pricing_calculator/multi_export_dropdown.tsx index af60b590165..b73f13c3b3e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/pricing_calculator/multi_export_dropdown.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/pricing_calculator/multi_export_dropdown.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef, useEffect } from "react"; -import { Button } from "@tremor/react"; -import { DownloadOutlined, FilePdfOutlined, FileExcelOutlined } from "@ant-design/icons"; +import { Download, FileSpreadsheet, FileText } from "lucide-react"; +import { Button } from "@/components/ui/button"; import { MultiModelResult } from "./types"; import { exportMultiToPDF, exportMultiToCSV } from "./multi_export_utils"; @@ -36,30 +36,31 @@ const MultiExportDropdown: React.FC = ({ multiResult } return (
- {isOpen && ( -
+
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/DefaultUserSettings.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/DefaultUserSettings.tsx index 7fee2e14b27..edd79fd5d5d 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/DefaultUserSettings.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/DefaultUserSettings.tsx @@ -1,7 +1,5 @@ import React, { useState, useEffect } from "react"; -import { Card, Title, Text, Divider, TextInput } from "@tremor/react"; -import { Button, Typography, Spin, Switch, Select, InputNumber } from "antd"; -import { PlusOutlined, DeleteOutlined } from "@ant-design/icons"; +import { Plus, Trash2 } from "lucide-react"; import { getInternalUserSettings, updateInternalUserSettings, modelAvailableCall } from "@/components/networking"; import BudgetDurationDropdown, { getBudgetDurationLabel, @@ -9,6 +7,25 @@ import BudgetDurationDropdown, { import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key"; import { formatNumberWithCommas } from "@/utils/dataUtils"; import NotificationManager from "@/components/molecules/notifications_manager"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { + Combobox, + ComboboxChip, + ComboboxChips, + ComboboxChipsInput, + ComboboxContent, + ComboboxEmpty, + ComboboxItem, + ComboboxList, + ComboboxValue, +} from "@/components/ui/combobox"; +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; +import { Switch } from "@/components/ui/switch"; +import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; interface DefaultUserSettingsProps { accessToken: string | null; @@ -23,6 +40,13 @@ interface TeamEntry { user_role: "user" | "admin"; } +const TEAM_MEMBER_ROLES: TeamEntry["user_role"][] = ["user", "admin"]; + +const TEAM_MEMBER_ROLE_LABELS: Record = { + user: "User", + admin: "Admin", +}; + const DefaultUserSettings: React.FC = ({ accessToken, possibleUIRoles, @@ -35,8 +59,6 @@ const DefaultUserSettings: React.FC = ({ const [editedValues, setEditedValues] = useState({}); const [saving, setSaving] = useState(false); const [availableModels, setAvailableModels] = useState([]); - const { Paragraph } = Typography; - const { Option } = Select; useEffect(() => { const fetchSSOSettings = async () => { @@ -129,16 +151,48 @@ const DefaultUserSettings: React.FC = ({ }); }; + const renderMultiSelect = (key: string, options: string[], placeholder: string, getLabel: (o: string) => string) => { + const selected: string[] = editedValues[key] || []; + + return ( + handleTextInputChange(key, value)} + > + + + {(values: string[]) => + values.map((option) => ( + + {getLabel(option)} + + )) + } + + + + + No options found + + {(option: string) => ( + + {getLabel(option)} + + )} + + + + ); + }; + // Teams editor component const renderTeamsEditor = (teams: any[]) => { const normalizedTeams = normalizeTeams(teams); const updateTeam = (index: number, field: keyof TeamEntry, value: any) => { - const updatedTeams = [...normalizedTeams]; - updatedTeams[index] = { - ...updatedTeams[index], - [field]: value, - }; + const updatedTeams = normalizedTeams.map((team, i) => (i === index ? { ...team, [field]: value } : team)); handleTextInputChange("teams", updatedTeams); }; @@ -158,53 +212,69 @@ const DefaultUserSettings: React.FC = ({ return (
{normalizedTeams.map((team, index) => ( -
-
- Team {index + 1} -
-
+
- Team ID - Team ID

+ updateTeam(index, "team_id", e.target.value)} + onChange={(event) => updateTeam(index, "team_id", event.target.value)} placeholder="Enter team ID" />
- Max Budget in Team - updateTeam(index, "max_budget_in_team", value)} +

Max Budget in Team

+ + updateTeam( + index, + "max_budget_in_team", + event.target.value === "" ? undefined : Number(event.target.value), + ) + } placeholder="Optional" min={0} step={0.01} - precision={2} />
- User Role +

User Role

))} -
@@ -217,23 +287,24 @@ const DefaultUserSettings: React.FC = ({ if (key === "teams") { return
{renderTeamsEditor(editedValues[key] || [])}
; } else if (key === "user_role" && possibleUIRoles) { + const internalUserRoles = Object.entries(possibleUIRoles).filter(([role]) => role.includes("internal_user")); + const selectedRole = editedValues[key] || null; + return ( - handleTextInputChange(key, role)}> + + + {selectedRole ? possibleUIRoles[selectedRole]?.ui_label || selectedRole : "Select a role"} + + + + {internalUserRoles.map(([role, { ui_label, description }]) => ( + + {ui_label} + {description} + ))} + ); } else if (key === "budget_duration") { @@ -247,67 +318,47 @@ const DefaultUserSettings: React.FC = ({ } else if (type === "boolean") { return (
- handleTextInputChange(key, checked)} /> + handleTextInputChange(key, checked)} + />
); } else if (type === "array" && property.items?.enum) { - return ( - - ); + return renderMultiSelect(key, property.items.enum as string[], "Select options", (option) => option); } else if (key === "models") { - return ( - + return renderMultiSelect( + key, + ["no-default-models", "all-proxy-models", ...availableModels], + "Select models", + (model) => { + if (model === "no-default-models") return "No Default Models"; + if (model === "all-proxy-models") return "All Proxy Models"; + return getModelDisplayName(model); + }, ); } else if (type === "string" && property.enum) { + const selected = editedValues[key] || null; + return ( - handleTextInputChange(key, option)}> + + + + + {(property.enum as string[]).map((option) => ( + + {option} + + ))} + ); } else { return ( - handleTextInputChange(key, e.target.value)} + handleTextInputChange(key, event.target.value)} placeholder={property.description || ""} className="mt-2" /> @@ -316,33 +367,33 @@ const DefaultUserSettings: React.FC = ({ }; const renderValue = (key: string, value: any): JSX.Element => { - if (value === null || value === undefined) return Not set; + if (value === null || value === undefined) return Not set; if (key === "teams" && Array.isArray(value)) { - if (value.length === 0) return No teams assigned; + if (value.length === 0) return No teams assigned; const normalizedTeams = normalizeTeams(value); return ( -
+
{normalizedTeams.map((team, index) => ( -
-
+
+
- Team ID: -

{team.team_id || "Not specified"}

+ Team ID: +

{team.team_id || "Not specified"}

- Max Budget: -

+ Max Budget: +

{team.max_budget_in_team !== undefined ? `$${formatNumberWithCommas(team.max_budget_in_team, 4)}` : "No limit"}

- Role: -

{team.user_role}

+ Role: +

{team.user_role}

@@ -356,7 +407,7 @@ const DefaultUserSettings: React.FC = ({ return (
{ui_label} - {description &&

{description}

} + {description &&

{description}

}
); } @@ -370,14 +421,14 @@ const DefaultUserSettings: React.FC = ({ } if (key === "models" && Array.isArray(value)) { - if (value.length === 0) return None; + if (value.length === 0) return None; return ( -
+
{value.map((model, index) => ( - + {getModelDisplayName(model)} - + ))}
); @@ -385,22 +436,20 @@ const DefaultUserSettings: React.FC = ({ if (typeof value === "object") { if (Array.isArray(value)) { - if (value.length === 0) return None; + if (value.length === 0) return None; return ( -
+
{value.map((item, index) => ( - + {typeof item === "object" ? JSON.stringify(item) : String(item)} - + ))}
); } - return ( -
{JSON.stringify(value, null, 2)}
- ); + return
{JSON.stringify(value, null, 2)}
; } return {String(value)}; @@ -408,8 +457,8 @@ const DefaultUserSettings: React.FC = ({ if (loading) { return ( -
- +
+
); } @@ -417,7 +466,9 @@ const DefaultUserSettings: React.FC = ({ if (!settings) { return ( - No settings available or you do not have permission to view them. + +

No settings available or you do not have permission to view them.

+
); } @@ -427,7 +478,7 @@ const DefaultUserSettings: React.FC = ({ const { values, field_schema } = settings; if (!field_schema || !field_schema.properties) { - return No schema information available; + return

No schema information available

; } return Object.entries(field_schema.properties).map(([key, property]: [string, any]) => { @@ -435,16 +486,14 @@ const DefaultUserSettings: React.FC = ({ const displayName = key.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()); return ( -
- {displayName} - - {property.description || "No description available"} - +
+

{displayName}

+

{property.description || "No description available"}

{isEditing ? (
{renderEditableField(key, property, value)}
) : ( -
{renderValue(key, value)}
+
{renderValue(key, value)}
)}
); @@ -453,38 +502,38 @@ const DefaultUserSettings: React.FC = ({ return ( -
- Default User Settings - {!loading && - settings && - (isEditing ? ( -
- - -
- ) : ( - - ))} -
+ +
+

Default User Settings

+ {!loading && + settings && + (isEditing ? ( +
+ + +
+ ) : ( + + ))} +
- {settings?.field_schema?.description && ( - {settings.field_schema.description} - )} - + {settings?.field_schema?.description &&

{settings.field_schema.description}

} + -
{renderSettings()}
+
{renderSettings()}
+
); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.tsx index ce912c09373..e689b87460e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.tsx @@ -1,7 +1,6 @@ -import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react"; import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { Button } from "antd"; +import { Button } from "@/components/ui/button"; import BulkEditUserModal from "./BulkEditUsers"; import { CreateUserButton } from "@/components/CreateUserButton"; import EditUserModal from "./edit_user"; @@ -34,7 +33,8 @@ import DefaultUserSettings from "./DefaultUserSettings"; import { UsersTable } from "./view_users/UsersTable"; import UserInfoView from "./view_users/user_info_view"; import { UserInfo } from "@/components/networking"; -import { Skeleton } from "antd"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; interface ViewUserDashboardProps { accessToken: string | null; @@ -353,9 +353,9 @@ const ViewUserDashboard: React.FC = ({
{userListQuery.isLoading && ( <> - - - + + + )} {!userListQuery.isLoading && userID && accessToken && ( @@ -372,8 +372,7 @@ const ViewUserDashboard: React.FC = ({ {isProxyAdmin && (
{isProxyAdmin ? ( - - - Users - Default User Settings - + + + + Users + + + Default User Settings + + - - {usersTable} + {usersTable} - - {!userID || !userRole || !accessToken ? ( -
- + + {!userID || !userRole || !accessToken ? ( +
+
+ + + + +
- ) : ( - - )} - - - +
+ ) : ( + + )} +
+ ) : ( usersTable )}