From b2cf17d4a21bb1faa7c30025ef58df2244c74029 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 23 Jul 2026 23:17:52 -0700 Subject: [PATCH] refactor(ui): migrate logging-and-alerts, caching and policies to shadcn Markup-only migration of the 17 files these three routes exclusively own, replacing antd and Tremor with the installed shadcn (base-vega) primitives and lucide icons. No route behaviour changes; the tests written in the previous commit are untouched here and pass against both the old and the new markup. Colour now comes from tokens rather than from hardcoded utilities, so the health-check button, the alerts and the badges no longer pin their own palette. email_settings also loses an invalid DOM nesting (a table cell inside a div, and a div inside a paragraph) that React had been warning about. Two modals on the policies page moved from the Policies panel up to the panel root. Base UI Tabs mounts only the active panel, unlike Tremor, and both are opened from the Templates tab, so leaving them nested would have made "Use Template" do nothing. Retires 53 antd import suppressions from the eslint baseline. --- ui/litellm-dashboard/eslint-suppressions.json | 63 +-- .../caching/_components/cache_dashboard.tsx | 277 +++++----- .../caching/_components/cache_health.tsx | 237 ++++----- .../cache_settings/RedisTypeSelector.tsx | 29 +- .../CoordinationRedisTypeSelector.tsx | 27 +- .../_components/ai_suggestion_modal.tsx | 454 +++++++++-------- .../_components/guardrail_selection_modal.tsx | 313 ++++++------ .../_components/impact_preview_alert.tsx | 92 ++-- .../policies/_components/index.tsx | 473 +++++++++--------- .../_components/pipeline_flow_builder.tsx | 134 ++--- .../policies/_components/policy_info.tsx | 262 +++++----- .../policies/_components/policy_templates.tsx | 162 +++--- .../_components/template_parameter_modal.tsx | 403 ++++++++------- .../CloudZeroCostTracking.tsx | 14 +- .../CloudZeroEmptyPlaceholder.tsx | 28 +- .../CloudZeroIntegrationSettings.tsx | 195 ++++---- .../email_events/email_event_settings.tsx | 76 +-- .../src/components/email_settings.tsx | 197 +++----- .../src/components/ui/radio-group.tsx | 29 ++ 19 files changed, 1734 insertions(+), 1731 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/ui/radio-group.tsx diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index ec1e3ac05ba..a2aaf61263e 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -177,9 +177,6 @@ "src/app/(dashboard)/caching/_components/cache_health.tsx": { "local/filename-pascal-case": { "count": 1 - }, - "no-restricted-imports": { - "count": 1 } }, "src/app/(dashboard)/caching/_components/cache_settings/CacheFormField.tsx": { @@ -187,11 +184,6 @@ "count": 1 } }, - "src/app/(dashboard)/caching/_components/cache_settings/RedisTypeSelector.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/app/(dashboard)/caching/_components/cache_settings/cacheSettingsFields.ts": { "no-restricted-imports": { "count": 1 @@ -213,11 +205,6 @@ "count": 1 } }, - "src/app/(dashboard)/caching/_components/coordination_redis_settings/CoordinationRedisTypeSelector.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/app/(dashboard)/caching/_components/coordination_redis_settings/coordinationRedisFields.ts": { "no-restricted-imports": { "count": 1 @@ -1469,9 +1456,6 @@ "no-nested-ternary": { "count": 10 }, - "no-restricted-imports": { - "count": 2 - }, "react-hooks/immutability": { "count": 1 } @@ -1483,9 +1467,6 @@ "no-nested-ternary": { "count": 1 }, - "no-restricted-imports": { - "count": 1 - }, "react-hooks/set-state-in-effect": { "count": 1 } @@ -1509,9 +1490,6 @@ "src/app/(dashboard)/policies/_components/impact_preview_alert.tsx": { "local/filename-pascal-case": { "count": 1 - }, - "no-restricted-imports": { - "count": 1 } }, "src/app/(dashboard)/policies/_components/index.test.tsx": { @@ -1526,9 +1504,6 @@ "local/no-complex-jsx-arrow": { "count": 1 }, - "no-restricted-imports": { - "count": 2 - }, "react-hooks/set-state-in-effect": { "count": 1 } @@ -1543,9 +1518,6 @@ "no-nested-ternary": { "count": 1 }, - "no-restricted-imports": { - "count": 2 - }, "react-hooks/set-state-in-effect": { "count": 2 } @@ -1554,9 +1526,6 @@ "local/filename-pascal-case": { "count": 1 }, - "no-restricted-imports": { - "count": 2 - }, "react-hooks/set-state-in-effect": { "count": 1 } @@ -1564,9 +1533,6 @@ "src/app/(dashboard)/policies/_components/policy_templates.tsx": { "local/filename-pascal-case": { "count": 1 - }, - "no-restricted-imports": { - "count": 1 } }, "src/app/(dashboard)/policies/_components/policy_test_panel.tsx": { @@ -1584,9 +1550,6 @@ "local/filename-pascal-case": { "count": 1 }, - "no-restricted-imports": { - "count": 2 - }, "react-hooks/immutability": { "count": 1 }, @@ -2218,26 +2181,11 @@ "count": 1 } }, - "src/components/CloudZeroCostTracking/CloudZeroCostTracking.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/CloudZeroCostTracking/CloudZeroCreateModal.tsx": { "no-restricted-imports": { "count": 1 } }, - "src/components/CloudZeroCostTracking/CloudZeroEmptyPlaceholder.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/components/CloudZeroCostTracking/CloudZeroIntegrationSettings.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/CloudZeroCostTracking/CloudZeroUpdateModal.tsx": { "no-restricted-imports": { "count": 1 @@ -3240,9 +3188,6 @@ "local/filename-pascal-case": { "count": 1 }, - "no-restricted-imports": { - "count": 2 - }, "react-hooks/immutability": { "count": 1 } @@ -3251,9 +3196,6 @@ "local/filename-pascal-case": { "count": 1 }, - "no-restricted-imports": { - "count": 2 - }, "prefer-const": { "count": 1 } @@ -4116,6 +4058,11 @@ "count": 1 } }, + "src/components/ui/radio-group.tsx": { + "local/filename-pascal-case": { + "count": 1 + } + }, "src/components/ui/scroll-area.tsx": { "local/filename-pascal-case": { "count": 1 diff --git a/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_dashboard.tsx b/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_dashboard.tsx index 51c0b85cedb..95b73d1aacb 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_dashboard.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_dashboard.tsx @@ -1,25 +1,24 @@ -import { - Card, - Col, - DateRangePickerValue, - Grid, - Icon, - MultiSelect, - MultiSelectItem, - Tab, - TabGroup, - TabList, - TabPanel, - TabPanels, - Text, -} from "@tremor/react"; +import { DateRangePickerValue } from "@tremor/react"; import React, { useEffect, useState } from "react"; import NotificationsManager from "@/components/molecules/notifications_manager"; import UsageDatePicker from "@/components/shared/usage_date_picker"; import { BarChart } from "@/components/shared/charts"; -import { Card as ChartCard, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Combobox, + ComboboxChip, + ComboboxChips, + ComboboxChipsInput, + ComboboxContent, + ComboboxEmpty, + ComboboxItem, + ComboboxList, + ComboboxValue, +} from "@/components/ui/combobox"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { RefreshIcon } from "@heroicons/react/outline"; +import { RefreshCw } from "lucide-react"; import { adminGlobalCacheActivity, cachingHealthCheckCall } from "@/components/networking"; // Import the new component @@ -258,31 +257,42 @@ const CacheDashboard: React.FC = ({ accessToken, token, userRole } }; + const statCards = [ + { label: "Cache Hit Ratio", value: `${cacheHitRatio}%` }, + { label: "Cache Hits", value: cachedResponses }, + { label: "Cached Completion Tokens", value: cachedTokens }, + ]; + return ( - - -
- Cache Analytics - Cache Health - Cache Settings - Coordination Redis -
+ +
+ + + Cache Analytics + + + Cache Health + + + Cache Settings + + + Coordination Redis + +
- {lastRefreshed && Last Refreshed: {lastRefreshed}} - + {lastRefreshed &&

Last Refreshed: {lastRefreshed}

} +
- - - - - +
+ + + + +

Analytics for LiteLLM's{" "} = ({ accessToken, token, userRole {" "} (cached input tokens from Anthropic, OpenAI, etc.) is not shown here; see "Prompt Caching Metrics" on the Usage page or individual requests in the Logs page. - - - - - {uniqueApiKeys.map((key) => ( - - {key} - - ))} - - - - - {uniqueModels.map((model) => ( - - {model} - - ))} - - - - { - setDateValue(value); - updateCachingData(value.from, value.to); - }} - /> - - +

-
- -

- Cache Hit Ratio -

-
-

- {cacheHitRatio}% -

-
-
- -

- Cache Hits -

-
-

- {cachedResponses} -

-
-
+
+ setSelectedApiKeys(keys)} + > + + + {(keys: string[]) => + keys.map((key) => ( + + {key} + + )) + } + + + + + No virtual keys found + + {(key: string) => ( + + {key} + + )} + + + - -

- Cached Completion Tokens -

-
-

- {cachedTokens} -

-
-
+ setSelectedModels(models)} + > + + + {(models: string[]) => + models.map((model) => ( + + {model} + + )) + } + + + + + No models found + + {(model: string) => ( + + {model} + + )} + + + + + { + setDateValue(value); + updateCachingData(value.from, value.to); + }} + />
- +
+ {statCards.map((stat) => ( + + +

{stat.label}

+
+

{stat.value}

+
+
+
+ ))} +
+ + Cache Hits vs API Requests @@ -387,9 +413,9 @@ const CacheDashboard: React.FC = ({ accessToken, token, userRole yAxisWidth={48} /> -
+ - + Cached Completion Tokens vs Generated Completion Tokens @@ -406,24 +432,27 @@ const CacheDashboard: React.FC = ({ accessToken, token, userRole yAxisWidth={48} /> - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + ); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_health.tsx b/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_health.tsx index 887c12a4f3d..85649c1cf26 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_health.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_health.tsx @@ -1,6 +1,7 @@ import React from "react"; -import { Text, Button, TabGroup, TabList, Tab, TabPanel, TabPanels } from "@tremor/react"; -import { CheckCircleIcon, XCircleIcon, ClipboardCopyIcon } from "@heroicons/react/outline"; +import { Button } from "@/components/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { CheckCircle2, XCircle, ClipboardCopy } from "lucide-react"; import { ResponseTimeIndicator } from "./response_time_indicator"; // Helper function to deep-parse a JSON string if possible @@ -30,22 +31,26 @@ const TableClickableErrorField: React.FC<{ label: string; value: string | null | }; return ( - + -
-
-
-
{label}
-
-                {isExpanded ? safeValue : truncated}
-              
+
{label}
+
{isExpanded ? safeValue : truncated}
-
@@ -151,112 +156,114 @@ const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => { }; return ( -
- - - Summary - Raw Response - +
+ + + + Summary + + + Raw Response + + - - -
-
- {response?.status === "healthy" ? ( - - ) : ( - + +
+
+ {response?.status === "healthy" ? ( + + ) : ( + + )} +

+ Cache Status: {response?.status || "unhealthy"} +

+
+ + + + {/* Show error message if present */} + {errorDetails && ( + <> + + + + + + )} - - Cache Status: {response?.status || "unhealthy"} - - -
+ Error Details +
- - {/* Show error message if present */} - {errorDetails && ( - <> - - - - - - - )} + {/* Always show cache details, regardless of error state */} + + + + + + + - {/* Always show cache details, regardless of error state */} - - - - - - - + {/* Redis Details Section */} + {parsedLitellmParams?.type === "redis" && ( + <> + + + + + + + + + + )} + +
- Error Details -
+ Cache Details +
- Cache Details -
+ Redis Details +
+
+
- {/* Redis Details Section */} - {parsedLitellmParams?.type === "redis" && ( - <> - - - Redis Details - - - - - - - - - )} - - -
- - - -
-
-                {(() => {
-                  try {
-                    const data = {
-                      ...response,
-                      litellm_cache_params: parsedLitellmParams,
-                      health_check_cache_params: parsedRedisParams,
-                    };
-                    // First parse any string JSON values
-                    const prettyData = JSON.parse(
-                      JSON.stringify(data, (key, value) => {
-                        if (typeof value === "string") {
-                          try {
-                            return JSON.parse(value);
-                          } catch {
-                            return value;
-                          }
+        
+          
+
+              {(() => {
+                try {
+                  const data = {
+                    ...response,
+                    litellm_cache_params: parsedLitellmParams,
+                    health_check_cache_params: parsedRedisParams,
+                  };
+                  // First parse any string JSON values
+                  const prettyData = JSON.parse(
+                    JSON.stringify(data, (key, value) => {
+                      if (typeof value === "string") {
+                        try {
+                          return JSON.parse(value);
+                        } catch {
+                          return value;
                         }
-                        return value;
-                      }),
-                    );
-                    // Then stringify with proper formatting
-                    return JSON.stringify(prettyData, null, 2);
-                  } catch (e) {
-                    return "Error formatting JSON: " + (e as Error).message;
-                  }
-                })()}
-              
-
- - - + } + return value; + }), + ); + // Then stringify with proper formatting + return JSON.stringify(prettyData, null, 2); + } catch (e) { + return "Error formatting JSON: " + (e as Error).message; + } + })()} +
+
+ +
); }; @@ -282,11 +289,7 @@ export const CacheHealthTab: React.FC<{ return (
- diff --git a/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_settings/RedisTypeSelector.tsx b/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_settings/RedisTypeSelector.tsx index fbca7ab5a97..40f553e6fde 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_settings/RedisTypeSelector.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_settings/RedisTypeSelector.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Select, SelectItem } from "@tremor/react"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; interface RedisTypeSelectorProps { redisType: string; @@ -7,17 +7,30 @@ interface RedisTypeSelectorProps { onTypeChange: (type: string) => void; } +const REDIS_TYPE_LABELS: Readonly> = { + node: "Node (Single Instance)", + cluster: "Cluster", + sentinel: "Sentinel", + semantic: "Semantic", +}; + const RedisTypeSelector: React.FC = ({ redisType, redisTypeDescriptions, onTypeChange }) => { return (
- - value !== null && onTypeChange(value)}> + + {REDIS_TYPE_LABELS[redisType] ?? redisType} + + + {Object.entries(REDIS_TYPE_LABELS).map(([value, label]) => ( + + {label} + + ))} + -

+

{redisTypeDescriptions[redisType] || "Select the type of Redis deployment you're using"}

diff --git a/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/coordination_redis_settings/CoordinationRedisTypeSelector.tsx b/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/coordination_redis_settings/CoordinationRedisTypeSelector.tsx index daab8505890..e6385e83649 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/coordination_redis_settings/CoordinationRedisTypeSelector.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/coordination_redis_settings/CoordinationRedisTypeSelector.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Select } from "antd"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { COORDINATION_REDIS_TYPES, COORDINATION_REDIS_TYPE_DESCRIPTIONS, @@ -12,21 +12,24 @@ interface CoordinationRedisTypeSelectorProps { onTypeChange: (type: CoordinationRedisType) => void; } -const OPTIONS = COORDINATION_REDIS_TYPES.map((type) => ({ value: type, label: COORDINATION_REDIS_TYPE_LABELS[type] })); - const CoordinationRedisTypeSelector: React.FC = ({ redisType, onTypeChange }) => (
-
); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/ai_suggestion_modal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/ai_suggestion_modal.tsx index 34204624702..cbf24b425f4 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/ai_suggestion_modal.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/ai_suggestion_modal.tsx @@ -1,13 +1,14 @@ import React, { useEffect, useMemo, useState } from "react"; -import { Modal, Spin, Checkbox, Select, Input, Typography, Tooltip } from "antd"; -import { Button, Card } from "@tremor/react"; -import { - CheckCircleOutlined, - CloseCircleOutlined, - InfoCircleOutlined, - DownOutlined, - RightOutlined, -} from "@ant-design/icons"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; +import { SearchSelect } from "@/components/shared/SearchSelect"; +import { CheckCircle2, ChevronDown, ChevronRight, Info, XCircle } from "lucide-react"; import { suggestPolicyTemplates, modelHubCall, @@ -15,9 +16,6 @@ import { enrichPolicyTemplateStream, } from "@/components/networking"; -const { TextArea } = Input; -const { Text } = Typography; - interface SuggestedTemplate { template_id: string; reason: string; @@ -427,7 +425,7 @@ const AiSuggestionModal: React.FC = ({
toggleTemplate(suggestion.template_id)} + onCheckedChange={() => toggleTemplate(suggestion.template_id)} className="mt-0.5" />
@@ -447,20 +445,25 @@ const AiSuggestionModal: React.FC = ({ )} {template.estimated_latency_ms != null && ( - - + + } > +{template.estimated_latency_ms <= 1 ? "<1" : template.estimated_latency_ms}ms latency - + + Estimated latency overhead added to each request )}
-

{template.description}

+

{template.description}

{template.guardrails && template.guardrails.slice(0, 4).map((g: string) => ( @@ -476,7 +479,7 @@ const AiSuggestionModal: React.FC = ({ )}
- +

{suggestion.reason}

@@ -490,7 +493,7 @@ const AiSuggestionModal: React.FC = ({ {explanation && (
- + Why these templates @@ -555,7 +558,7 @@ const AiSuggestionModal: React.FC = ({ >
{hasEnrichedGuardrails ? ( - + ) : ( = ({
setEnrichBrandName(e.target.value)} - onPressEnter={() => enrichBrandName.trim() && handleEnrichCompetitors()} + onKeyDown={(e) => { + if (e.key === "Enter" && enrichBrandName.trim() && !isEnriching) handleEnrichCompetitors(); + }} className="flex-1" /> -
{isEnriching && enrichStatusMessage && ( -
- +
+ {enrichStatusMessage}
)} {hasEnrichedGuardrails && (
- + Competitor names loaded for {enrichBrandName}
)} @@ -631,33 +630,29 @@ const AiSuggestionModal: React.FC = ({
- - + + } /> + Press Enter to submit. Use Shift+Enter for new line.
- Characters: {testInputText.length} + Characters: {testInputText.length}
-