feat(logging): implement log pruning functionality
- Add LogPruneService to handle the deletion of old logs based on retention settings. - Create PruneLogsCommand to provide a console command for log pruning. - Introduce PruneLogsMessage and PruneLogsHandler for message handling related to log pruning. - Update the AST cache with new classes and their relationships.
This commit is contained in:
+148
-32
@@ -1,12 +1,14 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { TrashIcon } from '@heroicons/react/24/outline';
|
||||
import { api } from '../lib/api';
|
||||
import type { PaginatedResponse } from '../lib/api';
|
||||
import type { PaginatedResponse, ApiResponse } 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 ConfirmDialog from '../components/ui/ConfirmDialog';
|
||||
import SearchableSelect from '../components/ui/SearchableSelect';
|
||||
|
||||
const LEVEL_META: Record<string, { label: string; cls: string }> = {
|
||||
@@ -27,12 +29,17 @@ const LEVEL_FILTER_OPTIONS = [
|
||||
{ value: 'critical', label: 'بحرانی' },
|
||||
];
|
||||
|
||||
type Tab = 'logs' | 'settings';
|
||||
|
||||
export default function LogsPage() {
|
||||
const [tab, setTab] = useState<Tab>('logs');
|
||||
const [page, setPage] = useState(1);
|
||||
const [level, setLevel] = useState('');
|
||||
const [search, setSearch] = useState('');
|
||||
const [viewLog, setViewLog] = useState<AppLog | null>(null);
|
||||
const [confirmClear, setConfirmClear] = useState(false);
|
||||
const limit = 25;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const logsQuery = useQuery({
|
||||
queryKey: ['admin-logs', page, level, search],
|
||||
@@ -44,6 +51,15 @@ export default function LogsPage() {
|
||||
),
|
||||
});
|
||||
|
||||
const clearMutation = useMutation({
|
||||
mutationFn: () => api.delete<ApiResponse<{ deleted: number }>>('/api/v1/admin/logs'),
|
||||
onSuccess: () => {
|
||||
setConfirmClear(false);
|
||||
setPage(1);
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-logs'] });
|
||||
},
|
||||
});
|
||||
|
||||
const columns: Column<AppLog>[] = [
|
||||
{ key: 'created_at', header: 'زمان', render: (l) => formatDateTime(l.created_at) },
|
||||
{
|
||||
@@ -78,42 +94,71 @@ export default function LogsPage() {
|
||||
<h1 className="section-title">لاگها</h1>
|
||||
<div className="muted">رویدادهای ثبتشدهی سیستم (اخطار و بالاتر)</div>
|
||||
</div>
|
||||
{tab === 'logs' && (
|
||||
<button
|
||||
className="btn danger sm"
|
||||
onClick={() => setConfirmClear(true)}
|
||||
disabled={(logsQuery.data?.meta?.totalRecords ?? 0) === 0}
|
||||
>
|
||||
<TrashIcon style={{ width: 16, height: 16 }} />
|
||||
حذف همه لاگها
|
||||
</button>
|
||||
)}
|
||||
</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 style={{ display: 'flex', gap: 8, marginBottom: 'var(--gap)' }}>
|
||||
<button
|
||||
className={`btn ${tab === 'logs' ? 'primary' : 'ghost'} sm`}
|
||||
onClick={() => setTab('logs')}
|
||||
>
|
||||
فهرست لاگها
|
||||
</button>
|
||||
<button
|
||||
className={`btn ${tab === 'settings' ? 'primary' : 'ghost'} sm`}
|
||||
onClick={() => setTab('settings')}
|
||||
>
|
||||
مدت نگهداری
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{tab === 'logs' && (
|
||||
<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>
|
||||
</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}
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'settings' && <RetentionSettings />}
|
||||
|
||||
<Modal open={!!viewLog} title="جزئیات لاگ" onClose={() => setViewLog(null)}>
|
||||
{viewLog && (
|
||||
@@ -137,6 +182,77 @@ export default function LogsPage() {
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<ConfirmDialog
|
||||
open={confirmClear}
|
||||
title="حذف همه لاگها"
|
||||
message="همهی لاگهای سیستم برای همیشه حذف میشوند. این عمل قابل بازگشت نیست. ادامه میدهید؟"
|
||||
confirmLabel="حذف همه"
|
||||
danger
|
||||
loading={clearMutation.isPending}
|
||||
onConfirm={() => clearMutation.mutate()}
|
||||
onCancel={() => setConfirmClear(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RetentionSettings() {
|
||||
const queryClient = useQueryClient();
|
||||
const [days, setDays] = useState<string>('');
|
||||
const [touched, setTouched] = useState(false);
|
||||
|
||||
const settingsQuery = useQuery({
|
||||
queryKey: ['admin-settings'],
|
||||
queryFn: () => api.get<ApiResponse<Record<string, string | boolean>>>('/api/v1/admin/settings'),
|
||||
});
|
||||
|
||||
const current = String(settingsQuery.data?.data?.log_retention_days ?? '');
|
||||
const value = touched ? days : current;
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: (retentionDays: string) =>
|
||||
api.patch<ApiResponse<Record<string, string | boolean>>>('/api/v1/admin/settings', {
|
||||
log_retention_days: retentionDays,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
setTouched(false);
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-settings'] });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-pad">
|
||||
<div style={{ maxWidth: 420, display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<div>
|
||||
<label className="muted" style={{ fontSize: 13, display: 'block', marginBottom: 6 }}>
|
||||
مدت نگهداری لاگها (روز)
|
||||
</label>
|
||||
<input
|
||||
className="input"
|
||||
type="number"
|
||||
min={0}
|
||||
value={value}
|
||||
placeholder={settingsQuery.isLoading ? 'در حال بارگذاری...' : '90'}
|
||||
onChange={(e) => { setDays(e.target.value); setTouched(true); }}
|
||||
style={{ maxWidth: 200 }}
|
||||
/>
|
||||
<div className="muted" style={{ fontSize: 12, marginTop: 6, lineHeight: 1.7 }}>
|
||||
لاگهای قدیمیتر از این مدت بهصورت روزانه بهصورت خودکار حذف میشوند. مقدار ۰ یعنی نگهداری نامحدود.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
className="btn primary sm"
|
||||
disabled={saveMutation.isPending || !touched || value === current}
|
||||
onClick={() => saveMutation.mutate(String(parseInt(value || '0', 10)))}
|
||||
>
|
||||
{saveMutation.isPending ? 'در حال ذخیره...' : 'ذخیره'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user