larxius commited on
Commit
ba6516e
·
verified ·
1 Parent(s): e778b42

Update frontend/src/pages/LogsAndThreats.jsx

Browse files
Files changed (1) hide show
  1. frontend/src/pages/LogsAndThreats.jsx +124 -41
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">{getSortedLogs().length}</strong> of {allLogs.length} entries
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
- <div className="overflow-x-auto">
316
- <table className="w-full text-left text-[13.5px]">
317
- <thead className="bg-surface-container-high/60 text-on-surface-variant text-[11px] uppercase tracking-wider select-none border-b border-outline-variant/70">
318
- <tr>
319
- {['Timestamp', 'User', 'Action', 'Target'].map((h, i) => (
320
- <th
321
- key={h}
322
- onClick={() => handleLogSort(h)}
323
- className="p-3.5 cursor-pointer hover:bg-surface-container-highest transition-colors group"
324
- >
325
- <div className="flex items-center gap-xs font-bold">
326
- {h}
327
- <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' : ''}`}>
328
- {sortLogCol === h && sortLogDir === 'desc' ? 'arrow_downward' : 'arrow_upward'}
329
- </span>
330
- </div>
331
- </th>
332
- ))}
333
- </tr>
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
- </tbody>
353
- </table>
354
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>