Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Update frontend/src/pages/LogsAndThreats.jsx
Browse files
frontend/src/pages/LogsAndThreats.jsx
CHANGED
|
@@ -19,6 +19,10 @@ const LogsAndThreats = () => {
|
|
| 19 |
const [actionCategory, setActionCategory] = useState('all');
|
| 20 |
const [dateFilter, setDateFilter] = useState('all');
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
const [sortLogCol, setSortLogCol] = useState('Timestamp');
|
| 23 |
const [sortLogDir, setSortLogDir] = useState('desc');
|
| 24 |
|
|
@@ -69,6 +73,10 @@ const LogsAndThreats = () => {
|
|
| 69 |
return () => clearInterval(interval);
|
| 70 |
}, []);
|
| 71 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
const openFullLogsView = () => {
|
| 73 |
setIsFullLogsView(true);
|
| 74 |
fetchAllAuditLogs();
|
|
@@ -149,6 +157,15 @@ const LogsAndThreats = () => {
|
|
| 149 |
});
|
| 150 |
};
|
| 151 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
const exportLogsToCSV = () => {
|
| 153 |
if (!filteredAllLogs.length) return;
|
| 154 |
const headers = ["Timestamp", "User Email", "Action", "Target"];
|
|
@@ -301,57 +318,123 @@ const LogsAndThreats = () => {
|
|
| 301 |
</div>
|
| 302 |
|
| 303 |
<div className="text-[12.5px] text-on-surface-variant font-medium shrink-0">
|
| 304 |
-
Showing <strong className="text-on-surface font-bold">{
|
| 305 |
</div>
|
| 306 |
</div>
|
| 307 |
|
| 308 |
{/* Audit Logs Data Table */}
|
| 309 |
-
<div className="bg-surface-container-lowest border border-outline-variant/70 rounded-xl overflow-hidden shadow-2xs">
|
| 310 |
{loadingAllLogs ? (
|
| 311 |
<div className="text-center py-16 text-on-surface-variant text-[14px]">Loading full audit history...</div>
|
| 312 |
) : filteredAllLogs.length === 0 ? (
|
| 313 |
<div className="text-center py-16 text-on-surface-variant text-[14px]">No audit logs match your search filter.</div>
|
| 314 |
) : (
|
| 315 |
-
<
|
| 316 |
-
<
|
| 317 |
-
<
|
| 318 |
-
<
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
</thead>
|
| 335 |
-
<tbody className="divide-y divide-outline-variant/60">
|
| 336 |
-
{getSortedLogs().map((log, i) => (
|
| 337 |
-
<tr key={log.id || i} className="hover:bg-surface-container-lowest/80 transition-colors">
|
| 338 |
-
<td className="p-3.5 text-[12px] font-mono text-on-surface-variant whitespace-nowrap">
|
| 339 |
-
{new Date(log.timestamp).toLocaleString()}
|
| 340 |
-
</td>
|
| 341 |
-
<td className="p-3.5 font-bold text-on-surface">
|
| 342 |
-
{log.user_email || log.admin_id || 'System'}
|
| 343 |
-
</td>
|
| 344 |
-
<td className="p-3.5 text-on-surface">
|
| 345 |
-
{log.action}
|
| 346 |
-
</td>
|
| 347 |
-
<td className="p-3.5 text-[12.5px] text-on-surface-variant">
|
| 348 |
-
{log.target_name || log.target_id || '-'}
|
| 349 |
-
</td>
|
| 350 |
</tr>
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
)}
|
| 356 |
</div>
|
| 357 |
</div>
|
|
|
|
| 19 |
const [actionCategory, setActionCategory] = useState('all');
|
| 20 |
const [dateFilter, setDateFilter] = useState('all');
|
| 21 |
|
| 22 |
+
// Pagination State
|
| 23 |
+
const [currentPage, setCurrentPage] = useState(1);
|
| 24 |
+
const [pageSize, setPageSize] = useState(100);
|
| 25 |
+
|
| 26 |
const [sortLogCol, setSortLogCol] = useState('Timestamp');
|
| 27 |
const [sortLogDir, setSortLogDir] = useState('desc');
|
| 28 |
|
|
|
|
| 73 |
return () => clearInterval(interval);
|
| 74 |
}, []);
|
| 75 |
|
| 76 |
+
useEffect(() => {
|
| 77 |
+
setCurrentPage(1);
|
| 78 |
+
}, [searchQuery, actionCategory, dateFilter]);
|
| 79 |
+
|
| 80 |
const openFullLogsView = () => {
|
| 81 |
setIsFullLogsView(true);
|
| 82 |
fetchAllAuditLogs();
|
|
|
|
| 157 |
});
|
| 158 |
};
|
| 159 |
|
| 160 |
+
// Pagination calculation
|
| 161 |
+
const sortedLogs = getSortedLogs();
|
| 162 |
+
const totalEntries = sortedLogs.length;
|
| 163 |
+
const totalPages = Math.ceil(totalEntries / pageSize) || 1;
|
| 164 |
+
const validCurrentPage = Math.min(Math.max(1, currentPage), totalPages);
|
| 165 |
+
const startIndex = (validCurrentPage - 1) * pageSize;
|
| 166 |
+
const endIndex = Math.min(startIndex + pageSize, totalEntries);
|
| 167 |
+
const paginatedLogs = sortedLogs.slice(startIndex, endIndex);
|
| 168 |
+
|
| 169 |
const exportLogsToCSV = () => {
|
| 170 |
if (!filteredAllLogs.length) return;
|
| 171 |
const headers = ["Timestamp", "User Email", "Action", "Target"];
|
|
|
|
| 318 |
</div>
|
| 319 |
|
| 320 |
<div className="text-[12.5px] text-on-surface-variant font-medium shrink-0">
|
| 321 |
+
Showing <strong className="text-on-surface font-bold">{totalEntries === 0 ? 0 : startIndex + 1} - {endIndex}</strong> of {totalEntries} entries
|
| 322 |
</div>
|
| 323 |
</div>
|
| 324 |
|
| 325 |
{/* Audit Logs Data Table */}
|
| 326 |
+
<div className="bg-surface-container-lowest border border-outline-variant/70 rounded-xl overflow-hidden shadow-2xs flex flex-col">
|
| 327 |
{loadingAllLogs ? (
|
| 328 |
<div className="text-center py-16 text-on-surface-variant text-[14px]">Loading full audit history...</div>
|
| 329 |
) : filteredAllLogs.length === 0 ? (
|
| 330 |
<div className="text-center py-16 text-on-surface-variant text-[14px]">No audit logs match your search filter.</div>
|
| 331 |
) : (
|
| 332 |
+
<>
|
| 333 |
+
<div className="overflow-x-auto">
|
| 334 |
+
<table className="w-full text-left text-[13.5px]">
|
| 335 |
+
<thead className="bg-surface-container-high/60 text-on-surface-variant text-[11px] uppercase tracking-wider select-none border-b border-outline-variant/70">
|
| 336 |
+
<tr>
|
| 337 |
+
{['Timestamp', 'User', 'Action', 'Target'].map((h, i) => (
|
| 338 |
+
<th
|
| 339 |
+
key={h}
|
| 340 |
+
onClick={() => handleLogSort(h)}
|
| 341 |
+
className="p-3.5 cursor-pointer hover:bg-surface-container-highest transition-colors group"
|
| 342 |
+
>
|
| 343 |
+
<div className="flex items-center gap-xs font-bold">
|
| 344 |
+
{h}
|
| 345 |
+
<span className={`material-symbols-outlined text-[14px] opacity-0 group-hover:opacity-50 transition-opacity ${sortLogCol === h ? 'opacity-100 group-hover:opacity-100 text-primary' : ''}`}>
|
| 346 |
+
{sortLogCol === h && sortLogDir === 'desc' ? 'arrow_downward' : 'arrow_upward'}
|
| 347 |
+
</span>
|
| 348 |
+
</div>
|
| 349 |
+
</th>
|
| 350 |
+
))}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
</tr>
|
| 352 |
+
</thead>
|
| 353 |
+
<tbody className="divide-y divide-outline-variant/60">
|
| 354 |
+
{paginatedLogs.map((log, i) => (
|
| 355 |
+
<tr key={log.id || i} className="hover:bg-surface-container-lowest/80 transition-colors">
|
| 356 |
+
<td className="p-3.5 text-[12px] font-mono text-on-surface-variant whitespace-nowrap">
|
| 357 |
+
{new Date(log.timestamp).toLocaleString()}
|
| 358 |
+
</td>
|
| 359 |
+
<td className="p-3.5 font-bold text-on-surface">
|
| 360 |
+
{log.user_email || log.admin_id || 'System'}
|
| 361 |
+
</td>
|
| 362 |
+
<td className="p-3.5 text-on-surface">
|
| 363 |
+
{log.action}
|
| 364 |
+
</td>
|
| 365 |
+
<td className="p-3.5 text-[12.5px] text-on-surface-variant">
|
| 366 |
+
{log.target_name || log.target_id || '-'}
|
| 367 |
+
</td>
|
| 368 |
+
</tr>
|
| 369 |
+
))}
|
| 370 |
+
</tbody>
|
| 371 |
+
</table>
|
| 372 |
+
</div>
|
| 373 |
+
|
| 374 |
+
{/* Pagination Controls */}
|
| 375 |
+
<div className="p-4 border-t border-outline-variant/60 bg-surface-container-lowest/50 flex flex-col sm:flex-row items-center justify-between gap-3 text-[13px] text-on-surface-variant">
|
| 376 |
+
<div className="flex items-center gap-2">
|
| 377 |
+
<span>Rows per page:</span>
|
| 378 |
+
<select
|
| 379 |
+
value={pageSize}
|
| 380 |
+
onChange={(e) => {
|
| 381 |
+
setPageSize(Number(e.target.value));
|
| 382 |
+
setCurrentPage(1);
|
| 383 |
+
}}
|
| 384 |
+
className="bg-surface border border-outline-variant/60 text-on-surface rounded-md px-2 py-1 text-[12px] font-bold focus:outline-none cursor-pointer"
|
| 385 |
+
>
|
| 386 |
+
<option value={25}>25</option>
|
| 387 |
+
<option value={50}>50</option>
|
| 388 |
+
<option value={100}>100</option>
|
| 389 |
+
<option value={200}>200</option>
|
| 390 |
+
</select>
|
| 391 |
+
<span className="ml-2 font-medium">
|
| 392 |
+
{totalEntries === 0 ? '0' : `${startIndex + 1} - ${endIndex}`} of {totalEntries} records
|
| 393 |
+
</span>
|
| 394 |
+
</div>
|
| 395 |
+
|
| 396 |
+
<div className="flex items-center gap-1.5">
|
| 397 |
+
<button
|
| 398 |
+
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
| 399 |
+
disabled={validCurrentPage === 1}
|
| 400 |
+
className="px-3 py-1.5 rounded-lg border border-outline-variant/60 bg-surface hover:bg-surface-container-high text-on-surface font-bold text-[12px] disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer transition-colors"
|
| 401 |
+
>
|
| 402 |
+
Previous
|
| 403 |
+
</button>
|
| 404 |
+
|
| 405 |
+
<div className="flex items-center gap-1 px-1">
|
| 406 |
+
{Array.from({ length: totalPages }, (_, i) => i + 1)
|
| 407 |
+
.filter(p => p === 1 || p === totalPages || Math.abs(p - validCurrentPage) <= 1)
|
| 408 |
+
.map((page, idx, arr) => {
|
| 409 |
+
const prev = arr[idx - 1];
|
| 410 |
+
return (
|
| 411 |
+
<React.Fragment key={page}>
|
| 412 |
+
{prev && page - prev > 1 && <span className="px-1 text-on-surface-variant text-[12px]">...</span>}
|
| 413 |
+
<button
|
| 414 |
+
onClick={() => setCurrentPage(page)}
|
| 415 |
+
className={`w-8 h-8 rounded-lg text-[12px] font-bold transition-colors cursor-pointer ${
|
| 416 |
+
validCurrentPage === page
|
| 417 |
+
? 'bg-primary text-on-primary shadow-2xs'
|
| 418 |
+
: 'bg-surface hover:bg-surface-container-high border border-outline-variant/60 text-on-surface'
|
| 419 |
+
}`}
|
| 420 |
+
>
|
| 421 |
+
{page}
|
| 422 |
+
</button>
|
| 423 |
+
</React.Fragment>
|
| 424 |
+
);
|
| 425 |
+
})}
|
| 426 |
+
</div>
|
| 427 |
+
|
| 428 |
+
<button
|
| 429 |
+
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
|
| 430 |
+
disabled={validCurrentPage >= totalPages}
|
| 431 |
+
className="px-3 py-1.5 rounded-lg border border-outline-variant/60 bg-surface hover:bg-surface-container-high text-on-surface font-bold text-[12px] disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer transition-colors"
|
| 432 |
+
>
|
| 433 |
+
Next
|
| 434 |
+
</button>
|
| 435 |
+
</div>
|
| 436 |
+
</div>
|
| 437 |
+
</>
|
| 438 |
)}
|
| 439 |
</div>
|
| 440 |
</div>
|