From 8f47b8775f2c2493c05ee2be861ca3f8667cfc0e Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Thu, 18 Dec 2025 18:23:38 -0500 Subject: [PATCH] feat(ui): redesign error logs monitor with split view layout - Replace dropdown accordion with master-detail split view - Add log list panel (240px) on left with selection state - Add content viewer panel on right for better readability - Add demo mode prop for UI testing - Auto-select first log via useMemo/derived state --- ui/src/components/error-logs-monitor.tsx | 361 +++++++++++++++-------- 1 file changed, 243 insertions(+), 118 deletions(-) diff --git a/ui/src/components/error-logs-monitor.tsx b/ui/src/components/error-logs-monitor.tsx index 74418e3a..b399a67b 100644 --- a/ui/src/components/error-logs-monitor.tsx +++ b/ui/src/components/error-logs-monitor.tsx @@ -1,11 +1,11 @@ /** * Error Logs Monitor Component * - * Displays CLIProxyAPI error logs with expandable details. - * Designed to complement the AuthMonitor on the Home page. + * Displays CLIProxyAPI error logs with master-detail split view. + * Log list on left, content panel on right for better readability. */ -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import { useCliproxyErrorLogs, useCliproxyErrorLogContent } from '@/hooks/use-cliproxy-stats'; import { useCliproxyStatus } from '@/hooks/use-cliproxy-stats'; import { cn, STATUS_COLORS } from '@/lib/utils'; @@ -13,14 +13,85 @@ import { Skeleton } from '@/components/ui/skeleton'; import { ScrollArea } from '@/components/ui/scroll-area'; import { AlertTriangle, - ChevronDown, - ChevronRight, FileWarning, Clock, FileText, XCircle, + FlaskConical, + Terminal, } from 'lucide-react'; +/** Demo mode mock data */ +const DEMO_ERROR_LOGS = [ + { + name: 'error-v1-chat-completions-2025-12-18T17-45-23.log', + size: 4523, + modified: Math.floor(Date.now() / 1000) - 120, + }, + { + name: 'error-v1-messages-2025-12-18T17-30-15.log', + size: 8912, + modified: Math.floor(Date.now() / 1000) - 900, + }, + { + name: 'error-v1-chat-completions-2025-12-18T16-22-08.log', + size: 2341, + modified: Math.floor(Date.now() / 1000) - 5400, + }, + { + name: 'error-v1-models-2025-12-18T14-10-55.log', + size: 1024, + modified: Math.floor(Date.now() / 1000) - 12600, + }, +]; + +const DEMO_LOG_CONTENT = `================================================================================ +REQUEST ERROR LOG +================================================================================ +Timestamp: 2025-12-18T17:45:23Z +Endpoint: /v1/chat/completions +Method: POST +Status: 429 Too Many Requests + +-------------------------------------------------------------------------------- +REQUEST HEADERS +-------------------------------------------------------------------------------- +Content-Type: application/json +Authorization: Bearer ccs-internal-managed +User-Agent: claude-code/1.0 + +-------------------------------------------------------------------------------- +REQUEST BODY +-------------------------------------------------------------------------------- +{ + "model": "gemini-claude-opus-4-5-thinking", + "messages": [ + {"role": "user", "content": "Explain quantum computing"} + ], + "max_tokens": 4096, + "stream": true +} + +-------------------------------------------------------------------------------- +RESPONSE +-------------------------------------------------------------------------------- +{ + "error": { + "message": "Rate limit exceeded. Please retry after 60 seconds.", + "type": "rate_limit_error", + "code": "rate_limit_exceeded" + } +} + +-------------------------------------------------------------------------------- +UPSTREAM API ERROR +-------------------------------------------------------------------------------- +Provider: gemini +Account: user@gmail.com +Quota: Daily limit reached (1500/1500 requests) +Retry-After: 60 +================================================================================`; + /** Format file size in human-readable format */ function formatSize(bytes: number): string { if (bytes < 1024) return `${bytes} B`; @@ -39,7 +110,6 @@ function formatRelativeTime(unixSeconds: number): string { /** Parse error log filename to extract endpoint and timestamp */ function parseErrorLogName(name: string): { endpoint: string; timestamp: string } { - // Format: error-v1-chat-completions-2025-01-15T10-30-00.log const match = name.match(/^error-(.+)-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2})\.log$/); if (match) { const endpoint = match[1].replace(/-/g, '/'); @@ -49,84 +119,181 @@ function parseErrorLogName(name: string): { endpoint: string; timestamp: string return { endpoint: name, timestamp: '' }; } -/** Error log content viewer with syntax highlighting */ -function ErrorLogContent({ name }: { name: string }) { - const { data: content, isLoading, error } = useCliproxyErrorLogContent(name); +/** Log content panel component */ +function LogContentPanel({ name, demo = false }: { name: string | null; demo?: boolean }) { + const { data: content, isLoading, error } = useCliproxyErrorLogContent(demo ? null : name); + // No log selected + if (!name) { + return ( +
+
+ +

Select a log to view details

+
+
+ ); + } + + // Demo mode + if (demo) { + const { endpoint } = parseErrorLogName(name); + return ( +
+
+ + {endpoint} +
+ +
+            {DEMO_LOG_CONTENT}
+          
+
+
+ ); + } + + // Loading state if (isLoading) { return ( -
+
+
); } + // Error or no content if (error || !content) { return ( -
Failed to load error log content
+
+

Failed to load log content

+
); } + // Show content + const { endpoint } = parseErrorLogName(name); return ( - -
-        {content}
-      
-
+
+
+ + {endpoint} +
+ +
+          {content}
+        
+
+
); } -export function ErrorLogsMonitor() { - const { data: status, isLoading: isStatusLoading } = useCliproxyStatus(); - const { data: logs, isLoading, error } = useCliproxyErrorLogs(status?.running ?? false); - const [expandedLog, setExpandedLog] = useState(null); +/** Error log item in the list */ +interface ErrorLogItemProps { + name: string; + size: number; + modified: number; + isSelected: boolean; + onClick: () => void; +} - // Don't show while status is loading or if proxy not running - if (isStatusLoading) { - return null; - } +function ErrorLogItem({ name, size, modified, isSelected, onClick }: ErrorLogItemProps) { + const { endpoint, timestamp } = parseErrorLogName(name); - if (!status?.running) { - return null; - } - - if (isLoading) { - return ( -
-
- - + return ( + + ); +} + +export function ErrorLogsMonitor({ demo = false }: { demo?: boolean } = {}) { + const { data: status, isLoading: isStatusLoading } = useCliproxyStatus(); + const { + data: logs, + isLoading, + error, + } = useCliproxyErrorLogs(demo ? false : (status?.running ?? false)); + // Use demo data or real data + const displayLogs = demo ? DEMO_ERROR_LOGS : logs; + + // Compute default selection (first log name or null) + const defaultLogName = useMemo(() => displayLogs?.[0]?.name ?? null, [displayLogs]); + + // Use controlled selection that defaults to first log + const [selectedLog, setSelectedLog] = useState(null); + + // Effective selection: use user selection if available, otherwise default + const effectiveSelection = selectedLog ?? defaultLogName; + + // Non-demo mode guards + if (!demo) { + if (isStatusLoading) return null; + if (!status?.running) return null; + if (isLoading) { + return ( +
+
+ + +
+
+ {[1, 2, 3].map((i) => ( + + ))} +
+
+ ); + } + if (!logs || logs.length === 0) return null; } - // Don't show if no errors (good state) - if (!logs || logs.length === 0) { - return null; - } - - const errorCount = logs.length; + const errorCount = displayLogs?.length ?? 0; return (
- {/* Header with warning styling */} + {/* Header */}
-
- -
- Error Logs + + Error Logs {errorCount} failed request{errorCount !== 1 ? 's' : ''} + {demo && ( + + + DEMO + + )}
@@ -134,78 +301,36 @@ export function ErrorLogsMonitor() {
- {/* Error logs list */} - -
- {logs.slice(0, 10).map((log) => { - const isExpanded = expandedLog === log.name; - const { endpoint, timestamp } = parseErrorLogName(log.name); - - return ( -
- - - {/* Expandable content */} - {isExpanded && ( -
- -
- )} + {/* Split View: List (left) + Content (right) */} +
+ {/* Left Panel: Log List */} +
+ +
+ {displayLogs?.slice(0, 10).map((log) => ( + setSelectedLog(log.name)} + /> + ))} +
+ {(displayLogs?.length ?? 0) > 10 && ( +
+ Showing 10 of {displayLogs?.length} logs
- ); - })} + )} +
- {/* Show more indicator */} - {logs.length > 10 && ( -
- Showing 10 of {logs.length} error logs -
- )} - + {/* Right Panel: Log Content */} + +
- {/* Footer hint */} + {/* Footer error */} {error && (
{error.message}