From 2dae5d937224c33404df0fdf749e16c74317f728 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 09:40:07 +0000 Subject: [PATCH] feat(ui): migrate cache_dashboard chrome to shadcn (keep BarChart) - @tremor TabGroup/TabList/Tab/TabPanel/Card/Grid/Col/MultiSelect*/ Icon/Text/Subtitle + @heroicons RefreshIcon \u2192 shadcn Tabs/Card/ Button/Select/Badge + lucide RefreshCw/X + plain Tailwind grid. - ChipMultiSelect helper mirrors the chip-input pattern used in AccessGroupBaseForm / TagSelector / TeamSSOSettings. - @tremor/react BarChart retained for the two analytics charts (chart exemption). cache_dashboard.tsx added to the .eslintrc.json override list because the chart itself isn't in a /charts/ subdir. - Tremor font-size tokens (text-tremor-metric / text-tremor-default / text-tremor-content*) replaced with shadcn semantic / Tailwind equivalents (text-3xl / text-sm / text-muted-foreground / text-foreground). Gates: TS \u2713 | Build \u2713. Co-authored-by: yuneng-jiang --- ui/litellm-dashboard/.eslintrc.json | 3 +- .../src/components/cache_dashboard.tsx | 498 +++++++++--------- 2 files changed, 257 insertions(+), 244 deletions(-) diff --git a/ui/litellm-dashboard/.eslintrc.json b/ui/litellm-dashboard/.eslintrc.json index a629bf4cc4a..0b5b1e1f97a 100644 --- a/ui/litellm-dashboard/.eslintrc.json +++ b/ui/litellm-dashboard/.eslintrc.json @@ -33,7 +33,8 @@ "src/components/DebugWarningBanner.tsx", "src/components/SCIM.tsx", "src/components/logging_settings_view.tsx", - "src/components/model_group_alias_settings.tsx" + "src/components/model_group_alias_settings.tsx", + "src/components/cache_dashboard.tsx" ], "rules": { "litellm-ui/no-banned-ui-imports": "off", diff --git a/ui/litellm-dashboard/src/components/cache_dashboard.tsx b/ui/litellm-dashboard/src/components/cache_dashboard.tsx index 874cb43276e..d7d19e336f6 100644 --- a/ui/litellm-dashboard/src/components/cache_dashboard.tsx +++ b/ui/litellm-dashboard/src/components/cache_dashboard.tsx @@ -1,28 +1,28 @@ -import { - BarChart, - Card, - Col, - DateRangePickerValue, - Grid, - Icon, - MultiSelect, - MultiSelectItem, - Subtitle, - Tab, - TabGroup, - TabList, - TabPanel, - TabPanels, - Text, -} from "@tremor/react"; +import { BarChart } from "@tremor/react"; +import type { DateRangePickerValue } from "@tremor/react"; import React, { useEffect, useState } from "react"; +import { Card } from "@/components/ui/card"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs"; +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Badge } from "@/components/ui/badge"; +import { RefreshCw, X } from "lucide-react"; import NotificationsManager from "./molecules/notifications_manager"; import UsageDatePicker from "./shared/usage_date_picker"; -import { RefreshIcon } from "@heroicons/react/outline"; import { adminGlobalCacheActivity, cachingHealthCheckCall } from "./networking"; -// Import the new component import { CacheHealthTab } from "./cache_health"; import CacheSettings from "./cache_settings"; @@ -37,7 +37,6 @@ function valueFormatterNumbers(number: number) { notation: "compact", compactDisplay: "short", }); - return formatter.format(number); } @@ -57,8 +56,6 @@ interface cacheDataItem { total_rows: number; generated_completion_tokens: number; call_type: string; - - // Add other properties as needed } interface uiData { @@ -69,34 +66,72 @@ interface uiData { "Generated Completion Tokens": number; } -interface CacheHealthResponse { - status?: string; - cache_type?: string; - ping_response?: boolean; - set_cache_response?: string; - litellm_cache_params?: string; - error?: { - message: string; - type: string; - param: string; - code: string; - }; +/** + * Multi-select chips wrapper around shadcn Select. Used for the API-key / + * model filter rows. + */ +function ChipMultiSelect({ + placeholder, + options, + value, + onChange, +}: { + placeholder: string; + options: string[]; + value: string[]; + onChange: (next: string[]) => void; +}) { + const remaining = options.filter((o) => !value.includes(o)); + return ( +
+ + {value.length > 0 && ( +
+ {value.map((v) => ( + + {v} + + + ))} +
+ )} +
+ ); } -// Helper function to deep-parse a JSON string if possible -const deepParse = (input: any) => { - let parsed = input; - if (typeof parsed === "string") { - try { - parsed = JSON.parse(parsed); - } catch { - return parsed; - } - } - return parsed; -}; - -const CacheDashboard: React.FC = ({ accessToken, token, userRole, userID, premiumUser }) => { +const CacheDashboard: React.FC = ({ + accessToken, + userRole, + userID, +}) => { const [filteredData, setFilteredData] = useState([]); const [selectedApiKeys, setSelectedApiKeys] = useState([]); const [selectedModels, setSelectedModels] = useState([]); @@ -111,12 +146,11 @@ const CacheDashboard: React.FC = ({ accessToken, token, userRole }); const [lastRefreshed, setLastRefreshed] = useState(""); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const [healthCheckResponse, setHealthCheckResponse] = useState(""); useEffect(() => { - if (!accessToken || !dateValue) { - return; - } + if (!accessToken || !dateValue) return; const fetchData = async () => { const response = await adminGlobalCacheActivity( accessToken, @@ -126,87 +160,64 @@ const CacheDashboard: React.FC = ({ accessToken, token, userRole setData(response); }; fetchData(); - - const currentDate = new Date(); - setLastRefreshed(currentDate.toLocaleString()); + setLastRefreshed(new Date().toLocaleString()); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [accessToken]); - const uniqueApiKeys = Array.from(new Set(data.map((item) => item?.api_key ?? ""))); - const uniqueModels = Array.from(new Set(data.map((item) => item?.model ?? ""))); - const uniqueCallTypes = Array.from(new Set(data.map((item) => item?.call_type ?? ""))); + const uniqueApiKeys = Array.from( + new Set(data.map((item) => item?.api_key ?? "")), + ); + const uniqueModels = Array.from( + new Set(data.map((item) => item?.model ?? "")), + ); - const updateCachingData = async (startTime: Date | undefined, endTime: Date | undefined) => { - if (!startTime || !endTime || !accessToken) { - return; - } - - let new_cache_data = await adminGlobalCacheActivity( + const updateCachingData = async ( + startTime: Date | undefined, + endTime: Date | undefined, + ) => { + if (!startTime || !endTime || !accessToken) return; + const new_cache_data = await adminGlobalCacheActivity( accessToken, formatDateWithoutTZ(startTime), formatDateWithoutTZ(endTime), ); - setData(new_cache_data); }; useEffect(() => { - console.log("DATA IN CACHE DASHBOARD", data); let newData: cacheDataItem[] = data; if (selectedApiKeys.length > 0) { - newData = newData.filter((item) => selectedApiKeys.includes(item.api_key)); + newData = newData.filter((item) => + selectedApiKeys.includes(item.api_key), + ); } - if (selectedModels.length > 0) { newData = newData.filter((item) => selectedModels.includes(item.model)); } - /* - Data looks like this - [{"api_key":"sk-test-mock-key-001","call_type":"acompletion","model":"llama3-8b-8192","total_rows":13,"cache_hit_true_rows":0}, - {"api_key":"sk-test-mock-key-002","call_type":"None","model":"chatgpt-v-2","total_rows":1,"cache_hit_true_rows":0}, - {"api_key":"sk-test-mock-key-123","call_type":"acompletion","model":"gpt-3.5-turbo","total_rows":19,"cache_hit_true_rows":0}, - {"api_key":"sk-test-mock-key-123","call_type":"aimage_generation","model":"","total_rows":3,"cache_hit_true_rows":0}, - {"api_key":"sk-test-mock-key-003","call_type":"None","model":"chatgpt-v-2","total_rows":1,"cache_hit_true_rows":0}, - {"api_key":"sk-test-mock-key-004","call_type":"","model":"chatgpt-v-2","total_rows":1,"cache_hit_true_rows":0}, - {"api_key":"sk-test-mock-key-005","call_type":"","model":"chatgpt-v-2","total_rows":1,"cache_hit_true_rows":0}, - */ - - // What data we need for bar chat - // ui_data = [ - // { - // name: "Call Type", - // Cache hit: 20, - // LLM API requests: 10, - // } - // ] - - console.log("before processed data in cache dashboard", newData); - let llm_api_requests = 0; let cache_hits = 0; let cached_tokens = 0; const processedData = newData.reduce((acc: uiData[], item) => { - console.log("Processing item:", item); - - if (!item.call_type) { - console.log("Item has no call_type:", item); - item.call_type = "Unknown"; - } - - llm_api_requests += (item.total_rows || 0) - (item.cache_hit_true_rows || 0); + if (!item.call_type) item.call_type = "Unknown"; + llm_api_requests += + (item.total_rows || 0) - (item.cache_hit_true_rows || 0); cache_hits += item.cache_hit_true_rows || 0; cached_tokens += item.cached_completion_tokens || 0; - const existingItem = acc.find((i) => i.name === item.call_type); if (existingItem) { - existingItem["LLM API requests"] += (item.total_rows || 0) - (item.cache_hit_true_rows || 0); + existingItem["LLM API requests"] += + (item.total_rows || 0) - (item.cache_hit_true_rows || 0); existingItem["Cache hit"] += item.cache_hit_true_rows || 0; - existingItem["Cached Completion Tokens"] += item.cached_completion_tokens || 0; - existingItem["Generated Completion Tokens"] += item.generated_completion_tokens || 0; + existingItem["Cached Completion Tokens"] += + item.cached_completion_tokens || 0; + existingItem["Generated Completion Tokens"] += + item.generated_completion_tokens || 0; } else { acc.push({ name: item.call_type, - "LLM API requests": (item.total_rows || 0) - (item.cache_hit_true_rows || 0), + "LLM API requests": + (item.total_rows || 0) - (item.cache_hit_true_rows || 0), "Cache hit": item.cache_hit_true_rows || 0, "Cached Completion Tokens": item.cached_completion_tokens || 0, "Generated Completion Tokens": item.generated_completion_tokens || 0, @@ -215,48 +226,38 @@ const CacheDashboard: React.FC = ({ accessToken, token, userRole return acc; }, []); - // set header cache statistics setCachedResponses(valueFormatterNumbers(cache_hits)); setCachedTokens(valueFormatterNumbers(cached_tokens)); - let allRequests = cache_hits + llm_api_requests; - if (allRequests > 0) { - let cache_hit_ratio = ((cache_hits / allRequests) * 100).toFixed(2); - setCacheHitRatio(cache_hit_ratio); - } else { - setCacheHitRatio("0"); - } - + const allRequests = cache_hits + llm_api_requests; + setCacheHitRatio( + allRequests > 0 ? ((cache_hits / allRequests) * 100).toFixed(2) : "0", + ); setFilteredData(processedData); - - console.log("PROCESSED DATA IN CACHE DASHBOARD", processedData); }, [selectedApiKeys, selectedModels, dateValue, data]); const handleRefreshClick = () => { - // Update the 'lastRefreshed' state to the current date and time - const currentDate = new Date(); - setLastRefreshed(currentDate.toLocaleString()); + setLastRefreshed(new Date().toLocaleString()); }; const runCachingHealthCheck = async () => { try { NotificationsManager.info("Running cache health check..."); setHealthCheckResponse(""); - const response = await cachingHealthCheckCall(accessToken !== null ? accessToken : ""); - console.log("CACHING HEALTH CHECK RESPONSE", response); + const response = await cachingHealthCheckCall( + accessToken !== null ? accessToken : "", + ); setHealthCheckResponse(response); + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { console.error("Error running health check:", error); - let errorData; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let errorData: any; if (error && error.message) { try { - // Parse the error message which may contain a nested error layer. let parsedData = JSON.parse(error.message); - // If the parsed object is wrapped (e.g. { error: { ... } }), unwrap it. - if (parsedData.error) { - parsedData = parsedData.error; - } + if (parsedData.error) parsedData = parsedData.error; errorData = parsedData; - } catch (e) { + } catch { errorData = { message: error.message }; } } else { @@ -267,133 +268,144 @@ const CacheDashboard: React.FC = ({ accessToken, token, userRole }; return ( - - -
- Cache Analytics - Cache Health - Cache Settings -
+ +
+ + Cache Analytics + Cache Health + Cache Settings +
- {lastRefreshed && Last Refreshed: {lastRefreshed}} - + Last Refreshed: {lastRefreshed} + + )} +
- - - - - - - - {uniqueApiKeys.map((key) => ( - - {key} - - ))} - - - - - {uniqueModels.map((model) => ( - - {model} - - ))} - - - - { - setDateValue(value); - updateCachingData(value.from, value.to); - }} - /> - - +
-
- -

- Cache Hit Ratio -

-
-

- {cacheHitRatio}% -

-
-
- -

- Cache Hits -

-
-

- {cachedResponses} -

-
-
- - -

- Cached Tokens -

-
-

- {cachedTokens} -

-
-
+ + +
+
+
+
+ +
+
+ { + setDateValue(value); + updateCachingData(value.from, value.to); + }} + /> +
+
- Cache Hits vs API Requests - +
+ +

+ Cache Hit Ratio +

+
+

+ {cacheHitRatio}% +

+
+
+ +

+ Cache Hits +

+
+

+ {cachedResponses} +

+
+
+ +

+ Cached Tokens +

+
+

+ {cachedTokens} +

+
+
+
- Cached Completion Tokens vs Generated Completion Tokens - -
- - - + Cache Hits vs API Requests +

+ -
- - - - - + +

+ Cached Completion Tokens vs Generated Completion Tokens +

+ + +
+ + + + + + + ); };