You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

572 lines
22 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { adminApi } from '@/lib/api';
import {
AlertTriangle,
Filter,
RefreshCw,
CheckCircle,
XCircle,
Clock,
ChevronDown,
ChevronUp,
Eye,
Trash2,
Calendar
} from 'lucide-react';
interface ErrorLog {
id: number;
request_id: string;
endpoint: string;
method: string;
path: string;
query_params?: any;
request_body?: any;
error_type: string;
error_message: string;
error_detail?: any;
status_code: number;
stack_trace?: string;
user_agent?: string;
client_ip?: string;
response_time_ms?: number;
is_resolved: boolean;
resolved_at?: string;
resolution_notes?: string;
created_at: string;
}
interface ErrorStats {
total_errors: number;
resolved_errors: number;
unresolved_errors: number;
resolution_rate: number;
errors_by_type: Record<string, number>;
errors_by_status_code: Record<string, number>;
errors_by_endpoint: Record<string, number>;
average_response_time_ms: number;
}
interface ErrorLogListResponse {
items: ErrorLog[];
total: number;
page: number;
page_size: number;
total_pages: number;
}
const ErrorLogViewer: React.FC = () => {
const [errorLogs, setErrorLogs] = useState<ErrorLog[]>([]);
const [stats, setStats] = useState<ErrorStats | null>(null);
const [loading, setLoading] = useState(true);
const [selectedLog, setSelectedLog] = useState<ErrorLog | null>(null);
const [showDetails, setShowDetails] = useState(false);
// Calculate default date range (last 30 days)
const getDefaultDateRange = () => {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - 30);
return {
start_date: startDate.toISOString().split('T')[0],
end_date: endDate.toISOString().split('T')[0]
};
};
const [filters, setFilters] = useState({
page: 1,
page_size: 50,
error_type: '',
status_code: '',
endpoint: '',
is_resolved: '',
...getDefaultDateRange()
});
useEffect(() => {
fetchErrorLogs();
fetchStats();
}, [filters]);
const fetchErrorLogs = async () => {
setLoading(true);
try {
const params: Record<string, string> = {};
Object.entries(filters).forEach(([key, value]) => {
if (value !== '' && value !== null && value !== undefined) {
params[key] = value.toString();
}
});
const data: ErrorLogListResponse = await adminApi.getErrorLogs(params);
setErrorLogs(data.items);
} catch (error) {
console.error('Error fetching error logs:', error);
}
setLoading(false);
};
const fetchStats = async () => {
try {
const params: Record<string, string> = {};
if (filters.start_date) params.start_date = filters.start_date;
if (filters.end_date) params.end_date = filters.end_date;
const data: ErrorStats = await adminApi.getErrorStats(params);
setStats(data);
} catch (error) {
console.error('Error fetching stats:', error);
}
};
const markAsResolved = async (logId: number, resolved: boolean, notes?: string) => {
try {
const updatedLog = await adminApi.resolveError(logId, resolved, notes);
await fetchErrorLogs();
await fetchStats();
if (selectedLog && selectedLog.id === logId) {
setSelectedLog(updatedLog);
}
} catch (error) {
console.error('Error updating error log:', error);
}
};
const deleteOldLogs = async (daysOld: number, onlyResolved: boolean) => {
try {
const result = await adminApi.deleteErrorLogs({
days_old: daysOld.toString(),
only_resolved: onlyResolved.toString(),
});
await fetchErrorLogs();
await fetchStats();
alert(result.message);
} catch (error) {
console.error('Error deleting old logs:', error);
}
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString();
};
const getStatusCodeColor = (statusCode: number) => {
if (statusCode >= 500) return 'text-red-600 bg-red-100';
if (statusCode >= 400) return 'text-orange-600 bg-orange-100';
return 'text-gray-600 bg-gray-100';
};
const getErrorTypeColor = (errorType: string) => {
const colors: Record<string, string> = {
'VALIDATION_ERROR': 'text-blue-600 bg-blue-100',
'DATA_NOT_FOUND': 'text-yellow-600 bg-yellow-100',
'PARSING_ERROR': 'text-purple-600 bg-purple-100',
'SEC_API_ERROR': 'text-red-600 bg-red-100',
'DATABASE_ERROR': 'text-red-800 bg-red-200',
'INTERNAL_SERVER_ERROR': 'text-red-800 bg-red-200'
};
return colors[errorType] || 'text-gray-600 bg-gray-100';
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-gray-900"> </h1>
</div>
<div className="flex gap-2">
<button
onClick={() => fetchErrorLogs()}
className="flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
>
<RefreshCw className="w-4 h-4" />
Refresh
</button>
<button
onClick={() => deleteOldLogs(30, true)}
className="flex items-center gap-2 px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600"
>
<Trash2 className="w-4 h-4" />
Delete Old
</button>
</div>
</div>
{/* Statistics */}
{stats && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-red-600">Total Errors</p>
<p className="text-2xl font-bold text-red-700">{stats.total_errors}</p>
</div>
<AlertTriangle className="w-8 h-8 text-red-500" />
</div>
</div>
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-green-600">Resolved</p>
<p className="text-2xl font-bold text-green-700">{stats.resolved_errors}</p>
</div>
<CheckCircle className="w-8 h-8 text-green-500" />
</div>
</div>
<div className="bg-orange-50 border border-orange-200 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-orange-600">Unresolved</p>
<p className="text-2xl font-bold text-orange-700">{stats.unresolved_errors}</p>
</div>
<XCircle className="w-8 h-8 text-orange-500" />
</div>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-blue-600">Resolution Rate</p>
<p className="text-2xl font-bold text-blue-700">{stats.resolution_rate.toFixed(1)}%</p>
</div>
<Clock className="w-8 h-8 text-blue-500" />
</div>
</div>
</div>
)}
{/* Filters */}
<div className="bg-white p-4 rounded-lg border border-gray-200">
<div className="flex items-center gap-2 mb-4">
<Filter className="w-5 h-5 text-gray-500" />
<h3 className="text-lg font-semibold">Filters</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Error Type
</label>
<select
value={filters.error_type}
onChange={(e) => setFilters(prev => ({ ...prev, error_type: e.target.value, page: 1 }))}
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
>
<option value="">All Types</option>
<option value="VALIDATION_ERROR">Validation Error</option>
<option value="DATA_NOT_FOUND">Data Not Found</option>
<option value="PARSING_ERROR">Parsing Error</option>
<option value="SEC_API_ERROR">SEC API Error</option>
<option value="DATABASE_ERROR">Database Error</option>
<option value="INTERNAL_SERVER_ERROR">Internal Server Error</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Status Code
</label>
<select
value={filters.status_code}
onChange={(e) => setFilters(prev => ({ ...prev, status_code: e.target.value, page: 1 }))}
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
>
<option value="">All Codes</option>
<option value="400">400</option>
<option value="404">404</option>
<option value="422">422</option>
<option value="500">500</option>
<option value="503">503</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Endpoint
</label>
<input
type="text"
value={filters.endpoint}
onChange={(e) => setFilters(prev => ({ ...prev, endpoint: e.target.value, page: 1 }))}
placeholder="e.g. /financial/data"
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Status
</label>
<select
value={filters.is_resolved}
onChange={(e) => setFilters(prev => ({ ...prev, is_resolved: e.target.value, page: 1 }))}
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
>
<option value="">All</option>
<option value="false">Unresolved</option>
<option value="true">Resolved</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Start Date
</label>
<input
type="date"
value={filters.start_date}
onChange={(e) => setFilters(prev => ({ ...prev, start_date: e.target.value, page: 1 }))}
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
End Date
</label>
<input
type="date"
value={filters.end_date}
onChange={(e) => setFilters(prev => ({ ...prev, end_date: e.target.value, page: 1 }))}
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
</div>
{/* Error Logs Table */}
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Time
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Endpoint
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Error Type
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Message
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Resolution
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{loading ? (
<tr>
<td colSpan={7} className="px-6 py-4 text-center text-gray-500">
Loading error logs...
</td>
</tr>
) : errorLogs.length === 0 ? (
<tr>
<td colSpan={7} className="px-6 py-4 text-center text-gray-500">
No error logs found
</td>
</tr>
) : (
errorLogs.map((log) => (
<tr key={log.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{formatDate(log.created_at)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<div>
<span className="font-medium">{log.method}</span>
<br />
<span className="text-gray-500 text-xs">{log.endpoint}</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 py-1 text-xs font-medium rounded-full ${getErrorTypeColor(log.error_type)}`}>
{log.error_type}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 py-1 text-xs font-medium rounded-full ${getStatusCodeColor(log.status_code)}`}>
{log.status_code}
</span>
</td>
<td className="px-6 py-4 text-sm text-gray-900 max-w-xs truncate">
{log.error_message}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{log.is_resolved ? (
<span className="flex items-center gap-1 text-green-600">
<CheckCircle className="w-4 h-4" />
Resolved
</span>
) : (
<span className="flex items-center gap-1 text-red-600">
<XCircle className="w-4 h-4" />
Open
</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
<button
onClick={() => {
setSelectedLog(log);
setShowDetails(true);
}}
className="text-blue-600 hover:text-blue-900"
>
<Eye className="w-4 h-4" />
</button>
<button
onClick={() => markAsResolved(log.id, !log.is_resolved)}
className={log.is_resolved ? "text-orange-600 hover:text-orange-900" : "text-green-600 hover:text-green-900"}
>
{log.is_resolved ? <XCircle className="w-4 h-4" /> : <CheckCircle className="w-4 h-4" />}
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
{/* Pagination */}
<div className="flex justify-between items-center">
<div className="text-sm text-gray-700">
Showing {errorLogs.length} errors
</div>
<div className="flex gap-2">
<button
onClick={() => setFilters(prev => ({ ...prev, page: Math.max(1, prev.page - 1) }))}
disabled={filters.page <= 1}
className="px-3 py-2 bg-gray-300 text-gray-700 rounded disabled:opacity-50"
>
Previous
</button>
<span className="px-3 py-2 bg-blue-500 text-white rounded">
{filters.page}
</span>
<button
onClick={() => setFilters(prev => ({ ...prev, page: prev.page + 1 }))}
disabled={errorLogs.length < filters.page_size}
className="px-3 py-2 bg-gray-300 text-gray-700 rounded disabled:opacity-50"
>
Next
</button>
</div>
</div>
{/* Error Details Modal */}
{showDetails && selectedLog && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] overflow-auto">
<div className="p-6">
<div className="flex justify-between items-start mb-4">
<h2 className="text-xl font-bold">Error Details</h2>
<button
onClick={() => setShowDetails(false)}
className="text-gray-500 hover:text-gray-700"
>
</button>
</div>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700">Request ID</label>
<p className="text-sm text-gray-900 font-mono">{selectedLog.request_id}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Timestamp</label>
<p className="text-sm text-gray-900">{formatDate(selectedLog.created_at)}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700">Method & Endpoint</label>
<p className="text-sm text-gray-900 font-mono">
{selectedLog.method} {selectedLog.endpoint}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Status Code</label>
<span className={`px-2 py-1 text-xs font-medium rounded-full ${getStatusCodeColor(selectedLog.status_code)}`}>
{selectedLog.status_code}
</span>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Error Message</label>
<p className="text-sm text-gray-900 bg-gray-50 p-3 rounded">{selectedLog.error_message}</p>
</div>
{selectedLog.error_detail && (
<div>
<label className="block text-sm font-medium text-gray-700">Error Details</label>
<pre className="text-sm text-gray-900 bg-gray-50 p-3 rounded overflow-auto">
{JSON.stringify(selectedLog.error_detail, null, 2)}
</pre>
</div>
)}
{selectedLog.request_body && (
<div>
<label className="block text-sm font-medium text-gray-700">Request Body</label>
<pre className="text-sm text-gray-900 bg-gray-50 p-3 rounded overflow-auto">
{JSON.stringify(selectedLog.request_body, null, 2)}
</pre>
</div>
)}
{selectedLog.stack_trace && (
<div>
<label className="block text-sm font-medium text-gray-700">Stack Trace</label>
<pre className="text-sm text-gray-900 bg-red-50 p-3 rounded overflow-auto text-red-800">
{selectedLog.stack_trace}
</pre>
</div>
)}
<div className="flex gap-4 pt-4 border-t">
<button
onClick={() => markAsResolved(selectedLog.id, !selectedLog.is_resolved)}
className={`px-4 py-2 rounded text-white ${
selectedLog.is_resolved
? 'bg-orange-500 hover:bg-orange-600'
: 'bg-green-500 hover:bg-green-600'
}`}
>
{selectedLog.is_resolved ? 'Mark as Unresolved' : 'Mark as Resolved'}
</button>
<button
onClick={() => setShowDetails(false)}
className="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600"
>
Close
</button>
</div>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default ErrorLogViewer;