From b69177712efe0cc7752a5b53b72254fbf60a79bd Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 13 Aug 2026 15:02:40 -0700 Subject: [PATCH] refactor(ui): migrate usage to shadcn Replaces antd and Tremor markup on the usage route with the installed shadcn base-vega primitives. Behaviour is unchanged: the route's characterisation tests were rewritten to role and text queries in the previous commit, proven green against the antd components, and pass through this commit unedited. Tremor tab panels stayed mounted once rendered, so every migrated TabsContent and the collapsible model sections in activity_metrics carry keepMounted to keep view-mode and expansion state alive across tab switches. --- ui/litellm-dashboard/eslint-suppressions.json | 54 +- .../components/EndpointUsageTable.tsx | 18 +- .../components/EntityUsage/EntityUsage.tsx | 338 ++++---- .../EntityUsage/SpendByProvider.tsx | 44 +- .../components/EntityUsage/TopModelView.tsx | 47 +- .../components/UsageAIChatPanel.tsx | 62 +- .../_components/components/UsagePageView.tsx | 763 +++++++++--------- .../UsageViewSelect/UsageViewSelect.tsx | 99 +-- .../EntityUsageExportModal.tsx | 81 +- .../ExportFormatSelector.tsx | 34 +- .../EntityUsageExport/ExportTypeSelector.tsx | 55 +- .../EntityUsageExport/UsageExportHeader.tsx | 116 ++- .../src/components/activity_metrics.tsx | 401 +++++---- .../common_components/team_multi_select.tsx | 132 +-- .../src/components/per_user_usage.tsx | 225 +++--- .../src/components/user_agent_activity.tsx | 320 ++++---- 16 files changed, 1454 insertions(+), 1335 deletions(-) diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 2f4def9ffa2..c94686b2f1e 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -1658,25 +1658,12 @@ "src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageTable.tsx": { "no-nested-ternary": { "count": 1 - }, - "no-restricted-imports": { - "count": 1 } }, "src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx": { "local/no-complex-jsx-arrow": { "count": 2 }, - "no-restricted-imports": { - "count": 2 - } - }, - "src/app/(dashboard)/usage/_components/components/EntityUsage/SpendByProvider.tsx": { - "no-restricted-imports": { - "count": 2 - } - }, - "src/app/(dashboard)/usage/_components/components/EntityUsage/TopModelView.tsx": { "no-restricted-imports": { "count": 1 } @@ -1685,9 +1672,6 @@ "no-nested-ternary": { "count": 1 }, - "no-restricted-imports": { - "count": 1 - }, "react-hooks/immutability": { "count": 1 } @@ -1703,7 +1687,7 @@ "count": 1 }, "no-restricted-imports": { - "count": 2 + "count": 1 }, "react-hooks/purity": { "count": 1 @@ -1712,14 +1696,6 @@ "count": 3 } }, - "src/app/(dashboard)/usage/_components/components/UsageViewSelect/UsageViewSelect.tsx": { - "local/no-complex-jsx-arrow": { - "count": 2 - }, - "no-restricted-imports": { - "count": 1 - } - }, "src/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity.ts": { "react-hooks/refs": { "count": 1 @@ -1982,29 +1958,14 @@ "count": 1 } }, - "src/components/EntityUsageExport/EntityUsageExportModal.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/components/EntityUsageExport/ExportFormatSelector.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/EntityUsageExport/ExportSummary.tsx": { "no-restricted-imports": { "count": 1 } }, - "src/components/EntityUsageExport/ExportTypeSelector.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/EntityUsageExport/UsageExportHeader.tsx": { "no-restricted-imports": { - "count": 3 + "count": 1 } }, "src/components/EntityUsageExport/types.ts": { @@ -2289,9 +2250,6 @@ }, "no-nested-ternary": { "count": 1 - }, - "no-restricted-imports": { - "count": 2 } }, "src/components/add_model/AdaptiveRoutingConfig.tsx": { @@ -2794,9 +2752,6 @@ "src/components/common_components/team_multi_select.tsx": { "local/filename-pascal-case": { "count": 1 - }, - "no-restricted-imports": { - "count": 1 } }, "src/components/common_components/user_search_modal.tsx": { @@ -3171,9 +3126,6 @@ "local/filename-pascal-case": { "count": 1 }, - "no-restricted-imports": { - "count": 1 - }, "react-hooks/set-state-in-effect": { "count": 1 } @@ -3714,7 +3666,7 @@ "count": 1 }, "no-restricted-imports": { - "count": 3 + "count": 1 }, "react-hooks/set-state-in-effect": { "count": 1 diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageTable.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageTable.tsx index 7b6bd57e758..5f72caae437 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageTable.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageTable.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { Progress } from "antd"; import type { ColumnDef } from "@tanstack/react-table"; +import { Meter, MeterIndicator, MeterTrack } from "@/components/ui/meter"; import { DataTable } from "@/components/shared/DataTable"; import { MoneyCell } from "@/components/shared/table_cells"; import { MetricWithMetadata } from "@/components/UsagePage/types"; @@ -53,19 +53,15 @@ const EndpointUsageTable: React.FC = ({ endpointData }) const failurePercentage = record.api_requests > 0 ? (record.failed_requests / record.api_requests) * 100 : 0; const totalPercentage = successPercentage + failurePercentage; - const strokeColorConfig: Record = { - "0%": "#22c55e", - }; - if (successPercentage > 0 && successPercentage < 100) { - strokeColorConfig[`${successPercentage}%`] = "#22c55e"; - strokeColorConfig[`${successPercentage + 0.01}%`] = "#ef4444"; - } - strokeColorConfig["100%"] = failurePercentage > 0 ? "#ef4444" : "#22c55e"; - return (
- + {/* The failed share is the track showing through behind the successful share. */} + + 0 ? "bg-red-500" : undefined}> + + +
{record.successful_requests.toLocaleString()} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx index 3a22eb7fd10..2d6dbb823cc 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx @@ -14,23 +14,13 @@ import { MoneyCell } from "@/components/shared/table_cells"; import { Card as ShadcnCard, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { hasCapability, type Capability } from "@/utils/capabilities"; import { formatNumberWithCommas } from "@/utils/dataUtils"; -import { - Card, - Col, - DateRangePickerValue, - Grid, - Subtitle, - Tab, - TabGroup, - TabList, - TabPanel, - TabPanels, - Text, - Title, -} from "@tremor/react"; -import { DownOutlined, ExportOutlined, InfoCircleOutlined, LoadingOutlined, RightOutlined } from "@ant-design/icons"; +import type { DateRangePickerValue } from "@tremor/react"; +import { ChevronDown, ChevronRight, ExternalLink, Info, Loader2 } from "lucide-react"; import type { ColumnDef } from "@tanstack/react-table"; -import { Alert, Button, Tooltip } from "antd"; +import { Alert, AlertDescription } from "@/components/shared/Alert"; +import { Button } from "@/components/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import React, { type ReactNode, useMemo, useState } from "react"; import TeamMultiSelect from "@/components/common_components/team_multi_select"; import { ActivityMetrics, processActivityData } from "@/components/activity_metrics"; @@ -341,23 +331,29 @@ const EntityUsage: React.FC = ({ [], ); - const chev = "text-gray-400 text-xs"; - const expandIcon = showCostBreakdown ? : ; - const infoIcon = ; + const chev = "size-3 text-gray-400"; + const expandIcon = showCostBreakdown ? : ; const renderSummaryTile = ({ title, value, className, tooltip, expandable }: SummaryTile) => ( - setShowCostBreakdown(!showCostBreakdown) : undefined} > -
- {title} - {tooltip ? {infoIcon} : null} - {expandable ? expandIcon : null} -
- {value} -
+ +
+

{title}

+ {tooltip ? ( + + } /> + {tooltip} + + ) : null} + {expandable ? expandIcon : null} +
+

{value}

+
+ ); const breakdownTiles = showFlatCost && showCostBreakdown ? buildCostBreakdownTiles(spendData.metadata) : []; @@ -366,18 +362,18 @@ const EntityUsage: React.FC = ({ const modelViewTitle = modelViewType === "groups" ? "Top Public Model Names" : "Top Litellm Models"; const costPanel = ( - - - - {capitalizedEntityLabel} Spend Overview - - {summaryTiles.map(renderSummaryTile)} - - - +
+
+ + +

{capitalizedEntityLabel} Spend Overview

+
{summaryTiles.map(renderSummaryTile)}
+
+
+
{/* Daily Spend Chart */} - +
Daily Spend @@ -451,15 +447,15 @@ const EntityUsage: React.FC = ({ /> - +
{/* Entity Breakdown Section */} - - -
+
+ +
- Spend Per {capitalizedEntityLabel} - Showing Top 5 by Spend +

Spend Per {capitalizedEntityLabel}

+

Showing Top 5 by Spend

Get Started by Tracking cost per {capitalizedEntityLabel} = ({
- - +
+
= ({ ); }} /> - - +
+
entity.metrics.spend > 0)} @@ -509,61 +505,69 @@ const EntityUsage: React.FC = ({ noDataMessage={`No ${entityType} spend data`} size="compact" /> - - -
- - +
+
+ + +
{/* Top API Keys */} - - - Top Virtual Keys - - - +
+ + +

Top Virtual Keys

+ +
+
+
{/* Top Models */} - - -
- {entityType === "agent" ? "Top Agents" : modelViewTitle} - -
- -
- +
+ + +
+

+ {entityType === "agent" ? "Top Agents" : modelViewTitle} +

+ +
+ +
+
+
{showAgentBreakdown && ( - - - Top Agents Driving Spend - - - +
+ + +

Top Agents Driving Spend

+ +
+
+
)} {/* Spend by Provider */} - - -
- Provider Usage - - +
+ + +

Provider Usage

+
+
= ({ startAngle={90} endAngle={-270} /> - - +
+
= ({ noDataMessage="No provider usage data" size="compact" /> - - -
- - - +
+
+ + +
+
); const tabs: readonly { key: string; label: string; content: ReactNode }[] = [ @@ -620,80 +624,60 @@ const EntityUsage: React.FC = ({ return (
{isFetchingMore && ( - - - - Currently fetching spend data: fetched {progress.currentPage} / {progress.totalPages} pages. Charts will - update periodically as data loads. Moving off of this page will stop and reset this. To continue using - the UI in the meantime,{" "} - - open a new tab - - . - - -
- } - /> + + + + + Currently fetching spend data: fetched {progress.currentPage} / {progress.totalPages} pages. Charts will + update periodically as data loads. Moving off of this page will stop and reset this. To continue using the + UI in the meantime,{" "} + + open a new tab + + . + + + + )} {cancelled && ( - - Showing partial data ({progress.currentPage}/{progress.totalPages} pages loaded) - - } - /> + + + Showing partial data ({progress.currentPage}/{progress.totalPages} pages loaded) + + )} {agentIsFetchingMore && showAgentBreakdown && ( - - - - Currently fetching agent data: fetched {agentProgress.currentPage} / {agentProgress.totalPages} pages. - Charts will update periodically as data loads. Moving off of this page will stop and reset this. To - continue using the UI in the meantime,{" "} - - open a new tab - - . - - -
- } - /> + + + + + Currently fetching agent data: fetched {agentProgress.currentPage} / {agentProgress.totalPages} pages. + Charts will update periodically as data loads. Moving off of this page will stop and reset this. To + continue using the UI in the meantime,{" "} + + open a new tab + + . + + + + )} {agentCancelled && showAgentBreakdown && ( - - Showing partial agent data ({agentProgress.currentPage}/{agentProgress.totalPages} pages loaded) - - } - /> + + + Showing partial agent data ({agentProgress.currentPage}/{agentProgress.totalPages} pages loaded) + + )} {entityType === "team" && (
- Filter by team +

Filter by team

)} @@ -710,18 +694,20 @@ const EntityUsage: React.FC = ({ filterMode={entityType === "user" ? "single" : "multiple"} teams={teams || []} /> - - + + {tabs.map(({ key, label }) => ( - {label} + + {label} + ))} - - - {tabs.map(({ key, content }) => ( - {content} - ))} - - + + {tabs.map(({ key, content }) => ( + + {content} + + ))} +
); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/SpendByProvider.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/SpendByProvider.tsx index ac8ae0e67e1..38f055f65f3 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/SpendByProvider.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/SpendByProvider.tsx @@ -2,10 +2,11 @@ import { DonutChart } from "@/components/shared/charts"; import { DataTable } from "@/components/shared/DataTable"; import { MoneyCell } from "@/components/shared/table_cells"; import { formatNumberWithCommas } from "@/utils/dataUtils"; -import { InfoCircleOutlined } from "@ant-design/icons"; +import { Info } from "lucide-react"; import type { ColumnDef } from "@tanstack/react-table"; -import { Card, Col, Grid, Switch, Title } from "@tremor/react"; -import { Tooltip } from "antd"; +import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Switch } from "@/components/ui/switch"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import React, { useState } from "react"; import { ProviderLogo } from "@/components/molecules/models/ProviderLogo"; import { ChartLoader } from "@/components/shared/chart_loader"; @@ -85,29 +86,30 @@ const SpendByProvider: React.FC = ({ loading, isDateChangi return ( -
- Spend by Provider -
+ + Spend by Provider +
- +
- - + + } /> + Requests that failed to route to a provider
- +
-
-
- {loading ? ( - - ) : ( - - + + + + {loading ? ( + + ) : ( +
= ({ loading, isDateChangi startAngle={90} endAngle={-270} /> - - = ({ loading, isDateChangi noDataMessage="No provider usage data" size="compact" /> - - - )} +
+ )} +
); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/TopModelView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/TopModelView.tsx index 2e2880e8763..2f5b750c118 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/TopModelView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/TopModelView.tsx @@ -1,7 +1,7 @@ import { BarChart } from "@/components/shared/charts"; import { DataTable } from "@/components/shared/DataTable"; import { MoneyCell } from "@/components/shared/table_cells"; -import { Segmented } from "antd"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useState } from "react"; import { formatNumberWithCommas } from "@/utils/dataUtils"; @@ -19,6 +19,8 @@ interface TopModelViewProps { setTopModelsLimit: (limit: number) => void; } +export const TOP_MODEL_LIMITS = [5, 10, 25, 50]; + export default function TopModelView({ topModels, topModelsLimit, setTopModelsLimit }: TopModelViewProps) { const [modelViewMode, setModelViewMode] = useState<"chart" | "table">("table"); @@ -58,30 +60,25 @@ export default function TopModelView({ topModels, topModelsLimit, setTopModelsLi return ( <>
- setTopModelsLimit(value as number)} - /> -
- - -
+ setTopModelsLimit(Number(value))}> + + {TOP_MODEL_LIMITS.map((limit) => ( + + {limit} + + ))} + + + setModelViewMode(value as "chart" | "table")}> + + + Table View + + + Chart View + + +
{modelViewMode === "chart" ? (
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsageAIChatPanel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsageAIChatPanel.tsx index 9d6b09353a2..21314594cb4 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsageAIChatPanel.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsageAIChatPanel.tsx @@ -1,10 +1,18 @@ import React, { useEffect, useRef, useState } from "react"; -import { Button, Select, Input, Spin } from "antd"; import ReactMarkdown from "react-markdown"; +import { Button } from "@/components/ui/button"; +import { + Combobox, + ComboboxContent, + ComboboxEmpty, + ComboboxInput, + ComboboxItem, + ComboboxList, +} from "@/components/ui/combobox"; +import { Textarea } from "@/components/ui/textarea"; +import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { modelHubCall, usageAiChatStream, UsageAiToolCallEvent } from "@/components/networking"; -const { TextArea } = Input; - interface ToolCallStep { tool_name: string; tool_label: string; @@ -41,7 +49,7 @@ const ToolCallDisplay: React.FC<{ step: ToolCallStep }> = ({ step }) => {
{step.status === "running" ? ( - + ) : step.status === "error" ? ( ✗ ) : ( @@ -259,18 +267,29 @@ const UsageAIChatPanel: React.FC = ({ open, onClose, acce {/* Model selector */}
-