Spaces:
Sleeping
Sleeping
import React from "react"; | |
import { Card, Text, Button, TabGroup, TabList, Tab, TabPanel, TabPanels } from "@tremor/react"; | |
import { CheckCircleIcon, XCircleIcon, ClipboardCopyIcon } from "@heroicons/react/outline"; | |
import { ResponseTimeIndicator } from "./response_time_indicator"; | |
// 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; | |
}; | |
// TableClickableErrorField component with copy-to-clipboard functionality | |
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 safeValue = value?.toString() || "N/A"; | |
const truncated = safeValue.length > 50 ? safeValue.substring(0, 50) + "..." : safeValue; | |
const handleCopy = () => { | |
navigator.clipboard.writeText(safeValue); | |
setCopied(true); | |
setTimeout(() => setCopied(false), 2000); | |
}; | |
return ( | |
<tr className="hover:bg-gray-50"> | |
<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" | |
> | |
{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"> | |
{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> | |
</div> | |
</td> | |
</tr> | |
); | |
}; | |
// Add new interface for Redis details | |
interface RedisDetails { | |
redis_host?: string; | |
redis_port?: string; | |
redis_version?: string; | |
startup_nodes?: string; | |
namespace?: string; | |
} | |
// Add new interface for Error Details | |
interface ErrorDetails { | |
message: string; | |
traceback: string; | |
litellm_params?: any; | |
health_check_cache_params?: any; | |
} | |
// Update HealthCheckDetails component to handle errors | |
const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => { | |
// Initialize with safe default values | |
let errorDetails: ErrorDetails | null = null; | |
let parsedLitellmParams: any = {}; | |
let parsedRedisParams: any = {}; | |
try { | |
if (response?.error) { | |
try { | |
const errorMessage = 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 || {} | |
}; | |
parsedLitellmParams = deepParse(errorDetails.litellm_params) || {}; | |
parsedRedisParams = deepParse(errorDetails.health_check_cache_params) || {}; | |
} catch (e) { | |
console.warn("Error parsing error details:", e); | |
errorDetails = { | |
message: String(response.error.message || 'Unknown error'), | |
traceback: 'Error parsing details', | |
litellm_params: {}, | |
health_check_cache_params: {} | |
}; | |
} | |
} else { | |
parsedLitellmParams = deepParse(response?.litellm_cache_params) || {}; | |
parsedRedisParams = deepParse(response?.health_check_cache_params) || {}; | |
} | |
} 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?.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?.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; | |
const 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) { | |
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> | |
<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" /> | |
)} | |
<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} | |
/> | |
</> | |
)} | |
{/* 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)} | |
/> | |
{/* 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; | |
} | |
} | |
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> | |
</div> | |
); | |
}; | |
export const CacheHealthTab: React.FC<{ | |
accessToken: string | null; | |
healthCheckResponse: any; | |
runCachingHealthCheck: () => void; | |
responseTimeMs?: number | null; | |
}> = ({ accessToken, healthCheckResponse, runCachingHealthCheck, responseTimeMs }) => { | |
const [localResponseTimeMs, setLocalResponseTimeMs] = React.useState<number | null>(null); | |
const [isLoading, setIsLoading] = React.useState<boolean>(false); | |
const handleHealthCheck = async () => { | |
setIsLoading(true); | |
const startTime = performance.now(); | |
await runCachingHealthCheck(); | |
const endTime = performance.now(); | |
setLocalResponseTimeMs(endTime - startTime); | |
setIsLoading(false); | |
}; | |
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" | |
> | |
{isLoading ? "Running Health Check..." : "Run Health Check"} | |
</Button> | |
<ResponseTimeIndicator responseTimeMs={localResponseTimeMs} /> | |
</div> | |
{healthCheckResponse && ( | |
<HealthCheckDetails response={healthCheckResponse} /> | |
)} | |
</div> | |
); | |
}; |