Files
clinicpro/assets/admin/pages/LogsPage.tsx
T
hamed 803196108c feat(logging): Implement database logging with app_log table
- Created migration to set up app_log table for storing application logs.
- Added AppLog entity and repository for ORM handling of logs.
- Developed DbLogger service to persist logs of level WARNING and above to the database while maintaining existing logging behavior.
- Implemented tests for admin log retrieval and DbLogger functionality to ensure proper logging behavior.
- Enhanced logging context sanitization for better error tracking.
2026-06-29 20:01:03 +03:30

143 lines
5.7 KiB
TypeScript

import React, { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { api } from '../lib/api';
import type { PaginatedResponse } from '../lib/api';
import type { AppLog } from '../types';
import { formatDateTime } from '../lib/utils';
import DataTable, { Column } from '../components/ui/DataTable';
import Pagination from '../components/ui/Pagination';
import Modal from '../components/ui/Modal';
import SearchableSelect from '../components/ui/SearchableSelect';
const LEVEL_META: Record<string, { label: string; cls: string }> = {
emergency: { label: 'اضطراری', cls: 'red' },
alert: { label: 'هشدار جدی', cls: 'red' },
critical: { label: 'بحرانی', cls: 'red' },
error: { label: 'خطا', cls: 'red' },
warning: { label: 'اخطار', cls: 'amber' },
notice: { label: 'اطلاع', cls: 'gray' },
info: { label: 'اطلاعات', cls: 'gray' },
debug: { label: 'دیباگ', cls: 'gray' },
};
const LEVEL_FILTER_OPTIONS = [
{ value: '', label: 'همه سطوح' },
{ value: 'error', label: 'خطا' },
{ value: 'warning', label: 'اخطار' },
{ value: 'critical', label: 'بحرانی' },
];
export default function LogsPage() {
const [page, setPage] = useState(1);
const [level, setLevel] = useState('');
const [search, setSearch] = useState('');
const [viewLog, setViewLog] = useState<AppLog | null>(null);
const limit = 25;
const logsQuery = useQuery({
queryKey: ['admin-logs', page, level, search],
queryFn: () =>
api.get<PaginatedResponse<AppLog>>(
`/api/v1/admin/logs?page=${page}&limit=${limit}` +
(level ? `&level=${level}` : '') +
(search ? `&search=${encodeURIComponent(search)}` : ''),
),
});
const columns: Column<AppLog>[] = [
{ key: 'created_at', header: 'زمان', render: (l) => formatDateTime(l.created_at) },
{
key: 'level',
header: 'سطح',
render: (l) => {
const meta = LEVEL_META[l.level] ?? { label: l.level, cls: 'gray' };
return <span className={`badge ${meta.cls}`}><span className="bdot" />{meta.label}</span>;
},
},
{
key: 'message',
header: 'پیام',
render: (l) => (
<button
type="button"
title="نمایش جزئیات"
onClick={() => setViewLog(l)}
style={{ fontSize: 12.5, display: 'block', maxWidth: 420, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'right', background: 'none', border: 'none', padding: 0, cursor: 'pointer', color: 'var(--primary)', width: '100%' }}
>
{l.message}
</button>
),
},
{ key: 'path', header: 'مسیر', render: (l) => <span dir="ltr" className="muted" style={{ fontSize: 12 }}>{l.path ?? '—'}</span> },
];
return (
<div className="fade-in">
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
<div>
<h1 className="section-title">لاگ‌ها</h1>
<div className="muted">رویدادهای ثبت‌شده‌ی سیستم (اخطار و بالاتر)</div>
</div>
</div>
<div className="card">
<div className="card-pad">
<div style={{ marginBottom: 12, display: 'flex', gap: 12, justifyContent: 'flex-end', flexWrap: 'wrap' }}>
<input
className="input"
style={{ maxWidth: 260 }}
placeholder="جستجو در متن..."
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
/>
<div style={{ width: 200 }}>
<SearchableSelect
options={LEVEL_FILTER_OPTIONS}
value={level}
onChange={(v) => { setLevel(v ? String(v) : ''); setPage(1); }}
placeholder="همه سطوح"
/>
</div>
</div>
<DataTable<AppLog>
columns={columns}
data={logsQuery.data?.data ?? []}
loading={logsQuery.isLoading}
emptyMessage="لاگی یافت نشد"
/>
<Pagination
page={page}
total={logsQuery.data?.meta?.totalRecords ?? 0}
limit={limit}
onPageChange={setPage}
/>
</div>
</div>
<Modal open={!!viewLog} title="جزئیات لاگ" onClose={() => setViewLog(null)}>
{viewLog && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, minWidth: 360, maxWidth: 640 }}>
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', fontSize: 12.5 }}>
<div><span className="muted">سطح: </span>{LEVEL_META[viewLog.level]?.label ?? viewLog.level}</div>
<div><span className="muted">زمان: </span>{formatDateTime(viewLog.created_at)}</div>
{viewLog.path && <div><span className="muted">مسیر: </span><span dir="ltr">{viewLog.path}</span></div>}
</div>
<div style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', lineHeight: 1.9, fontSize: 13.5, padding: '12px 14px', borderRadius: 'var(--r-sm)', background: 'var(--surface-3)', border: '1px solid var(--border)' }} dir="ltr">
{viewLog.message}
</div>
{viewLog.context && (
<div>
<div className="muted" style={{ fontSize: 12, marginBottom: 4 }}>context</div>
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 12, padding: '10px 12px', borderRadius: 'var(--r-sm)', background: 'var(--surface)', border: '1px solid var(--border)', margin: 0 }} dir="ltr">
{viewLog.context}
</pre>
</div>
)}
</div>
)}
</Modal>
</div>
);
}