mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): migrate cache_health to shadcn
- @tremor TabGroup/TabList/Tab/TabPanel/TabPanels/Text/Button \u2192 shadcn Tabs/TabsList/TabsTrigger/TabsContent + Button. - @heroicons CheckCircleIcon/XCircleIcon/ClipboardCopyIcon \u2192 lucide CheckCircle/XCircle/ClipboardCopy. - Raw text-gray-* / bg-gray-* / text-green-500 / text-red-500 \u2192 semantic tokens (text-muted-foreground/bg-muted/text-foreground/text-emerald-500/ text-destructive). Status badge uses non-semantic emerald/red because health/error is a categorical signal. - Replace hardcoded indigo primary button gradient with default Button (now driven by shadcn primary token). Gates: TS \u2713 | Build \u2713. Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
d553000e66
commit
74c36c920e
1 changed files with 191 additions and 142 deletions
|
|
@ -1,9 +1,10 @@
|
|||
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 { CheckCircle, ClipboardCopy, XCircle } from "lucide-react";
|
||||
import { ResponseTimeIndicator } from "./response_time_indicator";
|
||||
|
||||
// Helper function to deep-parse a JSON string if possible
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const deepParse = (input: any) => {
|
||||
let parsed = input;
|
||||
if (typeof parsed === "string") {
|
||||
|
|
@ -16,12 +17,15 @@ const deepParse = (input: any) => {
|
|||
return parsed;
|
||||
};
|
||||
|
||||
// TableClickableErrorField component with copy-to-clipboard functionality
|
||||
const TableClickableErrorField: React.FC<{ label: string; value: string | null | undefined }> = ({ label, value }) => {
|
||||
const TableClickableErrorField: React.FC<{
|
||||
label: string;
|
||||
value: string | null | undefined;
|
||||
}> = ({ label, value }) => {
|
||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
const [, setCopied] = React.useState(false);
|
||||
const safeValue = value?.toString() || "N/A";
|
||||
const truncated = safeValue.length > 50 ? safeValue.substring(0, 50) + "..." : safeValue;
|
||||
const truncated =
|
||||
safeValue.length > 50 ? safeValue.substring(0, 50) + "..." : safeValue;
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(safeValue);
|
||||
|
|
@ -30,22 +34,29 @@ const TableClickableErrorField: React.FC<{ label: string; value: string | null |
|
|||
};
|
||||
|
||||
return (
|
||||
<tr className="hover:bg-gray-50">
|
||||
<tr className="hover:bg-muted">
|
||||
<td className="px-4 py-2 align-top" colSpan={2}>
|
||||
<div className="flex items-center justify-between group">
|
||||
<div className="flex items-center flex-1">
|
||||
<button onClick={() => setIsExpanded(!isExpanded)} className="text-gray-400 hover:text-gray-600 mr-2">
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="text-muted-foreground hover:text-foreground mr-2"
|
||||
>
|
||||
{isExpanded ? "▼" : "▶"}
|
||||
</button>
|
||||
<div>
|
||||
<div className="text-sm text-gray-600">{label}</div>
|
||||
<pre className="mt-1 text-sm font-mono text-gray-800 whitespace-pre-wrap">
|
||||
<div className="text-sm text-muted-foreground">{label}</div>
|
||||
<pre className="mt-1 text-sm font-mono text-foreground whitespace-pre-wrap">
|
||||
{isExpanded ? safeValue : truncated}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={handleCopy} className="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-gray-600">
|
||||
<ClipboardCopyIcon className="h-4 w-4" />
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-foreground"
|
||||
aria-label="Copy"
|
||||
>
|
||||
<ClipboardCopy className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
|
|
@ -53,7 +64,6 @@ const TableClickableErrorField: React.FC<{ label: string; value: string | null |
|
|||
);
|
||||
};
|
||||
|
||||
// Add new interface for Redis details
|
||||
interface RedisDetails {
|
||||
redis_host?: string;
|
||||
redis_port?: string;
|
||||
|
|
@ -62,36 +72,42 @@ interface RedisDetails {
|
|||
namespace?: string;
|
||||
}
|
||||
|
||||
// Add new interface for Error Details
|
||||
interface ErrorDetails {
|
||||
message: string;
|
||||
traceback: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
litellm_params?: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
health_check_cache_params?: any;
|
||||
}
|
||||
|
||||
// Update HealthCheckDetails component to handle errors
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => {
|
||||
// Initialize with safe default values
|
||||
let errorDetails: ErrorDetails | null = null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let parsedLitellmParams: any = {};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let parsedRedisParams: any = {};
|
||||
|
||||
try {
|
||||
if (response?.error) {
|
||||
try {
|
||||
const errorMessage =
|
||||
typeof response.error.message === "string" ? JSON.parse(response.error.message) : response.error.message;
|
||||
typeof response.error.message === "string"
|
||||
? JSON.parse(response.error.message)
|
||||
: response.error.message;
|
||||
|
||||
errorDetails = {
|
||||
message: errorMessage?.message || "Unknown error",
|
||||
traceback: errorMessage?.traceback || "No traceback available",
|
||||
litellm_params: errorMessage?.litellm_cache_params || {},
|
||||
health_check_cache_params: errorMessage?.health_check_cache_params || {},
|
||||
health_check_cache_params:
|
||||
errorMessage?.health_check_cache_params || {},
|
||||
};
|
||||
|
||||
parsedLitellmParams = deepParse(errorDetails.litellm_params) || {};
|
||||
parsedRedisParams = deepParse(errorDetails.health_check_cache_params) || {};
|
||||
parsedRedisParams =
|
||||
deepParse(errorDetails.health_check_cache_params) || {};
|
||||
} catch (e) {
|
||||
console.warn("Error parsing error details:", e);
|
||||
errorDetails = {
|
||||
|
|
@ -107,167 +123,204 @@ const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => {
|
|||
}
|
||||
} catch (e) {
|
||||
console.warn("Error in response parsing:", e);
|
||||
// Provide safe fallback values
|
||||
parsedLitellmParams = {};
|
||||
parsedRedisParams = {};
|
||||
}
|
||||
|
||||
// Safely extract Redis details with fallbacks
|
||||
const redisDetails: RedisDetails = {
|
||||
redis_host:
|
||||
parsedRedisParams?.redis_client?.connection_pool?.connection_kwargs?.host ||
|
||||
parsedRedisParams?.redis_async_client?.connection_pool?.connection_kwargs?.host ||
|
||||
parsedRedisParams?.redis_client?.connection_pool?.connection_kwargs
|
||||
?.host ||
|
||||
parsedRedisParams?.redis_async_client?.connection_pool?.connection_kwargs
|
||||
?.host ||
|
||||
parsedRedisParams?.connection_kwargs?.host ||
|
||||
parsedRedisParams?.host ||
|
||||
"N/A",
|
||||
|
||||
redis_port:
|
||||
parsedRedisParams?.redis_client?.connection_pool?.connection_kwargs?.port ||
|
||||
parsedRedisParams?.redis_async_client?.connection_pool?.connection_kwargs?.port ||
|
||||
parsedRedisParams?.redis_client?.connection_pool?.connection_kwargs
|
||||
?.port ||
|
||||
parsedRedisParams?.redis_async_client?.connection_pool?.connection_kwargs
|
||||
?.port ||
|
||||
parsedRedisParams?.connection_kwargs?.port ||
|
||||
parsedRedisParams?.port ||
|
||||
"N/A",
|
||||
|
||||
redis_version: parsedRedisParams?.redis_version || "N/A",
|
||||
|
||||
startup_nodes: (() => {
|
||||
try {
|
||||
if (parsedRedisParams?.redis_kwargs?.startup_nodes) {
|
||||
return JSON.stringify(parsedRedisParams.redis_kwargs.startup_nodes);
|
||||
}
|
||||
const host =
|
||||
parsedRedisParams?.redis_client?.connection_pool?.connection_kwargs?.host ||
|
||||
parsedRedisParams?.redis_async_client?.connection_pool?.connection_kwargs?.host;
|
||||
parsedRedisParams?.redis_client?.connection_pool?.connection_kwargs
|
||||
?.host ||
|
||||
parsedRedisParams?.redis_async_client?.connection_pool
|
||||
?.connection_kwargs?.host;
|
||||
const port =
|
||||
parsedRedisParams?.redis_client?.connection_pool?.connection_kwargs?.port ||
|
||||
parsedRedisParams?.redis_async_client?.connection_pool?.connection_kwargs?.port;
|
||||
parsedRedisParams?.redis_client?.connection_pool?.connection_kwargs
|
||||
?.port ||
|
||||
parsedRedisParams?.redis_async_client?.connection_pool
|
||||
?.connection_kwargs?.port;
|
||||
return host && port ? JSON.stringify([{ host, port }]) : "N/A";
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return "N/A";
|
||||
}
|
||||
})(),
|
||||
|
||||
namespace: parsedRedisParams?.namespace || "N/A",
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<TabGroup>
|
||||
<TabList className="border-b border-gray-200 px-4">
|
||||
<Tab className="px-4 py-2 text-sm font-medium text-gray-600 hover:text-gray-800">Summary</Tab>
|
||||
<Tab className="px-4 py-2 text-sm font-medium text-gray-600 hover:text-gray-800">Raw Response</Tab>
|
||||
</TabList>
|
||||
<div className="bg-background rounded-lg shadow border border-border">
|
||||
<Tabs defaultValue="summary">
|
||||
<div className="border-b border-border px-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="summary">Summary</TabsTrigger>
|
||||
<TabsTrigger value="raw">Raw Response</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel className="p-4">
|
||||
<div>
|
||||
<div className="flex items-center mb-6">
|
||||
{response?.status === "healthy" ? (
|
||||
<CheckCircleIcon className="h-5 w-5 text-green-500 mr-2" />
|
||||
) : (
|
||||
<XCircleIcon className="h-5 w-5 text-red-500 mr-2" />
|
||||
<TabsContent value="summary" className="p-4">
|
||||
<div>
|
||||
<div className="flex items-center mb-6">
|
||||
{response?.status === "healthy" ? (
|
||||
<CheckCircle className="h-5 w-5 text-emerald-500 mr-2" />
|
||||
) : (
|
||||
<XCircle className="h-5 w-5 text-red-500 mr-2" />
|
||||
)}
|
||||
<span
|
||||
className={`text-sm font-medium ${
|
||||
response?.status === "healthy"
|
||||
? "text-emerald-500"
|
||||
: "text-red-500"
|
||||
}`}
|
||||
>
|
||||
Cache Status: {response?.status || "unhealthy"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<table className="w-full border-collapse">
|
||||
<tbody>
|
||||
{errorDetails && (
|
||||
<>
|
||||
<tr>
|
||||
<td
|
||||
colSpan={2}
|
||||
className="pt-4 pb-2 font-semibold text-destructive"
|
||||
>
|
||||
Error Details
|
||||
</td>
|
||||
</tr>
|
||||
<TableClickableErrorField
|
||||
label="Error Message"
|
||||
value={errorDetails.message}
|
||||
/>
|
||||
<TableClickableErrorField
|
||||
label="Traceback"
|
||||
value={errorDetails.traceback}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Text
|
||||
className={`text-sm font-medium ${response?.status === "healthy" ? "text-green-500" : "text-red-500"}`}
|
||||
>
|
||||
Cache Status: {response?.status || "unhealthy"}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<table className="w-full border-collapse">
|
||||
<tbody>
|
||||
{/* Show error message if present */}
|
||||
{errorDetails && (
|
||||
<>
|
||||
<tr>
|
||||
<td colSpan={2} className="pt-4 pb-2 font-semibold text-red-600">
|
||||
Error Details
|
||||
</td>
|
||||
</tr>
|
||||
<TableClickableErrorField label="Error Message" value={errorDetails.message} />
|
||||
<TableClickableErrorField label="Traceback" value={errorDetails.traceback} />
|
||||
</>
|
||||
)}
|
||||
<tr>
|
||||
<td colSpan={2} className="pt-4 pb-2 font-semibold">
|
||||
Cache Details
|
||||
</td>
|
||||
</tr>
|
||||
<TableClickableErrorField
|
||||
label="Cache Configuration"
|
||||
value={String(parsedLitellmParams?.type)}
|
||||
/>
|
||||
<TableClickableErrorField
|
||||
label="Ping Response"
|
||||
value={String(response.ping_response)}
|
||||
/>
|
||||
<TableClickableErrorField
|
||||
label="Set Cache Response"
|
||||
value={response.set_cache_response || "N/A"}
|
||||
/>
|
||||
<TableClickableErrorField
|
||||
label="litellm_settings.cache_params"
|
||||
value={JSON.stringify(parsedLitellmParams, null, 2)}
|
||||
/>
|
||||
|
||||
{/* Always show cache details, regardless of error state */}
|
||||
<tr>
|
||||
<td colSpan={2} className="pt-4 pb-2 font-semibold">
|
||||
Cache Details
|
||||
</td>
|
||||
</tr>
|
||||
<TableClickableErrorField label="Cache Configuration" value={String(parsedLitellmParams?.type)} />
|
||||
<TableClickableErrorField label="Ping Response" value={String(response.ping_response)} />
|
||||
<TableClickableErrorField label="Set Cache Response" value={response.set_cache_response || "N/A"} />
|
||||
<TableClickableErrorField
|
||||
label="litellm_settings.cache_params"
|
||||
value={JSON.stringify(parsedLitellmParams, null, 2)}
|
||||
/>
|
||||
{parsedLitellmParams?.type === "redis" && (
|
||||
<>
|
||||
<tr>
|
||||
<td colSpan={2} className="pt-4 pb-2 font-semibold">
|
||||
Redis Details
|
||||
</td>
|
||||
</tr>
|
||||
<TableClickableErrorField
|
||||
label="Redis Host"
|
||||
value={redisDetails.redis_host || "N/A"}
|
||||
/>
|
||||
<TableClickableErrorField
|
||||
label="Redis Port"
|
||||
value={redisDetails.redis_port || "N/A"}
|
||||
/>
|
||||
<TableClickableErrorField
|
||||
label="Redis Version"
|
||||
value={redisDetails.redis_version || "N/A"}
|
||||
/>
|
||||
<TableClickableErrorField
|
||||
label="Startup Nodes"
|
||||
value={redisDetails.startup_nodes || "N/A"}
|
||||
/>
|
||||
<TableClickableErrorField
|
||||
label="Namespace"
|
||||
value={redisDetails.namespace || "N/A"}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Redis Details Section */}
|
||||
{parsedLitellmParams?.type === "redis" && (
|
||||
<>
|
||||
<tr>
|
||||
<td colSpan={2} className="pt-4 pb-2 font-semibold">
|
||||
Redis Details
|
||||
</td>
|
||||
</tr>
|
||||
<TableClickableErrorField label="Redis Host" value={redisDetails.redis_host || "N/A"} />
|
||||
<TableClickableErrorField label="Redis Port" value={redisDetails.redis_port || "N/A"} />
|
||||
<TableClickableErrorField label="Redis Version" value={redisDetails.redis_version || "N/A"} />
|
||||
<TableClickableErrorField label="Startup Nodes" value={redisDetails.startup_nodes || "N/A"} />
|
||||
<TableClickableErrorField label="Namespace" value={redisDetails.namespace || "N/A"} />
|
||||
</>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel className="p-4">
|
||||
<div className="bg-gray-50 rounded-md p-4 font-mono text-sm">
|
||||
<pre className="whitespace-pre-wrap break-words overflow-auto max-h-[500px]">
|
||||
{(() => {
|
||||
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;
|
||||
}
|
||||
<TabsContent value="raw" className="p-4">
|
||||
<div className="bg-muted rounded-md p-4 font-mono text-sm">
|
||||
<pre className="whitespace-pre-wrap break-words overflow-auto max-h-[500px]">
|
||||
{(() => {
|
||||
try {
|
||||
const data = {
|
||||
...response,
|
||||
litellm_cache_params: parsedLitellmParams,
|
||||
health_check_cache_params: parsedRedisParams,
|
||||
};
|
||||
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;
|
||||
}
|
||||
})()}
|
||||
</pre>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
}
|
||||
return value;
|
||||
}),
|
||||
);
|
||||
return JSON.stringify(prettyData, null, 2);
|
||||
} catch (e) {
|
||||
return "Error formatting JSON: " + (e as Error).message;
|
||||
}
|
||||
})()}
|
||||
</pre>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CacheHealthTab: React.FC<{
|
||||
accessToken: string | null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
healthCheckResponse: any;
|
||||
runCachingHealthCheck: () => void;
|
||||
responseTimeMs?: number | null;
|
||||
}> = ({ accessToken, healthCheckResponse, runCachingHealthCheck, responseTimeMs }) => {
|
||||
const [localResponseTimeMs, setLocalResponseTimeMs] = React.useState<number | null>(null);
|
||||
}> = ({ healthCheckResponse, runCachingHealthCheck }) => {
|
||||
const [localResponseTimeMs, setLocalResponseTimeMs] = React.useState<
|
||||
number | null
|
||||
>(null);
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
|
||||
const handleHealthCheck = async () => {
|
||||
|
|
@ -282,11 +335,7 @@ export const CacheHealthTab: React.FC<{
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
onClick={handleHealthCheck}
|
||||
disabled={isLoading}
|
||||
className="bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-400 text-white text-sm px-4 py-2 rounded-md"
|
||||
>
|
||||
<Button onClick={handleHealthCheck} disabled={isLoading}>
|
||||
{isLoading ? "Running Health Check..." : "Run Health Check"}
|
||||
</Button>
|
||||
<ResponseTimeIndicator responseTimeMs={localResponseTimeMs} />
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue