feat: Implement advanced access logging with Caddy JSON format, filtering, and download
This commit is contained in:
79
frontend/src/components/LogFilters.tsx
Normal file
79
frontend/src/components/LogFilters.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import { Search, Download, RefreshCw } from 'lucide-react';
|
||||
import { Button } from './ui/Button';
|
||||
|
||||
interface LogFiltersProps {
|
||||
search: string;
|
||||
onSearchChange: (value: string) => void;
|
||||
status: string;
|
||||
onStatusChange: (value: string) => void;
|
||||
host: string;
|
||||
onHostChange: (value: string) => void;
|
||||
onRefresh: () => void;
|
||||
onDownload: () => void;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const LogFilters: React.FC<LogFiltersProps> = ({
|
||||
search,
|
||||
onSearchChange,
|
||||
status,
|
||||
onStatusChange,
|
||||
host,
|
||||
onHostChange,
|
||||
onRefresh,
|
||||
onDownload,
|
||||
isLoading
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row gap-4 p-4 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex-1 relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Search className="h-4 w-4 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search logs..."
|
||||
value={search}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="block w-full pl-10 rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full md:w-48">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter by Host"
|
||||
value={host}
|
||||
onChange={(e) => onHostChange(e.target.value)}
|
||||
className="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full md:w-32">
|
||||
<select
|
||||
value={status}
|
||||
onChange={(e) => onStatusChange(e.target.value)}
|
||||
className="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="2xx">2xx Success</option>
|
||||
<option value="3xx">3xx Redirect</option>
|
||||
<option value="4xx">4xx Client Error</option>
|
||||
<option value="5xx">5xx Server Error</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={onRefresh} variant="secondary" size="sm" isLoading={isLoading}>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
Refresh
|
||||
</Button>
|
||||
<Button onClick={onDownload} variant="secondary" size="sm">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
83
frontend/src/components/LogTable.tsx
Normal file
83
frontend/src/components/LogTable.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import { CaddyAccessLog } from '../api/logs';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
interface LogTableProps {
|
||||
logs: CaddyAccessLog[];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const LogTable: React.FC<LogTableProps> = ({ logs, isLoading }) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="w-full h-64 flex items-center justify-center text-gray-500">
|
||||
Loading logs...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!logs || logs.length === 0) {
|
||||
return (
|
||||
<div className="w-full h-64 flex items-center justify-center text-gray-500">
|
||||
No logs found matching criteria.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Time</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Status</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Method</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Host</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Path</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">IP</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Latency</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{logs.map((log, idx) => (
|
||||
<tr key={idx} className="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
||||
{format(new Date(log.ts * 1000), 'MMM d HH:mm:ss')}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
||||
{log.status > 0 && (
|
||||
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full
|
||||
${log.status >= 500 ? 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' :
|
||||
log.status >= 400 ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' :
|
||||
log.status >= 300 ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' :
|
||||
'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'}`}>
|
||||
{log.status}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
|
||||
{log.request?.method}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
||||
{log.request?.host}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400 max-w-xs truncate" title={log.request?.uri}>
|
||||
{log.request?.uri}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
||||
{log.request?.remote_ip}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
||||
{log.duration > 0 ? (log.duration * 1000).toFixed(2) + 'ms' : ''}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400 max-w-xs truncate" title={log.msg}>
|
||||
{log.msg}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user