access logs detail

This commit is contained in:
hamid zarghami
2025-09-03 12:05:52 +03:30
parent 3ca8b89a47
commit 5b376b83d1
4 changed files with 289 additions and 195 deletions
+54 -7
View File
@@ -1,9 +1,9 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { import {
DocumentText, DocumentText,
Refresh,
Activity, Activity,
InfoCircle InfoCircle,
Refresh
} from "iconsax-react"; } from "iconsax-react";
import Button from "../../components/Button"; import Button from "../../components/Button";
import Pagination from "../../components/Pagination"; import Pagination from "../../components/Pagination";
@@ -14,7 +14,7 @@ import FilterPanel from "./components/FilterPanel";
import AccessLogsTable from "./components/AccessLogsTable"; import AccessLogsTable from "./components/AccessLogsTable";
import LogDetailsModal from "./components/LogDetailsModal"; import LogDetailsModal from "./components/LogDetailsModal";
import { useAccessLogs, useAccessLogDetail } from "./hooks/useAccessLogs"; import { useAccessLogs, useAccessLogDetail } from "./hooks/useAccessLogs";
import { AccessLog } from "./types/AccessLogTypes"; import { AccessLog, OperationType, UserType } from "./types/AccessLogTypes";
const AccessLogsList: React.FC = () => { const AccessLogsList: React.FC = () => {
const { const {
@@ -28,17 +28,32 @@ const AccessLogsList: React.FC = () => {
refetch, refetch,
} = useAccessLogs(); } = useAccessLogs();
const { log: selectedLog, fetchLog, clearLog } = useAccessLogDetail(); const { log: selectedLog, loading: detailLoading, error: detailError, fetchLog, clearLog } = useAccessLogDetail();
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false); const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
const [directLog, setDirectLog] = useState<AccessLog | null>(null);
const handleViewDetails = async (log: AccessLog) => { const handleViewDetails = async (log: AccessLog) => {
await fetchLog(log.id); try {
setIsDetailsModalOpen(true); setIsDetailsModalOpen(true);
// اگر لاگ کامل است (مثل داده‌های نمونه)، مستقیماً نمایش بده
if (log.user !== undefined || log.oldValues !== undefined || log.newValues !== undefined) {
setDirectLog(log);
clearLog(); // پاک کردن selectedLog
} else {
// اگر لاگ از API آمده، جزئیات کامل را دریافت کن
setDirectLog(null);
await fetchLog(log.id);
}
} catch (error) {
console.error('خطا در دریافت جزئیات لاگ:', error);
// مودال باز می‌ماند تا خطا نمایش داده شود
}
}; };
const handleCloseDetailsModal = () => { const handleCloseDetailsModal = () => {
setIsDetailsModalOpen(false); setIsDetailsModalOpen(false);
clearLog(); clearLog();
setDirectLog(null);
}; };
const handleSort = (field: string, order: "ASC" | "DESC") => { const handleSort = (field: string, order: "ASC" | "DESC") => {
@@ -80,6 +95,36 @@ const AccessLogsList: React.FC = () => {
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Button
onClick={() => {
// تست مودال با داده‌های نمونه
const sampleLog: AccessLog = {
id: "eb528348-d1c8-4216-8a78-fa62ca56b65a",
createdAt: "2025-09-02T12:55:47.593Z",
updatedAt: "2025-09-02T12:55:47.593Z",
operationType: "UPDATE" as OperationType,
userType: "ADMIN" as UserType,
entityName: "Blog",
entityId: "9763ceb4-ce32-4474-979c-5581c9d2cd24",
actionDescription: "Updated Blog",
oldValues: undefined,
newValues: '{"userId":"2c6895a4-e04a-4b1d-aa22-390c37353630","userType":"ADMIN","actionDescription":"Admin updated blog: میل سرور چیست؟ راهنمای کاربردی + معرفی بهترین گزینه برای شرکت‌ها | DanakCorp","metadata":{"categoryId":"a940f9b5-c1a9-4ff1-b67d-ae78c62fc792"}}',
ipAddress: undefined,
userAgent: undefined,
endpoint: undefined,
statusCode: undefined,
errorMessage: undefined,
metadata: undefined,
requestId: undefined,
user: undefined
};
handleViewDetails(sampleLog);
}}
className="flex items-center gap-2 border border-blue-300 bg-blue-50 text-blue-700 hover:bg-blue-100"
>
<InfoCircle size={16} />
تست مودال
</Button>
<Button <Button
onClick={refetch} onClick={refetch}
disabled={loading} disabled={loading}
@@ -174,9 +219,11 @@ const AccessLogsList: React.FC = () => {
{/* Details Modal */} {/* Details Modal */}
<LogDetailsModal <LogDetailsModal
log={selectedLog} log={directLog || selectedLog}
isOpen={isDetailsModalOpen} isOpen={isDetailsModalOpen}
onClose={handleCloseDetailsModal} onClose={handleCloseDetailsModal}
loading={detailLoading}
error={detailError}
/> />
{/* Empty State Info */} {/* Empty State Info */}
@@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { Eye, ArrowUp, ArrowDown, Clock, User, DocumentText } from "iconsax-react"; import { ArrowUp, ArrowDown, Clock, User, DocumentText } from "iconsax-react";
import Button from "../../../components/Button"; import Button from "../../../components/Button";
import StatusCircle from "../../../components/StatusCircle"; import StatusCircle from "../../../components/StatusCircle";
import Td from "../../../components/Td"; import Td from "../../../components/Td";
@@ -224,9 +224,8 @@ const AccessLogsTable: React.FC<AccessLogsTableProps> = ({
<Td text=""> <Td text="">
<Button <Button
onClick={() => onViewDetails(log)} onClick={() => onViewDetails(log)}
className="text-sm px-3 py-1 bg-transparent text-blue-600 hover:text-blue-800 hover:bg-blue-50" className="w-fit px-4"
> >
<Eye size={16} className="ml-1" />
جزئیات جزئیات
</Button> </Button>
</Td> </Td>
@@ -26,14 +26,18 @@ interface LogDetailsModalProps {
log: AccessLog | null; log: AccessLog | null;
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
loading?: boolean;
error?: string | null;
} }
const LogDetailsModal: React.FC<LogDetailsModalProps> = ({ const LogDetailsModal: React.FC<LogDetailsModalProps> = ({
log, log,
isOpen, isOpen,
onClose, onClose,
loading = false,
error = null,
}) => { }) => {
if (!log) return null; if (!isOpen) return null;
const getOperationTypeColor = (type: OperationType): string => { const getOperationTypeColor = (type: OperationType): string => {
const colors = { const colors = {
@@ -154,209 +158,245 @@ const LogDetailsModal: React.FC<LogDetailsModalProps> = ({
</Button> </Button>
</div> </div>
{/* Basic Information */} {/* Loading State */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6"> {loading && (
<InfoRow <div className="flex items-center justify-center py-12">
icon={<Activity size={18} />} <div className="text-center">
label="نوع عملیات" <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
value={ <p className="text-gray-600">در حال بارگذاری جزئیات...</p>
<StatusCircle </div>
color={getOperationTypeColor(log.operationType)} </div>
text={OperationTypeLabels[log.operationType]} )}
/>
}
/>
<InfoRow {/* Error State */}
icon={<User size={18} />} {error && (
label="نوع کاربر" <div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
value={ <div className="flex items-center gap-3">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getUserTypeBadgeColor(log.userType)}`}> <Warning2 size={20} className="text-red-600" />
{UserTypeLabels[log.userType]}
</span>
}
/>
<InfoRow
icon={<DocumentText size={18} />}
label="موجودیت"
value={
<div> <div>
<div className="font-medium">{log.entityName}</div> <h3 className="text-sm font-medium text-red-900">خطا در بارگذاری</h3>
{log.entityId && ( <p className="text-sm text-red-700 mt-1">{error}</p>
<div className="text-xs text-gray-500 mt-1">{log.entityId}</div>
)}
</div> </div>
} </div>
/> </div>
)}
<InfoRow {/* Content - Only show if not loading and no error */}
icon={<Clock size={18} />} {!loading && !error && log && (
label="تاریخ و زمان" <>
value={formatDate(log.createdAt)} {/* Basic Information */}
/> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<InfoRow
icon={<Activity size={18} />}
label="نوع عملیات"
value={
<StatusCircle
color={getOperationTypeColor(log.operationType)}
text={OperationTypeLabels[log.operationType]}
/>
}
/>
{log.actionDescription && (
<InfoRow
icon={<InfoCircle size={18} />}
label="توضیحات عملیات"
value={log.actionDescription}
fullWidth
/>
)}
</div>
{/* User Information */}
{log.user && (
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-3 flex items-center gap-2">
<User size={18} />
اطلاعات کاربر
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<InfoRow <InfoRow
icon={<User size={18} />} icon={<User size={18} />}
label=ام و نام خانوادگی" label=وع کاربر"
value={`${log.user.firstName} ${log.user.lastName}`} value={
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getUserTypeBadgeColor(log.userType)}`}>
{UserTypeLabels[log.userType]}
</span>
}
/> />
<InfoRow
icon={<InfoCircle size={18} />}
label="ایمیل"
value={log.user.email}
/>
{log.user.phone && (
<InfoRow
icon={<InfoCircle size={18} />}
label="شماره تماس"
value={log.user.phone}
/>
)}
<InfoRow
icon={<InfoCircle size={18} />}
label="شناسه کاربر"
value={log.user.id}
/>
</div>
</div>
)}
{/* Technical Details */}
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-3 flex items-center gap-2">
<Monitor size={18} />
جزئیات فنی
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{log.ipAddress && (
<InfoRow <InfoRow
icon={<Monitor size={18} />} icon={<DocumentText size={18} />}
label="آدرس IP" label="موجودیت"
value={log.ipAddress} value={
<div>
<div className="font-medium">{log.entityName}</div>
{log.entityId && (
<div className="text-xs text-gray-500 mt-1">{log.entityId}</div>
)}
</div>
}
/> />
)}
{log.endpoint && (
<InfoRow
icon={<Code size={18} />}
label="Endpoint"
value={log.endpoint}
/>
)}
{log.httpMethod && (
<InfoRow
icon={<Code size={18} />}
label="متد HTTP"
value={HttpMethodLabels[log.httpMethod]}
/>
)}
<InfoRow
icon={<InfoCircle size={18} />}
label="کد وضعیت"
value={
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(log.statusCode)}`}>
{log.statusCode || "N/A"}
</span>
}
/>
{log.executionTime && (
<InfoRow <InfoRow
icon={<Clock size={18} />} icon={<Clock size={18} />}
label="زمان اجرا" label="تاریخ و زمان"
value={formatExecutionTime(log.executionTime)} value={formatDate(log.createdAt)}
/> />
)}
{log.sessionId && ( {log.actionDescription && (
<InfoRow <InfoRow
icon={<InfoCircle size={18} />} icon={<InfoCircle size={18} />}
label="شناسه جلسه" label="توضیحات عملیات"
value={log.sessionId} value={log.actionDescription}
/> fullWidth
)} />
)}
{log.requestId && (
<InfoRow
icon={<InfoCircle size={18} />}
label="شناسه درخواست"
value={log.requestId}
/>
)}
</div>
</div>
{/* Error Information */}
{log.errorMessage && (
<div className="mb-6">
<h3 className="text-lg font-semibold text-red-600 mb-3 flex items-center gap-2">
<Warning2 size={18} />
اطلاعات خطا
</h3>
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-red-800">{log.errorMessage}</p>
</div> </div>
{/* User Information */}
{log.user && (
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-3 flex items-center gap-2">
<User size={18} />
اطلاعات کاربر
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<InfoRow
icon={<User size={18} />}
label="نام و نام خانوادگی"
value={`${log.user.firstName} ${log.user.lastName}`}
/>
<InfoRow
icon={<InfoCircle size={18} />}
label="ایمیل"
value={log.user.email}
/>
{log.user.phone && (
<InfoRow
icon={<InfoCircle size={18} />}
label="شماره تماس"
value={log.user.phone}
/>
)}
<InfoRow
icon={<InfoCircle size={18} />}
label="شناسه کاربر"
value={log.user.id}
/>
</div>
</div>
)}
{/* Technical Details */}
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-3 flex items-center gap-2">
<Monitor size={18} />
جزئیات فنی
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{log.ipAddress && (
<InfoRow
icon={<Monitor size={18} />}
label="آدرس IP"
value={log.ipAddress}
/>
)}
{log.endpoint && (
<InfoRow
icon={<Code size={18} />}
label="Endpoint"
value={log.endpoint}
/>
)}
{log.httpMethod && (
<InfoRow
icon={<Code size={18} />}
label="متد HTTP"
value={HttpMethodLabels[log.httpMethod]}
/>
)}
<InfoRow
icon={<InfoCircle size={18} />}
label="کد وضعیت"
value={
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(log.statusCode)}`}>
{log.statusCode || "N/A"}
</span>
}
/>
{log.executionTime && (
<InfoRow
icon={<Clock size={18} />}
label="زمان اجرا"
value={formatExecutionTime(log.executionTime)}
/>
)}
{log.sessionId && (
<InfoRow
icon={<InfoCircle size={18} />}
label="شناسه جلسه"
value={log.sessionId}
/>
)}
{log.requestId && (
<InfoRow
icon={<InfoCircle size={18} />}
label="شناسه درخواست"
value={log.requestId}
/>
)}
</div>
</div>
{/* Error Information */}
{log.errorMessage && (
<div className="mb-6">
<h3 className="text-lg font-semibold text-red-600 mb-3 flex items-center gap-2">
<Warning2 size={18} />
اطلاعات خطا
</h3>
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-red-800">{log.errorMessage}</p>
</div>
</div>
)}
{/* User Agent */}
{log.userAgent && (
<InfoRow
icon={<Monitor size={18} />}
label="User Agent"
value={
<div className="text-xs font-mono bg-gray-100 p-2 rounded break-all">
{log.userAgent}
</div>
}
fullWidth
/>
)}
{/* Data Changes */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<CodeBlock title="مقادیر قبلی" content={log.oldValues} />
<CodeBlock title="مقادیر جدید" content={log.newValues} />
</div>
{/* Metadata */}
{log.metadata && Object.keys(log.metadata).length > 0 && (
<CodeBlock
title="متادیتا"
content={JSON.stringify(log.metadata, null, 2)}
/>
)}
{/* Footer */}
<div className="flex justify-end mt-8 pt-6 border-t border-gray-200">
<Button
onClick={onClose}
className="bg-blue-600 text-white hover:bg-blue-700"
>
بستن
</Button>
</div>
</>
)}
{/* No Data State */}
{!loading && !error && !log && (
<div className="text-center py-12">
<InfoCircle size={48} className="text-gray-400 mx-auto mb-4" />
<p className="text-gray-600">هیچ دادهای برای نمایش وجود ندارد</p>
</div> </div>
)} )}
{/* User Agent */}
{log.userAgent && (
<InfoRow
icon={<Monitor size={18} />}
label="User Agent"
value={
<div className="text-xs font-mono bg-gray-100 p-2 rounded break-all">
{log.userAgent}
</div>
}
fullWidth
/>
)}
{/* Data Changes */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<CodeBlock title="مقادیر قبلی" content={log.oldValues} />
<CodeBlock title="مقادیر جدید" content={log.newValues} />
</div>
{/* Metadata */}
{log.metadata && Object.keys(log.metadata).length > 0 && (
<CodeBlock
title="متادیتا"
content={JSON.stringify(log.metadata, null, 2)}
/>
)}
{/* Footer */}
<div className="flex justify-end mt-8 pt-6 border-t border-gray-200">
<Button
onClick={onClose}
className="bg-blue-600 text-white hover:bg-blue-700"
>
بستن
</Button>
</div>
</div> </div>
</DefaulModal> </DefaulModal>
); );
@@ -79,6 +79,14 @@ class AccessLogsService {
// Get specific log by ID // Get specific log by ID
async getLogById(id: string): Promise<{ accessLog: AccessLog }> { async getLogById(id: string): Promise<{ accessLog: AccessLog }> {
const response = await axios.get(`${this.baseUrl}/${id}`); const response = await axios.get(`${this.baseUrl}/${id}`);
// Handle the nested data structure
if (response.data.data && response.data.data.accessLog) {
return {
accessLog: response.data.data.accessLog,
};
}
return response.data; return response.data;
} }