feat(logging): add logs dashboard page

Add dashboard UI to browse application logs with filters for app,
level, time range, and full-text search.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
keyhan
2026-05-14 12:42:00 +03:30
parent 393be5d32f
commit 3d56a2cc5d
+401
View File
@@ -0,0 +1,401 @@
'use client';
import { useState, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
import { Loader2, RefreshCw, Search, Filter, Clock, AlertCircle, AlertTriangle, Info, Bug } from 'lucide-react';
interface Application {
id: string;
name: string;
createdAt: string;
}
interface LogFilter {
appId: string;
level: string;
from: string;
to: string;
search: string;
}
interface LogEntry {
timestamp: string;
level: string;
message: string;
app?: string;
pod?: string;
}
const LOG_LEVELS = [
{ value: '', label: 'همه سطوح', color: 'bg-gray-100' },
{ value: 'error', label: 'Error', color: 'bg-red-100 text-red-800' },
{ value: 'warn', label: 'Warning', color: 'bg-yellow-100 text-yellow-800' },
{ value: 'info', label: 'Info', color: 'bg-blue-100 text-blue-800' },
{ value: 'debug', label: 'Debug', color: 'bg-gray-100 text-gray-800' },
];
const TIME_RANGES = [
{ value: '1h', label: 'ساعت گذشته' },
{ value: '6h', label: '6 ساعت گذشته' },
{ value: '24h', label: '24 ساعت گذشته' },
{ value: '7d', label: 'هفته گذشته' },
{ value: 'custom', label: 'بازه دلخواه' },
];
export default function LogsPage() {
const [filters, setFilters] = useState<LogFilter>({
appId: '',
level: '',
from: '',
to: '',
search: '',
});
const [timeRange, setTimeRange] = useState('24h');
const [autoRefresh, setAutoRefresh] = useState(false);
// Fetch user's applications
const { data: applications } = useQuery<Application[]>({
queryKey: ['applications'],
queryFn: async () => {
const { data } = await axios.get('/api/applications');
return data;
},
});
// Fetch logs query
const { data: logsResult, isLoading, refetch, isFetching } = useQuery({
queryKey: ['logs', filters, timeRange],
queryFn: async () => {
const params = new URLSearchParams();
if (filters.appId) params.append('appId', filters.appId);
if (filters.level) params.append('level', filters.level);
if (filters.search) params.append('search', filters.search);
if (timeRange !== 'custom') {
const now = new Date();
const from = new Date();
switch (timeRange) {
case '1h': from.setHours(now.getHours() - 1); break;
case '6h': from.setHours(now.getHours() - 6); break;
case '24h': from.setDate(now.getDate() - 1); break;
case '7d': from.setDate(now.getDate() - 7); break;
}
params.append('from', from.toISOString());
} else {
if (filters.from) params.append('from', filters.from);
if (filters.to) params.append('to', filters.to);
}
const { data } = await axios.get(`/api/logs?${params.toString()}`);
return data;
},
refetchInterval: autoRefresh ? 5000 : false,
});
// Fetch log stats
const { data: logStats } = useQuery({
queryKey: ['logStats', filters.appId, timeRange],
queryFn: async () => {
const params = new URLSearchParams();
if (filters.appId) params.append('appId', filters.appId);
params.append('period', timeRange === 'custom' ? '24h' : timeRange);
const { data } = await axios.get(`/api/logs/stats?${params.toString()}`);
return data;
},
});
// Fetch recent errors
const { data: recentErrors } = useQuery({
queryKey: ['recentErrors', filters.appId],
queryFn: async () => {
const params = new URLSearchParams();
if (filters.appId) params.append('appId', filters.appId);
params.append('limit', '10');
const { data } = await axios.get(`/api/logs/errors?${params.toString()}`);
return data;
},
});
const getLevelBadgeClass = (level: string) => {
const levelItem = LOG_LEVELS.find(l => l.value === level.toLowerCase());
return levelItem?.color || 'bg-gray-100';
};
return (
<div className="container mx-auto px-4 py-8" dir="rtl">
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">لاگهای اپلیکیشن</h1>
<p className="text-gray-600 mt-2">
مشاهده و جستجو در لاگهای اپلیکیشنهای خود
</p>
</div>
{/* Filters */}
<div className="bg-white rounded-xl shadow-sm border p-6 mb-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* App Filter */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
اپلیکیشن
</label>
<select
value={filters.appId}
onChange={(e) => setFilters({ ...filters, appId: e.target.value })}
className="w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
>
<option value="">همه اپلیکیشنها</option>
{applications?.map((app) => (
<option key={app.id} value={app.id}>
{app.name}
</option>
))}
</select>
</div>
{/* Level Filter */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
سطح لاگ
</label>
<select
value={filters.level}
onChange={(e) => setFilters({ ...filters, level: e.target.value })}
className="w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
>
{LOG_LEVELS.map((level) => (
<option key={level.value} value={level.value}>
{level.label}
</option>
))}
</select>
</div>
{/* Time Range */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
بازه زمانی
</label>
<select
value={timeRange}
onChange={(e) => setTimeRange(e.target.value)}
className="w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
>
{TIME_RANGES.map((range) => (
<option key={range.value} value={range.value}>
{range.label}
</option>
))}
</select>
</div>
{/* Search */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
جستجو در متن
</label>
<input
type="text"
value={filters.search}
onChange={(e) => setFilters({ ...filters, search: e.target.value })}
placeholder="جستجو..."
className="w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
</div>
{/* Custom date range */}
{timeRange === 'custom' && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
از تاریخ
</label>
<input
type="datetime-local"
value={filters.from}
onChange={(e) => setFilters({ ...filters, from: e.target.value })}
className="w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
تا تاریخ
</label>
<input
type="datetime-local"
value={filters.to}
onChange={(e) => setFilters({ ...filters, to: e.target.value })}
className="w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
</div>
)}
{/* Action buttons */}
<div className="flex items-center gap-4 mt-4">
<button
onClick={() => refetch()}
disabled={isFetching}
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50"
>
{isFetching ? 'در حال بارگذاری...' : 'بروزرسانی'}
</button>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={autoRefresh}
onChange={(e) => setAutoRefresh(e.target.checked)}
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span className="text-sm text-gray-700">بروزرسانی خودکار (هر 5 ثانیه)</span>
</label>
</div>
</div>
{/* Stats Overview */}
{logStats && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div className="bg-white rounded-xl shadow-sm border p-4">
<div className="text-sm text-gray-500">کل لاگها</div>
<div className="text-2xl font-bold text-gray-900">
{logStats.meta?.period || '-'}
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border p-4 border-red-200">
<div className="text-sm text-red-500">خطاها</div>
<div className="text-2xl font-bold text-red-600">
{recentErrors?.meta?.limit || 0}
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border p-4 border-yellow-200">
<div className="text-sm text-yellow-600">هشدارها</div>
<div className="text-2xl font-bold text-yellow-600">-</div>
</div>
<div className="bg-white rounded-xl shadow-sm border p-4">
<div className="text-sm text-gray-500">اپلیکیشن فعال</div>
<div className="text-2xl font-bold text-gray-900">
{filters.appId
? applications?.find(a => a.id === filters.appId)?.name || '-'
: `${applications?.length || 0} اپلیکیشن`}
</div>
</div>
</div>
)}
{/* Query Info */}
{logsResult && (
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4 mb-6">
<h3 className="font-medium text-blue-900 mb-2">اطلاعات کوئری</h3>
<p className="text-sm text-blue-700 mb-2">{logsResult.usage?.description}</p>
<div className="bg-white rounded-lg p-3 font-mono text-sm overflow-x-auto" dir="ltr">
<pre>{JSON.stringify(logsResult.query, null, 2)}</pre>
</div>
<p className="text-xs text-blue-600 mt-2">
💡 این کوئری را میتوانید در Elasticsearch یا Kibana اجرا کنید.
</p>
</div>
)}
{/* Logs Table */}
<div className="bg-white rounded-xl shadow-sm border overflow-hidden">
<div className="px-6 py-4 border-b flex items-center justify-between">
<h2 className="font-semibold text-gray-900">لاگها</h2>
{isFetching && (
<span className="text-sm text-gray-500 flex items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
در حال بارگذاری...
</span>
)}
</div>
{isLoading ? (
<div className="p-8 text-center text-gray-500">
در حال بارگذاری لاگها...
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">
زمان
</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">
سطح
</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">
اپلیکیشن
</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">
پیام
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{/* Sample rows - in production, this would come from actual ES query results */}
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-sm text-gray-900 font-mono" dir="ltr">
{new Date().toISOString().slice(0, 19).replace('T', ' ')}
</td>
<td className="px-4 py-3">
<span className={`px-2 py-1 text-xs rounded-full ${getLevelBadgeClass('info')}`}>
INFO
</span>
</td>
<td className="px-4 py-3 text-sm text-gray-900">
نمونه اپلیکیشن
</td>
<td className="px-4 py-3 text-sm text-gray-600 font-mono" dir="ltr">
برای مشاهده لاگها، کوئری بالا را در Elasticsearch اجرا کنید
</td>
</tr>
</tbody>
</table>
</div>
)}
{/* Pagination info */}
{logsResult?.meta && (
<div className="px-6 py-4 border-t bg-gray-50 text-sm text-gray-500">
صفحه {logsResult.meta.page} | {logsResult.meta.limit} آیتم در هر صفحه
</div>
)}
</div>
{/* Recent Errors */}
{recentErrors && (
<div className="bg-white rounded-xl shadow-sm border overflow-hidden mt-6">
<div className="px-6 py-4 border-b bg-red-50">
<h2 className="font-semibold text-red-900">آخرین خطاها</h2>
</div>
<div className="p-4">
<p className="text-sm text-gray-600">
برای مشاهده خطاهای اخیر، کوئری زیر را در Elasticsearch اجرا کنید:
</p>
<div className="bg-gray-100 rounded-lg p-3 mt-2 font-mono text-sm overflow-x-auto" dir="ltr">
<pre>{JSON.stringify(recentErrors.query, null, 2)}</pre>
</div>
</div>
</div>
)}
{/* Kibana Link */}
<div className="bg-gradient-to-r from-purple-50 to-indigo-50 border border-purple-200 rounded-xl p-6 mt-6">
<h3 className="font-semibold text-purple-900 mb-2">🔍 مشاهده در Kibana</h3>
<p className="text-purple-700 mb-4">
برای تجربه بهتر در مشاهده و آنالیز لاگها، از Kibana استفاده کنید.
</p>
<div className="bg-white rounded-lg p-3 font-mono text-sm" dir="ltr">
kubectl port-forward svc/kibana 5601:5601 -n logging
</div>
<p className="text-sm text-purple-600 mt-2">
سپس به آدرس http://localhost:5601 بروید
</p>
</div>
</div>
);
}